Skip to content

chore: sync upstream 2026-10-01 (e471cff4..2ea66073) - #107

Merged
rhlsthrm merged 46 commits into
mainfrom
sync/upstream-2026-10-01
Oct 1, 2026
Merged

rhlsthrm merged 46 commits into
mainfrom
sync/upstream-2026-10-01

Conversation

@rhlsthrm

@rhlsthrm rhlsthrm commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Syncs ColeMurray/background-agents e471cff4..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

  • Dependency manifests (package.json x7, uv.lock x2): each pin resolved to the higher version on either side; keys unioned (upstream added @radix-ui/react-tabs). package-lock.json regenerated by npm install. packages/sandbox-images/locks/runtime.txt regenerated by cli.py lock (fork pydantic 2.13.5 + upstream pyjwt 2.15.1).
  • scripts/compose-smoke.sh: takes upstream's new replication loop, which uses grep … >/dev/null instead of grep -q (avoids SIGPIPE under pipefail) and is pinned by the new scripts/compose-smoke.test.mjs. Fork-side changes: the pattern matches both snapshot written (Litestream 0.3, upstream) and snapshot 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 new collaboratorSourceSessionId / privateCreationActor fields 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's canonicalUserIdOf viewer (covers personal-access-token principals) combined with upstream's legacyStarted handling.
  • 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.
  • Route fixtures: base 193/147, fork 206/158, upstream 203/154. Merged: 216/165. Snapshots regenerated; the only new admission rows are upstream's three /teams/:id/secrets routes.

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 explicitly null (fix: preserve participant identity and Better Auth authority ColeMurray/background-agents#2165). GET /teams/:id returns 200 to non-members (fix(control-plane): quiet team reads and complete subscribe capabilities ColeMurray/background-agents#2174, admission snapshot GET /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. Running npm install over the merged lock dropped the five top-level @cloudflare/workerd-*@1.20260815.1 optional entries. npm ci then 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 ci passes 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, and format:check would 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 run 0090–0093.

ColeMurray and others added 30 commits September 28, 2026 23:01
…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>
ColeMurray and others added 14 commits September 30, 2026 19:17
…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/&lt;adapter&gt;</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 />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=hono&package-manager=npm_and_yarn&previous-version=4.13.5&new-version=4.13.12)](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>
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Too 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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: ddeb11e2-4072-4a99-9311-2677986c0069

📥 Commits

Reviewing files that changed from the base of the PR and between 7d8f6cf and 519a906.

⛔ Files ignored due to path filters (5)
  • package-lock.json is excluded by !**/package-lock.json
  • packages/control-plane/test/integration/__snapshots__/hono-route-catalog-conformance.test.ts.snap is excluded by !**/*.snap
  • packages/control-plane/test/integration/__snapshots__/route-admission-matrix.test.ts.snap is excluded by !**/*.snap
  • packages/modal-infra/uv.lock is excluded by !**/*.lock
  • packages/sandbox-runtime/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (477)
  • .github/dependabot.yml
  • .github/workflows/compose-smoke.yml
  • .github/workflows/terraform.yml
  • CHANGELOG.md
  • docs/AUTH.md
  • docs/AVAILABLE_MODELS.md
  • docs/GETTING_STARTED.md
  • docs/MODAL_DOCKER.md
  • docs/plans/managed-skills.md
  • docs/plans/sandbox-lifecycle-manager-refactor.md
  • docs/plans/sandbox-lifecycle-refactor-baseline.md
  • eslint.config.js
  • package.json
  • packages/control-plane/package.json
  • packages/control-plane/src/auth/github.test.ts
  • packages/control-plane/src/auth/github.ts
  • packages/control-plane/src/auth/model-provider-account-adapters.ts
  • packages/control-plane/src/auth/openai.ts
  • packages/control-plane/src/authorization/session-admission.ts
  • packages/control-plane/src/authorization/session-socket-audit.ts
  • packages/control-plane/src/authorization/teams-enforcement.test.ts
  • packages/control-plane/src/authorization/teams-enforcement.ts
  • packages/control-plane/src/autofix/service.test.ts
  • packages/control-plane/src/created-at-cursor.ts
  • packages/control-plane/src/db/analytics-dashboard-store.test.ts
  • packages/control-plane/src/db/analytics-dashboard-store.ts
  • packages/control-plane/src/db/analytics-store.test.ts
  • packages/control-plane/src/db/analytics-store.ts
  • packages/control-plane/src/db/audit-event-cursor.ts
  • packages/control-plane/src/db/audit-event-store.ts
  • packages/control-plane/src/db/automation-store.ts
  • packages/control-plane/src/db/model-provider-account-atomic-writer.ts
  • packages/control-plane/src/db/pr-autofix-feedback-store.ts
  • packages/control-plane/src/db/provider-account-authorizations.ts
  • packages/control-plane/src/db/pull-request-analytics-store.test.ts
  • packages/control-plane/src/db/pull-request-analytics-store.ts
  • packages/control-plane/src/db/query-limits.ts
  • packages/control-plane/src/db/session-audit.ts
  • packages/control-plane/src/db/session-collaborators.ts
  • packages/control-plane/src/db/session-export-cursor.ts
  • packages/control-plane/src/db/session-export-store.ts
  • packages/control-plane/src/db/session-inbox-store.ts
  • packages/control-plane/src/db/session-index.test.ts
  • packages/control-plane/src/db/session-index.ts
  • packages/control-plane/src/db/session-list-predicates.test.ts
  • packages/control-plane/src/db/session-list-predicates.ts
  • packages/control-plane/src/db/session-run-store.test.ts
  • packages/control-plane/src/db/session-run-store.ts
  • packages/control-plane/src/db/session-scope-store.ts
  • packages/control-plane/src/db/session-visibility.test.ts
  • packages/control-plane/src/db/session-visibility.ts
  • packages/control-plane/src/db/team-audit.ts
  • packages/control-plane/src/db/team-memberships.ts
  • packages/control-plane/src/db/team-repository-grants.ts
  • packages/control-plane/src/db/team-secrets.ts
  • packages/control-plane/src/db/team-settings.ts
  • packages/control-plane/src/db/teams.ts
  • packages/control-plane/src/db/user-store.ts
  • packages/control-plane/src/http/bounded-body.ts
  • packages/control-plane/src/image-builds/daytona-adapter.test.ts
  • packages/control-plane/src/image-builds/finalizer.ts
  • packages/control-plane/src/image-builds/lookup.ts
  • packages/control-plane/src/image-builds/scheduler.test.ts
  • packages/control-plane/src/image-builds/scope.test.ts
  • packages/control-plane/src/image-builds/scope.ts
  • packages/control-plane/src/image-builds/types.ts
  • packages/control-plane/src/image-builds/workflow.test.ts
  • packages/control-plane/src/node/host-alarm-index.ts
  • packages/control-plane/src/router.create-session.test.ts
  • packages/control-plane/src/router.policy.test.ts
  • packages/control-plane/src/router.spawn-child.test.ts
  • packages/control-plane/src/routes/analytics.test.ts
  • packages/control-plane/src/routes/analytics.ts
  • packages/control-plane/src/routes/audit-events.test.ts
  • packages/control-plane/src/routes/audit-events.ts
  • packages/control-plane/src/routes/automation-lifecycle.test.ts
  • packages/control-plane/src/routes/automation-lifecycle.ts
  • packages/control-plane/src/routes/automation-validation.ts
  • packages/control-plane/src/routes/catalog.ts
  • packages/control-plane/src/routes/session-batch-archive.ts
  • packages/control-plane/src/routes/session-child-spawn.ts
  • packages/control-plane/src/routes/session-children.test.ts
  • packages/control-plane/src/routes/session-children.ts
  • packages/control-plane/src/routes/session-create.ts
  • packages/control-plane/src/routes/session-export.test.ts
  • packages/control-plane/src/routes/session-export.ts
  • packages/control-plane/src/routes/session-index.test.ts
  • packages/control-plane/src/routes/session-index.ts
  • packages/control-plane/src/routes/session-runtime-proxy.test.ts
  • packages/control-plane/src/routes/session-runtime-proxy.ts
  • packages/control-plane/src/routes/session-scope.ts
  • packages/control-plane/src/routes/session-team-grants.ts
  • packages/control-plane/src/routes/session-ws-token.test.ts
  • packages/control-plane/src/routes/session-ws-token.ts
  • packages/control-plane/src/routes/sessions.ts
  • packages/control-plane/src/routes/settings-teams.ts
  • packages/control-plane/src/routes/shared.test.ts
  • packages/control-plane/src/routes/shared.ts
  • packages/control-plane/src/routes/team-secrets.test.ts
  • packages/control-plane/src/routes/team-secrets.ts
  • packages/control-plane/src/routes/teams.test.ts
  • packages/control-plane/src/routes/teams.ts
  • packages/control-plane/src/routing/route-admission.ts
  • packages/control-plane/src/sandbox/client.test.ts
  • packages/control-plane/src/sandbox/client.ts
  • packages/control-plane/src/sandbox/daytona-rest-client.ts
  • packages/control-plane/src/sandbox/lifecycle/alarm-boot-budget-effects.test.ts
  • packages/control-plane/src/sandbox/lifecycle/allocation-cleanup.test.ts
  • packages/control-plane/src/sandbox/lifecycle/allocation-cleanup.ts
  • packages/control-plane/src/sandbox/lifecycle/image-selection.ts
  • packages/control-plane/src/sandbox/lifecycle/launch-context.test.ts
  • packages/control-plane/src/sandbox/lifecycle/launch-context.ts
  • packages/control-plane/src/sandbox/lifecycle/launch-orchestration.test.ts
  • packages/control-plane/src/sandbox/lifecycle/manager-shutdown.test.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.test.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.ts
  • packages/control-plane/src/sandbox/lifecycle/pending-vm-respawn.test.ts
  • packages/control-plane/src/sandbox/lifecycle/ports.ts
  • packages/control-plane/src/sandbox/lifecycle/provider-stop.ts
  • packages/control-plane/src/sandbox/lifecycle/rejected-allocation.test.ts
  • packages/control-plane/src/sandbox/lifecycle/sandbox-access.test.ts
  • packages/control-plane/src/sandbox/lifecycle/sandbox-access.ts
  • packages/control-plane/src/sandbox/lifecycle/startup-errors.ts
  • packages/control-plane/src/sandbox/lifecycle/test-helpers.ts
  • packages/control-plane/src/sandbox/lifecycle/vm-resolve.test.ts
  • packages/control-plane/src/sandbox/lifecycle/vm-startup-reconciliation.test.ts
  • packages/control-plane/src/sandbox/lifecycle/vm-startup-reconciliation.ts
  • packages/control-plane/src/sandbox/provider.test.ts
  • packages/control-plane/src/sandbox/provider.ts
  • packages/control-plane/src/sandbox/providers/daytona-provider.test.ts
  • packages/control-plane/src/sandbox/providers/e2b-provider-shutdown.test.ts
  • packages/control-plane/src/sandbox/providers/e2b-provider.test-helpers.ts
  • packages/control-plane/src/sandbox/providers/modal-provider.test.ts
  • packages/control-plane/src/sandbox/providers/modal-provider.ts
  • packages/control-plane/src/sandbox/providers/opencomputer-provider.test.ts
  • packages/control-plane/src/sandbox/providers/vercel/base-snapshot.test.ts
  • packages/control-plane/src/sandbox/providers/vercel/provider.test.ts
  • packages/control-plane/src/scheduler/scheduler.test.ts
  • packages/control-plane/src/scheduler/scheduler.ts
  • packages/control-plane/src/session/alarm/handler.test.ts
  • packages/control-plane/src/session/alarm/handler.ts
  • packages/control-plane/src/session/callback-delivery.ts
  • packages/control-plane/src/session/client-command-facade.ts
  • packages/control-plane/src/session/components.ts
  • packages/control-plane/src/session/connection-authenticator.test.ts
  • packages/control-plane/src/session/connection-authenticator.ts
  • packages/control-plane/src/session/enqueue-prompt-contract.ts
  • packages/control-plane/src/session/event-cursor.ts
  • packages/control-plane/src/session/http/handlers/child-session-summary.ts
  • packages/control-plane/src/session/http/handlers/session-init.handler.test.ts
  • packages/control-plane/src/session/http/handlers/session-init.handler.ts
  • packages/control-plane/src/session/http/handlers/ws-token.handler.test.ts
  • packages/control-plane/src/session/http/handlers/ws-token.handler.ts
  • packages/control-plane/src/session/identity.test.ts
  • packages/control-plane/src/session/identity.ts
  • packages/control-plane/src/session/initialize.test.ts
  • packages/control-plane/src/session/initialize.ts
  • packages/control-plane/src/session/message-queue.test.ts
  • packages/control-plane/src/session/message-queue.ts
  • packages/control-plane/src/session/message-router.ts
  • packages/control-plane/src/session/messenger.test.ts
  • packages/control-plane/src/session/participant-repository.test.ts
  • packages/control-plane/src/session/participant-repository.ts
  • packages/control-plane/src/session/participant-service.test.ts
  • packages/control-plane/src/session/participant-service.ts
  • packages/control-plane/src/session/ports.ts
  • packages/control-plane/src/session/sandbox-events/processor.test.ts
  • packages/control-plane/src/session/sandbox-push-service.test.ts
  • packages/control-plane/src/session/sandbox-repository.test.ts
  • packages/control-plane/src/session/sandbox-shutdown-repository.test.ts
  • packages/control-plane/src/session/sandbox-shutdown-repository.ts
  • packages/control-plane/src/session/sandbox-shutdown-safety.test.ts
  • packages/control-plane/src/session/sandbox-shutdown.test.ts
  • packages/control-plane/src/session/sandbox-shutdown.ts
  • packages/control-plane/src/session/server.test.ts
  • packages/control-plane/src/session/services/session-attachment-storage.ts
  • packages/control-plane/src/session/session-target-secrets.test.ts
  • packages/control-plane/src/session/session-target-secrets.ts
  • packages/control-plane/src/session/title.ts
  • packages/control-plane/src/session/user-env-resolver.test.ts
  • packages/control-plane/src/session/user-env-resolver.ts
  • packages/control-plane/src/source-control/github-credential-authority.ts
  • packages/control-plane/test/conformance/session-core-conformance.ts
  • packages/control-plane/test/integration/access-tokens.test.ts
  • packages/control-plane/test/integration/analytics.test.ts
  • packages/control-plane/test/integration/audit-event-store.test.ts
  • packages/control-plane/test/integration/audit-events-route.test.ts
  • packages/control-plane/test/integration/autofix-activity-route.test.ts
  • packages/control-plane/test/integration/child-session-ops.test.ts
  • packages/control-plane/test/integration/collaborator-candidates.test.ts
  • packages/control-plane/test/integration/create-pr.test.ts
  • packages/control-plane/test/integration/d1-session-index.test.ts
  • packages/control-plane/test/integration/helpers.ts
  • packages/control-plane/test/integration/hono-route-catalog-conformance.test.ts
  • packages/control-plane/test/integration/image-build-lookup.test.ts
  • packages/control-plane/test/integration/pr-autofix-feedback-store.test.ts
  • packages/control-plane/test/integration/pull-request-analytics.test.ts
  • packages/control-plane/test/integration/route-admission-matrix.test.ts
  • packages/control-plane/test/integration/sandbox-lifecycle-harness.ts
  • packages/control-plane/test/integration/sandbox-shutdown.test.ts
  • packages/control-plane/test/integration/sandbox-state-retention.test.ts
  • packages/control-plane/test/integration/scheduler-slack-events.test.ts
  • packages/control-plane/test/integration/service-auth.test.ts
  • packages/control-plane/test/integration/session-access-routes.test.ts
  • packages/control-plane/test/integration/session-collaborators.test.ts
  • packages/control-plane/test/integration/session-components.test.ts
  • packages/control-plane/test/integration/session-discovery.test.ts
  • packages/control-plane/test/integration/session-do-access.ts
  • packages/control-plane/test/integration/session-export-store.test.ts
  • packages/control-plane/test/integration/session-export.test.ts
  • packages/control-plane/test/integration/session-inbox-scoping.test.ts
  • packages/control-plane/test/integration/session-inbox-test-helpers.ts
  • packages/control-plane/test/integration/session-inbox.test.ts
  • packages/control-plane/test/integration/session-provider-auth.test.ts
  • packages/control-plane/test/integration/session-read-state.test.ts
  • packages/control-plane/test/integration/session-repositories.test.ts
  • packages/control-plane/test/integration/session-runs.test.ts
  • packages/control-plane/test/integration/session-scope-routes.test.ts
  • packages/control-plane/test/integration/session-snapshot.test.ts
  • packages/control-plane/test/integration/session-visibility-lists.test.ts
  • packages/control-plane/test/integration/spawn-children.test.ts
  • packages/control-plane/test/integration/team-member-privacy.test.ts
  • packages/control-plane/test/integration/team-route-helpers.ts
  • packages/control-plane/test/integration/team-secrets-routes.test.ts
  • packages/control-plane/test/integration/team-secrets-store.test.ts
  • packages/control-plane/test/integration/team-secrets.test.ts
  • packages/control-plane/test/integration/team-stores.test.ts
  • packages/control-plane/test/integration/teams-routes.test.ts
  • packages/control-plane/test/integration/user-store.test.ts
  • packages/control-plane/test/integration/websocket-client.test.ts
  • packages/control-plane/test/integration/websocket-sandbox.test.ts
  • packages/control-plane/test/integration/websocket-session-access.test.ts
  • packages/control-plane/test/integration/ws-token-participants.test.ts
  • packages/docs/README.md
  • packages/docs/content/docs/models/choosing-a-model.mdx
  • packages/docs/package.json
  • packages/github-bot/package.json
  • packages/github-bot/src/github-auth.ts
  • packages/github-bot/src/session-target.ts
  • packages/linear-bot/package.json
  • packages/linear-bot/src/callbacks.complete.test.ts
  • packages/linear-bot/src/callbacks.ts
  • packages/modal-infra/pyproject.toml
  • packages/modal-infra/src/sandbox/build_session.py
  • packages/modal-infra/src/sandbox/launch.py
  • packages/modal-infra/src/sandbox/manager.py
  • packages/modal-infra/src/sandbox/termination.py
  • packages/modal-infra/src/sandbox/vm_recovery.py
  • packages/modal-infra/tests/modal_sdk_contract.py
  • packages/modal-infra/tests/test_build_sandbox_lifecycle.py
  • packages/modal-infra/tests/test_modal_sdk_contract.py
  • packages/modal-infra/tests/test_sandbox_launch.py
  • packages/modal-infra/tests/test_snapshot_timeout.py
  • packages/modal-infra/tests/test_vm_resolve.py
  • packages/modal-infra/tests/test_web_api_build_sandbox.py
  • packages/sandbox-images/locks/runtime.txt
  • packages/sandbox-runtime/pyproject.toml
  • packages/sandbox-runtime/src/sandbox_runtime/bridge.py
  • packages/sandbox-runtime/src/sandbox_runtime/docker_control.py
  • packages/sandbox-runtime/src/sandbox_runtime/docker_service.py
  • packages/sandbox-runtime/src/sandbox_runtime/plugins/codex-auth-plugin.js
  • packages/sandbox-runtime/src/sandbox_runtime/supervisor.py
  • packages/sandbox-runtime/tests/codex-auth-plugin.test.mjs
  • packages/sandbox-runtime/tests/fixtures/reasoning-models.json
  • packages/sandbox-runtime/tests/test_bridge_shutdown_preparation.py
  • packages/sandbox-runtime/tests/test_docker_control.py
  • packages/sandbox-runtime/tests/test_docker_service.py
  • packages/sandbox-runtime/tests/test_opencode_reasoning_contract.py
  • packages/shared/src/models.test.ts
  • packages/shared/src/models.ts
  • packages/shared/src/service-auth.ts
  • packages/shared/src/session-list-query.test.ts
  • packages/shared/src/session-list-query.ts
  • packages/shared/src/slack/client.ts
  • packages/shared/src/triggers/slack/conditions.ts
  • packages/shared/src/types/analytics.ts
  • packages/shared/src/types/audit-events.test.ts
  • packages/shared/src/types/audit-events.ts
  • packages/shared/src/types/integrations.ts
  • packages/shared/src/types/keyboard-shortcuts.ts
  • packages/shared/src/types/provider-accounts.ts
  • packages/shared/src/types/server-messages.test.ts
  • packages/shared/src/types/server-messages.ts
  • packages/shared/src/types/session-access.test.ts
  • packages/shared/src/types/session-access.ts
  • packages/shared/src/types/session-api.ts
  • packages/shared/src/types/session-archive.ts
  • packages/shared/src/types/session-inbox.ts
  • packages/shared/src/types/sessions.ts
  • packages/shared/src/types/teams.test.ts
  • packages/shared/src/types/teams.ts
  • packages/slack-bot/package.json
  • packages/slack-bot/src/app-home/slack-types.ts
  • packages/slack-bot/src/attachments.ts
  • packages/slack-bot/src/inline-flags.ts
  • packages/slack-bot/src/sessions/control-plane-client.ts
  • packages/slack-bot/src/target-clarification.ts
  • packages/web/package.json
  • packages/web/src/app/(app)/(sidebar)/analytics/page.test.tsx
  • packages/web/src/app/(app)/(sidebar)/layout.tsx
  • packages/web/src/app/(app)/(sidebar)/page-team-context.test.tsx
  • packages/web/src/app/(app)/(sidebar)/page.test-fixture.tsx
  • packages/web/src/app/(app)/(sidebar)/page.test.tsx
  • packages/web/src/app/(app)/(sidebar)/page.tsx
  • packages/web/src/app/(app)/(sidebar)/session/[id]/page.test.tsx
  • packages/web/src/app/(app)/(sidebar)/session/[id]/page.tsx
  • packages/web/src/app/(app)/(sidebar)/session/[id]/session-snapshot-provider.test.tsx
  • packages/web/src/app/(app)/(sidebar)/session/[id]/session-snapshot-provider.tsx
  • packages/web/src/app/(app)/(sidebar)/sessions/page.test.tsx
  • packages/web/src/app/(app)/(sidebar)/sessions/page.tsx
  • packages/web/src/app/(app)/(sidebar)/teams/[slug]/page.tsx
  • packages/web/src/app/(app)/(sidebar)/teams/page.tsx
  • packages/web/src/app/api/audit-events/route.test.ts
  • packages/web/src/app/api/audit-events/route.ts
  • packages/web/src/app/api/environments/route.test.ts
  • packages/web/src/app/api/environments/route.ts
  • packages/web/src/app/api/repos/route.test.ts
  • packages/web/src/app/api/repos/route.ts
  • packages/web/src/app/api/sessions/[id]/collaborator-candidates/route.test.ts
  • packages/web/src/app/api/sessions/[id]/collaborator-candidates/route.ts
  • packages/web/src/app/api/sessions/[id]/collaborators/[userId]/route.test.ts
  • packages/web/src/app/api/sessions/[id]/collaborators/[userId]/route.ts
  • packages/web/src/app/api/sessions/[id]/route.test.ts
  • packages/web/src/app/api/sessions/[id]/route.ts
  • packages/web/src/app/api/sessions/[id]/visibility/route.identity.test.ts
  • packages/web/src/app/api/sessions/[id]/visibility/route.test.ts
  • packages/web/src/app/api/sessions/[id]/visibility/route.ts
  • packages/web/src/app/api/sessions/inbox/route.test.ts
  • packages/web/src/app/api/sessions/inbox/route.ts
  • packages/web/src/app/api/sessions/route.test.ts
  • packages/web/src/app/api/sessions/route.ts
  • packages/web/src/app/api/settings/teams/route.test.ts
  • packages/web/src/app/api/settings/teams/route.ts
  • packages/web/src/app/api/teams/[id]/secrets/[key]/route.test.ts
  • packages/web/src/app/api/teams/[id]/secrets/[key]/route.ts
  • packages/web/src/app/api/teams/[id]/secrets/route.test.ts
  • packages/web/src/app/api/teams/[id]/secrets/route.ts
  • packages/web/src/app/api/teams/[id]/sessions/route.test.ts
  • packages/web/src/app/api/teams/[id]/sessions/route.ts
  • packages/web/src/app/api/teams/route.test.ts
  • packages/web/src/components/action-bar.test.tsx
  • packages/web/src/components/analytics/summary-cards.test.tsx
  • packages/web/src/components/analytics/token-cards.test.tsx
  • packages/web/src/components/app-destinations.test.ts
  • packages/web/src/components/app-destinations.ts
  • packages/web/src/components/automations/automation-target-selection.ts
  • packages/web/src/components/diff-retry-notice.test.tsx
  • packages/web/src/components/global-command-menu.test.tsx
  • packages/web/src/components/global-command-menu.tsx
  • packages/web/src/components/pierre-diff-renderer.test.tsx
  • packages/web/src/components/pierre-diff-renderer.tsx
  • packages/web/src/components/queued-prompt-stack.test.tsx
  • packages/web/src/components/secrets-editor.tsx
  • packages/web/src/components/session-access-selector.test.tsx
  • packages/web/src/components/session-access-selector.tsx
  • packages/web/src/components/session-changes-panel.test.tsx
  • packages/web/src/components/session-changes-panel.tsx
  • packages/web/src/components/session-controls.test.tsx
  • packages/web/src/components/session-desktop-layout.test.tsx
  • packages/web/src/components/session-desktop-layout.tsx
  • packages/web/src/components/session-details-overlay.test.tsx
  • packages/web/src/components/session-details-overlay.tsx
  • packages/web/src/components/session-discovery-filters.tsx
  • packages/web/src/components/session-header.test.tsx
  • packages/web/src/components/session-header.tsx
  • packages/web/src/components/session-list-item.test.tsx
  • packages/web/src/components/session-list-item.tsx
  • packages/web/src/components/session-prompt-composer.test.tsx
  • packages/web/src/components/session-right-sidebar.test.tsx
  • packages/web/src/components/session-right-sidebar.tsx
  • packages/web/src/components/session-sidebar.test.tsx
  • packages/web/src/components/session-sidebar.tsx
  • packages/web/src/components/session-timeline-autofix.test.tsx
  • packages/web/src/components/session-timeline-scroll.test.tsx
  • packages/web/src/components/session-timeline.test.tsx
  • packages/web/src/components/session-visibility-control.tsx
  • packages/web/src/components/settings/audit-log-settings.test.tsx
  • packages/web/src/components/settings/audit-log-settings.tsx
  • packages/web/src/components/settings/data-controls-settings.test.tsx
  • packages/web/src/components/settings/data-controls-settings.tsx
  • packages/web/src/components/settings/settings-registry.ts
  • packages/web/src/components/settings/team-members-table.tsx
  • packages/web/src/components/settings/teams-settings.test.tsx
  • packages/web/src/components/settings/teams-settings.tsx
  • packages/web/src/components/sidebar-layout.test.tsx
  • packages/web/src/components/sidebar-layout.tsx
  • packages/web/src/components/sidebar/budget-section.test.tsx
  • packages/web/src/components/sidebar/budget-section.tsx
  • packages/web/src/components/sidebar/collaborators-section.directory.test.tsx
  • packages/web/src/components/sidebar/collaborators-section.tsx
  • packages/web/src/components/sidebar/collapsible-section.tsx
  • packages/web/src/components/sidebar/details-section.tsx
  • packages/web/src/components/sidebar/files-changed-section.test.tsx
  • packages/web/src/components/sidebar/files-changed-section.tsx
  • packages/web/src/components/sidebar/managed-skills-section.tsx
  • packages/web/src/components/sidebar/metadata-section.test.tsx
  • packages/web/src/components/sidebar/metadata-section.tsx
  • packages/web/src/components/sidebar/participants-section.tsx
  • packages/web/src/components/team-switcher.test.tsx
  • packages/web/src/components/team-switcher.tsx
  • packages/web/src/components/teams/team-overview.test.tsx
  • packages/web/src/components/teams/team-overview.tsx
  • packages/web/src/components/teams/team-page.tsx
  • packages/web/src/components/teams/team-secrets.test.tsx
  • packages/web/src/components/teams/team-secrets.tsx
  • packages/web/src/components/teams/teams-icon.tsx
  • packages/web/src/components/teams/teams-index.tsx
  • packages/web/src/components/teams/teams-pages.test.tsx
  • packages/web/src/components/ui/command.tsx
  • packages/web/src/components/ui/icons.tsx
  • packages/web/src/components/ui/tabs.tsx
  • packages/web/src/components/user-identity.tsx
  • packages/web/src/hooks/use-active-team.test.tsx
  • packages/web/src/hooks/use-active-team.ts
  • packages/web/src/hooks/use-analytics.test.tsx
  • packages/web/src/hooks/use-audit-events.test.tsx
  • packages/web/src/hooks/use-audit-events.ts
  • packages/web/src/hooks/use-automations.test.tsx
  • packages/web/src/hooks/use-browser-layout-storage.test.tsx
  • packages/web/src/hooks/use-browser-layout-storage.ts
  • packages/web/src/hooks/use-environments.test.tsx
  • packages/web/src/hooks/use-environments.ts
  • packages/web/src/hooks/use-provider-authorization-code.ts
  • packages/web/src/hooks/use-repos.test.tsx
  • packages/web/src/hooks/use-repos.ts
  • packages/web/src/hooks/use-session-attachments.test.tsx
  • packages/web/src/hooks/use-session-attachments.ts
  • packages/web/src/hooks/use-session-collaborator-candidates.ts
  • packages/web/src/hooks/use-session-diff-selection.test.tsx
  • packages/web/src/hooks/use-session-diff-selection.ts
  • packages/web/src/hooks/use-session-diffs.test.tsx
  • packages/web/src/hooks/use-session-diffs.ts
  • packages/web/src/hooks/use-session-inspector-tab.test.tsx
  • packages/web/src/hooks/use-session-inspector-tab.ts
  • packages/web/src/hooks/use-session-rename.test.tsx
  • packages/web/src/hooks/use-session-rename.ts
  • packages/web/src/hooks/use-session-socket.test.tsx
  • packages/web/src/hooks/use-session-socket.ts
  • packages/web/src/hooks/use-session-target-picker.test.ts
  • packages/web/src/hooks/use-session-target-picker.ts
  • packages/web/src/hooks/use-session-transport.test.tsx
  • packages/web/src/hooks/use-session-transport.ts
  • packages/web/src/hooks/use-sidebar-sessions.test.tsx
  • packages/web/src/hooks/use-sidebar-sessions.ts
  • packages/web/src/hooks/use-teams.test.tsx
  • packages/web/src/hooks/use-teams.ts
  • packages/web/src/hooks/use-warm-draft-session.test.tsx
  • packages/web/src/hooks/use-warm-draft-session.ts
  • packages/web/src/lib/archive-session.test.ts
  • packages/web/src/lib/archive-session.ts
  • packages/web/src/lib/automation-templates.ts
  • packages/web/src/lib/composer-access.test.ts
  • packages/web/src/lib/composer-access.ts
  • packages/web/src/lib/me-teams-cache.test.ts
  • packages/web/src/lib/me-teams-cache.ts
  • packages/web/src/lib/session-action-error.ts
  • packages/web/src/lib/session-capabilities.test.ts
  • packages/web/src/lib/session-capabilities.ts
  • packages/web/src/lib/session-discovery.test.ts
  • packages/web/src/lib/session-discovery.ts
  • packages/web/src/lib/session-harness.ts
  • packages/web/src/lib/session-inbox-api.test.ts
  • packages/web/src/lib/session-inbox-api.ts
  • packages/web/src/lib/session-scope-refresh.test.tsx
  • packages/web/src/lib/session-scope.test.ts
  • packages/web/src/lib/session-scope.ts
  • packages/web/src/lib/session-socket/reducer.test.ts
  • packages/web/src/lib/session-socket/reducer.ts
  • packages/web/src/lib/timeline-virtual-rows.ts
  • scripts/compose-smoke.sh
  • scripts/compose-smoke.test.mjs
  • scripts/lint-sandbox-boundaries.test.mjs
  • scripts/terraform-workflow-contract.test.mjs
  • terraform/README.md
  • terraform/environments/production/docs-vercel.tf
  • terraform/environments/production/locals.tf
  • terraform/environments/production/tests/docs_site.tftest.hcl

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format ✅
Init ✅
Validate ✅
Tests ✅
Modal module tests ✅

Pushed by: @rhlsthrm, Action: pull_request

@codos-reviewer codos-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/control-plane/src/routes/session-children.ts
Comment thread packages/control-plane/src/routes/session-child-spawn.ts
Comment thread packages/modal-infra/src/sandbox/vm_recovery.py
Comment thread packages/web/src/components/settings/teams-settings.tsx
Comment thread packages/web/src/hooks/use-warm-draft-session.ts
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format ✅
Init ✅
Validate ✅
Tests ✅
Modal module tests ✅

Pushed by: @rhlsthrm, Action: pull_request

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

Terraform Plan Results

Status: ✅ Success

Show Plan
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]
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

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format ✅
Init ✅
Validate ✅
Tests ✅
Modal module tests ✅

Pushed by: @rhlsthrm, Action: pull_request

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

Terraform Plan Results

Status: ✅ Success

Show Plan
terraform_data.access_control_gate: Refreshing state... [id=2b965617-b42b-4b42-5ea5-a1c3c05f10be]
terraform_data.cloudflare_custom_domain_gate: Refreshing state... [id=09b89c9b-996a-d28b-1f9c-08760a492dac]
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

@codos-reviewer codos-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/control-plane/src/routes/session-ws-token.ts
Comment thread packages/web/src/components/teams/team-page.tsx
@rhlsthrm

rhlsthrm commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Disposition of the codos-reviewer blocking findings

All seven threads are on files this sync carries from upstream unchanged: git diff --quiet origin/main HEAD -- <path> is clean for each one. Patching them here would add fork divergence, so each one was reproduced on clean upstream (f1cf0697) and routed upstream, or closed with evidence.

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.

@rhlsthrm
rhlsthrm merged commit 1ab03cc into main Oct 1, 2026
45 of 48 checks passed
@rhlsthrm
rhlsthrm deleted the sync/upstream-2026-10-01 branch October 1, 2026 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants