Repository navigation
Merge upstream/main into main (sync fork, 67 commits) - #16
Merged
Merged
Conversation
I noticed a strange behaviour when testing the opencomputer provider with openinspect - the sandboxes would hibernate (automatically) after a few mins of inactive. After 10 minutes it seems that they get killed by openinspect, seems to be detecting that box as stale. Subsequent messages lead to a failed start Upon digging i noticed that openinspect attempts to checkpoint the (automatically) hibernated sandbox and that attempt fails, which leads to subsequent reconnects failing. In order to keep things simpler, in this change we don't set an automatic timeout when the sandbox starts instead we delegate explicit hibernation to the durable object session to perform the hibernation and waking up. On this test it seems more stable. This change removes the implicit OpenComputer timeout for normal runtime sessions. We now only pass/set a timeout when one is explicitly supplied, while leaving repo-image build timeouts unchanged. The intent is to let the Durable Object own normal session hibernation and wake behavior, which has been more stable in testing <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Sandbox actions now only apply a timeout when one is explicitly provided. * Default sandbox creation, restore, and resume flows no longer force a timeout. * Explicit timeout values are still honored when set. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Mohamed Habib <mohamedhabib@Mohameds-MacBook-Pro.local>
These documentation guides were missing in the opencomputer provider support. I have instructed an agent to generate them and verified correctness manually Also bundled in this change: I removed the arguments opencomputer_project_id abd opencomputer_target which are hullociations from the agent (they don't really exist) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support and setup guidance for using OpenComputer as a sandbox provider. * Expanded the getting-started docs with updated provider and prerequisites information. * **Bug Fixes** * Improved sandbox timeout handling so timeouts are only applied when explicitly provided. * Simplified OpenComputer setup by removing unnecessary scoping settings from configuration and deployment examples. * **Documentation** * Updated the README credits and package table formatting. * Added a new OpenComputer provider guide and refreshed production setup instructions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Mohamed Habib <mohamedhabib@Mohameds-MacBook-Pro.local>
## Summary - tighten repo-image provider boundaries around provider policy/model objects and adapter creation - keep route-level HTTP concerns at the route boundary while workflow methods operate on typed commands and domain outcomes - replace planner/workflow error result bags with typed RepoImageError exceptions that routes map to HTTP responses - expand repo-image unit and integration coverage across Modal, Vercel, and OpenComputer callback/build flows - resolve latest main conflicts and fix shared create-session schema composition so the merged tree typechecks ## Validation - npm run build -w @open-inspect/shared - npm run lint -w @open-inspect/shared - npm run typecheck -w @open-inspect/shared - npm run typecheck -w @open-inspect/control-plane - npm run lint -w @open-inspect/control-plane - npm test -w @open-inspect/control-plane -- src/repo-images/workflow.test.ts src/routes/repo-images.trigger.test.ts src/routes/repo-images.test.ts --reporter=dot - npm run test:integration -w @open-inspect/control-plane -- repo-images.test.ts --reporter=dot - npm run build -w @open-inspect/control-plane - git diff --check <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Extended repo-image workflow callback support, including returning a direct `buildId` on successful triggers and streamlined completion/failure payloads. * Added stronger, more consistent callback handling for build finalization, including authorization header support. * **Bug Fixes** * Improved request validation for build-complete/build-failed callbacks, including payload-size limits and stricter “accept/fail” eligibility checks. * More consistent error responses and status codes for unavailable providers, unconfigured providers, rejected/invalid callbacks, and build readiness/finalization failures. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Add source-level architecture comments for the repo image domain model, typed build plans, workflow, planner, store, provider policy/factory, and provider adapters. - Clarify the difference between provider-image callbacks and provider-session callbacks without changing runtime behavior. - Document that repo image artifact IDs are provider-opaque and should stay interpreted inside provider adapters. ## Validation - `git diff --check` - `npm run lint -w @open-inspect/control-plane` - `npm run build -w @open-inspect/shared` - `npm run typecheck -w @open-inspect/shared` - `npm run typecheck -w @open-inspect/control-plane` - `npm run build -w @open-inspect/control-plane` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Expanded developer-facing JSDoc documentation across repo image build planning, provider policy, adapter-provider boundaries, and callback-mode semantics. * Clarified build lifecycle behavior, including temporary sandbox usage, checkpointing/snapshotting artifacts on success, and best-effort cleanup. * Added clearer explanations of workflow sequencing and repo image store transitions, including safe handling of duplicate callbacks and racing builds. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Pass credential-helper clone auth through OpenComputer repo-image builds, matching the Vercel provider-session path while leaving normal sessions broker-based. - Preserve repo/user LLM secret precedence over provider defaults and scrub reserved repo-image callback env keys from OpenComputer build env/secrets. - Clear inherited repo-image build clone tokens from runtime forks unless the session explicitly supplies one. - Clean up allocated OpenComputer sandboxes on startup/bind failures. ## Tests - npm run typecheck -w @open-inspect/control-plane - npm test -w @open-inspect/control-plane <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * OpenComputer builds now support repository clone authentication, improving private repo setup. * Repo-image workflows now handle provider-specific clone auth more consistently across supported providers. * **Bug Fixes** * Fixed environment handling so user-provided secrets and callback values are preserved or cleared appropriately. * Improved cleanup when sandbox startup fails, preventing leftover resources. * Added safer behavior for repo-image forks, including clearer handling of clone tokens and build-mode markers. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Add OpenComputer to repo image prebuild availability and backend architecture docs - Use provider-neutral image artifact wording for Modal images, Vercel snapshots, and OpenComputer checkpoints - Update repo image debugging log references from Vercel-specific snapshot events to current provider-neutral/finalization events ## Validation - `git diff --check` - `npm exec --yes --package prettier@3.8.4 -- prettier --check docs/IMAGE_PREBUILD.md docs/HOW_IT_WORKS.md docs/DEBUGGING_PLAYBOOK.md` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Expanded “How it works” sandbox backend guidance with a clearer provider-by-provider view of repo-image builds and snapshot/restore behavior (Modal, Daytona, Vercel Sandboxes, OpenComputer). * Updated terminology to “repo image artifact” (and “checkpoint” where applicable) and clarified how prebuilt environment state enables faster subsequent sessions. * Refreshed the debugging event catalog by replacing Vercel-specific snapshot events with provider-general finalize lifecycle events and updated the listed key fields for build completion and finalize outcomes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…oleMurray#868) Slack requires action_id to be unique within an actions block and rejects the message with invalid_blocks otherwise. When the repo-clarification picker offers two or more quick-picks, it doesn't post and the user gets no response. `action_id` "select_repo_quick_pick" already exists [json-pointer:/blocks/0/elements/1/action_id] 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved repo selection handling in Slack so quick-pick buttons route correctly. * Each quick-pick button now uses a unique interaction ID, reducing collisions and making button clicks more reliable. * Repo selection options continue to behave the same for users, but interactions are now handled more consistently. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary - Render repo clarification pickers as inline `static_select` menus when the full repo list fits Slack's 100-option limit - Keep `external_select` for larger repo lists that need dynamic option loading - Pass the already-fetched repo list into the clarification block builder and cover both picker paths in tests ## Why The repo clarification dropdown became dependent on Slack's Select Menus Options Load URL after moving to `external_select`. If that URL is not configured, Slack shows `no result` and the worker never receives a `/interactions` request, so it cannot show a better runtime error. Small installs can avoid that dependency by embedding the repo options directly in the message. ## Validation - `npm test -w @open-inspect/slack-bot` - `npm run typecheck -w @open-inspect/slack-bot` - `npm run lint -w @open-inspect/slack-bot` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a smarter repository chooser in Slack clarification messages. * When the repository list is small enough, users now see inline selectable repo options; larger lists switch to search-based selection. * Updated the prompt text to match whether users should choose or search for another repository. * **Tests** * Expanded coverage for the new repo selection behavior, including inline selection and search-based fallback. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Document Slack's separate Select Menus Options Load URL for external select menus - Point the Options Load URL at the existing `/interactions` endpoint - Clarify that searchable repository pickers require this configuration ## Why Slack quick-pick buttons are rendered inline, but the searchable repo dropdown uses an `external_select`. Slack only requests those dynamic options when the app has a Select Menus Options Load URL configured, so the dropdown can show `no result` even though repos are available. ## Validation - `npx prettier --check docs/GETTING_STARTED.md` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Updated the Slack setup guide to add a required **Select Menus → Options Load URL** configuration for searchable repository pickers. * Clarified that the Options Load URL should match the same `/interactions` endpoint used for the Request URL. * Renumbered/reformatted the subsequent setup steps to reflect the added configuration. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Add an explicit `No repository` option to the homepage session target selector. - Create repo-less sessions from the web composer with `repoOwner: null` and `repoName: null`. - Hide branch selection and omit branch context for repo-less sessions. - Default to `No repository` when the user has no accessible repositories. ## Testing - `npm test -w @open-inspect/web -- src/app/(app)/page.test.tsx` - `npm run typecheck -w @open-inspect/web` - `npm run lint -w @open-inspect/web` - `npm test -w @open-inspect/web` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an explicit “start without a repository” option for session creation. * Updated the repository picker to reflect this choice and only show branch selection when a repository is selected. * **Bug Fixes** * Improved session startup when no repositories are available, including clearer guidance in the empty state. * **Tests** * Added UI flow coverage to ensure sessions can start with a “no repository” target and send the expected session creation payload. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This is an automated nightly unsafe-cast remediation. It replaces selected high-risk TypeScript assertions at OAuth/API trust boundaries with Zod `safeParse` validation, following the TypeScript Coding Standards unsafe-cast / parse-don't-assert guidance and the Zod boundary-validation pattern established in PR ColeMurray#807. | Finding | Risk | Cast removed | Fix | | --- | --- | --- | --- | | `packages/control-plane/src/auth/github.ts:48` | HIGH | `(await response.json()) as GitHubTokenResponse & { error?: string; error_description?: string }` for GitHub OAuth code exchange | Added `githubTokenResponseSchema` as the `GitHubTokenResponse` source of truth and parse through `parseGitHubTokenResponse` with preserved OAuth error handling | | `packages/control-plane/src/auth/github.ts:81` | HIGH | `(await response.json()) as GitHubTokenResponse & { error?: string; error_description?: string }` for GitHub OAuth refresh | Reused `githubTokenResponseSchema.safeParse` at the refresh boundary | | `packages/control-plane/src/auth/openai.ts:50` | HIGH | `response.json() as Promise<OpenAITokenResponse>` for OpenAI OAuth refresh | Replaced the hand-written interface with `openAITokenResponseSchema` / `z.infer` and validate the parsed response before returning | Verification: | Command | Result | | --- | --- | | `npm test -w @open-inspect/control-plane -- src/auth/github.test.ts src/auth/openai.test.ts` | Passed | | `npm run build -w @open-inspect/shared` | Passed | | `npm run build -w @open-inspect/control-plane` | Passed | | `npm run typecheck` | Passed | | `npm run lint` | Passed after temporarily moving the pre-existing untracked `.opencode/` tooling directory out of the workspace so the root lint command evaluated tracked repository files | | `npm run format` | Passed | | `npm test -w @open-inspect/control-plane` | Passed | --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/c0eee7014d656c67ad338b45f014cd85)* Co-authored-by: OpenInspect <open-inspect@noreply.github.com>
This is an automated nightly unsafe-cast remediation. It replaces three high-risk TypeScript assertions at request/internal-session/API trust boundaries with Zod `safeParse` validation, following the TypeScript Coding Standards unsafe-cast / parse-don't-assert guidance and the Zod boundary-validation pattern established in PR ColeMurray#807. | Finding | Risk | Cast removed | Fix | | --- | --- | --- | --- | | `packages/control-plane/src/routes/session-child-spawn.ts:36` | HIGH | `(await request.json()) as SpawnChildSessionRequest` for child session creation input | Added shared `spawnChildSessionRequestSchema` with `z.infer` as the type source and validate the request body with `safeParse`, preserving the existing 400 `title and prompt are required` path | | `packages/control-plane/src/routes/session-child-spawn.ts:82` | HIGH | `(await spawnContextRes.json()) as SpawnContext` for parent session state used to initialize child sessions | Added shared `spawnContextSchema` with nullable fields modeled explicitly and validate the internal response with `safeParse`, preserving the existing 500 `Failed to get parent session context` path | | `packages/github-bot/src/github-auth.ts:132` | HIGH | `(await response.json()) as { permission: string }` for GitHub collaborator permission authorization response | Added a local Zod response schema and validate with `safeParse`, preserving `{ hasPermission: false, error: true }` on malformed API responses | Verification: | Command | Result | | --- | --- | | `npm test -w @open-inspect/shared -- src/types/boundary-schemas.test.ts` | Passed | | `npm test -w @open-inspect/github-bot -- test/github-auth.test.ts` | Passed | | `npm test -w @open-inspect/control-plane -- src/router.spawn-child.test.ts` | Passed after `@open-inspect/shared` rebuild refreshed the new exports | | `npm run build -w @open-inspect/shared` | Passed | | `npm run build -w @open-inspect/control-plane` | Passed | | `npm run build -w @open-inspect/github-bot` | Passed | | `npm test -w @open-inspect/shared` | Passed | | `npm test -w @open-inspect/control-plane` | Passed | | `npm test -w @open-inspect/github-bot` | Passed | | `npm run typecheck` | Passed | | `npm run lint` | Passed after temporarily moving the pre-existing untracked `.opencode/` tooling directory out of the workspace so the root lint command evaluated tracked repository files | | `npm run format` | Passed | | `git diff --check` | Passed | References: TypeScript Coding Standards unsafe-cast / parse-don't-assert guidance; Zod boundary-validation pattern established in PR ColeMurray#807. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e71a3545b1132e0bd0e5132142bf3fc3)* Co-authored-by: OpenInspect <open-inspect@noreply.github.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
This is an automated nightly unsafe-cast remediation sweep. It replaces selected high-risk response-boundary assertions with Zod validation following the TypeScript Coding Standards unsafe-cast / parse-don't-assert guidance and the Zod boundary-validation pattern established in PR ColeMurray#807. This PR is marked DRAFT in the title/body because two exact verification gates are blocked in this workspace, as detailed below. Requested label: `automation:unsafe-cast` ## Findings Fixed | file:line | risk | cast removed | how it was fixed | | --- | --- | --- | --- | | `packages/web/src/lib/auth.ts:77` | HIGH | `(await response.json()) as GithubEmail[]` from GitHub's `/user/emails` API, feeding auth allowlist email resolution | Package-local Zod schema in `github-email-schema.ts`; validates consumed fields and accepts `visibility: null`; malformed responses fail closed with `[]` | | `packages/github-bot/src/handlers.ts:69` | HIGH | `(await response.json()) as { sessionId: string }`, feeding GitHub bot session state | Package-local Zod schema in `control-plane-responses.ts`; invalid success responses throw through the existing session creation error path | | `packages/github-bot/src/handlers.ts:91` | HIGH | `(await response.json()) as { messageId: string }`, feeding GitHub bot message state | Package-local Zod schema in `control-plane-responses.ts`; invalid success responses throw through the existing prompt delivery error path | ## Verification | command | result | | --- | --- | | `npm run build -w @open-inspect/shared` | PASS | | `npm run build -w @open-inspect/github-bot` | PASS | | `npm run build -w @open-inspect/web` | FAIL: TypeScript passes, then prerender of `/automations/new` fails with `TypeError: Cannot read properties of null (reading 'useContext')`; this page was not touched by this change | | `npm run typecheck` | PASS | | `npm run lint` | FAIL: root lint scans pre-existing untracked `.opencode/` tooling files and reports globals such as `process`, `fetch`, `Headers`; committed files are clean | | `npm run lint -- --ignore-pattern '.opencode/**'` | PASS | | `npm run lint -w @open-inspect/shared` | PASS | | `npm run lint -w @open-inspect/github-bot` | PASS | | `npm run lint -w @open-inspect/web` | PASS | | `npm test -w @open-inspect/shared` | PASS | | `npm test -w @open-inspect/github-bot` | PASS | | `npm test -w @open-inspect/web` | PASS | ## Dependency Check Added `zod@^4.4.3` to `@open-inspect/web` for a genuinely package-local auth boundary. `package-lock.json` changed only to add the corresponding `packages/web` dependency entry; no unrelated lockfile churn remains. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/662960e2a119deb77df0849505476f36)* Co-authored-by: OpenInspect <open-inspect@noreply.github.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
## Summary - remove the exact `total` count from the sessions list API - derive `hasMore` by fetching one extra session row - update archived-session pagination to use the returned `hasMore` flag ## Why The frontend only consumes `sessions` and `hasMore`, so the D1 `COUNT(*)` query was doing unnecessary work on a hot session-list path. Removing it avoids the growing table scan for `status != archived` requests. ## Validation - `npm test -w @open-inspect/control-plane -- session-index` - `npm test -w @open-inspect/web -- session-list control-plane-query data-controls-settings session-sidebar` - `npm run test:integration -w @open-inspect/control-plane -- d1-session-index auth` - `npm run typecheck` - `npm run lint -w @open-inspect/control-plane` - `npm run lint -w @open-inspect/web` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Session lists now use server-provided pagination info, making “Load more” behavior more reliable. * Archived sessions now load more accurately without relying on page size checks. * **Improvements** * Session list responses no longer include a total count and now return a clearer `hasMore` indicator. * Pagination handling was updated across the app to match the new session list response format. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…y#854) ## Problem `scripts/d1-migrate.sh` tracks applied migrations by their **numeric prefix** — `VERSION=$(echo "$FILENAME" | grep -oE '^[0-9]+')`, stored as the `_schema_migrations.version` primary key, and the skip check is `grep -qxF "$VERSION"`. If two migration files ever share a prefix, only one is recorded and the other is **silently skipped — permanently**. A re-run never recovers it, because the number is already marked applied. The result is schema drift that doesn't surface at migrate time; it shows up later as a confusing runtime error (e.g. `table X has no column named Y`). This is easy to hit without anyone doing anything wrong: two PRs in flight each grab the next sequential number (`00NN_...`), both pass CI independently, and after both merge the directory has two `00NN_*.sql` files. One migration is then dropped on the next deploy. ## Fix Detect colliding numeric prefixes at the start of the run and fail with a clear, actionable message instead of silently skipping: ``` ERROR: duplicate migration version prefixes detected: 0022 Renumber the colliding files so each prefix is unique before deploying. ``` No behavior change when prefixes are unique. One-file change, no new dependencies (`sort | uniq -d` over the basenames' prefixes). ## Why a guard rather than reworking the tracking Keying on the full filename instead of the prefix would also avoid the silent skip, but it changes the `_schema_migrations` PK semantics and the INSERT path for existing deployments. The guard is the minimal, behavior-preserving fix; it just turns a silent data-loss case into a loud, self-explaining failure. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Added a pre-flight validation for migration files to stop the migration process when two SQL files share the same version prefix. * Migrations now fail fast with clear error output when duplicate migration numbers are detected (and require filenames with numeric prefixes). <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This is an automated nightly unsafe-cast remediation sweep. It replaces selected scheduler boundary assertions with Zod parsing so untrusted internal request bodies are validated before they drive automation matching, run creation, or run completion state. This follows the TypeScript Coding Standards guidance for unsafe casts / parse-don't-assert and the Zod boundary-validation pattern established in PR ColeMurray#807. | Finding | Risk | Cast removed | Fix | | --- | --- | --- | --- | | `packages/control-plane/src/scheduler/durable-object.ts:427` | HIGH | `(await request.json()) as AutomationEvent` for scheduler automation events feeding matching, dedup, and run creation | Shared `automationEventSchema` Zod discriminated union in `@open-inspect/shared`, parsed with `safeParse` and 400 on invalid payload | | `packages/control-plane/src/scheduler/durable-object.ts:598` | HIGH | `(await request.json()) as { automationId: string }` for manual trigger run creation | Package-local Zod `manualTriggerBodySchema`, parsed with `safeParse` and existing 400 `automationId required` response | | `packages/control-plane/src/scheduler/durable-object.ts:686` | HIGH | `(await request.json()) as { automationId; runId; sessionId; messageId?; success; error? }` for run-complete state transitions | Package-local Zod `runCompleteBodySchema`, parsed with `safeParse` and 400 on invalid callback | Verification commands run in a clean worktree for this branch: | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run build -w @open-inspect/control-plane` | Passed | | `npm run typecheck` | Passed | | `npm run lint` | Passed | | `npm run format` | Passed | | `npm test -w @open-inspect/shared` | Passed, 22 files / 275 tests | | `npm test -w @open-inspect/control-plane` | Passed, 89 files / 1415 tests | Note: in the original workspace, `npm run lint` was also attempted and failed only because of a pre-existing untracked `.opencode/` directory outside this PR; the exact command passed in the clean worktree containing only committed branch files. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e7ba177d5123895cea9e49c69689c0e1)* --------- Co-authored-by: OpenInspect <open-inspect@noreply.github.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
This is an automated nightly unsafe-cast remediation sweep. It replaces selected high-risk TypeScript boundary assertions with parse-don't-assert validation, following the TypeScript Coding Standards for unsafe casts and the Zod boundary-validation pattern established in PR ColeMurray#807. | Finding | Risk | Cast removed | Fix | | --- | --- | --- | --- | | `packages/web/src/hooks/use-session-socket.ts:139` | HIGH | WebSocket message `raw as WsMessage` after only checking for a `type` key | Added shared `serverMessageSchema` in `@open-inspect/shared` and now `safeParse`s WebSocket messages before handling them. | | `packages/web/src/app/api/sessions/[id]/title/route.ts:27` | HIGH | `await request.json() as { title?: string }` on an API request body | Added a minimal inline parser for the request body that preserves the existing 400 invalid-body path. | ## Verification | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `env -u NODE_ENV npm run build -w @open-inspect/web` | Passed; the plain inherited shell environment had a non-standard `NODE_ENV` that caused Next prerendering to fail before this retry. | | `npm run typecheck` | Passed | | `npm run lint` | Passed after temporarily moving the untracked local `.opencode/` tool directory aside and restoring it; the tracked repo files lint clean. | | `npm run format` | Passed | | `npm test -w @open-inspect/shared` | Passed: 21 files, 274 tests | | `npm test -w @open-inspect/web` | Passed: 48 files, 513 tests | --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/bc3972ace378acf23238764498c05c1f)* Co-authored-by: OpenInspect <open-inspect@noreply.github.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
## Summary Linear agent-session auth failures now preserve the concrete reason they failed instead of collapsing every OAuth problem into a bare missing-client return. When a workspace token is missing, malformed, expired without a refresh token, rejected by Linear during refresh, or blocked by a KV/read failure, the token loader raises a `LinearAuthError` with the failure reason and any available OAuth response details. Agent-session webhooks catch that error, log `agent_session.no_oauth_token` with the org, issue, trace ID, mode, and auth failure reason, then stop before creating or prompting an Open-Inspect session. The final behavior is intentionally log-only for these agent-session paths. Earlier fallback issue-comment handling was removed after review because a failed OAuth refresh usually means operators should inspect the structured logs rather than trying to notify Linear through the static API-key fallback. ## Validation - npm run lint - npm run typecheck - npm test ## Post-Deploy Monitoring & Validation - Watch Linear bot logs for `agent_session.no_oauth_token`, `oauth.refresh_failed`, `oauth.refresh_error`, and `oauth.token_read_error`. - Healthy signal: auth failures include `auth_failure_reason`, issue context, and trace ID, with no control-plane session or prompt created after the auth failure. - Failure signal: repeated `token_read_error` / `refresh_error` spikes, missing issue context in `agent_session.no_oauth_token`, or unexpected Linear agent-session requests continuing after auth failure. - Validation window: first deploy after merge; owner: operator handling the Linear bot deploy. Related: ColeMurray#863 --- [](https://github.com/EveryInc/compound-engineering-plugin)  <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved handling of expired or invalid Linear OAuth tokens, reducing failed agent session starts and follow-ups. * When authentication can’t be restored, the system now skips the action safely and records clearer failure details. * Refresh failures are now classified more consistently, helping diagnose issues like invalid grants and token read errors. * **Tests** * Added coverage for token expiry, refresh success/failure, and agent session flows. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Support a custom domain for the Cloudflare web Worker Adds the ability to serve the web app on a custom domain (e.g. `app.example.com`) instead of the default `*.workers.dev` URL when `web_platform = "cloudflare"`. There was an existing `cloudflare_zone_id` terraform variable, but it was unused. ### Changes - **`variables.tf`** — new optional `cloudflare_custom_domain` variable (the hostname to attach), alongside the existing `cloudflare_zone_id`. Defaults to `null`. - **`web-cloudflare.tf`** — new `cloudflare_workers_custom_domain.web_app` resource that attaches the hostname to the web Worker. Cloudflare provisions the DNS record and edge certificate. Gated on `local.web_custom_domain_enabled` and depends on the Worker deploy. - **`locals.tf`** — added `web_custom_domain_enabled` flag and a third case to `web_app_url`. When a custom domain is set, the app URL (used for `NEXTAUTH_URL` and cross-service config) becomes `https://<custom-domain>` instead of the `workers.dev` URL. - **`terraform.tfvars.example`** — documented the new `cloudflare_custom_domain` input under the Cloudflare zone section. ### Behavior The custom domain only activates when **all** of the following are true: - `web_platform = "cloudflare"` - `cloudflare_zone_id` is set - `cloudflare_custom_domain` is non-empty Otherwise the deployment falls back to the existing `workers.dev` URL — no change for existing deployments. ### Notes - Requires a Cloudflare zone you control (set `cloudflare_zone_id`). - The Cloudflare API token needs **Workers Routes: Edit** to manage the custom domain. - `terraform validate` passes cleanly (dropped the deprecated `environment` attribute on the resource). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added optional Cloudflare custom-domain support for production web workers, including automatic URL switching to the custom hostname when configured. * **Safety Checks** * Added validation to prevent misconfigured custom-domain/zone settings; falls back to the default workers subdomain when required inputs are missing. * **Documentation** * Expanded production Cloudflare setup guidance and examples for the custom-domain option. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…oleMurray#882) Follow-ups to ColeMurray#747 (custom domain support for the Cloudflare web Worker). ## Bug: `terraform plan` fails for every Cloudflare-platform deployment without a custom domain Terraform's `coalesce()` errors when **all** arguments are null or empty strings — `coalesce(null, "")` is a hard error, not `""`: ``` Call to function "coalesce" failed: no non-null, non-empty-string arguments. ``` The `web_custom_domain_enabled` local from ColeMurray#747 evaluates `trimspace(coalesce(var.cloudflare_custom_domain, ""))` whenever `web_platform == "cloudflare"` (`&&` short-circuits, so Vercel deployments escape). Both `cloudflare_custom_domain` and `cloudflare_zone_id` default to `null`, so any existing `web_platform = "cloudflare"` deployment that hasn't opted into a custom domain now fails at plan time. Setting `cloudflare_custom_domain = ""` explicitly errors the same way. This slipped past `terraform validate` because validate treats variables as unknown values — the error only fires on plan/apply with concrete values. **Fix**: null-safe normalization locals (`var.x == null ? "" : trimspace(var.x)`) consumed by the gate, the host, and the `cloudflare_workers_custom_domain` resource. This also fixes the inconsistency where the gate trimmed the domain but the resource/URL consumed the raw value. ## Hard validation instead of an advisory warning The `check` block from ColeMurray#747 had the same `coalesce` problem (it would report an evaluation error rather than its intended message), and as a warning it let a misconfigured domain silently fall back to workers.dev — leaving `NEXTAUTH_URL` and OAuth callbacks pointing somewhere the operator didn't intend. Replaced with two hard failures: - a `validation` block on `cloudflare_custom_domain` for hostname shape: must be a bare hostname — rejects scheme, port, path, trailing dot, whitespace, wildcards - a `terraform_data.cloudflare_custom_domain_gate` precondition in `checks.tf` for the cross-input policy (same pattern as the existing `access_control_gate`), expressed against the normalized locals as `local.web_custom_domain == "" || local.web_custom_domain_enabled`: a set domain requires `web_platform = "cloudflare"` and a non-empty `cloudflare_zone_id` (also catches the previously-silent domain-set-on-Vercel case) `null`, `""`, and whitespace-only all still mean "not configured". ## Single canonical origin Attaching a custom domain doesn't disable the workers.dev route, so the app was reachable on two origins with `NEXTAUTH_URL` pointing at only one. The generated `wrangler.production.toml` now sets `workers_dev = false` when the custom domain is enabled (`true` otherwise — existing behavior, now explicit). > **Note for deployers already running ColeMurray#747 with a custom domain**: after the next apply, the workers.dev URL for the web app stops serving; the custom domain is the single origin. ## Docs - `GETTING_STARTED.md`: custom-domain subsection under Step 8 (vars, token permission, OAuth-callback reminder), custom-domain entries in the callback-URL list and redirect-URI troubleshooting, commented vars in the tfvars sample. - `terraform.tfvars.example`: notes that workers.dev is disabled and GitHub/Google OAuth callback URLs must be updated to the new hostname. ## Verification `terraform fmt -check` and `terraform validate` pass. Validation and locals exercised end-to-end with `terraform plan` on a minimal harness under Terraform 1.14.6 (the repo's required floor): | Scenario | Result | | --- | --- | | all unset (defaults) | plans cleanly, `enabled = false` — previously the coalesce error | | domain set, `web_platform = "vercel"` | hard error (precondition gate) | | domain set, zone id unset | hard error (precondition gate) | | domain + zone, `web_platform = "cloudflare"` | `enabled = true`, host = custom domain | | `https://app.example.com` | hard error (bare-hostname rule) | | domain `""` | plans cleanly, `enabled = false` | <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for serving the web app on a Cloudflare custom domain. * Expanded setup docs with Cloudflare-specific DNS, certificate, and OAuth callback guidance. * **Bug Fixes** * Improved handling of custom-domain settings by normalizing empty values and whitespace. * Tightened validation so custom domains must be valid hostnames and require the related Cloudflare zone setting when used. * Updated production routing to disable the default `workers.dev` route when a custom domain is enabled. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Take over ColeMurray#734 and resolve conflicts with current `main`, preserving archived-project filtering. - Use GitLab `namespace.full_path` for repository owner mapping so nested groups round-trip correctly. - Preserve nested group separators in manual MR URLs while still encoding individual path segments. ## Test plan - `npm test -w @open-inspect/control-plane -- gitlab-provider.test.ts` - `npm run typecheck -w @open-inspect/control-plane` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/8b33d071aa6249421aeed1640d5ca6f7)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed repository ownership and listing for projects inside nested GitLab groups. * Improved manually generated merge request links so nested group paths open correctly. * Preserved archived repository filtering while updating project path handling. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Mikhail Dubov <mikhail@chattermill.io>
## Summary - retain the secret store for the active OpenComputer repo image - carry provider secret-store ids through repo-image replacement cleanup - delete OpenComputer secret stores only for failed builds or superseded images ## Verification - npm test -w @open-inspect/control-plane -- repo-images opencomputer-adapter - npm run typecheck -w @open-inspect/control-plane <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Sandbox cleanup now also removes the associated OpenComputer secret store when a provider session sandbox is deleted. * Deleting a sandbox now supports an optional setting to include secret-store removal in the request. * Existing behavior for missing or already-deleted sandboxes remains unchanged. * **Tests** * Updated and added unit tests to verify secret-store deletion options are forwarded correctly. * **Documentation** * Public sandbox options type is now re-exported for easier use. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Mohamed Habib <mohamedhabib@Mohameds-MacBook-Pro.local> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
## Summary - allow sandbox `step_finish.tokens` to be either the legacy numeric count or a structured usage object - add shared schema coverage for OpenCode-style token usage details - add a workerd sandbox WebSocket integration test for the invalid-message regression ## Root Cause The sandbox bridge forwards OpenCode `step-finish` parts verbatim. OpenCode can send `tokens` as an object with usage details such as `total`, `input`, `output`, `reasoning`, and `cache`, but the control-plane boundary schema only accepted a number, so the Durable Object rejected the entire sandbox message. ## Validation - `npm run build -w @open-inspect/shared` - `npm run typecheck -w @open-inspect/shared` - `npm test -w @open-inspect/shared -- src/types/boundary-schemas.test.ts` - `npm run test:integration -w @open-inspect/control-plane -- test/integration/websocket-sandbox.test.ts` - `npx prettier --check packages/shared/src/types/index.ts packages/shared/src/types/boundary-schemas.test.ts packages/control-plane/test/integration/websocket-sandbox.test.ts` ## Known Existing Issue - `npm run typecheck -w @open-inspect/control-plane` still fails on an unrelated scheduler webhook typing issue at `src/scheduler/durable-object.ts(522,49)`: webhook automation `body` is optional where `WebhookAutomationEvent` requires it. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Sandbox step completion events now support richer token usage details, including total, input, output, reasoning, and cache counts. * Token usage can still be provided as a simple total value for compatibility. * **Bug Fixes** * Improved validation and handling of sandbox event token data so structured usage is accepted and preserved consistently. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - add `zai-coding-plan/glm-5.2` as an opt-in model category - document `ZHIPU_API_KEY` setup through Open-Inspect web app secrets - update model utility tests for the new provider group ## Notes - no Terraform, Modal secret, or control-plane provider-key plumbing was added; customers configure `ZHIPU_API_KEY` via Settings > Secrets - verified the OpenCode catalog exposes provider `zai-coding-plan` with model `glm-5.2` and env key `ZHIPU_API_KEY` ## Tests - `npm test -w @open-inspect/shared` - `npm run typecheck -w @open-inspect/shared` - `npm run lint -w @open-inspect/shared` - `npx prettier --check README.md docs/AVAILABLE_MODELS.md docs/GETTING_STARTED.md docs/HOW_IT_WORKS.md docs/OPENCOMPUTER_PROVIDER.md docs/SECRETS.md packages/shared/src/models.ts packages/shared/src/models.test.ts` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for the **Z.AI Coding Plan** model in model selection and validation. * The new provider appears in the available models list and related model groupings. * **Documentation** * Updated setup and troubleshooting docs to explain when provider-specific secrets are needed. * Added guidance for enabling Z.AI Coding Plan and its required API key. * Refreshed model availability tables and secret examples across the docs. * **Tests** * Expanded model coverage to include the new provider and its default-enable behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…ray#886) Supersedes ColeMurray#850. Huge thanks to @rubenlangeweg for ColeMurray#850 — the product design there (and its decision doc) is what ships here. This PR keeps that design and rebuilds the execution/storage layer on a single unified pipeline so the multi-repo and single-repo paths cannot drift apart. Design record: [`docs/MULTI_REPO_AUTOMATIONS.md`](docs/MULTI_REPO_AUTOMATIONS.md); full review findings that motivated the rebuild are in the repo's review notes for ColeMurray#850. ## What this does An automation now selects **0–10 repositories** (`repositories: [{repoOwner, repoName, baseBranch?}]`). Every firing records one **invocation**; a non-skipped invocation fans out into one **run per repository**, each an ordinary session with its own branch/PR flow. Multi-repo selections are schedule/manual-only in v1 (product scope cut — event fan-out semantics are undefined, not technically blocked). ``` automation ── repositories (live selection, editable any time) └── invocation (one per firing; skips are childless invocations) └── runs (one per repo, firing-time repo snapshot) ── session ``` - New history endpoint `GET /automations/:id/invocations`; the web UI renders one row per firing (fan-outs expand into repository rows; UI copy keeps "Run History" per the API↔UI noun split). - Invocation status is **derived from children, never stored** (`starting/running/completed/failed/partial_failed/skipped`) — the stored-aggregate wedge class from ColeMurray#850's review is unrepresentable. - Runs snapshot `repo_owner/repo_name/repo_id/base_branch` at firing time, so **repository edits are always allowed** (no cardinality freeze, no edit-while-active guard): history and in-flight children never depend on the live selection. - Auto-pause: an invocation with ≥1 failed child counts **one** strike, stamped via a `failure_counted_at` CAS (exactly-once across callbacks, launch failures, and recovery sweeps); only fully-completed invocations reset the counter; childless skips count neither way. A bounded finalization sweep closes crash windows. - Scheduler: one `startInvocation` path for tick/manual/event; per-source overlap (whole-automation for schedule/manual, per-concurrency-key for events); atomic skip+advance batches so cron slots can't wedge; a per-tick child-launch budget (~50) keeps 25×10-repo ticks under the Workers subrequest cap. ## Kept from ColeMurray#850 vs. rebuilt | Kept | Rebuilt | | --- | --- | | Product model: fan-out, cap 10, one session/PR per repo, shared instructions | Pipeline: one `startInvocation` path, no single-repo/multi-repo fork | | Decision-doc Q&A (overlap, auto-pause counting partial failures, skips don't count, manual-while-paused, duplicate rejection) | Storage: thin `automation_invocations` + normalized `automation_repositories`; status derived, not stored | | Failure-count CAS idea (exactly-once strike) | State machine: guarded child updates, born-terminal finalization, recovery/finalization sweeps | | Sessions linkage (`automation_run_id`) | Migrations: additive `0030` (two idempotent backfills) + `0031` (drops the legacy columns) instead of the 0030–0033 sequence | | Web form multi-select UX | History API: single unified invocations endpoint, instead of a parallel groups shape | Naming follows the review outcome: `automation / repository / invocation / run / session`. ## Deploy & rollback Standard pipeline order (Terraform applies D1 migrations before the worker). `0030` adds the repositories/invocations tables and backfills them (replay-safe — rerunning the file is the recovery for a partial apply); `0031` then drops the deprecated `automations.repo_*` mirror (a `PRAGMA defer_foreign_keys` table rebuild, since those columns carry CHECK constraints from 0029) and the frozen `automation_runs.trigger_key/concurrency_key/trigger_run_metadata` columns. They apply in order, so an instance upgrading from before this feature migrates its data (0030) before the drop (0031) in the same deploy. **Not code-revertible without a database restore.** `0031` drops columns the previous worker reads (`automations.repo_*`), so reverting the code alone leaves it querying columns that no longer exist. Snapshot D1 before applying if you want a rollback path. This is deliberate — see "Cleanup folded in" below. **Migration reconciliation for ColeMurray#850 deployments**: any D1 that applied ColeMurray#850's branch migrations (its 0030–0033: run groups table, `target_*` columns, repair/rebuild steps) has schema objects and `_schema_migrations` markers this chain doesn't know. Reconcile manually before applying this PR's `0030`/`0031` — restore a pre-ColeMurray#850 state, or drop ColeMurray#850's objects/markers first. ## Cleanup folded into this PR (no deprecation window) The single-release compatibility window the plan called for was **folded into this PR** rather than shipped as a follow-up. Rationale: this is a self-hosted, internal-only system that deploys control-plane + web together, and prod never runs this PR in isolation before the cleanup — so the compat shims' only beneficiaries (a stale browser tab during the brief deploy skew; a code-revert rollback) are ones we chose not to spend a release protecting. Stale tabs recover on refresh (a scalar-only edit from an old bundle now lands as "repositories unchanged", so it can't clobber the selection); rollback is a D1 restore (above). Removed here instead of one release later: 1. `GET /automations/:id/runs` list alias (+ its web proxy) — the UI uses `/invocations`. 2. `Automation.repoOwner/repoName/repoId/baseBranch` response mirrors of `repositories[0]`. 3. Scalar-trio request sugar (`repoOwner/repoName/baseBranch` on create/update). 4. `run` field in the `POST …/trigger` response. 5. The `automations.repo_*` dual-write + scalar mirror, and the frozen `automation_runs.trigger_key/concurrency_key/trigger_run_metadata` columns with their indexes (`idx_runs_trigger_key`, `idx_runs_concurrency`, `idx_runs_thread_continuity`, `idx_automations_repo`, `idx_automations_event_match`). Firing keys now live only on the invocation; overlap / dedup / Slack thread-continuity reach them via a join to runs. ### Note on CHANGELOG The plan called for a `CHANGELOG.md` entry, but the repo has no tracked changelog; release notes live in this description and `docs/MULTI_REPO_AUTOMATIONS.md` instead. ## Behavioral notes (intentional deltas from main) - Event-source overlap skips are now recorded for **all** event sources as childless skipped invocations (previously only Slack skips were recorded) — uniform history. - Skip invocations never carry the event `trigger_key`, so a skip can never consume the dedup slot of the real delivery. - `POST …/trigger` returns `201 {invocationId, runs}` and `500` only when every child failed to launch. ## Validation Full matrix, green on the branch tip: - `npm run build` (shared first), `npm run typecheck`, `npm run lint` — clean - Unit: shared 302, control-plane 1517, web 542, github-bot 110, slack-bot 170, linear-bot 125 - Integration (workerd + real D1): 439 — the `0030` invocation-link backfill, the derived-status truth table (incl. childless skips), the F1/F2/F3/F7 regression races (concurrent last-child completes, crash-before-finalize sweeps, mid-fan-out launch failure, manual-vs-cron guarded insert), the overlap/auto-pause matrix, per-key event concurrency + dedup, and a mid-flight repo-set edit - Python: modal-infra 179, sandbox-runtime 364 — green Commits are incremental by layer (contracts → schema/store → routes → scheduler → web → docs → deprecation cleanup); migration `0031` applies cleanly under the integration harness's real D1. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added multi-repository automation support (up to 10) with per-invocation history and a new invocation list API/page. * Automation UI now shows repository lists, repository counts, and per-repository session details; run history is invocation-based. * Updated the automation form to submit a canonical `repositories` selection (including repo-less support where allowed). * **Bug Fixes** * Improved reporting for fan-out statuses, including partial failures, and refined auto-pause consecutive-failure behavior. * Hardened concurrency/overlap handling and trigger “active invocation” semantics. * Fixed no-repository sandbox/snapshot environment scoping. * **Documentation** * Updated automations and multi-repo execution documentation and limits. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…oleMurray#888) ## Summary Session detail pages can hang on "loading session" and never render. ## Root cause `sandboxEventSchema` — the `SandboxEvent` discriminated union in `packages/shared/src/types/index.ts` — does not include the `ready` event type. The sandbox bridge emits a `ready` event on every connect (`packages/sandbox-runtime/src/sandbox_runtime/bridge.py`), so `ready` is present in essentially every session's replay history. The client validates the `subscribed` hydration message with `serverMessageSchema.safeParse(...)` and silently drops the message when validation fails (`packages/web/src/hooks/use-session-socket.ts`). Because `replay.events` was `z.array(sandboxEventSchema)`, a single unrecognized `ready` event failed the entire message — so the client never received the session state and the page stayed on "loading session." Any session whose history contains a `ready` event is affected. ## Fix (`packages/shared/src/types/index.ts`) 1. Add the `ready` branch to `sandboxEventSchema` (`{ type, sandboxId, timestamp, opencodeSessionId }`). 2. Validate replay events individually and drop unrecognized ones instead of failing the whole `subscribed` message, so a single unknown or legacy event can never wedge session hydration. ## Verification - Added regression tests: a `ready` event parses; the `subscribed` replay keeps recognized events and drops unknown ones without failing. - `@open-inspect/shared` test suite passes (304 tests); `typecheck` is clean for `shared`, `web`, and `control-plane`. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for a new `"ready"` sandbox event. * Replay and history results now handle mixed event/item data more gracefully. * **Bug Fixes** * Parsing no longer fails when replay or history payloads include unknown entries. * Valid events and items are preserved while unsupported ones are skipped automatically. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Adds a bounded local worker pool for automation child-session launches with concurrency 4. - Keeps per-child failure handling inside each launch task and counts launched children from final running statuses. - Adds a regression test proving a later child init begins while an earlier child is still pending. ## Concurrency Notes - `createSessionForAutomationRun` is per-child: each call generates its own `sessionId`, uses the run repo snapshot, and initializes its own session. - `sendPromptToSession` enqueues work per session, so prompt delivery remains isolated. - `AutomationStore.updateRun` prepares SQL and params per call; concurrent updates target distinct run ids and do not reuse a shared mutable statement buffer. - Each child object is only mutated by its own launch task; the pool only shares a cursor. ## Verification - `npm test -w @open-inspect/control-plane` - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint:fix` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added bounded concurrency for child automation run launches (capped at 4 per start invocation) to smooth multi-repo fan-out spikes. * **Bug Fixes** * Updated scheduler orchestration so later child sessions begin initialization while earlier ones are still blocked, improving parallel startup behavior and reliability. * **Tests** * Added a unit test covering scheduler launch concurrency and confirmed both init requests and run updates occur as expected under blocked initialization. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Updates the fork clone command in `CONTRIBUTING.md` to use `background-agents.git`. - Fixes the broken `open-inspect.git` repository path reported in ColeMurray#890. ## Testing - Not run; documentation-only change. Closes ColeMurray#890 --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/c0fe2862611b9d09cf3331cfec519eb6)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Updated the getting started instructions to use the current repository URL for cloning the project. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Add `HttpError` and map it once in the central router dispatch catch while preserving fall-through logging and headers. - Convert repo resolution helpers to return success types and throw on failures, including ordered `Promise.allSettled` handling for automation repository selection. - Remove the seven direct resolver `instanceof Response` checks and add router coverage for 404 body plus CORS/trace headers. ## Validation - `npm run build -w @open-inspect/shared` - `npm run typecheck -w @open-inspect/control-plane` - `npm test -w @open-inspect/control-plane` - `npm run lint:fix -w @open-inspect/control-plane` - `npm run lint:fix` fails because root ESLint descends into `worktrees/multi-repo-automations/` and reports 139 unrelated pre-existing errors there. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Repository resolution failures now map to the correct HTTP status and JSON error message (including not-found), with CORS and tracing/request-id headers preserved. * Session, automation, and secret endpoints no longer stop prematurely on repository lookup results and now handle failures consistently. * Automation repository selection now runs lookups concurrently while keeping input-order behavior for determining failures. * **Tests** * Added coverage for error mapping behavior, ensuring expected status, message, and headers are returned and that downstream fetch is not called. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Remove the unused Modal warm-sandbox HTTP endpoint and matching control-plane client method/types. - Remove the stale repo-keyed warm-pool implementation and provider `supportsWarm` capability. - Update docs/comments that still described warm pools, while preserving typing-triggered sandbox start behavior. ## Behavior Typing-start warming still works through the existing WebSocket `typing` message, `PresenceService.handleTyping()`, and lifecycle `warmSandbox()` path. That path uses the normal session spawn flow, including repo image lookup when available. ## Validation - `npm test -w @open-inspect/control-plane -- src/sandbox/providers/modal-provider.test.ts src/sandbox/providers/vercel/provider.test.ts src/sandbox/providers/daytona-provider.test.ts src/sandbox/providers/opencomputer-provider.test.ts src/sandbox/lifecycle/manager.test.ts src/session/presence-service.test.ts src/session/http/handlers/session-lifecycle.handler.test.ts` - `cd packages/modal-infra && python -m pytest tests/ -q` - `npm run typecheck` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Removed the pre-warm sandbox flow from sandbox APIs and provider capabilities, so supported features now reflect snapshot/restore-based behavior more accurately. * Updated sandbox lifecycle and scheduler notes to match the current behavior. * **Documentation** * Revised sandbox guidance and architecture docs to remove references to warm pools and the removed endpoint. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## What
PR-7 of the multi-repo sessions plan (§PR-7): the web read-only surface
for multi-repo sessions. Renders the member list, per-repo PR chips,
non-fatal boot warnings, and `+N` session cards. No picker changes
(that's Phase 2 / PR-12) — this is the sole owner of the v1 session UI
and is a hard prerequisite of PR-12, so a customer can't create a
multi-repo session whose page shows one repo.
## Changes
**`use-session-socket.ts` — repo-aware `session_branch`**
- `state.repositories` already flows through the `subscribed` handler
(wholesale state spread); no change needed there.
- New `applySessionBranchUpdate` helper: a repo-scoped `session_branch`
(message carries optional `repoOwner`/`repoName`, added in PR-3) updates
the matching member's `branchName` in `state.repositories`, and mirrors
to the scalar `branchName` **only when it names the primary** (position
0). An absent identity names the session's sole repo (the primary) and
mirrors too. Sessions without a hydrated member list stay scalar-only,
exactly as before. A branch update for an unknown member leaves state
untouched rather than clobbering the scalar.
- `toUiArtifact` now carries `repoOwner`/`repoName` through PR-artifact
metadata (needed for per-repo chip matching).
**`metadata-section.tsx` — member list, per-repo PR chips, warnings**
- Multi-member sessions (`repositories.length > 1`) render a
"Repositories" block: each member links to its repo, the primary is
tagged, and each shows its own working branch + PR chip (`#number` +
state badge). PR chips are matched by artifact metadata repo;
identity-less artifacts (pre-multi-repo) attribute to the primary —
mirrors the control-plane `findPrArtifactForRepo`.
- Single-repo / scalar-era sessions render **exactly as before** (the
scalar PR badge / base branch / working branch / repo tag are untouched,
just gated behind `!isMultiRepo`).
- Non-fatal `warning` sandbox events
(`sync`/`setup`/`start`/`assembly`/`secrets` scopes) are now surfaced in
the sidebar with repo attribution — previously defined in the shared
schema but rendered nowhere.
**Session cards (`session-sidebar.tsx`)**
- New `formatSessionRepositoriesLabel` shows the primary repo with a
`+N` suffix (`acme/web +1`) reading the hydrated `Session.repositories`.
Scalar-only sessions render exactly as today. Kept the existing
`formatRepositoriesLabel` (automation cards) unchanged rather than
overloading it.
**Contracts (shared + control-plane)**
- Added `SessionListRepository` to shared and threaded `repositories?`
onto the web-facing `Session` type. The `/sessions` list route already
serializes this (PR-4 hydration), so the card is web-only.
- Re-pointed control-plane's `SessionIndexRepository` to alias
`SessionListRepository` so the wire shape has a single home (no behavior
change).
## Gating
No feature flag. The multi-repo UI is gated by data presence
(`repositories.length > 1` / warning events present), and `repositories`
is only populated for multi-repo sessions, which can't be created until
the PR-12 picker ships. Single-repo/scalar sessions are byte-identical
to today. This avoids the flag-removal coupling the plan flags ("removal
of the flag is not [optional] — PR-12 requires it live").
## Tests
- `repo-label.test.ts` (new): `formatSessionRepositoriesLabel` — scalar,
single-member, `+N`, hydrated-primary-over-scalar.
- `metadata-section.test.tsx`: per-repo member list + chips matched by
artifact metadata, identity-less→primary attribution, warning rendering.
(Added `afterEach(cleanup)` — this suite renders into a shared body
without auto-cleanup.)
- `use-session-socket.test.tsx`: repo-scoped `session_branch` routes to
the matching member and mirrors the scalar only for the primary.
- Full suite: 551 web + 331 shared pass; typecheck + lint clean across
packages.
## Notes for reviewers
- Deviation from the plan's literal wording: the plan said "`+N` via
`formatRepositoriesLabel`", but that helper (shared with automation
cards) renders "N repositories". To get the plan's stated "primary +N"
UX without regressing automations, I added a session-specific
`formatSessionRepositoriesLabel` rather than overloading the existing
one.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added multi-repository session support across the sidebar and
metadata: per-repository PR/branch/primary-member details and improved
repository labels (including “+N” for additional members).
* Surfaced non-fatal session warnings in the session details, with
optional repository attribution.
* **Bug Fixes**
* Fixed multi-repo `session_branch` updates to apply to the correct
repository member without overwriting unrelated members.
* Ensured PR artifact attribution follows the correct repository,
falling back to the primary member when repo identity is missing.
* **Tests**
* Expanded coverage for multi-repo session routing, metadata rendering
(including warnings), and repository label formatting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…ray#913) ## What Flips the combined-secrets cap default from `warn` to **`enforce`**, completing the staged rollout landed in PR-6 (ColeMurray#909). `parseSecretsCapMode` is now **fail-closed**: only the literal `SECRETS_CAP_ENFORCEMENT=warn` opts out — unset or garbled values enforce. This is **behavior change #2** from the multi-repo-sessions plan (D§6.4's ship-enforced spec). A merged secrets payload that exceeds `MAX_COMBINED_SECRETS_BYTES` (128 KiB, after the global + member fold) now **rejects the spawn/build** with a value-free `SecretsCapExceededError` that attributes bytes/keys per source, instead of only emitting a `secrets.cap_exceeded` warn log and proceeding. Applies identically on the spawn path (`durable-object.getUserEnvVars`) and the repo-image build planner — both route through the shared `auditSecretsMerge`. ## Why now PR-7 (ColeMurray#912) merged, so all Phase-1 substrate PRs (PR-1…PR-7) are landed. The plan gates the **Phase-1 verification checklist** on this flip: > the `enforce` flip is the still-owed follow-up commit **owned by the PR-6 author > and gated before the Phase-1 verification checklist runs — the phase gate cannot > pass in warn mode.** ## Rollback No code revert needed — set `SECRETS_CAP_ENFORCEMENT=warn` in the control-plane worker vars (`terraform/environments/production/workers-control-plane.tf`) to fall back to warn-only logging. ##⚠️ Pre-deploy check The warn window existed precisely to surface any launch unit already over the cap. **Before this deploys, confirm no `secrets.cap_exceeded` warn logs are firing in prod** — otherwise an existing oversized setup will start failing spawns. (Plan risk-table mitigation for behavior change #2.) ## Tests - `parseSecretsCapMode` unit assertions flipped to the fail-closed contract. - Full Phase-1 regression green locally with the flip applied: all 9 TS suites (shared, control-plane unit + integration, web, slack/github/linear bots, typecheck) + pytest (435 sandbox-runtime, 186 modal-infra). No test seeds a combined payload anywhere near the 128 KiB cap (largest is 20 KiB), so the flip is behavior-neutral for the suite outside the `parseSecretsCapMode` assertions. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Tightened secret-size handling so the safer enforcement mode is now the default. * Only an exact `warn` setting keeps the previous logging-only behavior; unset or unrecognized values now use enforcement. * Updated related guidance to match the new default behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…oleMurray#915) ## What PR-8 of the multi-repo sessions & environments plan — the Phase-2 kickoff. Introduces the **environment** entity: a named, prebuildable set of repositories with its own secrets. Everything here is **additive and dark** until the web picker lands (PR-12); the create-a-session-from-an-environment path is PR-9. ## Changes - **Migration `0033_environments.sql`** (DDL from design §7.2): `environments`, `environment_repositories` (position-ordered), `environment_secrets` (mirrors `repo_secrets`), `environment_images` (mirrors `repo_images`), plus `sessions.environment_id` (written by PR-9) and a reserved `automations.environment_id` (§13.3). - **shared**: `Environment` / `EnvironmentRepository` types and `create`/`updateEnvironmentInputSchema`. Repositories reuse the session list contract (non-empty, deduped by owner/name and repoName, capped at `MAX_TARGET_REPOSITORIES`). - **`db/environments.ts`** — `EnvironmentStore`: CRUD, case-insensitive name uniqueness, atomic scalar + repository updates, and an application-level DELETE cascade (repository + secret rows in one batch, live images superseded so the PR-10 reaper reclaims provider artifacts). - **`db/environment-secrets.ts`** — `EnvironmentSecretsStore`: repo-secrets parity keyed by `environment_id` (same `REPO_SECRETS_ENCRYPTION_KEY`), plus a per-key **import** that copies ciphertext **verbatim** from a member repo (no decrypt/re-encrypt — plaintext never transits the control plane). - **`routes/environments.ts`** — `POST/GET/PUT/DELETE /environments[...]`, `GET/PUT/DELETE /environments/:id/secrets[...]`, and `POST /environments/:id/secrets/import`. Internal-HMAC only (web BFF proxied). Import authorization: the source repo must be a current member (else 403), and responses carry **key names only**. - **web BFF** — proxy routes under `app/api/environments/**`. - **hygiene** — rename the Modal client's `environment?` param to `modalEnvironment` to disambiguate from the new entity (§7.1). ## Naming The repository list is `repositories` (the design prose said `members`): no surveyed product uses "members" (Cursor `repos[]`, Codespaces `repositories`, Gitpod `additionalRepositories`), the design's own API-shape reference is Cursor's `repos[] XOR env`, and automations/sessions already use `repositories`. "member" survives only in prose and the mirrored DB columns (`members_fingerprint`, `member_shas`). ## Tests - shared: environment schema validation (name bounds, non-empty/deduped repos). - control-plane integration (real D1 + workerd): 0033 applies; store CRUD + cascade; secrets CRUD; verbatim-ciphertext import (byte-equality + decrypt), non-member 403, value-free responses; route auth / validation / 409 / 404. The happy-path `POST /environments` resolves repositories through the SCM provider, which is unconfigured in the test env — so environments are seeded via `EnvironmentStore` and the routes are tested for auth/validation/secrets/import (mirrors PR-4's DO/store-vs-route split). ## Follow-up (not in this PR) The `repo` / `global` / `environment` secrets stores are now three near-identical implementations. A `refactor: unify scoped secrets stores` PR should extract a shared scoped-secrets base — deferred here to keep this PR additive and avoid churning the spawn-critical `repo-secrets` path. ## Validation `typecheck` (control-plane incl. test config + web route types + shared), `eslint`, `prettier` clean. Full suites green: shared (343), control-plane unit (1609) + integration (481), web (552). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added environment management endpoints in the control plane and web app (create, list, retrieve, update, delete) with ordered repository-set handling. * Added environment-scoped secrets APIs (list, replace, delete) and secret import of selected repo secrets. * Introduced new environments and environment secrets persistence plus corresponding API route groups. * **Bug Fixes** * Updated Modal sandbox dashboard URL parameter naming and aligned related session/URL usage. * **Tests** * Added integration coverage for environments and environment secrets, including import behavior and authorization. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## PR-9 · sessions from environments Phase-2 continuation of the multi-repo-sessions plan (follows ColeMurray#915). Wires the environment entity (PR-8) into session creation, filling the null seams PR-6 (ColeMurray#909) left. **Additive; no D1 migration file change** — `0033` already added `sessions.environment_id`; the DO gets it via a new SQLite schema migration (id 32). ### What it does - **Three-way exclusivity** (shared): `createSessionRequestSchema`/`createSessionInputSchema` accept `environmentId`, mutually exclusive with `repositories` and the scalar `repoOwner/repoName/branch`. Replaced `hasExclusiveRepositoryTarget` with a presence-counting `hasExclusiveSessionTarget` (at most one mode active). - **Environment resolution**: new `resolveEnvironmentTarget(store, id)` loads the environment (404 if missing) and returns its members as resolution inputs in position order. The create handler feeds those through the **same** `resolveSessionRepositories` all-or-nothing SCM check an ad-hoc list gets — a member whose access was revoked after the environment was created fails the create cleanly rather than booting a partial workspace — and records `environment_id` provenance. Scalar/list paths are untouched (keeps scalar's `404` semantics off the hot single-repo path). - **Provenance storage**: `environment_id` threaded through `initialize.ts` → D1 `sessions` INSERT + DO init → `session` table (migration 32, `SessionRow`, `upsertSession`). `SessionEntry.environmentId` hydrates on the session list (PR-12 renders it). - **Environment-scoped secrets** (fills PR-6's seam): the secret-sourcing builder's environment branch now loads real environment secrets — **global + environment only; member repos never inherit** (§6.4/§7.4). Also fixed a placeholder `environmentId: number` → `string` (environment ids are strings). - **Token-refresh environment source** (re-adds what ColeMurray#909 reverted, now with a real source): `OpenAITokenRefreshService` resolves a `TokenSecretSource` (environment | repo | global) carrying everything needed to read *and* write. Environment sessions rotate against environment secrets, never the repo. The union makes the old `repoId === null` guard unrepresentable, so it (and the `session` params it needed) are deleted. - **`SessionState.environmentId` + live `environmentName`**: `getSessionState` resolves the name live so a deleted environment surfaces as `null` (§7.6 "environment deleted"), with the snapshot left intact. ### Tests All green locally: shared 347 (+4 exclusivity), control-plane unit 1612, control-plane integration 488 (+6 new `session-from-environment.test.ts`: resolver order/404, member+provenance snapshot, live name + deleted→null, and the key **member-repo-secrets-absent** assertion), web 552; typecheck + lint + prettier clean. ### Deliberate deviations - Named the new function `resolveEnvironmentTarget` rather than the design's unified `resolveSessionTarget`. Scalar/list normalization already lives inline in the handler, and a 3-mode resolver with a single active caller would be a thin wrapper; unifying scalar into the list path would change the single-repo `404`→`400` error. The genuinely reusable piece (env → member inputs) is what got extracted — a unified funnel can come with the Slack/Linear surface PRs that actually need it. - `environmentName` is resolved via a per-`getSessionState` D1 lookup for environment sessions — intentional, since deleted-env-shows-null requires live resolution, not a snapshot. - **Naming**: new code deliberately avoids the `LaunchUnit` term (the token-refresh source resolver is `resolveSessionSecretSource`). The pre-existing secret-sourcing builder from PR-6 keeps its name in this PR — renaming it (and its file/type) is a standalone follow-up so we don't fold a broad rename into this change. ### Not covered (consistent with PR-4/PR-8) The full route happy path (`POST /sessions {environmentId}` → SCM resolve → create) isn't integration-tested because the control-plane integration harness has no `GITHUB_APP_*` bindings, so the SCM provider raises `GitHub App not configured` (→ 500) before the environment snapshot logic runs — a suite-wide limitation for **any** repo-resolving create path, not specific to this PR. Instead, environments are seeded via the store and the SCM-free pieces are exercised directly (resolver, DO storage/provenance, `getSessionState`, secret scoping). Live e2e remains owed on deployed infra. **Follow-up (tracked):** make the create route testable without live SCM — inject a `SourceControlProvider` into `handleCreateSession` (the seam already exists on `resolveSessionRepositories`), or add fake App bindings + a mocked GitHub API to the harness — so the full `POST /sessions` path (scalar, list, and environment) can be covered end-to-end. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Sessions can now be created from an environment via a new `environmentId` option. * Session state now exposes `environmentId` and `environmentName`. * Environment-scoped secrets are used for sandbox launches and OpenAI token refreshes. * **Bug Fixes** * Session environment provenance is persisted and returned consistently, including after environment deletion. * OpenAI token refresh now reads/writes secrets to the correct scope. * **Tests** * Added/updated unit and integration coverage for environment-targeted session creation, state, secret scoping, and refresh behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## PR-10 · environment image builds Phase-2 continuation of the multi-repo-sessions plan (follows ColeMurray#916). The environment prebuild pipeline (design §7.3/§7.4): environments get built as a unit — every repository cloned at its base branch, setup hooks run sequentially-fatally, filesystem snapshotted — and the resulting images are registered, invalidated, and reclaimed so PR-11 can select them at spawn. One migration: `0034` renames the `environment_images` provenance columns shipped in `0033` (PR-8) to repository nomenclature (`repositories_fingerprint`, `repository_shas`) — safe as a plain rename since nothing writes to the table until this PR lands. Staged commits, as the plan sequences it: **Modal end-to-end first**, then the **Vercel/OpenComputer parity step**, followed by a review-fix pass and the member→repository nomenclature scrub. ### What it does **Runtime (provider-neutral):** build mode now reports what only the build knows — per-repository clone provenance `repository_shas` (`[{repoOwner, repoName, baseSha}]`, one cross-language document shape used unchanged from runtime → callback → D1 → cron) and the baked `runtime_version` (read from the image's own `SANDBOX_VERSION`, so orchestrators never guess it from their code). Emitted via log events for Modal's orchestrator and via `report_success` for provider-session builds. **Modal:** `build_environment_image` generalizes the repo-image worker (`SESSION_CONFIG.repositories` through the existing `create_build_sandbox` seam; redaction covers the clone token plus every build secret value; a completed build missing `repository_shas`/`runtime_version` fails with an actionable message instead of an opaque callback rejection). The 30-min cron gains an isolated **environments pass**: GET enabled environments + status → rebuild triggers, cheapest first — (1) no ready image matching the current repositories fingerprint, (3) runtime below the compatibility floor (or unparseable — fail closed), (2) per-repository `git ls-remote refs/heads/<branch>` drift (honors non-default branches, an improvement over repo images) — with a **per-tick trigger cap of 4** and per-environment concurrency 1. **Control plane:** `EnvironmentImageStore` mirrors the repo-image state machine (conditional transitions, newer-ready races, `(created_at, id)` tiebreaks) with two model-owed differences: supersede scope is `(environment_id, provider)` — the fingerprint covers branches, so one live image per environment/provider — and rows are also superseded **out-of-band** (environment delete, secret change), so the cleanup pass gains a **superseded-artifact reaper** (artifact deleted first, row only after, failures retried next pass). The planner assembles build secrets as **global + environment** — the same set the environment's sessions get (§6.4 build/session parity) — through the PR-6 cap enforcement, and honors the primary repository's `buildTimeoutSeconds`. The workflow enforces **concurrency-1 at the trigger** (cron, save-hooks, and future "Rebuild now" all converge there) and **fails registration closed** on missing/unparseable `runtime_version` or `repository_shas` for *all* providers — an unversioned image must never pass PR-11's floor check. Routes: `/environment-images/{build-complete,build-failed,trigger/:id,status,enabled,mark-stale,cleanup}` mirroring the repo-image surface; `enabled` also serves each environment's fingerprint and the floor so the fingerprint algorithm never leaves the control plane. **Save-hooks (§7.3/§7.4):** environment create/update triggers an immediate build (skipped when a ready image already matches the current repository set — trigger-1 evaluated eagerly); a secret change (PUT/DELETE/import) **supersedes every live image in the same request** — including in-flight builds, which are baking the outdated values — then kicks a rebuild. The supersede is awaited and fail-visible; the rebuild is detached and best-effort. **Vercel/OpenComputer parity (commit 2):** both providers' hand-rolled branch-only build `SESSION_CONFIG`s are generalized so environment builds serialize the repositories-bearing payload; environment builds get the full provider-session lifecycle — bearer callback tokens (same token scheme as repo images), trigger-time session binding with teardown on failure, deferred snapshot/checkpoint finalization behind `waitUntil`, and build-sandbox cleanup hooks. Daytona untouched (base image), as designed. ### Tests All green locally: control-plane unit 1656 (+44: workflow trigger/completion/failure/cleanup incl. provider-session paths, fingerprint/runtime-version parsing), control-plane integration 508 (+20 new `environment-images.test.ts`: store state machine vs real D1, cron-facing routes, callback auth + fail-closed registration matrix, secret-change supersede through the real PUT/DELETE routes, reaper leaves artifact-bearing rows for retry when the provider is unconfigured), sandbox-runtime 436 (+1 multi-repo repository_shas emission), modal-infra 195 (BuildLogResult + both cron passes); typecheck + eslint + prettier + ruff clean. ### Deliberate deviations from the plan doc - **No `ttl=None` at snapshot time.** The Modal TTL change (ColeMurray#897) was rolled back and Modal is pinned at 1.4.3, where `snapshot_filesystem` has no `ttl` parameter and snapshots don't auto-expire — so **trigger 4 (provider-side expiry) is inert** and documented as the seam that activates with the snapshot-TTL follow-up. Nothing else in this PR depends on it. - **`repositories_fingerprint` is computed control-plane-side at registration**, not echoed by the build callback. The same algorithm must run at spawn matching (PR-11); putting it in the callback would duplicate the hash across two languages. The callback carries only what the build alone knows: `repository_shas` + `runtime_version`. - **`markRestoreFailed` is deferred to PR-11** with its caller (the spawn restore-failure fallback). It was specced here as "PR-0's twin", but PR-0 was reverted — repo-images has no such method today, and per the ColeMurray#909 precedent seams land with the code that uses them. - **The concurrency-1 guard lives in the control-plane trigger path**, not cron-side like repo images — environment builds are triggered from three places (cron, save-hooks, later "Rebuild now"), so the guard sits where they converge. The cron still checks first to save the HTTP round-trip. - **Mirror, not generalize.** The subsystem parallels repo-images (store/planner/workflow/adapters/errors) rather than parameterizing the merged, spawn-critical repo-image module; the genuinely shared leaves (callback-token scheme, provider policy, secrets cap enforcement, bearer-token extraction) are imported from their existing homes. Folding both into one image-build subsystem is a tracked follow-up refactor alongside the scoped-secrets-store unification. ### Not covered - Live end-to-end (trigger → Modal build → callback → spawn selection) needs deployed infra — owed with the standing live-e2e pass; spawn selection itself is PR-11. - Triggering a build via the route isn't integration-tested end-to-end (the harness has no Modal/Vercel credentials — the same suite-wide limitation as the SCM gap noted on ColeMurray#916); trigger orchestration is covered at the workflow layer with mocked adapters, and everything from the callback onward runs against real D1 through the real routes. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced environment image builds across supported providers, including trigger, completion, failure, and cleanup workflows. * Added callback routes, status/enabled views, and maintenance endpoints for stale builds and cleanup. * Implemented multi-repository “repositories fingerprint” and environment-image build scheduling on save and after secrets changes. * **Bug Fixes** * Enforced single in-flight builds and token-gated, fail-closed completion handling. * Improved superseding, late completion reporting, and reaping behavior (including artifact-retention edge cases). * **Tests** * Added unit and integration coverage for fingerprints, workflow behavior, and scheduler rebuild decisions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
PR-11 of the multi-repo sessions plan (design §7.3 "Spawn-time matching", §6.5). Environment sessions now boot from their environment's pre-built image when it matches, with base-image fallback on any miss and on provider restore failure — sessions are never blocked on builds. ## What **Spawn-time matching** (`sandbox/lifecycle/environment-image-selection.ts`, new): an environment session uses the latest ready environment image iff 1. `status = 'ready'` on the active provider (latest by `created_at`), 2. `runtime_version` passes `MIN_COMPATIBLE_RUNTIME_VERSION` (fail-closed on an unparseable version), and 3. `repositories_fingerprint` equals the fingerprint of the **session's own repository snapshot** — not the environment's current repositories, so an environment edited after the session was created can never hand it a mismatched image. Any miss → base image, with the miss reason logged (`sandbox.environment_image_miss`: `no_ready_image` / `missing_artifact` / `runtime_below_floor` / `fingerprint_mismatch` / `lookup_failed`) — these are the observability numbers the plan wants for judging the prebuild fast-follows. **Store** (`db/environment-images.ts`): - `getLatestReadyForSpawn(environmentId, provider)` — mirrors the repo-images `repo_metadata` join: an `environments` INNER JOIN so a lingering row can never serve a deleted environment, plus `prebuild_enabled = 1` mirroring repo images' `image_build_enabled` gate (a disabled environment's frozen image never rebuilds, so serving it would drift unboundedly). - `markRestoreFailed(environmentImageId, error)` — the deferred PR-0 twin: flips a `ready` row to `failed` by id so the rebuild cron sees no matching ready image and rebuilds next tick. Scoped to `ready` (building rows belong to the build workflow, superseded rows to the reaper). **Restore-failure fallback** (design §7.3 remedy (b), lifecycle manager): if `createSandbox` with an environment image fails, the manager marks the row restore-failed (best-effort — a D1 outage must not block the retry) and retries once from the base image. Unrelated create failures (quota, network) can false-positive into a mark; the cost is one rebuild, and the base retry surfaces them through the normal failure path anyway. **Repo images never apply to environment sessions.** Previously a single-repo environment session would fall into the repo-image lookup for its member repo — an image baked with that *repository's* setup and secrets, not the environment's (§7.4 launch-unit scoping). The repo-image branch is now gated on `!session.environment_id`. **Boot-mode flags flow through all providers with zero data-plane change**: the environment image travels through the existing `repoImageId`/`repoImageSha` plumbing, which all three providers already convert to boot flags (Modal `repo_image_id` → `FROM_REPO_IMAGE`, Vercel snapshot id, OpenComputer checkpoint id). The runtime's `repo_image` boot mode has exactly the semantics an environment-image boot needs — skip setup hooks (ran at build time), per-repo sync with lenient failure policy, start hooks per member — so the floor stays at 53 and no runtime/CACHE_BUSTER change is needed. `repoImageSha` carries the primary repository's baked SHA (the usual scalar mirror; it is informational — boot logging only). ## Deviations from the plan - **The plan said the fallback "depends on PR-0's `Image.from_id` wrap … verify, don't assume." Verified: it is gone.** ColeMurray#897 (PR-0) was reverted on main — `manager.py:439` is a bare `modal.Image.from_id()` with no probe, no `image_restore_failed` flag, no CP `markRestoreFailed` plumbing for repo images. The fallback here is therefore implemented **provider-generically in the control plane** (catch → mark → retry from base), which is what the design's remedy (b) prescribes ("implemented provider-generically") and avoids the eager-hydrate probe that caused ColeMurray#897's rollback. The repo-image twin of this fallback (plus `RepoImageStore.markRestoreFailed`) belongs to the owed ColeMurray#897 fix-forward, not this PR — single-repo spawn behavior is untouched. - No new boot-mode env flag / runtime change (plan: "Boot-mode env flags flow through all providers"): a distinct `FROM_ENVIRONMENT_IMAGE` flag would be a breaking runtime change requiring a floor bump to 54 and buys only cosmetic log naming — the `repo_image` boot mode's policy is exactly correct for environment images. Documented on `CreateSandboxConfig.repoImageId`. ## Tests - `environment-image-selection.test.ts` (new, 8): matching matrix — match, case-insensitive identity / case-sensitive branches, no ready image, missing artifact, below-floor + unparseable version fail-closed, edited-environment fingerprint mismatch, reorder mismatch, malformed provenance still selects with null SHA. - `manager.test.ts` (+9): env session boots from matching image (and repo lookup untouched); fingerprint miss → base; repo-image lookup never consulted for env sessions (even single-repo); lookup failure → base; no lookup bound → base; restore failure → `markRestoreFailed` + base retry succeeds; retry also fails → normal failure path; mark failure doesn't block the retry; repo-image spawn failures are NOT retried (scope note above). - Integration (+4, real D1): `getLatestReadyForSpawn` latest-ready-per-provider selection across statuses/providers/environments; deleted environment's lingering ready row never served (raw `DELETE FROM environments` bypassing the store's supersede batch); prebuild-disabled environment not served; `markRestoreFailed` ready→failed exactly once, building rows untouched. - Full suites green: control-plane 1675 unit / 514 integration, typecheck, lint, prettier. ## Not covered - End-to-end spawn through the DO with a live provider (same SCM-less/provider-less harness split as PR-4/8/9/10); the wiring is the same one-line-per-method lookup binding pattern as `repoImageLookup`. - Restore-only paths: session snapshot restores (`restoreFromSnapshot`) are unrelated to environment images by design — the image applies to fresh spawns only. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Environment sessions can now select a matching prebuilt environment image at spawn time for faster startup. * If the selected image fails to restore, the system retries automatically using the base image. * **Bug Fixes** * Selection is now stricter: only the latest “ready” image is used, fingerprint mismatches won’t load, and runtime versions below the minimum (or unparseable) won’t be considered. * Restore failure tracking now only marks eligible “ready” images and won’t interfere with other states. * **Changes** * Renamed image inputs from `repoImageId`/`repoImageSha` to `prebuiltImageId`/`prebuiltImageSha`. * **Tests** * Expanded unit/integration coverage for spawn-time selection and restore retry behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…ay#919) PR-12 of the multi-repo sessions plan (design §5.1, §5.2, §7.5 web row). The last web-facing feature PR of Phase 2: environments become creatable and launchable from the UI. ## Settings › Environments New settings tab (`environments-settings.tsx`) with the full lifecycle: - **List** — name, description, repository summary (`primary +N`), prebuild toggle, build status (ready/building/failed dot + last build duration + error tooltip, mirroring Settings › Images), **Rebuild now**, edit, and inline-confirm delete. - **Create/edit form** (`environment-form.tsx`) — name, description, and the ordered repository list: a searchable multi-select popover (the automation form's selector pattern, extracted into a shared `repository-multi-select.tsx`) plus one row per selected repository with a per-repository base-branch picker, reorder controls (`repositories[0]` is the primary and gets the badge), and remove. Client-side guards mirror the shared schema: `MAX_TARGET_REPOSITORIES` cap and no duplicate repository *name* across owners (checkout paths are `/workspace/{repoName}`). - **Secrets tab** — reuses `secrets-editor.tsx` via a new `environment` scope (`/api/environments/{id}/secrets`), with the inherited-global rendering intact, plus the per-key **import from a repository** flow (`environment-secrets-import.tsx`): pick a source repository, check keys, values copied server-side (PR-8's endpoint enforces that the source is a current repository of the environment and never returns values). Creating an environment lands on the Secrets tab so setup can finish in one pass. Prebuild-specific UI (toggle, status, rebuild) is gated on `supportsRepoImages()`; the tab itself works on any provider. ## Unified new-session picker `SessionTarget` grows from `none | repo` to `none | repo | environment | repos`, moved out of `page.tsx` into `lib/session-target.ts` together with the option-value round-trip and `buildSessionTargetRequestFields`, which emits **exactly one** of the three mutually exclusive create modes (scalar / `environmentId` / `repositories`) so the picker can never trip `createSessionRequestSchema`'s exclusivity refinement. - The repo combobox becomes one unified searchable list: an **Environments** group (name + "N repositories · prebuilt/prebuild building/prebuilds on", driven by one `GET /api/environment-images` call) above the **Repositories** group. Instances with no environments see the flat repo list as today. - **Ad-hoc multi-select**: a "Multiple repositories" option switches to the `repos` mode, seeded with the currently selected repo, edited through the same multi-select popover. Order is primary order. - Secrets-disclosure copy per §7.4 under the prompt box: environment sessions use global + environment secrets; ad-hoc sessions use global + the selected repositories' secrets — with the "save this set as an environment" nudge (ad-hoc sets don't get prebuilds). - The sandbox-warming config key now distinguishes environment ids and ad-hoc lists, so changing the selection invalidates a warmed session exactly like repo/branch changes do. Single-repo behavior (branch dropdown, localStorage restore, warm-on-type) is byte-identical. ## BFF - `POST /api/sessions` allowlist forwards `environmentId` and `repositories`; identity fields stay server-derived and everything else is still stripped (tested). - New proxies: `GET /api/environments/[id]/images` (per-environment status incl. failed rows, for settings), `POST /api/environments/[id]/images/trigger` (Rebuild now), `GET /api/environment-images` (cross-environment ready/building, for the picker) — all 501-gated like the repo-images routes. ## Session page `MetadataSection` gains the environment provenance row (threaded from `SessionState.environmentId/environmentName`, which PR-9 already hydrates through the socket): the environment name when it resolves, and **"Environment deleted"** when the name resolves null (§7.6 contract). ## Tests 571 web tests pass (19 new): - `session-target.test.ts` — per-mode request-field exclusivity, option-value round trip, multi-mode seeding, warm-config identity, launchability. - `page.test.tsx` — environment launch sends only `environmentId`; ad-hoc launch sends only `repositories` (seeded from the selected repo, edited through the popover); existing no-repository flows unchanged. - `environment-form.test.tsx` — primary marking + reorder changes submitted order, cap disables further selection, name-collision guard, submit gating. - `sessions/route.test.ts` — forwards `environmentId` / `repositories`, still strips `userId`/`spawnSource`/`scmToken`/`authUserId`. - `metadata-section.test.tsx` — environment name, deleted state, absent for repo-launched sessions. ## Not in this PR - Environment names on session-list cards (`Session` carries `environmentId` but not a resolved name; list rendering is a fast follow if wanted). - Slack/Linear/GitHub-bot environment targets (Phase 3, §7.5 — targets unify, nothing migrates). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an **Environments** settings tab with create/edit/list, delete, and prebuilt image management (status, rebuild, enable/disable). * Enhanced the launch flow to support **environment targets** and **ordered multi-repo selection**. * Added **environment-scoped secrets** and secret import from a selected repository. * **Bug Fixes** * Improved session creation so environment and multi-repo launches send the correct fields, and invalid/extra fields are stripped. * Strengthened authentication gating for environment/repo image actions. * **Documentation** * Updated launch help/disclosures to reflect environment vs repository launch modes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…ay#920) ## Summary Fast-follow committed on the ColeMurray#919 review thread ([discussion](ColeMurray#919)): extract the session-target picker from the home page now that the unified environment/repository picker landed. - **`useSessionTargetPicker`** (new hook) owns the `SessionTarget` state, the unified environment/repository option list (including prebuild-status subtitles), display labels, branch and multi-repo selection handling, and request-field construction (`buildRequestFields()`, which returns `null` when the selection isn't launchable). - **`SessionTargetPicker`** (new component) owns the three target controls: the unified target combobox, the ad-hoc repository multi-select, and the branch combobox. - **`page.tsx`** keeps exactly what the review discussion scoped to it — model selection, prompt state, and sandbox warming — and shrinks from 733 to 437 lines. The 24-prop `HomeContent` boundary collapses to 13 props (one of which is the picker state object). The pure pieces were already in `lib/session-target.ts`, so this is the mechanical half: no behavior change, no new API calls, no markup changes. ## Testing - `npm test -w @open-inspect/web` — 589 tests pass, including the four `Home` tests that drive the real picker end-to-end (no-repository, environment-only body, ad-hoc seeded multi-select). - `npm run typecheck -w @open-inspect/web`, `npm run lint` — clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a unified session target picker to select environments, repositories, and branches in a single interface. * Automatically selects a default repository, restores the last-used choice when available, and persists repository selection for future sessions. * Launch eligibility and request payloads are now driven by the picker’s selection state. * **Bug Fixes** * Improved warm-session behavior by ensuring the returned warmed session matches the currently selected target configuration before proceeding. * Submit and prompt-triggered actions are disabled until the selected target is ready to launch. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…Murray#921) ## Summary PR-13 of the multi-repo sessions plan: user-facing and package documentation for multi-repository sessions, environments, and environment prebuilds (shipped across ColeMurray#915–ColeMurray#919). - **docs/HOW_IT_WORKS.md** — session targets (single repo / ad-hoc set / environment / none), the `/workspace/<repo-name>` checkout layout and primary-repository rule, a new **Environments** section (snapshot-at-creation semantics, "Environment deleted" state, save-as-environment nudge), multi-repository notes in the fresh-start and prebuilt-image lifecycles, and the launch-unit secrets rule in the security model. - **docs/SECRETS.md** — scope table gains **Environment**; new "Which secrets a session receives" section (global + launch unit; environment sessions never inherit repository secrets; primary-wins for ad-hoc sets), the per-key import flow, the 128 KB combined cap with per-scope attribution, and the §8.6 guidance: don't persist long-lived secrets to disk in `setup.sh` (they get baked into prebuilt images; environment-secret changes supersede + rebuild, repo/global rotation does not). - **docs/IMAGE_PREBUILD.md** — new **Environment Images** section: enabling, how N-wide builds run (sequential per-repository setup, fail names the repository, build/session secret parity), rebuild triggers (fingerprint / new commits / runtime floor / secret-change supersede), spawn matching with base-image fallback, and the explicit "ad-hoc sessions never get prebuilds" tradeoff. - **READMEs** — root README gains a Multi-Repository Sessions & Environments feature blurb; control-plane README gains the `/environments*` + `/environment-images*` endpoint tables and the new D1 tables (`session_repositories`, `environments`, `environment_repositories`, `environment_secrets`, `environment_images`); modal-infra README's stale "scheduling currently disabled" notes replaced with the actual 30-minute two-pass cron; AGENTS.md D1/data-plane lines updated. Docs-only — no code changes. ## E2E verification checklist (execution pending deployment) The plan's Phase-2 checklist, to run against a deployed instance once the prod sync (Modal ColeMurray#900 first) lands; recorded here so it's tracked: - [x] Create environment (2 repos, one heavy setup) → save triggers build → completes - [x] Session from the environment boots in seconds, **no setup.sh** - [x] Agent edits both repositories → two PRs - [x] Edit an environment repository's branch → next session boots base until rebuild - [x] Environment-secret change triggers rebuild (and supersedes the ready image) - [x] Resume from snapshot works for a multi-repository session - [x] Ad-hoc 2-repo session boots base with full setup (documented tradeoff) - [x] Single-repo + bot (Slack/GitHub/Linear) regression pass <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Expanded setup and architecture materials to describe environments, multi-repository session workspaces (including ordering and sandbox startup flow), and how sessions target repositories. * Overhauled prebuilt image docs to differentiate repository vs environment image artifacts, explain rebuild triggers/timing, and clarify when images are used (including ad-hoc multi-repo restrictions). * Reworked secret documentation with a new secret-scope model (global/repository/environment), environment secret import behavior, and updated troubleshooting/limits. * Added environment-focused API and infrastructure notes, including environment endpoints and scheduled image rebuilds. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…rray#923) ## Summary Fixes the bug where picking an **environment** in the new-session picker is forgotten: any return to the home page — the sidebar "+", the `Cmd/Ctrl+Shift+O` shortcut, the command-menu "New session", or the header logo (all of which `router.push("/")` and remount `Home`) — fell back to the previously stored repository. **Root cause** (in `hooks/use-session-target-picker.ts`): the persistence effect early-returned unless `sessionTarget.kind === "repo"`, so environment selections were never written to `localStorage`; and the restore effect only validated the stored value against the repo list and could only ever produce a repo (or "none") target. **Fix**: - **Persist the picker's select value** — a repo fullName or `env:<id>` — under the existing storage key. Stored legacy repo fullNames keep working unchanged (an `env:` value can never collide with a fullName), so no key migration is needed. Ad-hoc "Multiple repositories" sets and "No repository" stay unpersisted, matching today's behavior. - **Restore branches on the parsed stored value.** A stored environment waits for the environments fetch to settle before deciding — the restore effect commits exactly once (the `if (sessionTarget) return` guard blocks later correction), so deciding while environments were still loading would flash the repo default and never recover. A stored environment that no longer exists falls back to the repo default. - **Guard against the session-loading window**: both `useRepos` and `useEnvironments` gate their SWR fetch on the auth session and report `loading: false` while it resolves, so an unguarded restore would read the empty unloaded lists as authoritative (classifying a stored environment as deleted, or locking in "no repository"). The restore now waits for `useSession().status !== "loading"`, which also hardens the pre-existing repo path. ## Testing `npm test -w @open-inspect/web` — 593 tests (4 new in `page.test.tsx`): - persists an environment selection (`env:env-1` in storage) and restores it on a fresh mount, launching with only `environmentId` - does **not** commit the repo default while a stored environment's fetch is pending, then resolves to the environment - falls back to the repo default when the stored environment was deleted - still restores a stored legacy repository fullName Typecheck and lint clean. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * The app now remembers your last selected repo or environment and restores it on return, including after sign-in and reloads. * **Bug Fixes** * Improved restoration timing so saved environment selections are applied only after environments finish loading. * If a saved environment is missing or the stored value is malformed, the app now falls back to a valid default repository. * Legacy saved repo selections are restored correctly. * Environments/repos loading states now respect session resolution to avoid premature empty results. * **Tests** * Expanded coverage for loading-state–dependent restoration and legacy fallback behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Phase 3 (reach) of the multi-repo sessions plan, first slice: **Slack
routing rules can now target an environment**. A keyword rule pointing
at a saved environment launches the full multi-repository workspace
(prebuilt image included) straight from Slack — the first bot surface to
gain environment targets, per the design's "targets unify instead of
migrate" principle (D§7.5, Phase A).
This is the deterministic path first. Channel-association environment
targets (Phase A2) and environments in the LLM classifier candidate set
(Phase B) are follow-up PRs.
## What changed
**shared**
- `SlackRoutingRule` activates the reserved `targetType?: "repository" |
"environment"` field. Environment rules store the stable `env_…` id
(never the rename-able display name).
- `normalizeRoutingRules` canonicalizes per kind: repository targets
stay trimmed + lowercased; environment ids are opaque and only trimmed.
Repository rules normalize to the bare `{ keyword, target }` shape,
keeping every pre-environment rule byte-identical. De-dupe key includes
the target type.
**control-plane**
- `validateRoutingRules` accepts the new field: environment rules must
carry an `env_…` id; repository rules keep the `owner/name` form check.
Target *existence* still isn't checked at this layer — the bot skips
stale targets at match time, same as stale repos.
**slack-bot**
- New `SlackSessionTarget` union (`repository` | `environment`) + value
encoding (`env:<id>`, mirroring the web picker; bare values keep parsing
as repo ids so in-flight clarification messages posted before this
deploy stay clickable).
- New `classifier/environments.ts`: `GET /environments` with the same
in-memory → KV cache and **fail-open-to-empty** posture as repos/rules.
Fetched lazily — only when a matched rule actually targets an
environment.
- The routing-rule stage resolves environment targets and now runs
**before the single-repo shortcut** (otherwise environment rules could
never fire in one-repo workspaces). Repo-rule behavior in single-repo
workspaces is unchanged: any accessible repo rule there resolves to the
same lone repo.
- `ClassificationResult` is now slack-local and target-aware
(`target`/`alternatives` as unions). The shared repo-only
`ClassificationResult` remains for the Linear bot.
- Clarification quick-picks render either kind (environments
disambiguate as `name (environment)` on label collisions); the selection
handler resolves `env:<id>` values; `createSession` sends `{
environmentId }` for environment targets — mutually exclusive with the
scalar repo fields and `branch` by the create schema (the environment
defines its own branches, so per-user branch preferences don't apply).
- Thread sessions and completion callbacks label environment sessions
with the environment name.
**web**
- Settings › Integrations › Slack routing rules: the target select
becomes a unified list (Environments group + Repositories group,
appearing only when environments exist — same convention as the session
picker). Drafts encode environments as `env:<id>` and split into
`target` + `targetType` on save. Stale-environment rules render with a
warning, like stale repos.
**docs**
- `HOW_IT_WORKS.md`: the "bots are single-repository" line now carves
out Slack routing rules.
## Deploy-order safety
Rules with `targetType: "environment"` read by an **old** bot normalize
to a bare `env_…` target that matches no repo → the rule is skipped
(fail-safe). New bot + old stored rules behave exactly as today.
## Testing
- shared: normalize preserves/canonicalizes env rules, mixed-kind
dedupe, repository-shape stability (`npm test -w @open-inspect/shared`,
351 ✓)
- control-plane: env-rule round-trip + rejection cases (unit 1678 ✓,
integration 514 ✓)
- slack-bot: env rule routes without LLM; no env fetch when no env rule
matches; env rule wins over the single-repo shortcut; mixed repo+env
clarification; deleted-env rule falls through to LLM; quick-pick `env:`
values + collision disambiguation; environments fetch cache/fail-open
(186 ✓)
- web: env-targeted rule add/save with `targetType`, stored-env rule
renders by name, stale-env warning (592 ✓)
- lint + typecheck clean across packages
## Plan/design refs
- docs/multi-repo-sessions-design.md §7.5 (Slack Phase A), decision 14
(additive bot targets)
- docs/multi-repo-sessions-implementation-plan.md — Phase 3 slice 1
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Slack session launches can now target either a repository or an
environment.
* Routing rules and clarification quick-picks support environment-based
destinations.
* Slack integration settings now let you configure and manage
environment routing rules alongside repositories.
* **Bug Fixes**
* Improved handling for missing, stale, or unresolved targets with
clearer warnings and graceful fallback.
* Enhanced reliability of environment lookup and routing-rule resolution
(including fail-open behavior).
* Slack mrkdwn output now escapes sensitive characters in completion
footers.
* **Documentation**
* Updated “how it works” guidance for bot-created single-repo behavior
and environment/workspace routing options.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Replace the deprecated sandbox file-handle write path for tunnel env files with the Modal filesystem writer. - Update tunnel-port tests to mock the filesystem writer while preserving graceful failure behavior. ## Why Modal deprecates the sandbox file-handle API. Writing through the filesystem API avoids deprecation warnings during sandbox setup. ## Validation - `uv run ruff check src/sandbox/manager.py tests/test_tunnel_ports.py` - `uv run ruff format --check src/sandbox/manager.py tests/test_tunnel_ports.py` - `uv run pytest tests` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved reliability when saving tunnel environment data. * Tunnel configuration files are now written through a more robust file-writing path, with the same warning and error handling behavior. * Updated validation coverage to ensure tunnel settings are written in the correct order and format, and that write failures do not interrupt tunnel setup. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This is an automated nightly unsafe-cast remediation sweep. It replaces selected high/medium-risk TypeScript assertions at trust boundaries with parse-don't-assert validation, following the TypeScript Coding Standards for unsafe casts and the Zod boundary-validation pattern established in PR ColeMurray#807. ## Findings Fixed | file:line | risk | cast removed | fix | | --- | --- | --- | --- | | `packages/slack-bot/src/callbacks.ts:242` | HIGH | `payload as CompletionCallback` after `request.json()` | Added a Zod `completionCallbackSchema` and route-local `safeParse`; HMAC verification still uses the original payload object so signed unknown fields are preserved. | | `packages/slack-bot/src/callbacks.ts:294` | HIGH | `payload as ToolCallCallback` after `request.json()` | Added a Zod `toolCallCallbackSchema` and route-local `safeParse`; malformed Slack context continues to return the existing 400 response. | | `packages/control-plane/src/scheduler/slack-completion.ts:35` | MEDIUM | `JSON.parse(raw) as SlackRunMetadata` from persisted trigger metadata | Added a Zod `slackRunMetadataSchema`, made `SlackRunMetadata` a `z.infer` type, and return `null` on parse/shape failure to preserve the existing skip contract. | ## Verification | command | result | | --- | --- | | `npm run build -w @open-inspect/shared` | passed | | `npm run build -w @open-inspect/control-plane` | passed | | `npm run build -w @open-inspect/slack-bot` | passed | | `npm run typecheck` | passed | | `npm run format` | passed | | `npm run lint` | passed in a clean temporary worktree with this patch applied; the main worktree has a pre-existing untracked `.opencode/` directory that causes root lint to scan non-repo tool files | | `npm run lint -w @open-inspect/control-plane` | passed | | `npm run lint -w @open-inspect/slack-bot` | passed | | `npm test -w @open-inspect/control-plane` | passed | | `npm test -w @open-inspect/slack-bot` | passed | --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/a20ac15ca97c48a0ccb3c1d3ea968bf9)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved validation for Slack callback requests using stricter schema checks, ensuring malformed or partially populated signed payloads fail cleanly. * Hardened completion and tool-call handling to reject invalid payloads early while preserving unknown signed fields on valid requests. * Added safer Slack run metadata parsing with schema validation, returning `null` for malformed or incomplete metadata. * **Tests** * Added coverage for callback validation and completion handling, including malformed/partial payload rejection and correct message routing for signed completions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: OpenInspect <open-inspect@noreply.github.com>
## Summary Slack Phase A2 of the multi-repo environments work (follows ColeMurray#924): environments can now be associated with Slack channels, the same way repositories can via repo metadata. A message in an associated channel routes deterministically to the environment — no keyword needed. ### Control plane - **Migration 0035**: additive `channel_associations TEXT` column on `environments` (JSON string array, mirroring `repo_metadata.channel_associations` from 0002). Rollback = stop reading the column. - `Environment` gains optional `channelAssociations`; `create/updateEnvironmentInputSchema` accept it (trimmed, non-empty ids, capped at 50). Update semantics match the rest of the patch shape: field absent → untouched, array → wholesale replace, `[]` → cleared (column collapses to NULL). The route dedupes before storing. - Extracted the JSON-array column parser shared with `repo_metadata` into `db/json-columns.ts` instead of duplicating it. - API-only, like repository channel associations — no web UI in this slice. ### Slack bot - The classifier's channel-association stage is now target-aware: `resolveChannelTargets` (in `classifier/routing.ts`, beside its routing-rule sibling) returns environments and repositories whose association lists name the channel. - **The stage moves ahead of the single-repo shortcut** — same reachability argument as the routing-rule stage in ColeMurray#924: without this, an environment-associated channel in a one-repo workspace could never route to the environment. For repository associations this is behavior-preserving (in a one-repo workspace a channel can only ever match that one repo). - Semantics: - exactly one associated target → high-confidence route (reasoning escapes the environment name for mrkdwn, per the escape-at-render policy from ColeMurray#924); - several targets including an environment → clarification with those targets as quick picks (the LLM can't select environments until Phase B, so silently falling through would drop them); - several repositories only → falls through to the LLM as today (channel associations are already a prompt signal it can arbitrate with). - The environments fetch stays the ColeMurray#924 cached resource (60s memory / 5min KV, **fail-open to `[]`**), so an environments outage degrades to today's repository-only matching. It is consulted only for messages that reach the channel stage inside a channel context. - `getReposByChannel` folded into `resolveChannelTargets` (its only caller was the classifier). ## Deploy-order safety The column is additive and dark: an old bot ignores `channelAssociations` on the API response; a new bot against an un-migrated DB simply sees no environment associations. No coordination needed. ## Testing - shared: schema accept/reject cases for `channelAssociations` (360 ✓) - control-plane: store JSON-column round-trip; route tests for set/dedupe/omit-untouched/clear and 400s on malformed payloads (1679 unit + 518 integration ✓) - slack-bot: repo association routing, environment association routing, mrkdwn escaping of hostile environment names, environment-beats-single-repo-shortcut, mixed repo+environment clarification, repo-only multi-match LLM fallthrough, no environments fetch outside channel context (205 ✓) - web/github-bot/linear-bot suites unaffected and green (599/115/125 ✓) Part of the multi-repo sessions Phase 3 plan (design §7.5, Slack Phase A). Next slice: Phase B — environments in the LLM classifier candidate set + unified clarification picker. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Environments can now be linked to Slack channels, allowing messages in those channels to route directly to the right environment. * Create and update environment endpoints now support channel associations, including automatic de-duplication. * Routing recognizes both repository and environment channel links for more reliable direct session targeting. * **Bug Fixes** * Clarified behavior for empty vs omitted channel associations: existing associations remain when omitted, and can be cleared when an empty list is provided. * **Documentation** * Updated “Session Targets” documentation to reflect channel-association routing without requiring a keyword. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This is an automated nightly unsafe-cast remediation sweep. It replaces three high-risk Slack bot trust-boundary assertions with Zod `safeParse` validation, following the TypeScript Coding Standards unsafe-cast / parse-don't-assert guidance and the Zod boundary-validation pattern established in PR ColeMurray#807. | file:line | risk | cast removed | how it was fixed | | --- | --- | --- | --- | | `packages/slack-bot/src/activity-status.ts:201` | HIGH | `(await response.json()) as SlackEnvelope & { detail?: unknown; response_metadata?: unknown; warning?: unknown; warnings?: unknown }` for Slack Web API status responses | Added a package-local Zod envelope schema and `safeParse`; malformed bodies preserve the existing `{ ok: false, error: "invalid_response" }` path. | | `packages/slack-bot/src/channel-trigger.ts:148` | HIGH | `(await response.json()) as { triggered?: number; skipped?: number; steered?: number }` for control-plane Slack trigger forwarding results that decide whether to react to a message | Added a package-local Zod response schema and `safeParse`; malformed bodies log `invalid_response` and skip reaction, matching the existing best-effort failure behavior. | | `packages/slack-bot/src/classifier/repos.ts:301,330` | HIGH | `cached as string[]` and `(await response.json()) as { channels?: string[] }` for watched-channel cache/control-plane payloads used to pre-filter Slack automation triggers | Added package-local Zod schemas for watched-channel arrays/responses and `safeParse`; malformed payloads fail closed to an empty watched-channel set. | ## Verification | command | result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run build -w @open-inspect/slack-bot` | Passed | | `npm run typecheck` | Passed | | `npm run lint -w @open-inspect/slack-bot` | Passed | | `npm run lint` | Passed after temporarily moving the unrelated untracked local `.opencode/` tooling directory out of the workspace and restoring it immediately; the command otherwise lints that untracked tooling. | | `npm run format` | Passed | | `npm test -w @open-inspect/slack-bot` | Passed: 14 files, 174 tests | References: TypeScript Coding Standards unsafe-cast / parse-don't-assert guidance; Zod boundary-validation pattern established in PR ColeMurray#807. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/93ea99ef0272013a1a6b1c6d38376786)* Co-authored-by: OpenInspect <open-inspect@noreply.github.com>
…oleMurray#930) ## Problem Since ColeMurray#926 deployed, sessions with `tunnelPorts` boot without `/workspace/.tunnels.env`: the supervisor logs `tunnel.stale_file_cleared` followed 30s later by `tunnel.env_file_wait_timeout`, delaying every boot by the full wait and leaving `start.sh` without tunnel URLs — even though the manager logs `tunnel.urls_written`. ## Root cause The supervisor's boot-time stale-file cleanup and the manager's tunnel env write have **no ordering guarantee** — the design (from ColeMurray#663) only works when the write lands *after* the clear: | ts (from prod logs) | who | event | |---|---|---| | 1783614343447 | manager | `tunnel.urls_written` — write completes | | 1783614343542 | supervisor | `supervisor.start` (+95ms) | | 1783614343543 | supervisor | `tunnel.stale_file_cleared` — **deletes the fresh file** | | 1783614374781 | supervisor | `tunnel.env_file_wait_timeout` (30s later) | ColeMurray#926 switched the write from the deprecated file-handle API (3–4 sequential gRPC round trips after container start — always slow enough to land after the clear) to `sandbox.filesystem.write_text`, which is implemented as a single exec of a fs-tools helper. That exec completes while the entrypoint is still importing Python modules, so the write now lands *before* `run()` begins and the clear deletes it on essentially every boot. The manager writes exactly once, so nothing recovers. Compounding the confusion: the old cleanup logged `tunnel.stale_file_cleared` unconditionally (`unlink(missing_ok=True)`), so a boot that deleted the fresh file was indistinguishable from a healthy no-op. ## Fix Remove the ordering dependency instead of re-tuning the race. Writers now tag the file's first line with the sandbox it was written for: ```dotenv TUNNEL_SANDBOX_ID=sandbox-acme-app-1783614336426 TUNNEL_3000=https://... ``` The supervisor's cleanup keeps a file tagged with its own `SANDBOX_ID` (the manager won the race — the URLs are for this sandbox and valid for its lifetime) and clears everything else: files tagged with another sandbox's ID (snapshot/image leftovers with dead URLs), untagged files, and anything it can't read. A supervisor without a `SANDBOX_ID` identity never trusts a pre-existing file. Write ordering no longer matters on either side of the race, on both fresh-create and snapshot-restore paths. The Vercel provider writes the same file via exec and had the same latent race, so it tags too. Cleanup logging is now truthful: `tunnel.fresh_file_kept` / `tunnel.stale_file_cleared` only when a file actually existed. ## Mixed-version rollout Safe in both directions, no version gate needed: - **Old runtime image + new writer**: the tag line is inert (the wait loop matches `TUNNEL_<port>=` prefixes); the old cleanup still clears unconditionally — status quo until images rebuild. - **New runtime + untagged writer**: untagged files are treated as stale — exactly today's behavior. The consumer-facing format is unchanged apart from the extra `KEY=value` line, which any dotenv consumer tolerates. ## Validation - `uv run --extra dev pytest tests/` — sandbox-runtime **439 passed** (new cases: keep-own-tag, clear-other-tag, clear-untagged, clear-when-identity-unknown), modal-infra **194 passed** - `npm test -w @open-inspect/control-plane` — **1686 passed** - `npm run typecheck -w @open-inspect/control-plane`, `ruff check` / `ruff format --check`, `eslint`, `prettier --check` — clean <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Updated tunnel env-file documentation to reflect how stale files are cleared and how current-sandbox files are preserved. * **New Features** * Added sandbox ID tagging to tunnel environment files to make the owning sandbox identifiable. * Ensured tunnel env-file writing includes the sandbox ID marker followed by sorted port URL entries. * **Bug Fixes** * Improved boot cleanup to retain a correctly tagged tunnel file and remove only truly stale/untagged or mismatched ones. * **Tests** * Updated unit/integration tests to validate the new sandbox ID marker and content ordering/cleanup behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…oleMurray#928) ## Summary Slack Phase B of the multi-repo environments work (design §7.5; follows ColeMurray#924 routing rules and ColeMurray#927 channel associations): **environments join the LLM classifier's candidate set**, and the clarification picker offers both repositories and environments. This closes the gap where an ambient message ("work on the full-stack app") could never reach an environment — only explicit keywords or channel associations could. Slack-bot only; no control-plane, shared, or migration changes. ### LLM classifier (`classifier/index.ts`) - The LLM stage now fetches environments through the ColeMurray#924 cached resource (fail-open to `[]` — an outage degrades to today's repository-only classification). Deterministic stages (routing rules, channel associations) still run first and unchanged. - **Single-repo shortcut yields to environments**: it now fires only when there is one repository *and no environments* — otherwise a one-repo workspace could never classify into an environment. - The prompt gains an "Available Environments" section (id, name, description, member repositories) with guidance to prefer an environment when the work spans several of its repositories. Omitted entirely when none exist. - Tool contract renamed `classify_repository`/`repoId` → `classify_target`/`targetId`. This is self-contained (the same worker builds the tool and parses the response), so no compatibility shim is needed. - Resolution ladder for `targetId` and alternatives (`matchTargetId` in `classifier/routing.ts`): `env_…` id → environment by id; else repository by id/fullName (the pre-environment behavior); else environment by its unique case-insensitive name — so a model that echoes the name instead of the id still resolves. Alternatives dedupe by target value and never repeat the match. - LLM `reasoning` is now escaped for mrkdwn at composition (it can quote environment names or message text), matching the deterministic stages' escape-at-composition policy. - Per-user branch preferences already don't apply to environment targets (the environment defines branches — ColeMurray#924). ### Unified clarification picker (`repo-clarification.ts` → `target-clarification.ts`) - The inline `static_select` uses **`option_groups`** (Environments above Repositories, mirroring the web picker's grouping) once environments exist; flat repository options otherwise. Environment options carry the existing `env:<id>` value encoding, so the ColeMurray#924 selection handler resolves them with zero changes. - The `external_select` suggestion path (`block_suggestion`) serves both kinds: environments filter on name, repositories on fullName, combined and capped at Slack's 100-option limit (environments first — the list is short). The response is `{options}` or `{option_groups}` per Slack's contract. - Copy follows the offer: placeholder/headline say "repository or environment" only when environments are actually on offer. - This lands the naming rework deferred from ColeMurray#924: module renamed, `buildTargetClarificationBlocks` / `getTargetClarificationOptions` / `SELECT_TARGET_*` / `MAX_TARGET_QUICK_PICKS`. **Wire `action_id` values are kept byte-identical** (`select_repo`, `select_repo_quick_pick`) so pickers and quick-picks on already-posted messages keep working. - `slack-blocks.ts`: `SlackStaticSelectElement` becomes `options XOR option_groups` (Slack accepts exactly one), with a new `SlackSelectOptionGroup` primitive. ## Deploy-order safety Nothing to coordinate: all changes are bot-internal, wire ids are stable for old messages, and the environments fetch fails open. ## Testing - classifier: environment id/name resolution, prompt section presence/absence, shortcut suppressed with environments and kept without, mixed alternatives deduped, reasoning escaping (28 tests) - target-clarification: grouped vs flat options, query filtering both kinds, 100-option cap split, grouped inline picker blocks, both-kinds copy, quick-pick env values, stable wire ids (28 tests) - Full matrix green: slack-bot 225, shared 360, control-plane 1686 unit + 518 integration, web 599, github-bot 115, linear-bot 125; lint/typecheck clean. Part of the multi-repo sessions Phase 3 plan. Remaining Phase 3 slices: Linear KV `env:<envId>` targets, GitHub bot `default_environment_id`, automations shared-workspace mode, repo-filter joins. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Expanded Slack clarification to select **either repositories or environments** via a unified target picker. * Added grouped target suggestions and capped quick-pick buttons for multi-candidate matches. * Updated AI-assisted routing to classify and resolve **targets** using a combined catalog of repositories + environments. * **Bug Fixes** * Improved robustness of Slack static select handling/validation to better tolerate missing or malformed option data. * **Documentation** * Clarified “Session Targets” behavior for bot-created sessions, including Slack environment targeting and classifier-driven selection. * **Tests** * Added/updated coverage for environment-aware classification and target-clarification UI flows. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…urray#931) ## Summary Linear-bot slice of the environments Phase 3 rollout (design §7.5): the team and project KV mappings can now target a saved environment alongside repositories, so a Linear issue can launch a full multi-repo workspace deterministically. - **Mapping entries accept `{ "environmentId": "env_…" }`** (the stable id, not the rename-able display name) alongside `{ "owner", "name" }`, in both `config:team-repos` (with the existing optional `label` filter) and `config:project-repos`. Targets unify instead of migrate — repository entries never stop working. - **New `target-resolution.ts`** extracts the 4-stage ladder (project mapping → team mapping → Linear repo suggestions → LLM classifier) out of `webhook-handler.ts` and owns the target-kind policy (`SessionTarget` union, labels, create-request fields). The suggestion and classifier stages remain repository-only for now (parity with the design's staged rollout). - **Unknown environments fail open**: an entry whose environment was deleted (or when the environments fetch fails) is skipped with a warning and resolution falls through to the next stage — mirroring how the Slack classifier skips rules targeting inaccessible targets. - **New `environments.ts`** fetch helper mirroring `classifier/repos.ts` (in-memory 60s → control plane → KV 300s, fail open to `[]`). - **Session creation** sends `{ environmentId }` XOR `{ repoOwner, repoName }` (the create schema makes the two mutually exclusive). The issue→session KV record stores the environment id for environment sessions. - **Integration settings** (enabled-repos gate, model config) resolve from the environment's **primary repository** until environment-level integration settings exist (design §13.5); `callbackContext.repoFullName` carries the primary repo (the field is echoed back and unread today). - `resolveStaticRepo` → `resolveStaticTarget` (it no longer resolves only repos). ## Behavior notes - Deterministic paths only: ambient issues with no mapping still classify over repositories. - Repository mappings, label filtering, and the elicitation flow are byte-for-byte unchanged in behavior. - The previously unreachable final "couldn't determine repository" guard in `handleNewSession` is gone — the extracted resolver always returns a full target or elicits. ## Testing - `resolveStaticTarget`: environment entries match by label and as label-less fallback. - Handler tests (new): project→environment mapping creates a session with `environmentId` and no repo scalars, settings resolve from the primary repo, KV issue record stores the environment id; label-matched team→environment mapping; unknown environment falls through without creating a session; repository mappings still produce repo sessions. - Full suite: 131 linear-bot tests green; repo-wide lint, typecheck, format clean. Part of the multi-repo sessions / environments Phase 3 rollout (after ColeMurray#924, ColeMurray#927, ColeMurray#928). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Linear sessions can now target either a GitHub repository or a saved environment. * Team and project mappings now support environment-based routing, including label-aware matching and fallback behavior. * **Bug Fixes** * Session setup now skips missing environments instead of failing the flow. * Improved target selection so the app falls back more reliably when a direct match isn’t found. * **Documentation** * Updated Linear and session behavior docs to reflect repository vs. environment targeting. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## What changed - added a dedicated formatter for future relative timestamps (`in 5m`, `in 2h`, `in 1d`) - defined sub-minute scheduled runs as `in <1m` - updated automation list metadata to use the future formatter - added formatter boundary tests and rendered list coverage ## Why Scheduled automation timestamps are future values, but the list rendered them with a formatter designed for past activity. As a result, every future `nextRunAt` value fell through to `just now`. ## Root cause `formatRelativeTime` subtracts the supplied timestamp from the current time and only handles positive past-time differences. Passing a future timestamp produces negative minute, hour, and day values, so the formatter returns its recent-past fallback. ## Checks - `npm run build -w @open-inspect/shared` - `npm test -w @open-inspect/web` (606 tests) - `npm run typecheck -w @open-inspect/web` - `npm run lint -w @open-inspect/web` - `npx prettier --check packages/web/src/lib/time.ts packages/web/src/lib/time.test.ts packages/web/src/components/automations/automations-list.tsx packages/web/src/components/automations/automations-list.test.tsx` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added clearer “Next” timing labels for scheduled automations, showing future run times like “in 2h” or “in 1d”. * Introduced improved future-time formatting for consistent countdown-style display. * **Tests** * Expanded automated coverage for time formatting and scheduled automation timing display. * Added deterministic timer-based tests to verify past and future relative time outputs. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## What changed - preserve caller-supplied `event` fields in structured JSON log output - keep logger-owned envelope fields protected from context and per-call data - document precedence across base context, child context, and per-call data - add focused coverage for emitted events, protected fields, precedence, and child loggers ## Why Control-plane callers attach stable event names such as `automation.created` for log indexing and queries, but those fields were silently missing from emitted JSON. ## Root cause The shared logger included `event` in its reserved-key set, so it stripped the field from both context and per-call data. Unlike the other reserved keys, the logger never added an `event` value back to the output envelope. ## Checks - `npm test -w @open-inspect/shared` (25 files, 364 tests) - `npm run build -w @open-inspect/shared` - `npm run typecheck -w @open-inspect/shared` - `npm run lint -w @open-inspect/shared` - `npx prettier --check packages/shared/src/logger.ts packages/shared/src/logger.test.ts` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Structured logs now preserve user-provided `event` fields instead of stripping them. * Logging output more reliably keeps custom context values while still protecting core log fields from being overwritten. * Child loggers now correctly inherit parent context and allow later values to override matching fields. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…Murray#935) ## Summary Phase 3 slice: GitHub bot environment targets (design §13.2). A repository can name a default environment in its repo metadata (`repo_metadata.default_environment_id`); PR reviews and @mention sessions triggered from that repo then open the environment's full multi-repository workspace — a PR comment on `backend` can spin up the whole full-stack workspace. Follows the Phase 3 pattern established by the Slack (ColeMurray#924/ColeMurray#927/ColeMurray#928) and Linear (ColeMurray#931) slices: **targets unify instead of migrate** — repo-bound sessions stay the default and the fallback. ## Control plane - **Migration 0036**: `repo_metadata.default_environment_id TEXT` (nullable opt-in). - `RepoMetadata.defaultEnvironmentId` (shared type) + `RepoMetadataStore` read/write + accepted by `PUT /repos/:owner/:name/metadata` — configured the same way `channelAssociations` is today (API; no web UI for repo metadata exists yet). ## GitHub bot - New `session-target.ts` owns launch-target resolution: fetch the trigger repo's metadata → fetch the environment → require the environment to **contain the trigger repo** (the session must be able to check out the PR under review) → launch with `{environmentId}`; every failure path fails open to the repo-bound session with a `target.*` warning log (deleted environment, membership lost, fetch/parse errors). - `createSession` takes mutually exclusive target fields (`{repoOwner, repoName} | {environmentId}`), spread into the create body — same contract the Linear bot uses. - Integration settings, enabled-repos gating, and permission checks are untouched: they always resolve from the trigger repository. ## Testing - github-bot: 5 new tests (environment launch, deleted-environment fallback, non-member fallback, metadata-failure fallback, case-insensitive membership); existing assertions moved from call-index to URL-based lookup since handlers now make a metadata lookup before session create. 120 pass. - control-plane: store round-trip + clear-on-omit tests; unit 1687 pass, integration 518 pass (migration 0036 applied). - Full repo typecheck + lint clean; slack-bot 227, linear-bot 131, web 599 pass. ## Docs - github-bot README: new "Session Launch Target" section. - docs/HOW_IT_WORKS.md: bots paragraph updated. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * GitHub-bot sessions can now launch a repository’s configured default environment (opt-in via repo metadata), instead of always starting in the repository. * If the environment is missing, no longer includes the trigger repository, or the sender lacks required access, sessions automatically fall back to repo-bound targeting. * **Bug Fixes** * Improved session targeting so review and comment-triggered sessions consistently use the correct launch location. * **Documentation** * Updated GitHub-bot documentation to describe the new session launch target behavior and fallback rules. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## What - Show `No matching sessions` when a non-empty sidebar search has no local matches. - Restore the session list when the query is cleared. - Keep genuine empty-session and fetch-failure messages distinct. - Add public-UI regression coverage for all three states. ## Why Searching for a session with no matches left the session panel blank, giving users no indication that the search completed successfully. ## Root cause The sidebar filtered sessions into its active and inactive lists, but the empty-state render branch checked the unfiltered `sessions.length`. When sessions existed but none matched the query, neither the normal empty state nor any session rows rendered. ## Checks - `npm run build -w @open-inspect/shared` - `npm test -w @open-inspect/web -- src/components/session-sidebar.test.tsx` (12 passed) - `npm test -w @open-inspect/web` (602 passed) - `npm run typecheck -w @open-inspect/web` - `npm run lint -w @open-inspect/web` - `npx prettier --check packages/web/src/components/session-sidebar.tsx packages/web/src/components/session-sidebar.test.tsx` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved the sessions sidebar so an active search with no results shows “No matching sessions”. * Kept “No sessions yet” for the genuine empty state (when there are no sessions). * Preserved the “Unable to load sessions” message when sessions fail to load, even after entering a search. * **Tests** * Added coverage for search/no-results, true empty state, and load-error scenarios in the sessions sidebar. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - add GPT-5.6 Sol, Terra, and Luna to the shared model registry, default-enabled OpenAI models, and model picker - allow all three models through the OpenAI OAuth proxy and support matching Linear `model:*` labels - document the new models and cover validation, reasoning, label routing, and OAuth allowlisting ## Compatibility notes - models.dev added all three OpenAI models on 2026-07-09 with 1.05M context windows and 128K output limits - OpenCode loads the model catalog dynamically from models.dev, so no hard-coded OpenCode catalog change is required - Open-Inspect's OAuth proxy has its own model allowlist, which must be updated so the new catalog entries are retained in ChatGPT-OAuth sandboxes - reasoning options stop at `xhigh` because both the repository's pinned OpenCode 1.14.41 runtime and current OpenCode 1.17.18 expose GPT-5.2+ through `xhigh`; this avoids offering the models.dev `max` option before OpenCode supports it - the OpenCode dependency remains pinned at 1.14.41 because newer releases break the legacy SSE event stream used by Open-Inspect (tracked in ColeMurray#567) ## Validation - `npm run build -w @open-inspect/shared` - `npm test -w @open-inspect/shared` (364 tests) - `npm test -w @open-inspect/linear-bot -- src/__tests__/pure-functions.test.ts` (33 tests) - `uv run --project packages/sandbox-runtime --extra dev pytest packages/sandbox-runtime/tests/test_codex_auth_plugin_setup.py -q` (4 tests) - `npm run lint` - `npm run typecheck` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for additional OpenAI GPT 5.6 variants across model selection, label resolution, and authentication checks. * These new models now appear in the model picker and are available by default for new setups. * **Bug Fixes** * Improved handling of GPT 5.6 model labels so they resolve correctly in related workflows. * Updated supported-model validation and reasoning settings to better match the new model variants. * **Documentation** * Refreshed the available models reference to include the latest GPT 5.6 options and updated descriptions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - remove GPT-5.2 and GPT-5.2 Codex from shared model metadata and default-enabled models - remove both variants from the Codex OAuth allowlist and Linear model labels - refresh model examples, fixtures, and documentation to supported OpenAI models - verify legacy GPT-5.2 values are rejected and fall back safely ## Why The Codex backend no longer supports the GPT-5.2 variants. Keeping them selectable caused new sessions and integration overrides to target unavailable models. ## Impact GPT-5.2 models no longer appear in model selectors or integration aliases. Existing stored preferences that reference them are treated as unsupported and resolve through the existing default-model fallback. ## Validation - `npm run build -w @open-inspect/shared` - focused Vitest suites for shared, Linear, Slack, and web (84 tests) - sandbox-runtime Codex auth plugin tests (5 tests) - type checks for shared, Linear, Slack, web, and control plane - ESLint for shared, Linear, Slack, web, and control plane - Ruff and Prettier checks for changed files <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Updated supported model lists to include newer GPT 5.4/5.5/5.6 variants and GPT 5.3 Codex options. * **Bug Fixes** * Removed outdated GPT 5.2 model references so unsupported models are no longer shown or selected by default. * Improved model fallback behavior when a saved model is no longer available. * **Documentation** * Refreshed setup and usage docs to match the current supported model set. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary Automations gain **environment targets** (multi-repo sessions design §13.3, extended to fan-out): an automation's selection may now name environments (`environmentIds`) alongside repositories, up to 10 targets combined. Each firing fans out **one run per target** — a repository run works that repository in its own session (unchanged), while an environment run launches one session opening that environment's full workspace. So an automation can run on Env A, Env B, and repo C in a single firing. > Scope note: this PR originally shipped a scalar single-environment binding (`environmentId`, mutually exclusive with repositories). It was reworked in place to the list/fan-out model in d3d35a6 after a requirements clarification — fan-out across environments (Env A, Env B, …) is expected, and mixing repository and environment targets falls out of the same model. ## How it works **Storage** — migration `0037`: new `automation_environments` table (the live environment selection, twin of `automation_repositories`) and `automation_runs.environment_id` (the firing-time target snapshot, twin of the `repo_*` snapshot columns, with a per-invocation uniqueness index). The scalar `automations.environment_id` reserved in 0033 goes back to being inert — never read or written. **API** — `POST/PUT /automations` accept `environmentIds: string[]` (update replaces wholesale; empty clears). Validation: id shape + existence (missing environments named in the 400), duplicate rejection, and combined target-count rules — repo-scoped `github_event`/`linear_event` triggers take exactly one repository and no environments; more than one target requires a schedule trigger; 10 targets combined max. On update, count rules validate the final state (replacement plus the other side's existing rows) only when a selection is touched, so stored selections predating a rule can never brick unrelated edits. **Firing pipeline** — `startInvocation` builds one child per resolved repository (unchanged, including per-repo pre-fail) plus one child per selected environment, snapshotting the environment id on the run row. No targets → one repo-less child, as before. Invocation status/accounting derive from the children with no new code; the tick's child-budget estimate counts both selections. **Launch** — `automation/session-target.ts` resolves what a run's session opens from the run row alone: environment runs resolve their environment exactly like the session-create route (all-or-nothing at launch; the session snapshots the member list per §7.6, primary mirrored to scalars), repository and repo-less runs pass their snapshot through. Environment resolution failures (deleted environment, revoked member access) fail that child through the existing launch-failure path — sibling targets are unaffected. Settings resolve via the canonical `resolveSessionScopedSettings` (primary member, design §6.2). **Web** — the automation form's target popover gains an "Environments" section (hidden for repo-scoped triggers): single-select mode picks one target of either kind; multi-select mode mixes repository and environment checkboxes under the combined cap. Cards and the detail page label targets via a shared `formatAutomationTargetsLabel` ("2 repositories + 1 environment"). Run history renders environment runs with the environment's name; a fan-out row says "N targets" when environments are involved. ## Known limitation (noted, not addressed) `GET /automations?repoOwner=…` filters via `automation_repositories`, so environment-targeted automations don't match repo filters even when an environment contains that repo. The filter currently has no consumer; extending it is a follow-up if one appears. ## Validation - shared 364, control-plane unit 1708 + integration 519 (real-D1 environment-selection round-trip, migration applied), web 610, slack-bot 227, github-bot 123, linear-bot 131 - lint + typecheck clean across all packages Design refs: docs/multi-repo-sessions-design.md §13.3 (extended per updated requirements), §7.6 (snapshot semantics); docs/MULTI_REPO_AUTOMATIONS.md decision updated. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Automations can now target environments alongside repositories, including mixed selections. * Environment-aware sessions/workspaces are created per selected environment, and run history displays environment-target outcomes. * Automation details, lists, forms, and editing now render environment-inclusive target labels. * **Bug Fixes** * Improved create/update validation for combined repository+environment target limits and trigger compatibility. * Environment selections are now persisted, correctly mapped into automation data, and reliably preserved/cleared across create/edit flows. * **Documentation** * Updated multi-repo automation and automations documentation to reflect environment fan-out behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Terraform Validation Results
Pushed by: @colemurray-31531, Action: |
Terraform Plan ResultsStatus: ✅ Success Show Planterraform_data.access_control_gate: Refreshing state... [id=4f38135f-b85d-3c66-bc4d-62e6bd696d0f]
data.external.modal_source_hash[0]: Reading...
null_resource.slack_bot_build[0]: Refreshing state... [id=8218184084727750027]
null_resource.control_plane_build: Refreshing state... [id=2068601036937546848]
module.modal_app[0].null_resource.modal_secrets[0]: Refreshing state... [id=5762703892982030197]
module.slack_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=b9e2c38df71f4855be972c58b6a1ee8d]
cloudflare_d1_database.main: Refreshing state... [id=f39968f8-149f-4efd-ba33-ba502ffd6f9d]
module.session_index_kv.cloudflare_workers_kv_namespace.this: Refreshing state... [id=58ad1907c80c44b1b2ecbdfe268bcac5]
cloudflare_r2_bucket.media: Refreshing state... [id=open-inspect-media-contractor]
null_resource.linear_bot_build[0]: Refreshing state... [id=7605461394166201776]
module.linear_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=03d1b14a4d12477f81cfcc3e4af77cf9]
data.external.modal_source_hash[0]: Read complete after 0s [id=-]
module.modal_app[0].null_resource.modal_deploy: Refreshing state... [id=7520787001111393755]
module.web_app[0].vercel_project.this: Refreshing state... [id=prj_0jIgFgS1feKoUhF8wrcP4TtVTPuM]
module.slack_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=393472842b684911bc95603990b37de0]
module.linear_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=437dba8e03dd48cbbf55638723ad9074]
null_resource.d1_migrations: Refreshing state... [id=4884201322476132031]
module.slack_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=5ae51c15-9895-4c87-bfe1-798a81cc53a7]
module.web_app[0].vercel_project_environment_variable.env[1]: Refreshing state... [id=qCP45SaeSAHKBDwG]
module.web_app[0].vercel_project_environment_variable.env[15]: Refreshing state... [id=c9JZitmNRb7At0uS]
module.web_app[0].vercel_project_environment_variable.env[7]: Refreshing state... [id=phHvYylvduIAtRD9]
module.web_app[0].vercel_project_environment_variable.env[13]: Refreshing state... [id=BQ6FduHlAYzIYzLE]
module.web_app[0].vercel_project_environment_variable.env[17]: Refreshing state... [id=7nrNLHbYGvbPQPgC]
module.web_app[0].vercel_project_environment_variable.env[4]: Refreshing state... [id=XMc4F44ltfL3QK3t]
module.web_app[0].vercel_project_environment_variable.env[12]: Refreshing state... [id=Ap5I5tGTeNlwbrsh]
module.web_app[0].vercel_project_environment_variable.env[14]: Refreshing state... [id=lNKiGcjHdNjbHB4E]
module.web_app[0].vercel_project_environment_variable.env[3]: Refreshing state... [id=5nuLo1tvZdpqcXJR]
module.web_app[0].vercel_project_environment_variable.env[16]: Refreshing state... [id=bjIku856byjGpqDu]
module.web_app[0].vercel_project_environment_variable.env[8]: Refreshing state... [id=9WvoP0YuLFeFgkd0]
module.web_app[0].vercel_project_environment_variable.env[5]: Refreshing state... [id=LO7XAE8j1pNTY7Jd]
module.web_app[0].vercel_project_environment_variable.env[0]: Refreshing state... [id=7aTLYrxQVXP0SSTu]
module.web_app[0].vercel_project_environment_variable.env[18]: Refreshing state... [id=gx7cq4XYtx7XFyxH]
module.web_app[0].vercel_project_environment_variable.env[2]: Refreshing state... [id=TYuSgPHntfm46xtc]
module.web_app[0].vercel_project_environment_variable.env[6]: Refreshing state... [id=T8zVCcakiFWjGZBZ]
module.web_app[0].vercel_project_environment_variable.env[10]: Refreshing state... [id=sqeMnRMF0xd8CIRe]
module.web_app[0].vercel_project_environment_variable.env[11]: Refreshing state... [id=DrDtGPyyny9UCMp4]
module.web_app[0].vercel_project_environment_variable.env[9]: Refreshing state... [id=KcvEppk5QtWTRvpH]
module.linear_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=72081a9d-c2ef-48e6-a825-868aececb722]
module.slack_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=55c2eb1e-ec67-40b7-8e5a-c8b0eb0e6c07]
module.linear_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=88ed99ac-30b6-408e-9228-f9d843b8f32e]
module.control_plane_worker.cloudflare_worker.this: Refreshing state... [id=fd3805c0f7cb454e8165d9959be40dad]
module.control_plane_worker.cloudflare_worker_version.this: Refreshing state... [id=f22fa5a2-851e-4eab-89f1-e3b85482067c]
module.control_plane_worker.cloudflare_workers_deployment.this: Refreshing state... [id=81f350bd-83f4-4ddb-ade1-08002add0372]
module.control_plane_worker.cloudflare_workers_cron_trigger.this[0]: Refreshing state... [id=open-inspect-control-plane-contractor]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
~ update in-place
-/+ destroy and then create replacement
Terraform will perform the following actions:
# null_resource.control_plane_build must be replaced
-/+ resource "null_resource" "control_plane_build" {
~ id = "2068601036937546848" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-06-30T19:03:37Z" -> (known after apply)
}
}
# null_resource.d1_migrations must be replaced
-/+ resource "null_resource" "d1_migrations" {
~ id = "4884201322476132031" -> (known after apply)
~ triggers = { # forces replacement
~ "migrations_sha" = "d24a5e6e436c9a30ddcff3e083c349fdd25f5f079853fac1a7e526fc4b697e74" -> "a5e33d18bfbcf570fbafe720af71701a0e94188e464a0592fef8612a63d6e439"
# (1 unchanged element hidden)
}
}
# null_resource.linear_bot_build[0] must be replaced
-/+ resource "null_resource" "linear_bot_build" {
~ id = "7605461394166201776" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-06-30T19:03:37Z" -> (known after apply)
}
}
# null_resource.slack_bot_build[0] must be replaced
-/+ resource "null_resource" "slack_bot_build" {
~ id = "8218184084727750027" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-06-30T19:03:37Z" -> (known after apply)
}
}
# terraform_data.cloudflare_custom_domain_gate will be created
+ resource "terraform_data" "cloudflare_custom_domain_gate" {
+ id = (known after apply)
}
# module.control_plane_worker.cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "fd3805c0f7cb454e8165d9959be40dad"
name = "open-inspect-control-plane-contractor"
~ observability = {
~ logs = {
+ destinations = (known after apply)
# (4 unchanged attributes hidden)
}
~ traces = {
+ destinations = (known after apply)
# (3 unchanged attributes hidden)
}
# (2 unchanged attributes hidden)
}
~ references = {
~ dispatch_namespace_outbounds = [] -> (known after apply)
~ domains = [] -> (known after apply)
~ durable_objects = [
- {
- namespace_id = "e84875aa1fef447bb067596a2dbba9d6" -> null
- namespace_name = "open-inspect-control-plane-contractor_SessionDO" -> null
- worker_id = "fd3805c0f7cb454e8165d9959be40dad" -> null
- worker_name = "open-inspect-control-plane-contractor" -> null
},
- {
- namespace_id = "a4717e3e330545deab51e1c9b65d7c83" -> null
- namespace_name = "open-inspect-control-plane-contractor_SchedulerDO" -> null
- worker_id = "fd3805c0f7cb454e8165d9959be40dad" -> null
- worker_name = "open-inspect-control-plane-contractor" -> null
},
] -> (known after apply)
~ queues = [] -> (known after apply)
~ workers = [
- {
- id = "393472842b684911bc95603990b37de0" -> null
- name = "open-inspect-slack-bot-contractor" -> null
},
- {
- id = "437dba8e03dd48cbbf55638723ad9074" -> null
- name = "open-inspect-linear-bot-contractor" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-06-30T19:03:39Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.control_plane_worker.cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
~ annotations = {
+ workers_message = (known after apply)
+ workers_tag = (known after apply)
~ workers_triggered_by = "create_version_api" -> (known after apply)
} -> (known after apply)
~ bindings = (sensitive value) # forces replacement
~ created_on = "2026-06-30T19:03:39Z" -> (known after apply)
~ id = "f22fa5a2-851e-4eab-89f1-e3b85482067c" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
~ migration_tag = "v1" -> (known after apply)
~ modules = [
- { # forces replacement
- content_file = "../../..//packages/control-plane/dist/index.js" -> null
- content_sha256 = "2458cefe96118df3fc1e41a6be46df2645eaeb41233d8862961585025380a11f" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/control-plane/dist/index.js"
+ content_sha256 = "c90b226b333ff46b2b3765be8fede9fc5b029fbe835ada31706cf1732e57673c"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 31 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 24 -> (known after apply)
~ urls = [] -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.control_plane_worker.cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
~ annotations = {
+ workers_message = (known after apply)
~ workers_triggered_by = "deployment" -> (known after apply)
} -> (known after apply)
~ author_email = "kyle.adams.ic@companycam.com" -> (known after apply)
~ created_on = "2026-06-30T19:03:41Z" -> (known after apply)
~ id = "81f350bd-83f4-4ddb-ade1-08002add0372" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "f22fa5a2-851e-4eab-89f1-e3b85482067c" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
# module.linear_bot_worker[0].cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "437dba8e03dd48cbbf55638723ad9074"
name = "open-inspect-linear-bot-contractor"
~ observability = {
~ logs = {
+ destinations = (known after apply)
# (4 unchanged attributes hidden)
}
~ traces = {
+ destinations = (known after apply)
# (3 unchanged attributes hidden)
}
# (2 unchanged attributes hidden)
}
~ references = {
~ dispatch_namespace_outbounds = [] -> (known after apply)
~ domains = [] -> (known after apply)
~ durable_objects = [] -> (known after apply)
~ queues = [] -> (known after apply)
~ workers = [
- {
- id = "fd3805c0f7cb454e8165d9959be40dad" -> null
- name = "open-inspect-control-plane-contractor" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-06-30T19:03:37Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.linear_bot_worker[0].cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
~ annotations = {
+ workers_message = (known after apply)
+ workers_tag = (known after apply)
~ workers_triggered_by = "create_version_api" -> (known after apply)
} -> (known after apply)
~ bindings = (sensitive value) # forces replacement
~ created_on = "2026-06-30T19:03:38Z" -> (known after apply)
~ id = "72081a9d-c2ef-48e6-a825-868aececb722" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
+ migration_tag = (known after apply)
~ modules = [
- { # forces replacement
- content_file = "../../..//packages/linear-bot/dist/index.js" -> null
- content_sha256 = "7e34053075e9e1e28a8763e39da6bc3090f801984d8c8114654e947b7feb107a" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/linear-bot/dist/index.js"
+ content_sha256 = "e19cbaff12e9b6c79aaad7fadeba94e7fa799c6e0104f40ec3988e0a3884fb9b"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 31 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 21 -> (known after apply)
~ urls = [
- "https://72081a9d-open-inspect-linear-bot-contractor.companycam.workers.dev",
] -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.linear_bot_worker[0].cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
~ annotations = {
+ workers_message = (known after apply)
~ workers_triggered_by = "deployment" -> (known after apply)
} -> (known after apply)
~ author_email = "kyle.adams.ic@companycam.com" -> (known after apply)
~ created_on = "2026-06-30T19:03:38Z" -> (known after apply)
~ id = "88ed99ac-30b6-408e-9228-f9d843b8f32e" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "72081a9d-c2ef-48e6-a825-868aececb722" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
# module.modal_app[0].null_resource.modal_deploy must be replaced
-/+ resource "null_resource" "modal_deploy" {
~ id = "7520787001111393755" -> (known after apply)
~ triggers = { # forces replacement
~ "source_hash" = "ee48b7750021443e5bcf71517b67a8b93551f952beda7ee19ce3902fcc763f14" -> "6507c9711d0503ede4e6e11167633451b9d29a1f660dfad0091ed5f1e5d8931a"
# (3 unchanged elements hidden)
}
}
# module.slack_bot_worker[0].cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "393472842b684911bc95603990b37de0"
name = "open-inspect-slack-bot-contractor"
~ observability = {
~ logs = {
+ destinations = (known after apply)
# (4 unchanged attributes hidden)
}
~ traces = {
+ destinations = (known after apply)
# (3 unchanged attributes hidden)
}
# (2 unchanged attributes hidden)
}
~ references = {
~ dispatch_namespace_outbounds = [] -> (known after apply)
~ domains = [] -> (known after apply)
~ durable_objects = [] -> (known after apply)
~ queues = [] -> (known after apply)
~ workers = [
- {
- id = "fd3805c0f7cb454e8165d9959be40dad" -> null
- name = "open-inspect-control-plane-contractor" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-06-30T19:03:38Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.slack_bot_worker[0].cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
~ annotations = {
+ workers_message = (known after apply)
+ workers_tag = (known after apply)
~ workers_triggered_by = "create_version_api" -> (known after apply)
} -> (known after apply)
~ bindings = (sensitive value) # forces replacement
~ created_on = "2026-06-30T19:03:38Z" -> (known after apply)
~ id = "5ae51c15-9895-4c87-bfe1-798a81cc53a7" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
+ migration_tag = (known after apply)
~ modules = [
- { # forces replacement
- content_file = "../../..//packages/slack-bot/dist/index.js" -> null
- content_sha256 = "1a08aca56f98ecdd7ac8af52f19ff76dedcd282e439d8120465971a4157fcf9c" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/slack-bot/dist/index.js"
+ content_sha256 = "15ae157488e63d6dbfa88dcb9dff0e07eda3764d876ac1c927ab5e5bcb9f6ba0"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 31 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 30 -> (known after apply)
~ urls = [
- "https://5ae51c15-open-inspect-slack-bot-contractor.companycam.workers.dev",
] -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.slack_bot_worker[0].cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
~ annotations = {
+ workers_message = (known after apply)
~ workers_triggered_by = "deployment" -> (known after apply)
} -> (known after apply)
~ author_email = "kyle.adams.ic@companycam.com" -> (known after apply)
~ created_on = "2026-06-30T19:03:39Z" -> (known after apply)
~ id = "55c2eb1e-ec67-40b7-8e5a-c8b0eb0e6c07" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "5ae51c15-9895-4c87-bfe1-798a81cc53a7" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
Plan: 12 to add, 3 to change, 11 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"Pushed by: @colemurray-31531 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sync fork with upstream ColeMurray/background-agents
Merges 67 upstream commits (through 07bc46c) into our main via a merge commit. The merge is conflict-free and the fork-only user-defined PR labels feature (#15) is preserved intact.
Verification
Notable upstream changes
Merge details