chore: sync upstream 2026-09-29 (700f9145..e471cff4) - #80
Conversation
…oleMurray#2117) ## Summary - Remove the fixed 160px desktop width from session titles and rename inputs so they use available header space. - Keep the title and repository label responsive when the window is narrow. - Add a regression test for the desktop header layout and rename field. ## Verification - `npm test -w @open-inspect/web` (226 files, 1,980 tests passed) - `npm run lint -w @open-inspect/web` - `npm run typecheck -w @open-inspect/web` - `git diff --check` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/9fabc27d2a3552ca949adecac0c09e55)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Session titles and repository labels can use more available horizontal space on medium and larger screens. * Repository labels now truncate when space is limited. * Header items have more spacing at medium screen widths. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary - Reduce `manager.py` from 722 to 294 lines (59%) by extracting concrete `SandboxLauncher` and `SandboxTunnels` collaborators, plus shared configuration/handle records. - Compute service-port ownership once for encrypted ports, runtime environment, URL routing, and best-effort tunnel publication. - Preserve public manager imports/signatures, image selection/error classification, environment precedence, legacy restore credentials, snapshot deadlines, and termination behavior. - Review follow-up: explicitly reject Boolean extra tunnel ports, restore all 17 legacy manager constant exports, and name retry defaults. ## Design | Component | Responsibility | | --- | --- | | `SandboxManager` | Create/restore normalization and logging; snapshot, lookup, termination | | `SandboxLauncher` | Image resolution, environment/credentials, resources, Modal create, handle assembly | | `SandboxTunnels` | Port ownership, runtime port settings, URL resolution/retries, tunnel-file publication | | `models.py` | Shared launch configuration and returned handle | Launch execution flows from manager to launcher to tunnels; compatibility exports reference the canonical constants. Collaborators never import the manager. No generic interfaces, provider registry, new lifecycle authority, or private forwarding wrappers. Across the four production files, total size increases by 65 lines for explicit module boundaries, exports, and named tunnel results. ## Verification - Baseline: 257 Modal tests passed before changes. - Python 3.12: `uv run --extra dev pytest tests/ -q` — **299 passed**. - `uv run --extra dev ruff check src/ tests/` — passed. - `uv run --extra dev ruff format --check src/ tests/` — passed. - `git diff --check` — passed. - `uv run --extra dev mypy src/`: unchanged base has 20 diagnostics; this branch has 15, all in unchanged files (`clone_token.py`, `build_session.py`, `web_api.py`). No diagnostics in the changed/new modules. - Launch matrix exercises real manager/launcher/tunnel composition with Modal I/O mocked. Added coverage for missing versus transient image lookup/spawn errors, absence of fallback/retry, partial/exhausted tunnel resolution, retry delays, and non-fatal file-write failures. - Added regressions for all 17 legacy manager constants and six create/restore cases rejecting Boolean tunnel ports without losing valid ports or consuming the port limit. No live Modal deployment or billable provider canary was performed. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Sandboxes can be launched from base images, repository images, or snapshots. * Launch results include available service connection URLs, credentials, and snapshot details. * Configure service and user tunnel ports for code-server, VNC, terminal access, and other services. * Tunnel URLs are resolved and made available through the sandbox environment. * Invalid and duplicate tunnel port settings are filtered out. * Existing sandbox allocations can be reused when their ownership matches. * **Bug Fixes** * Missing repository images are reported clearly. * Sandboxes remain available when tunnel resolution or environment-file updates fail. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…y#2115) ## Summary - Add an authenticated, lookup-only Modal VM resolution endpoint with typed allocation outcomes (`not_visible`, `other_generation`, `window_closed`, `race_pending`). It returns the owned generation's real VM ID, enabled access credentials, and tunnels without creating or retiring a VM. - Keep modal-vm create and restore generations alive after ambiguous startup outcomes. Retry resolution with a bounded backoff, claim the real handle through the existing startup path, and fail only on a definitive outcome or proven absence. - Reconcile pending handles asynchronously on bridge attach/readiness after a Durable Object restart. Generation- and pending-handle-guarded access writes replace the sandbox and shutdown handles without resetting the recorded lifetime; terminal access remains unavailable after restart when the auth token was lost. - Document the resolve contract and add Modal, provider, lifecycle, client, and repository regression tests. ## Reserved environment variables - VM launch now reserves `CODE_SERVER_PASSWORD`, `CODE_SERVER_PORT`, `TTYD_PROXY_PORT`, and `EXPECTED_TUNNEL_PORTS`. User environment variables and repository secrets with those names no longer reach the sandbox as user-provided values; resolution reads the VM-owned values to recover access. ## Verification - Control-plane unit tests: 330 files, 5,303 tests passed before final targeted tests; final focused recovery suites: 213 tests passed. - Control-plane integration tests: 120 files, 1,432 passed, 1 skipped (`--maxWorkers=2`). - Control-plane typecheck and lint passed. - Modal infra tests: 369 passed; Ruff check and format check passed. - Prettier check and `git diff --check` passed. Refs COL-237. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/11c07cec2c20ec539a74a8e8b3fbaff7)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * VM sandboxes can be looked up when a launch response is unavailable, returning connection details so startup can continue. * During bridge reconnection, access details for a running VM can be restored without changing its existing lifetime. * Resolved sandbox details can include enabled service access information and available tunnel mappings. * Added an authenticated lookup endpoint for resolving VM sandboxes by session and sandbox ID. Lookup does not create or stop allocations. * Older or incomplete launch metadata may limit lookup results to the VM’s object ID. * **Bug Fixes** * Startup retries temporary errors and delayed VM visibility while preventing results from being applied to replaced or outdated sandboxes. * Missing, mismatched, or unavailable VM allocations now return more specific errors. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…rray#2118) ## Summary - Gate all 35 active-user session item routes on the persisted session row, with private access enforced in every mode and `TEAMS_ENFORCEMENT` defaulting to `shadow`. - Centralize per-session admission for item and batch routes. The existing actorless route grant authorizes the action after the service viewer passes visibility; private sessions remain concealed. Child summary and cancel routes check both the parent and child rows. - Emit one response-time shadow audit decision with the actual HTTP status, including batch target observations. Record every permitted private Owner break-glass read before later checks; classify and label it as an audit operation. - Preserve legacy bulk-only custom-role access in `off` and `shadow`; keep the private snapshot sandbox redaction in all modes. Cache mode and memberships per request, validate the Node flag at startup, and return 503 for invalid Worker flag values. Issue: https://linear.app/colemurray/issue/COL-198/teams-pr-5-control-plane-session-route-requirement-on-every-sessionsid ## Checkpoint **Validation (latest commit):** - `npm run build -w @open-inspect/shared`: passed. - `npm run typecheck`: passed across all workspaces. - `npm run lint:fix`, `npm run format:check`: passed. - `npm run lint:sql-portability`: clean (24 baselined occurrences across 4 files). - `npm run lint:complexity`: report-only, command passed. - `npm test -w @open-inspect/control-plane`: 330 files, 5,301 tests passed. - `npm run test:integration -w @open-inspect/control-plane`: 121 files, 1,455 tests passed, 1 skipped. Existing forced-eviction and NDJSON diagnostics remain but the command exits successfully. - `npm test -w @open-inspect/web`: 226 files, 1,977 tests passed. - Targeted shared audit and session-access tests: 57 passed. **Red checkpoints fixed during implementation (verbatim excerpts):** ``` AssertionError: GET /sessions/:id/children/:childId: expected false to be true // Object.is equality AssertionError: expected 500 to be 503 // Object.is equality AssertionError: expected [ { …(2) }, { …(2) } ] to have a length of 1 but got 2 ``` The first full integration run also found an older RBAC fixture that lacked a parent row despite asserting a create-permission denial. Session-first admission correctly returned 404; the test now seeds its parent: ``` FAIL test/integration/rbac-routes.test.ts > RBAC routes > requires sessions.create in addition to parent collaboration when spawning a child AssertionError: expected 404 to be 403 // Object.is equality ``` The frozen catalog snapshot was refreshed for the child requirement declarations. All suites subsequently passed. **Main drift:** `SessionIndexStore.get` returns a validated `SessionEntry` rather than `SessionRow` (`session-index.ts:393-397`); admission maps it to the shared access row. The shared resolver is exported at the package root rather than through a `types/session-access` subpath. Single-export registration moved to `session-export.ts:351-368` on the current main; the route inventory still contains 35 active-user and 10 `none` session item routes. **Deliberately excluded:** No D1 or DO migration. Bulk `/sessions/export` scoping belongs to PR6, WebSocket subscribe/command checks to PR7, and child-list filtering plus visibility/collaborator creation endpoints to PR8. Batch `skipped` is a wire change to a strict shared response schema; no consumer parses that response body today. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added configurable team access enforcement with `off`, `shadow` (the default), and `on` modes. Private sessions remain restricted in every mode, with authorized break-glass access recorded in the audit log. * With enforcement enabled, team membership, role, and session collaboration determine access. Deleting a session requires its owner, a team lead, or a workspace administrator. * Shadow mode records access decisions that enforcement would deny while preserving legacy access behavior. * Batch archive responses now list sessions skipped because they were not found or the requester lacked permission. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary - Add `anthropic/claude-sonnet-5-5` to the shared model catalog, model picker, and documentation, with low through max adaptive-thinking efforts and a high default. - Upgrade the Claude Agent SDK pin from 0.2.158 to 0.2.161 (bundled Claude Code 2.1.284, which adds Sonnet 5.5), regenerate runtime and Modal locks, and raise only the Claude harness prepared-image floor to generation 74. - Extend Claude harness and OpenCode reasoning-contract coverage for the new model. OpenCode 1.18.29 resolves it via the refreshed model registry; no OpenCode binary change is needed. ## Verification - `npm run build -w @open-inspect/shared` - `npm test -w @open-inspect/shared` (1,052 passed) - `npm run typecheck` and `npm run lint` - `uv run --frozen --project packages/sandbox-runtime --extra dev pytest packages/sandbox-runtime/tests -q` (1,390 passed, 3 skipped) - OpenCode wire contract with `OPENCODE_TEST_BINARY` (3 passed, fake provider, no live Anthropic request) - Modal manifest and sandbox image bundle tests, sandbox image lock check, Ruff and Prettier checks No authenticated Anthropic model call or provider-native image build was run. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/aabcae601ce9b9f80642bb30543f7947)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Claude Sonnet 5.5 is now available in the model picker and integrations, with adaptive thinking controls from low to max. * Claude Sonnet 5 is listed with adaptive thinking options, and model documentation includes updated reasoning settings. * **Documentation** * Updated model guides and release information with Claude Sonnet 5.5 availability and model details. * Clarified that older sandbox images or resumed sessions may not support newer models. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…2130) Closes ColeMurray#1546 Sets persist-credentials:false on all 9 actions/checkout steps in .github/workflows/ci.yml (7 added, 2 already present). Verification: - python yaml.safe_load passes - 9 checkout steps, 9 persist-credentials:false - No push/release jobs affected, permissions contents:read <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Security** * Checkout credentials are no longer persisted across the affected lint, typecheck, build, and test jobs. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Disable the Bun fetch idle timeout for Codex OAuth proxied requests so OpenCode owns stalled-stream handling and retry classification. - Assert that the upstream fetch receives `timeout: false`. - Pin Modal to the tested 1.4.3 SDK version: CI otherwise installs 1.6.0, whose removal of endpoint introspection APIs breaks unrelated modal-infra tests. ## Validation - `node --test packages/sandbox-runtime/tests/codex-auth-plugin.test.mjs` (5 passed) - `bun test packages/sandbox-runtime/tests/codex-auth-plugin.test.mjs` (5 passed) - `uv run pytest tests/ -q` in `packages/modal-infra` (350 passed) - `uv lock --check` and `uv run ruff check src/ tests/` / `uv run ruff format --check src/ tests/` in `packages/modal-infra` passed. Long-stall OpenCode retry behavior was not tested end to end. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Proxied Codex authentication requests no longer use the default fetch timeout. Requests can continue beyond that timeout window rather than being interrupted by it, helping avoid unexpected timeouts when the authentication service takes longer to respond. This change applies specifically to requests forwarded through the Codex authentication proxy; other authentication behavior is unchanged. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - Adds an integration test for `TeamStore.update` default environment ownership rules. - Covers accepting a default environment owned by the team. - Covers rejecting cross-team and missing default environments while preserving the previous valid default. ## Why Team default environment selection is core configuration logic that gates which environment a team uses by default. Recent team/environment ownership changes had coverage for an empty invalid ID, but not the high-risk ownership invariant or the edge case that failed updates must not clear an existing default. ## Tests - `npm run test:integration -w @open-inspect/control-plane -- team-stores.test.ts` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/b336ed978fd99b8b47efe9ee2f266009)* Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…rray#2131) ## Summary - Filter `GET /sessions/:id/children` through the existing per-session admission evaluator before serializing each child. For user/service requests, children the viewer cannot see are omitted: private visibility applies in every mode, team visibility in `on`, and shadow mode records would-be denials. Preserve the existing parent-bound sandbox fallback behavior. - Narrow the CHANGELOG claim to active-user session item routes. Workspace-wide session lists, bulk export, and WebSocket authorization remain follow-up work; no route currently makes a session private or team-owned. Follow-up to merged PR ColeMurray#2118. Issue: https://linear.app/colemurray/issue/COL-198/teams-pr-5-control-plane-session-route-requirement-on-every-sessionsid ## Validation On this branch, freshly based on `main`: - `npm run build -w @open-inspect/shared`: passed. - `npm test -w @open-inspect/control-plane -- --run src/routes/session-children.test.ts`: 9 passed. - `npm run test:integration -w @open-inspect/control-plane -- --run test/integration/session-access-routes.test.ts test/integration/child-session-ops.test.ts test/integration/spawn-children.test.ts`: 61 passed. - `npm run typecheck`, `npm run lint:fix`, `npm run format:check`, and `npm run lint:sql-portability`: passed. The red integration case before the fix showed a visible parent listing a private child: ``` AssertionError: expected [ …(3) ] to deeply equal [ …(2) ] ``` The first full integration run also exposed a sandbox-fallback fixture failure, fixed by retaining the bound sandbox principal path. Full control-plane unit and integration suites passed before this one commit was cherry-picked onto updated `main` (5,301 unit tests; 1,456 integration tests, 1 skipped). Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…oleMurray#2073) A terminal-enabled Daytona session that stays stopped for more than 24 hours loses its workspace when it resumes. On resume the stored terminal token has expired, so the lifecycle manager deletes the resumed sandbox and spawns a fresh one from the repository. Daytona has no snapshots, so that stopped sandbox held the only copy of the working tree, including uncommitted changes and installed dependencies. A missing token has the same effect. That covers a create-time terminal preview failure and a stored token that no longer decrypts. The same condition on a retained-state recovery (`resume_retained`) puts the session on a recovery hold that retrying cannot clear. This change resumes the sandbox without terminal access instead: the terminal URL and token are not published, the same result as a create-time preview failure. Because those recoveries now get past the resume, it also stops a failed boot from deleting a retained source: a boot of the provider object that a retained recovery point names is held instead, whether it came from that recovery or from a later ordinary resume of the same object. On main that deletion already happens whenever the token is valid. ## Mechanism - `storeTtyd` mints the terminal JWT at spawn with a 24-hour expiry (`packages/control-plane/src/sandbox/lifecycle/manager.ts:97`, `:2377-2395`). The token is signed with the sandbox auth token, which the ttyd proxy verifies (`packages/sandbox-runtime/src/sandbox_runtime/ttyd_proxy/server.ts:2-5`). The control plane stores only a hash of that token (`manager.ts:710`), so it cannot mint a new terminal token for a resumed sandbox. - `resumeSandbox` sets `replaceForTerminalCredential` when the provider returns a terminal URL and the stored token is missing or expired (`manager.ts:1439`). For an ordinary resume it then calls `doSpawn(previousGeneration)` (`manager.ts:1482-1490`). `doSpawn` begins with `stopPriorProviderSandbox`, which stops the resumed provider object with intent `destroy` (`manager.ts:787`, `:1580-1622`). Daytona implements that intent as `deleteSandbox` (`packages/control-plane/src/sandbox/providers/daytona-provider.ts:222-224`) and declares `supportsSnapshots: false` (`daytona-provider.ts:86-92`), so nothing is kept. - Daytona resumes stopped and archived sandboxes (`daytona-provider.ts:188-195`) well past 24 hours, and it is the only persistent-resume provider that returns a terminal URL from `resumeSandbox` (`daytona-provider.ts:199-211`). - On a retained-state recovery the same condition calls `holdFailedRecovery` (`manager.ts:1440-1443`) after the provider has already started the sandbox. The "restore" recovery action resumes the same sandbox again (`packages/control-plane/src/session/sandbox-shutdown.ts:455-476`) and finds the same expired token, so the session stays on hold. - When a retained recovery's resume does commit, `recordProviderStartup` moves the shutdown state from `restoring` to `running` (`sandbox-shutdown.ts:267`) before the bridge reconnects, so the generic watchdogs run. If that boot then fails, the connect watchdog (`manager.ts:1814-1817`), the boot budget (`manager.ts:1951-1963`) and the fatal-runtime-error path (`manager.ts:2115`) stop the sandbox with intent `destroy`, and the next prompt spawns a fresh one. The missing/expired-token case never reached this on main because it stopped at the hold above. The replacement was added in ColeMurray#1988 so that a resumed terminal would never show a URL with an unusable token. This change still guarantees that: without a valid token, neither URL nor token is stored, and the UI hides the terminal. The difference is that the workspace is kept and the terminal is dropped. ## Fix - When the terminal credential is unusable, `resumeSandbox` logs `sandbox.resume_terminal_credential_unavailable` (now at warn), commits the resume with `ttyd: null`, and completes normally. This applies to both ordinary resumes and retained-state recovery. A valid token is handled as before: the refreshed URL is stored next to the preserved token. A later fresh spawn or snapshot restore mints a new token as it does today. - When a boot of the provider object a retained receipt names fails (connect timeout, boot budget or fatal runtime error), `SandboxShutdownCoordinator.holdFailedRetainedBoot` records that object as the provider handle and holds the session with `holdFailedRecovery`, as a failed retained resume already is. Recording the handle matters when the failure lands before the resume commits: an ordinary resume reserves its generation without one, and without it "restore saved" is not offered. It does not fence the row, send the runtime `shutdown`, or stop the sandbox. Skipping the fence matters: `fenceSandboxGeneration` clears the sandbox auth token hash (`packages/control-plane/src/session/sandbox-repository.ts:280-284`), and the next resume of the same sandbox needs it. "Restore saved" then preserve-stops the source and resumes it, and "discard" remains available. The receipt stays in the shutdown record across later generations, and a persistent resume keeps the object id, so this also covers a later ordinary resume of that object, which is still the only copy of the workspace. Generations running any other object fail as before. Not covered: a prompt or typing event can still replace a retained source stuck in `connecting`. `spawnSandbox` treats a row older than the 240 s spawn timeout as dead and `doSpawn` stops the prior object with intent `destroy`. That only happens between the connect-watchdog deadline, which is the same 240 s, and the alarm firing. Main has the same replacement for a `failed` row left by a resume error and for a `ready` row with no bridge. That is replacement admission for persistent-resume providers, which is left to a separate change. A terminal link issued earlier for the same sandbox is not revoked. The ttyd proxy checks only the JWT signature, against the sandbox's unchanged auth token, and its expiry, so the link keeps working until the JWT expires (24 hours after minting) or its signed preview URL does. Main already behaves this way for a running sandbox whose stored token no longer decrypts (the access response omits the terminal, `packages/control-plane/src/session/sandbox-access-reader.ts:39`, `:65`) and for the retained-recovery hold above, which leaves the resumed sandbox running. On main an ordinary resume ended such a link only by deleting the sandbox. ## Verification - `manager.test.ts`: - "keeps a resumed sandbox without terminal access when its terminal token is missing/expired" replaces the two cases that asserted replacement. On unmodified main both fail at `expect(stopSandbox).not.toHaveBeenCalled()` ("called 1 times"). - "keeps resuming a sandbox whose initial terminal preview could not be issued" replaces the test that asserted replacement after a create-time preview failure. On main it fails at `expect(createSandbox).toHaveBeenCalledOnce()` ("got 2 times"). - "resumes retained saved state without terminal access when its terminal token expired" is new. On main it fails at `expect(shutdown.holdFailedRecovery).not.toHaveBeenCalled()`. - "holds a resumed retained source after a connect timeout / fatal runtime error / boot budget instead of deleting it" is new. It runs the real shutdown coordinator with an expired terminal token and has no bridge report ready. Without the hold, the connect-timeout and boot-budget cases fail at `expect(stopSandbox).not.toHaveBeenCalled()` (called with `intent: "destroy"`), and the fatal case fails because `terminateFailedSandbox` reports that it took the sandbox down. - "keeps restore available after a later ordinary resume of the retained source fails mid-resume" is new. A fatal report lands while the resume call is still pending, and the test checks that "restore saved" is offered and resumes the same object. - All pass with the fix. "refreshes terminal URL after resume without replacing its token" (valid token) is unchanged and passes. - A throwaway test (not committed) with the real coordinator and `DaytonaSandboxProvider` over a mock REST client. On main, a retained recovery with a valid token calls `deleteSandbox` after a connect timeout, a boot budget or a fatal runtime error, and so does one with the terminal disabled after a connect timeout; the next prompt then creates a fresh sandbox. With this change none of those cases, and none of the missing/expired-token ones, calls `deleteSandbox`, and "restore saved" resumes the same sandbox. - `npm test -w @open-inspect/control-plane`: 329 files, 5,313 tests passed. `npm run test:integration -w @open-inspect/control-plane`: 120 files, 1,432 tests passed, 1 skipped. - `npm run typecheck -w @open-inspect/control-plane`, ESLint and Prettier on the changed files: clean. - Not run: a live Daytona session resumed after the token expired, or a live resume whose boot failed. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Saved workspaces are retained when a resume attempt fails, including after connection timeouts, boot-budget expiry, and fatal runtime errors. Recovery can be retried without replacing the retained sandbox. * Sandboxes now resume in place when terminal credentials are missing or expired, without triggering a replacement. Terminal access remains unavailable until valid credentials are provided. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…stop (ColeMurray#2081) When a turn is stopped, the session's token totals in D1 (`sessions.input_tokens` and the other token columns added in ColeMurray#2067) can stay below the session's actual step usage. `/sessions/export` and `/analytics/runs` then report lower totals than the session's own `step_usage` rows, which the export's `usage` collection reads from the session (`/internal/trace-export`). The difference lasts until another turn settles. If the session cost limit caused the stop and another prompt is queued, no turn settles while the budget stays exhausted, so the lower totals remain. ## Mechanism - A stop ends the turn in the control plane before the sandbox acts on it. `ExecutionStopCoordinator.prepare` marks the processing message failed (`packages/control-plane/src/session/execution-stop-coordinator.ts:43-56`). `deliver` then sends `stop` to the sandbox and calls `reconcileAfterExecution(false)` (`:67-72`). - With nothing queued, the session moves to `failed` and `syncSessionMetrics` writes the token totals at that point. - With a prompt queued, the session stays `active`. That is not a settled status, so nothing is written (`packages/control-plane/src/session/session-status-service.ts:227-233`, `:55-73`). - The turn keeps running until the sandbox processes `stop` (`packages/sandbox-runtime/src/sandbox_runtime/bridge.py:630-631`). A `step_finish` already on the socket, or emitted before the cancel, therefore arrives after the stop. If the cancel interrupts a `step_finish` send, the event is buffered again (`packages/sandbox-runtime/src/sandbox_runtime/event_forwarder.py:190-201`) and is only replayed on the next bind, so it arrives even later. - `SandboxStreamingEventHandler.handleStep` records every `step_finish` in `step_usage` and cost, whatever state its message is in, so the usage rows include the late step. - The sandbox's own `execution_complete` for the stopped turn finds no processing message. It logs `already_stopped` and does not reconcile (`packages/control-plane/src/session/sandbox-events/execution.handler.ts:67-77`, `:116-122`). The projection only refreshes when a transition settles a turn, so nothing rewrites D1 after the late step. - The step whose cost reaches the session limit triggers a budget stop (`packages/control-plane/src/session/budget-service.ts:121-158`). If a prompt is queued, the session stays `active`, and the queue does not dispatch while `budget_exhausted = 1` (`packages/control-plane/src/session/message-queue.ts:404-406`). No later settle projects the stopped turn at all. ## Fix - `SessionStatusService.refreshMetricsAfterStep(messageId)` re-projects metrics after a `step_finish` unless the step's own message is still `processing`. The step's turn decides this, not the session status: - A step of a stopped or finished turn is projected, even while a queued prompt keeps the session `active`. - A step of the processing turn waits for the next settle, so steps during a running turn add no D1 writes. - In a budget stop, the step that reaches the limit ends its own turn while it is ingested. The refresh after that step already writes the stopped turn's totals, and each late step writes them again. - `handleStep` now submits that refresh through `BackgroundTasks` instead of calling it in its `finally`. The factory still runs synchronously, so the refresh reads state at the same point. The task boundary absorbs and logs a failed refresh, for example `getSession()` throwing on a malformed session row (`packages/control-plane/src/session/session-core-repository.ts:314-317`). A failed refresh can therefore no longer replace the step's result or its usage/budget error. - Metrics writes are last-write-wins, and a late-step refresh can overlap a settle's write. To stop older values from landing after newer ones, only one write is in flight per session, and each write reads the session when it runs. - A request made while a write is in flight marks it stale, and the writer runs one more pass with the current values. - If the in-flight write fails, the queued pass still runs. The first failure is rethrown to the background-task boundary once the writer drains, so it is still logged. - A failed write with nothing newer pending is not retried. - With no write in flight, the first write is still issued synchronously. Unchanged from main, and out of scope here because neither case involves a step landing late: - A turn that ends while a prompt is queued is still projected only by the next settle, not at that boundary. The budget can then hold the queue with no later step landing, either because a limit edit below the running total stopped the turn or because the turn's own `execution_complete` carried the cost report that reached the limit. In that case D1 waits until the limit is raised or the queued prompt is removed. - A stopped turn's own `execution_complete` can still raise `total_cost` through its final cumulative cost report. That cost-only change is not projected until the next settle. No migration. ## Verification - `test/integration/sandbox-events.test.ts`, "projects a budget-stopped turn's steps while a queued prompt waits on the budget": - Setup: a processing turn and a queued prompt under a $1 limit. Events: a step, a step whose cost reaches the limit, a late `step_finish`, then the sandbox's cancelled `execution_complete`. - Checks: the session stays `active` with the budget exhausted and the prompt still pending. D1 already holds the stopped turn's tokens once the limit is reached. D1's token totals end equal to the sums of the usage rows from `/internal/trace-export?include=usage`. - It fails on main: D1 `inputTokens` is 0 once the limit is reached (expected 300). It also fails on the previous head, where every D1 token column is still 0 at the end. - `test/integration/sandbox-events.test.ts`, "projects a step that finishes after a stop settled the session": a step, then `/internal/stop`, then a late `step_finish` and the sandbox's cancelled `execution_complete`. On main it fails at the final `toMatchObject`: D1 has `inputTokens: 100` (expected 350), and the other token kinds are likewise at the first step's values. - `sandbox-events/processor.test.ts`: - "keeps a recorded step's result when refreshing its metrics fails": with the refresh throwing, the step resolves and the failure is recorded at the task boundary. When the refresh was called in `finally`, the step rejected with the refresh error. - "reports the usage error rather than a failed metrics refresh": when the usage write and the refresh both throw, the step rejects with the usage error. When the refresh was called in `finally`, the refresh error replaced it. - `session-status-service.test.ts`: - "defers a step of the processing turn but projects one whose turn has ended": on an `active` session, a step of the processing turn writes nothing, and a step of a stopped turn writes the current totals. - "writes usage that lands during a metrics write after it, never beside it": holds the first `updateMetrics` open and checks that later refreshes are not issued alongside it. After the first write resolves, exactly one more write follows, with the newer totals. - "still writes usage that landed during a metrics write that failed": rejects the first write after a refresh marked it stale. It checks that a second write follows with the newer totals and that the failure is reported once. Without the per-pass error handling, it fails with `updateMetrics` called 1 time instead of 2. - "does not retry a failed metrics write when nothing newer is pending": checks that a single failed write is reported and not retried, and that the next refresh writes normally. - Rebased onto main at 700f914. `npm test -w @open-inspect/control-plane`: 329 files, 5,314 tests passed. `vitest.integration.config.ts` (full): 120 files, 1,434 tests passed, 1 skipped. `npm run typecheck -w @open-inspect/control-plane`, plus ESLint and Prettier on the changed files: clean. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Session usage metrics now include usage from steps that finish after a stop has settled the session, without allowing a later failed terminal event to change the settled state. * Overlapping metric updates are serialized and use the latest available totals. If an update fails, newer pending usage can still be processed; failed updates aren’t retried unless a newer update is pending or another refresh is requested. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…2119) ## Problem The single-session export route (`GET /sessions/:id/export`) always answers `200 application/x-ndjson` once the session exists. Failures that happen while producing the record are encoded inside the stream (`packages/control-plane/src/routes/session-export.ts`): - a trace read timeout or runtime failure becomes a `{"type":"session_error", ...}` line instead of the `session` line (`streamExport`, ~line 248–252, via `sessionErrorLine`); - any other exception while streaming becomes a `{"type":"error"}` line (~line 259). The "Download trace" action in `packages/web/src/components/session-right-sidebar.tsx` (`downloadTrace`, ~line 101) only checks `response.ok`, so in both cases it saves an error-only `session-<id>.ndjson` file and shows no failure toast. ## Fix After reading the body, check its NDJSON records; if any record has type `session_error` or `error`, throw so the existing `catch` shows "Failed to download trace" and no file is downloaded. Successful traces are downloaded unchanged. (A body that is not valid NDJSON also ends in the failure toast now, because `JSON.parse` throws into the same `catch`.) ## Reproduction / tests Added `it.each(["session_error", "error"])("reports a %s export record as a failed download")` to `session-right-sidebar.test.tsx`: mocks a 200 NDJSON response containing only that record and expects the failure toast with no anchor click / object URL. Both cases fail on current `main` and pass with the fix. Ran in `packages/web`: `npm run typecheck`, `npx vitest run` (full web suite), plus prettier and eslint on the two touched files. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Trace downloads now show an error notification and prevent downloading when the response contains an error or cannot be parsed. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
# Conflicts: # .github/workflows/ci-python.yml # .github/workflows/ci.yml # packages/sandbox-runtime/src/sandbox_runtime/plugins/codex-auth-plugin.js
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: 📒 Files selected for processing (2)
Included review availability: This review used your included allowance. 4 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 6 reviews per hour. Your free on-demand review promotion remains active until October 9, 2026 at 6:00 PM UTC. 📝 WalkthroughWalkthroughThe pull request adds configurable session team enforcement and applies session admission across active-user routes. It also adds Modal VM recovery, serialized session metrics refreshes, model catalog updates, and separate runtime, CI, and web changes. ChangesSession team enforcement
Modal VM recovery and launch
Session metrics projection
Model catalog and documentation
Other runtime, CI, and interface updates
Priority: ➖ Normal Merge Risk: 🟡 Moderate · up to When Modal cannot provide the configured Docker image, VM startup can stall for about 210 seconds and repeated failures are not counted by the circuit breaker. Correct the permanent-error handling before merging. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
Terraform Validation Results
Pushed by: @rhlsthrm, Action: |
Terraform Plan ResultsStatus: ✅ Success Show Planterraform_data.cloudflare_custom_domain_gate: Refreshing state... [id=09b89c9b-996a-d28b-1f9c-08760a492dac]
terraform_data.access_control_gate: Refreshing state... [id=2b965617-b42b-4b42-5ea5-a1c3c05f10be]
terraform_data.sign_in_provider_gate: Refreshing state... [id=b29a3d55-0be5-fb92-10a9-027df10c4b75]
local_file.web_app_wrangler_production[0]: Refreshing state... [id=cd7918121eb3c2c13074f16fa9e61f598c949663]
random_password.service_auth_secret_slack_bot: Refreshing state... [id=none]
random_bytes.provider_accounts_encryption_key: Refreshing state...
random_password.service_auth_secret_linear_bot: Refreshing state... [id=none]
random_password.service_auth_secret_github_bot: Refreshing state... [id=none]
random_password.service_auth_secret_web: Refreshing state... [id=none]
random_password.image_callback_token_pepper: Refreshing state... [id=none]
module.session_index_kv.cloudflare_workers_kv_namespace.this: Refreshing state... [id=ea0a253d5cb64d75a841acb88040cd2f]
module.github_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=e0848d433a4f466cafd4ed5d140aad7d]
module.linear_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=777f94c3595f4de680c256a4e5fc6653]
cloudflare_queue.image_build_finalization_dlq: Refreshing state... [id=61535c686d8546099cfddcf39833572b]
data.external.modal_source_hash[0]: Reading...
cloudflare_queue.github_autofix[0]: Refreshing state... [id=033a23f13783415385b2f8799416c20f]
cloudflare_r2_bucket.media: Refreshing state... [id=open-inspect-media-codos]
cloudflare_queue.github_autofix_dlq[0]: Refreshing state... [id=3a27213aeba149d4b7cbf2d3551842f8]
cloudflare_d1_database.main: Refreshing state... [id=f747a908-5c69-45a1-86ab-ceb5250cf5e0]
cloudflare_queue.slack_completion_delivery[0]: Refreshing state... [id=247b1100bac2408684d6a75c1bca0d28]
data.external.modal_source_hash[0]: Read complete after 0s [id=-]
module.slack_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=ab5c371c8bc04a938ff2f71809933aa0]
cloudflare_queue.slack_completion_delivery_dlq[0]: Refreshing state... [id=396865e4939b4160937b2dc9ad5dabe1]
null_resource.linear_bot_build[0]: Refreshing state... [id=8289240060346761763]
null_resource.github_bot_build[0]: Refreshing state... [id=4818861609229143583]
null_resource.slack_bot_build[0]: Refreshing state... [id=980825262361258332]
cloudflare_queue.image_build_finalization: Refreshing state... [id=a0647323f7424e778b9d59da50dc55cf]
null_resource.control_plane_build: Refreshing state... [id=6366987019291609683]
null_resource.web_app_cloudflare_build[0]: Refreshing state... [id=5630643105118197991]
module.modal_app[0].null_resource.modal_secrets[0]: Refreshing state... [id=8477737195823217344]
module.modal_app[0].null_resource.modal_deploy: Refreshing state... [id=7248752025978423767]
module.linear_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=049cd48117bc48b9b4332683a97d0a0e]
module.slack_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=375c2c6875904657bce05c62c8048c76]
null_resource.d1_migrations: Refreshing state... [id=263751651589333239]
module.linear_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=b2d7c275-cbcf-4910-9137-90f8ad2a741e]
module.linear_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=73a63f4e-982e-441e-8ec8-0160907a28a4]
module.control_plane_worker.cloudflare_worker.this: Refreshing state... [id=3457352971a74b89be5ed3700db48a8e]
module.slack_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=96d1fa76-f117-495b-afe1-831926325448]
module.control_plane_worker.cloudflare_worker_version.this: Refreshing state... [id=9e932d5a-fd5a-442d-9bef-c299d61e3b04]
module.slack_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=8df8e48e-933f-420a-8b8a-a695a1ebf297]
cloudflare_queue_consumer.slack_completion_delivery[0]: Refreshing state...
module.control_plane_worker.cloudflare_workers_deployment.this: Refreshing state... [id=e46d5c61-1e44-4715-a250-9756904f6b42]
module.control_plane_worker.cloudflare_workers_cron_trigger.this[0]: Refreshing state... [id=open-inspect-control-plane-codos]
null_resource.web_app_cloudflare_deploy[0]: Refreshing state... [id=5235858790472054544]
cloudflare_queue_consumer.image_build_finalization: Refreshing state...
module.github_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=fa832fd890a14336bc3c63305e9bc36f]
null_resource.web_app_cloudflare_secrets[0]: Refreshing state... [id=8981633407656564074]
module.github_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=8ba6e274-72dc-4cdd-b5f8-0bcf53d340eb]
module.github_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=3f843bce-f2d6-45b2-b220-787bec76d029]
cloudflare_queue_consumer.github_autofix[0]: Refreshing state...
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:
# local_file.web_app_wrangler_production[0] will be created
+ resource "local_file" "web_app_wrangler_production" {
+ content = <<-EOT
name = "open-inspect-web-codos"
main = ".open-next/worker.js"
compatibility_date = "2025-08-15"
compatibility_flags = ["nodejs_compat", "global_fetch_strictly_public"]
# A custom-domain deployment has one canonical browser origin.
workers_dev = true
[vars]
CONTROL_PLANE_URL = "https://open-inspect-control-plane-codos.opencodos.workers.dev"
NEXT_PUBLIC_WS_URL = "wss://open-inspect-control-plane-codos.opencodos.workers.dev"
NEXT_PUBLIC_SANDBOX_PROVIDER = "modal"
NEXT_PUBLIC_APP_NAME = "Open-Inspect"
NEXT_PUBLIC_APP_ICON_URL = ""
[assets]
directory = ".open-next/assets"
binding = "ASSETS"
[[services]]
binding = "CONTROL_PLANE_WORKER"
service = "open-inspect-control-plane-codos"
EOT
+ content_base64sha256 = (known after apply)
+ content_base64sha512 = (known after apply)
+ content_md5 = (known after apply)
+ content_sha1 = (known after apply)
+ content_sha256 = (known after apply)
+ content_sha512 = (known after apply)
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "../../..//packages/web/wrangler.production.toml"
+ id = (known after apply)
}
# null_resource.control_plane_build must be replaced
-/+ resource "null_resource" "control_plane_build" {
~ id = "6366987019291609683" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-28T13:14:38Z" -> (known after apply)
}
}
# null_resource.github_bot_build[0] must be replaced
-/+ resource "null_resource" "github_bot_build" {
~ id = "4818861609229143583" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-28T13:14:38Z" -> (known after apply)
}
}
# null_resource.linear_bot_build[0] must be replaced
-/+ resource "null_resource" "linear_bot_build" {
~ id = "8289240060346761763" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-28T13:14:38Z" -> (known after apply)
}
}
# null_resource.slack_bot_build[0] must be replaced
-/+ resource "null_resource" "slack_bot_build" {
~ id = "980825262361258332" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-28T13:14:38Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_build[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_build" {
~ id = "5630643105118197991" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-28T13:14:38Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_deploy[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_deploy" {
~ id = "5235858790472054544" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-28T13:15:28Z" -> (known after apply)
}
}
# module.control_plane_worker.cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "3457352971a74b89be5ed3700db48a8e"
name = "open-inspect-control-plane-codos"
~ 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 = "34735ba6d2804d67a82cf0bdf5a3175f" -> null
- namespace_name = "open-inspect-control-plane-codos_SessionDO" -> null
- worker_id = "3457352971a74b89be5ed3700db48a8e" -> null
- worker_name = "open-inspect-control-plane-codos" -> null
},
] -> (known after apply)
~ queues = [
- {
- queue_consumer_id = "4e24da4810c84b3e9e80ea014860d145" -> null
- queue_id = "033a23f13783415385b2f8799416c20f" -> null
- queue_name = "open-inspect-github-autofix-codos" -> null
},
- {
- queue_consumer_id = "f37fe99c4658470aa36767dfb68c3d6f" -> null
- queue_id = "a0647323f7424e778b9d59da50dc55cf" -> null
- queue_name = "open-inspect-image-build-finalization-codos" -> null
},
] -> (known after apply)
~ workers = [
- {
- id = "7aa4fa7a556a48708d1ebd7bbba3263a" -> null
- name = "open-inspect-web-codos" -> null
},
- {
- id = "fa832fd890a14336bc3c63305e9bc36f" -> null
- name = "open-inspect-github-bot-codos" -> null
},
- {
- id = "049cd48117bc48b9b4332683a97d0a0e" -> null
- name = "open-inspect-linear-bot-codos" -> null
},
- {
- id = "375c2c6875904657bce05c62c8048c76" -> null
- name = "open-inspect-slack-bot-codos" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-09-28T13:14:41Z" -> (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-09-28T13:14:43Z" -> (known after apply)
~ id = "9e932d5a-fd5a-442d-9bef-c299d61e3b04" -> (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 = "593db08c1e73a6194ac63f00dc7041b51416c0ced57b6c3e8f67d327a3237d23" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/control-plane/dist/index.js"
+ content_sha256 = "f6510dded1f796cc96e9dbd25cc9a8269069d02fefe39628b9a40412ad112ffc"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 65 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 148 -> (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 = (known after apply)
~ created_on = "2026-09-28T13:14:45Z" -> (known after apply)
~ id = "e46d5c61-1e44-4715-a250-9756904f6b42" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "9e932d5a-fd5a-442d-9bef-c299d61e3b04" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
# module.github_bot_worker[0].cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "fa832fd890a14336bc3c63305e9bc36f"
name = "open-inspect-github-bot-codos"
~ 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 = "3457352971a74b89be5ed3700db48a8e" -> null
- name = "open-inspect-control-plane-codos" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-09-28T13:14:46Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.github_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-09-28T13:14:47Z" -> (known after apply)
~ id = "8ba6e274-72dc-4cdd-b5f8-0bcf53d340eb" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
+ migration_tag = (known after apply)
~ modules = [
- { # forces replacement
- content_file = "../../..//packages/github-bot/dist/index.js" -> null
- content_sha256 = "c472fc810f60769d760854be2125a4135705985064d83ee980cb5aac3d43d967" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/github-bot/dist/index.js"
+ content_sha256 = "565c293770fca6087815bfb92eacbe51671dba6a73c90ecebf56998f1777f194"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 63 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 64 -> (known after apply)
~ urls = [
- "https://8ba6e274-open-inspect-github-bot-codos.opencodos.workers.dev",
] -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.github_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 = (known after apply)
~ created_on = "2026-09-28T13:14:50Z" -> (known after apply)
~ id = "3f843bce-f2d6-45b2-b220-787bec76d029" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "8ba6e274-72dc-4cdd-b5f8-0bcf53d340eb" -> (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 = "049cd48117bc48b9b4332683a97d0a0e"
name = "open-inspect-linear-bot-codos"
~ 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 = "3457352971a74b89be5ed3700db48a8e" -> null
- name = "open-inspect-control-plane-codos" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-09-28T13:14:38Z" -> (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-09-28T13:14:39Z" -> (known after apply)
~ id = "b2d7c275-cbcf-4910-9137-90f8ad2a741e" -> (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 = "299986359323af432a26c285e5aba078114868ee1d99889d7d7390cfdc7b0464" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/linear-bot/dist/index.js"
+ content_sha256 = "7b666d533ee06f3eafa1d8a4392e218d9d45a8035b472b8c15f0d5bd39f78856"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 69 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 54 -> (known after apply)
~ urls = [
- "https://b2d7c275-open-inspect-linear-bot-codos.opencodos.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 = (known after apply)
~ created_on = "2026-09-28T13:14:40Z" -> (known after apply)
~ id = "73a63f4e-982e-441e-8ec8-0160907a28a4" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "b2d7c275-cbcf-4910-9137-90f8ad2a741e" -> (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 = "7248752025978423767" -> (known after apply)
~ triggers = { # forces replacement
~ "source_hash" = "928bf5f4184634e305c4e7845fb7c2fd0f59fa79f6a7062304e2c8798c2dd691" -> "838e5042d2f3d2ae1d60e74f0e66ad33dfc3c66196477b5625c02532da626463"
# (4 unchanged elements hidden)
}
}
# module.slack_bot_worker[0].cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "375c2c6875904657bce05c62c8048c76"
name = "open-inspect-slack-bot-codos"
~ 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 = [
- {
- queue_consumer_id = "767c5dfe751c4852a536d98b836cdd94" -> null
- queue_id = "247b1100bac2408684d6a75c1bca0d28" -> null
- queue_name = "open-inspect-slack-completion-codos" -> null
},
] -> (known after apply)
~ workers = [
- {
- id = "3457352971a74b89be5ed3700db48a8e" -> null
- name = "open-inspect-control-plane-codos" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-09-28T13:14: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-09-28T13:14:39Z" -> (known after apply)
~ id = "96d1fa76-f117-495b-afe1-831926325448" -> (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 = "c52db492cb448f16341a0212b13bf1576760d018faecdd3e0131990d8588af21" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/slack-bot/dist/index.js"
+ content_sha256 = "0336219441bdf501ebc3bd9f16b53ddd6199f1895353ae8cc6ce971217ddf1f0"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 67 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 88 -> (known after apply)
~ urls = [
- "https://96d1fa76-open-inspect-slack-bot-codos.opencodos.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 = (known after apply)
~ created_on = "2026-09-28T13:14:41Z" -> (known after apply)
~ id = "8df8e48e-933f-420a-8b8a-a695a1ebf297" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "96d1fa76-f117-495b-afe1-831926325448" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
Plan: 16 to add, 4 to change, 15 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"Pushed by: @rhlsthrm |
There was a problem hiding this comment.
Blocking: 3 · Non-blocking: 0
The VM recovery path needs to accept an owned allocation even when optional tunnel publication is partial, and a restarted control plane must reconcile an in-progress snapshot restore rather than leave it held. The OAuth proxy also removes the fetch timeout from non-generation requests that have no generation-stream timeout. Targeted checks passed: 19 VM lifecycle tests, 30 Modal resolve tests, and 44 Codex proxy tests; none exercises these failing states.
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Team
Run ID: edb5d524-ded5-4f84-a60a-19a831ec5a68
⛔ Files ignored due to path filters (4)
packages/control-plane/test/integration/__snapshots__/hono-route-catalog-conformance.test.ts.snapis excluded by!**/*.snappackages/control-plane/test/integration/__snapshots__/route-admission-matrix.test.ts.snapis excluded by!**/*.snappackages/modal-infra/uv.lockis excluded by!**/*.lockpackages/sandbox-runtime/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (110)
.env.example.github/workflows/ci-python.yml.github/workflows/ci.ymlCHANGELOG.mdREADME.mddocs/AVAILABLE_MODELS.mddocs/CLAUDE_AGENT.mddocs/MODAL_DOCKER.mdpackages/control-plane/src/authorization/request-audit.tspackages/control-plane/src/authorization/session-admission.tspackages/control-plane/src/authorization/teams-enforcement.test.tspackages/control-plane/src/authorization/teams-enforcement.tspackages/control-plane/src/db/session-collaborators.tspackages/control-plane/src/http/request-context.tspackages/control-plane/src/node/config.test.tspackages/control-plane/src/node/config.tspackages/control-plane/src/router.policy.test.tspackages/control-plane/src/router.scm-credentials.test.tspackages/control-plane/src/router.session-prompt.test.tspackages/control-plane/src/router.spawn-child.test.tspackages/control-plane/src/router.test-support.tspackages/control-plane/src/routes/session-attachments.tspackages/control-plane/src/routes/session-batch-archive.tspackages/control-plane/src/routes/session-child-spawn.tspackages/control-plane/src/routes/session-children.test.tspackages/control-plane/src/routes/session-children.tspackages/control-plane/src/routes/session-diffs.tspackages/control-plane/src/routes/session-export.test.tspackages/control-plane/src/routes/session-export.tspackages/control-plane/src/routes/session-index.tspackages/control-plane/src/routes/session-media-stream.tspackages/control-plane/src/routes/session-media-upload.tspackages/control-plane/src/routes/session-prompt.tspackages/control-plane/src/routes/session-pull-requests.tspackages/control-plane/src/routes/session-route.tspackages/control-plane/src/routes/session-runtime-proxy.test.tspackages/control-plane/src/routes/session-runtime-proxy.tspackages/control-plane/src/routes/session-skills.tspackages/control-plane/src/routes/session-ws-token.tspackages/control-plane/src/routes/shared.tspackages/control-plane/src/routes/slack-notify.tspackages/control-plane/src/routing/hono-app.tspackages/control-plane/src/routing/route-admission.tspackages/control-plane/src/sandbox/client.test.tspackages/control-plane/src/sandbox/client.tspackages/control-plane/src/sandbox/lifecycle/manager.test.tspackages/control-plane/src/sandbox/lifecycle/manager.tspackages/control-plane/src/sandbox/lifecycle/test-helpers.tspackages/control-plane/src/sandbox/lifecycle/vm-resolve.test.tspackages/control-plane/src/sandbox/provider.tspackages/control-plane/src/sandbox/providers/modal-provider.test.tspackages/control-plane/src/sandbox/providers/modal-provider.tspackages/control-plane/src/session/components.tspackages/control-plane/src/session/sandbox-events/processor.test.tspackages/control-plane/src/session/sandbox-events/streaming.handler.tspackages/control-plane/src/session/sandbox-repository.test.tspackages/control-plane/src/session/sandbox-repository.tspackages/control-plane/src/session/sandbox-shutdown.tspackages/control-plane/src/session/session-status-service.test.tspackages/control-plane/src/session/session-status-service.tspackages/control-plane/src/types.tspackages/control-plane/test/integration/helpers.tspackages/control-plane/test/integration/rbac-routes.test.tspackages/control-plane/test/integration/route-admission-matrix.test.tspackages/control-plane/test/integration/sandbox-events.test.tspackages/control-plane/test/integration/session-access-routes.test.tspackages/control-plane/test/integration/session-batch-archive.test.tspackages/control-plane/test/integration/team-stores.test.tspackages/docs/content/docs/models/choosing-a-model.mdxpackages/modal-infra/pyproject.tomlpackages/modal-infra/src/sandbox/launch.pypackages/modal-infra/src/sandbox/manager.pypackages/modal-infra/src/sandbox/models.pypackages/modal-infra/src/sandbox/tunnels.pypackages/modal-infra/src/sandbox/vm_recovery.pypackages/modal-infra/src/web_api.pypackages/modal-infra/tests/test_agent_slack_notify_env.pypackages/modal-infra/tests/test_code_server.pypackages/modal-infra/tests/test_docker_launch.pypackages/modal-infra/tests/test_llm_secrets.pypackages/modal-infra/tests/test_manager_exports.pypackages/modal-infra/tests/test_sandbox_env_vars.pypackages/modal-infra/tests/test_sandbox_launch.pypackages/modal-infra/tests/test_sandbox_resources.pypackages/modal-infra/tests/test_snapshot_timeout.pypackages/modal-infra/tests/test_ttyd.pypackages/modal-infra/tests/test_tunnel_ports.pypackages/modal-infra/tests/test_vm_resolve.pypackages/modal-infra/tests/test_vnc.pypackages/sandbox-images/locks/runtime.txtpackages/sandbox-runtime/pyproject.tomlpackages/sandbox-runtime/src/sandbox_runtime/plugins/codex-auth-plugin.jspackages/sandbox-runtime/tests/codex-auth-plugin.test.mjspackages/sandbox-runtime/tests/fixtures/reasoning-models.jsonpackages/sandbox-runtime/tests/test_claude_harness.pypackages/sandbox-runtime/tests/test_opencode_reasoning_contract.pypackages/shared/src/models.test.tspackages/shared/src/models.tspackages/shared/src/types/audit-events.test.tspackages/shared/src/types/audit-events.tspackages/shared/src/types/session-archive.tspackages/web/src/components/session-header.test.tsxpackages/web/src/components/session-header.tsxpackages/web/src/components/session-right-sidebar.test.tsxpackages/web/src/components/session-right-sidebar.tsxpackages/web/src/components/settings/audit-log-settings.test.tsxpackages/web/src/components/settings/audit-log-settings.tsxterraform/environments/aws-production/main.tfterraform/environments/production/variables.tfterraform/environments/production/workers-control-plane.tf
Included review availability: This review used your included allowance. 5 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 6 reviews per hour. Your free on-demand review promotion remains active until October 9, 2026 at 6:00 PM UTC.
Terraform Validation Results
Pushed by: @rhlsthrm, Action: |
Terraform Plan ResultsStatus: ✅ Success Show Planterraform_data.sign_in_provider_gate: Refreshing state... [id=b29a3d55-0be5-fb92-10a9-027df10c4b75]
terraform_data.access_control_gate: Refreshing state... [id=2b965617-b42b-4b42-5ea5-a1c3c05f10be]
terraform_data.cloudflare_custom_domain_gate: Refreshing state... [id=09b89c9b-996a-d28b-1f9c-08760a492dac]
data.external.modal_source_hash[0]: Reading...
random_password.image_callback_token_pepper: Refreshing state... [id=none]
random_password.service_auth_secret_linear_bot: Refreshing state... [id=none]
cloudflare_r2_bucket.media: Refreshing state... [id=open-inspect-media-codos]
module.linear_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=777f94c3595f4de680c256a4e5fc6653]
cloudflare_queue.slack_completion_delivery_dlq[0]: Refreshing state... [id=396865e4939b4160937b2dc9ad5dabe1]
cloudflare_queue.image_build_finalization: Refreshing state... [id=a0647323f7424e778b9d59da50dc55cf]
cloudflare_queue.github_autofix[0]: Refreshing state... [id=033a23f13783415385b2f8799416c20f]
random_password.service_auth_secret_github_bot: Refreshing state... [id=none]
random_bytes.provider_accounts_encryption_key: Refreshing state...
random_password.service_auth_secret_slack_bot: Refreshing state... [id=none]
random_password.service_auth_secret_web: Refreshing state... [id=none]
cloudflare_queue.image_build_finalization_dlq: Refreshing state... [id=61535c686d8546099cfddcf39833572b]
cloudflare_d1_database.main: Refreshing state... [id=f747a908-5c69-45a1-86ab-ceb5250cf5e0]
module.session_index_kv.cloudflare_workers_kv_namespace.this: Refreshing state... [id=ea0a253d5cb64d75a841acb88040cd2f]
local_file.web_app_wrangler_production[0]: Refreshing state... [id=cd7918121eb3c2c13074f16fa9e61f598c949663]
cloudflare_queue.slack_completion_delivery[0]: Refreshing state... [id=247b1100bac2408684d6a75c1bca0d28]
data.external.modal_source_hash[0]: Read complete after 0s [id=-]
cloudflare_queue.github_autofix_dlq[0]: Refreshing state... [id=3a27213aeba149d4b7cbf2d3551842f8]
module.slack_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=ab5c371c8bc04a938ff2f71809933aa0]
null_resource.linear_bot_build[0]: Refreshing state... [id=8289240060346761763]
null_resource.web_app_cloudflare_build[0]: Refreshing state... [id=5630643105118197991]
null_resource.github_bot_build[0]: Refreshing state... [id=4818861609229143583]
module.github_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=e0848d433a4f466cafd4ed5d140aad7d]
null_resource.slack_bot_build[0]: Refreshing state... [id=980825262361258332]
null_resource.control_plane_build: Refreshing state... [id=6366987019291609683]
module.modal_app[0].null_resource.modal_secrets[0]: Refreshing state... [id=8477737195823217344]
module.linear_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=049cd48117bc48b9b4332683a97d0a0e]
module.modal_app[0].null_resource.modal_deploy: Refreshing state... [id=7248752025978423767]
module.slack_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=375c2c6875904657bce05c62c8048c76]
null_resource.d1_migrations: Refreshing state... [id=263751651589333239]
module.linear_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=b2d7c275-cbcf-4910-9137-90f8ad2a741e]
module.slack_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=96d1fa76-f117-495b-afe1-831926325448]
module.linear_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=73a63f4e-982e-441e-8ec8-0160907a28a4]
module.slack_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=8df8e48e-933f-420a-8b8a-a695a1ebf297]
cloudflare_queue_consumer.slack_completion_delivery[0]: Refreshing state...
module.control_plane_worker.cloudflare_worker.this: Refreshing state... [id=3457352971a74b89be5ed3700db48a8e]
module.control_plane_worker.cloudflare_worker_version.this: Refreshing state... [id=9e932d5a-fd5a-442d-9bef-c299d61e3b04]
module.control_plane_worker.cloudflare_workers_deployment.this: Refreshing state... [id=e46d5c61-1e44-4715-a250-9756904f6b42]
module.control_plane_worker.cloudflare_workers_cron_trigger.this[0]: Refreshing state... [id=open-inspect-control-plane-codos]
cloudflare_queue_consumer.image_build_finalization: Refreshing state...
null_resource.web_app_cloudflare_deploy[0]: Refreshing state... [id=5235858790472054544]
module.github_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=fa832fd890a14336bc3c63305e9bc36f]
null_resource.web_app_cloudflare_secrets[0]: Refreshing state... [id=8981633407656564074]
module.github_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=8ba6e274-72dc-4cdd-b5f8-0bcf53d340eb]
module.github_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=3f843bce-f2d6-45b2-b220-787bec76d029]
cloudflare_queue_consumer.github_autofix[0]: Refreshing state...
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:
# local_file.web_app_wrangler_production[0] will be created
+ resource "local_file" "web_app_wrangler_production" {
+ content = <<-EOT
name = "open-inspect-web-codos"
main = ".open-next/worker.js"
compatibility_date = "2025-08-15"
compatibility_flags = ["nodejs_compat", "global_fetch_strictly_public"]
# A custom-domain deployment has one canonical browser origin.
workers_dev = true
[vars]
CONTROL_PLANE_URL = "https://open-inspect-control-plane-codos.opencodos.workers.dev"
NEXT_PUBLIC_WS_URL = "wss://open-inspect-control-plane-codos.opencodos.workers.dev"
NEXT_PUBLIC_SANDBOX_PROVIDER = "modal"
NEXT_PUBLIC_APP_NAME = "Open-Inspect"
NEXT_PUBLIC_APP_ICON_URL = ""
[assets]
directory = ".open-next/assets"
binding = "ASSETS"
[[services]]
binding = "CONTROL_PLANE_WORKER"
service = "open-inspect-control-plane-codos"
EOT
+ content_base64sha256 = (known after apply)
+ content_base64sha512 = (known after apply)
+ content_md5 = (known after apply)
+ content_sha1 = (known after apply)
+ content_sha256 = (known after apply)
+ content_sha512 = (known after apply)
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "../../..//packages/web/wrangler.production.toml"
+ id = (known after apply)
}
# null_resource.control_plane_build must be replaced
-/+ resource "null_resource" "control_plane_build" {
~ id = "6366987019291609683" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-28T13:14:38Z" -> (known after apply)
}
}
# null_resource.github_bot_build[0] must be replaced
-/+ resource "null_resource" "github_bot_build" {
~ id = "4818861609229143583" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-28T13:14:38Z" -> (known after apply)
}
}
# null_resource.linear_bot_build[0] must be replaced
-/+ resource "null_resource" "linear_bot_build" {
~ id = "8289240060346761763" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-28T13:14:38Z" -> (known after apply)
}
}
# null_resource.slack_bot_build[0] must be replaced
-/+ resource "null_resource" "slack_bot_build" {
~ id = "980825262361258332" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-28T13:14:38Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_build[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_build" {
~ id = "5630643105118197991" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-28T13:14:38Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_deploy[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_deploy" {
~ id = "5235858790472054544" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-28T13:15:28Z" -> (known after apply)
}
}
# module.control_plane_worker.cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "3457352971a74b89be5ed3700db48a8e"
name = "open-inspect-control-plane-codos"
~ 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 = "34735ba6d2804d67a82cf0bdf5a3175f" -> null
- namespace_name = "open-inspect-control-plane-codos_SessionDO" -> null
- worker_id = "3457352971a74b89be5ed3700db48a8e" -> null
- worker_name = "open-inspect-control-plane-codos" -> null
},
] -> (known after apply)
~ queues = [
- {
- queue_consumer_id = "4e24da4810c84b3e9e80ea014860d145" -> null
- queue_id = "033a23f13783415385b2f8799416c20f" -> null
- queue_name = "open-inspect-github-autofix-codos" -> null
},
- {
- queue_consumer_id = "f37fe99c4658470aa36767dfb68c3d6f" -> null
- queue_id = "a0647323f7424e778b9d59da50dc55cf" -> null
- queue_name = "open-inspect-image-build-finalization-codos" -> null
},
] -> (known after apply)
~ workers = [
- {
- id = "7aa4fa7a556a48708d1ebd7bbba3263a" -> null
- name = "open-inspect-web-codos" -> null
},
- {
- id = "fa832fd890a14336bc3c63305e9bc36f" -> null
- name = "open-inspect-github-bot-codos" -> null
},
- {
- id = "049cd48117bc48b9b4332683a97d0a0e" -> null
- name = "open-inspect-linear-bot-codos" -> null
},
- {
- id = "375c2c6875904657bce05c62c8048c76" -> null
- name = "open-inspect-slack-bot-codos" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-09-28T13:14:41Z" -> (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-09-28T13:14:43Z" -> (known after apply)
~ id = "9e932d5a-fd5a-442d-9bef-c299d61e3b04" -> (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 = "593db08c1e73a6194ac63f00dc7041b51416c0ced57b6c3e8f67d327a3237d23" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/control-plane/dist/index.js"
+ content_sha256 = "f6510dded1f796cc96e9dbd25cc9a8269069d02fefe39628b9a40412ad112ffc"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 65 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 148 -> (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 = (known after apply)
~ created_on = "2026-09-28T13:14:45Z" -> (known after apply)
~ id = "e46d5c61-1e44-4715-a250-9756904f6b42" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "9e932d5a-fd5a-442d-9bef-c299d61e3b04" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
# module.github_bot_worker[0].cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "fa832fd890a14336bc3c63305e9bc36f"
name = "open-inspect-github-bot-codos"
~ 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 = "3457352971a74b89be5ed3700db48a8e" -> null
- name = "open-inspect-control-plane-codos" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-09-28T13:14:46Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.github_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-09-28T13:14:47Z" -> (known after apply)
~ id = "8ba6e274-72dc-4cdd-b5f8-0bcf53d340eb" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
+ migration_tag = (known after apply)
~ modules = [
- { # forces replacement
- content_file = "../../..//packages/github-bot/dist/index.js" -> null
- content_sha256 = "c472fc810f60769d760854be2125a4135705985064d83ee980cb5aac3d43d967" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/github-bot/dist/index.js"
+ content_sha256 = "565c293770fca6087815bfb92eacbe51671dba6a73c90ecebf56998f1777f194"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 63 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 64 -> (known after apply)
~ urls = [
- "https://8ba6e274-open-inspect-github-bot-codos.opencodos.workers.dev",
] -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.github_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 = (known after apply)
~ created_on = "2026-09-28T13:14:50Z" -> (known after apply)
~ id = "3f843bce-f2d6-45b2-b220-787bec76d029" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "8ba6e274-72dc-4cdd-b5f8-0bcf53d340eb" -> (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 = "049cd48117bc48b9b4332683a97d0a0e"
name = "open-inspect-linear-bot-codos"
~ 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 = "3457352971a74b89be5ed3700db48a8e" -> null
- name = "open-inspect-control-plane-codos" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-09-28T13:14:38Z" -> (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-09-28T13:14:39Z" -> (known after apply)
~ id = "b2d7c275-cbcf-4910-9137-90f8ad2a741e" -> (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 = "299986359323af432a26c285e5aba078114868ee1d99889d7d7390cfdc7b0464" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/linear-bot/dist/index.js"
+ content_sha256 = "7b666d533ee06f3eafa1d8a4392e218d9d45a8035b472b8c15f0d5bd39f78856"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 69 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 54 -> (known after apply)
~ urls = [
- "https://b2d7c275-open-inspect-linear-bot-codos.opencodos.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 = (known after apply)
~ created_on = "2026-09-28T13:14:40Z" -> (known after apply)
~ id = "73a63f4e-982e-441e-8ec8-0160907a28a4" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "b2d7c275-cbcf-4910-9137-90f8ad2a741e" -> (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 = "7248752025978423767" -> (known after apply)
~ triggers = { # forces replacement
~ "source_hash" = "928bf5f4184634e305c4e7845fb7c2fd0f59fa79f6a7062304e2c8798c2dd691" -> "c20daafaef4ec5b383170cd99471ef981c2eff13b59b64a7b12aab06014bfece"
# (4 unchanged elements hidden)
}
}
# module.slack_bot_worker[0].cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "375c2c6875904657bce05c62c8048c76"
name = "open-inspect-slack-bot-codos"
~ 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 = [
- {
- queue_consumer_id = "767c5dfe751c4852a536d98b836cdd94" -> null
- queue_id = "247b1100bac2408684d6a75c1bca0d28" -> null
- queue_name = "open-inspect-slack-completion-codos" -> null
},
] -> (known after apply)
~ workers = [
- {
- id = "3457352971a74b89be5ed3700db48a8e" -> null
- name = "open-inspect-control-plane-codos" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-09-28T13:14: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-09-28T13:14:39Z" -> (known after apply)
~ id = "96d1fa76-f117-495b-afe1-831926325448" -> (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 = "c52db492cb448f16341a0212b13bf1576760d018faecdd3e0131990d8588af21" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/slack-bot/dist/index.js"
+ content_sha256 = "0336219441bdf501ebc3bd9f16b53ddd6199f1895353ae8cc6ce971217ddf1f0"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 67 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 88 -> (known after apply)
~ urls = [
- "https://96d1fa76-open-inspect-slack-bot-codos.opencodos.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 = (known after apply)
~ created_on = "2026-09-28T13:14:41Z" -> (known after apply)
~ id = "8df8e48e-933f-420a-8b8a-a695a1ebf297" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "96d1fa76-f117-495b-afe1-831926325448" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
Plan: 16 to add, 4 to change, 15 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"Pushed by: @rhlsthrm |
There was a problem hiding this comment.
Blocking: 7 · Non-blocking: 0
The non-generation OAuth timeout issue was fixed at this head, and the break-glass audit failure is deliberately fail-closed. Two previously reported VM recovery failures remain unanswered and reproducible from the current paths. The new session gates also leave bulk export, session listings, and recursive child cancellation outside the per-session access boundary; VM lookup retries and a known unavailable-image response need correction before this ships.
|
Review dispositions. Each thread has its own reply with the evidence.
|
Syncs ColeMurray/background-agents
700f9145..e471cff4(12 commits, 114 files) into the fork.Incoming: ColeMurray#2117, ColeMurray#2015 (Modal sandbox manager split), ColeMurray#2115, ColeMurray#2118 (session routes behind the teams flag), ColeMurray#2128 (Claude Sonnet 5.5), ColeMurray#2130 (
persist-credentials: falseon checkouts), ColeMurray#2129 (Bun fetch timeout off for Codex OAuth streams), ColeMurray#2126, ColeMurray#2131, ColeMurray#2073, ColeMurray#2081, ColeMurray#2119. Three of those (ColeMurray#2073, ColeMurray#2081, ColeMurray#2119) are our own upstream PRs.Conflicts
.github/workflows/ci.yml(7 blocks),.github/workflows/ci-python.yml(1): the fork's dependabot merges bumpedactions/checkout,actions/setup-nodeandastral-sh/setup-uvto v7, while upstream addedwith: persist-credentials: falseto every checkout and renamed "Setup frozen image lock checker" to "Setup uv". Kept both: v7 pins, upstream'spersist-credentials: falseon all checkouts, upstream's step name.ci.ymlnow differs from upstream only in the version pins and the fork'stest:d1-migratestep.packages/sandbox-runtime/src/sandbox_runtime/plugins/codex-auth-plugin.js: upstream fix: disable Bun fetch timeout for Codex OAuth streams ColeMurray/background-agents#2129 changed its singlefetch(proxiedRequest)tofetch(proxiedRequest, { timeout: false }). The fork's spillover proxy has no single call, so the opt-out is applied to each OAuth path. The model path putstimeout: falseintobaseInit, which feeds both the subscription call and everyfetchFallbackleg. The non-generation passthrough keeps Bun's timeout (fetch(proxied), ff8daa3), because OpenCode's stream timeout does not cover it. The usage probe keeps its ownAbortSignal.timeout. Upstream's merged test assertiondeepEqual(upstreamInit, { timeout: false })assumes the whole init is that one key. The fork passes a full init, so the assertion checksupstreamInit.timeout === false.Route catalog: upstream added no routes (base and upstream both 193/147), so the fork's 206/158 carries forward. Migrations: no new upstream migrations, no version collisions.
Gates (sequential, all exit 0)
install, build shared, typecheck, control-plane unit, control-plane integration, web, github-bot, slack-bot, linear-bot, shared, mcp-server, sandbox-runtime node tests, sandbox-runtime pytest, SQL portability, d1-migrate, modal-infra pytest, lint,
terraform test(66 passed).terraform testfirst failed locally because the fork's dependabot provider bumps were missing from the local.terraformcache.terraform init -backend=falsefixed it. The failure came from the local environment, not from the merge.