chore: sync upstream 2026-10-01 (e471cff4..2ea66073) - #107
Conversation
…ay#2132) ## Summary - Resolve the current D1 session row, workspace authorization, team memberships, and collaborators at WebSocket subscribe and on every gated command. The row, not the URL or DO participants, determines scope and visibility. - Require `read` for subscribe, history, and presence; `collaborate` for prompt and typing; `lifecycle` for cancel, stop, and recovery. Use the resolver in `on`; preserve legacy team-rule permissions in `off`/`shadow`, while applying private-session rules in every mode. Redact sandbox URLs when the sandbox decision denies access. The five-minute lease and 4010 reconnect behavior are unchanged. - Cover mode differences, Owner break-glass (redacted read, denied collaboration, permitted lifecycle), changed membership and scope, revoked private collaborators, stale tokens, and HTTP re-mint refusal. Update the dated changelog. Issue: [COL-200](https://linear.app/colemurray/issue/COL-200/teams-pr-7-control-plane-do-subscribe-and-per-command-checks-through) ## Checkpoint **Validation commands and results** - `npm run build -w @open-inspect/shared`: passed. - `npm run typecheck`: passed across workspaces after building shared. - `npm run lint:fix`: passed. - `npm run lint:sql-portability`: passed (`SQL portability: clean (24 baselined occurrence(s) across 4 file(s)).`). - `npm test -w @open-inspect/control-plane`: passed, 331 files and 5,356 tests. - `npm run test:integration -w @open-inspect/control-plane`: passed, 122 files and 1,457 tests (1 skipped). The runner emitted forced-eviction/workerd and Miniflare warnings, but no test failures. - Targeted auth/router tests after the final test edit: passed, 73 tests. Targeted new workerd tests: passed, 2 tests. `git diff --check`: passed. **Failures encountered during red/green and fixture correction** (exact failure output excerpts, followed by passing reruns): ```text TypeError: this.deps.resolveAuthorization is not a function AssertionError: expected "vi.fn()" to be called with arguments: [ { …(2) }, 'collaborate' ] Number of calls: 0 Test Files 2 failed (2) Tests 14 failed | 56 passed (70) ``` ```text Error: "./types/session-access" is not exported under the conditions ["node", "development", "import"] from package /workspace/background-agents/node_modules/@open-inspect/shared (see exports field in /workspace/background-agents/node_modules/@open-inspect/shared/package.json) Test Files 1 failed | 1 passed (2) Tests 35 passed (35) ``` ```text src/session/connection-authenticator.ts(516,25): error TS2345: Argument of type 'SessionViewerResolution' is not assignable to parameter of type '{ kind: "valid"; authorization: { userId: string; suspendedAt: number | null; role: { id: string; key: "owner" | "member" | "administrator" | "viewer" | null; name: string; }; permissions: ("analytics.read" | ... 43 more ... | "workspace.transfer_ownership")[]; }; viewer: SessionViewer; row: SessionAccessRow; }'. ``` ```text src/session/connection-authenticator.test.ts(679,49): error TS2493: Tuple type '[]' of length '0' has no element at index '1'. src/session/connection-authenticator.test.ts(679,62): error TS18048: 'message' is possibly 'undefined'. src/session/connection-authenticator.test.ts(679,95): error TS2493: Tuple type '[]' of length '0' has no element at index '1'. src/session/connection-authenticator.test.ts(680,24): error TS2339: Property 'session' does not exist on type 'never'. src/session/connection-authenticator.test.ts(681,24): error TS2339: Property 'session' does not exist on type 'never'. src/session/connection-authenticator.test.ts(682,24): error TS2339: Property 'session' does not exist on type 'never'. src/session/connection-authenticator.test.ts(683,24): error TS2339: Property 'session' does not exist on type 'never'. src/session/connection-authenticator.test.ts(684,24): error TS2339: Property 'session' does not exist on type 'never'. ``` ```text AssertionError: expected 500 to be 404 // Object.is equality - Expected + Received - 404 + 500 ``` The last failure was a test fixture using a non-canonical browser user ID; it was corrected to a 32-character canonical ID. The same 500 assertion appeared on the first retry before that correction. **Plan drift and scope** - Base is `main` at `0530683`. The spec's `components.ts:808-836` is now `components.ts:814-867`; subscribe and command checks shifted to `connection-authenticator.ts:379-524` and `message-router.ts:143-221`. They have the same described behavior, so the edits followed their current locations. Latest DO migration is 56 rather than the plan-wide snapshot's 55; D1 migration 0083 is present. Neither needs a new migration. - The requested Owner `stop` denial in the original issue conflicted with the shared resolver. Per clarification, lifecycle remains permitted during break-glass; prompt/typing and sandbox access do not. - No schema, session-creation, HTTP token-mint, or web changes. PR 8's `PUT /sessions/:id/scope` is not on this branch; the unit test changes the authoritative row to exercise the next-command recheck. PR 9 owns the browser reconnect/not-found test. No DO audit rows are written. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/72cdbc9744d881b50924e0ee223a20ed)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Access and Permissions** * WebSocket subscriptions and session commands check access against the current session and team membership. * Private sessions remain restricted to authorized collaborators in every enforcement mode. * When team enforcement is enabled, team access follows current membership and session scope. * Access changes apply to subsequent commands without requiring an active connection to close. * Owners can read private sessions with sandbox URLs hidden; collaboration actions remain restricted. * **Bug Fixes** * Invalid or rate-limited history requests are rejected before authorization and history retrieval. <!-- 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#2133) ## Summary - Apply row-based visibility before pagination and aggregation across session lists, inbox, children, bulk export, analytics, runs and autofix activity. Every visibility-aware reader now requires a `readScope` and enforcement mode; only a parent-bound sandbox uses an explicit internal scope for its children. A hidden export row is filtered before any included trace fetch. - Bound services see all non-private work in `off`/`shadow`, and their team scope applies in `on`. `teamIds[]` and `createdBy` share one filter-ID cap, with actual D1 bind counts checked before execution. - Return scoped unattributed private-session cost only to Owners and administrators (`null` otherwise). PR funnel queries retain deleted-session rows as workspace-level via a left join. Autofix activity continues to show unattached feedback after `ON DELETE SET NULL`; the PR metadata limitation is documented in the store. Implements [COL-199](https://linear.app/colemurray/issue/COL-199/teams-pr-6-control-plane-shared-visiblesessionspredicate-in-every-list). ## Checkpoint **Validation** - `npm run build -w @open-inspect/shared`: passed (via `npm run typecheck`). - `npm run typecheck`: passed across all workspaces. - `npm run lint:fix`: passed. - `npm run lint:sql-portability`: `SQL portability: clean (24 baselined occurrence(s) across 4 file(s)).` - `npm test -w @open-inspect/control-plane`: 332 files, 5,383 tests passed. - `npm run test:integration -w @open-inspect/control-plane`: 122 files, 1,477 passed, 1 skipped. The workerd force-eviction and NDJSON warnings appear in this passing run. - `npm test -w @open-inspect/web`: 226 files, 1,985 tests passed. - Prettier check and `git diff --check`: passed. **Red-phase failure fixed before the final green run** ```text FAIL test/integration/session-access-routes.test.ts > HTTP session access by enforcement mode > lists only children visible in the selected enforcement mode AssertionError: expected [] to have a length of 1 but got +0 ``` The list seam does not write batch shadow audit rows; the assertion introduced in PR 2131 was removed while retaining its visible-parent/hidden-child coverage. **Baseline drift and resolution** - Rebasing onto `main` at `e471cff` brought PR 2131 changes to the children route and the September 29 changelog entry. The conflict was resolved with the predicate-based children list, no per-child item admission, and a separate changelog paragraph. D1 migration `0083` and DO migration `56` were already present; no migration was needed. The older `listRun()` export wrapper was already gone. **Deliberately left out** - Team-dimension analytics and per-team cost lines, WebSocket authorization, and session ownership/visibility write routes belong to later work. No schema or session write path changed. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Session lists and inboxes support filtering by team, ownership, visibility, and workspace scope. Owners and administrators can request sessions across all scopes. * Session lists, inboxes, child sessions, exports, analytics, and run views respect session visibility and team access. Private sessions remain restricted. * Analytics summaries show owners and administrators an unattributed, scope-filtered total for private-session costs. * **Bug Fixes** * Inaccessible sessions and runs no longer appear in exports or inbox results. * Autofix activity excludes feedback associated with private sessions. <!-- 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>
…leMurray#2136) ## Summary - Classify daemon exits requested by snapshot preparation as expected, even when the stop times out or exits non-zero. - On failed interactive preparation, stop any remaining daemon, restart it to readiness, and rearm the supervisor watcher. Keep unrelated crashes fatal and never acknowledge a failed save as prepared. - Derive preparation and control deadlines from Docker stop/start budgets so a failed stop and restart fit within the Modal VM capture budget. ## Verification - Added real daemon/control/supervisor regressions for ignored SIGTERM, late clean exit, non-zero exit, preparation cancellation, retry, restart failure, clean preparation, requested stop, and unrelated crashes. - `packages/sandbox-runtime`: 1399 passed, 3 skipped; Ruff check/format and mypy passed. - `packages/modal-infra`: 432 passed with the local sandbox-runtime checkout; Ruff check/format passed. Closes COL-221. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/006a6e47ab65d935f87d3badcd479d6b)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved sandbox preparation reliability: after a preparation failure or timeout, Docker can recover so preparation can be retried. * Improved handling of Docker restarts: monitoring resumes after recovery, and recovery failures are reported. * Corrected crash reporting so requested Docker stops are not mistaken for unexpected crashes. * Improved timeout handling to keep preparation, Docker operations, and snapshot capture within supported time limits. * Improved shutdown responsiveness: shutdown preparation persists the session and stops execution without waiting for Docker preparation to finish. <!-- 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>
…urray#2139) ## Summary When a `modal-vm` VM is created or restored, `SandboxTunnels.resolve` returns the tunnel URLs Modal has published. A missing port is not an error, and create/restore still return the handle with a partial map. `test_launch_returns_handle_despite_tunnel_failures` covers that case. The lookup-only `POST /api-resolve-vm-sandbox` added in ColeMurray#2115 rebuilds the same tunnel set from the VM's launch tags. However, `recover_vm_access` returns `409 race_pending` unless every enabled service (code-server, desktop, terminal) and every extra tunnel port has a URL. Suppose a create/restore response is lost and one exposed port has no tunnel. Every lookup of that VM then returns `race_pending`, even though the original request would have returned the handle. Each lookup repeats the same check against the same provider state, so if the port is never published, the lookup can never succeed. On the launch side, Modal's SDK (1.4.3) caches the first non-empty `Sandbox.tunnels()` result on the sandbox object. The launch path's own retries therefore get that same map back and return it as final. I reproduced this on `main` (eef911f) using the mocks from the existing tests. The VM had code-server, desktop and terminal enabled and `tunnelPorts: [3000, 3001]`, and `Sandbox.tunnels()` published every port except 3001: - Create returned the real VM ID, the three service URLs and `{3000: ...}`. - Five resolve calls against the same VM state then all returned `409 race_pending`. Each call used a fresh `from_name` object. On the control-plane side, `race_pending` is treated as an unknown startup outcome. I checked the effect with a throwaway lifecycle test (not included) that used the `vm-resolve.test.ts` fixture and returned `race_pending` on every lookup: - The spawn loop and the bridge-attach loop each retried until their bounded window ran out (44 lookups in total), and then stopped. - The sandbox row kept the pending `modal-vm-session:` reference. No code-server, desktop or tunnel URLs were stored, including the ones Modal did publish. - For a restore, the shutdown record also stayed in `restoring` with `restoreInvoked: true`, and work admission reported `held`. Stopping the VM still works through the pending reference, because the Modal stop endpoint resolves it by name and checks ownership. ## Changes - `recover_vm_access` now returns the URLs `SandboxTunnels.resolve` produced, as create/restore do. It returns `race_pending` only when the VM exposes ports but none of their URLs could be read. That happens when every `tunnels()` attempt failed or returned none of the exposed ports. This keeps the retry ColeMurray#2115 added for tunnels that are not yet visible. - The ownership and launch-metadata checks are unchanged. `find_owned_vm` and `parse_vm_service_launch` still run first, and legacy allocations still resolve only the VM ID. - `docs/MODAL_DOCKER.md` now describes the narrower `race_pending` condition. One trade-off: if Modal publishes a VM's tunnels incrementally, a lookup made between publications now returns the partial map instead of retrying. The launch path already returns the partial map in that situation. ## Tests - `test_resolve_returns_the_partial_tunnels_launch_would_return` replaces `test_resolve_retries_when_enabled_tunnel_is_missing`, which asserted the old behaviour. It is in `tests/test_vm_resolve.py` and runs once each with the code-server, desktop, terminal or an extra port missing. It checks that resolve returns the real VM ID and exactly the published URLs, and does not create, terminate or write to the VM. All 4 cases fail on `main` with `409 race_pending` and pass with this change. - `test_resolve_retries_while_no_tunnel_is_readable` checks that resolve still returns `race_pending` when no tunnel URL can be read. It passes on `main` and with this change. It fails if the check is removed entirely. - In `packages/modal-infra`, `uv run pytest tests/ -q` passes (432 tests). `ruff check` and `ruff format --check` pass on the touched files, and Prettier passes on the doc. Not tested: - Nothing here ran against live Modal. I have not observed Modal publishing only some of a sandbox's `encrypted_ports`; the reproduction uses the same `Sandbox.tunnels()` subset that the existing launch tests model. - No control-plane code changed, and I did not rerun the control-plane suites. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * VM resolution now returns available tunnel URLs even when some service or app tunnel URLs are unavailable. * The `race_pending` error is returned only when none of the exposed tunnel URLs are readable; creation and restoration behavior is unchanged. * **Documentation** * Updated the error description to clarify that resolution can return a partial tunnel map. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…eMurray#2138) ## Summary When a deployment has no verified Docker image, `api_create_sandbox` returns HTTP 501 with detail `docker_not_available`. `SandboxLauncher.launch` raises it while choosing the base image. That happens before `_launch_docker_sandbox` retires the predecessor, looks up the named allocation or calls `Sandbox.create`, so the request definitely created nothing. The existing `test_docker_launch_without_a_provisioned_image_never_uses_the_default` confirms that no create call is made. The modal-vm startup path still treats this response as an unknown outcome. There are three places that do this: - `ModalClient.postJson` wraps every VM-startup 5xx in `ModalVmStartupError("unknown")`. - `ModalSandboxProvider.isUnknownStartupError` treats any `ModalApiError` with status >= 500 as unknown. - `ModalSandboxProvider.classifyError` makes every modal-vm 5xx transient. Reproduced on `main` in a lifecycle test. The create returns 501 `docker_not_available` and resolve reports `not_visible`, as it does when nothing was allocated. The manager polls `resolveVmSandbox` 22 times and fails the attempt at 210 s (`PENDING_VM_REFERENCE_MATERIALIZE_BOUND_MS`) with "The VM allocation did not appear for this attempt. Please retry." Because that error is transient, the circuit breaker is not incremented. On a deployment missing the image, each spawn therefore waits about 3.5 minutes before failing, tells the user to retry, and never trips the breaker. ## Changes - `client.ts`: add `isAmbiguousModalVmLaunchError`. It returns true for 5xx, except for the `docker_not_available` detail. `postJson` now uses it, so this response reaches callers as the original `ModalApiError`. - `modal-provider.ts`: `isUnknownStartupError` and the modal-vm branch of `classifyError` use the same predicate. The 501 falls through to `classifyErrorWithStatus` and becomes permanent, which is how the standard `modal` backend already classifies it. Other 5xx responses, including plain 500s, remain unknown/transient. - `docs/MODAL_DOCKER.md`: document the 501 detail next to the typed 409 details. The classification uses the typed `detail`, following the existing 409 vocabulary. It is not a bare 501 status carve-out. ## Tests - `vm-resolve.test.ts`: new test "fails a create rejected before allocation without resolving, counting the failure". It checks that resolve is never called, the sandbox is `failed`, and `spawn_failure_count` is 1. It fails on `main`, where the breaker count stays 0 after 22 resolve calls. - `client.test.ts`: new test "keeps a VM create rejected before allocation as its HTTP error". It fails on `main`, where the client returns `ModalVmStartupError`. - `modal-provider.test.ts`: added a 501 / `docker_not_available` row to the VM launch classification table (not unknown, permanent). The row fails on `main`. The expected error type is now an explicit column. The existing rows keep their previous expectations. - Reverting either provider call site on its own makes the provider row and the lifecycle test fail again. - `packages/control-plane`: unit suite (331 files / 5376 tests), `npm run typecheck` and eslint on the touched files pass. Integration files `sandbox-shutdown.test.ts` and `modal-backend-builds.test.ts` pass (15 tests). The full integration suite was not run. - `packages/modal-infra` is unchanged. `test_sandbox_launch.py`, `test_web_api_create_sandbox.py` and `test_docker_launch.py` pass (142 tests). Not tested against a real Modal deployment without a Docker image. The failure message is now "Failed to create sandbox with HTTP 501", which does not include the detail text; this PR does not change that wording. The auth-misconfiguration 503 from `require_auth` is also returned before any allocation. It is left ambiguous because it has no typed detail and 503 is also a gateway status. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Sandbox launches now treat the `docker_not_available` response as a permanent failure rather than attempting to resolve the launch. The request fails before existing resources are retired or new ones are allocated. * Other server-side errors continue to be classified according to their status. * **Documentation** * Updated Docker deployment guidance to describe the unavailable-image response and its effect on sandbox creation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…rray#2144) ## Summary - Record the actual checkout versus the researched baseline, current state/method ownership, and a 15-invariant compatibility evidence matrix in `docs/plans/sandbox-lifecycle-refactor-baseline.md`. - Add deterministic characterizations for encrypted resume/bridge commits across row changes, VM foreground-to-bridge token handoff, rejected-allocation retry and handle retention, and boot-budget termination-guard ordering. - Separate existing behavior gaps (unscoped fresh/restore artifact writes, attach-time status recheck, prior-generation handle clearing, and VM finalizer overlap) from this behavior-preserving refactor. No production code or schema changes. ## Verification - Pre-edit: shared build; 600 focused unit tests; 55 Workerd integration tests; control-plane typecheck; boundary lint; `git diff --check` passed. - Post-edit: 608 focused unit tests; 55 Workerd integration tests; 278 related session tests; control-plane typecheck; boundary lint; targeted ESLint and Prettier; `git diff --check` passed. - The first targeted test run expected two broadcasts in the new guard test; the existing path emits three. The characterization was corrected before the passing re-run. No live provider or full suite/bundle validation is claimed. COL-241 / prerequisite to COL-242. No deployment. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/7a16f985831db82710ec590beeeda6e3)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Tests** - Added coverage for sandbox lifecycle alarms, rejected-allocation cleanup, and VM resolution when lookups are inconclusive. - Added tests for provider resume behavior when sandbox records or bridge references change during encryption. - **Documentation** - Added a verification baseline outlining lifecycle compatibility checks, coverage limits, and command results. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…49) (ColeMurray#2146) ## Summary - Pass a whole-second integer timeout to the Modal VM Docker preparation `exec` call without extending the capture budget. - Treat only `modal.exception.SandboxTimeoutError` from `terminate(wait=True)` as a completed stop in session stop, prior VM retirement, and image-build termination. Preserve other errors and the build termination exit-code log. - Add regression tests using Modal's protobuf request validation and cover timed-out stops through all three paths and the stop endpoint. ## Verification - Confirmed the new regressions fail before the fix with the protobuf float `TypeError` and unhandled sandbox timeout. - `uv run pytest tests/ -q` (442 passed) - `uv run ruff check` - `uv run ruff format --check` Only modal-infra web-function code changes; no VM image rebuild or control-plane changes are required. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/701cf22bead64ab78622ded0d5d3ddff)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Sandbox shutdown now completes gracefully when the hosting service reports a termination timeout, while other timeout errors continue to propagate. * Docker preparation now uses an integer execution timeout capped by the remaining snapshot time and control timeout. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary Adds `openai/gpt-6.1-sol` to the shared model catalog and the ChatGPT subscription allowlist, following ColeMurray#2028. It appears in the existing model selectors and bot model overrides, with `medium` as the default reasoning effort. The published OpenCode catalog lists `low`, `medium`, `high`, `xhigh`, and `max` for this model. `none` is not supported. Existing models and deployment defaults are unchanged. ## Changes - Add the catalog entry and subscription allowlist ID. - Extend the existing model and subscription-plugin tests. - Add the published model metadata to the frozen wire-test fixture and record its source and subset hashes. - Update the available-models reference and the public docs table required by the current catalog-consistency test. ## OpenCode compatibility The pinned OpenCode 1.18.29 passes the existing wire tests with GPT-6.1 Sol and all five reasoning efforts. These tests use a local mock endpoint, not live OpenAI or ChatGPT requests. No OpenCode upgrade is needed for the tested request format. Sandbox images and existing repository/environment prebuilds need rebuilding to include the current OpenCode model catalog, as with ColeMurray#2028. ## Validation - Typecheck, ESLint, changed-file Prettier checks, and Ruff passed. - All TypeScript workspace test suites passed, including 1,062 shared and 1,985 web tests. - Codex plugin tests: 5 passed. - Pinned OpenCode wire tests: 3 passed. - Sandbox runtime tests: 1,405 passed, 3 skipped. Local test runs required disabling Node 26's experimental web storage and using short Python temporary paths with isolated Git configuration. No repository changes were made for these environment settings. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added GPT-6.1 Sol to the available OpenAI models. It supports low, medium, high, extra-high, and maximum reasoning effort, with medium as the default. * GPT-6.1 Sol is available for use with Codex OAuth. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - Include the extracted `execution_complete` error in failed Linear completion messages, falling back to the signed callback's error when event data lacks one. - Preserve partial agent text on failures and retain the existing generic copy when no reason is available. - Add signed callback tests for partial-text, no-text, and no-reason failures through the Linear comment fallback. ## Verification - `npm test -w @open-inspect/linear-bot` (264 tests passed) - `npm run lint -w @open-inspect/linear-bot` - `npm run typecheck -w @open-inspect/linear-bot` - `npx prettier --check packages/linear-bot/src/callbacks.ts packages/linear-bot/src/callbacks.complete.test.ts` - `git diff --check origin/main...HEAD` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/571d5c83b93e21c79434656a4d236624)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Failed task completions now provide clearer error messages, including available partial results. * When multiple error details are available, the most relevant one is shown. Sensitive or oversized details are omitted for safety. * If no useful error details or partial results are available, a general failure message is displayed. <!-- 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 Extract stateless sandbox launch-input and image mechanics from `SandboxLifecycleManager` into `sandbox/lifecycle/launch-context.ts`, the second increment of COL-240. COL-241 characterization is already integrated in the base branch. - Give launch context narrow environment/repository readers, default model and MCP/Slack lookup config, provider metadata, image lookup, and a lazy logger. It has no lifecycle storage, shutdown, admission, reservation, or provider-operation authority. - Move model/harness defaults, ordered repository fields/base SHAs, MCP/Slack resolution, persisted-setting normalization, timeout conversion, and image lookup/best-effort explicit invalidation. Reuse the existing image evaluator. - Keep manager-owned mode selection, generation/token reservation, provider dispatch, pending-reference/recovery recording, failure accounting, and confirmed-unavailable base-image retry/identity rotation. - Preserve the existing await points and differing fresh/restore integration lookup order. Resume gains no env/repository/integration/image reads; bridge timeout resolution remains behind pending-reference eligibility. - Add 44 direct narrow-dependency cases, three exact-payload/effect-order manager cases, and a lazy bridge-settings regression. Retain existing image fallback, identity rotation, VM lifetime, early-connect, and shutdown coverage. - Add the previously absent repository design document with actual ownership, related-work status, validation evidence, and limitations. COL-161 feature work and COL-156 broader construction changes are not included. ## Validation Node v24.20.0, npm 11.19.0; required package checks run sequentially from repository root: - `npm run build -w @open-inspect/shared` passed. - `npm test -w @open-inspect/control-plane -- src/sandbox/lifecycle` passed: 17 files / 547 tests (48 new cases). - `npm run test:integration -w @open-inspect/control-plane -- sandbox-early-connect sandbox-shutdown` passed: 2 files / 20 tests. - `npm run typecheck -w @open-inspect/control-plane` passed all four configurations. - `npm run lint -w @open-inspect/control-plane` passed. - `npm run test:lint-sandbox-boundaries` passed: 2 tests. - Targeted `npx prettier --check` on all nine touched files passed. - `git diff --check` and staged/base diff whitespace checks passed. Initial validation caught two test-only errors: the new bridge fixture used an ineligible pending status, and a resume mock widened its success literal to boolean. Both were corrected; the lifecycle suite and typecheck passed on rerun. Commit hooks also passed ESLint and Prettier. ## Scope And Handoff No schema, wire protocol, runtime, provider backend, timeout/retry policy, deployment, or live-provider verification changes. Workerd checks use provider substitutes; full-story/bundle verification remains COL-247. Existing T1 behavioral gaps are not fixed or hidden. Integrate this PR before COL-243 starts. Issue: https://linear.app/colemurray/issue/COL-242 --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/c881621062e6bf0a04c60f35312dc095)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Sandbox launches continue with the base image when a prebuilt image is unavailable or its lookup fails, rather than blocking sessions. * Pending sandbox connections are resolved only when session and sandbox references match, preventing resolution for unrelated or ineligible sandboxes. * Configured timeouts return a clear error when the selected provider does not support them. * **Reliability** * Launch and restore flows handle missing or unavailable settings and integrations more consistently, including fallback behavior for image, notification, and server lookups. <!-- 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 Follow-up to COL-242 / PR ColeMurray#2148, which merged while this requested class conversion was being committed. This branch is based on the updated `main` and contains only the class conversion, not the already-merged extraction. - Replace `createSandboxLaunchContext` with an explicitly constructed `SandboxLaunchContext` class. - Inject the same narrow constructor inputs: environment/repository reader, provider metadata, model/MCP/Slack config, optional image lookup, and lazy logger. - Store dependencies in individual readonly fields; remove the redundant method interface and factory instead of keeping compatibility wrappers. - Update the manager and direct test fixture to use `new SandboxLaunchContext(...)`, and update ownership documentation. Method signatures, payloads, lookup/error policies, synchronous image eligibility, await placement, settings/timeout resolution, lazy logging, and lifecycle authority are unchanged. Constructor assignment performs no session reads, lookups, or logging. ## Validation Rerun on this branch after applying the class-only commit to updated `main`: - `npm run build -w @open-inspect/shared` passed. - `npm test -w @open-inspect/control-plane -- src/sandbox/lifecycle` passed: 17 files / 552 tests. - `npm run test:integration -w @open-inspect/control-plane -- sandbox-early-connect sandbox-shutdown` passed: 2 files / 20 tests. - `npm run typecheck -w @open-inspect/control-plane` passed all four configurations. - `npm run lint -w @open-inspect/control-plane` passed. - `npm run test:lint-sandbox-boundaries` passed: 2 tests. - Targeted Prettier checks on all four touched files passed. - `git diff --check` and base-branch whitespace checks passed. Existing constructor-dormancy, lazy-logger, exact-payload, ordered-await, ineligible-image no-await, resume, and bridge regressions pass without new behavior assertions or altered expectations. No deployment or live-provider verification was performed. Issue: https://linear.app/colemurray/issue/COL-242 --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/c881621062e6bf0a04c60f35312dc095)* Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…rray#2145) ## Summary - Add always-enforced visibility, team-scope, and collaborator routes, with grant checks, descendant handling, active-user validation, and session/team audit rows. - Accept team and visibility on session creation, inherit private ownership and collaborators in child sessions, and return row-derived scope and capabilities in session snapshots and lists. - Add the workspace `requireTeamOnCreate` setting and Settings > Teams toggle; update authentication documentation and the changelog. ## Checkpoint - Based on `main` at `671661e`, with D1 migration `0083` already present. No D1 or DO migration added. `TEAMS_ENFORCEMENT` defaults to `shadow`; existing routes keep `off`/`shadow` legacy handling for non-private rows, while these new mutations always run the resolver. The persisted D1 row, not the path or runtime participants, determines access. - Verified `handleCreateSession` at `routes/session-create.ts:73`, `handleSpawnChild` at `routes/session-child-spawn.ts:54`, `handleSessionSnapshot` at `routes/session-runtime-proxy.ts:164`, and `SessionIndexStore.listByParent` at `db/session-index.ts:766` on the base. The child prompt handler is at `routes/session-children.ts:83-100`, not the cited `279-283` (those lines describe another route); its child/parent check was retained and extended. No behavioral premise needed redesign. - Initial route tests were red with 404s before the new endpoints were mounted. The first complete unit run reported `Test Files 2 failed | 330 passed (332)` and `Tests 3 failed | 5398 passed (5401)` from expectations predating response fields. The first complete integration run reported `Test Files 3 failed | 121 passed (124)` and `Tests 6 failed | 1493 passed | 1 skipped (1500)` from route snapshots, Viewer denial reasons, and off-mode snapshot membership loading. The expectations and route snapshots were corrected; subsequent suites passed. - Final validation: `npm run build -w @open-inspect/shared`, `npm run typecheck`, `npm run lint:fix`, `npm run lint:sql-portability`, `npm run format:check`, `npm run lint:complexity` (report-only), `npm test -w @open-inspect/shared` (1,062 tests), `npm test -w @open-inspect/control-plane` (5,401 tests at full-suite run), `npm run test:integration -w @open-inspect/control-plane` (1,499 passed, 1 skipped at full-suite run), and `npm test -w @open-inspect/web` (1,989 tests) passed. Focused unit/integration tests and typecheck passed after the final small store/route adjustments. - Deliberately left out repository-grant management UI/API, scoped sandbox tokens, and session-page team controls: those are separate follow-up work. No migration or credential expansion is included here. Issue: https://linear.app/colemurray/issue/COL-201/teams-pr-8-control-plane-docs-visibility-move-and-collaborator-routes-team-fields-on-post-sessions-child-inheritance-docsauthmd --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/7c08ddf567238ca57030de14d183e3ea)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Sessions can be assigned to teams and set to team, workspace, or private visibility. Access reflects team membership, ownership, and collaborator status. * Authorized users can change visibility, move sessions between teams, and manage collaborators; changes are recorded in the audit log. * Workspace managers can require team assignment when creating sessions. Team assignment checks membership and repository access. * Session lists and snapshots show available access capabilities. * Team access enforcement supports off, shadow, and on modes, with shadow as the default. Private-session restrictions apply in every mode. * **Documentation** * Updated access guidance to explain how team membership, session visibility, and repository permissions affect access. <!-- 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 ======= - Raise the PyJWT[crypto] version floor from >=2.9.0 to >=2.14.0 in `packages/modal-infra/pyproject.toml` and `packages/sandbox-runtime/pyproject.toml`. Resolves 20 known advisories (1 critical, 6 high) affecting versions below 2.14.0. - Re-lock both packages (`uv lock --upgrade-package pyjwt`), pulling in PyJWT 2.15.1. - Regenerate `packages/sandbox-images/locks/runtime.txt`, which is exported from the sandbox-runtime lockfile and pins PyJWT for the sandbox image build. https://github.com/advisories?query=affects%3Apyjwt Test plan ========= - [x] `pytest tests/ -v` in `packages/modal-infra` — 442 passed - [x] `pytest tests/ -v` in `packages/sandbox-runtime` — 1405 passed, 3 skipped - [x] `ruff check .` and `ruff format --check .` in both packages — clean <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated the versions of the JSON Web Token libraries used across application components. This keeps token-related packages aligned with newer releases. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…oleMurray#2122) ## Problem The docs site's one-time setup (`packages/docs/README.md:76-83`) sets `docs_site_enabled = true` and `docs_custom_domain` for a local `terraform apply`. The `Terraform` workflow's plan and apply jobs pass neither variable (`.github/workflows/terraform.yml:235-321` and `:426-512` have no `TF_VAR_docs_*` line), so CI evaluates `docs_site_enabled` with its default `false` (`terraform/environments/production/variables.tf:835-839`). `module "docs_site"` uses `count = var.docs_site_enabled ? 1 : 0` (`terraform/environments/production/docs-vercel.tf:8`), so the next `terraform apply -auto-approve` on `main` after the site is provisioned plans to destroy the docs Vercel project and its domain. `scripts/terraform-workflow-contract.test.mjs` did not catch this because it pins only two named inputs (Daytona snapshot memory and the classifier Anthropic key, lines 29-40). It never compares the workflow against the variables that are declared. A second problem shows up once `docs_custom_domain` is threaded the way other optional hostnames are. Actions renders an unset `vars.X || secrets.X` as `""`, and Terraform keeps `""` rather than treating it as null. `docs-vercel.tf:26` passes it straight to `modules/vercel-project`, which creates a `vercel_project_domain` whenever `custom_domain != null` (`terraform/modules/vercel-project/main.tf:40`). `locals.tf:98` would also produce `docs_site_url = "https://"`. ## Change - Pass `TF_VAR_docs_site_enabled` (default `'false'`) and `TF_VAR_docs_custom_domain` through both the plan and apply jobs. They use the same `vars.X || secrets.X` resolution as the other optional settings. - Add a `local.docs_custom_domain` that turns null, empty and whitespace-only values into `null` and trims the value. `docs-vercel.tf` and `docs_custom_domain_url` use it. The local follows the `web_custom_domain` normalization in `locals.tf:32-36`. - The contract test now checks that every variable in `terraform/environments/production/variables.tf` appears exactly once as `TF_VAR_<name>:` in each job. It exempts `control_plane_*`, which the "Stage SchedulerDO deletion migration" step writes to an `auto.tfvars.json`, and `project_root`, which is a checkout path. On `main` these are the only unthreaded variables apart from the two docs ones. - Document `DOCS_SITE_ENABLED` and `DOCS_CUSTOM_DOMAIN` in `packages/docs/README.md`, the CI/CD section of `docs/GETTING_STARTED.md` and `terraform/README.md`. Alternative considered: make the docs project independent of CI by not reading `docs_site_enabled` in the workflow. That would change how the variable is meant to work, so I kept to threading it like every other opt-in flag (`ENABLE_LINEAR_BOT`, `ENABLE_GITHUB_BOT`). ## Reproduction (on `main` at 700f914) ``` $ node --test scripts/terraform-workflow-contract.test.mjs # new test only ✖ Every production Terraform variable reaches plan and apply + [ 'docs_site_enabled', 'docs_custom_domain' ] - [] $ terraform test -filter=tests/docs_site.tftest.hcl # new run only run "docs_site_ignores_an_empty_custom_domain"... fail condition = module.docs_site[0].custom_domain == null - "" + null ``` ## Tests - `npm run test:terraform-workflow-contract`: 3 pass - `npm run test:node-version-workflow-contract`: 1 pass - `terraform/environments/production`: `terraform fmt -check -recursive`, `terraform validate`, `terraform test`: 63 passed, 0 failed. This includes the new `docs_site_ignores_an_empty_custom_domain` and `docs_site_attaches_a_configured_custom_domain` runs. Run locally with Terraform 1.15.3; CI pins 1.14.8. - `npm run typecheck -w @open-inspect/docs` - `prettier --check` on the touched Markdown, YAML and test files I did not run a real Terraform plan against a provisioned docs project. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Production deployments can now be configured to keep the documentation site and optionally use a custom domain. The site setting defaults to off; without enabling it, a deployment may remove the docs project and domain. * Blank or whitespace-only custom domains now fall back to the default `vercel.app` address. * **Documentation** * Added setup guidance for configuring the documentation site and its custom domain. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…2160) ## Summary - Pass `TEAMS_ENFORCEMENT` to both Terraform plan and apply as `TF_VAR_teams_enforcement`. - Default to `shadow`, matching the production Terraform variable, while allowing deployments to configure `off` or `on` through a repository variable or secret. The contract test added by ColeMurray#2122 fails on `main` because `teams_enforcement` was added after that PR branched but was not present in either workflow job. ## Verification - Reproduced `npm run test:terraform-workflow-contract` failure on current `main` (`teams_enforcement` missing). - `npm run test:terraform-workflow-contract` (3 passed) - `npm run test:node-version-workflow-contract` (1 passed) - `npx prettier --check .github/workflows/terraform.yml` - `git diff --check origin/main...HEAD` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/0ed93eaf0b664e8b81a8fa9634967b50)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Terraform plan and apply runs now use the configured team-enforcement mode. They prioritize the repository setting, then the secret value, and default to shadow mode if neither is available. This keeps the selected mode consistent across both stages. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…2152) ## Summary Fixes COL-251. The coordinator retries classified failed shutdown captures from its own alarm when `captureByMs` is due, without depending on a refused runtime reconnect or a user clicking Retry. Earlier wake-ups and capture failures re-arm the existing deadline. - Separate automatic retry policy from authenticated recovery actions. The private `captureFailure` marker in the existing JSON record is set only for terminal capture failures and interrupted captures. Generic `fail()` only publishes failure; checkpoint and restore uncertainty cannot authorize unattended capture/retirement. - Allow capture retries only on the alarm handler's first preservation pass. The post-projection pass still checks shutdown deadlines and watchdog holds, but cannot start a second retry in the same delivery. - Re-arm classified capture failures after releasing `activeOperation`, including failures after an in-flight alarm has been consumed. - Keep manual recovery actions, the fixed 30-minute Retry window, runtime keepalive, and alarm/reconnect retry logging unchanged. Unclassified historical holds remain manual-recovery only; interrupted `capturing` records are classified during restart recovery. - Add 27 regression cases across the PR, including timed-out retries through the composed handler, checkpoint uncertainty during draining, JSON provenance round-trips, and the public projection boundary. No SQL migrations or new database columns, Modal-side changes, or source cleanup after the Retry window closes. The optional internal JSON marker is the only persisted-record addition. ## Verification The latest two lifecycle regressions were reproduced before implementation: one delivery started another capture after its retry timed out, and checkpoint uncertainty during draining scheduled unattended recovery. Both now pass. - `npm test -w @open-inspect/control-plane -- --maxWorkers=2`: 5,492 passed across 334 files - Focused coordinator, safety, repository, alarm handler/scheduler, and rejected-allocation suites: 159 passed - `npm run test:integration -w @open-inspect/control-plane -- test/integration/sandbox-shutdown.test.ts test/integration/session-lifecycle-alarm-recovery.test.ts --maxWorkers=1`: 14 passed - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint` - Prettier check and `git diff --check` - Independent review found no actionable findings. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/37204cb097fb963d7b419250261344aa)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Reliability** * Failed or uncertain shutdown captures can be retried automatically when their capture deadline arrives, without requiring a reconnect. * Early alarms preserve the scheduled capture deadline; retry windows advance in five-minute increments and stop at a defined limit after shutdown. * Retries are coordinated with reconnects and active captures, including after a restart, to avoid duplicate or overlapping attempts. * Superseded or ineligible captures and failed restores without capture deadlines are not retried. <!-- 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 - Replace the command menu's solid accent selection background with the existing neutral `muted` surface and `foreground` text, matching other menu highlights. - Keep descriptions and shortcuts readable during both pointer hover and keyboard selection in light and dark themes. - Add a regression test covering pointer selection, keyboard selection, and the neutral highlight classes. ## Root Cause The selected row used a brown accent background while descriptions and shortcuts retained explicitly muted gray text. Their light-mode contrast was approximately 1.28:1. The neutral highlight improves that contrast to approximately 5.15:1 without changing global theme tokens or introducing child-style overrides. ## Validation - Command menu tests: 19 passed. - Global keyboard shortcut tests: 2 passed. - Shared package build and web typecheck passed. - ESLint, Prettier checks, and `git diff --check` passed. - Visually inspected the real application at `http://localhost:3000` with browser-mocked authentication, authorization, and session-list responses. - Verified hover, keyboard selection, settings search, exhaustive-search handoff, and Escape dismissal. - Uploaded viewport screenshots: desktop light and dark at 1440x900, mobile light at 390x844. ## Visual Evidence - Light mode artifact: `725376067a5e3b8b525a0d3cd6b63d86` - Dark mode artifact: `135057d55fc85bd4c172f39034f7c92b` - Mobile light artifact: `2ee196be6250689ab0f75ec2eeaf4e21` ## Existing Accessibility Finding The scoped axe scan reports an existing `aria-required-children` violation because the cmdk listbox contains separator elements. This change does not alter the menu structure; that finding is outside the contrast fix. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/c794d8814455493ca3ce9de38f4b70aa)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Style** * Selected command-menu options now use a muted background and foreground text with a visible inset ring instead of accent colors. Descriptions and shortcut labels remain muted for selected options. * **Tests** * Added coverage confirming the updated selected-option styling for both pointer and keyboard selection. <!-- 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 Implements [COL-243](https://linear.app/colemurray/issue/COL-243), the third increment of COL-240. The prerequisite COL-241 and COL-242 changes are integrated in starting HEAD `44ca1a9`; no deployment is included. - Add internal `sandbox/lifecycle/sandbox-access.ts` for access storage mechanics, terminal JWT signing/reuse, retirement and notifications. - Construct access before shutdown and manager. Shutdown receives `access.retireShutdownAccess` directly, removing the manager callback cycle and obsolete forwarding method. - Keep lifecycle eligibility, startup claims, admission flags, generation arbitration and caller error boundaries in manager. Keep encryption and atomic completion in the unchanged repository. - Narrow manager's storage/config contracts and extend the existing ESLint boundary tests so session/platform consumers cannot import the access collaborator. - Retain assembled-manager, session access-reader, real repository and Workerd coverage, with new extraction-specific regressions and updated ownership notes. ## Internal Operations and Composition Access exposes `clearAccess`, `retireShutdownAccess`, `storeCodeServer`, `storeVnc`, `storeAndBroadcastTunnelUrls`, `storeTtyd`, `mintTtydToken`, `reusableTtydToken`, `broadcastSandboxDashboardUrl` and `broadcastProviderAccessIfConnected` only to internal composition/lifecycle code. The graph is repository/socket/messenger leaves -> access -> shutdown -> manager. Access has narrow storage/broadcast/socket/capability/logging dependencies, no manager reference, mutable lifecycle state, encryption key or retained signing key. Logging resolves the current session context at use, and construction invokes no runtime operations. URL-only retirement retains credentials on resumable providers when supported, falls back to full clearing otherwise, and always clears tunnels and notifies before shutdown detaches with `1000`, `Sandbox state preserved`. Other paths retain their differing order. Dashboard, tunnel and connected-access notifications remain distinct and repeatable. Fresh/restore retain individual artifact writes and existing await points. Resume/bridge retain `completeProviderResume`; only bridge supplies the expected pending reference. Resume secret-read sequencing remains unchanged, and JWT reuse validation is synchronous, so disabled terminal access gains no await. Missing/expired tokens and hash-only restarts cannot invent terminal access. The single terminal TTL and launch/session/sandbox claims are unchanged. ## Changed Paths - Production: `packages/control-plane/src/sandbox/lifecycle/{manager,sandbox-access}.ts`, `packages/control-plane/src/session/components.ts`. - Unit coverage/composition: lifecycle `{manager,sandbox-access,vm-resolve,launch-orchestration,pending-vm-respawn}.test.ts`, `test-helpers.ts`, and `src/session/sandbox-repository.test.ts`. - Workerd coverage/composition: `test/integration/{sandbox-lifecycle-harness.ts,sandbox-shutdown.test.ts,session-components.test.ts}`. - Boundaries: `eslint.config.js`, `scripts/lint-sandbox-boundaries.test.mjs`. - Ownership/gaps: `docs/plans/{sandbox-lifecycle-manager-refactor,sandbox-lifecycle-refactor-baseline}.md`. ## Verification Node `v24.20.0`, installed dependencies; shared built first and expensive checks run sequentially. | Check | Result | | --- | --- | | Pre-edit focused unit baseline | 23 files, 742 tests passed | | `npm run build -w @open-inspect/shared` | Passed | | `npm test -w @open-inspect/control-plane -- src/sandbox/lifecycle src/session/sandbox-access src/session/sandbox-repository src/session/sandbox-shutdown` | 24 files, 776 tests passed | | `npm run test:integration -w @open-inspect/control-plane -- sandbox-early-connect sandbox-shutdown sandbox-state-retention` | 3 files, 55 Workerd tests passed | | `npm run test:integration -w @open-inspect/control-plane -- session-components` | 1 file, 5 tests passed | | `npm test -w @open-inspect/control-plane` | 335 files, 5,498 tests passed | | `npm run typecheck -w @open-inspect/control-plane` | All four TypeScript configurations passed | | `npm run build -w @open-inspect/control-plane` | Worker and Node bundles passed; existing bundle-size warnings emitted | | `npm run lint -w @open-inspect/control-plane` | Passed | | `npm run test:lint-sandbox-boundaries` | 2 tests passed | | Additional ESLint on touched integration/config files | Passed | | Prettier check on all touched files | Passed | | `git diff --check` and committed base diff check | Passed | An initial new Workerd fixture accepted the WebSocket peer before DO adoption and failed before its assertions; correcting the fixture ordering made the retirement regression pass. No baseline/environment blocker remains. Full Workerd integration sweep and live-provider canaries were not run; Workerd provider substitutes are not live canaries. ## Separate Existing Behavior The real-SQL reproducer `characterizes the unguarded fresh/restore artifact write across replacement` demonstrates that an old per-artifact write can publish to a replacement after encryption yields. This extraction deliberately preserves that gap rather than adding generation checks or replacing those writes with atomic resume completion. Another characterization pins the existing pre-commit boundary: a successful saved resume followed by terminal-secret read failure can fail the attempt and hold recovery. Access-write/publication failures after committed recovery retain their existing success treatment. Both distinctions are documented; neither is fixed incidentally. No schema, wire, provider-backend, runtime, timeout/retry or access-eligibility changes. Merge this increment before COL-244; this PR does not deploy. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/bac90a60f1ba67df3bca099ab80e15fa)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Centralized sandbox access and shutdown handling, including credentials, terminal access, tunnel links, and notifications. * Updated session and shutdown flows to use the shared handling. * **Tests** * Expanded coverage for access updates, shutdown recovery and cleanup, resume and restore failures, and lifecycle race conditions. * **Documentation** * Clarified sandbox access ownership, lifecycle behavior, failure cases, and verification guidance. <!-- 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>
…2162) ## Summary Fix user-authored PR creation when a browser continues a bot-created session. Browser WebSocket participants can have a canonical user ID but no cached GitHub subject, which previously skipped OAuth resolution and selected the GitHub App token. - Resolve Better Auth credentials for the exact prompting canonical user even when the cached GitHub subject is null. - Select and verify that user's single linked GitHub account; retain fail-closed handling for a conflicting populated subject, ambiguous identities, or a substituted provider profile. - Preserve legacy participant credentials and genuine missing-account/missing-grant App fallback. The production fix changes only `session/participant-service.ts` and `session/identity.ts`. No schema, WebSocket route, token copying, or reconnect requirement is introduced; existing incomplete participants recover at the next PR credential lookup. ## Regression and TDD Incident: https://open-inspect-prod.vercel.app/session/11ccd3ae2a51d85891d19aa3e4e817cd (PR ColeMurray#2158). Wrote the Worker/D1 regression before implementation. It mints the browser participant through the real authenticated `/sessions/:id/ws-token` route in a Linear-created session, models that participant's processing prompt, and invokes the real PR handler. Better Auth resolves a correctly encrypted grant from D1; GitHub profile HTTP and PR publication are mocked external boundaries. **Red:** PR publication received `{ authType: "app", token: "push-token" }` instead of the expected browser OAuth authentication. **Green:** the same regression receives the browser OAuth authentication after the two-file fix. Additional unit coverage checks canonical account selection, conflicting/ambiguous accounts, provider substitution, unlinking, and missing grants. ## Validation - Shared package build - Control-plane unit tests (one worker) — 334 files, 5,498 passing - Focused integration tests: `create-pr`, `ws-token-participants`, `browser-auth-callback` — 26 passing - Control-plane typecheck, including Node and integration test configurations - ESLint and Prettier on changed files - `git diff --check` No production deployment or live provider canary is claimed. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved GitHub authentication when creating pull requests in sessions continued by a browser participant. Pull requests can now use that participant’s valid GitHub authorization, including when a cached GitHub identity is missing. * Improved handling of linked GitHub identities: a single canonical identity can be recovered when cached information is absent, while conflicting or ambiguous identities are rejected. Unlinked identities return no GitHub token. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…dates (ColeMurray#2159) Bumps the npm_and_yarn group with 2 updates in the / directory: [next](https://github.com/vercel/next.js) and [brace-expansion](https://github.com/juliangruber/brace-expansion). Updates `next` from 16.3.5 to 16.3.8 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/vercel/next.js/releases">next's releases</a>.</em></p> <blockquote> <h2>v16.3.8</h2> <p>This release contains security fixes for the following advisories:</p> <p>High:</p> <ul> <li><a href="https://github.com/vercel/next.js/security/advisories/GHSA-cjq9-62q9-8jv4">Server-Side Request Forgery in Image Optimization</a></li> </ul> <p>Medium:</p> <ul> <li><a href="https://github.com/vercel/next.js/security/advisories/GHSA-f87g-xv8r-7p7x">Information disclosure in Next.js App Router metadata image routes via dynamicParams bypass</a></li> <li><a href="https://github.com/vercel/next.js/security/advisories/GHSA-4jqv-mc3x-m676">Cache poisoning of SSG and ISR pages in self-hosted Next.js applications</a></li> <li><a href="https://github.com/vercel/next.js/security/advisories/GHSA-mcj8-r9mp-w47p">Cache poisoning in Next.js SSG/ISR rendering leads to cross-user content substitution and persistent denial of service</a></li> <li><a href="https://github.com/vercel/next.js/security/advisories/GHSA-3w37-wq28-93x7">Pending <code>use cache</code> fill can leak Draft Mode content into regular responses and persisted pages</a></li> <li><a href="https://github.com/vercel/next.js/security/advisories/GHSA-h694-7cp9-m8p3">Cache leak across root param values in nested 'use cache' functions</a></li> </ul> <p>Low:</p> <ul> <li><a href="https://github.com/vercel/next.js/security/advisories/GHSA-39w2-rjm5-chcv">Information disclosure in the Next.js development server's Model Context Protocol endpoint</a></li> </ul> <h2>v16.3.7</h2> <blockquote> <p>[!NOTE] This release is backporting bug fixes. It does <strong>not</strong> include all pending features/changes on canary.</p> </blockquote> <h3>Core Changes</h3> <ul> <li>turbo-tasks-backend: fix strongly consistent read hanging on a canceled task (<a href="https://redirect.github.com/vercel/next.js/issues/98931">#98931</a>)</li> </ul> <h3>Credits</h3> <p>Huge thanks to <a href="https://github.com/lukesandberg"><code>@lukesandberg</code></a> for helping!</p> <h2>v16.3.6</h2> <p>This release contains a security fix for <a href="https://github.com/vercel/next.js/security/advisories/GHSA-vcvr-r3jv-pc5j">GHSA-vcvr-r3jv-pc5j: Remote Code Execution in next/og ImageResponse</a></p> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/vercel/next.js/commit/b0fad0d45eb4c4430fda5eeeb442e8a5af08a5f6"><code>b0fad0d</code></a> v16.3.8</li> <li><a href="https://github.com/vercel/next.js/commit/719e4c67d6e92df60246f95e1d96e2dd60789a52"><code>719e4c6</code></a> [lts-active] Scope response cache keys to their source route (<a href="https://redirect.github.com/vercel/next.js/issues/218">#218</a>)</li> <li><a href="https://github.com/vercel/next.js/commit/e92db4536a7f34ae2dbda583cfa1b4e0d06d1865"><code>e92db45</code></a> [lts-active] Fix metadata propagation for deduplicated nested caches (<a href="https://redirect.github.com/vercel/next.js/issues/223">#223</a>)</li> <li><a href="https://github.com/vercel/next.js/commit/40c2ba904289a65ed2fd4633c5dfb1bdc29b52de"><code>40c2ba9</code></a> [lts-active] Match Next data paths case-sensitively (<a href="https://redirect.github.com/vercel/next.js/issues/196">#196</a>)</li> <li><a href="https://github.com/vercel/next.js/commit/2d9f50a409312696145b82b3157aadb6b1fef476"><code>2d9f50a</code></a> [lts-active] Fix MCP middleware DNS rebinding (<a href="https://redirect.github.com/vercel/next.js/issues/213">#213</a>)</li> <li><a href="https://github.com/vercel/next.js/commit/bd9214f9a32854a011bf5fe58e481dffe1bbf598"><code>bd9214f</code></a> [lts-active] Fix draft mode leaks through cross-request <code>'use cache'</code> dedupli...</li> <li><a href="https://github.com/vercel/next.js/commit/8db4a627c91e718514406ff5644ea4985dcaaae0"><code>8db4a62</code></a> [lts-active][webpack] Ensure <code>dynamicParams</code> is respected in `opengraph-image...</li> <li><a href="https://github.com/vercel/next.js/commit/e002ad68bd676bb0ed0c87bb22e3590304763e0b"><code>e002ad6</code></a> [lts-active] fix(next/image): Pin DNS resolution when fetching external image...</li> <li><a href="https://github.com/vercel/next.js/commit/4c20699e29178d444994cf5a31b8a617ca3a2c80"><code>4c20699</code></a> v16.3.7</li> <li><a href="https://github.com/vercel/next.js/commit/2521aec5815e7de2121db253d12dae9f13e25361"><code>2521aec</code></a> [backport] turbo-tasks-backend: fix strongly consistent read hanging on a can...</li> <li>Additional commits viewable in <a href="https://github.com/vercel/next.js/compare/v16.3.5...v16.3.8">compare view</a></li> </ul> </details> <br /> Updates `brace-expansion` from 5.0.7 to 5.0.12 <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/juliangruber/brace-expansion/commit/f3410159d768f56c9d9f4511d3e1b46425fc1099"><code>f341015</code></a> 5.0.12</li> <li><a href="https://github.com/juliangruber/brace-expansion/commit/33a5ef17b8d800bbfa8c52b14c39043b6aac1a96"><code>33a5ef1</code></a> Merge commit from fork</li> <li><a href="https://github.com/juliangruber/brace-expansion/commit/82479277b90f2f86263e946f9ff89689b3734568"><code>8247927</code></a> 5.0.11</li> <li><a href="https://github.com/juliangruber/brace-expansion/commit/935d78f32f335b2ff76578e5c5e877d31ae9888c"><code>935d78f</code></a> Merge commit from fork</li> <li><a href="https://github.com/juliangruber/brace-expansion/commit/df7682f386cdf2d7fef6067bc78ed70d824e1f3f"><code>df7682f</code></a> 5.0.10</li> <li><a href="https://github.com/juliangruber/brace-expansion/commit/1ade9de71f3a8719c82c61a7977121067bb55b02"><code>1ade9de</code></a> npm run format</li> <li><a href="https://github.com/juliangruber/brace-expansion/commit/6735c94873ca570bcdd6a0690033bdd3126379d3"><code>6735c94</code></a> Merge commit from fork</li> <li><a href="https://github.com/juliangruber/brace-expansion/commit/4e7046543469d31e2b324b1bf14d8606d74f7f18"><code>4e70465</code></a> chore: ensure prettier formatting (<a href="https://redirect.github.com/juliangruber/brace-expansion/issues/154">#154</a>)</li> <li><a href="https://github.com/juliangruber/brace-expansion/commit/fd7a5e34cfcd9a9df6e0ee17817807104392ecff"><code>fd7a5e3</code></a> Bump ip-address from 10.2.0 to 10.4.0 (<a href="https://redirect.github.com/juliangruber/brace-expansion/issues/152">#152</a>)</li> <li><a href="https://github.com/juliangruber/brace-expansion/commit/1790143e9aa05279b087b94104c03d3cb775e2e4"><code>1790143</code></a> Bump uuid and <code>@tapjs/processinfo</code> (<a href="https://redirect.github.com/juliangruber/brace-expansion/issues/120">#120</a>)</li> <li>Additional commits viewable in <a href="https://github.com/juliangruber/brace-expansion/compare/v5.0.7...v5.0.12">compare view</a></li> </ul> </details> <br /> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ColeMurray/background-agents/network/alerts). </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
…2158) ## Summary Closes https://linear.app/colemurray/issue/COL-202 - Add a shared, persisted active-team context reconciled against active memberships. The sidebar switcher offers Workspace, memberships, All my teams, and administrator-only All teams, and remains hidden with fewer than two active memberships. - Apply the context to sidebar snapshots and pagination, discovery filters, recent-command requests, and exhaustive search links. Add Team, Owner, and Visibility filters while preserving the existing creator-only Mine behavior. - Add team and visibility to composer defaults, warm-draft identity, and the create BFF. Read the require-team setting through `/me/teams`, block teamless required creation before warming, pass team IDs to target catalogs, and preselect team default environments without overwriting explicit draft choices. - Render session controls from server capabilities, preserve HTTP capabilities when the shipped live subscription omits them, terminate transport on token-mint 404, render not-found without cached session content, and show action denial reason codes in toasts. - Extend inbox filtering through the existing predicate builder and return effective capabilities for roots and descendants. Correct authorization documentation and the changelog. ## Checkpoint Report ### 1. Commands And Results Final validation ran sequentially. Test suites used one worker to respect resource limits. | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1` | 334 files, 5,488 tests passed | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1` | 124 files, 1,544 tests passed, 1 skipped | | `npm test -w @open-inspect/web -- --maxWorkers=1` | 235 files, 2,137 tests passed | | `npm test -w @open-inspect/shared -- --maxWorkers=1` | 64 files, 1,062 tests passed | | `npm run typecheck` | All workspaces passed | | `npm run lint:fix` | Passed | | `npm run lint:sql-portability` | Clean, existing baseline unchanged | | Changed tracked and new files checked with `npx prettier --check` | Passed | | `git diff --check` and `git diff --check origin/main...HEAD` | Passed | An earlier concurrent attempt at the full test/typecheck commands was interrupted before results. Those interrupted attempts are not counted as passes; the sequential runs above completed successfully. Integration output included existing intentional forced-eviction/error-path diagnostics, without failed tests. Focused red tests preceded implementation. Selected failure output is reproduced verbatim below; all affected files subsequently passed the final full suites. `npm test -w @open-inspect/control-plane -- src/routes/session-index.test.ts -t 'scoped inbox routes'`: ```text AssertionError: expected 200 to be 403 // Object.is equality AssertionError: expected 200 to be 400 // Object.is equality ``` Initial scoped route result: 22 failed, 47 skipped. Initial focused inbox/membership integration result: 8 failed, 6 passed, 42 skipped. `npm test -w @open-inspect/web -- src/hooks/use-active-team.test.tsx src/components/team-switcher.test.tsx src/lib/session-inbox-api.test.ts src/app/api/sessions/inbox/route.test.ts`: ```text Error: Failed to resolve import "./team-switcher" from "src/components/team-switcher.test.tsx". Does the file exist? Error: Failed to resolve import "./use-active-team" from "src/hooks/use-active-team.test.tsx". Does the file exist? Expected: "/api/sessions/inbox?teamIds%5B%5D=team_alpha" Received: "/api/sessions/inbox" AssertionError: expected undefined to deeply equal { canRead: true, …(7) } Test Files 4 failed (4) Tests 3 failed | 22 passed (25) ``` The first native combined sidebar run exposed incomplete test fixtures after the new switcher was mounted: ```text TypeError: Cannot read properties of undefined (reading 'length') Test Files 1 failed | 8 passed (9) Tests 12 failed | 80 passed (92) ``` Fixtures were updated to the actual hook contract, including scope-only aggregate switches. `npm test -w @open-inspect/web -- src/lib/session-socket/reducer.test.ts src/hooks/use-session-socket.test.tsx -t 'retains server capabilities|replaces server capabilities|production subscribed|preserves them when later subscribed'` initially reproduced the production subscription capability loss: 4 failed, 1 passed, 84 skipped. The realistic subscription fixture now passes, alongside explicit denial-replacement tests. `npm test -w @open-inspect/web -- 'src/app/(app)/(sidebar)/page.test.tsx' -t 'first prompt'`: ```text AssertionError: expected "vi.fn()" to be called with arguments: [ Array(1) ] Number of calls: 0 Test Files 1 failed (1) Tests 1 failed | 37 skipped (38) ``` The composer now preserves the first prompt's `reason_code` in its toast and inline error. Fixture-backed browser checks mounted the actual composer, switcher, active-team provider, membership hook, target picker, and discovery page. Desktop 1440x900 and mobile 390x844 viewport captures verified team switching, visibility/environment defaults, filter/search preservation, and no horizontal overflow. Uploaded artifacts: `6a614a28ad2316d04bc1aa2ce7c70f9e`, `464c84a835b2c64ec2ef3c59ec3cb303`, `1e1549940fe2f572b110b7ca98872c06`, and `caf5f893a509f61176f5e67765700ed1`. This was not a live deployment or sandbox-launch check. ### 2. Verified Facts And Drift Branched from `main` at `cf345db`; `origin/main` remained at that revision when preparing this PR. - The sidebar header, All/Mine state, discovery URL codec, composer warm-draft identity, BFF allow-list, and 4010 authorization refresh matched the supplied code locations. - The actual team API uses `defaultVisibility` and `defaultEnvironmentId`, not database-style `default_visibility`; the UI uses the API fields. - The inbox shared projection did not include capabilities and its decoder stripped them. Added an optional capability field to preserve the server response, with missing capabilities still denying controls. - HTTP snapshots carry effective capabilities, but the production WebSocket subscription snapshot omits them. Preserve the previous server-computed capability object for that incremental omission; explicit capability updates replace it. No token-mint or authorization write path was changed. - Trace export has no session response capability flag. Preserve its existing server-effective `sessions.export` grant, additionally requiring the response read capability. No migration was added. This uses D1 migration 0083 and does not change enforcement defaults. Inbox queries reuse the existing predicates: non-private list behavior treats `off` and `shadow` alike, `on` enforces team visibility, and private restrictions apply in every mode. `scope=all` remains administrator-only and does not enumerate break-glass-only private sessions. ### 3. Deliberately Excluded - Team pages, directory widening, team-page links, move dialogs, and team/session membership-management UI remain separate work. Edits to the teams route and teams response schema are localized to the `/me/teams` creation-setting field. - Repository grants and control-plane team-filtered repository/environment catalogs are not implemented here; this PR only forwards the team context. Missing grants still refuse repository-backed team creation with `target_team_missing_grant`. - Bot team selection, automation ownership, credential scoping, and enforcement-default changes are excluded. - No live deployment, external notification, migration, or credential reach expansion was performed. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/11ccd3ae2a51d85891d19aa3e4e817cd)* --------- Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
## Summary Implements COL-244, increment 4 of COL-240, on integrated COL-243 HEAD `d3de8c0e7d99620236c17b2a0fb95f09a34e5e06`. - Extract `VmStartupReconciliation` with pending registration, nullable create recovery, foreground lookup and bridge lookup, plus all five transient bridge/auth fields and the existing retry constant. - Keep mode selection, reservation/hash publication, fallback, admission flags, failure/breaker accounting and generic `claimProviderStartup` in the manager. Shutdown retains durable recovery/hold/lifetime authority; repositories retain encrypted conditional commits. - Expose explicit foreground auth registration, retry reset and finalization operations without setters. Preserve object identity for foreground generation/claim ownership and value equality for bridge observations. - Move unchanged Modal detail decoding beside the provider and share the two existing internal startup errors. No new provider/backend/wire/storage contracts. - Add 17 focused reconciliation boundary cases, two assembled restore-registration rejection cases, four Modal classification/gating cases, and internal-module ESLint coverage. Retain assembled manager/race/restart and real-storage tests. - Update `docs/plans/sandbox-lifecycle-manager-refactor.md` with actual ownership, callback and sequencing contracts. ## Boundary And Behavior Evidence The sole manager callback is `acceptResolvedStartup(generation, providerObjectId, lifetime)`, delegating to existing generic acceptance, late cleanup, lifetime recording and announcements. Resume still uses that same manager operation. Bridge access publication uses the narrow access collaborator after repository completion. Foreground cleanup preserves reference equality and cannot clear newer auth; bounded uncertainty retains the token for an equal-valued late bridge. Retry auth is cleared before replacement reservation yields. The bridge cache contains identity/lifetime only, and reconstruction cannot invent a terminal signing key. New boundary tests cover these transitions and successful older lookup/latest-generation queueing. Bridge completion still supplies the expected pending reference to `completeProviderResume`; real repository tests cover generation/reference changes during encryption. Restore still awaits pending registration, calls `markRecoveryInvoked` synchronously, then invokes the provider with no added await. Rejected pending registration never records invocation or calls restore. `not_visible`, `other_generation` and unknown transport outcomes remain distinct. Recovery nulls abandon the foreground path and never replay create. Standard Modal hook presence does not enable VM allocation recovery. Retry/materialization bounds, lifetime provenance and resolved-handle replacement are unchanged. ## Verification Node `v24.20.0`, existing dependencies; heavy checks ran sequentially with one Vitest worker. | Command | Final Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm test -w @open-inspect/control-plane -- src/sandbox/lifecycle src/sandbox/providers src/session/sandbox-repository src/session/sandbox-shutdown --maxWorkers=1` | 38 files, 1,094 tests passed | | `npm run test:integration -w @open-inspect/control-plane -- sandbox-early-connect sandbox-shutdown sandbox-state-retention --maxWorkers=1` | 3 files, 55 tests passed in Workerd | | `npm run typecheck -w @open-inspect/control-plane` | All four configurations passed | | `npm run lint -w @open-inspect/control-plane` | Passed | | `npm run test:lint-sandbox-boundaries` | 2 tests passed | | `npm run build -w @open-inspect/control-plane` | Worker and Node bundles passed | | Touched-file `npx prettier --check` | All 10 files passed | | `git diff --check` and committed base diff check | Passed | Initial validation caught one missed bridge call-site delegation and an incomplete new settings fixture type; both were corrected and rerun successfully. Documentation formatting was corrected before the final formatting check. Commit hooks passed. Read-only behavior review found no introduced regression. ## Limits And Remaining Risks This is an extraction, not a safety redesign. Known baseline gaps, including unguarded fresh/restore artifact writes across replacement and prior-generation retirement handle clearing, remain separate work as recorded in the characterization document. These tests do not prove exhaustive interleaving safety. Workerd/provider substitutes are not provider-backed canaries; no full-package sweep or live-provider verification is claimed. No deployment performed. Integrate this increment before COL-245. Linear: https://linear.app/colemurray/issue/COL-244 --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/cb1a295654d96949a27a90e0f7441713)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved recovery when a sandbox startup response is delayed or uncertain, helping avoid unnecessary launch attempts. * Prevented restore launches when a pending provider registration has expired or been superseded. * Improved handling of startup and provider errors so recovery can distinguish missing or outdated allocations from temporary uncertainty. * Prevented access-change notifications when a bridge resolution is refused, and preserved held shutdown state during resolution. * **Reliability** * Improved coordination of startup recovery across overlapping launches, retries, and restore operations. <!-- 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>
ColeMurray#2168) ## Summary The session details sidebar was one long stacked column: participants, run metadata, cost, trace download, sandbox tools, tasks, child sessions, skills, changed files and media, in that order. The things people check most (what changed, what the agent is doing) sat at the bottom. This splits it into four tabs with one continuous surface each. It reuses the existing sections and data; no API or backend changes. | Tab | Contents | | ----------- | ---------------------------------------------------------------------------------------------------- | | **Changes** | `+/-` totals, the filterable changed-file list, diff lifecycle states, and media the agent captured | | **Info** | Run information (started, model, environment, cost and limit), the repository or repositories with branches and PRs, managed skills, participants, Download trace | | **Tasks** | The agent's checklist with a completion count and progress bar, then child sessions | | **Tools** | Editor, desktop, terminal and tunnel links, still gated by sandbox access | The tab strip shows the changed-file and task counts. The mobile/tablet details sheet renders the same tabs. ## Behavior - **Tabs** use Radix Tabs (`@radix-ui/react-tabs`, new direct dependency of `web`; it was already in the lockfile transitively, so the lock change is one line). Arrow keys, Home and End move between tabs; each panel is linked to its tab. - **Inactive panels stay mounted**, so the file filter, collapsed sections and managed-skills state survive switching tabs. **Each panel scrolls on its own** under a fixed tab strip, so switching from a long Info or Tasks panel opens Changes at its top, and returning keeps each panel's position. - **The tab the viewer last picked is remembered per browser** (`localStorage`, optional: it falls back to Changes when storage is unavailable). Switching tabs for them, as below, doesn't replace that choice. - **Opening a diff switches the sidebar to Changes.** A diff opened from an agent-output file link while another tab was showing used to leave focus with nowhere to return on close; the file row it returns to is now visible. - **Info reads as a properties list**: aligned label/value rows grouped under headings, separated by spacing rather than rules. The session cost is a `Cost` row under Run information (`BudgetSection` renders as a row in `MetadataSection`'s list). The Changes, Tasks and Tools summaries are separated from their lists the same way. - **Changed files show their folder** under the file name. A filter with no matches now says so instead of showing an empty list. - **Branch, repository and environment names wrap instead of truncating.** Long or nested names (`group/subgroup/repo`, long feature branches) were previously cut to `...` at a fixed width. - **PR rows keep their place in Info**, under Repository (or under each member of Repositories). The sync button sits in that section's heading for one PR as well as several, and each row gets its state icon. Single- and multi-repository sessions share one `PullRequestRow`, so the two layouts can't drift apart. - **Mobile details sheet**: fixed height so switching tabs doesn't make the sheet jump, and the closed overlay is `inert` and `aria-hidden`. - Tab headings replace the old per-section collapsible headers; the "Tasks and artifacts will appear here" placeholder is replaced by each tab's own empty state. ## Testing - `npm test -w @open-inspect/web` (merged with current `main`): 238 files, 2,187 tests pass. - New tests cover tab semantics and keyboard navigation, one visible panel at a time, filter state surviving tab switches, every diff lifecycle message in Changes, retry permission checks, task counts and progress, tool permission gates, media in Changes, the switch to Changes when a diff opens without replacing the remembered tab (choose Info, open a diff, remount), independent panel scrolling, and the remembered tab (hook and component). Existing trace-download, budget and sandbox-permission tests now open the tab they exercise; the budget and metadata tests cover the `Cost` row and the sync action in the Repository heading. - `npm run typecheck -w @open-inspect/web`, ESLint, Prettier and `npm run build -w @open-inspect/web` pass. - Checked in a browser against sample data at desktop and phone widths, in light and dark themes: every tab, multi-repository sessions with several PRs, the phone details sheet, and panel scroll positions when switching tabs in a short window. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * The session inspector now organizes Changes, Info, Tasks, and Tools into tabs, remembers your selected tab, and shows file and task counts, change totals, and related session details. * Changed files show their folder paths, and searches with no matches display an empty-results message. * Session details present repository, branch, pull request, cost, and run information in clearer sections. * **Improvements** * The details panel has updated layouts for phone and larger screens, with improved keyboard and screen-reader behavior. * Sidebar sections use more consistent formatting, and long repository and branch names remain readable. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
) ## Summary Implements [COL-245](https://linear.app/colemurray/issue/COL-245), the fifth incremental SandboxLifecycleManager extraction. Based on `64aa395`, which includes the integrated COL-244 VM reconciliation prerequisite. - Move `destroyLateProviderResult`, `rearmRejectedStartupCleanupAlarm`, and `attemptRejectedStartupCleanup` mechanics into `sandbox/lifecycle/allocation-cleanup.ts` as stateless functions. - Keep rejection/claim policy, credential fencing, socket/access retirement, failure publication/accounting, admission flags, and prior-generation replacement retirement in the manager. - Preserve the manager's public reconstruction rearm entry point and rejected-cleanup priority over ordinary shutdown processing, including under a hold. - Extend existing ESLint boundary restrictions and update the enduring ownership/design guide. ## Boundary and Ordering The helper receives only a two-method repository port, shared scheduler scheduling, explicit-stop eligibility, a handle-pinned stop callback, and a lazy warning logger. Rearming and late destruction accept only their dependency subsets. There is no new mutable ownership state, operation journal, persisted format, shutdown policy, or raw platform alarm. Ordering remains: synchronously reject/fence/retain the handle; detach the rejected bridge; clear/notify access; publish/count only the repository result `failed`; await retry scheduling before provider I/O; bound the explicit stop locally; clear the handle only after success and a matching sandbox ID, timestamp, and handle. Terminal `retained` and `superseded` outcomes do not acquire failure-accounting authority. The original 10-second stop bound and 30-second retry interval are unchanged. The stop callback preserves `startup_superseded`, `destroy`, the session-name/internal-ID fallback, the signal, and `generationCreatedAtMs: undefined`. Provider absence classification is unchanged. Generic current-held claims still avoid destruction; replacement retirement remains a separate manager operation with its original confirmation/clearing semantics. ## Regression Coverage - Strengthen early-bridge fencing, exact detach/access order, and owning failure accounting. - Preserve terminal errors/statuses and verify generation-ID, timestamp-only, and handle-only replacement isolation across deferred stops. - Add narrow schedule-gate, failed-stop, local-timeout/late-completion, matching completion, and unsupported/absent-handle tests. - Pin exact manager stop arguments, unsuccessful provider results, capability/method absence, young/old pending-reference classification, and superseded successful/rejected results against a held successor. - Extend repeated assembled alarm coverage and add a Workerd regression rebuilding the production runtime over a persisted rejected row and durable shutdown hold. It exercises the real reconstruction hook/shared deadline storage and preserves the hold/recovery receipt across failed, successful, and repeated cleanup. ## Verification Run sequentially on Node `v24.20.0`; test commands use one worker to respect sandbox resources. | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm test -w @open-inspect/control-plane -- src/sandbox/lifecycle src/sandbox/providers src/session/sandbox-repository src/session/sandbox-shutdown --maxWorkers=1` | 39 files, 1,115 tests passed | | `npm run test:integration -w @open-inspect/control-plane -- sandbox-shutdown sandbox-state-retention sandbox-early-connect --maxWorkers=1` | 3 files, 56 tests passed | | `npm run typecheck -w @open-inspect/control-plane` | All four configurations passed | | `npm run lint -w @open-inspect/control-plane` | Passed | | `npm run test:lint-sandbox-boundaries` | 2 tests passed | | `npm run build -w @open-inspect/control-plane` | Worker and Node bundles passed | | `npx eslint eslint.config.js scripts/lint-sandbox-boundaries.test.mjs packages/control-plane/test/integration/sandbox-state-retention.test.ts` | Passed | | `npx prettier --check` with all eight touched files explicitly supplied | Passed | | `git diff --check` and `git diff main...HEAD --check` | Passed | The pre-edit rejected-allocation/pending-VM/VM-resolution baseline passed 52 tests. During new test development, an assertion placed before fixture initialization and the background collector's `Promise<void>` annotation caused local failures; both were corrected and affected checks rerun successfully. No remaining environment blockers or baseline failures. Commit hooks also passed. ## Unchanged Limits Generic superseded-result destruction remains bounded best effort, not newly durable. The assembled handler can attempt failed rejected cleanup twice per delivery around terminal-projection I/O before generic watchdogs. Previously documented unscoped prior-generation clearing and access-retirement failure boundaries remain separate behavior work; this extraction does not harden them or claim exhaustive interleaving safety. Provider substitutes do not establish live retirement guarantees. No deployment or live-provider verification was performed. Merge this increment before the serial watchdog-effects extraction. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/0ba1b5dc7abdf1ab6b2c3bd9fba49c91)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved cleanup of sandboxes whose startup is rejected or superseded, including retries when provider shutdown fails. * Preserved shutdown holds and blocked queued work during restart recovery until cleanup can be retried. * Prevented cleanup of a newer sandbox generation when an older startup finishes late. * Retained provider handles when shutdown was not confirmed, avoiding premature cleanup. * **Tests** * Expanded coverage for startup rejection, cleanup retries, provider shutdown outcomes, and recovery after restart. <!-- 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 Closes [COL-203](https://linear.app/colemurray/issue/COL-203). - Add `/teams` with membership filters, search, per-user local favorites, counts, join policy, and capability-gated joining. - Add team pages with directory-visible headers and Members, member/admin Overview and Activity, and capability-gated Settings using the existing team-management components. - Add member-only team session buckets and visibility-filtered, cursor-paginated activity; add a team filter to the workspace audit viewer. - Add owning-team and visibility metadata, private-session collaborator management, visibility cascades, and desktop/mobile move dialogs with join consent, repository-grant errors, owner-access warnings, and retry without children. - Use server snapshot capabilities for session actions, retaining the global permission check only for trace export. Explicitly refresh snapshots and invalidate mounted, inactive, infinite, and retained list pages after scope mutations. - Add BFF proxies, shared response schemas, route-catalog/admission coverage, and a changelog entry. ## Access And Storage Directory reads admit active human users. Team work routes require membership or an administrator role; denied capabilities remain 403 with `reason_code`, while nonmembers cannot enumerate member-only team feeds. Existing join-denial codes are preserved. The collaborator picker has a narrowly scoped `GET /sessions/:id/collaborator-candidates` reader under always-enforced `manageCollaborators` admission. It returns only active users' picker identities; `/members` and its workspace permission remain unchanged. Invisible sessions return the same 404 as missing sessions before the candidate handler runs, including in `off` and `shadow`. Session lists retain the existing `off`/`shadow`/`on` semantics and private-session restrictions. Scope writes, audit writers, credential boundaries, and enforcement defaults are unchanged. No D1 or DO migration is added; the existing Teams schema is migration 0083. ## Checkpoint: Commands And Results Final checks ran sequentially with targeted test selections. The branch was rebased from `cf345db` onto current `main` at `3beccb0`, then affected-package typechecks, scoped lint, SQL portability, and targeted tests were rerun. ```bash npm run build -w @open-inspect/shared npm run typecheck npm run typecheck -w @open-inspect/control-plane -w @open-inspect/web npm run lint:sql-portability git diff --name-only -z origin/main...HEAD -- '*.ts' '*.tsx' | xargs -0 npx eslint git diff --name-only -z origin/main...HEAD -- '*.ts' '*.tsx' '*.md' | xargs -0 npx prettier --check git diff --check origin/main...HEAD git diff --relative=packages/web --name-only -z origin/main...HEAD -- ':(glob)packages/web/src/**/*.test.ts' ':(glob)packages/web/src/**/*.test.tsx' | xargs -0 -r npm test -w @open-inspect/web -- npm test -w @open-inspect/control-plane -- src/routes/teams.test.ts src/routes/audit-events.test.ts src/routes/session-runtime-proxy.test.ts src/db/audit-event-store.test.ts src/router.policy.test.ts npm run test:integration -w @open-inspect/control-plane -- test/integration/teams-routes.test.ts test/integration/audit-event-store.test.ts test/integration/audit-events-route.test.ts test/integration/session-inbox.test.ts test/integration/session-snapshot.test.ts test/integration/collaborator-candidates.test.ts test/integration/hono-route-catalog-conformance.test.ts test/integration/route-admission-matrix.test.ts npm test -w @open-inspect/shared -- src/types/server-messages.test.ts ``` - Shared build and all-workspace typecheck passed before rebase; control-plane and web typechecks passed again after rebase. - Final web selection: **31 files, 368 tests passed**. - Final control-plane unit selection: **5 files, 204 tests passed**. - Final control-plane integration selection: **8 files, 106 tests passed**. - Shared snapshot-contract selection: **21 tests passed**. - Scoped ESLint, Prettier, SQL portability, commit hooks, and diff checks passed. Coverage includes a two-team parent/child move with both buckets updating despite unchanged timestamps, Member collaborator add/remove without workspace-directory access, all enforcement modes, nonmember concealment, private audit filtering, missing capabilities, account/cache isolation, cascade retry, and not-found rendering after access loss. ### Failures Encountered And Fixed The tests-first capability/snapshot regression run failed as expected: ```text Test Files 2 failed (2) Tests 4 failed | 2 passed (6) TypeError: useRefreshSessionSnapshot is not a function ``` The tests-first desktop-action, fresh-SSR, inactive-cache, and target-membership regression run also failed before the fixes: ```text Test Files 4 failed (4) Tests 4 failed | 51 passed (55) ``` A subsequent targeted web run caught stale expectations after unconditional cache invalidation; those assertions now require old head rows and retained pages to disappear while refreshed data is pending: ```text Test Files 2 failed | 25 passed (27) Tests 4 failed | 342 passed (346) AssertionError: expected [] to deeply equal [ 'attention' ] AssertionError: expected "vi.fn()" to be called once, but got 2 times ``` The initial collaborator-candidate integration fixture omitted nullable `ownerTeamId`; it was corrected without changing the store: ```text Test Files 1 failed | 6 passed (7) Tests 12 failed | 80 passed (92) Error: D1_TYPE_ERROR: Type 'undefined' not supported for value 'undefined' ``` The first typecheck used a shared subpath that is not exported. Both imports now use the existing root type export: ```text src/lib/session-capabilities.test.ts(3,42): error TS2307: Cannot find module '@open-inspect/shared/types/session-access' or its corresponding type declarations. src/lib/session-capabilities.ts(2,71): error TS2307: Cannot find module '@open-inspect/shared/types/session-access' or its corresponding type declarations. ``` SQL lint caught the candidate sort; it now uses portable `LOWER(COALESCE(...))`: ```text SQL portability: 1 disallowed construct(s). packages/control-plane/src/routes/session-scope.ts:297 collate-nocase COLLATE NOCASE ``` The first scoped formatting command included generated Vitest snapshots. They were excluded from subsequent formatting commands: ```text [error] No parser could be inferred for file "/workspace/background-agents/packages/control-plane/test/integration/__snapshots__/hono-route-catalog-conformance.test.ts.snap". [error] No parser could be inferred for file "/workspace/background-agents/packages/control-plane/test/integration/__snapshots__/route-admission-matrix.test.ts.snap". ``` ## Checkpoint: Baseline Differences - The HTTP snapshot at `session-runtime-proxy.ts` exposed team, visibility, collaborators, and capabilities but no canonical owner identity. It now also decorates `ownerUserId` from the admitted D1 row, never participant presence, for the owner-access warning. - The existing workspace candidate reader required `workspace.members.read`, which built-in Members do not have. The scoped picker reader solves this without widening the workspace directory. - The inbox store filtered by teams but omitted the owner/scope fields needed for effective per-row capabilities. Its internal projection now preserves those fields; the team route decorates the response. - SSR snapshots had no client refresh path, and sidebar cursor pages retained rows outside SWR. The provider now explicitly refetches and accepts fresh SSR data; successful scope mutations also reset retained pagination. - The existing move write requires target membership or explicit joining of an open team, including for administrators. The dialog follows that contract instead of changing the write path. ## Checkpoint: Deliberately Deferred - PR 9's switcher, active-team hook, sidebar/discovery filters, composer team/visibility fields, transport `sessionGone`, and `requireTeamOnCreate` response field are untouched. PR 9 is not on the rebased `main`; its switcher-to-team-page link and switch-teams browser step remain follow-ups. - The PR-state board and later repository, channel, secrets, automation, and environment tabs are not implemented. - Activity reads `team_id` only. Move-departure audit writers are unchanged. Team Activity contains domain events only. HTTP authorization decisions remain in the permission-gated workspace audit; the store does not inspect route prefixes. - Screenshots, additional browser recordings, and full-suite reruns were deferred in favor of sequential targeted checks. The parent/child move, distinct team lists, collaborator flow, and not-found behavior are covered by integration and component tests. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/1bea9d45abf4fd9a829466aba8845ab5)* ## Audit Cleanup Follow-up commit `d91bf24` removes route-prefix matching from visibility-scoped audit reads. Team Activity filters by resource type and retains current session visibility and read-permission checks. Its event-type selector contains operation events only. Workspace audit still retains HTTP authorization decisions. No schema, migration, or audit-writer changes. Sequential validation: 55 targeted control-plane integration tests and 34 web tests passed; control-plane/web typechecks, scoped ESLint, Prettier, SQL portability, and commit hooks passed. The new regression coverage was red before the change and verifies path-independent exclusion, null resource IDs, pagination, and preservation of workspace evidence. --------- Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com> Co-authored-by: ColeMurray <2492022+ColeMurray@users.noreply.github.com>
…oleMurray#2169) > Follows ColeMurray#2168 (session details tabs, merged). This PR's diff is only the diff-view changes. ## Summary Opening a changed file now shows the diff in the main column, where the conversation was, instead of a resizable panel squeezed between the conversation and the details sidebar. The sidebar stays where it is, and its Changes list becomes the file navigator for the diff. At 1440px with both sidebars open, the old side panel left roughly 400px for code: Split was unavailable, and the panel repeated the sidebar's file list in a narrow rail. The diff now gets the whole main column (about 790px at that width), so Split is available. ## Layout - **The diff replaces the conversation in the main column.** The timeline, terminal and composer stay mounted underneath, so scroll position and an unsent draft survive opening and closing a diff. The resizable conversation/diff split and its stored layout (`session-changes-layout-v2`, `useBrowserLayoutStorage`) are removed. - **The details sidebar stays open while a diff is showing**, and the header's sidebar toggle stays available (the `showDesktopDetailsToggle` prop, which only existed to hide it, is removed). - **The session page owns the inspector tab** (it was private to the sidebar), so the layout can tell whether the sidebar is showing the file list. Opening a diff shows the Changes tab without replacing the tab the viewer remembers; moving between files doesn't touch the tab. - **The diff's own file list starts hidden while the sidebar shows the Changes list**, and appears when the sidebar is closed or on another tab. Once the viewer toggles it, their choice holds while the diff is open. - **Closing the diff returns focus** to the row of the file being viewed, else to whatever opened the diff (such as a file link in the agent's reply), else to the sidebar toggle. `useSessionDiffSelection` owns the selection and this focus order. ## Review view - A top bar with **← Session**, the file count, and the **File list** toggle. - A selected-file header with the repository, full path, change summary, and previous/next with the file's position (`2 / 4`). - **Split depends on the code column's own width** (at least 640px), not the whole panel. Falling back to Unified doesn't overwrite a saved Split preference. - Switching files scrolls the code back to the top. - "Compared with session start" moves to a footer, with the base → head SHAs. - **Phones** keep the full-screen diff; the file list starts collapsed, and picking a file closes it and returns focus to the diff. ## Renderer - 12px code on 20px lines, GitHub light/dark syntax themes, and code drawn on the app background with the app's added/removed/modified colors. - **The first diff opened after a page load could render blank in development**: mounting the renderer while the shared highlighter initialized left its shadow root empty under React Strict Mode. The renderer now loads the highlighter and review themes first and shows the raw patch in the meantime. The raw patch also stays readable if highlighting fails to load. Once the review themes are attached, later diffs mount highlighted immediately; the check uses `areThemesAttached` because the shared highlighter reports itself loaded before its themes attach. ## Testing - `npm test -w @open-inspect/web` (on current `main`, which includes ColeMurray#2168 and ColeMurray#2164): 256 files, 2,408 tests pass. - Diff selection tests: opening runs the Changes-tab switch but moving between files doesn't; focus returns to the current file's row, else the opener, else the fallback when the sidebar was hidden; rows inside the closed (inert) mobile sheet are skipped. - Layout tests: the diff takes the main column with the sidebar visible, and the workspace and sidebar stay mounted across opening and closing. - Diff panel tests: the file list's default with and without the sidebar, the viewer's toggle holding, Split availability by code-column width without overwriting the saved preference, patch loading/error/empty/stale states, non-renderable files not being fetched, previous/next bounds, Escape, the phone list closing with focus returned, and the list appearing once the sidebar stops showing the files. - Renderer tests: options and palette, raw patch until the highlighter and review themes load, a loaded highlighter still missing the themes, the failure fallback, and immediate rendering once the themes are attached. - `npm run typecheck -w @open-inspect/web`, ESLint, Prettier and `npm run build -w @open-inspect/web` pass. - Checked in a browser against sample data: desktop with the sidebar open and closed, focus after returning to the session, light and dark themes, and phone width. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * The Changes view now fills the main workspace when a diff is open, while the sidebar remains available. * Inspector tabs stay consistent across desktop, mobile, and session details views. * Diff panels include file details, change counts, and previous/next file navigation. * **Improvements** * Split diffs are available on wider screens; narrower layouts use unified diffs. * Diff rendering uses light and dark themes, with readable raw-diff content shown while styling loads or if it fails. * Closing a diff restores focus to an appropriate file or control. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…leMurray#2163) ## Summary Fixes COL-252. - Add a small test-only helper module for `Sandbox.exec`, `Sandbox.create`, and `Sandbox.snapshot_filesystem`, using request messages from the installed `modal_proto` package and the SDK's own CPU/memory resource converter. - Bind create and exec arguments to the installed SDK signatures before serialization, rejecting unknown keywords even when SDK methods are mocked. - Apply the helpers to recorded fake arguments at all six call sites in snapshot management, session launch, access-password recovery, and image-build sessions. - Extend existing test matrices for timeout budgets, fractional preparation time, default/custom resources, image sources, tunnel-port filtering, and enabled access services. - Add direct helper tests covering float rejection for integer timeout fields, resource serialization, and float acceptance for snapshot timeouts. No production code, dependency pins, migrations, or persisted fields changed. The already-merged snapshot hotfix remains intact. ## SDK Detail The installed Modal 1.4.3 protobuf declares `SandboxSnapshotFsRequest.timeout` as `TYPE_FLOAT`. The legacy snapshot path therefore accepts fractional timeouts too, while the command-router path converts its separately supplied timeout with `float(timeout)`. The helpers follow those actual SDK contracts rather than impose an integer-only snapshot check. Each recorded snapshot timeout is checked against both internal contracts in one helper call, without a synthetic provider-mode flag or duplicate call-site tests. ## Verification From `packages/modal-infra`: - `uv run pytest tests/ -q`: 489 passed after removing redundant snapshot-mode cases. - `uv run ruff check`: passed. - `uv run ruff format --check`: passed. - `git diff --check`: passed. Temporary mutation checks both failed at the helper with `TypeError: 'float' object cannot be interpreted as an integer`: - Removed `int()` from the Docker-preparation exec timeout in `take_snapshot`. - Passed a deliberately float timeout through the existing session-create behavior test. Both mutations were removed before final validation and are not included in this PR. No live Modal calls were required. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/7900f609350eb4d024b8ab22b1c1beb0)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Expanded validation of sandbox creation, execution, resource settings, and filesystem snapshots across launch configurations. * Added coverage for default, integer, and fractional CPU settings; memory options; and execution and snapshot timeout boundaries. * Verified that launch requests preserve configured options and that access credentials are recovered only for enabled services. <!-- 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>
…ray#2165) ## Summary - Make Better Auth the sole source of participant OAuth credentials. Remove copied-token fallback, local refresh/decryption, the unused legacy refresh helper, and participant token updater. - Separate a child participant's canonical credential identity from inherited session ownership. Unresolved prompt authors remain unresolved, while private children retain their owner and collaborators. - Enrich browser WebSocket joins/reconnects from the verified canonical GitHub account, and scheduled/Slack automation prompts from the actual execution author, preserving Git commit attribution. ## Test-first verification Added regression tests before production changes and observed failures for: - Stale, refresh-only, and unexpired copied credentials bypassing current Better Auth credentials; copied tokens also bypassing missing-grant and identity-integrity handling. - Private children substituting the parent's owner for unresolved reviewers, including missing SCM identity. - Actual browser WebSocket prompts dispatching `agent-only` instead of the linked user's Git identity on join and reconnect. - Scheduled automation and Slack follow-up prompts omitting the execution author's GitHub attribution. All are now green. External GitHub publication/profile calls are mocked; the browser dispatch and child creation tests exercise real workerd/D1 paths. ## Review hardening - Require an explicit nullable participant credential identity; never infer it from session ownership. - Settle one attribution snapshot after invocation admission. Ambiguity or unavailable optional profiles omit attribution; storage/integrity errors fail without routing Slack replies into owner-authored new runs. - Obtain verified profiles when cached logins are missing, reject caller-supplied GitHub attribution, and replace stale SCM fields on unlink/relink. - Remove legacy OAuth token inputs and writes from init, WS minting, and participant persistence; retain historical readable columns only. - Treat supplied prompt enrichment as an authoritative snapshot. Successive Slack turns clear old attribution after unlinking, ambiguity, missing login, or optional grant failure. - Bound optional GitHub attribution for WS joins to five seconds, below the browser proxy deadline; delayed failures do not block minting or overwrite the empty snapshot later. Added regressions first and observed red for these failure paths before implementing fixes. All are now green. ## Validation - Shared build - Control-plane unit suite: **335 files, 5,559 tests passed** (`--maxWorkers=1`) - Targeted workerd integration/conformance suites: **11 files, 199 tests passed** (PR creation, WebSocket participants, child spawning/operations, scheduler, Slack events, invocation fan-out, session-core conformance, provider auth, internal routes, and prompt queueing) - Control-plane typechecks: Worker, Node, unit/conformance, and integration configurations - Control-plane Worker and Node builds - Changed-file ESLint, Prettier, and `git diff --check` ## Scope No schema migration, backfill, or production deployment. Historical SQLite credential fields remain readable but are ignored for authentication and are no longer written by participant producers. App fallback remains available when the prompting author genuinely has no usable Better Auth grant; credential identity mismatches remain fail-closed. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Session, automation, and Slack prompts can include verified GitHub identity details, such as login, name, email, and SCM user ID, when available. * Child sessions retain the prompt author’s canonical identity separately from session ownership. * Pull request creation uses current browser OAuth credentials across different cached-credential states. * Browser Git attribution is available after joining or reconnecting, and is omitted when an account is unlinked or lacks a login. * **Changes** * SCM tokens are no longer stored with participants or refreshed locally for pull request authorization. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…urray#2171) ## Summary - Replace the native session team and visibility dropdowns in the new-session composer with the existing shared Radix Select component. - Keep compact borderless triggers and open menus above the composer, consistent with adjacent controls. - Preserve Workspace-to-null team mapping, team visibility availability, defaults, and loading/creation disabled states. - Update regression tests to exercise the real custom menus, including returning to Workspace and disabled/required-team behavior. ## Validation - Home/sidebar tests: 10 files, 96 tests passed (`npm test -w @open-inspect/web -- 'src/app/(app)/(sidebar)/' --maxWorkers=1`). - Web typecheck passed (`npm run typecheck -w @open-inspect/web`). - Targeted ESLint, Prettier, and `git diff --check` passed. - Browser-verified both menus at desktop 1440x900 and mobile 390x844 using mocked API responses. Checked keyboard selection back to Workspace and no mobile horizontal overflow. ## Visual Evidence Viewport screenshots from `http://localhost:3000` are uploaded to the session: - Desktop team menu: `1aa99eb4907cf6204641825aca797958` - Desktop visibility menu: `5b254d0fc3767825e8c9af721d60741e` - Mobile visibility menu: `5388f1d6ae602bc7fc52329787bb32ac` - Mobile team menu: `22af0c5baddd485d346e6605b5c1df7e` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/d10f14fdafa21d245dc763be440929f9)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added session access controls for choosing Private, Team, or Workspace visibility and selecting a team. * Team selection supports a “No team” option when allowed; selecting Team visibility requires a team. * **Improvements** * Changing teams preserves the draft’s visibility. Workspace visibility clears the active team. * Access controls are disabled while teams are loading, and keyboard navigation is supported. <!-- 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 - Adopt the selected inline visibility layout: label and shared component dropdown on one row, with the child-session checkbox and a compact ghost-style Save action below. - Replace the native select with the existing Radix-based Select component. - Preserve explicit confirmation, unavailable visibility options, owner-membership warnings, pending states, and retry-without-children behavior. - Keep the implementation in the existing control; remove all temporary design variations and preview code. ## Verification - `npm test -w @open-inspect/web -- src/components/session-controls.test.tsx src/components/session-right-sidebar.test.tsx --maxWorkers=1` (74 tests passed) - `npm run typecheck -w @open-inspect/web` - ESLint and Prettier checks on all changed files - `git diff --check` - Browser verification of the actual control in a temporary development harness: keyboard dropdown selection, disabled unavailable options, and no horizontal overflow at 390x844 and 1440x960. The harness is not included in this PR. ## Regression Coverage - Selecting visibility or toggling the child-session checkbox does not mutate permissions before Save. - All controls remain disabled until the updated session snapshot finishes refreshing, preventing duplicate requests. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/872736fed342173cfd8b31e15f5d08af)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **User Interface** * Replaced the visibility dropdown with a shared styled selector and reorganized the visibility and save controls. The save button is now labeled “Save.” * Pressing Escape closes the visibility dropdown first; pressing it again closes the dialog. If another action has already prevented Escape, the dialog stays open. * Visibility options and controls remain unavailable when required capabilities or team context are missing. When Escape closes the dialog, focus returns to its previous location. <!-- 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>
…ies (ColeMurray#2174) ## Summary Closes [COL-253](https://linear.app/colemurray/issue/COL-253/teams-follow-up-control-plane-stop-auditing-allowed-team-reads). - Default `requireTeam` allowed auditing to false for `read` and `member`, with an optional override. Preserve allowed auditing for capability needs and explicitly for membership deletion. - Disable allowed-decision auditing for team-session and collaborator-candidate reads while retaining their existing authorization requirements. Directory, member, and activity reads inherit the quiet defaults. - Resolve memberships at WebSocket subscribe in every enforcement mode so non-owner team leads retain move and visibility capabilities. Collaborator reads and per-command scope-read short-circuiting remain unchanged. - Add request-correlated audit regression tests, all-mode subscribe capability/query-count coverage, and route-policy assertions. Update the catalog and admission-matrix snapshots and operator changelog. - Narrow the existing `requireSession` and `requireAll` return annotations to their actual `active-user` variant, permitting typed policy overrides without casts or runtime changes. No migration, access-decision change, credential expansion, new route, or response-schema change. The route catalog remains 202 routes / 154 paths. ## Checkpoint Report ### 1. Commands And Results Heavy validation ran sequentially with one Vitest worker and 600000 ms shell timeouts. | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed before dependent checks. | | `npm test -w @open-inspect/control-plane -- src/router.policy.test.ts src/session/connection-authenticator.test.ts --maxWorkers=1` | Red phase: 8 failed, 171 passed. After implementation: all 179 passed. | | `npm run test:integration -w @open-inspect/control-plane -- test/integration/teams-routes.test.ts test/integration/session-scope-routes.test.ts test/integration/websocket-session-access.test.ts --maxWorkers=1 -t 'does not audit an allowed team read\|audits an allowed team capability write\|still audits\|does not audit allowed collaborator-candidate\|loads memberships at subscribe\|sends move and visibility'` | Red phase: 10 failed, 4 passed, 80 skipped. Nine failures reproduced the requested defects; one exposed an incorrect initial expectation about an existing concealed-team 404, described below. | | `npm run test:integration -w @open-inspect/control-plane -- test/integration/teams-routes.test.ts test/integration/session-scope-routes.test.ts test/integration/websocket-session-access.test.ts test/integration/hono-route-catalog-conformance.test.ts test/integration/route-admission-matrix.test.ts --maxWorkers=1 --update --silent` | All 108 passed; catalog snapshot updated. | | `npm run test:integration -w @open-inspect/control-plane -- test/integration/route-admission-matrix.test.ts --maxWorkers=1 --update --silent` | All 13 passed; matrix snapshot updated to record audit policy alongside unchanged statuses. | | `npm run typecheck` | Initial run failed on overly broad helper return types; narrowed those annotations, then all workspaces passed. | | `npm run lint:fix` | Passed. | | `npm run lint:sql-portability` | Passed: clean, with 24 existing baselined occurrences across four files. | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1 --silent` | 338 files passed; 5,645 tests passed. | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1 --silent` | 127 files passed; 1,633 tests passed and one skipped. | | `npx prettier --write` on the changed TypeScript files and `CHANGELOG.md` | Passed; only touched files formatted. Commit hooks also passed ESLint and Prettier. | | `npm run test:integration -w @open-inspect/control-plane -- test/integration/teams-routes.test.ts test/integration/session-scope-routes.test.ts test/integration/websocket-session-access.test.ts --maxWorkers=1 --silent` | Final post-format check: all 94 passed. | | `git diff --check` | Passed. | <details> <summary>Failure excerpts, verbatim</summary> Unit red-phase assertions: ```text - "auditAllowed": false, + "auditAllowed": true, AssertionError: expected 1st "vi.fn()" call to have been called with [ 'member-user', …(1) ] - Expected + Received [ "member-user", - { - "includeMemberships": true, - }, ] Test Files 2 failed (2) Tests 8 failed | 171 passed (179) ``` Integration red-phase assertions: ```text AssertionError: expected [ Array(1) ] to deeply equal [] - Expected + Received - [] + [ + { + "action": "authorization.request_allowed", + }, + ] AssertionError: expected "listForUser" to be called 1 times, but got 0 times - Expected + Received { "session": { "capabilities": { - "canChangeVisibility": true, + "canChangeVisibility": false, "canManageCollaborators": false, - "canMove": true, + "canMove": false, }, }, } AssertionError: expected [] to deeply equal [ Array(1) ] - Expected + Received - [ - { - "action": "authorization.request_denied", - }, - ] + [] Test Files 3 failed (3) Tests 10 failed | 4 passed | 80 skipped (94) ``` Initial typecheck failure: ```text src/routes/session-scope.ts(313,5): error TS2322: Type '{ auditAllowed: false; kind: "none"; } | { auditAllowed: false; kind: "authenticated"; } | { auditAllowed: false; kind: "active-self"; } | { auditAllowed: false; kind: "active-global"; service: ServiceAuthorization; } | { ...; } | { ...; }' is not assignable to type 'RouteAuthorization'. Type '{ auditAllowed: false; kind: "service"; services: readonly BotServiceName[]; actor: "required" | "optional"; }' is not assignable to type 'RouteAuthorization'. Type '{ auditAllowed: false; kind: "service"; services: readonly BotServiceName[]; actor: "required" | "optional"; }' is not assignable to type '{ kind: "none"; auditAllowed: false; } | { kind: "authenticated"; auditAllowed: false; } | { kind: "active-self"; auditAllowed: boolean; } | { kind: "active-global"; service: ServiceAuthorization; auditAllowed: boolean; } | { ...; }'. Type '{ auditAllowed: false; kind: "service"; services: readonly BotServiceName[]; actor: "optional" | "required"; }' is missing the following properties from type '{ kind: "active-user"; allOf: readonly RouteAuthorizationRequirement[]; service: ServiceAuthorization; auditAllowed: boolean; }': allOf, service src/routes/teams.ts(517,7): error TS2322: Type '{ service: { kind: "deny"; }; auditAllowed: false; kind: "none"; } | { service: { kind: "deny"; }; auditAllowed: false; kind: "authenticated"; } | { service: { kind: "deny"; }; auditAllowed: false; kind: "active-self"; } | { ...; } | { ...; } | { ...; }' is not assignable to type 'RouteAuthorization'. Object literal may only specify known properties, and 'service' does not exist in type '{ kind: "none"; auditAllowed: false; }'. ``` </details> The full integration run emitted workerd diagnostics from deliberate eviction/error-path tests and NDJSON warnings, but exited successfully with the totals above. ### 2. Verified Facts And Drift Branched from `main` at `3b0b575`; fetched `origin/main` again before commit, with no newer base changes and no repository-grant routes merged. - Confirmed the reported causes at `packages/control-plane/src/routes/shared.ts:187-196`, `routes/teams.ts:496-520`, `routes/session-scope.ts:313-316`, `session/components.ts:847-862`, and `session/connection-authenticator.ts:385,450-458` on the base commit. - Confirmed migration `0083_teams.sql`, DO schema migration 56, and the existing route counts. This PR leaves them unchanged. - Confirmed that `off` and `shadow` use legacy non-private WebSocket access decisions, while `on` uses scoped decisions; private access remains scoped in every mode. Only subscribe capability enrichment changes. - One existing audit gap differs from the blanket denial-auditing premise: `packages/control-plane/src/routing/route-admission.ts:626` returns a concealed-team 404 without authorization-decision evidence. This PR does not alter that admission path. The denial regression instead checks the existing explicit team-capability 403, and the collaborator-candidate regression checks its action-denied 403; both retain audit rows. ### 3. Deliberately Left Out - Repository-grant routes and later team work: outside this fix; existing team routes only. - The pre-existing concealed-team 404 audit gap: separate admission behavior, documented above rather than widening this patch. - Per-command membership/collaborator enrichment in `off` and `shadow`: intentionally stays short-circuited, with query-count regressions. - Web, bot, sandbox-runtime, and schema changes: unnecessary for these control-plane fixes. All-workspace typechecking still passed. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/d5ae386788e57ec379680339c4da0129)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Permitted reads of team directories, members, sessions, activity, and collaborator candidates no longer generate authorization audit entries. Denied requests remain auditable, as do capability changes and team departures. * Team leads retain their move and visibility controls in live sessions across all enforcement modes. * Members cannot remove other members unless they have member-management permissions; denied removal attempts are audited. <!-- 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 Fixes https://linear.app/colemurray/issue/COL-254 - Default navigation to **All my teams**, preserve explicit Workspace/team choices, and show the switcher for a single active membership. Users without active teams keep unfiltered lists. - Keep composer team and visibility draft-local, including required-team preselection and reconciliation when a local team becomes unavailable. Composer changes never update sidebar context. - Retain the last successful membership response after revalidation failures. First-load failures still block readiness. - Retire superseded warm drafts without eagerly creating replacements on configuration changes; warming happens on first input or submit. - Clear/revalidate list, team, activity, audit, and infinite-list caches after scope mutations. Revalidate memberships without clearing them; leave sandbox access and other per-session resources intact. Snapshots still refresh explicitly. - Disable unchanged visibility mutations and require confirmation before non-private child cascades. Populate audit filters from the all-team directory. - Update authorization documentation and the changelog. ## Checkpoint Report ### Commands and Results | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run typecheck` | Passed across all workspaces | | `npm run lint:fix` | Passed | | `npm run lint:sql-portability` | Passed; 24 existing baselined occurrences across 4 files | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1` | 338 files passed; 5,642 tests passed | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1` | 127 files passed; 1,622 tests passed, 1 skipped | | `npm test -w @open-inspect/web -- --maxWorkers=1` | Final run: 257 files passed; 2,497 tests passed | | `git diff --name-only -z \| xargs -0 npx prettier --check` | Passed | | `git diff --check origin/main...HEAD` | Passed | Targeted validation also passed: ```bash npm test -w @open-inspect/web -- src/hooks/use-active-team.test.tsx src/hooks/use-teams.test.tsx src/components/team-switcher.test.tsx 'src/app/(app)/(sidebar)/page-team-context.test.tsx' 'src/app/(app)/(sidebar)/page.test.tsx' src/lib/session-scope.test.ts src/lib/session-scope-refresh.test.tsx src/components/session-controls.test.tsx src/components/settings/audit-log-settings.test.tsx --maxWorkers=1 ``` Result: 9 files passed; 242 tests passed. After the additional unavailable-local-team regression and disabled-directory test, the focused three-file run passed all 49 tests: ```bash npm test -w @open-inspect/web -- 'src/app/(app)/(sidebar)/page-team-context.test.tsx' src/hooks/use-active-team.test.tsx src/hooks/use-teams.test.tsx --maxWorkers=1 ``` Expected red runs before implementing the context and composer changes: ```bash npm test -w @open-inspect/web -- src/hooks/use-active-team.test.tsx src/components/team-switcher.test.tsx --maxWorkers=1 ``` ```text Test Files 2 failed (2) Tests 14 failed | 14 passed (28) ``` Representative failure: ```text AssertionError: expected "vi.fn()" to be called with arguments: [ '/api/sessions/inbox' ] Received: 1st vi.fn() call: [ - "/api/sessions/inbox", + "/api/sessions/inbox?scope=workspace", ] Number of calls: 1 ``` ```bash npm test -w @open-inspect/web -- 'src/app/(app)/(sidebar)/page-team-context.test.tsx' --maxWorkers=1 ``` ```text Test Files 1 failed (1) Tests 8 failed | 6 passed (14) ``` Representative eager-warm failure: ```text AssertionError: expected [ [ '/api/sessions', …(1) ], …(1) ] to have a length of 1 but got 2 - Expected + Received - 1 + 2 ``` The first full web run caught a sequencing mistake in the new unavailable-local-team test, not an implementation failure. The helper clicked a popover trigger while the popover was already open. Closing it before invoking the helper fixed the test; the subsequent focused and full runs passed. ```text FAIL src/app/(app)/(sidebar)/page-team-context.test.tsx > Home team context > falls back locally when the composer's team is no longer an active membership TestingLibraryElementError: Unable to find an accessible element with the role "radio" and name "Engineering team" Test Files 1 failed | 256 passed (257) Tests 1 failed | 2496 passed (2497) ``` Browser verification used the actual local Next.js application with intercepted fixture API responses at `http://localhost:3000`, at desktop 1440x1000 and mobile 390x844. Verified the single-team selector, team-owned recent session, required-team local preselection, private composer visibility, and model changes producing one archive and no eager replacement. Viewport screenshots were captured and uploaded. Real SWR tests additionally verify that a terminal-like child remains mounted while membership refresh is pending or fails, and that per-session resources do not refetch. ### Baseline Facts and Drift Based on `main` at `3b0b575`; `origin/main` was checked again before committing and had not advanced. Confirmed baseline behavior at `packages/web/src/hooks/use-active-team.ts:36-60`, `components/team-switcher.tsx:14-18`, `hooks/use-teams.ts:71-81`, `app/(app)/(sidebar)/page.tsx:109-144,294-297`, and `lib/session-scope.ts:23-35,89-105`. The requested failure modes were present. The composer UI had advanced in ColeMurray#2171: it now uses `SessionAccessSelector` and shared dropdowns instead of the earlier standalone select. This PR preserves that UI and changes its state wiring and regression tests. The existing `/api/teams` proxy already requests `/teams?membership=all&includeArchived=true`, so the audit viewer reuses `useTeams` with an enabled flag rather than adding another API path. Directory, activity, and collaborator documentation was verified against existing server routes. Latest schema remains D1 `0083_teams.sql` and DO migration 56. No migration is added. Enforcement configuration and all access seams are unchanged; the web fixes work with `off`, `shadow`, and `on`. ### Deliberately Excluded Repository grants, target-picker and warm-draft hook internals, team-page changes, server authorization, schema, credential scope, email-address display policy, and enforcement defaults are untouched. These fixes use existing capabilities and APIs. Live remote sandboxes and terminal connections were not exercised by browser fixtures; cache continuity is covered by real SWR regression tests. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/a8b1f561a828e5bc9e277eea2e3b78fd)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * The team selector is available when you have a single active team. * Choose a team for a new session without changing the active sidebar team. * Choose a team and visibility for a new session without changing your active team selection. * **Bug Fixes** * Changing visibility for child sessions now requires confirmation when the change would make them non-private; changes to private visibility apply without confirmation. * Team and session lists refresh more reliably after access or scope changes, while active session tools remain available during membership refreshes. * Temporary membership refresh failures no longer clear previously loaded team data. * Audit logs can be filtered by teams beyond your own memberships, including archived teams. <!-- 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>
…oleMurray#2070) ## Summary - add a root `overrides` entry pinning `sharp` to `^0.35.4`, alongside the existing `minimatch` and `undici` overrides - `miniflare` (via `wrangler`) pins `"sharp": "0.34.5"` exactly, so the hoisted `node_modules/sharp` was 0.34.5 while `next` already carried its own nested `sharp@0.35.4` - with the override, both consumers dedupe to a single hoisted `sharp@0.35.4` (bundled libvips 8.18.6), and the nested `next/node_modules/sharp` tree is removed - resolves two high-severity advisories against the 0.34.5 copy: - [GHSA-f88m-g3jw-g9cj](GHSA-f88m-g3jw-g9cj): inherited libvips vulnerabilities (CVE-2026-33327, CVE-2026-33328, CVE-2026-35590, CVE-2026-35591), patched in 0.35.0 - [GHSA-rgj7-g3m4-5g8c](GHSA-rgj7-g3m4-5g8c): libheif vulnerabilities, patched in 0.35.4 ## Why an override instead of a wrangler bump The newest `wrangler` (4.140.0) depends on `miniflare@5.20260923.0-alpha`, so a wrangler bump would pull an alpha miniflare into the local dev and integration-test runtime. The override leaves `wrangler`/`miniflare` where they are and only moves the image library. miniflare lazily imports `sharp` only to emulate the Cloudflare Images binding locally, and no worker in this repo configures that binding. ## Lockfile The lockfile was regenerated with npm 11.10.0 so no unrelated entries change. A semantic comparison of `packages` shows only `sharp`, `sharp/node_modules/semver`, and `@img/*` entries changed, plus removal of the now-duplicate `next/node_modules/sharp`, `next/node_modules/@img/*`, `next/node_modules/semver`, and unused `@img/sharp-wasm32` entries. ## Verification - `npm ci` - `npm ls sharp` resolves a single `sharp@0.35.4` for both `next` and `miniflare` (deduped) - `require('sharp')` loads the native binding (vips 8.18.6) and encodes a PNG - `npm run build -w @open-inspect/shared` - `npm test -w @open-inspect/web` (212 files / 1903 tests) - `npm run test:integration -w @open-inspect/control-plane` (runs in workerd via miniflare) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated an internal package version override. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
) **Summary** - Include team-directory and collaborator-candidate emails only for viewers with `workspace.members.read`. Restricted SQL projections return `NULL AS email` and do not sort by email. - Render names and avatars, with neutral short-ID labels for unnamed users. Both identity pickers share trimmed-name / response-authorized-email / neutral-fallback typeahead. - Remove the member-facing team Activity tab and `GET /teams/:id/activity`, including the web proxy, component, response-schema aliases, and activity-only audit-store/hook options. - Keep all team/session audit writes, stored audit history, and the workspace audit API/UI behind `workspace.audit.read`, including team filtering. New membership audits contain membership fields rather than response profile fields. - Keep privacy route tests focused and update the canonical route catalog to 201 routes / 153 paths. Update auth documentation and add a removal changelog entry. Fixes [COL-255](https://linear.app/colemurray/issue/COL-255). **Scope Decision** The generic team audit feed was removed because its product purpose is unclear and exposing raw audit metadata to ordinary team members creates an unnecessary privacy boundary. Audit events remain recorded and available through the permission-gated workspace audit viewer. The removed endpoint returns 404 for Members, Administrators, and Owners in every enforcement mode. No audit history is deleted, and no migration is required. **Checkpoint Report** **Validation** | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run typecheck` | Passed across all workspaces | | `npm run lint:fix` | Passed | | `npm run lint:sql-portability` | Clean; existing baseline unchanged | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1` | 339 files, 5,651 tests passed | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1` | 128 files passed; 1,646 tests passed, 1 skipped | | `npm test -w @open-inspect/web -- --maxWorkers=1` | 257 files, 2,576 tests passed | | `npx prettier --check` on changed source/test/docs files | Passed | | `git diff --check` and staged diff checks | Passed | Heavy suites ran sequentially with 600000 ms timeouts. The route-catalog/admission snapshots were regenerated after removing exactly one route; the manifest fixture indices shift accordingly. Regression tests were added before the removal. Initial targeted results were: ```text Test Files 1 failed (1) Tests 1 failed | 136 skipped (137) ``` The policy test found the still-registered activity route. ```text Test Files 1 failed (1) Tests 3 failed | 12 skipped (15) AssertionError: expected 200 to be 404 // Object.is equality ``` The endpoint-absence checks failed in `off`, `shadow`, and `on` before implementation. ```text Test Files 1 failed (1) Tests 3 failed | 17 skipped (20) ``` The new UI assertions found the Activity tab for a team member, Administrator, and Owner before removal. A preservation test initially counted an extra default-role audit event generated when its fixture inserted a user: ```text Test Files 1 failed | 5 passed (6) Tests 1 failed | 85 passed (86) ``` The test query was scoped to its team so it verifies retained HTTP/private-session/history evidence and team filtering without counting unrelated workspace bootstrap events. The final focused integration run passed all 86 tests; the complete suites above also passed. **Visual Verification** Verified real TeamPage, Members, Overview, Settings, and workspace Audit log components in Chromium fixtures at desktop 1280x900 and mobile 390x844. Confirmed no Activity tab or team-activity requests, permission-appropriate remaining tabs/actions, redacted member emails, workspace audit permission/team filtering, and no horizontal overflow. Screenshots were uploaded. These checks used synthetic auth/data and mocked APIs in an isolated Vite harness, not a production-authenticated end-to-end test. **Facts And Exclusions** The branch includes the team audit-admission and web team-context updates merged from `main`, with conflict resolutions preserving both changes. Removing activity intentionally changes the catalog from 202 routes / 154 paths to 201 / 153. Latest schema assumptions remain D1 migration 0083 and DO migration 56; neither was changed. The new upstream dependency-only commit does not conflict with this branch. No new write paths, credential changes, repository-grant work, session creation changes, database backfill, or migrations. Team sessions, generic session/autofix activity, audit operation names, workspace audit team filtering, and all stored audit history remain. Historical changelog entries are retained, with a new entry documenting the removal. --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…Murray#2184) ## Summary - Require current owning-team membership for every user action except `read` on a team-owned session, including for workspace Owners and Administrators. Enforce the full action resolver in `off`, `shadow`, and `on` through one shared rollout predicate across HTTP admission, WebSocket commands, snapshot capabilities, and sandbox URL redaction. - Preserve existing visibility reads, workspace-owned behavior, private break-glass auditing, and read-authorized collaborator self-removal. - Remove session ownership moves end to end: control-plane route and store methods, web BFF/dialog/buttons, and session/automation/environment `move` actions and `canMove` capabilities. Retain historical `session.moved` audit registration and its viewer label, but remove it from the session audit write union. - Update authorization documentation, the changelog, fixtures, route counts, and catalog/admission snapshots. Visibility changes and their cascade remain intact. This closes the interaction boundary that could expose a team's sandbox credentials to nonmembers, and removes ownership changes that could leave a running sandbox carrying credentials from its previous team. Issue: [COL-257](https://linear.app/colemurray/issue/COL-257/teams-follow-up-shared-control-plane-web-only-team-members-act-on-team) ## Checkpoint Report ### Commands and Results Final validation was run sequentially after rebasing onto `main` at `3789205`: | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run typecheck` | Passed across all TypeScript workspaces, including control-plane unit/integration configurations | | `npm run lint:fix` | Passed | | `npm run lint:sql-portability` | Passed; no new violations | | `npm test -w @open-inspect/shared -- --maxWorkers=1 --silent` | 64 files passed; 1,079 tests passed | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1 --silent` | 339 files passed; 5,690 tests passed | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1 --silent` | 128 files passed; 1,644 tests passed, 1 skipped | | `npm test -w @open-inspect/web -- --maxWorkers=1 --silent` | 256 files passed; 2,545 tests passed | | `git diff --check origin/main...HEAD` | Passed | The integration suite prints its fault-injection diagnostics for forced DO eviction and malformed D1 input, plus NDJSON body warnings. The web suite prints a timer warning and jsdom navigation diagnostic. All final commands exited successfully. Regression coverage includes all session actions/visibilities/roles in the pure resolver; HTTP prompt and sandbox admission for nonmember Members, Administrators, Owners, and session creators; WebSocket prompt/typing before membership, after joining, and after removal; private removed owners; collaborator self-removal; false capabilities and redacted sandbox URLs in every mode; and 404 for the deleted scope route. Additional development commands: - `npm test -w @open-inspect/shared -- src/types/session-access.test.ts --maxWorkers=1`: red, then green with all 32 tests passing before runtime wiring. - `npm test -w @open-inspect/control-plane -- src/authorization/teams-enforcement.test.ts src/session/connection-authenticator.test.ts src/routes/session-runtime-proxy.test.ts --maxWorkers=1`: red before wiring; one subsequent fixture failure; then green with all 131 tests passing. - `npm run test:integration -w @open-inspect/control-plane -- test/integration/session-access-routes.test.ts test/integration/websocket-session-access.test.ts test/integration/session-scope-routes.test.ts --maxWorkers=1`: six fixture/expectation failures, corrected before the full green run. - `npm test -w @open-inspect/control-plane -- --maxWorkers=1`: initially 26 failures in the child-spawn suite because its partial mocked workspace row omitted `ownerTeamId` and `visibility`. The fixture now supplies those fields; no production child-creation path changed. - `npm test -w @open-inspect/control-plane -- src/router.spawn-child.test.ts --maxWorkers=1 --silent`: passed after correcting that fixture. - `npm run test:integration -w @open-inspect/control-plane -- test/integration/session-access-routes.test.ts test/integration/websocket-session-access.test.ts test/integration/session-scope-routes.test.ts test/integration/teams-routes.test.ts test/integration/hono-route-catalog-conformance.test.ts test/integration/route-admission-matrix.test.ts --maxWorkers=1 --silent -u`: wrote the catalog update; three sandbox-readiness fixture failures, then corrected by seeding a ready test sandbox and checking sandbox access before prompting. - `npm run test:integration -w @open-inspect/control-plane -- test/integration/session-access-routes.test.ts --maxWorkers=1 --silent`: all 28 tests passed. - `npm run test:integration -w @open-inspect/control-plane -- test/integration/hono-route-catalog-conformance.test.ts test/integration/route-admission-matrix.test.ts --maxWorkers=1 --silent -u`: all 15 tests passed and regenerated the catalog after rebase. - Changed TypeScript/Markdown files were formatted with Prettier; commit hooks passed. <details> <summary>Development failures, verbatim excerpts</summary> Pure resolver red run: ```text Test Files 1 failed (1) Tests 7 failed | 25 passed (32) AssertionError: team, owner, owner, suspended=false, move: expected { allowed: true } to deeply equal { allowed: false, …(1) } AssertionError: expected { allowed: true } to deeply equal { Object (allowed, reason) } - Expected + Received { - "allowed": false, - "reason": "not_member", + "allowed": true, } AssertionError: expected { canRead: false, …(3) } to deeply equal { canRead: false, …(2) } - Expected + Received { "canManage": true, + "canMove": true, "canRead": false, "canTrigger": true, } ``` Seam red run: ```text Test Files 3 failed (3) Tests 21 failed | 110 passed (131) TypeError: resolverDecides is not a function AssertionError: expected { id: 'session-1', …(20) } to not have property "codeServerUrl" - Expected: undefined + Received: "https://code.example" AssertionError: expected 200 to be 403 // Object.is equality - Expected + Received - 403 + 200 AssertionError: expected { kind: 'allowed' } to deeply equal { Object (kind, reason) } - Expected + Received { - "kind": "denied", - "reason": "not_member", + "kind": "allowed", } ``` Post-wiring partial workspace fixture: ```text FAIL src/routes/session-runtime-proxy.test.ts > session runtime proxy routes > budget updates > forwards budget updates from the session owner AssertionError: expected 403 to be 200 // Object.is equality - Expected + Received - 200 + 403 Test Files 1 failed | 2 passed (3) Tests 1 failed | 130 passed (131) ``` Initial integration fixture/expectation failures: ```text Test Files 2 failed | 1 passed (3) Tests 6 failed | 57 passed (63) AssertionError: expected { …(9) } to match object { httpStatus: 403, …(1) } (7 matching properties omitted from actual) - Expected + Received { "httpStatus": 403, - "responseCode": "session_action_denied", + "responseCode": "not_member", } TypeError: Cannot read properties of undefined (reading 'id') AssertionError: expected { session: { …(26) }, …(5) } to match object { session: { capabilities: { …(3) } } } (44 matching properties omitted from actual) - Expected + Received { "session": { "capabilities": { "canChangeVisibility": false, - "canCollaborate": true, + "canCollaborate": false, "canRead": true, }, }, } ``` Full unit run before completing the child-spawn fixture: ```text FAIL src/router.spawn-child.test.ts > handleSpawnChild prompt enqueue handling > copies the exact parent provider auth snapshot with immediate inheritance AssertionError: expected 403 to be 201 // Object.is equality - Expected + Received - 201 + 403 Test Files 1 failed | 338 passed (339) Tests 26 failed | 5664 passed (5690) ``` Sandbox-readiness fixture: ```text AssertionError: expected 409 to be 200 // Object.is equality - Expected + Received - 200 + 409 Snapshots 1 updated Test Files 1 failed | 5 passed (6) Tests 3 failed | 133 passed (136) ``` </details> ### Baseline Facts and Drift - Initial clean `main` was `70b8ca4`. The resolver's participant rules at `packages/shared/src/types/session-access.ts:164-181`, HTTP rollout checks at `authorization/session-admission.ts:54,87,119`, WebSocket check at `session/connection-authenticator.ts:559`, membership loading at `session/components.ts:856`, and snapshot redaction at `routes/session-runtime-proxy.ts:202-205` matched the described interaction gap. - D1 migration `0083` and DO migration `56` remain unchanged. No migration, table, column, session-creation path, or token-mint change was needed. The unset enforcement default remains `shadow`. - During validation, `main` advanced to `3789205` with directory-email privacy changes and removal of the team Activity route. Rebased onto it and retained those changes, including `listCollaboratorCandidates` filtering and its updated web tests. Resolved the shared test-import conflict without restoring move tests, and regenerated the route catalog rather than preserving stale indexed captures. - Consequently the base has 201 routes / 153 paths, rather than the original 202 / 154. This PR removes exactly one route and one path: final counts are 200 / 152. Admission snapshot changes remove only the four scope-route entries. - The changelog did not have an Unreleased section. Added the operator-visible behavior change there; dated entries remain historical. ### Deliberately Left Out - No grants, token narrowing, team secrets, automation/environment scope routes, or unrelated ownership redesign. Only their existing move decision/capability fields are removed. - No migration or retroactive ownership rewrite. Historical independently scoped-child guard tests use direct SQL fixtures rather than a deleted move store method. - No production authentication bypass or permanent UI harness. Browser verification used the actual action components and application CSS in a temporary local fixture because no authenticated local backend/browser session was configured. ## Visual Verification Viewport screenshots at `http://127.0.0.1:4173` show the real components in the temporary verification fixture, not an authenticated session page: - Desktop, 1512x982: Copy link remains in the menu and Archive remains available; no move control. Uploaded artifact `109636aa4532b32fee3cd2db5a27f1b7`. - Mobile, 390x844: Details, Copy link, and Archive remain; no move control. Uploaded artifact `84470d42cbff051bf94fd83292909452`. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/c6dcc77c3bc65f8c373822e9b0f17043)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Changed** * Team-owned sessions now require current membership in the owning team for actions beyond reading, including for Owners and Administrators. Collaborators can still remove themselves with read access. * Sessions remain tied to their existing team or workspace; changing their ownership scope is no longer available. * Team visibility continues to control read access, and historical session-move audit records remain readable. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…up across 1 directory (ColeMurray#2182) Bumps the npm_and_yarn group with 1 update in the / directory: [hono](https://github.com/honojs/hono). Updates `hono` from 4.13.5 to 4.13.12 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/honojs/hono/releases">hono's releases</a>.</em></p> <blockquote> <h2>v4.13.12</h2> <h2>What's Changed</h2> <ul> <li>fix(build): keep internal types private in bundled d.ts and avoid a self-referencing JSX.IntrinsicElements in <a href="https://redirect.github.com/honojs/hono/pull/5485">honojs/hono#5485</a></li> <li>test(build): type-check the bundled declarations from a consumer project in <a href="https://redirect.github.com/honojs/hono/pull/5486">honojs/hono#5486</a></li> <li>fix(etag): correctly match mixed-case header name in retainedHeader option in <a href="https://redirect.github.com/honojs/hono/pull/5475">honojs/hono#5475</a></li> <li>fix(jsx): add px to numeric gridGap, gridRowGap and gridColumnGap in <a href="https://redirect.github.com/honojs/hono/pull/5487">honojs/hono#5487</a></li> <li>fix(combine): return a Response from a short-circuiting middleware in some() in <a href="https://redirect.github.com/honojs/hono/pull/5391">honojs/hono#5391</a></li> <li>chore(deps): upgrade vite-plus to 1.0.0 in <a href="https://redirect.github.com/honojs/hono/pull/5464">honojs/hono#5464</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/honojs/hono/compare/v4.13.11...v4.13.12">https://github.com/honojs/hono/compare/v4.13.11...v4.13.12</a></p> <h2>v4.13.11</h2> <h2>Security fixes</h2> <h3><code>serveStatic</code> decodes the request path a second time, leading to bypass of middleware on static paths</h3> <p>Affects: <code>hono/serve-static</code> and the adapters built on it (<code>hono/bun</code>, <code>hono/deno</code>, <code>hono/cloudflare-workers</code>, <code>@hono/bun</code>, <code>@hono/deno</code>, <code>@hono/cloudflare-workers</code>). Fixes <code>serveStatic</code> decoding an already-decoded path, where a crafted request could be routed as one path and served as another, skipping middleware mounted on a static prefix. GHSA-5r4p-p66f-jhc7</p> <p><code>serveStatic</code> now rejects request paths that still contain <code>%</code> after decoding. To serve files whose names contain a literal <code>%</code>, set <code>allowPercentInPath: true</code>.</p> <p>The same fix ships in <code>@hono/node-server</code> v2.1.3.</p> <h2>v4.13.10</h2> <h2>Adapters are now separate packages</h2> <p>The runtime adapters are now published as their own packages: <code>@hono/bun</code>, <code>@hono/deno</code>, <code>@hono/cloudflare-workers</code>, <code>@hono/aws-lambda</code>, <code>@hono/lambda-edge</code>, <code>@hono/netlify</code>, <code>@hono/vercel</code>, and <code>@hono/service-worker</code>. <code>@hono/deno</code> is also on JSR.</p> <p><code>hono/<adapter></code> still works in v4 but is deprecated and will be removed in v5. Migrating is an import change:</p> <pre lang="diff"><code>- import { serveStatic } from 'hono/bun' + import { serveStatic } from '@hono/bun' </code></pre> <p>hono/cloudflare-pages is deprecated without a replacement package; Cloudflare recommends Workers with static assets.</p> <h2>What's Changed</h2> <ul> <li>chore: migrate the package manager from bun to pnpm in <a href="https://redirect.github.com/honojs/hono/pull/5433">honojs/hono#5433</a></li> <li>chore(package.json): invoke package scripts through pnpm instead of bun in <a href="https://redirect.github.com/honojs/hono/pull/5434">honojs/hono#5434</a></li> <li>chore: replace prettier with oxfmt in <a href="https://redirect.github.com/honojs/hono/pull/5435">honojs/hono#5435</a></li> <li>chore(deps): upgrade vitest to 5.0.1 in <a href="https://redirect.github.com/honojs/hono/pull/5437">honojs/hono#5437</a></li> <li>chore: let oxfmt sort imports instead of eslint in <a href="https://redirect.github.com/honojs/hono/pull/5442">honojs/hono#5442</a></li> <li>chore: replace eslint with oxlint in <a href="https://redirect.github.com/honojs/hono/pull/5443">honojs/hono#5443</a></li> <li>chore: introduce Vite+ in <a href="https://redirect.github.com/honojs/hono/pull/5444">honojs/hono#5444</a></li> <li>fix(types): allow returning a Blob as a response body in <a href="https://redirect.github.com/honojs/hono/pull/5446">honojs/hono#5446</a></li> <li>chore: convert build scripts into plugins in <a href="https://redirect.github.com/honojs/hono/pull/5448">honojs/hono#5448</a></li> <li>chore: stop editorconfig-checker from checking Markdown indent size in <a href="https://redirect.github.com/honojs/hono/pull/5455">honojs/hono#5455</a></li> <li>ci: remove empty step left in <code>cr.yml</code> by the pnpm migration in <a href="https://redirect.github.com/honojs/hono/pull/5456">honojs/hono#5456</a></li> <li>chore(deps): upgrade vite-plus to <code>1.0.0-rc.1</code> in <a href="https://redirect.github.com/honojs/hono/pull/5459">honojs/hono#5459</a></li> <li>feat(adapters): add <code>@hono/bun</code> as a workspace package in <a href="https://redirect.github.com/honojs/hono/pull/5447">honojs/hono#5447</a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/honojs/hono/commit/6abd35b0a5f35f67b6417627d5b0a6c2d266ac04"><code>6abd35b</code></a> 4.13.12</li> <li><a href="https://github.com/honojs/hono/commit/95eb860473d26bd303a2a29716e61a77c8c5e5f5"><code>95eb860</code></a> chore(deps): upgrade vite-plus to 1.0.0 (<a href="https://redirect.github.com/honojs/hono/issues/5464">#5464</a>)</li> <li><a href="https://github.com/honojs/hono/commit/afb2068c1c53a530528ac5f237e245793dd03c07"><code>afb2068</code></a> fix(combine): return a Response from a short-circuiting middleware in some() ...</li> <li><a href="https://github.com/honojs/hono/commit/e5bb2062a3ddd645e3a16d1300bd76d6c9af33a5"><code>e5bb206</code></a> fix(jsx): add px to numeric gridGap, gridRowGap and gridColumnGap (<a href="https://redirect.github.com/honojs/hono/issues/5487">#5487</a>)</li> <li><a href="https://github.com/honojs/hono/commit/c3053ccf14f1c4c70a0c2888eed6121b9f6c8f95"><code>c3053cc</code></a> fix(etag): correctly match mixed-case header name in retainedHeader option (#...</li> <li><a href="https://github.com/honojs/hono/commit/c437d7569219a21e45fa81797749e8f34fe19dcc"><code>c437d75</code></a> test(build): type-check the bundled declarations from a consumer project (<a href="https://redirect.github.com/honojs/hono/issues/5486">#5486</a>)</li> <li><a href="https://github.com/honojs/hono/commit/be1f7498fed269544a3c544db0a5a727048462a8"><code>be1f749</code></a> fix(build): keep internal types private in bundled d.ts and avoid a self-refe...</li> <li><a href="https://github.com/honojs/hono/commit/37ce06904e732d4bc11c9075adf362c76049a594"><code>37ce069</code></a> 4.13.11</li> <li><a href="https://github.com/honojs/hono/commit/1e1207cabfcd154146bf6ac9fd6a0b646f7fc484"><code>1e1207c</code></a> test(serve-static): fix the test (<a href="https://redirect.github.com/honojs/hono/issues/5479">#5479</a>)</li> <li><a href="https://github.com/honojs/hono/commit/8b05c774ef1555c70f5ba5e2991b7e77cc3635cc"><code>8b05c77</code></a> Merge commit from fork</li> <li>Additional commits viewable in <a href="https://github.com/honojs/hono/compare/v4.13.5...v4.13.12">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ColeMurray/background-agents/network/alerts). </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
**Summary**
Add team-scoped encrypted secrets for team-owned work, with precedence
`global -> team -> environment or repositories`. Workspace-owned
sessions retain their existing secret scopes.
- Add a Zod-validated `TeamSecretsStore` using the existing encryption
key, validation, and per-scope key cap.
- Add human-only team secret GET/PUT/DELETE routes authorized by
`requireTeam("canManageSecrets")` in every enforcement mode.
- Batch secret mutations with `team.secret_set` / `team.secret_deleted`
audit rows containing key names only.
- Resolve sandbox team secrets from current D1 session ownership and
retain the `team` source in the managed-provider filter.
- Include the environment owner's team secrets in environment builds,
never in repository-shared builds.
- Add a capability-gated Secrets tab, reuse the existing editor, and add
encoded browser proxy routes.
- Update audit labels, route policy counts/snapshots, and the operator
changelog.
Issue: https://linear.app/colemurray/issue/COL-207
**Checkpoint Report**
1. Commands and results
| Command | Result |
| --- | --- |
| `npm run build -w @open-inspect/shared` | Passed |
| `npm run typecheck` | Passed across every TypeScript workspace; rerun
after final editor changes |
| `npm run lint:fix` | Passed; rerun after final changes |
| `npm run lint:sql-portability` | Passed: `SQL portability: clean (24
baselined occurrence(s) across 4 file(s)).` |
| `npm test -w @open-inspect/control-plane -- --maxWorkers=1` | 338
files, 5,653 tests passed |
| `npm run test:integration -w @open-inspect/control-plane --
--maxWorkers=1` | 130 files passed; 1,669 tests passed, 1 skipped |
| `npm test -w @open-inspect/web -- --maxWorkers=1` | Final rerun: 260
files, 2,476 tests passed |
| `npm test -w @open-inspect/shared -- --maxWorkers=1
src/types/audit-events.test.ts` | 32 tests passed |
| `npm test -w @open-inspect/control-plane -- --maxWorkers=1
src/session/session-target-secrets.test.ts
src/session/user-env-resolver.test.ts src/image-builds/scope.test.ts` |
Red before resolution wiring: 6 failed, 60 passed. Green afterward: 66
passed |
| `npm run test:integration -w @open-inspect/control-plane --
--maxWorkers=1 test/integration/team-secrets.test.ts
test/integration/team-secrets-routes.test.ts
test/integration/team-secrets-store.test.ts` | Initial run: 9 failed, 38
passed. Six resolution failures before wiring, three invalid bot-actor
fixtures corrected |
| `npm run test:integration -w @open-inspect/control-plane --
--maxWorkers=1 test/integration/team-secrets.test.ts
test/integration/team-secrets-routes.test.ts
test/integration/team-secrets-store.test.ts
test/integration/hono-route-catalog-conformance.test.ts
test/integration/route-admission-matrix.test.ts` | 61 tests passed |
| `npm test -w @open-inspect/web -- --maxWorkers=1
src/components/teams/team-secrets.test.tsx
src/components/teams/teams-pages.test.tsx
src/components/settings/audit-log-settings.test.tsx
"src/app/api/teams/[id]/secrets/route.test.ts"
"src/app/api/teams/[id]/secrets/[key]/route.test.ts"` | Initial targeted
frontend run: 78 tests passed |
| `npm test -w @open-inspect/web -- --maxWorkers=1
src/components/teams/team-secrets.test.tsx` | Final targeted editor run:
13 tests passed, including a scope-switch mutation regression test |
| `git ls-files --modified --others --exclude-standard -z \| xargs -0
npx prettier --write --ignore-unknown` | Formatted intended changes |
| `git diff --check` and `git diff --check origin/main...HEAD` | Passed
|
Verbatim red unit assertion excerpts:
```text
FAIL src/image-builds/scope.test.ts > loadScopeBuildSecrets > loads the environment owner's team before environment secrets
AssertionError: expected "vi.fn()" to be called with arguments: [ 'team_build' ]
Number of calls: 0
FAIL src/session/session-target-secrets.test.ts > buildSessionTargetSecretSources > merges team after global and before target secrets for target null
AssertionError: expected [ 'global', 'acme/web' ] to deeply equal [ 'global', 'team', 'acme/web' ]
FAIL src/session/session-target-secrets.test.ts > buildSessionTargetSecretSources > merges team after global and before target secrets for target env_team
AssertionError: expected [ 'global', 'environment' ] to deeply equal [ 'global', 'team', 'environment' ]
FAIL src/session/session-target-secrets.test.ts > buildSessionTargetSecretSources > includes team secrets in an ad-hoc session without repositories
AssertionError: expected { SHARED: 'global' } to deeply equal { SHARED: 'team' }
FAIL src/session/user-env-resolver.test.ts > UserEnvResolver > session-target secret fold > includes team broker secrets from the current public D1 session ownership
AssertionError: expected { SHARED: 'global' } to deeply equal { SHARED: 'team', …(1) }
FAIL src/session/user-env-resolver.test.ts > UserEnvResolver > session-target secret fold > fails closed when the authoritative session index row is missing
AssertionError: promise resolved "undefined" instead of rejecting
Test Files 3 failed (3)
Tests 6 failed | 60 passed (66)
```
Verbatim initial integration assertion excerpts:
```text
FAIL test/integration/team-secrets-routes.test.ts > team secrets routes > denies members, nonmembers, bots and suspended leads in off mode
FAIL test/integration/team-secrets-routes.test.ts > team secrets routes > denies members, nonmembers, bots and suspended leads in shadow mode
FAIL test/integration/team-secrets-routes.test.ts > team secrets routes > denies members, nonmembers, bots and suspended leads in on mode
AssertionError: github-bot/slack:U-SECRET-ACTOR/GET: expected 401 to be 403 // Object.is equality
FAIL test/integration/team-secrets.test.ts > team secret resolution > gives environment precedence over team and global and retains team broker credentials
AssertionError: expected { SHARED: 'environment', …(2) } to deeply equal { SHARED: 'environment', …(4) }
FAIL test/integration/team-secrets.test.ts > team secret resolution > gives team precedence over global without letting member repositories change the broker
AssertionError: expected { SHARED: 'global', …(2) } to deeply equal { SHARED: 'team-a', …(4) }
FAIL test/integration/team-secrets.test.ts > team secret resolution > keeps primary repository precedence over team secrets
AssertionError: expected 'global' to be 'team-a' // Object.is equality
FAIL test/integration/team-secrets.test.ts > team secret resolution > never injects another team's secrets or any team secrets into workspace sessions
AssertionError: expected { SHARED: 'global', …(2) } to deeply equal { SHARED: 'team-b', …(3) }
FAIL test/integration/team-secrets.test.ts > team secret resolution > uses current D1 ownership after a session moves teams
AssertionError: expected undefined to be 'b' // Object.is equality
FAIL test/integration/team-secrets.test.ts > team secret resolution > adds the environment owner's team to builds but never to repository-shared builds
AssertionError: expected { SHARED: 'environment', …(2) } to deeply equal { SHARED: 'environment', …(4) }
Test Files 2 failed | 1 passed (3)
Tests 9 failed | 38 passed (47)
```
The bot fixture used a Slack actor for GitHub and Linear credentials.
Fixtures now use valid service-specific actors and still assert exactly
403; production authentication was not changed.
The passing full integration suite emitted NDJSON decoding warnings and
diagnostics from deliberate eviction/invalid-D1 fixtures. The passing
web suite emitted a `TimeoutNaNWarning` and jsdom navigation warning.
2. Verified facts and drift
Branched from `main` at `19e7993`, still matching `origin/main` before
opening. Existing D1 migration `0083` supplies `team_secrets` and
`environments.owner_team_id`; no migration was added. The main session
fold, broker filter, separate build fold, encryption helpers, key cap,
and human team capability admission match the cited behavior. Catalog
baseline was 202 routes / 154 paths; this adds three routes / two paths,
resulting in 205 / 156.
The DO `SessionRow` has no team ownership field
(`session/types.ts:36-65`). Ownership is authoritative in D1
(`db/session-index.ts:80,448-452`), so the resolver reads the existing
index by public session name on each resolution instead of adding DO
state or a migration. Tests cover ownership changes and a missing index
row.
3. Deliberately excluded / limitations
- No new migration, schema field, ownership mutation, repository
grant/token change, or edits to the parallel team/environment/planner
work.
- `OAuthSecretScope` remains `environment | repo | global`; no team
OAuth persistence/write-back was added. Team-only legacy OAuth refresh
tokens are not end-to-end supported: the requested managed-source fold
retains the team layer, but existing OpenAI/xAI brokers still read only
target/global scopes (`session/openai-token-refresh-service.ts:43-46`,
`session/xai-token-refresh-service.ts:78-85`). Team API keys and
ordinary secrets use the new precedence normally. The broker paths were
deliberately left unchanged rather than expanding the
credential/write-back design.
- No new snapshot or prebuilt-image invalidation guarantees. Current
resolution is not revocation of credentials already baked into existing
artifacts. Per-scope capacity retains the existing stores'
read-before-write concurrency behavior.
- Visual verification used the real Next team page with local synthetic
auth/API fixtures, not a production login or remote persistence.
**Visual Verification**
Opened `http://127.0.0.1:3000/teams/platform` as a team lead, verified
tab visibility, list metadata, create/update/delete, clearing saved
values, and no horizontal overflow at mobile width. Uploaded viewport
captures:
- Desktop 1440x1000: artifact `55a3727e9b76c72f089518af573c4e60`.
- Mobile 390x844, sidebar closed: artifact
`4891293b8afb43836a4f8de6bcacfeb4`.
---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/97c7dd352452cdbbe444bffb0c0b0c97)*
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Team members with secret-management access can manage encrypted team
secrets from the team’s Secrets tab, including adding, updating, and
deleting secrets.
* Team-owned sessions and environment image builds can use team secrets;
repository-shared image builds do not. Team secrets override global
secrets, while environment or repository secrets take precedence where
applicable.
* Audit logs identify team-secret changes without exposing secret
values, and secret lists show key metadata rather than stored values.
* Team-secret changes supersede affected environment images and trigger
best-effort rebuild scheduling for eligible team environments.
* **Bug Fixes**
* Environment image builds now fail rather than proceed when team
secrets cannot be read or decrypted.
<!-- 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>
# Conflicts: # package-lock.json # package.json # packages/control-plane/package.json # packages/control-plane/src/router.policy.test.ts # packages/control-plane/src/routes/catalog.ts # packages/control-plane/src/routes/session-index.ts # packages/control-plane/src/routes/teams.ts # packages/control-plane/src/session/connection-authenticator.ts # packages/control-plane/src/session/initialize.ts # packages/control-plane/test/integration/__snapshots__/hono-route-catalog-conformance.test.ts.snap # packages/control-plane/test/integration/__snapshots__/route-admission-matrix.test.ts.snap # packages/control-plane/test/integration/hono-route-catalog-conformance.test.ts # packages/docs/package.json # packages/github-bot/package.json # packages/linear-bot/package.json # packages/modal-infra/uv.lock # packages/sandbox-images/locks/runtime.txt # packages/sandbox-runtime/uv.lock # packages/slack-bot/package.json # packages/web/package.json # packages/web/src/components/session-desktop-layout.tsx # scripts/compose-smoke.sh
…rray#2186) Follow-ups from the review of ColeMurray#2184. That PR was merged before these could be pushed to it. ## Behavior changes - **Collaborators on team-owned sessions must be team members.** `PUT /sessions/:id/collaborators/:userId` now returns `409 not_team_member` when the session is team-owned and the target user isn't in the owning team. Previously the grant returned `updated` but couldn't be used, because the `not_member` check from ColeMurray#2184 denies every non-read action to non-members. Re-adding an existing non-member collaborator also returns the 409 now, instead of `unchanged`. Sessions without an owning team behave as before. - **The collaborator picker only lists the owning team.** For team-owned sessions, `GET /sessions/:id/collaborator-candidates` returns only members of the owning team. `UserStore.listCollaboratorCandidates` takes an optional `teamId` for this. - **Batch archive reports `not_member`.** Sessions skipped because of team membership are reported as `not_member` instead of `missing_permission`, so admins who have `sessions.lifecycle` see the real cause. The shared `sessionBatchArchiveResponseSchema` gains this value. The web route only passes the response through, so no deployed client parses it. ## No behavior change - `effectiveSessionCapabilities` reuses the results `sessionCapabilities` already computed, instead of calling `checkSessionAccess` again for each action. This runs on every session-list and inbox row. - `updateSessionScope` no longer refetches `/api/me/teams`. That refetch was only needed by the removed move-and-join-team flow. - Removed the unused `TeamStore.isActive` and made `TeamMembershipStore.bindAddIfJoinable` private. ## Testing - New integration tests cover rejecting non-member collaborators on team-owned sessions, filtering the picker, and the `not_member` batch-archive reason in `off`, `shadow` and `on` modes. - `npm run typecheck`, ESLint and Prettier pass. - Control-plane unit tests (5690), the full control-plane integration suite (1648 passed, 1 skipped) and the web `session-scope` tests (88) pass. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/b6f9227a231685a574df1b97128c1176)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Collaborator suggestions for team-owned sessions are limited to active members of the owning team, and adding a non-member is rejected. * Batch archive results identify sessions skipped because the requester is not a team member. * **Bug Fixes** * Collaborators on team-owned private sessions can access them only while they remain members of the owning team. Leaving or being removed from the team ends access, while the collaborator record remains. * Batch archive results distinguish missing sessions, team-membership restrictions, and permission issues. * Updating session scope no longer triggers an unnecessary membership-data refresh. <!-- 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>
…ray#2187) Follow-up to ColeMurray#2186. Route handlers should not contain raw SQL. ## Changes - Added `UserStore.getCollaboratorEligibility(userId, teamId)`. It runs the existing user, role assignment and team membership lookup, and returns one of `"eligible" | "not_found" | "inactive" | "not_team_member"`. It sits next to `listCollaboratorCandidates`, which applies the same rules (active user, optionally restricted to a team). - `changeCollaborator` in `routes/session-scope.ts` now calls the store and maps each outcome to the same HTTP responses as before (`404`, `409 user_inactive`, `409 not_team_member`). The route no longer contains raw SQL or the zod row schema. No behavior change: the SQL, the eligibility rules and the responses are unchanged. ## Testing - New `UserStore` integration test covering every outcome: missing, suspended, no role assignment, eligible without a team, not a team member, and team member. - The existing route-level tests for `session-scope-routes`, `session-access-routes` and `collaborator-candidates` pass unchanged. - Control-plane typecheck and ESLint pass. Control-plane unit tests pass (5718). --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/b6f9227a231685a574df1b97128c1176)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Collaborator changes now consistently check whether the selected person is eligible, active, and—when applicable—a member of the specified team. * Existing error responses remain in place for missing users, inactive users, and people who are not team members. * **Tests** * Added coverage for eligibility outcomes, including active users with and without team membership. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
|
Important Review skippedToo many files! This PR contains 477 files, which is 177 over the limit of 300. To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: ⛔ Files ignored due to path filters (5)
📒 Files selected for processing (477)
You can disable this status message by setting the
Comment |
Terraform Validation Results
Pushed by: @rhlsthrm, Action: |
There was a problem hiding this comment.
Blocking: 5 · Non-blocking: 0
The sync has five reachable regressions: two child-session operations continue after team-membership revocation, partial Modal VM resolution can permanently drop configured access URLs, and two team-session composer states do not recover when server-side settings or grants change. Targeted web, control-plane, and Modal suites passed (134 tests total); these states are not exercised by those tests.
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.sign_in_provider_gate: Refreshing state... [id=b29a3d55-0be5-fb92-10a9-027df10c4b75]
local_file.web_app_wrangler_production[0]: Refreshing state... [id=d58ccd8dd2962c70f7cff2ffac9821e9e711af31]
terraform_data.access_control_gate: Refreshing state... [id=2b965617-b42b-4b42-5ea5-a1c3c05f10be]
random_bytes.provider_accounts_encryption_key: Refreshing state...
data.external.modal_source_hash[0]: Reading...
random_password.service_auth_secret_web: Refreshing state... [id=none]
random_password.image_callback_token_pepper: Refreshing state... [id=none]
random_password.service_auth_secret_github_bot: Refreshing state... [id=none]
random_password.service_auth_secret_slack_bot: Refreshing state... [id=none]
module.linear_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=777f94c3595f4de680c256a4e5fc6653]
cloudflare_queue.image_build_finalization: Refreshing state... [id=a0647323f7424e778b9d59da50dc55cf]
null_resource.linear_bot_build[0]: Refreshing state... [id=2336431810903569290]
module.slack_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=ab5c371c8bc04a938ff2f71809933aa0]
null_resource.github_bot_build[0]: Refreshing state... [id=460415052272141772]
null_resource.control_plane_build: Refreshing state... [id=4032343797984097467]
null_resource.slack_bot_build[0]: Refreshing state... [id=8806402866395017856]
cloudflare_queue.slack_completion_delivery_dlq[0]: Refreshing state... [id=396865e4939b4160937b2dc9ad5dabe1]
cloudflare_queue.github_autofix[0]: Refreshing state... [id=033a23f13783415385b2f8799416c20f]
cloudflare_queue.image_build_finalization_dlq: Refreshing state... [id=61535c686d8546099cfddcf39833572b]
cloudflare_queue.github_autofix_dlq[0]: Refreshing state... [id=3a27213aeba149d4b7cbf2d3551842f8]
module.session_index_kv.cloudflare_workers_kv_namespace.this: Refreshing state... [id=ea0a253d5cb64d75a841acb88040cd2f]
cloudflare_d1_database.main: Refreshing state... [id=f747a908-5c69-45a1-86ab-ceb5250cf5e0]
data.external.modal_source_hash[0]: Read complete after 0s [id=-]
cloudflare_queue.slack_completion_delivery[0]: Refreshing state... [id=247b1100bac2408684d6a75c1bca0d28]
module.modal_app[0].null_resource.modal_secrets[0]: Refreshing state... [id=8477737195823217344]
module.github_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=e0848d433a4f466cafd4ed5d140aad7d]
null_resource.web_app_cloudflare_build[0]: Refreshing state... [id=5919987247446669914]
cloudflare_r2_bucket.media: Refreshing state... [id=open-inspect-media-codos]
random_password.service_auth_secret_linear_bot: Refreshing state... [id=none]
module.modal_app[0].null_resource.modal_deploy: Refreshing state... [id=7017762631504497281]
module.slack_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=375c2c6875904657bce05c62c8048c76]
module.linear_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=049cd48117bc48b9b4332683a97d0a0e]
module.linear_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=462ec59e-45f2-4919-bc8c-67ccd8e78603]
module.slack_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=616c9f2d-42ba-428f-aa92-c92a7b3d9062]
null_resource.d1_migrations: Refreshing state... [id=263751651589333239]
module.linear_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=b3551012-aa08-4887-8111-f50cd84f6606]
module.slack_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=98bb72f6-43c7-4b54-b83f-ff03417c8c8e]
module.control_plane_worker.cloudflare_worker.this: Refreshing state... [id=3457352971a74b89be5ed3700db48a8e]
cloudflare_queue_consumer.slack_completion_delivery[0]: Refreshing state...
module.control_plane_worker.cloudflare_worker_version.this: Refreshing state... [id=e4cb6179-e3b8-4fc5-b475-16e426337d5e]
module.control_plane_worker.cloudflare_workers_deployment.this: Refreshing state... [id=daa99ba5-ee7c-4ed0-8297-ac45291ade2e]
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=7564838195349044890]
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=bf55403e-9f54-4f87-aa0f-3566f5be6d37]
module.github_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=245cb52e-0297-4467-ac40-135020a5f360]
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"]
# Keep-names makes esbuild emit __name() calls, which leak into next-themes'
# inline script and throw in the browser.
keep_names = false
# 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 = "4032343797984097467" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-30T06:09:54Z" -> (known after apply)
}
}
# null_resource.github_bot_build[0] must be replaced
-/+ resource "null_resource" "github_bot_build" {
~ id = "460415052272141772" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-30T06:09:54Z" -> (known after apply)
}
}
# null_resource.linear_bot_build[0] must be replaced
-/+ resource "null_resource" "linear_bot_build" {
~ id = "2336431810903569290" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-30T06:09:54Z" -> (known after apply)
}
}
# null_resource.slack_bot_build[0] must be replaced
-/+ resource "null_resource" "slack_bot_build" {
~ id = "8806402866395017856" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-30T06:09:54Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_build[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_build" {
~ id = "5919987247446669914" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-30T06:09:54Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_deploy[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_deploy" {
~ id = "7564838195349044890" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-30T06:10:32Z" -> (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-30T06:09:56Z" -> (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-30T06:09:57Z" -> (known after apply)
~ id = "e4cb6179-e3b8-4fc5-b475-16e426337d5e" -> (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 = "7c326d097bd00f90415ba7ea647ac70560d159a441e8f8ddcdd06a0c52b86db5" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/control-plane/dist/index.js"
+ content_sha256 = "854e8ac71750e38324cf7b66d059d72ca16e62fb6074fd2ecc7f976909621078"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 75 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 114 -> (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-30T06:09:59Z" -> (known after apply)
~ id = "daa99ba5-ee7c-4ed0-8297-ac45291ade2e" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "e4cb6179-e3b8-4fc5-b475-16e426337d5e" -> (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-30T06:09:59Z" -> (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-30T06:10:00Z" -> (known after apply)
~ id = "bf55403e-9f54-4f87-aa0f-3566f5be6d37" -> (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 = "24a995f9a6400cc83e954f37d0b51fd285bff5acde3171c65b0c510bfea21d1e" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/github-bot/dist/index.js"
+ content_sha256 = "5cdbd14abb2645c367130bc1db746dca2929dc7ea270f57914d1c3cdc084bc44"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 73 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 43 -> (known after apply)
~ urls = [
- "https://bf55403e-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-30T06:10:02Z" -> (known after apply)
~ id = "245cb52e-0297-4467-ac40-135020a5f360" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "bf55403e-9f54-4f87-aa0f-3566f5be6d37" -> (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-30T06:09:54Z" -> (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-30T06:09:55Z" -> (known after apply)
~ id = "462ec59e-45f2-4919-bc8c-67ccd8e78603" -> (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 = "b9038ee4ceaeef0b63f068a5b4c28196cf50e10a06059a73349d6d4759c34fa5" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/linear-bot/dist/index.js"
+ content_sha256 = "896f64892838c78a55e22e96e5362ddd9e6d308f1d8238b5dcbe5cde6d83f593"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 79 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 36 -> (known after apply)
~ urls = [
- "https://462ec59e-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-30T06:09:56Z" -> (known after apply)
~ id = "b3551012-aa08-4887-8111-f50cd84f6606" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "462ec59e-45f2-4919-bc8c-67ccd8e78603" -> (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 = "7017762631504497281" -> (known after apply)
~ triggers = { # forces replacement
~ "source_hash" = "505874e03fa4789888fc99e4f0ccbbd066826ff3da9536cde08b26cad10bbd00" -> "2ba85fecd95506fef751e24b3b0ffa9c6c9c26ff72519878575273c24e06a415"
# (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-30T06:09:54Z" -> (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-30T06:09:55Z" -> (known after apply)
~ id = "616c9f2d-42ba-428f-aa92-c92a7b3d9062" -> (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 = "a05dcd3c4f467eab77b59442a603212626d705ae22f163bee0536a1d49a4b10a" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/slack-bot/dist/index.js"
+ content_sha256 = "d461e51f22ef13a3c3e7048bad91ed13f450872b4af43e534acce447d7d50a65"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 77 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 54 -> (known after apply)
~ urls = [
- "https://616c9f2d-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-30T06:09:56Z" -> (known after apply)
~ id = "98bb72f6-43c7-4b54-b83f-ff03417c8c8e" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "616c9f2d-42ba-428f-aa92-c92a7b3d9062" -> (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 |
Terraform Validation Results
Pushed by: @rhlsthrm, Action: |
Terraform Plan ResultsStatus: ✅ Success Show Planterraform_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]
terraform_data.sign_in_provider_gate: Refreshing state... [id=b29a3d55-0be5-fb92-10a9-027df10c4b75]
random_bytes.provider_accounts_encryption_key: Refreshing state...
null_resource.github_bot_build[0]: Refreshing state... [id=460415052272141772]
null_resource.control_plane_build: Refreshing state... [id=4032343797984097467]
null_resource.linear_bot_build[0]: Refreshing state... [id=2336431810903569290]
local_file.web_app_wrangler_production[0]: Refreshing state... [id=d58ccd8dd2962c70f7cff2ffac9821e9e711af31]
random_password.service_auth_secret_linear_bot: Refreshing state... [id=none]
random_password.image_callback_token_pepper: Refreshing state... [id=none]
random_password.service_auth_secret_web: Refreshing state... [id=none]
random_password.service_auth_secret_github_bot: Refreshing state... [id=none]
random_password.service_auth_secret_slack_bot: Refreshing state... [id=none]
null_resource.slack_bot_build[0]: Refreshing state... [id=8806402866395017856]
module.modal_app[0].null_resource.modal_secrets[0]: Refreshing state... [id=8477737195823217344]
null_resource.web_app_cloudflare_build[0]: Refreshing state... [id=5919987247446669914]
data.external.modal_source_hash[0]: Reading...
module.session_index_kv.cloudflare_workers_kv_namespace.this: Refreshing state... [id=ea0a253d5cb64d75a841acb88040cd2f]
module.slack_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=ab5c371c8bc04a938ff2f71809933aa0]
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_d1_database.main: Refreshing state... [id=f747a908-5c69-45a1-86ab-ceb5250cf5e0]
cloudflare_queue.github_autofix_dlq[0]: Refreshing state... [id=3a27213aeba149d4b7cbf2d3551842f8]
module.github_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=e0848d433a4f466cafd4ed5d140aad7d]
cloudflare_r2_bucket.media: Refreshing state... [id=open-inspect-media-codos]
data.external.modal_source_hash[0]: Read complete after 0s [id=-]
cloudflare_queue.slack_completion_delivery[0]: Refreshing state... [id=247b1100bac2408684d6a75c1bca0d28]
cloudflare_queue.image_build_finalization_dlq: Refreshing state... [id=61535c686d8546099cfddcf39833572b]
cloudflare_queue.github_autofix[0]: Refreshing state... [id=033a23f13783415385b2f8799416c20f]
module.modal_app[0].null_resource.modal_deploy: Refreshing state... [id=7017762631504497281]
module.slack_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=375c2c6875904657bce05c62c8048c76]
module.linear_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=049cd48117bc48b9b4332683a97d0a0e]
module.slack_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=616c9f2d-42ba-428f-aa92-c92a7b3d9062]
module.linear_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=462ec59e-45f2-4919-bc8c-67ccd8e78603]
null_resource.d1_migrations: Refreshing state... [id=263751651589333239]
module.slack_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=98bb72f6-43c7-4b54-b83f-ff03417c8c8e]
module.linear_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=b3551012-aa08-4887-8111-f50cd84f6606]
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=e4cb6179-e3b8-4fc5-b475-16e426337d5e]
module.control_plane_worker.cloudflare_workers_deployment.this: Refreshing state... [id=daa99ba5-ee7c-4ed0-8297-ac45291ade2e]
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...
module.github_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=fa832fd890a14336bc3c63305e9bc36f]
null_resource.web_app_cloudflare_deploy[0]: Refreshing state... [id=7564838195349044890]
null_resource.web_app_cloudflare_secrets[0]: Refreshing state... [id=8981633407656564074]
module.github_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=bf55403e-9f54-4f87-aa0f-3566f5be6d37]
module.github_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=245cb52e-0297-4467-ac40-135020a5f360]
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"]
# Keep-names makes esbuild emit __name() calls, which leak into next-themes'
# inline script and throw in the browser.
keep_names = false
# 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 = "4032343797984097467" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-30T06:09:54Z" -> (known after apply)
}
}
# null_resource.github_bot_build[0] must be replaced
-/+ resource "null_resource" "github_bot_build" {
~ id = "460415052272141772" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-30T06:09:54Z" -> (known after apply)
}
}
# null_resource.linear_bot_build[0] must be replaced
-/+ resource "null_resource" "linear_bot_build" {
~ id = "2336431810903569290" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-30T06:09:54Z" -> (known after apply)
}
}
# null_resource.slack_bot_build[0] must be replaced
-/+ resource "null_resource" "slack_bot_build" {
~ id = "8806402866395017856" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-30T06:09:54Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_build[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_build" {
~ id = "5919987247446669914" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-30T06:09:54Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_deploy[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_deploy" {
~ id = "7564838195349044890" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-09-30T06:10:32Z" -> (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-30T06:09:56Z" -> (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-30T06:09:57Z" -> (known after apply)
~ id = "e4cb6179-e3b8-4fc5-b475-16e426337d5e" -> (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 = "7c326d097bd00f90415ba7ea647ac70560d159a441e8f8ddcdd06a0c52b86db5" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/control-plane/dist/index.js"
+ content_sha256 = "854e8ac71750e38324cf7b66d059d72ca16e62fb6074fd2ecc7f976909621078"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 75 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 114 -> (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-30T06:09:59Z" -> (known after apply)
~ id = "daa99ba5-ee7c-4ed0-8297-ac45291ade2e" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "e4cb6179-e3b8-4fc5-b475-16e426337d5e" -> (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-30T06:09:59Z" -> (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-30T06:10:00Z" -> (known after apply)
~ id = "bf55403e-9f54-4f87-aa0f-3566f5be6d37" -> (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 = "24a995f9a6400cc83e954f37d0b51fd285bff5acde3171c65b0c510bfea21d1e" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/github-bot/dist/index.js"
+ content_sha256 = "5cdbd14abb2645c367130bc1db746dca2929dc7ea270f57914d1c3cdc084bc44"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 73 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 43 -> (known after apply)
~ urls = [
- "https://bf55403e-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-30T06:10:02Z" -> (known after apply)
~ id = "245cb52e-0297-4467-ac40-135020a5f360" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "bf55403e-9f54-4f87-aa0f-3566f5be6d37" -> (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-30T06:09:54Z" -> (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-30T06:09:55Z" -> (known after apply)
~ id = "462ec59e-45f2-4919-bc8c-67ccd8e78603" -> (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 = "b9038ee4ceaeef0b63f068a5b4c28196cf50e10a06059a73349d6d4759c34fa5" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/linear-bot/dist/index.js"
+ content_sha256 = "896f64892838c78a55e22e96e5362ddd9e6d308f1d8238b5dcbe5cde6d83f593"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 79 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 36 -> (known after apply)
~ urls = [
- "https://462ec59e-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-30T06:09:56Z" -> (known after apply)
~ id = "b3551012-aa08-4887-8111-f50cd84f6606" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "462ec59e-45f2-4919-bc8c-67ccd8e78603" -> (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 = "7017762631504497281" -> (known after apply)
~ triggers = { # forces replacement
~ "source_hash" = "505874e03fa4789888fc99e4f0ccbbd066826ff3da9536cde08b26cad10bbd00" -> "2ba85fecd95506fef751e24b3b0ffa9c6c9c26ff72519878575273c24e06a415"
# (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-30T06:09:54Z" -> (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-30T06:09:55Z" -> (known after apply)
~ id = "616c9f2d-42ba-428f-aa92-c92a7b3d9062" -> (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 = "a05dcd3c4f467eab77b59442a603212626d705ae22f163bee0536a1d49a4b10a" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/slack-bot/dist/index.js"
+ content_sha256 = "d461e51f22ef13a3c3e7048bad91ed13f450872b4af43e534acce447d7d50a65"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 77 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 54 -> (known after apply)
~ urls = [
- "https://616c9f2d-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-30T06:09:56Z" -> (known after apply)
~ id = "98bb72f6-43c7-4b54-b83f-ff03417c8c8e" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "616c9f2d-42ba-428f-aa92-c92a7b3d9062" -> (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: 2 · Non-blocking: 0
Two additional regressions are reachable at this head: a transient GitHub attribution lookup failure clears a participant's persisted SCM identity, and renaming a team slug replaces its detail page with a false 'Team not found' state. The five earlier blocking comments remain on their existing threads and are not duplicated here. GitHub's full PR diff endpoint returned HTTP 406 for the 482-file change; I reviewed the local base-to-head diff and traced these paths through their consumers.
Disposition of the
|
| Thread | Upstream origin | Disposition |
|---|---|---|
routes/session-children.ts:56: parent sandbox acts on team children without a current-author membership check |
ColeMurray#2145 | Reproduced in all three modes (prompt and cancel both bypassed; the suggested shortcut-only fix leaves prompt open) → ColeMurray#2192 |
routes/session-child-spawn.ts:270: sandbox spawns a team child after its author left the team |
ColeMurray#2165 | Reproduced in all three modes, and also for grandchildren → ColeMurray#2191 |
sandbox/vm_recovery.py:144: partial tunnel map committed, later URL never filled |
ColeMurray#2139 | Not a defect. This is the partial-result contract ColeMurray#2139 documented (docs/MODAL_DOCKER.md:108-110) and pinned (test_vm_resolve.py:137-159). The ordinary launch path ends in the same state, and is more exposed because Modal SDK 1.4.3 caches the first non-empty tunnels() response. Waiting for every enabled port would revert ColeMurray#2139's fix for the stuck race_pending. |
settings/teams-settings.tsx:52: /api/me/teams cache not revalidated after a policy change |
ColeMurray#2145 | Reproduced (2 failing → passing; mutation red) → ColeMurray#2189 |
hooks/use-warm-draft-session.ts:97: terminal draft never retried after a grant is added |
ColeMurray#2158 | Covered upstream by ColeMurray's open ColeMurray#2181. It exempts target_team_missing_grant from the terminal latch and adds the grant routes that make the case reachable. Other terminal codes are pinned on purpose by page-team-context.test.tsx:495-510. |
routes/session-ws-token.ts:73: SCM identity cleared when the attribution lookup fails |
ColeMurray#2165 | By design. Reproduced through workerd/D1, but ColeMurray#2165 deliberately treats enrichment as an authoritative snapshot: see its body, the review replies on session-ws-token.ts:41/:55, and the tests session-ws-token.test.ts:250-279 and scheduler-slack-events.test.ts:229-317. The suggested remedy turns those pinned tests red and restores stale attribution after a relink. |
teams/team-page.tsx:27: renaming the slug renders "Team not found" |
ColeMurray#2179 | Reproduced (fails before the fix; three mutations red) → ColeMurray#2190 |
Nothing here changes this sync's diff.
Syncs
ColeMurray/background-agentse471cff4..2ea66073(39 commits) into the fork.Upstream content
Team-scoped sessions (visibility, scope, team pages, team secrets, session access enforcement in the WebSocket DO), sandbox lifecycle refactor (COL-241..249), GPT-6.1 Sol model, Better Auth as sole participant credential source (ColeMurray#2165).
Four of our upstream PRs arrive in this sync as ordinary upstream commits: ColeMurray#2138, ColeMurray#2139, ColeMurray#2122, ColeMurray#2070.
Conflict resolutions
package.jsonx7,uv.lockx2): each pin resolved to the higher version on either side; keys unioned (upstream added@radix-ui/react-tabs).package-lock.jsonregenerated bynpm install.packages/sandbox-images/locks/runtime.txtregenerated bycli.py lock(fork pydantic 2.13.5 + upstream pyjwt 2.15.1).scripts/compose-smoke.sh: takes upstream's new replication loop, which usesgrep … >/dev/nullinstead ofgrep -q(avoids SIGPIPE underpipefail) and is pinned by the newscripts/compose-smoke.test.mjs. Fork-side changes: the pattern matches bothsnapshot written(Litestream 0.3, upstream) andsnapshot complete(0.5.17, the fork), and the deadline stays the fork's 60s. The first push resolved this file to the fork's side, which failed 2 of the 3 contract cases.session/initialize.ts: the fork's review-generation fence stays Step 1. Upstream's private-session owner guard and the newcollaboratorSourceSessionId/privateCreationActorfields are ported into the fork's Step 2. The guard runs before the fence insert, so it rejects before any write.routes/session-index.ts: the fork'scanonicalUserIdOfviewer (covers personal-access-token principals) combined with upstream'slegacyStartedhandling.routes/catalog.ts,routes/teams.ts: additive.connection-authenticator.ts,web/.../session-desktop-layout.tsx: upstream's version. The fork side of each conflict was only a formatter reflow or a prop type that upstream deleted./teams/:id/secretsroutes.Follow-up commits
test(control-plane): follow upstream team-read and enrichment contracts. Two fork personal-access-token tests asserted contracts that upstream changed. Trigger enrichment is now explicitlynull(fix: preserve participant identity and Better Auth authority ColeMurray/background-agents#2165).GET /teams/:idreturns 200 to non-members (fix(control-plane): quiet team reads and complete subscribe capabilities ColeMurray/background-agents#2174, admission snapshotGET /teams/:id off/nonmember=200). The test's invariant, that a token sees what its owner sees, still holds.fix(deps): restore workerd platform packages dropped from the lockfile. Runningnpm installover the merged lock dropped the five top-level@cloudflare/workerd-*@1.20260815.1optional entries.npm cithen rejected the lock, which failed every TypeScript job and Compose smoke on the first push. This commit restores those entries byte-for-byte from the fork's last CI-green lock (7d8f6cf4) with no other change;npm cipasses locally.chore(deps): hold prettier at upstream's locked version. Dependabot chore: bump the npm-minor-patch group across 1 directory with 46 updates #94 bumped prettier from 3.8.4 to 3.9.9, which reformatted 64 upstream-owned files. That reflow caused one of this sync's conflicts, andformat:checkwould fail on files that every sync brings in unchanged. This commit pins 3.8.4 (upstream's lockfile version), restores those 64 files to upstream bytes, reformats 6 fork-edited files, and adds a Dependabot ignore. Divergent files: 264 → 199.Verification
Local, sequential: typecheck 0, lint 0, prettier check clean on tracked files, control-plane unit and integration, web, github-bot, slack-bot, linear-bot, shared, mcp-server, docs, sandbox-runtime node + pytest, modal-infra pytest, SQL portability,
terraform test: all green.No D1 migration collisions: upstream's highest is
0083; the fork's run0090–0093.