chore: sync upstream 2026-10-02 (team-owned automations/environments) - #110
Conversation
…ray#2178) ## Summary Implements COL-226. Modal snapshot restores no longer get a git token minted by Modal. Every Modal launch path now uses the control-plane credential broker, and Modal holds no source-control credentials. - **The control plane is the only source of VCS identity.** `createModalProviderFromEnv` passes `SCM_PROVIDER` into `ModalSandboxProvider`, which resolves `scmCloneIdentity()` once, the same way the Daytona, E2B, Vercel, and OpenComputer providers do. `ModalClient` sends `clone_host` / `clone_username` on create, restore, and image-build requests. The session lifecycle, launch config, and provider interfaces are unchanged. - **Modal requires the identity and has no fallback.** `api-create-sandbox`, `api-restore-sandbox`, and `api-create-build-sandbox` reject requests without `clone_host` / `clone_username`. `inject_vcs_env_vars` no longer reads `SCM_PROVIDER`, and the GitHub/GitLab/Bitbucket defaults are removed. - **Modal-side minting is removed.** This deletes `clone_token.py`, the `sandbox_runtime.auth.github_app` module and its PyJWT dependency, the `github-app` secret binding on `api_restore_sandbox`, Terraform provisioning of that secret, and the generated `GITHUB_TOKEN` / `GITHUB_APP_TOKEN` / `OI_GITHUB_TOKEN_IS_FALLBACK` aliases on restore. `_gh_wrapper_should_mint` now only checks for a user-supplied `GH_TOKEN` / `GITHUB_TOKEN`. - **Image-build clone auth is simpler.** `ImageBuildCloneAuth` now carries only the token. The Modal provider supplies host and username from its own identity. - **Docs are consolidated** into one CHANGELOG entry plus updated statements in HOW_IT_WORKS, the control-plane, github-bot, and modal-infra READMEs, and the security and sandbox-environment pages. Image builds still receive a one-shot `VCS_CLONE_TOKEN` because they have no session to broker through. User-supplied token overrides are preserved. ## Verification - Control-plane unit tests: 5,655 passed. Integration tests: 1,646 passed, 1 skipped. - Modal-infra pytest: 541 passed. Sandbox-runtime pytest: 1,401 passed, 3 skipped. - Docs tests: 44 passed. The production docs build passed. - `npm run typecheck`, ESLint, Prettier, and Ruff check/format all passed. - Not exercised: a live Modal deploy, or restoring a real production snapshot. ## Rollout - The new Modal endpoints reject requests that lack `clone_host` / `clone_username`. Terraform deploys Modal before the control-plane Worker, so session launches and image builds return 400 for the short window between the two deploys. We accept that window instead of carrying a compatibility fallback. - Terraform no longer provisions Modal's `github-app` secret but does not delete an existing one. Delete it from Modal after the upgrade. Rolling back to a version that binds it requires recreating it. - Keep the GitHub App credentials configured for the control plane and the GitHub bot. - Team-scoped credential minting (COL-205) is unaffected. This PR only moves the non-secret host/username, and tokens are still minted per request by the broker. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/d7e84d56dd72df3752b1971396368869)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Changed** * Snapshot-restored sessions now obtain short-lived Git credentials through the control plane, consistent with fresh and prebuilt sessions. * Sandbox launches and restores now use the configured source-control host and clone username. One-shot image builds continue to use a clone token. * Modal no longer needs GitHub App credentials or the related secret. Existing GitHub App secrets can be deleted after upgrading. <!-- 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 repository-grant management, team-scoped repository catalogs, and repository authorization at the specified write sites. Issue: https://linear.app/colemurray/issue/COL-204 - Add installation-wide or named repository grants, with SCM identity validation, mutually exclusive grant kinds, and an atomic 500-repository cap. - Batch actual grant mutations with `teams.grants_version` increments and `team.grant_added` / `team.grant_removed` domain audit rows. Duplicate PUTs are idempotent; existing resource references are never rewritten. - Filter `/repos?teamId=` after reading the existing global `repos:list:v3` cache. Workspace and actorless calls without a team keep the full catalog. Missing and inaccessible teams return the identical 404. - Share identity-bearing target/grant checks across session creation, sandbox child creation, session moves, environment repository edits, automation selections, skill assignments/import sources, repository secrets, image-build triggers, and environment secret import. - Workspace repository surfaces retain existing permissions when no team grants the repository. Otherwise, callers need membership in any granting team, lead membership for repository secrets, or a built-in Owner/Administrator role. Installation grants count for every repository; these rules do not depend on enforcement mode. - Manual team-owned environment builds require membership in the owning team or Owner/Administrator status, and still require the team's repository coverage. Secret import checks both destination coverage and source secret authorization before copying or scheduling. - Add the capability-gated Repositories tab and BFF routes. Members read grants; leads and administrators add/remove them. Empty or failed team catalogs never implicitly select “No repository”; the missing-grant error names the repository. - Audit missing-team and nonmember team 404s through `authorizationDenial` with `team_not_visible`, retaining the identical `Team not found` response. Allowed-decision auditing is unchanged by this PR. ## Checkpoint Report ### Commands And Results Validation was run sequentially with one Vitest worker, then repeated after rebasing onto `main` at `70b8ca4`: | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed; also rebuilt by the final root typecheck | | `npm run typecheck` | Passed across every workspace, including control-plane Node and integration types | | `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 -- --maxWorkers=1` | 345 files passed; 5,846 tests passed | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1` | 128 files passed; 1,654 tests passed, 1 skipped; shell timeout 900000 ms | | `npm test -w @open-inspect/web -- --maxWorkers=1` | 263 files passed; 2,630 tests passed | | `npm test -w @open-inspect/shared -- --maxWorkers=1` | 64 files passed; 1,074 tests passed | | `git diff --name-only -z origin/main...HEAD -- "*.ts" "*.tsx" "*.md" \| xargs -0 npx prettier --check` | Passed | | `git diff --check origin/main...HEAD` | Passed | The passing integration suite emitted workerd eviction/invalid-request diagnostics and NDJSON warnings; the web suite emitted jsdom navigation and timeout warnings. Neither suite reported test failures or Vitest unhandled errors. Targeted regression runs also passed: grant storage/workspace authorization/image routes (168 tests), source-import authorization/environment secrets (146 tests), and the regenerated route/admission snapshots plus grant integration tests (28 tests). ### Red Tests And Resolved Failures Tests were added before the grant storage/API implementations. Initial failures included these verbatim excerpts: ```text TypeError: store.add is not a function TypeError: store.listTeamsForRepository is not a function Error: The property "listTeamsForRepository" is not defined on the object. ``` The repository-owner lookup red run reported: ```text Test Files 2 failed (2) Tests 24 failed | 17 passed (41) ``` After adding the lookup but before replacing the old mode-dependent authorization, the behavioral red run reported: ```text AssertionError: expected undefined to be 403 // Object.is equality Test Files 1 failed | 1 passed (2) Tests 12 failed | 29 passed (41) ``` Intermediate validation failures were resolved rather than suppressed: ```text AssertionError: expected 409 to be 200 // Object.is equality AssertionError: expected 500 to be 201 // Object.is equality AssertionError: expected [] to deep equally contain { Object (action) } ``` The first two exposed outdated fixtures: automation rows omitted their required null `owner_team_id`, and the team-owned sandbox-child fixture had neither a grant nor a configured SCM resolution. Fixtures now accurately represent workspace ownership or granted team ownership. The third exposed the missing denial audit decision and is covered by the minimal admission fix. Initial type/lint failures also included: ```text Property 'name' is missing in type '{ readonly id: "role_builtin_owner"; readonly key: "owner"; }' but required in type '{ id: string; key: "owner" | "administrator" | "member" | "viewer" | null; name: string; }'. error `import()` type annotations are forbidden @typescript-eslint/consistent-type-imports ``` These were corrected with complete authorization fixtures and normal type imports. The grant-read policy's `auditAllowed: false` spread required narrowing `requireTeam`'s return type to its actual `active-user` variant; its runtime behavior was not changed by that type correction. ### Verified Facts And Drift - No schema migration: the existing D1 migration `0083` and DO migration `56` provide the needed fields. `listForTeam` retains its numeric-ID projection; null repository IDs require an installation grant. - The global repository cache remains `repos:list:v3`. Scope filtering applies to both cached and freshly fetched results, never to the cached installation catalog itself. - The supplied session-create pointers had moved to `session-create.ts:104–108` and `185–198` on the initial checkout; the existing separate grant check was moved into the common target helper without changing creation ownership or visibility semantics. - The listed grant URLs introduce two distinct paths, not one. The verified catalog is 205 routes and 156 paths, rather than 205/155; policy tests and both snapshots reflect the actual routes. - `enforceTeamRequirement`'s missing-team and nonmember branches returned only a response, so no denial decision reached the audit writer. They now use the existing denial mechanism and the same reason code/body. - Automation/environment stores already carry `owner_team_id`, but their creation APIs still write null ownership. Checks use persisted ownership on updates; no ownership API was pulled forward. - Rebased over the merged quiet-team-read/subscribe-capability and composer/scope-refresh changes in ColeMurray#2174 and ColeMurray#2176. Their implementations remain intact. Conflict resolution preserved the new `requireTeam` options/defaults, both changelog entries, and regenerated the route snapshots from the combined implementation. ### Deliberately Excluded - No installation-token mint/cache changes, provider scope arguments, Modal restore changes, or App-key removal. Sandbox credentials are not narrowed or revoked by this PR; scoped credentials remain a separate change. - No automation/environment ownership APIs or scheduler ownership/execution guards, team secrets, or bot bindings. - No independent composer-page, active-team, scope-purge, allowed-read auditing, or member-email visibility implementation. Merged follow-ups are inherited rather than reimplemented. - Existing workspace-level skill catalog lifecycle and background save-hook admission remain unchanged. Repository prebuild disablement remains a permission-gated cleanup operation that does not resolve or execute an inaccessible repository. ## Browser Verification Verified the real Next.js application with isolated browser API fixtures, without committing fixture routes or bypassing application authentication: - Named grant add/remove, scope locking, removal from the installation selector, and installation-wide grant behavior. - Members retain read-only grant visibility; missing capabilities expose no management controls. - Desktop and mobile layouts, including no horizontal overflow at 390 px. Viewport captures from `http://127.0.0.1:3131/teams/design`: | Capture | Viewport | Uploaded Artifact | | --- | --- | --- | | Named grant management | 1440 × 1000 | `5e87768770faab32555d127ef9030c3b` | | Mobile grant management | 390 × 844 | `b378de863da0bbdb4210eb6e225babb2` | | Read-only member, installation grant | 390 × 844 | `430aae856ae5bd1e9b597fb6cd56120d` | Browser verification covers rendering/interactions with fixture-backed APIs, not live OAuth or GitHub credentials. Backend authorization and storage are separately exercised by the real-D1 integration suite. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/cf9a05bc1aa439a722201fa0456eb202)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Team leads and workspace Owners or Administrators can grant a team access to all installation repositories or selected repositories, and remove grants. Team members can view grants. * Team-scoped repository and environment catalogs show only resources covered by the team’s grants. * Repository access checks apply to sessions, automations, environments, skills, secrets, and image builds. Team-owned resources also require appropriate team membership or leadership. * Session target selection displays grant and selection errors and lets you choose repositories again when a selection becomes unavailable. * Audit logs display repository grant additions and removals. * **Bug Fixes** * Secret imports verify the repository’s current identity and access before importing. <!-- 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#2180) ## Summary GitHub sandbox credentials now reach only the session's own repositories, for workspace-owned and team-owned sessions alike. This replaces the earlier team-grant-wide policy in this PR. - Resolve primary/member IDs from persisted D1 session membership. Environment sessions retain their copied members; environment provenance does not expand existing sessions or primary-only children. - For team sessions, retain only those candidates covered by one current `TeamRepositoryGrantStore.listForTeam` snapshot. Installation grants retain session members, never installation-wide sandbox access. - Resolve NULL IDs by structured owner/name from the identity-checked cached installation catalog. Empty, unresolved, invalid or over-500 unique final scopes fail closed before cache lookup or POST. No truncation or `all` fallback. - Repository builds mint for that repository alone, even without team grants. Environment builds use planned members intersected with the environment owner's grants and refuse changed membership during planning. - Preserve the exact scope union and ID-set v2 cache keys, 128-scope LRU cache, expiry rules, and per-key single-flight 401 recovery. - Metadata/catalog calls, access checks, branch lists, skills import and the GitHub bot's collaborator-metadata mint remain installation-wide. GitLab retains its deployment-wide PAT limitation. - Update `CHANGELOG.md` and `docs/GETTING_STARTED.md`; remove the obsolete grant-wide resolver. Closes [COL-205](https://linear.app/colemurray/issue/COL-205). ## Operator Impact Private submodules, repository-backed private dependencies and sibling-clone setup scripts now fail unless those repositories are included in the session's environment. Team sessions additionally need grants for those members. Editing an environment does not expand an existing session's snapshot. Legacy NULL IDs require an identity-matching cached repository catalog; loading the repository list populates it. Grant removal changes the next credential scope but does not revoke already-issued credentials, which remain valid until expiry. ## Checkpoint Report ### Validation | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed; also rebuilt by every root typecheck. | | `npm run typecheck` | Final post-merge run passed in every workspace, including control-plane production, Node, unit and integration projects. | | `npm run lint:fix` | Final run and commit hooks passed. Initial two type-import failures are reproduced below. | | `npm run lint:sql-portability` | Passed: 24 baselined occurrences across 4 files, unchanged. | | Scope factory/mint boundary TDD | Initial red: 11 failed, 10 passed. Green after implementation: 21 passed. | | Focused new-scope unit run | 11 files, 190 tests passed. Final cleanup run: 7 files, 132 passed. | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1` | Final post-merge run: 349 files, 5,873 tests passed. Pre-merge redesign run: 5,834 passed. | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1` | Redesign full suite passed before the final concurrent-main merge: 128 files, 1,658 passed, 1 skipped; 592.00 seconds with a 1,800,000 ms timeout. | | Post-merge focused workerd run | 8 files, 111 passed, covering scoped tokens, SCM credentials, PR creation, environment snapshots, image reconciliation, session access, WebSockets and team-member privacy. | | Prettier, `git diff --check`, commit hooks | Passed. | Post-merge integration command: ```bash npm run test:integration -w @open-inspect/control-plane -- test/integration/scoped-installation-token.test.ts test/integration/scm-credentials.test.ts test/integration/create-pr.test.ts test/integration/session-from-environment.test.ts test/integration/image-build-scheduler.test.ts test/integration/session-access-routes.test.ts test/integration/websocket-session-access.test.ts test/integration/team-member-privacy.test.ts --maxWorkers=1 ``` No live credentials were used. Real D1/KV tests inspect mocked mint bodies for session isolation, grant removal, installation-grant narrowing, NULL-member/scalar-fallback resolution and repository-only builds. <details> <summary>Development failures, verbatim excerpts</summary> Initial boundary red: ```text TypeError: repositoryCredentialScope is not a function Test Files 2 failed (2) Tests 11 failed | 10 passed (21) ``` The first focused workerd run failed despite passing assertions because the immediately rejected nested scope promise was reported unhandled. Explicitly awaiting scope resolution fixed it; the subsequent scoped-file and full runs passed cleanly. ```text SourceControlProviderError: Cannot generate credentials: no repositories in scope ❯ repositoryCredentialScope src/source-control/credential-scope.ts:16:11 ❯ resolveRepositoryCredentialScope src/source-control/repository-scope.ts:45:10 ❯ resolveSessionCredentialScope src/source-control/session-scope.ts:23:10 ❯ test/integration/scoped-installation-token.test.ts:246:5 Test Files 7 passed (7) Tests 109 passed (109) Errors 1 error ``` Initial lint: ```text /workspace/background-agents/packages/control-plane/src/image-builds/planner.test.ts 43:35 error `import()` type annotations are forbidden @typescript-eslint/consistent-type-imports /workspace/background-agents/packages/control-plane/src/image-builds/scheduler.test.ts 19:35 error `import()` type annotations are forbidden @typescript-eslint/consistent-type-imports 2 problems (2 errors, 0 warnings) ``` </details> ### Main Verification and Drift - Integrated `main` first at `ef4f737`, then at `eb50181` after it advanced during validation, and most recently at `f1cf069` (`d4306fe`), which brought in ColeMurray#2178. Both changelog sides and the newer authorization/directory behavior are retained; the published branch was not rewritten. - The newer main removed ownership-move APIs and their repository reader. This PR does not restore those mutations: a focused `SessionRepositoryStore` reads ordered membership, and the credential resolver uses the already-loaded scalar identity only as the legacy fallback. - D1 has no scalar `repo_id`; member rows carry IDs. Environment members are copied on session creation, and `environment_id` is provenance rather than credential authority. - The shared reader preserves the existing `repos:list:v3` cache key, payload schema and SCM fingerprint. Refreshes, writes, freshness, metadata enrichment and metadata routing are unchanged. - The 500-ID limit applies to the final filtered, de-duplicated scope, not total team grants. Repository builds no longer enumerate teams; each nonempty team credential resolution reads grants once and filters candidates in memory. - No schema or grant-write change is added. Credential narrowing remains independent of `TEAMS_ENFORCEMENT`; the merged access-policy changes are retained. Schema assumptions remain D1 0083 and DO 56. ### Deliberately Left Out - Modal restore-token PR ColeMurray#2178 has since merged (`f1cf069`). Restored sandboxes now fetch credentials through the control-plane broker (`ScmCredentialsService`), so they receive the same session-scoped tokens as fresh sessions without further changes here. - Grant APIs/UI and writes remain separate; integration tests seed grants directly in D1. - Image-build secret folding is untouched. - The owner-run GitHub live gate is still pending. Immediate revocation and cross-isolate cache deletion/write fencing are not introduced. All earlier prebuild, whole-team size-limit and all-team fan-out review threads have been answered and resolved under the repository-only policy. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * GitHub sandbox credentials are limited to repositories included in each session; team-owned sessions are further restricted to repositories with current team grants. * Pull request operations and repository or environment image builds use credentials scoped to the relevant repositories. * Sessions with empty, unresolved, or oversized repository scopes do not receive credentials, and access is not broadened as a fallback. * Updated guidance explains repository access, installation-wide metadata and catalog operations, GitLab credentials, and legacy sessions. * **Bug Fixes** * GitHub provider requests retry once with refreshed credentials after an authorization failure. <!-- end of auto-generated comment: release notes by coderabbit.ai --> ## Grant Snapshot Review Follow-Up `b21e81e` replaces repeated coverage queries with one fresh `listForTeam` snapshot per nonempty team credential resolution. Installation grants and fail-closed empty/oversized scopes retain their existing behavior; grants are not cached across resolutions. A regression first reproduced mixed-snapshot selection (expected repository 12, received 30); it now passes and asserts exactly one grant read. Local validation passed: 6 focused unit files / 118 tests; 3 D1/KV integration files / 26 tests; all workspace typechecks; changed-file ESLint and Prettier; diff checks and commit hooks. ## Code Review Follow-Up Net change against `b21e81e` (`7988327` + `7b2e28c`), with no behavior change to credential scoping: - `coveredRepositoryIds` in `db/team-repository-grants.ts` is now the only definition of grant coverage. `TeamRepositoryGrantStore.covers` and `resolveRepositoryCredentialScope` both use it, so credential narrowing can't drift from the grant check. - `getInstallationTokenCacheKey` and `reposCacheIdentity` use the shared `sha256Hex` helper instead of hand-rolled hex encoding. - A memory token cache hit now only refreshes the entry's LRU position. Expired entries are still pruned when a token is inserted. `7988327` added a fallback that refreshed the repo catalog for NULL repository IDs. `7b2e28c` reverted it: only sessions created before migration 0032 (2026-07-07) lack IDs, and those are outside the 30-day support window. The fail-closed behavior described under Operator Impact still applies. Other review findings were left unchanged because the existing tests or this description already make them deliberate: - The planner skips the clone token instead of rejecting the build. - Members without a team grant are dropped from team scopes. - GitLab still resolves a credential scope even though its PAT ignores it. - PR refresh resolves the scope for each artifact. - Membership is not read for a missing session. Validation: control-plane typecheck (all four configs), ESLint and Prettier; full unit suite on the final code: 349 files, 5,874 tests passed; 6 related workerd integration files / 96 tests passed. ### Merge with `main` at `f1cf069` (`d4306fe`) - `image-builds/planner.ts`: kept the scoped `generateCredentialHelperAuth(tokenScope)` call and adopted ColeMurray#2178's `{ type: "credential_helper", token }` clone-auth shape; the Modal provider now supplies host and username. Updated the matching planner test. - `CHANGELOG.md`: kept both entries and dropped "Modal restore-token scoping remains a separate change", since restores now use the scoped broker. - No new unscoped credential mints came in from `main`; the only `{ kind: "all" }` callers are the skills import paths, which are installation-wide by design. - Validation after the merge: control-plane typecheck (all four configs) passed. The full unit suite (350 files, 5,906 tests) passed except for the one planner test asserting the pre-ColeMurray#2178 clone-auth shape; it passes after the update, and the image-build unit tests pass (321 tests). 6 related workerd integration files / 96 tests passed. CI runs the workspace-wide typecheck. ### Test trim (`f67a506`) Reduced the PR's test additions from about 4,300 to about 2,700 lines with no coverage loss. Line and branch coverage is unchanged for all 22 production files the PR touches, and `session-scope.ts` branch coverage goes from 75% to 100%. - Composition-layer tests (`components.credentials`, `autofix/handler`, planner, scheduler) re-ran resolver scenarios through prototype spies: team A/B grants, revoked grants, NULL-id catalog lookups, empty and over-500 scopes. They now only check that the resolved scope is passed through and that failures fail closed. The resolver logic stays covered by `repository-scope`/`session-scope`/`credential-scope` tests and the real-D1 integration test. - Removed tests that only replayed their own mock sequences or asserted incidental call counts and order. Examples: "re-reads on every call", "resolves scope again on the next delivery", and `toHaveBeenCalledBefore` checks. - Removed scope assertions that had been copied onto every GitHub provider method. One focused per-call scope test and the 401-retry test remain. - Folded `github-app.scope.test.ts` into `github-app.cache.test.ts`. The LRU and memory-cache tests now actually prove the entries come from memory and are evicted in least-recently-used order. Added a direct test of the `repository_ids` mint body. - Integration: removed cases that repeated unit-level cache mechanics (cold KV read, expiry limits, v1 keys, concurrent dedupe). Kept isolation, grant narrowing, installation-grant narrowing, NULL/scalar resolution, repository-only builds, fail-closed, and 401 recovery. Validation: control-plane typecheck and ESLint are clean. The full unit suite passed (349 files, 5,803 tests), and the 6 related workerd integration files passed (88 tests). ### Test file restore (`c814e23`) Undid the split of `pull-request-service.test.ts` into `pull-request-service.test-support.ts` and `pull-request-service.credentials.test.ts`. The helpers and the three moved tests are back in their original positions. That file's diff against `main` is now only the credential-scope additions (+31 lines): the harness scope, the push-auth scope assertions, and the fail-closed test. The session unit suite passes (90 files, 1,627 tests). --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…2199) ## Summary Reorganizes the session inspector sidebar: - **Info is now the first tab** (order: Info, Changes, Tasks, Tools) and is the default tab for viewers with no stored tab choice. Viewers who already picked a tab still reopen on it. - **Captured media moved from Changes into Info**, renamed from "Media (n)" to **"Artifacts (n)"**. It uses the existing `CollapsibleSection` (expanded by default, toggle exposes `aria-expanded`). It sits right after Run information / Repository. - The Changes tab now only shows file changes. Opening a diff still switches the inspector to Changes via `showTab("changes")`, so the diff flow is unchanged. ## Notes for review - Making Info the **default** tab, not just the first in order, is my reading of "move info to be the first tab". If Changes should stay the default, revert the one-line change to `DEFAULT_SESSION_INSPECTOR_TAB`. - The "Media (n)" pill in the desktop action bar and mobile actions menu is unchanged. It's outside the sidebar. ## Testing - `npm test -w @open-inspect/web`: 263 files / 2672 tests pass - Web `tsc --noEmit` is clean; ESLint + Prettier pass - Updated tests: tab order and keyboard navigation with Info first, the Info default in the hook, the Changes-panel tests now open Changes first, and a new test that checks Artifacts renders in Info and collapses/expands. ## Visual verification Checked in a local dev server using a temporary, uncommitted preview page that renders `SessionRightSidebar` with fixture data. Viewport was 1512×982. - Info first and selected, with Artifacts (2) expanded: screenshot artifact `c13d29d7a5b7b08722d457e9e131887f` - Artifacts collapsed: screenshot artifact `040c3fcfbf1ea80f6805990012796179` - Collapse → expand → switch to Changes (no media there) → back to Info: video artifact `ffd194fe2a181537433d85240f7b8a30` The media thumbnails show as blank placeholders because the preview had no media backend. The Changes tab showed "Unable to load changes." because the fixture had no diff. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/eee2433c00c3516de6af9e1b0550cba3)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Updates** * The session inspector now opens to the Info tab by default, with Info appearing before Changes in the tab order. * Captured media appears in an expandable Artifacts section in Info instead of the Changes panel. * The Changes panel focuses on checkout changes. * Opening media from the mobile session header shows the Info tab without changing the remembered inspector tab. <!-- 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-246, increment 6 of the COL-240 lifecycle-manager refactor. - Extract `failConnectTimeout`, `terminateStaleHeartbeat`, `snapshotAndStopStaleSandbox`, and `stopForInactivity` into stateless `watchdog-effects.ts` functions with per-function dependency subsets. - Keep the complete boot-budget effect, alarm context capture, policy dispatch, healthy/warning scheduling, failure accounting/reporting, public policies, and the single termination flag in `SandboxLifecycleManager`. - Add assembled-manager ordering/interleaving coverage, extend existing ESLint consumer boundaries, and update the repository ownership design. ## Ownership and Ordering The effects receive narrowed storage/socket/broadcast/shutdown/access ports and named checkpoint, failure, and generation-targeted stop operations. They have no manager reference, flag setter, scheduler, timer, or separate lifecycle state. For non-held boot budget, shutdown send, generation fence, failed state/breaker/access retirement, status/error publication and persistence, and socket detachment all precede guard acquisition. Only explicit provider stop runs inside the manager's `failBootBudget` try/finally. Fatal/unresponsive termination remains in the manager; other watchdogs intentionally do not acquire this guard. Existing retained-source holds, unfenced no-stop late-bridge self-heal, half-boot versus ready-workspace preservation, provider-managed preserve-stop, detached heartbeat capture, shutdown-first inactivity, post-await abandonment checks, log/user messages, and `SandboxAlarmResult` meanings remain unchanged. The shared alarm scheduler and session-handler ordering are untouched. This extraction does not add broader generation hardening or fix the separately documented baseline safety gaps. ## Coverage Retains all existing assembled-manager and Workerd suites. Strengthens the exact boot-budget failure/access/publication/persistence trace, actual spawn exclusion during stop and successful spawn after failed stop, retained-source no-send/no-detach and duplicate-accounting behavior. Continuation tests use fresh row snapshots and prove that non-identity field changes do not abandon the same generation. Adds deferred stop coverage for independent ID/timestamp replacements with legacy/resumable heartbeat and inactivity paths, checkpoint uncertainty blocking competing teardown, and inactivity waiting for held/owned shutdown before any legacy effects. ## Related Work Starting checkout: `60930ce`, with COL-245 integrated via ColeMurray#2170 after launch/access/VM extraction. COL-238 remains In Progress and ColeMurray#2141 is open/unmerged; its proposed heartbeat confirmation is not imported. Existing alarm policy and boot-phase helpers are reused unchanged. ## Verification Node `v24.20.0`, npm `11.19.0`; dependencies were already installed. Checks ran sequentially, with one Vitest worker to respect sandbox resources. | Command | Final Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm test -w @open-inspect/control-plane -- src/sandbox/lifecycle src/session/alarm src/session/sandbox-shutdown --maxWorkers=1` | Passed: 26 files, 785 tests | | `npm run test:integration -w @open-inspect/control-plane -- session-lifecycle-alarm-recovery sandbox-early-connect sandbox-shutdown sandbox-state-retention --maxWorkers=1` | Passed: 4 files, 57 tests | | `npm run typecheck -w @open-inspect/control-plane` | Passed: Worker, Node, unit, integration configurations | | `npm run lint -w @open-inspect/control-plane` | Passed | | `npm run test:lint-sandbox-boundaries` | Passed: 2 tests | | `git diff --check` | Passed; committed base diff also passed | Touched-file formatting passed: ```bash npx prettier --check docs/plans/sandbox-lifecycle-manager-refactor.md eslint.config.js scripts/lint-sandbox-boundaries.test.mjs packages/control-plane/src/sandbox/lifecycle/watchdog-effects.ts packages/control-plane/src/sandbox/lifecycle/manager.ts packages/control-plane/src/sandbox/lifecycle/alarm-boot-budget-effects.test.ts packages/control-plane/src/sandbox/lifecycle/alarm-effects.test.ts packages/control-plane/src/sandbox/lifecycle/alarm-inactivity-effects.test.ts packages/control-plane/src/sandbox/lifecycle/manager-shutdown.test.ts ``` An initial targeted run failed two new assertions because its fixture stubs startup decisions as unmanaged despite real checkpoint ownership. The new tests were narrowed to competing teardown; existing real-coordinator tests retain startup-hold coverage. The corrected targeted run passed 79 tests. Initial typecheck found an overly narrow mock return type, corrected to the existing `StopResult`; the focused unit suite and typecheck were rerun successfully. No remaining verification blockers or production behavior fixes are claimed. Full-package/full-story tests and bundle builds remain COL-247's scope. Workerd uses provider substitutes; no deployment or live-provider verification was performed. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/d1b90cca7d20fef75c0f94573635dada)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved sandbox lifecycle reliability during connection-timeout, stale-heartbeat, and inactivity handling. If a sandbox is replaced while shutdown is underway, the replacement remains unaffected, and shutdown already owned by another operation is not repeated. * Improved handling of uncertain checkpoint outcomes so competing alarm, failure-reporting, and teardown actions do not proceed. * **Refactor** * Updated internal lifecycle handling while preserving existing timeout, shutdown, and recovery behavior. <!-- 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 Addresses COL-234 with documentation-only changes. - Correct the build-allocation recovery claim: a lost create response marks the build failed, and its VM remains until the provider timeout (40 minutes by default, up to 70 minutes including finalization grace). A re-triggered build uses a new build ID and allocation name. - Add `modal-vm` to the pre-built image provider list and explain that it builds Modal images on the VM backend, separately from `modal` images. - Add a linked `modal-vm` entry to the `SANDBOX_PROVIDER` deployment-settings row, preserving Prettier's table alignment. - Require a web redeployment after changing `sandbox_provider` with Terraform. For Vercel, operators must manually run **Deploy Web** after the apply so the build-time provider value and Pre-Built Images filtering are updated. This applies to every provider switch. ## Verification - Checked the documentation against the current build workflow, Modal client and build lifecycle, provider registry, Terraform configuration, Deploy Web workflow, and image-builds API filtering. - `npx prettier --check docs/MODAL_DOCKER.md docs/IMAGE_PREBUILD.md docs/GETTING_STARTED.md` passes. - `git diff --check origin/main...HEAD` passes. - Only `docs/MODAL_DOCKER.md`, `docs/IMAGE_PREBUILD.md`, and `docs/GETTING_STARTED.md` changed. No application code changes. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/9f7eb3c5e11650155e8b1e71abd13700)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added `modal-vm` as a supported sandbox provider in the configuration and getting-started guides. * Clarified that `modal-vm` sessions use images built on the VM backend, not images built under `modal`. * Updated build-retry guidance: after a lost create response, the VM may remain running until its provider timeout; retries use a new build and allocation. * Clarified provider-switch deployment steps for Cloudflare and Vercel, including that Vercel’s image-build list may show the previous provider until a new production deployment is live. <!-- 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#2204) ## Summary The team page had five dropdowns rendered as native `<select>` elements, so they used the browser's default dropdown instead of our design system. They now use the Radix-based `@/components/ui/select` component, which other team controls like the "Add member" picker already use. | Component | Dropdown | | --- | --- | | `settings/team-detail.tsx` | Join policy, Default visibility | | `settings/team-members-table.tsx` | Member role (compact density to fit the table row) | | `teams/team-repositories.tsx` | Grant scope, Repository | The behavior stays the same: - Labels are still linked to the triggers through `id`/`htmlFor` or `aria-label`, so the accessible names don't change. - Disabled states and the per-option disabling for grant scope are preserved. - The repository dropdown uses `SelectValue`'s placeholder instead of an empty `<option>`. ## Tests - Changed tests that used `fireEvent.change` on native selects to open the dropdown and pick an option with `userEvent`. - When grants exist, the grant scope dropdown is disabled, so its options can't be opened. Those tests now check that the trigger is disabled and shows the right value, instead of checking options in a hidden native list. - Added a test that picks a join policy and a default visibility from the dropdowns and checks the PATCH payload. `npm test -w @open-inspect/web` (263 files, 2674 tests), `tsc --noEmit`, and eslint all pass. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/c9e4de358d8ac55b88cf4d0b2af19633)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **UI Improvements** * Updated team settings, member-role controls, and repository grant selectors to use consistent dropdowns. * Existing options, permissions, saving states, and grant-scope restrictions are preserved. * Repository selection continues to show available repositories and disables controls during loading or errors. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…urray#2203) ## Summary Closes the local audit/verification work for [COL-247](https://linear.app/colemurray/issue/COL-247), final increment of [COL-240](https://linear.app/colemurray/issue/COL-240). All six prerequisite implementations were verified in the actual checkout, not inferred from issue status. Parent design is updated; review/merge and normal release remain required. Audited combined changes from research baseline `eef911f36e704fc49104546a9cd52b584d8b9223` through starting `b98a378bdbe2dba1237953da9162e8a85a7e926a`. Tested the source/test contents committed as `3b9e7a0` using Node `v24.20.0`, installed dependencies, and sequential checks from repository root. ## Files and Evidence - `eslint.config.js` and `scripts/lint-sandbox-boundaries.test.mjs`: extend existing import enforcement to launch-context/startup-errors, including extension-bearing imports. No custom scanner or rule weakening. - `src/session/components.ts`: explicitly justify the composition-only launch integration-port import; runtime wiring unchanged. - `test/integration/sandbox-vm-reconciliation.test.ts`: three production-composed Workerd regressions with real SQL/encryption, foreground pending reservation and reconstructed runtime. Independent expected-reference/timestamp changes during encryption refuse commit without mutation/adoption/publication. Success preserves early readiness, conservative lifetime, encrypted access and distinct admission/acknowledgement/foreground gates, without reconstructing terminal signing authority. - `test/integration/sandbox-state-retention.test.ts`: run reconstructed rejected cleanup through production scheduled delivery, both shutdown-priority passes, retry-before-I/O, delivery acknowledgement and duplicate wake-ups under a durable hold. Assert observations outside intentionally caught provider errors. - `src/sandbox/lifecycle/alarm-effects.test.ts`: two controlled assembled reproducers explicitly characterize inherited unsafe boundaries rather than quietly fixing them. - `docs/plans/sandbox-lifecycle-manager-refactor.md` and new `sandbox-lifecycle-refactor-verification.md`: actual ownership, combined history, validation results, related-work reconciliation, deviations, separate follow-ups and release limits. Control-plane source/test paths above are relative to `packages/control-plane/`. ## Actual Ownership | State / Responsibility | Owner | | --- | --- | | `isSpawningSandbox`, `isTerminatingSandbox`, `providerStartupPending`, lazy logger | Manager | | All five bridge/auth fields | `VmStartupReconciliation` | | Launch inputs/settings/images | Readonly `SandboxLaunchContext` | | Access signing/reuse/artifacts/retirement/notifications | `SandboxAccess`, with no retained signing key | | Rejected cleanup / local bounded stop | Stateless cleanup/provider-stop functions | | Connect/heartbeat/snapshot/inactivity effects | Stateless watchdog functions | | Entire boot-budget effect, termination guard, fatal/unresponsive policy, claims/rejection/breaker/admission/recovery | Manager | | Conditional SQL/encryption | `SandboxRepository` | | Durable checkpoint/shutdown/holds/receipts/continuation/recovery | Shutdown coordinator and repository | | Shared pending/in-flight deadlines | Existing alarm scheduler | One public lifecycle authority and one durable shutdown owner remain. Access precedes shutdown/manager construction; shutdown calls focused access retirement directly, with no manager callback cycle. Constructors do not execute runtime work. No broad manager/context dependency or universal mutex was added. ## Compatibility T7 changes no lifecycle/provider/repository runtime implementation, public consumer return contract, stored/wire shape, schema, backend protocol, settings, timeout or retry. Earlier constructor/internal type changes remain internal composition details. The combined series includes two explicit earlier reviewed fixes: COL-244 suppresses notification after refused deferred acceptance; COL-245 requires an actually dispatched successful stop for retirement confirmation. These are documented exceptions to a purely mechanical extraction claim, not new T7 fixes. Separately landed held-save retry, Modal Docker/tunnel/save/termination, COL-226 restore credentials, model catalog and identity/team secret/grant/scoped-token work are preserved. COL-130/151/155/156/159 remain separate; COL-161/238 remain in progress. Heartbeat PR ColeMurray#2141 is still open/unmerged and was not imported. ## Validation All expensive checks ran sequentially with one Vitest worker. No required command remains blocked. | Exact Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Exit 0 | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1` | Exit 0: 359 files / 6,114 tests | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1` | Terminal timeout at 600 s; not counted as a pass | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1 --reporter=verbose` | Exit 0: 137 files / 1,742 passed / 1 existing skip. Entire final test version rerun successfully in 647.39 s | | `npm run typecheck -w @open-inspect/control-plane` | Exit 0: Worker, Node, unit and integration configs; rerun after final test refinement | | `npm run build -w @open-inspect/control-plane` | Exit 0: Worker and Node bundles; esbuild size warnings only | | `npm run lint -w @open-inspect/control-plane` | Exit 0: no warnings/errors | | `npm run test:lint-sandbox-boundaries` | Exit 0: 2 tests | | `npm run format:check` | Exit 0: entire repository, including committed tree | | `git diff --check` | Exit 0; staged check also passed | Additional ESLint check covers the edited config/scripts/integration tests omitted by `eslint src/`; it passed. Commit hooks ran normally. Focused commands and their exact results are in the repository report, including the initial fixture-only failure caused by retaining a recent predecessor spawn timestamp. Only that test timestamp was corrected. No baseline application suite failure was found. The existing Workerd skip is Cloudflare KV TTL expiry because its conformance fixture has no controllable clock. No lifecycle check was newly skipped. Workerd deliberately exercises eviction failures and provider substitutes; this is not live-provider evidence. ## Separate Follow-ups and Limits The added characterization tests reproduce baseline gaps without authorizing a behavioral redesign: - An unmanaged shutdown-ownership await permits successor access/status retirement; an absent captured handle can retarget the successor using the old timestamp. Separate follow-up: generation/ownership revalidation and explicit-target stop semantics preserving absence. - Old connect-timeout completion can persist/publish failure on a ready successor. Separate follow-up: generation-scoped error persistence/publication. Existing gaps remain documented: unguarded fresh/restore access writes, prior retirement handle clearing, partial access retirement/secret-read/bridge acceptance boundaries, separate pending-record writes, warning scheduling and final attachment status rechecks. Evidence is not exhaustive: combined second-reservation/hash authentication, outstanding old bridge during actual prebuilt retry, full Node lifecycle transport, and every Workerd watchdog-to-queue combination remain limits. Existing assembled/real-storage/Node conformance tests are retained, not replaced with collaborator mocks. ## Release and Rollback No schema migration, feature flag or deployment. No live-provider canary, exactly-once, remote cancellation or guaranteed recovery claim. Normal review/merge/release remains required. Rollback must revert a coherent reviewed increment with dependent increments and independently landed fixes accounted for, not an arbitrary old binary or destructive reset. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/d99e1cddda5aed8e11b2f815a355b483)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Updated lifecycle ownership and verification records with validation results, test coverage, known gaps, evidence limits, and release and rollback considerations. * **Tests** * Expanded coverage for VM startup reconciliation, replacement sandboxes, connection timeouts, and cleanup retries through scheduled deadlines. * **Maintenance** * Added checks restricting imports of internal lifecycle modules. No runtime or user-facing behavior changes are included. <!-- 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 This is part 1 of 2, split out of ColeMurray#2188. The automations half is stacked on this branch. It is rebased onto current `main`, including ColeMurray#2181 (team repository grants) and ColeMurray#2180 (installation token scoping). - **Ownership on create.** Environments can be created with a `teamId`. Creation honors the require-team setting and refuses archived teams, and creating a team environment requires lead authority plus `environments.manage`. Names are unique within each team or workspace scope; the unique-index race is answered with 409. - **Owned-resource admission.** A new `environment` route requirement (`requireEnvironment` / `environmentRequirement`) is evaluated by `authorization/owned-resource-admission.ts`. - Team environments are hidden from non-members with the same 404 as a missing environment. - Visible denials return 403 with `environment_action_denied` and a `reason_code`. - Secrets, settings, and image-trigger routes require the feature permission *and* access to the owning environment. - Workspace-owned environments stay permission-based, including `environments.manage` for custom roles. - **Session targets.** A new `authorizeEnvironmentTarget` (alongside the unchanged `authorizeSessionTarget` permission/grant preflight) requires `use` access and binds the environment to the destination `ownerTeamId`. A team environment can only be used by sessions owned by that team, including child sessions spawned from a parent (checked before settings resolution or child admission). Sandbox clone inheritance tolerates dangling environment provenance. - **Lists and capabilities.** `GET /environments` returns only readable rows, with `ownerTeamId` and `capabilities` (`canRead`/`canManage`/`canUse`). A new `?ownerTeamId=<team|null>` filter selects exact ownership. The mixed image-status feed omits unreadable environment scopes. - **Web.** - The environment form gets a team selector at creation, and ownership is read-only when editing. - Team pages get an Environments tab. - Row actions are gated on server capabilities. - `useEnvironments` now takes `{ teamId, ownerTeamId }`. - The BFF strips ownership fields from PUT bodies. ## Decisions made while rebasing onto `main` (differences from ColeMurray#2188) - **`?teamId=` keeps `main`'s meaning.** `main` already used it as the team session catalog (repositories fully granted to the team), so ColeMurray#2188's ownership filter moved to `?ownerTeamId=`. The team catalog now also excludes environments owned by *other* teams, since launching with them is rejected with `environment_team_mismatch`. - **Grant checks use `main`'s helpers.** ColeMurray#2188's duplicate checks (`validateTeamRepositories` and the separate secret-import `covers` check) were dropped in favor of `authorizeSessionTarget` and `authorizeTeamRepositories`. Grant errors therefore use `main`'s body, `target_team_missing_grant` without `reason_code`. - **Scalar edits follow `main`.** They don't re-check grants unless prebuilds stay enabled on a team environment, so disabling prebuilds on an environment with revoked grants still works. - **Smaller cleanups.** - The environment image trigger reuses the admitted environment; its old `not_member` branch could no longer be reached. - The list handler reuses the request's membership snapshot. - **Review follow-ups (consolidation).** - Environment access rules live entirely in shared `checkEnvironmentAccess`. Workspace `manage` is encoded in `ENVIRONMENT_RULES`, and unbound services see only workspace environments, so the control plane has no extra exceptions. - `evaluateEnvironmentAdmission` returns the admitted environment instead of writing `ctx`. Only route admission stores `ctx.environmentAdmission`, and handlers read it through `admittedEnvironment(ctx)`, which throws if the route didn't admit one. - New `routes/team-ownership.ts` provides `resolveCreationOwnerTeam` (used by environment and session create) and `admitTeamCatalog` (used by the `?teamId=` catalogs of `/repos` and `/environments`). This removes copies that `main` already had. Session create's `team_archived` response now also includes `reason_code`. - Environment create and update share `resolveAuthorizedRepositories`. The prebuild grant re-check is `authorizeStoredTeamRepositories`. Store parameters are named `ownerTeamId`, and `getByName` requires an explicit scope. - Shared `teamIdSchema` replaces the duplicated regex. - Spawn-child runs only the environment permission check and the ownership check early. The repository preflight is back in `main`'s position, so the parent repository comes from one source. - Web: `environmentAccess()` combines server capabilities with feature grants, and the edit view moved to `EnvironmentDetail`. The API proxy's PUT allowlist is derived from `updateEnvironmentInputSchema`. - Left as is, deliberately: `capabilities` stays optional, because the bots re-validate their KV-cached environment lists with this schema. `?teamId=` and the create body's `teamId` keep their wire names, matching `main` and session create. - **Behavior change.** Changing a *workspace* environment's secrets, integration settings, or image builds now also requires `environments.manage`, in addition to `environments.secrets.manage` / `environments.settings.manage` / `environments.images.manage`. Built-in roles are unaffected; custom roles holding only the feature permission lose those actions. - **Behavior change.** Actorless bots (e.g. github-bot's `GET /environments/:id`) now get the same 404 for team-owned environments as for missing ones, matching the list, which already hid them. - **Interim guard until the automations PR lands.** Automations are still workspace-owned on this branch, so automation environment selection treats team-owned environments as missing. Without this, a workspace automation could launch with a team's environment. The automations PR replaces it with full team validation. - `useResourceTeams` is included whole, `automation` mode too, because both PRs share it. - Test updates: `main`'s ColeMurray#2181 unit tests needed fixture updates for the new admission (e.g. callers now lead the owning team, and `listForUser` is loaded once by admission). Every case keeps its original intent. A team member who isn't lead now gets `not_owner_or_lead` before source-grant checks. No migration: migration `0083` already has `owner_team_id` and the per-team name index. ## Validation | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | passed | | `npm run typecheck` | passed | | `npm run lint` | passed | | `npx prettier --check` (changed files) | passed | | `npm test -w @open-inspect/shared -- --maxWorkers=1` | 1,093 passed | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1` | 6,125 passed | | `npm test -w @open-inspect/web -- --maxWorkers=1` | 2,699 passed | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1` | 1,779 passed, 1 skipped | The route-catalog snapshot was regenerated. Only the 11 environment routes changed. Supersedes the environments half of ColeMurray#2188. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/5f8068042d7f4f3642a2c58687d5bd18)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Teams have an Environments tab for viewing and managing team-owned environments. * Environments can be assigned to a team when created. Names are unique within each ownership scope, and workspace settings can require new environments to belong to a team. * Environment lists reflect the selected scope and show available read, manage, and use access. * **Bug Fixes** * Access to environment settings, secrets, and image actions now respects environment permissions and team membership. * Sessions and child sessions cannot use environments owned by a different team. * Workspace automations no longer accept team-owned environments. * Environment updates forward only supported configuration fields. <!-- 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 This is part 2 of 2, split out of ColeMurray#2188, and it is **stacked on ColeMurray#2205** (team-owned environments). Review and merge ColeMurray#2205 first; this PR's diff covers only the automation work. The tree of this branch is exactly ColeMurray#2188 rebased onto current `main`, apart from the CHANGELOG wording. - **Ownership on create.** `POST /automations` accepts a `teamId`. It honors the require-team setting and refuses archived teams with `team_archived`. The canonical executor must be a team member. - **Admission.** Automation routes move onto the shared owned-resource admission from ColeMurray#2205, extended with a `read` operation (`requireAutomation("read")`, with actorless slack-bot read). - Team automations are hidden from non-members with the same 404 as a missing automation. - Visible denials return 403 with `automation_action_denied` and a `reason_code`. - Management and trigger grants stay independent of read grants. - The executor-only check in `route-admission.ts` is replaced by the shared evaluator. - **Discovery.** `GET /automations` is filtered to what the viewer can see, accepts `?teamId=` for team scope, and returns `ownerTeamId` and `capabilities` (`canRead`/`canManage`/`canTrigger`). - **Targets.** Selected environments must be visible and belong to the automation's team (`environment_team_mismatch`). `use` is required only for replaced environments. - Edits revalidate team repository grants across all final targets (stored repositories and every environment's repositories) through `main`'s `authorizeTeamRepositories`. - This replaces ColeMurray#2188's separate grant helper, so denials use `main`'s `target_team_missing_grant` body. - **Executor reassignment.** New `PATCH /automations/:id` lets a lead or admin reassign the executor, including after the current executor has left the team. It checks that the candidate is active, a member of the team, and able to launch. The update is atomic and writes an `automation.executor_changed` audit row. - **Execution.** - The scheduler and authorization guard require the executor (and manual requesters) to stay team members, the team to be unarchived, and repository grants to still be current. - Run sessions inherit the automation's team and its default visibility. - Slack follow-ups apply the persisted session's collaboration decision. - Run history hides linked-session details from viewers without access. - **Web.** - Automation forms get a team selector at creation, and ownership is read-only when editing. - Team pages get an Automations tab. - Target catalogs are scoped to the team. - The browser-side ownership rule mirror (`lib/automation-authorization.ts`) is deleted; server capabilities now drive the controls. - Navigation identifiers are encoded. - New `automation-collection` and `automation-cache` helpers. The route catalog grows by one route (`PATCH /automations/:id`): 207 routes across 156 paths. No migration. ## Changes from ColeMurray#2188 during rebase - The interim guard from ColeMurray#2205 (team-owned environments treated as missing for workspace automations) is replaced here by full team validation in `resolveEnvironmentSelection`. - Automation unit tests from `main`'s ColeMurray#2181 (`automation-update.test.ts`) needed fixtures for environment ownership and viewer membership. No expectations changed. - Integration tests now expect `main`'s grant-denial body. One ColeMurray#2181 test seeds team-owned environments, because they must now match the automation's team. ## Validation | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | passed | | `npm run typecheck` | passed | | `npm run lint` | passed | | `npx prettier --check` (changed files) | passed | | `npm test -w @open-inspect/shared -- --maxWorkers=1` | 1,097 passed | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1` | 6,160 passed | | `npm test -w @open-inspect/web -- --maxWorkers=1` | 2,749 passed | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1` | 1,863 passed, 1 skipped | Supersedes the automations half of ColeMurray#2188. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/5f8068042d7f4f3642a2c58687d5bd18)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Automations can be owned by a team and managed from a team’s Automations tab, with team-scoped browsing and creation. * Team leads can reassign automation executors; changes are recorded in the audit log. * Automation controls reflect your access to each automation, and team-owned automations use the team’s default session visibility. * **Bug Fixes** * Automation runs now hide linked session details when you don’t have access to those sessions. * Automation execution and Slack follow-ups check current team membership, permissions, and repository access. * Environment selection respects team ownership and access, while allowing inherited environments that are no longer available. <!-- 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#2192) ## Problem `docs/AUTH.md:145-152` says that on a team-owned session, every non-read action requires current membership in the owning team, in `off`, `shadow`, and `on` modes. The rule names prompting and lifecycle operations. Despite this, a parent session's sandbox can prompt or cancel a team-owned child for an active prompt author who is not in the owning team. That happens when an author is removed from the team while their prompt is still processing, because membership is read at check time. Sandbox tokens skip the route-level session requirements. `routing/route-admission.ts:774-780` admits a sandbox principal on sandbox-fallback routes such as `…/children/:childId/cancel`, and `…/children/:childId/prompt` is declared with `NO_AUTHORIZATION` (`routes/session-children.ts:434`). That leaves the handler checks as the only gate: - `sandboxChildAccess` (`routes/session-children.ts:55-59`) returns `true` for any same-team child that is `workspace`-visible, or `team`-visible under a `team` parent. It does this for every action and never looks at the active prompt author. `handleCancelChild` relies on it for `lifecycle` on the child (`:326`) and on each nested descendant (`:351`). - `handlePromptChild` only calls `sandboxChildAccess` when the child is `private` (`:213-217`). Every other child receives the follow-up prompt with `author: author.data` (`:247`) and no access check. That includes a `team` child under a `workspace` parent, which `sandboxChildAccess` would otherwise send to the author check. Neither handler reads `TEAMS_ENFORCEMENT`, so the result is the same in every mode. Observed on main (`f1cf0697`) with a team-owned, team-visible parent and child, where the active prompt author is a workspace `member` but not in the owning team: | `TEAMS_ENFORCEMENT` | Same user cancelling through the user route | Parent sandbox `POST …/cancel` | Parent sandbox `POST …/prompt` | | --- | --- | --- | --- | | `off` | 403 | 200, child status `cancelled` | 200, message stored in the child | | `shadow` (default) | 403 | 200, child status `cancelled` | 200, message stored in the child | | `on` | 404 | 200, child status `cancelled` | 200, message stored in the child | ## Fix - `sandboxChildAccess`: the visibility shortcut now applies only to reads and to teamless children. Non-read actions on a team-owned child go through the existing active-author check (`checkSessionAccess`), which private children already use. - `handlePromptChild`: runs `sandboxChildAccess` for every child, as `handleCancelChild` already does. Parent-sandbox list and detail reads, teamless children, and private children behave as before. Both changes are needed. With only the `sandboxChildAccess` change, cancel is fixed but the prompt route still delivers the prompt (see the mutation results below). ## Verification - New integration test in `test/integration/child-session-ops.test.ts`, run for both `prompt` and `cancel`: "requires the parent prompt author's current team membership to … a team child". The parent sandbox gets 404 while the author is outside the owning team, and the child stays `active` with no messages. After the author is added to the team, the same request returns 200. - Before the fix (main `f1cf0697`): both cases fail with `expected 200 to be 404`. - After the fix: both pass. - Mutation: reverting only the `sandboxChildAccess` change fails both cases. Reverting only the `handlePromptChild` change fails `prompt` and passes `cancel`. - A throwaway (uncommitted) probe ran the same scenario through `routeRequest` with `TEAMS_ENFORCEMENT` set to `off`, `shadow`, and `on`. Before the fix it produced the table above. After the fix, the sandbox got 404 for both actions in all three modes with the child untouched, and 200 once the author was a member. - `session-children.test.ts`: the `handlePromptChild` unit tests now give the handler a parent-bound sandbox principal and separate teamless child and parent rows (`ownerTeamId: null`, `visibility: "workspace"`). The handler now runs the access check for every child, so it needs both. - Commands, run in `packages/control-plane` on the PR head: - `npx vitest run src/routes/session-children.test.ts src/router.policy.test.ts src/router.scm-credentials.test.ts`: 169 passed - `npx vitest run --config vitest.integration.config.ts test/integration/child-session-ops.test.ts test/integration/route-admission-matrix.test.ts test/integration/session-access-routes.test.ts test/integration/session-scope-routes.test.ts test/integration/spawn-children.test.ts`: 118 passed (36 / 14 / 31 / 17 / 20) - `npm run typecheck -w @open-inspect/control-plane`: exit 0 - `npx eslint` and `prettier --check` (3.8.4) on the three touched files: clean Related: ColeMurray#2191 applies the same membership rule to `POST /sessions/:id/children` (spawn). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Updated access checks for child sessions across visibility settings. Actions on team-owned child sessions now require the person initiating the action to have current team membership; without it, prompt and cancel requests are rejected and the child session remains unchanged. * Once membership is granted, those actions can proceed as expected. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…Murray#2124) ## Summary Team mutations wrote their state change and their operation-audit row as two separate statements. If the audit INSERT failed after a valid request, the change was already committed: the caller got a 500, the change persisted with no audit event, and a retry could not recover the original before-state. The result was a permanent gap in the audit trail. This PR commits each change and its audit row in a single D1 batch, the same way `createWithLead` already does. If the audit write fails, both roll back. Affected mutations. Each was reproduced on `main` by forcing the audit INSERT to fail: | Route | Audit action | | --- | --- | | `PATCH /teams/:id` | `team.updated` | | `POST /teams/:id/archive` | `team.archived` | | `POST /teams/:id/restore` | `team.restored` | | `PUT /teams/:id/members/:userId` (new member) | `team.member_added` | | `PUT /teams/:id/members/:userId` (existing member) | `team.member_role_changed` | | `DELETE /teams/:id/members/:userId` | `team.member_removed` | | `POST /teams/:id/join` | `team.member_joined` | ## Changes - The audit rows use the existing `TeamAuditStore.bind(input, true)`. Its optional `onlyIfPreviousChanged` flag defaults to `false`; passing `true` builds the audit INSERT as `INSERT … SELECT … WHERE changes() = 1`. This is the same guard `model-provider-account-atomic-writer.ts` and `TeamSecretsStore.deleteSecret` use, so the row is written only when the statement just before it in the batch changed a row. Guarded no-ops, such as archiving an already-archived team, losing a join/add race, or the last-lead guard rejecting a demotion or removal, stay unaudited as before. Any failure rolls back the whole batch. `TeamAuditStore.write` is removed; `createWithLead` keeps its unconditional audit row. - `TeamStore.update/archive/restore` and `TeamMembershipStore.add/addIfJoinable/setRole/remove` take an optional audit actor (plus the before-state where one exists) and batch the mutation with its audit row. Without it they behave as before; existing store tests and seeding use that path. - The team routes pass the request's actor to the stores and no longer write audit rows themselves. The route-level `auditTeamEvent` helper is removed. ## Audit row contents and remaining race The pattern is still read-then-batch, as in `createWithLead`: - For `team.updated/archived/restored`, `before` is the team as loaded at route admission, as it was before this change. `after` is now `before` plus the requested fields and the new `updatedAt`/`archivedAt`, not a re-read after commit. If another write lands on the same team between admission and the batch, `before` and the untouched fields in `after` can be stale. The fields this request changed are still recorded correctly. - For `team.member_role_changed/removed`, `before` is the membership read just before the batch. A concurrent role change between that read and the batch can leave `before.role` stale; the guarded UPDATE/DELETE does not compare against it. - `team.member_joined` now records the written membership (`teamId`, `userId`, `role`, `source`, `createdAt`) as `after`; previously it recorded `{ userId, role }`. `team.member_added` records the same membership shape as before. ## Tests - New `teams-routes.test.ts` cases, one per mutation above, create a `BEFORE INSERT` trigger on `authorization_audit_events` that aborts team audit rows, call the route, and check that it returns 500 with the team/membership state unchanged and no audit row. They then drop the trigger, retry, and check that the change applies with exactly one audit row whose before/after differ. All 7 fail on `main`: the state change persists. - In `packages/control-plane`, `npm run typecheck` exits 0. The team integration tests (`teams-routes`, `team-member-privacy`, `team-stores`, `team-secrets-routes`, `team-secrets-store`, `team-secrets`, `teams-migration`, `audit-event-store`: 140/140 across 8 files) and the team route unit tests (`src/routes/teams.test.ts`, `src/routes/team-secrets.test.ts`: 9/9) pass. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Reliability** * Team and membership changes now succeed or roll back together with their audit records. If audit recording fails, the associated change is not applied. * **Tests** * Added coverage for audit failures across team and membership operations, verifying that changes are rolled back and can be retried successfully. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Problem A plain team member cannot leave a team through the web app, even though the API allows it. - `packages/shared/src/types/team-access.ts:24`: `canLeave` is true for any member (and for a lead when another lead remains). `canManageMembers` is false for plain members. - `packages/control-plane/src/routes/teams.ts:457-467` and `packages/control-plane/src/routing/route-admission.ts:638-641`: `DELETE /teams/:id/members/:userId` is admitted with `need: "removeMember"`. It returns `team_capability_required` only when the caller removes *someone else* without `canManageMembers`. Self-removal is explicitly allowed. - `packages/web/src/components/settings/team-members-table.tsx:75`: the per-row "Remove" button is the only membership-removal control in the web UI, and it is disabled whenever `canManageMembers` is false. So the member's own row is disabled too. No other "Leave team" control exists (`TeamDetail` offers no join/leave action; `canLeave` is not read anywhere in `packages/web`). ## Fix In `TeamMembersTable`, the Remove button is enabled when: - the viewer has `canManageMembers` (unchanged), or - the row is the viewer's own membership (session user id from `useAuthSession`, the same id the control plane checks as `viewer(ctx).userId`) **and** `canLeave` is true. Role changes and adding members still require `canManageMembers`. Using `canLeave` for the self row keeps the button disabled for a sole lead, matching the server's last-lead guard, so the UI doesn't offer an action that is certain to fail. An alternative would be a dedicated "Leave team" button in `TeamDetail`. I kept the existing per-row control to keep this change small; happy to switch if you'd prefer a separate action. ## Tests - New test `teams-settings.test.tsx` › "lets a member without manage capability leave but not remove others". It fails on `main` (`removeMember` is never called because the button is disabled) and passes with the fix. It also asserts that another member's Remove button and the viewer's role select stay disabled. - New test `teams-settings.test.tsx` › "keeps a managing sole lead from removing themselves". It fails on `main` (the sole lead's own Remove button is enabled) and passes with the fix; another member's Remove button stays enabled. - `npm test -w @open-inspect/web -- src/components/settings/`: 32 files, 349 tests passed. - `npx tsc --noEmit` (packages/web), plus prettier and eslint on the two touched files. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Corrected team member removal permissions: members can remove themselves when leaving is allowed, while removing others remains restricted to members with management permissions. * Removal controls are unavailable when the viewer lacks the applicable permission or an update is in progress. Management permission alone does not allow a member to remove themselves when leaving is not permitted, including when they are the sole lead. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…ray#2189) ## Problem The "Require a team for new sessions" switch in Settings → Teams saves through `PATCH /api/settings/teams` and then updates only the `/api/settings/teams` SWR entry (`packages/web/src/components/settings/teams-settings.tsx:52-54`). The session composer reads the same policy from a different cache entry. `ActiveTeamProvider` takes `requireTeamOnCreate` from `useMeTeams()` (`packages/web/src/hooks/use-active-team.ts:20,90`), which is keyed `["/api/me/teams", userId]` (`packages/web/src/hooks/use-teams.ts:94-101`). The settings page keeps that entry populated, because the settings shell and nav also call `useMeTeams()`. As a result, when you open Home after changing the policy, the provider starts from the pre-change snapshot. `loading` is false because cached data exists, so Home (`app/(app)/(sidebar)/page.tsx:111-116`) treats the old policy as settled: - **After turning the requirement on:** the composer still offers a no-team session, and `POST /sessions` rejects it with `team_required` (`packages/control-plane/src/routes/session-create.ts:170-171`). - **After turning it off, for a user with no team:** creation stays blocked behind "Join a team to create a session." How long the stale value lasts depends on SWR deduplication: - **Last `/api/me/teams` fetch more than `dedupingInterval` (2s) ago:** Home's mount revalidation fixes the value after one round trip. - **Last fetch within that interval:** the mount revalidation is deduplicated and the stale value stays until the next focus or reconnect revalidation. ## Fix After a successful policy update, revalidate the membership cache with `mutate(isMeTeamsCacheKey)`. Every other team mutation in `use-teams.ts` already does this after it writes. ## Verification New `packages/web/src/components/settings/teams-settings-cache.test.tsx`. It uses one shared SWR cache and the real `TeamsSettings`, `useMeTeams`, `ActiveTeamProvider` and `useCurrentUserAuthorization`. Only `browserApiFetch` (backed by a stateful policy stub) and the auth session are mocked. The test loads Settings, flips the switch, waits for the save to finish, swaps the tree to `ActiveTeamProvider`, and checks the first ready render. It runs once per direction (`true` and `false`). - **Before the fix (clean `main`):** 2 failed. The `true` case got `requireTeamOnCreate=false` and the `false` case got `requireTeamOnCreate=true`. - **After the fix:** 2 passed. - **Mutation check:** I deleted only the `await mutate(isMeTeamsCacheKey)` line and both cases failed again with the same values. Restoring the line made them pass. - **Throwaway probe (not committed):** I set `dedupingInterval: 0` with the fix absent. The first Home render showed the stale value and the correct value arrived after the mount refetch. This confirms the transient case described above. - `npx vitest run src/hooks/use-active-team.test.tsx src/hooks/use-teams.test.tsx src/components/settings/` passed: 35 files, 413 tests. - `npm run typecheck` in `packages/web` (`next typegen && tsc --noEmit`) exited with code 0. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Changes to the team requirement for new sessions are now reflected in the home composer after saving team settings. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
# Conflicts: # docs/GETTING_STARTED.md # packages/control-plane/src/db/automation-store.test.ts # packages/control-plane/src/router.policy.test.ts # packages/control-plane/src/routes/automation-crud.ts # packages/control-plane/src/routes/skills.ts # packages/control-plane/src/routing/route-admission.ts # packages/control-plane/src/scheduler/scheduler.test.ts # packages/control-plane/test/integration/__snapshots__/hono-route-catalog-conformance.test.ts.snap # packages/control-plane/test/integration/hono-route-catalog-conformance.test.ts # packages/shared/src/types/automations.test.ts
…oleMurray#2123) ## Summary Since ColeMurray#2083, `BufferedEventForwarder.send()` declines any non-tool, non-critical event larger than `MAX_EVENT_BYTES` (1,000,000 bytes) instead of sending the frame (`packages/sandbox-runtime/src/sandbox_runtime/event_forwarder.py:152-174`). `token` events carry cumulative text, so once a text passes the limit, every later update to it is declined and the rest of that text never reaches the control plane. The bridge counts output as emitted before it sends it and ignores the result (`packages/sandbox-runtime/src/sandbox_runtime/bridge.py:696-707`), so `execution_complete` still reports `success: true`. The shape of `token` events differs by harness: - Claude: every token carries the whole turn's text, joined across each assistant message in the turn (`packages/sandbox-runtime/src/sandbox_runtime/harness/claude.py:191-192`, `:904-909`). A long multi-step turn can therefore lose its final answer entirely, even when no single message is close to the limit. - OpenCode: tokens are cumulative per text part (`packages/sandbox-runtime/src/sandbox_runtime/harness/opencode_stream.py:623-640`). The final-state fetch after `session.idle` (`:925-988`) compares against the text the harness emitted, not the text that was delivered, so it does not resend the missing text. Any token it does emit for that part is over the limit as well. Nothing else carries the text. `execution_complete` has only `success`, `error` and cost (`bridge.py:770-776`). The control plane keeps the last token per message or part (`packages/control-plane/src/session/event-repository.ts:121-126`). The web shows the last token per segment (`packages/web/src/lib/session-socket/event-log.ts:111-132`), and completion callbacks read the last token (`packages/shared/src/completion/extractor.ts:191-196`). When the overflowing text is the last one in the turn, as it always is with Claude, the stored, displayed and posted answer is the last snapshot under the limit, and nothing marks it as incomplete. ## Fix In `emit`, the bridge now notes a token that `send()` declined: `send()` returned `False` and the token is over the limit. Any other `False` return for a token means the forwarder buffered it for the next connection. The forwarder stamps `sandboxId` and `timestamp` in place before it sizes an event, so the bridge measures the same bytes. If the harness otherwise succeeded, the turn fails with "The agent's response exceeded the event size limit and was not delivered in full." and logs `prompt.text_undelivered`, next to the existing no-output guard (`bridge.py:734-743`). The text delivered before the limit stays in the timeline, followed by "Execution failed: …" (`packages/web/src/components/session-timeline.tsx:667-672`). Callbacks receive `success: false` with the error. A harness error or a cancellation still takes precedence. The forwarder is unchanged: declining the frame is still right, since `MAX_EVENT_BYTES` keeps each event under the Durable Object SQLite row limit (`packages/sandbox-runtime/src/sandbox_runtime/event_size.py:8-12`). Tool calls are unaffected because ColeMurray#2083 truncates them and marks them `truncated`. Any declined token fails the turn. With OpenCode, that includes an earlier text part that overflowed even when a later part was delivered, because the stored text of the earlier part is then incomplete. Alternative: deliver bounded text instead of failing. The current protocol has no complete representation. Each token replaces the stored text for its message or part, and the web and callbacks read only the last token. A delta would therefore replace the stored answer, and a split into several parts would still show and post only the last part. A bounded prefix is what is already stored. A bounded tail would keep the end of the answer but silently drop its start: the `token` schema (`packages/shared/src/types/sandbox-events.ts:126-130`) has no truncation marker, and the control plane strips unknown fields when it parses sandbox events. ColeMurray#2083 also deliberately left non-tool events untruncated. If you'd rather keep the tail, it needs a `truncated` field on `token` events, as `tool_call` has, plus a marker in the UI. ## Reproduction `TestAssistantTextDelivery::test_text_past_the_event_limit_fails_the_turn` in `packages/sandbox-runtime/tests/test_bridge_event_buffer.py` streams a short token, then a cumulative token over the limit, through `_handle_prompt` and the real forwarder bound to a fake socket. On `main` (`700f9145`), only the first token reaches the socket, and the test fails on the outcome: ```text > assert completion["success"] is False E assert True is False ``` `test_text_the_envelope_pushes_past_the_limit_fails_the_turn` uses a token that is exactly at the limit until the forwarder stamps `sandboxId` on it. It also fails on `main` with `assert True is False`, and it fails if the bridge sizes the token before the forwarder stamps it. I also ran a check that is not committed. It drives the real harnesses through `_handle_prompt` and the real forwarder. On `main`: - Claude harness, with a 600 KB message followed by a 450 KB message that ends in the answer: the 1,050,153-byte token is declined, and only the first message's text is sent. The completion is `{'success': True, 'messageCostUsd': 0.1}`. - OpenCode, with one text part that grows to 1.1 MB: the 900 KB snapshot is sent, and the later snapshots (1,100,135 and 1,100,169 bytes) are declined. The completion is `{'success': True}`. With this change, both runs report `success: false` with the error above. Two further tests pass on `main` and guard the fix itself. `test_text_buffered_while_disconnected_does_not_fail_the_turn` checks that a token buffered while no connection is bound does not fail the turn and is delivered on bind. `test_harness_error_outranks_undelivered_text` checks that the harness's own error is reported rather than the size error. ## Tests In `packages/sandbox-runtime`: - `pytest tests/`: 1394 passed, 3 skipped - `ruff check` and `ruff format --check` on the two touched files: clean - `mypy src/`: 6 errors, the same 6 as on `main`, in `auth/github_app.py`, `entrypoint.py` and `managed_skills.py`; none in `bridge.py` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Prompts now report a failure when assistant text cannot be delivered because it exceeds the supported message size, instead of appearing to complete successfully. * Existing errors continue to take precedence when a prompt fails for another reason. * Assistant text buffered while disconnected continues to be delivered after the connection is restored, without causing the prompt to fail. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
|
Important Review skippedToo many files! This PR contains 326 files, which is 26 over the limit of 300. To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (326)
You can disable this status message by setting the
Comment |
This is an automated nightly unsafe-cast remediation sweep. It replaces three remaining persisted SQLite message-row assertions with parse-don't-assert validation, following the TypeScript Coding Standards guidance for unsafe-cast / parse-don't-assert and the Zod boundary-validation pattern established in PR ColeMurray#807. | file:line | risk | cast removed | how it was fixed | | --- | --- | --- | --- | | `packages/control-plane/src/session/message-repository.ts:354` | MEDIUM | `result.toArray() as Array<{ callback_context: string | null; source: string | null }>` for callback delivery state | Added `messageCallbackContextRowSchema` and parsed rows through existing `parseStorageRows`; valid `callback_context: null` remains accepted. | | `packages/control-plane/src/session/message-repository.ts:450` | MEDIUM | `result.toArray() as Array<{ status?: unknown; created_at: number; started_at: number | null }>` before recording completion and canonical events | Added `messageCompletionStateRowSchema` and parsed rows through existing `parseStorageRows`; `status` remains `unknown` so malformed statuses preserve the existing null/skip behavior. | | `packages/control-plane/src/session/message-repository.ts:528` | MEDIUM | `result.toArray() as Array<{ author_id: string }>` for processing-message author state | Added `messageProcessingAuthorRowSchema` and parsed rows through existing `parseStorageRows`. | Verification: | command | result | | --- | --- | | `npm run format -- --write packages/control-plane/src/session/message-repository.ts packages/control-plane/src/session/message-repository.test.ts` | Passed | | `npm test -w @open-inspect/control-plane -- src/session/message-repository.test.ts --maxWorkers=1` | Passed, 48 tests | | `npm run build -w @open-inspect/shared` | Passed | | `npm run build -w @open-inspect/control-plane` | Passed | | `npm run typecheck` | Passed | | `npm run lint` | Passed | | `npm run format` | Passed | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1` | Passed, 360 files / 6136 tests | No dependency changes were made. Existing open PRs labeled `automation:unsafe-cast` were checked first; this sweep excludes their covered findings. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/84c0638f9356482a7bbd440369e50e12)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved handling of invalid stored session message data. Malformed timestamps, callback context, completion state, or processing authors now trigger a storage integrity error instead of being accepted. * Prevented further database operations when a malformed completion record is detected. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
Terraform Validation Results
Pushed by: @rhlsthrm, Action: |
Terraform Plan ResultsStatus: ✅ Success Show Planterraform_data.cloudflare_custom_domain_gate: Refreshing state... [id=09b89c9b-996a-d28b-1f9c-08760a492dac]
terraform_data.sign_in_provider_gate: Refreshing state... [id=b29a3d55-0be5-fb92-10a9-027df10c4b75]
terraform_data.access_control_gate: Refreshing state... [id=2b965617-b42b-4b42-5ea5-a1c3c05f10be]
local_file.web_app_wrangler_production[0]: Refreshing state... [id=d58ccd8dd2962c70f7cff2ffac9821e9e711af31]
module.github_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=e0848d433a4f466cafd4ed5d140aad7d]
cloudflare_queue.image_build_finalization_dlq: Refreshing state... [id=61535c686d8546099cfddcf39833572b]
cloudflare_queue.slack_completion_delivery_dlq[0]: Refreshing state... [id=396865e4939b4160937b2dc9ad5dabe1]
module.slack_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=ab5c371c8bc04a938ff2f71809933aa0]
module.session_index_kv.cloudflare_workers_kv_namespace.this: Refreshing state... [id=ea0a253d5cb64d75a841acb88040cd2f]
cloudflare_queue.github_autofix_dlq[0]: Refreshing state... [id=3a27213aeba149d4b7cbf2d3551842f8]
cloudflare_d1_database.main: Refreshing state... [id=f747a908-5c69-45a1-86ab-ceb5250cf5e0]
cloudflare_r2_bucket.media: Refreshing state... [id=open-inspect-media-codos]
cloudflare_queue.slack_completion_delivery[0]: Refreshing state... [id=247b1100bac2408684d6a75c1bca0d28]
cloudflare_queue.github_autofix[0]: Refreshing state... [id=033a23f13783415385b2f8799416c20f]
cloudflare_queue.image_build_finalization: Refreshing state... [id=a0647323f7424e778b9d59da50dc55cf]
module.linear_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=777f94c3595f4de680c256a4e5fc6653]
data.external.modal_source_hash[0]: Reading...
null_resource.control_plane_build: Refreshing state... [id=5528656732809257011]
null_resource.github_bot_build[0]: Refreshing state... [id=6908611508837039030]
module.modal_app[0].null_resource.modal_secrets[0]: Refreshing state... [id=8477737195823217344]
random_password.service_auth_secret_github_bot: Refreshing state... [id=none]
random_password.service_auth_secret_slack_bot: Refreshing state... [id=none]
random_bytes.provider_accounts_encryption_key: Refreshing state...
null_resource.slack_bot_build[0]: Refreshing state... [id=139287417073036493]
random_password.service_auth_secret_web: Refreshing state... [id=none]
null_resource.web_app_cloudflare_build[0]: Refreshing state... [id=3047571768604585709]
random_password.service_auth_secret_linear_bot: Refreshing state... [id=none]
random_password.image_callback_token_pepper: Refreshing state... [id=none]
null_resource.linear_bot_build[0]: Refreshing state... [id=3415715852648161979]
module.slack_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=375c2c6875904657bce05c62c8048c76]
data.external.modal_source_hash[0]: Read complete after 0s [id=-]
module.linear_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=049cd48117bc48b9b4332683a97d0a0e]
module.modal_app[0].null_resource.modal_deploy: Refreshing state... [id=8891027293183044333]
null_resource.d1_migrations: Refreshing state... [id=263751651589333239]
module.linear_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=14ed4788-feb0-4b6e-9ceb-0d1c7b47bf4d]
module.slack_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=ee23c16d-82c8-45c5-bf5b-df2b18e0f5a2]
module.linear_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=67610c8f-eb33-4aaa-954b-154744e911d3]
module.control_plane_worker.cloudflare_worker.this: Refreshing state... [id=3457352971a74b89be5ed3700db48a8e]
module.slack_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=8bbfc3b8-8d25-47ee-8904-22447bbf8773]
cloudflare_queue_consumer.slack_completion_delivery[0]: Refreshing state...
module.control_plane_worker.cloudflare_worker_version.this: Refreshing state... [id=b93c0070-0c3d-4ffa-83b8-567b6777d98c]
module.control_plane_worker.cloudflare_workers_deployment.this: Refreshing state... [id=cebefa4e-9f03-4b7e-a545-86f94cab5c9a]
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=5183708612434619948]
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=f7356b06-2065-4b43-9577-f9d17e05d1eb]
module.github_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=624981be-d5b4-467f-a418-465957bfaf1d]
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 = "5528656732809257011" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-10-01T08:29:31Z" -> (known after apply)
}
}
# null_resource.github_bot_build[0] must be replaced
-/+ resource "null_resource" "github_bot_build" {
~ id = "6908611508837039030" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-10-01T08:29:30Z" -> (known after apply)
}
}
# null_resource.linear_bot_build[0] must be replaced
-/+ resource "null_resource" "linear_bot_build" {
~ id = "3415715852648161979" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-10-01T08:29:31Z" -> (known after apply)
}
}
# null_resource.slack_bot_build[0] must be replaced
-/+ resource "null_resource" "slack_bot_build" {
~ id = "139287417073036493" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-10-01T08:29:30Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_build[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_build" {
~ id = "3047571768604585709" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-10-01T08:29:30Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_deploy[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_deploy" {
~ id = "5183708612434619948" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-10-01T08:31:55Z" -> (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-10-01T08:31:51Z" -> (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-10-01T08:31:53Z" -> (known after apply)
~ id = "b93c0070-0c3d-4ffa-83b8-567b6777d98c" -> (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 = "854e8ac71750e38324cf7b66d059d72ca16e62fb6074fd2ecc7f976909621078" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/control-plane/dist/index.js"
+ content_sha256 = "cbbffb441718f28d4cdcb01e5294c596dfeb8a4d5b4d0107d3d63dd67c10894f"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 76 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 91 -> (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-10-01T08:31:55Z" -> (known after apply)
~ id = "cebefa4e-9f03-4b7e-a545-86f94cab5c9a" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "b93c0070-0c3d-4ffa-83b8-567b6777d98c" -> (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-10-01T08:31:55Z" -> (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-10-01T08:31:56Z" -> (known after apply)
~ id = "f7356b06-2065-4b43-9577-f9d17e05d1eb" -> (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 = "5cdbd14abb2645c367130bc1db746dca2929dc7ea270f57914d1c3cdc084bc44" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/github-bot/dist/index.js"
+ content_sha256 = "23f9979b7b93a642f841ec5954878c060e916f0e5ea6346f182c0c0daf8b8efd"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 74 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 40 -> (known after apply)
~ urls = [
- "https://f7356b06-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-10-01T08:31:57Z" -> (known after apply)
~ id = "624981be-d5b4-467f-a418-465957bfaf1d" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "f7356b06-2065-4b43-9577-f9d17e05d1eb" -> (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-10-01T08:29:31Z" -> (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-10-01T08:29:32Z" -> (known after apply)
~ id = "14ed4788-feb0-4b6e-9ceb-0d1c7b47bf4d" -> (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 = "896f64892838c78a55e22e96e5362ddd9e6d308f1d8238b5dcbe5cde6d83f593" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/linear-bot/dist/index.js"
+ content_sha256 = "7b42cf70800722f964d7272de95ecc31f5307b71f4a0a0b2d9d8f68aecc38417"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 80 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 31 -> (known after apply)
~ urls = [
- "https://14ed4788-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-10-01T08:29:33Z" -> (known after apply)
~ id = "67610c8f-eb33-4aaa-954b-154744e911d3" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "14ed4788-feb0-4b6e-9ceb-0d1c7b47bf4d" -> (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 = "8891027293183044333" -> (known after apply)
~ triggers = { # forces replacement
~ "secrets_created" = (sensitive value)
~ "source_hash" = "2ba85fecd95506fef751e24b3b0ffa9c6c9c26ff72519878575273c24e06a415" -> "a9117ffe75e842adc92a751db93784b8d2157180329f2428b1a66fbbf8a5e0c0"
# (3 unchanged elements hidden)
}
}
# module.modal_app[0].null_resource.modal_secrets[0] must be replaced
-/+ resource "null_resource" "modal_secrets" {
~ id = "8477737195823217344" -> (known after apply)
~ triggers = { # forces replacement
~ "secrets_hash" = (sensitive value)
# (1 unchanged element 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-10-01T08:29:31Z" -> (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-10-01T08:29:32Z" -> (known after apply)
~ id = "ee23c16d-82c8-45c5-bf5b-df2b18e0f5a2" -> (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 = "d461e51f22ef13a3c3e7048bad91ed13f450872b4af43e534acce447d7d50a65" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/slack-bot/dist/index.js"
+ content_sha256 = "be4f799a0ea8968b96ff8d00133bb23b95e4174ad082cb58c73b2bf0f71efbea"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 78 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 65 -> (known after apply)
~ urls = [
- "https://ee23c16d-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-10-01T08:29:33Z" -> (known after apply)
~ id = "8bbfc3b8-8d25-47ee-8904-22447bbf8773" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "ee23c16d-82c8-45c5-bf5b-df2b18e0f5a2" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
Plan: 17 to add, 4 to change, 16 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"Pushed by: @rhlsthrm |
There was a problem hiding this comment.
Blocking: 7 · Non-blocking: 0
The sync introduces regressions in credential availability for retained sandboxes and legacy sessions, allows team automations to be saved or launched without their executor's current authority, and leaves several team-scoped web workflows in incorrect states. Focused control-plane tests passed (164); focused web tests passed (71) after rebuilding shared. GitHub's PR diff endpoint returned 406 for this 326-file PR, so the review used the local main-to-head diff. No prior review threads were present.
Terraform Validation Results
Pushed by: @rhlsthrm, Action: |
Terraform Plan ResultsStatus: ✅ Success Show Planterraform_data.sign_in_provider_gate: Refreshing state... [id=b29a3d55-0be5-fb92-10a9-027df10c4b75]
terraform_data.access_control_gate: Refreshing state... [id=2b965617-b42b-4b42-5ea5-a1c3c05f10be]
terraform_data.cloudflare_custom_domain_gate: Refreshing state... [id=09b89c9b-996a-d28b-1f9c-08760a492dac]
local_file.web_app_wrangler_production[0]: Refreshing state... [id=d58ccd8dd2962c70f7cff2ffac9821e9e711af31]
random_password.service_auth_secret_slack_bot: Refreshing state... [id=none]
random_password.service_auth_secret_github_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_bytes.provider_accounts_encryption_key: Refreshing state...
module.github_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=e0848d433a4f466cafd4ed5d140aad7d]
cloudflare_queue.github_autofix[0]: Refreshing state... [id=033a23f13783415385b2f8799416c20f]
cloudflare_queue.slack_completion_delivery_dlq[0]: Refreshing state... [id=396865e4939b4160937b2dc9ad5dabe1]
random_password.service_auth_secret_linear_bot: Refreshing state... [id=none]
cloudflare_queue.slack_completion_delivery[0]: Refreshing state... [id=247b1100bac2408684d6a75c1bca0d28]
null_resource.slack_bot_build[0]: Refreshing state... [id=139287417073036493]
cloudflare_queue.image_build_finalization_dlq: Refreshing state... [id=61535c686d8546099cfddcf39833572b]
module.session_index_kv.cloudflare_workers_kv_namespace.this: Refreshing state... [id=ea0a253d5cb64d75a841acb88040cd2f]
cloudflare_queue.github_autofix_dlq[0]: Refreshing state... [id=3a27213aeba149d4b7cbf2d3551842f8]
cloudflare_r2_bucket.media: Refreshing state... [id=open-inspect-media-codos]
null_resource.control_plane_build: Refreshing state... [id=5528656732809257011]
cloudflare_queue.image_build_finalization: Refreshing state... [id=a0647323f7424e778b9d59da50dc55cf]
module.linear_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=777f94c3595f4de680c256a4e5fc6653]
module.slack_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=ab5c371c8bc04a938ff2f71809933aa0]
cloudflare_d1_database.main: Refreshing state... [id=f747a908-5c69-45a1-86ab-ceb5250cf5e0]
data.external.modal_source_hash[0]: Reading...
null_resource.linear_bot_build[0]: Refreshing state... [id=3415715852648161979]
module.modal_app[0].null_resource.modal_secrets[0]: Refreshing state... [id=8477737195823217344]
null_resource.github_bot_build[0]: Refreshing state... [id=6908611508837039030]
null_resource.web_app_cloudflare_build[0]: Refreshing state... [id=3047571768604585709]
module.linear_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=049cd48117bc48b9b4332683a97d0a0e]
data.external.modal_source_hash[0]: Read complete after 0s [id=-]
module.modal_app[0].null_resource.modal_deploy: Refreshing state... [id=8891027293183044333]
module.slack_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=375c2c6875904657bce05c62c8048c76]
module.slack_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=ee23c16d-82c8-45c5-bf5b-df2b18e0f5a2]
module.linear_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=14ed4788-feb0-4b6e-9ceb-0d1c7b47bf4d]
null_resource.d1_migrations: Refreshing state... [id=263751651589333239]
module.linear_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=67610c8f-eb33-4aaa-954b-154744e911d3]
module.slack_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=8bbfc3b8-8d25-47ee-8904-22447bbf8773]
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=b93c0070-0c3d-4ffa-83b8-567b6777d98c]
module.control_plane_worker.cloudflare_workers_deployment.this: Refreshing state... [id=cebefa4e-9f03-4b7e-a545-86f94cab5c9a]
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=5183708612434619948]
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=f7356b06-2065-4b43-9577-f9d17e05d1eb]
module.github_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=624981be-d5b4-467f-a418-465957bfaf1d]
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 = "5528656732809257011" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-10-01T08:29:31Z" -> (known after apply)
}
}
# null_resource.github_bot_build[0] must be replaced
-/+ resource "null_resource" "github_bot_build" {
~ id = "6908611508837039030" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-10-01T08:29:30Z" -> (known after apply)
}
}
# null_resource.linear_bot_build[0] must be replaced
-/+ resource "null_resource" "linear_bot_build" {
~ id = "3415715852648161979" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-10-01T08:29:31Z" -> (known after apply)
}
}
# null_resource.slack_bot_build[0] must be replaced
-/+ resource "null_resource" "slack_bot_build" {
~ id = "139287417073036493" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-10-01T08:29:30Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_build[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_build" {
~ id = "3047571768604585709" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-10-01T08:29:30Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_deploy[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_deploy" {
~ id = "5183708612434619948" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-10-01T08:31:55Z" -> (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-10-01T08:31:51Z" -> (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-10-01T08:31:53Z" -> (known after apply)
~ id = "b93c0070-0c3d-4ffa-83b8-567b6777d98c" -> (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 = "854e8ac71750e38324cf7b66d059d72ca16e62fb6074fd2ecc7f976909621078" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/control-plane/dist/index.js"
+ content_sha256 = "3e33de33bcda44f9bd57d1c64347b2ac7d86f588c4a15276569f984a33c4cd91"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 76 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 91 -> (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-10-01T08:31:55Z" -> (known after apply)
~ id = "cebefa4e-9f03-4b7e-a545-86f94cab5c9a" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "b93c0070-0c3d-4ffa-83b8-567b6777d98c" -> (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-10-01T08:31:55Z" -> (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-10-01T08:31:56Z" -> (known after apply)
~ id = "f7356b06-2065-4b43-9577-f9d17e05d1eb" -> (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 = "5cdbd14abb2645c367130bc1db746dca2929dc7ea270f57914d1c3cdc084bc44" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/github-bot/dist/index.js"
+ content_sha256 = "23f9979b7b93a642f841ec5954878c060e916f0e5ea6346f182c0c0daf8b8efd"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 74 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 40 -> (known after apply)
~ urls = [
- "https://f7356b06-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-10-01T08:31:57Z" -> (known after apply)
~ id = "624981be-d5b4-467f-a418-465957bfaf1d" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "f7356b06-2065-4b43-9577-f9d17e05d1eb" -> (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-10-01T08:29:31Z" -> (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-10-01T08:29:32Z" -> (known after apply)
~ id = "14ed4788-feb0-4b6e-9ceb-0d1c7b47bf4d" -> (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 = "896f64892838c78a55e22e96e5362ddd9e6d308f1d8238b5dcbe5cde6d83f593" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/linear-bot/dist/index.js"
+ content_sha256 = "7b42cf70800722f964d7272de95ecc31f5307b71f4a0a0b2d9d8f68aecc38417"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 80 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 31 -> (known after apply)
~ urls = [
- "https://14ed4788-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-10-01T08:29:33Z" -> (known after apply)
~ id = "67610c8f-eb33-4aaa-954b-154744e911d3" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "14ed4788-feb0-4b6e-9ceb-0d1c7b47bf4d" -> (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 = "8891027293183044333" -> (known after apply)
~ triggers = { # forces replacement
~ "secrets_created" = (sensitive value)
~ "source_hash" = "2ba85fecd95506fef751e24b3b0ffa9c6c9c26ff72519878575273c24e06a415" -> "320955e4ffa54637ea646b4563c126ffc3c60ca4c4d584a8a83c2476d4386960"
# (3 unchanged elements hidden)
}
}
# module.modal_app[0].null_resource.modal_secrets[0] must be replaced
-/+ resource "null_resource" "modal_secrets" {
~ id = "8477737195823217344" -> (known after apply)
~ triggers = { # forces replacement
~ "secrets_hash" = (sensitive value)
# (1 unchanged element 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-10-01T08:29:31Z" -> (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-10-01T08:29:32Z" -> (known after apply)
~ id = "ee23c16d-82c8-45c5-bf5b-df2b18e0f5a2" -> (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 = "d461e51f22ef13a3c3e7048bad91ed13f450872b4af43e534acce447d7d50a65" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/slack-bot/dist/index.js"
+ content_sha256 = "be4f799a0ea8968b96ff8d00133bb23b95e4174ad082cb58c73b2bf0f71efbea"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 78 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 65 -> (known after apply)
~ urls = [
- "https://ee23c16d-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-10-01T08:29:33Z" -> (known after apply)
~ id = "8bbfc3b8-8d25-47ee-8904-22447bbf8773" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "ee23c16d-82c8-45c5-bf5b-df2b18e0f5a2" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
Plan: 17 to add, 4 to change, 16 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"Pushed by: @rhlsthrm |
There was a problem hiding this comment.
Blocking: 9 · Non-blocking: 0
The seven previously reported failures remain reproducible at this head and have no replies on their threads. Two additional edit flows now resend unchanged repository selections and reject otherwise valid edits after a grant is revoked. GitHub rejects the 330-file PR diff with HTTP 406, so this review used the local main-to-head diff at the leased commit. I did not rerun the full suites in this pass.
Review dispositions (codos-reviewer, 9 blocking)All nine findings are on upstream-authored code this sync carries: ColeMurray#2178, ColeMurray#2180, ColeMurray#2205 and ColeMurray#2206. For every flagged file, either the file is byte-identical to upstream
None of these block deploying the fork. T1 and T0 are the deploy-time risks, and production exposure was measured at zero for T1; T0 does not reproduce. The other findings are pre-existing upstream behaviour that the upstream PRs fix. |
Syncs upstream
ColeMurray/background-agentsf1cf0697..b8c9a80b(16 commits, 328 files) into the fork.Five of the incoming commits are our own upstream PRs landing: ColeMurray#2192, ColeMurray#2189, ColeMurray#2124, ColeMurray#2121, ColeMurray#2123. Their files are byte-identical to upstream after the merge. The one exception is
routes/teams.ts, where the fork's PAT viewer line remains.Conflicts
docs/GETTING_STARTED.md: kept upstream's table (now withmodal-vm) and added back the fork's four reviewer-App rows.route-admission.ts,automation-crud.ts,skills.ts: took upstream'sevaluateOwnedResourceAdmission/authorizeSessionTargetrefactor. An access token gets its owner'sctx.authorization, so the owner-scoped viewer covers the fork's oldisSelfActingPrincipalownership gate.automations.test.ts,automation-store.test.ts,scheduler.test.ts): kept both sides.Fork-feature adaptations
authorizeSessionTargetnow applies therepositories.use/environments.usechecks to access tokens as well as users and services. Without this, a token creating an automation would skip the target checks its owner faces. The PAT trigger-denial test now expects upstream's newautomation_action_deniedbody. It still returns 403 and the scheduler is never called.repo_idfrom the review fence row), intersected with the owner team's current grants. The session's full repository set is not requested, because that would fail wherever the reviewer App isn't installed on a context repository. The integration test assertsrepository_ids: [reviewed]for a session with two repositories.maxConcurrentRuns.Gates
typecheck 0; lint 0. Control-plane unit 6252 and integration 1955 pass after the two fixes above. web, github-bot, slack-bot, linear-bot, shared, mcp-server, sandbox-runtime node, SQL portability, modal-infra, and
terraform testall pass. sandbox-runtime pytest passed 1402. It needs--basetemp=/tmp/...on macOS, because the new upstreamtest_docker_service.pyhits the 104-byteAF_UNIXpath limit under the default$TMPDIR.No new D1 migrations upstream; high-water stays 0083, fork at 0090–0093.