Repository navigation
chore: sync fork with upstream (ColeMurray/background-agents) - #27
Open
jasoncuriano wants to merge 457 commits into
Open
jasoncuriano wants to merge 457 commits into
jasoncuriano wants to merge 457 commits into
Conversation
## Summary Closes [COL-203](https://linear.app/colemurray/issue/COL-203). - Add `/teams` with membership filters, search, per-user local favorites, counts, join policy, and capability-gated joining. - Add team pages with directory-visible headers and Members, member/admin Overview and Activity, and capability-gated Settings using the existing team-management components. - Add member-only team session buckets and visibility-filtered, cursor-paginated activity; add a team filter to the workspace audit viewer. - Add owning-team and visibility metadata, private-session collaborator management, visibility cascades, and desktop/mobile move dialogs with join consent, repository-grant errors, owner-access warnings, and retry without children. - Use server snapshot capabilities for session actions, retaining the global permission check only for trace export. Explicitly refresh snapshots and invalidate mounted, inactive, infinite, and retained list pages after scope mutations. - Add BFF proxies, shared response schemas, route-catalog/admission coverage, and a changelog entry. ## Access And Storage Directory reads admit active human users. Team work routes require membership or an administrator role; denied capabilities remain 403 with `reason_code`, while nonmembers cannot enumerate member-only team feeds. Existing join-denial codes are preserved. The collaborator picker has a narrowly scoped `GET /sessions/:id/collaborator-candidates` reader under always-enforced `manageCollaborators` admission. It returns only active users' picker identities; `/members` and its workspace permission remain unchanged. Invisible sessions return the same 404 as missing sessions before the candidate handler runs, including in `off` and `shadow`. Session lists retain the existing `off`/`shadow`/`on` semantics and private-session restrictions. Scope writes, audit writers, credential boundaries, and enforcement defaults are unchanged. No D1 or DO migration is added; the existing Teams schema is migration 0083. ## Checkpoint: Commands And Results Final checks ran sequentially with targeted test selections. The branch was rebased from `cf345db` onto current `main` at `3beccb0`, then affected-package typechecks, scoped lint, SQL portability, and targeted tests were rerun. ```bash npm run build -w @open-inspect/shared npm run typecheck npm run typecheck -w @open-inspect/control-plane -w @open-inspect/web npm run lint:sql-portability git diff --name-only -z origin/main...HEAD -- '*.ts' '*.tsx' | xargs -0 npx eslint git diff --name-only -z origin/main...HEAD -- '*.ts' '*.tsx' '*.md' | xargs -0 npx prettier --check git diff --check origin/main...HEAD git diff --relative=packages/web --name-only -z origin/main...HEAD -- ':(glob)packages/web/src/**/*.test.ts' ':(glob)packages/web/src/**/*.test.tsx' | xargs -0 -r npm test -w @open-inspect/web -- npm test -w @open-inspect/control-plane -- src/routes/teams.test.ts src/routes/audit-events.test.ts src/routes/session-runtime-proxy.test.ts src/db/audit-event-store.test.ts src/router.policy.test.ts npm run test:integration -w @open-inspect/control-plane -- test/integration/teams-routes.test.ts test/integration/audit-event-store.test.ts test/integration/audit-events-route.test.ts test/integration/session-inbox.test.ts test/integration/session-snapshot.test.ts test/integration/collaborator-candidates.test.ts test/integration/hono-route-catalog-conformance.test.ts test/integration/route-admission-matrix.test.ts npm test -w @open-inspect/shared -- src/types/server-messages.test.ts ``` - Shared build and all-workspace typecheck passed before rebase; control-plane and web typechecks passed again after rebase. - Final web selection: **31 files, 368 tests passed**. - Final control-plane unit selection: **5 files, 204 tests passed**. - Final control-plane integration selection: **8 files, 106 tests passed**. - Shared snapshot-contract selection: **21 tests passed**. - Scoped ESLint, Prettier, SQL portability, commit hooks, and diff checks passed. Coverage includes a two-team parent/child move with both buckets updating despite unchanged timestamps, Member collaborator add/remove without workspace-directory access, all enforcement modes, nonmember concealment, private audit filtering, missing capabilities, account/cache isolation, cascade retry, and not-found rendering after access loss. ### Failures Encountered And Fixed The tests-first capability/snapshot regression run failed as expected: ```text Test Files 2 failed (2) Tests 4 failed | 2 passed (6) TypeError: useRefreshSessionSnapshot is not a function ``` The tests-first desktop-action, fresh-SSR, inactive-cache, and target-membership regression run also failed before the fixes: ```text Test Files 4 failed (4) Tests 4 failed | 51 passed (55) ``` A subsequent targeted web run caught stale expectations after unconditional cache invalidation; those assertions now require old head rows and retained pages to disappear while refreshed data is pending: ```text Test Files 2 failed | 25 passed (27) Tests 4 failed | 342 passed (346) AssertionError: expected [] to deeply equal [ 'attention' ] AssertionError: expected "vi.fn()" to be called once, but got 2 times ``` The initial collaborator-candidate integration fixture omitted nullable `ownerTeamId`; it was corrected without changing the store: ```text Test Files 1 failed | 6 passed (7) Tests 12 failed | 80 passed (92) Error: D1_TYPE_ERROR: Type 'undefined' not supported for value 'undefined' ``` The first typecheck used a shared subpath that is not exported. Both imports now use the existing root type export: ```text src/lib/session-capabilities.test.ts(3,42): error TS2307: Cannot find module '@open-inspect/shared/types/session-access' or its corresponding type declarations. src/lib/session-capabilities.ts(2,71): error TS2307: Cannot find module '@open-inspect/shared/types/session-access' or its corresponding type declarations. ``` SQL lint caught the candidate sort; it now uses portable `LOWER(COALESCE(...))`: ```text SQL portability: 1 disallowed construct(s). packages/control-plane/src/routes/session-scope.ts:297 collate-nocase COLLATE NOCASE ``` The first scoped formatting command included generated Vitest snapshots. They were excluded from subsequent formatting commands: ```text [error] No parser could be inferred for file "/workspace/background-agents/packages/control-plane/test/integration/__snapshots__/hono-route-catalog-conformance.test.ts.snap". [error] No parser could be inferred for file "/workspace/background-agents/packages/control-plane/test/integration/__snapshots__/route-admission-matrix.test.ts.snap". ``` ## Checkpoint: Baseline Differences - The HTTP snapshot at `session-runtime-proxy.ts` exposed team, visibility, collaborators, and capabilities but no canonical owner identity. It now also decorates `ownerUserId` from the admitted D1 row, never participant presence, for the owner-access warning. - The existing workspace candidate reader required `workspace.members.read`, which built-in Members do not have. The scoped picker reader solves this without widening the workspace directory. - The inbox store filtered by teams but omitted the owner/scope fields needed for effective per-row capabilities. Its internal projection now preserves those fields; the team route decorates the response. - SSR snapshots had no client refresh path, and sidebar cursor pages retained rows outside SWR. The provider now explicitly refetches and accepts fresh SSR data; successful scope mutations also reset retained pagination. - The existing move write requires target membership or explicit joining of an open team, including for administrators. The dialog follows that contract instead of changing the write path. ## Checkpoint: Deliberately Deferred - PR 9's switcher, active-team hook, sidebar/discovery filters, composer team/visibility fields, transport `sessionGone`, and `requireTeamOnCreate` response field are untouched. PR 9 is not on the rebased `main`; its switcher-to-team-page link and switch-teams browser step remain follow-ups. - The PR-state board and later repository, channel, secrets, automation, and environment tabs are not implemented. - Activity reads `team_id` only. Move-departure audit writers are unchanged. Team Activity contains domain events only. HTTP authorization decisions remain in the permission-gated workspace audit; the store does not inspect route prefixes. - Screenshots, additional browser recordings, and full-suite reruns were deferred in favor of sequential targeted checks. The parent/child move, distinct team lists, collaborator flow, and not-found behavior are covered by integration and component tests. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/1bea9d45abf4fd9a829466aba8845ab5)* ## Audit Cleanup Follow-up commit `d91bf24` removes route-prefix matching from visibility-scoped audit reads. Team Activity filters by resource type and retains current session visibility and read-permission checks. Its event-type selector contains operation events only. Workspace audit still retains HTTP authorization decisions. No schema, migration, or audit-writer changes. Sequential validation: 55 targeted control-plane integration tests and 34 web tests passed; control-plane/web typechecks, scoped ESLint, Prettier, SQL portability, and commit hooks passed. The new regression coverage was red before the change and verifies path-independent exclusion, null resource IDs, pagination, and preservation of workspace evidence. --------- Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com> Co-authored-by: ColeMurray <2492022+ColeMurray@users.noreply.github.com>
…oleMurray#2169) > Follows ColeMurray#2168 (session details tabs, merged). This PR's diff is only the diff-view changes. ## Summary Opening a changed file now shows the diff in the main column, where the conversation was, instead of a resizable panel squeezed between the conversation and the details sidebar. The sidebar stays where it is, and its Changes list becomes the file navigator for the diff. At 1440px with both sidebars open, the old side panel left roughly 400px for code: Split was unavailable, and the panel repeated the sidebar's file list in a narrow rail. The diff now gets the whole main column (about 790px at that width), so Split is available. ## Layout - **The diff replaces the conversation in the main column.** The timeline, terminal and composer stay mounted underneath, so scroll position and an unsent draft survive opening and closing a diff. The resizable conversation/diff split and its stored layout (`session-changes-layout-v2`, `useBrowserLayoutStorage`) are removed. - **The details sidebar stays open while a diff is showing**, and the header's sidebar toggle stays available (the `showDesktopDetailsToggle` prop, which only existed to hide it, is removed). - **The session page owns the inspector tab** (it was private to the sidebar), so the layout can tell whether the sidebar is showing the file list. Opening a diff shows the Changes tab without replacing the tab the viewer remembers; moving between files doesn't touch the tab. - **The diff's own file list starts hidden while the sidebar shows the Changes list**, and appears when the sidebar is closed or on another tab. Once the viewer toggles it, their choice holds while the diff is open. - **Closing the diff returns focus** to the row of the file being viewed, else to whatever opened the diff (such as a file link in the agent's reply), else to the sidebar toggle. `useSessionDiffSelection` owns the selection and this focus order. ## Review view - A top bar with **← Session**, the file count, and the **File list** toggle. - A selected-file header with the repository, full path, change summary, and previous/next with the file's position (`2 / 4`). - **Split depends on the code column's own width** (at least 640px), not the whole panel. Falling back to Unified doesn't overwrite a saved Split preference. - Switching files scrolls the code back to the top. - "Compared with session start" moves to a footer, with the base → head SHAs. - **Phones** keep the full-screen diff; the file list starts collapsed, and picking a file closes it and returns focus to the diff. ## Renderer - 12px code on 20px lines, GitHub light/dark syntax themes, and code drawn on the app background with the app's added/removed/modified colors. - **The first diff opened after a page load could render blank in development**: mounting the renderer while the shared highlighter initialized left its shadow root empty under React Strict Mode. The renderer now loads the highlighter and review themes first and shows the raw patch in the meantime. The raw patch also stays readable if highlighting fails to load. Once the review themes are attached, later diffs mount highlighted immediately; the check uses `areThemesAttached` because the shared highlighter reports itself loaded before its themes attach. ## Testing - `npm test -w @open-inspect/web` (on current `main`, which includes ColeMurray#2168 and ColeMurray#2164): 256 files, 2,408 tests pass. - Diff selection tests: opening runs the Changes-tab switch but moving between files doesn't; focus returns to the current file's row, else the opener, else the fallback when the sidebar was hidden; rows inside the closed (inert) mobile sheet are skipped. - Layout tests: the diff takes the main column with the sidebar visible, and the workspace and sidebar stay mounted across opening and closing. - Diff panel tests: the file list's default with and without the sidebar, the viewer's toggle holding, Split availability by code-column width without overwriting the saved preference, patch loading/error/empty/stale states, non-renderable files not being fetched, previous/next bounds, Escape, the phone list closing with focus returned, and the list appearing once the sidebar stops showing the files. - Renderer tests: options and palette, raw patch until the highlighter and review themes load, a loaded highlighter still missing the themes, the failure fallback, and immediate rendering once the themes are attached. - `npm run typecheck -w @open-inspect/web`, ESLint, Prettier and `npm run build -w @open-inspect/web` pass. - Checked in a browser against sample data: desktop with the sidebar open and closed, focus after returning to the session, light and dark themes, and phone width. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * The Changes view now fills the main workspace when a diff is open, while the sidebar remains available. * Inspector tabs stay consistent across desktop, mobile, and session details views. * Diff panels include file details, change counts, and previous/next file navigation. * **Improvements** * Split diffs are available on wider screens; narrower layouts use unified diffs. * Diff rendering uses light and dark themes, with readable raw-diff content shown while styling loads or if it fails. * Closing a diff restores focus to an appropriate file or control. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…leMurray#2163) ## Summary Fixes COL-252. - Add a small test-only helper module for `Sandbox.exec`, `Sandbox.create`, and `Sandbox.snapshot_filesystem`, using request messages from the installed `modal_proto` package and the SDK's own CPU/memory resource converter. - Bind create and exec arguments to the installed SDK signatures before serialization, rejecting unknown keywords even when SDK methods are mocked. - Apply the helpers to recorded fake arguments at all six call sites in snapshot management, session launch, access-password recovery, and image-build sessions. - Extend existing test matrices for timeout budgets, fractional preparation time, default/custom resources, image sources, tunnel-port filtering, and enabled access services. - Add direct helper tests covering float rejection for integer timeout fields, resource serialization, and float acceptance for snapshot timeouts. No production code, dependency pins, migrations, or persisted fields changed. The already-merged snapshot hotfix remains intact. ## SDK Detail The installed Modal 1.4.3 protobuf declares `SandboxSnapshotFsRequest.timeout` as `TYPE_FLOAT`. The legacy snapshot path therefore accepts fractional timeouts too, while the command-router path converts its separately supplied timeout with `float(timeout)`. The helpers follow those actual SDK contracts rather than impose an integer-only snapshot check. Each recorded snapshot timeout is checked against both internal contracts in one helper call, without a synthetic provider-mode flag or duplicate call-site tests. ## Verification From `packages/modal-infra`: - `uv run pytest tests/ -q`: 489 passed after removing redundant snapshot-mode cases. - `uv run ruff check`: passed. - `uv run ruff format --check`: passed. - `git diff --check`: passed. Temporary mutation checks both failed at the helper with `TypeError: 'float' object cannot be interpreted as an integer`: - Removed `int()` from the Docker-preparation exec timeout in `take_snapshot`. - Passed a deliberately float timeout through the existing session-create behavior test. Both mutations were removed before final validation and are not included in this PR. No live Modal calls were required. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/7900f609350eb4d024b8ab22b1c1beb0)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Expanded validation of sandbox creation, execution, resource settings, and filesystem snapshots across launch configurations. * Added coverage for default, integer, and fractional CPU settings; memory options; and execution and snapshot timeout boundaries. * Verified that launch requests preserve configured options and that access credentials are recovered only for enabled services. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…ray#2165) ## Summary - Make Better Auth the sole source of participant OAuth credentials. Remove copied-token fallback, local refresh/decryption, the unused legacy refresh helper, and participant token updater. - Separate a child participant's canonical credential identity from inherited session ownership. Unresolved prompt authors remain unresolved, while private children retain their owner and collaborators. - Enrich browser WebSocket joins/reconnects from the verified canonical GitHub account, and scheduled/Slack automation prompts from the actual execution author, preserving Git commit attribution. ## Test-first verification Added regression tests before production changes and observed failures for: - Stale, refresh-only, and unexpired copied credentials bypassing current Better Auth credentials; copied tokens also bypassing missing-grant and identity-integrity handling. - Private children substituting the parent's owner for unresolved reviewers, including missing SCM identity. - Actual browser WebSocket prompts dispatching `agent-only` instead of the linked user's Git identity on join and reconnect. - Scheduled automation and Slack follow-up prompts omitting the execution author's GitHub attribution. All are now green. External GitHub publication/profile calls are mocked; the browser dispatch and child creation tests exercise real workerd/D1 paths. ## Review hardening - Require an explicit nullable participant credential identity; never infer it from session ownership. - Settle one attribution snapshot after invocation admission. Ambiguity or unavailable optional profiles omit attribution; storage/integrity errors fail without routing Slack replies into owner-authored new runs. - Obtain verified profiles when cached logins are missing, reject caller-supplied GitHub attribution, and replace stale SCM fields on unlink/relink. - Remove legacy OAuth token inputs and writes from init, WS minting, and participant persistence; retain historical readable columns only. - Treat supplied prompt enrichment as an authoritative snapshot. Successive Slack turns clear old attribution after unlinking, ambiguity, missing login, or optional grant failure. - Bound optional GitHub attribution for WS joins to five seconds, below the browser proxy deadline; delayed failures do not block minting or overwrite the empty snapshot later. Added regressions first and observed red for these failure paths before implementing fixes. All are now green. ## Validation - Shared build - Control-plane unit suite: **335 files, 5,559 tests passed** (`--maxWorkers=1`) - Targeted workerd integration/conformance suites: **11 files, 199 tests passed** (PR creation, WebSocket participants, child spawning/operations, scheduler, Slack events, invocation fan-out, session-core conformance, provider auth, internal routes, and prompt queueing) - Control-plane typechecks: Worker, Node, unit/conformance, and integration configurations - Control-plane Worker and Node builds - Changed-file ESLint, Prettier, and `git diff --check` ## Scope No schema migration, backfill, or production deployment. Historical SQLite credential fields remain readable but are ignored for authentication and are no longer written by participant producers. App fallback remains available when the prompting author genuinely has no usable Better Auth grant; credential identity mismatches remain fail-closed. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Session, automation, and Slack prompts can include verified GitHub identity details, such as login, name, email, and SCM user ID, when available. * Child sessions retain the prompt author’s canonical identity separately from session ownership. * Pull request creation uses current browser OAuth credentials across different cached-credential states. * Browser Git attribution is available after joining or reconnecting, and is omitted when an account is unlinked or lacks a login. * **Changes** * SCM tokens are no longer stored with participants or refreshed locally for pull request authorization. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…urray#2171) ## Summary - Replace the native session team and visibility dropdowns in the new-session composer with the existing shared Radix Select component. - Keep compact borderless triggers and open menus above the composer, consistent with adjacent controls. - Preserve Workspace-to-null team mapping, team visibility availability, defaults, and loading/creation disabled states. - Update regression tests to exercise the real custom menus, including returning to Workspace and disabled/required-team behavior. ## Validation - Home/sidebar tests: 10 files, 96 tests passed (`npm test -w @open-inspect/web -- 'src/app/(app)/(sidebar)/' --maxWorkers=1`). - Web typecheck passed (`npm run typecheck -w @open-inspect/web`). - Targeted ESLint, Prettier, and `git diff --check` passed. - Browser-verified both menus at desktop 1440x900 and mobile 390x844 using mocked API responses. Checked keyboard selection back to Workspace and no mobile horizontal overflow. ## Visual Evidence Viewport screenshots from `http://localhost:3000` are uploaded to the session: - Desktop team menu: `1aa99eb4907cf6204641825aca797958` - Desktop visibility menu: `5b254d0fc3767825e8c9af721d60741e` - Mobile visibility menu: `5388f1d6ae602bc7fc52329787bb32ac` - Mobile team menu: `22af0c5baddd485d346e6605b5c1df7e` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/d10f14fdafa21d245dc763be440929f9)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added session access controls for choosing Private, Team, or Workspace visibility and selecting a team. * Team selection supports a “No team” option when allowed; selecting Team visibility requires a team. * **Improvements** * Changing teams preserves the draft’s visibility. Workspace visibility clears the active team. * Access controls are disabled while teams are loading, and keyboard navigation is supported. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary - Adopt the selected inline visibility layout: label and shared component dropdown on one row, with the child-session checkbox and a compact ghost-style Save action below. - Replace the native select with the existing Radix-based Select component. - Preserve explicit confirmation, unavailable visibility options, owner-membership warnings, pending states, and retry-without-children behavior. - Keep the implementation in the existing control; remove all temporary design variations and preview code. ## Verification - `npm test -w @open-inspect/web -- src/components/session-controls.test.tsx src/components/session-right-sidebar.test.tsx --maxWorkers=1` (74 tests passed) - `npm run typecheck -w @open-inspect/web` - ESLint and Prettier checks on all changed files - `git diff --check` - Browser verification of the actual control in a temporary development harness: keyboard dropdown selection, disabled unavailable options, and no horizontal overflow at 390x844 and 1440x960. The harness is not included in this PR. ## Regression Coverage - Selecting visibility or toggling the child-session checkbox does not mutate permissions before Save. - All controls remain disabled until the updated session snapshot finishes refreshing, preventing duplicate requests. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/872736fed342173cfd8b31e15f5d08af)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **User Interface** * Replaced the visibility dropdown with a shared styled selector and reorganized the visibility and save controls. The save button is now labeled “Save.” * Pressing Escape closes the visibility dropdown first; pressing it again closes the dialog. If another action has already prevented Escape, the dialog stays open. * Visibility options and controls remain unavailable when required capabilities or team context are missing. When Escape closes the dialog, focus returns to its previous location. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…ies (ColeMurray#2174) ## Summary Closes [COL-253](https://linear.app/colemurray/issue/COL-253/teams-follow-up-control-plane-stop-auditing-allowed-team-reads). - Default `requireTeam` allowed auditing to false for `read` and `member`, with an optional override. Preserve allowed auditing for capability needs and explicitly for membership deletion. - Disable allowed-decision auditing for team-session and collaborator-candidate reads while retaining their existing authorization requirements. Directory, member, and activity reads inherit the quiet defaults. - Resolve memberships at WebSocket subscribe in every enforcement mode so non-owner team leads retain move and visibility capabilities. Collaborator reads and per-command scope-read short-circuiting remain unchanged. - Add request-correlated audit regression tests, all-mode subscribe capability/query-count coverage, and route-policy assertions. Update the catalog and admission-matrix snapshots and operator changelog. - Narrow the existing `requireSession` and `requireAll` return annotations to their actual `active-user` variant, permitting typed policy overrides without casts or runtime changes. No migration, access-decision change, credential expansion, new route, or response-schema change. The route catalog remains 202 routes / 154 paths. ## Checkpoint Report ### 1. Commands And Results Heavy validation ran sequentially with one Vitest worker and 600000 ms shell timeouts. | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed before dependent checks. | | `npm test -w @open-inspect/control-plane -- src/router.policy.test.ts src/session/connection-authenticator.test.ts --maxWorkers=1` | Red phase: 8 failed, 171 passed. After implementation: all 179 passed. | | `npm run test:integration -w @open-inspect/control-plane -- test/integration/teams-routes.test.ts test/integration/session-scope-routes.test.ts test/integration/websocket-session-access.test.ts --maxWorkers=1 -t 'does not audit an allowed team read\|audits an allowed team capability write\|still audits\|does not audit allowed collaborator-candidate\|loads memberships at subscribe\|sends move and visibility'` | Red phase: 10 failed, 4 passed, 80 skipped. Nine failures reproduced the requested defects; one exposed an incorrect initial expectation about an existing concealed-team 404, described below. | | `npm run test:integration -w @open-inspect/control-plane -- test/integration/teams-routes.test.ts test/integration/session-scope-routes.test.ts test/integration/websocket-session-access.test.ts test/integration/hono-route-catalog-conformance.test.ts test/integration/route-admission-matrix.test.ts --maxWorkers=1 --update --silent` | All 108 passed; catalog snapshot updated. | | `npm run test:integration -w @open-inspect/control-plane -- test/integration/route-admission-matrix.test.ts --maxWorkers=1 --update --silent` | All 13 passed; matrix snapshot updated to record audit policy alongside unchanged statuses. | | `npm run typecheck` | Initial run failed on overly broad helper return types; narrowed those annotations, then all workspaces passed. | | `npm run lint:fix` | Passed. | | `npm run lint:sql-portability` | Passed: clean, with 24 existing baselined occurrences across four files. | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1 --silent` | 338 files passed; 5,645 tests passed. | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1 --silent` | 127 files passed; 1,633 tests passed and one skipped. | | `npx prettier --write` on the changed TypeScript files and `CHANGELOG.md` | Passed; only touched files formatted. Commit hooks also passed ESLint and Prettier. | | `npm run test:integration -w @open-inspect/control-plane -- test/integration/teams-routes.test.ts test/integration/session-scope-routes.test.ts test/integration/websocket-session-access.test.ts --maxWorkers=1 --silent` | Final post-format check: all 94 passed. | | `git diff --check` | Passed. | <details> <summary>Failure excerpts, verbatim</summary> Unit red-phase assertions: ```text - "auditAllowed": false, + "auditAllowed": true, AssertionError: expected 1st "vi.fn()" call to have been called with [ 'member-user', …(1) ] - Expected + Received [ "member-user", - { - "includeMemberships": true, - }, ] Test Files 2 failed (2) Tests 8 failed | 171 passed (179) ``` Integration red-phase assertions: ```text AssertionError: expected [ Array(1) ] to deeply equal [] - Expected + Received - [] + [ + { + "action": "authorization.request_allowed", + }, + ] AssertionError: expected "listForUser" to be called 1 times, but got 0 times - Expected + Received { "session": { "capabilities": { - "canChangeVisibility": true, + "canChangeVisibility": false, "canManageCollaborators": false, - "canMove": true, + "canMove": false, }, }, } AssertionError: expected [] to deeply equal [ Array(1) ] - Expected + Received - [ - { - "action": "authorization.request_denied", - }, - ] + [] Test Files 3 failed (3) Tests 10 failed | 4 passed | 80 skipped (94) ``` Initial typecheck failure: ```text src/routes/session-scope.ts(313,5): error TS2322: Type '{ auditAllowed: false; kind: "none"; } | { auditAllowed: false; kind: "authenticated"; } | { auditAllowed: false; kind: "active-self"; } | { auditAllowed: false; kind: "active-global"; service: ServiceAuthorization; } | { ...; } | { ...; }' is not assignable to type 'RouteAuthorization'. Type '{ auditAllowed: false; kind: "service"; services: readonly BotServiceName[]; actor: "required" | "optional"; }' is not assignable to type 'RouteAuthorization'. Type '{ auditAllowed: false; kind: "service"; services: readonly BotServiceName[]; actor: "required" | "optional"; }' is not assignable to type '{ kind: "none"; auditAllowed: false; } | { kind: "authenticated"; auditAllowed: false; } | { kind: "active-self"; auditAllowed: boolean; } | { kind: "active-global"; service: ServiceAuthorization; auditAllowed: boolean; } | { ...; }'. Type '{ auditAllowed: false; kind: "service"; services: readonly BotServiceName[]; actor: "optional" | "required"; }' is missing the following properties from type '{ kind: "active-user"; allOf: readonly RouteAuthorizationRequirement[]; service: ServiceAuthorization; auditAllowed: boolean; }': allOf, service src/routes/teams.ts(517,7): error TS2322: Type '{ service: { kind: "deny"; }; auditAllowed: false; kind: "none"; } | { service: { kind: "deny"; }; auditAllowed: false; kind: "authenticated"; } | { service: { kind: "deny"; }; auditAllowed: false; kind: "active-self"; } | { ...; } | { ...; } | { ...; }' is not assignable to type 'RouteAuthorization'. Object literal may only specify known properties, and 'service' does not exist in type '{ kind: "none"; auditAllowed: false; }'. ``` </details> The full integration run emitted workerd diagnostics from deliberate eviction/error-path tests and NDJSON warnings, but exited successfully with the totals above. ### 2. Verified Facts And Drift Branched from `main` at `3b0b575`; fetched `origin/main` again before commit, with no newer base changes and no repository-grant routes merged. - Confirmed the reported causes at `packages/control-plane/src/routes/shared.ts:187-196`, `routes/teams.ts:496-520`, `routes/session-scope.ts:313-316`, `session/components.ts:847-862`, and `session/connection-authenticator.ts:385,450-458` on the base commit. - Confirmed migration `0083_teams.sql`, DO schema migration 56, and the existing route counts. This PR leaves them unchanged. - Confirmed that `off` and `shadow` use legacy non-private WebSocket access decisions, while `on` uses scoped decisions; private access remains scoped in every mode. Only subscribe capability enrichment changes. - One existing audit gap differs from the blanket denial-auditing premise: `packages/control-plane/src/routing/route-admission.ts:626` returns a concealed-team 404 without authorization-decision evidence. This PR does not alter that admission path. The denial regression instead checks the existing explicit team-capability 403, and the collaborator-candidate regression checks its action-denied 403; both retain audit rows. ### 3. Deliberately Left Out - Repository-grant routes and later team work: outside this fix; existing team routes only. - The pre-existing concealed-team 404 audit gap: separate admission behavior, documented above rather than widening this patch. - Per-command membership/collaborator enrichment in `off` and `shadow`: intentionally stays short-circuited, with query-count regressions. - Web, bot, sandbox-runtime, and schema changes: unnecessary for these control-plane fixes. All-workspace typechecking still passed. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/d5ae386788e57ec379680339c4da0129)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Permitted reads of team directories, members, sessions, activity, and collaborator candidates no longer generate authorization audit entries. Denied requests remain auditable, as do capability changes and team departures. * Team leads retain their move and visibility controls in live sessions across all enforcement modes. * Members cannot remove other members unless they have member-management permissions; denied removal attempts are audited. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary Fixes https://linear.app/colemurray/issue/COL-254 - Default navigation to **All my teams**, preserve explicit Workspace/team choices, and show the switcher for a single active membership. Users without active teams keep unfiltered lists. - Keep composer team and visibility draft-local, including required-team preselection and reconciliation when a local team becomes unavailable. Composer changes never update sidebar context. - Retain the last successful membership response after revalidation failures. First-load failures still block readiness. - Retire superseded warm drafts without eagerly creating replacements on configuration changes; warming happens on first input or submit. - Clear/revalidate list, team, activity, audit, and infinite-list caches after scope mutations. Revalidate memberships without clearing them; leave sandbox access and other per-session resources intact. Snapshots still refresh explicitly. - Disable unchanged visibility mutations and require confirmation before non-private child cascades. Populate audit filters from the all-team directory. - Update authorization documentation and the changelog. ## Checkpoint Report ### Commands and Results | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run typecheck` | Passed across all workspaces | | `npm run lint:fix` | Passed | | `npm run lint:sql-portability` | Passed; 24 existing baselined occurrences across 4 files | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1` | 338 files passed; 5,642 tests passed | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1` | 127 files passed; 1,622 tests passed, 1 skipped | | `npm test -w @open-inspect/web -- --maxWorkers=1` | Final run: 257 files passed; 2,497 tests passed | | `git diff --name-only -z \| xargs -0 npx prettier --check` | Passed | | `git diff --check origin/main...HEAD` | Passed | Targeted validation also passed: ```bash npm test -w @open-inspect/web -- src/hooks/use-active-team.test.tsx src/hooks/use-teams.test.tsx src/components/team-switcher.test.tsx 'src/app/(app)/(sidebar)/page-team-context.test.tsx' 'src/app/(app)/(sidebar)/page.test.tsx' src/lib/session-scope.test.ts src/lib/session-scope-refresh.test.tsx src/components/session-controls.test.tsx src/components/settings/audit-log-settings.test.tsx --maxWorkers=1 ``` Result: 9 files passed; 242 tests passed. After the additional unavailable-local-team regression and disabled-directory test, the focused three-file run passed all 49 tests: ```bash npm test -w @open-inspect/web -- 'src/app/(app)/(sidebar)/page-team-context.test.tsx' src/hooks/use-active-team.test.tsx src/hooks/use-teams.test.tsx --maxWorkers=1 ``` Expected red runs before implementing the context and composer changes: ```bash npm test -w @open-inspect/web -- src/hooks/use-active-team.test.tsx src/components/team-switcher.test.tsx --maxWorkers=1 ``` ```text Test Files 2 failed (2) Tests 14 failed | 14 passed (28) ``` Representative failure: ```text AssertionError: expected "vi.fn()" to be called with arguments: [ '/api/sessions/inbox' ] Received: 1st vi.fn() call: [ - "/api/sessions/inbox", + "/api/sessions/inbox?scope=workspace", ] Number of calls: 1 ``` ```bash npm test -w @open-inspect/web -- 'src/app/(app)/(sidebar)/page-team-context.test.tsx' --maxWorkers=1 ``` ```text Test Files 1 failed (1) Tests 8 failed | 6 passed (14) ``` Representative eager-warm failure: ```text AssertionError: expected [ [ '/api/sessions', …(1) ], …(1) ] to have a length of 1 but got 2 - Expected + Received - 1 + 2 ``` The first full web run caught a sequencing mistake in the new unavailable-local-team test, not an implementation failure. The helper clicked a popover trigger while the popover was already open. Closing it before invoking the helper fixed the test; the subsequent focused and full runs passed. ```text FAIL src/app/(app)/(sidebar)/page-team-context.test.tsx > Home team context > falls back locally when the composer's team is no longer an active membership TestingLibraryElementError: Unable to find an accessible element with the role "radio" and name "Engineering team" Test Files 1 failed | 256 passed (257) Tests 1 failed | 2496 passed (2497) ``` Browser verification used the actual local Next.js application with intercepted fixture API responses at `http://localhost:3000`, at desktop 1440x1000 and mobile 390x844. Verified the single-team selector, team-owned recent session, required-team local preselection, private composer visibility, and model changes producing one archive and no eager replacement. Viewport screenshots were captured and uploaded. Real SWR tests additionally verify that a terminal-like child remains mounted while membership refresh is pending or fails, and that per-session resources do not refetch. ### Baseline Facts and Drift Based on `main` at `3b0b575`; `origin/main` was checked again before committing and had not advanced. Confirmed baseline behavior at `packages/web/src/hooks/use-active-team.ts:36-60`, `components/team-switcher.tsx:14-18`, `hooks/use-teams.ts:71-81`, `app/(app)/(sidebar)/page.tsx:109-144,294-297`, and `lib/session-scope.ts:23-35,89-105`. The requested failure modes were present. The composer UI had advanced in ColeMurray#2171: it now uses `SessionAccessSelector` and shared dropdowns instead of the earlier standalone select. This PR preserves that UI and changes its state wiring and regression tests. The existing `/api/teams` proxy already requests `/teams?membership=all&includeArchived=true`, so the audit viewer reuses `useTeams` with an enabled flag rather than adding another API path. Directory, activity, and collaborator documentation was verified against existing server routes. Latest schema remains D1 `0083_teams.sql` and DO migration 56. No migration is added. Enforcement configuration and all access seams are unchanged; the web fixes work with `off`, `shadow`, and `on`. ### Deliberately Excluded Repository grants, target-picker and warm-draft hook internals, team-page changes, server authorization, schema, credential scope, email-address display policy, and enforcement defaults are untouched. These fixes use existing capabilities and APIs. Live remote sandboxes and terminal connections were not exercised by browser fixtures; cache continuity is covered by real SWR regression tests. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/a8b1f561a828e5bc9e277eea2e3b78fd)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * The team selector is available when you have a single active team. * Choose a team for a new session without changing the active sidebar team. * Choose a team and visibility for a new session without changing your active team selection. * **Bug Fixes** * Changing visibility for child sessions now requires confirmation when the change would make them non-private; changes to private visibility apply without confirmation. * Team and session lists refresh more reliably after access or scope changes, while active session tools remain available during membership refreshes. * Temporary membership refresh failures no longer clear previously loaded team data. * Audit logs can be filtered by teams beyond your own memberships, including archived teams. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…oleMurray#2070) ## Summary - add a root `overrides` entry pinning `sharp` to `^0.35.4`, alongside the existing `minimatch` and `undici` overrides - `miniflare` (via `wrangler`) pins `"sharp": "0.34.5"` exactly, so the hoisted `node_modules/sharp` was 0.34.5 while `next` already carried its own nested `sharp@0.35.4` - with the override, both consumers dedupe to a single hoisted `sharp@0.35.4` (bundled libvips 8.18.6), and the nested `next/node_modules/sharp` tree is removed - resolves two high-severity advisories against the 0.34.5 copy: - [GHSA-f88m-g3jw-g9cj](GHSA-f88m-g3jw-g9cj): inherited libvips vulnerabilities (CVE-2026-33327, CVE-2026-33328, CVE-2026-35590, CVE-2026-35591), patched in 0.35.0 - [GHSA-rgj7-g3m4-5g8c](GHSA-rgj7-g3m4-5g8c): libheif vulnerabilities, patched in 0.35.4 ## Why an override instead of a wrangler bump The newest `wrangler` (4.140.0) depends on `miniflare@5.20260923.0-alpha`, so a wrangler bump would pull an alpha miniflare into the local dev and integration-test runtime. The override leaves `wrangler`/`miniflare` where they are and only moves the image library. miniflare lazily imports `sharp` only to emulate the Cloudflare Images binding locally, and no worker in this repo configures that binding. ## Lockfile The lockfile was regenerated with npm 11.10.0 so no unrelated entries change. A semantic comparison of `packages` shows only `sharp`, `sharp/node_modules/semver`, and `@img/*` entries changed, plus removal of the now-duplicate `next/node_modules/sharp`, `next/node_modules/@img/*`, `next/node_modules/semver`, and unused `@img/sharp-wasm32` entries. ## Verification - `npm ci` - `npm ls sharp` resolves a single `sharp@0.35.4` for both `next` and `miniflare` (deduped) - `require('sharp')` loads the native binding (vips 8.18.6) and encodes a PNG - `npm run build -w @open-inspect/shared` - `npm test -w @open-inspect/web` (212 files / 1903 tests) - `npm run test:integration -w @open-inspect/control-plane` (runs in workerd via miniflare) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated an internal package version override. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
) **Summary** - Include team-directory and collaborator-candidate emails only for viewers with `workspace.members.read`. Restricted SQL projections return `NULL AS email` and do not sort by email. - Render names and avatars, with neutral short-ID labels for unnamed users. Both identity pickers share trimmed-name / response-authorized-email / neutral-fallback typeahead. - Remove the member-facing team Activity tab and `GET /teams/:id/activity`, including the web proxy, component, response-schema aliases, and activity-only audit-store/hook options. - Keep all team/session audit writes, stored audit history, and the workspace audit API/UI behind `workspace.audit.read`, including team filtering. New membership audits contain membership fields rather than response profile fields. - Keep privacy route tests focused and update the canonical route catalog to 201 routes / 153 paths. Update auth documentation and add a removal changelog entry. Fixes [COL-255](https://linear.app/colemurray/issue/COL-255). **Scope Decision** The generic team audit feed was removed because its product purpose is unclear and exposing raw audit metadata to ordinary team members creates an unnecessary privacy boundary. Audit events remain recorded and available through the permission-gated workspace audit viewer. The removed endpoint returns 404 for Members, Administrators, and Owners in every enforcement mode. No audit history is deleted, and no migration is required. **Checkpoint Report** **Validation** | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run typecheck` | Passed across all workspaces | | `npm run lint:fix` | Passed | | `npm run lint:sql-portability` | Clean; existing baseline unchanged | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1` | 339 files, 5,651 tests passed | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1` | 128 files passed; 1,646 tests passed, 1 skipped | | `npm test -w @open-inspect/web -- --maxWorkers=1` | 257 files, 2,576 tests passed | | `npx prettier --check` on changed source/test/docs files | Passed | | `git diff --check` and staged diff checks | Passed | Heavy suites ran sequentially with 600000 ms timeouts. The route-catalog/admission snapshots were regenerated after removing exactly one route; the manifest fixture indices shift accordingly. Regression tests were added before the removal. Initial targeted results were: ```text Test Files 1 failed (1) Tests 1 failed | 136 skipped (137) ``` The policy test found the still-registered activity route. ```text Test Files 1 failed (1) Tests 3 failed | 12 skipped (15) AssertionError: expected 200 to be 404 // Object.is equality ``` The endpoint-absence checks failed in `off`, `shadow`, and `on` before implementation. ```text Test Files 1 failed (1) Tests 3 failed | 17 skipped (20) ``` The new UI assertions found the Activity tab for a team member, Administrator, and Owner before removal. A preservation test initially counted an extra default-role audit event generated when its fixture inserted a user: ```text Test Files 1 failed | 5 passed (6) Tests 1 failed | 85 passed (86) ``` The test query was scoped to its team so it verifies retained HTTP/private-session/history evidence and team filtering without counting unrelated workspace bootstrap events. The final focused integration run passed all 86 tests; the complete suites above also passed. **Visual Verification** Verified real TeamPage, Members, Overview, Settings, and workspace Audit log components in Chromium fixtures at desktop 1280x900 and mobile 390x844. Confirmed no Activity tab or team-activity requests, permission-appropriate remaining tabs/actions, redacted member emails, workspace audit permission/team filtering, and no horizontal overflow. Screenshots were uploaded. These checks used synthetic auth/data and mocked APIs in an isolated Vite harness, not a production-authenticated end-to-end test. **Facts And Exclusions** The branch includes the team audit-admission and web team-context updates merged from `main`, with conflict resolutions preserving both changes. Removing activity intentionally changes the catalog from 202 routes / 154 paths to 201 / 153. Latest schema assumptions remain D1 migration 0083 and DO migration 56; neither was changed. The new upstream dependency-only commit does not conflict with this branch. No new write paths, credential changes, repository-grant work, session creation changes, database backfill, or migrations. Team sessions, generic session/autofix activity, audit operation names, workspace audit team filtering, and all stored audit history remain. Historical changelog entries are retained, with a new entry documenting the removal. --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…Murray#2184) ## Summary - Require current owning-team membership for every user action except `read` on a team-owned session, including for workspace Owners and Administrators. Enforce the full action resolver in `off`, `shadow`, and `on` through one shared rollout predicate across HTTP admission, WebSocket commands, snapshot capabilities, and sandbox URL redaction. - Preserve existing visibility reads, workspace-owned behavior, private break-glass auditing, and read-authorized collaborator self-removal. - Remove session ownership moves end to end: control-plane route and store methods, web BFF/dialog/buttons, and session/automation/environment `move` actions and `canMove` capabilities. Retain historical `session.moved` audit registration and its viewer label, but remove it from the session audit write union. - Update authorization documentation, the changelog, fixtures, route counts, and catalog/admission snapshots. Visibility changes and their cascade remain intact. This closes the interaction boundary that could expose a team's sandbox credentials to nonmembers, and removes ownership changes that could leave a running sandbox carrying credentials from its previous team. Issue: [COL-257](https://linear.app/colemurray/issue/COL-257/teams-follow-up-shared-control-plane-web-only-team-members-act-on-team) ## Checkpoint Report ### Commands and Results Final validation was run sequentially after rebasing onto `main` at `3789205`: | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run typecheck` | Passed across all TypeScript workspaces, including control-plane unit/integration configurations | | `npm run lint:fix` | Passed | | `npm run lint:sql-portability` | Passed; no new violations | | `npm test -w @open-inspect/shared -- --maxWorkers=1 --silent` | 64 files passed; 1,079 tests passed | | `npm test -w @open-inspect/control-plane -- --maxWorkers=1 --silent` | 339 files passed; 5,690 tests passed | | `npm run test:integration -w @open-inspect/control-plane -- --maxWorkers=1 --silent` | 128 files passed; 1,644 tests passed, 1 skipped | | `npm test -w @open-inspect/web -- --maxWorkers=1 --silent` | 256 files passed; 2,545 tests passed | | `git diff --check origin/main...HEAD` | Passed | The integration suite prints its fault-injection diagnostics for forced DO eviction and malformed D1 input, plus NDJSON body warnings. The web suite prints a timer warning and jsdom navigation diagnostic. All final commands exited successfully. Regression coverage includes all session actions/visibilities/roles in the pure resolver; HTTP prompt and sandbox admission for nonmember Members, Administrators, Owners, and session creators; WebSocket prompt/typing before membership, after joining, and after removal; private removed owners; collaborator self-removal; false capabilities and redacted sandbox URLs in every mode; and 404 for the deleted scope route. Additional development commands: - `npm test -w @open-inspect/shared -- src/types/session-access.test.ts --maxWorkers=1`: red, then green with all 32 tests passing before runtime wiring. - `npm test -w @open-inspect/control-plane -- src/authorization/teams-enforcement.test.ts src/session/connection-authenticator.test.ts src/routes/session-runtime-proxy.test.ts --maxWorkers=1`: red before wiring; one subsequent fixture failure; then green with all 131 tests passing. - `npm run test:integration -w @open-inspect/control-plane -- test/integration/session-access-routes.test.ts test/integration/websocket-session-access.test.ts test/integration/session-scope-routes.test.ts --maxWorkers=1`: six fixture/expectation failures, corrected before the full green run. - `npm test -w @open-inspect/control-plane -- --maxWorkers=1`: initially 26 failures in the child-spawn suite because its partial mocked workspace row omitted `ownerTeamId` and `visibility`. The fixture now supplies those fields; no production child-creation path changed. - `npm test -w @open-inspect/control-plane -- src/router.spawn-child.test.ts --maxWorkers=1 --silent`: passed after correcting that fixture. - `npm run test:integration -w @open-inspect/control-plane -- test/integration/session-access-routes.test.ts test/integration/websocket-session-access.test.ts test/integration/session-scope-routes.test.ts test/integration/teams-routes.test.ts test/integration/hono-route-catalog-conformance.test.ts test/integration/route-admission-matrix.test.ts --maxWorkers=1 --silent -u`: wrote the catalog update; three sandbox-readiness fixture failures, then corrected by seeding a ready test sandbox and checking sandbox access before prompting. - `npm run test:integration -w @open-inspect/control-plane -- test/integration/session-access-routes.test.ts --maxWorkers=1 --silent`: all 28 tests passed. - `npm run test:integration -w @open-inspect/control-plane -- test/integration/hono-route-catalog-conformance.test.ts test/integration/route-admission-matrix.test.ts --maxWorkers=1 --silent -u`: all 15 tests passed and regenerated the catalog after rebase. - Changed TypeScript/Markdown files were formatted with Prettier; commit hooks passed. <details> <summary>Development failures, verbatim excerpts</summary> Pure resolver red run: ```text Test Files 1 failed (1) Tests 7 failed | 25 passed (32) AssertionError: team, owner, owner, suspended=false, move: expected { allowed: true } to deeply equal { allowed: false, …(1) } AssertionError: expected { allowed: true } to deeply equal { Object (allowed, reason) } - Expected + Received { - "allowed": false, - "reason": "not_member", + "allowed": true, } AssertionError: expected { canRead: false, …(3) } to deeply equal { canRead: false, …(2) } - Expected + Received { "canManage": true, + "canMove": true, "canRead": false, "canTrigger": true, } ``` Seam red run: ```text Test Files 3 failed (3) Tests 21 failed | 110 passed (131) TypeError: resolverDecides is not a function AssertionError: expected { id: 'session-1', …(20) } to not have property "codeServerUrl" - Expected: undefined + Received: "https://code.example" AssertionError: expected 200 to be 403 // Object.is equality - Expected + Received - 403 + 200 AssertionError: expected { kind: 'allowed' } to deeply equal { Object (kind, reason) } - Expected + Received { - "kind": "denied", - "reason": "not_member", + "kind": "allowed", } ``` Post-wiring partial workspace fixture: ```text FAIL src/routes/session-runtime-proxy.test.ts > session runtime proxy routes > budget updates > forwards budget updates from the session owner AssertionError: expected 403 to be 200 // Object.is equality - Expected + Received - 200 + 403 Test Files 1 failed | 2 passed (3) Tests 1 failed | 130 passed (131) ``` Initial integration fixture/expectation failures: ```text Test Files 2 failed | 1 passed (3) Tests 6 failed | 57 passed (63) AssertionError: expected { …(9) } to match object { httpStatus: 403, …(1) } (7 matching properties omitted from actual) - Expected + Received { "httpStatus": 403, - "responseCode": "session_action_denied", + "responseCode": "not_member", } TypeError: Cannot read properties of undefined (reading 'id') AssertionError: expected { session: { …(26) }, …(5) } to match object { session: { capabilities: { …(3) } } } (44 matching properties omitted from actual) - Expected + Received { "session": { "capabilities": { "canChangeVisibility": false, - "canCollaborate": true, + "canCollaborate": false, "canRead": true, }, }, } ``` Full unit run before completing the child-spawn fixture: ```text FAIL src/router.spawn-child.test.ts > handleSpawnChild prompt enqueue handling > copies the exact parent provider auth snapshot with immediate inheritance AssertionError: expected 403 to be 201 // Object.is equality - Expected + Received - 201 + 403 Test Files 1 failed | 338 passed (339) Tests 26 failed | 5664 passed (5690) ``` Sandbox-readiness fixture: ```text AssertionError: expected 409 to be 200 // Object.is equality - Expected + Received - 200 + 409 Snapshots 1 updated Test Files 1 failed | 5 passed (6) Tests 3 failed | 133 passed (136) ``` </details> ### Baseline Facts and Drift - Initial clean `main` was `70b8ca4`. The resolver's participant rules at `packages/shared/src/types/session-access.ts:164-181`, HTTP rollout checks at `authorization/session-admission.ts:54,87,119`, WebSocket check at `session/connection-authenticator.ts:559`, membership loading at `session/components.ts:856`, and snapshot redaction at `routes/session-runtime-proxy.ts:202-205` matched the described interaction gap. - D1 migration `0083` and DO migration `56` remain unchanged. No migration, table, column, session-creation path, or token-mint change was needed. The unset enforcement default remains `shadow`. - During validation, `main` advanced to `3789205` with directory-email privacy changes and removal of the team Activity route. Rebased onto it and retained those changes, including `listCollaboratorCandidates` filtering and its updated web tests. Resolved the shared test-import conflict without restoring move tests, and regenerated the route catalog rather than preserving stale indexed captures. - Consequently the base has 201 routes / 153 paths, rather than the original 202 / 154. This PR removes exactly one route and one path: final counts are 200 / 152. Admission snapshot changes remove only the four scope-route entries. - The changelog did not have an Unreleased section. Added the operator-visible behavior change there; dated entries remain historical. ### Deliberately Left Out - No grants, token narrowing, team secrets, automation/environment scope routes, or unrelated ownership redesign. Only their existing move decision/capability fields are removed. - No migration or retroactive ownership rewrite. Historical independently scoped-child guard tests use direct SQL fixtures rather than a deleted move store method. - No production authentication bypass or permanent UI harness. Browser verification used the actual action components and application CSS in a temporary local fixture because no authenticated local backend/browser session was configured. ## Visual Verification Viewport screenshots at `http://127.0.0.1:4173` show the real components in the temporary verification fixture, not an authenticated session page: - Desktop, 1512x982: Copy link remains in the menu and Archive remains available; no move control. Uploaded artifact `109636aa4532b32fee3cd2db5a27f1b7`. - Mobile, 390x844: Details, Copy link, and Archive remain; no move control. Uploaded artifact `84470d42cbff051bf94fd83292909452`. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/c6dcc77c3bc65f8c373822e9b0f17043)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Changed** * Team-owned sessions now require current membership in the owning team for actions beyond reading, including for Owners and Administrators. Collaborators can still remove themselves with read access. * Sessions remain tied to their existing team or workspace; changing their ownership scope is no longer available. * Team visibility continues to control read access, and historical session-move audit records remain readable. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…up across 1 directory (ColeMurray#2182) Bumps the npm_and_yarn group with 1 update in the / directory: [hono](https://github.com/honojs/hono). Updates `hono` from 4.13.5 to 4.13.12 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/honojs/hono/releases">hono's releases</a>.</em></p> <blockquote> <h2>v4.13.12</h2> <h2>What's Changed</h2> <ul> <li>fix(build): keep internal types private in bundled d.ts and avoid a self-referencing JSX.IntrinsicElements in <a href="https://redirect.github.com/honojs/hono/pull/5485">honojs/hono#5485</a></li> <li>test(build): type-check the bundled declarations from a consumer project in <a href="https://redirect.github.com/honojs/hono/pull/5486">honojs/hono#5486</a></li> <li>fix(etag): correctly match mixed-case header name in retainedHeader option in <a href="https://redirect.github.com/honojs/hono/pull/5475">honojs/hono#5475</a></li> <li>fix(jsx): add px to numeric gridGap, gridRowGap and gridColumnGap in <a href="https://redirect.github.com/honojs/hono/pull/5487">honojs/hono#5487</a></li> <li>fix(combine): return a Response from a short-circuiting middleware in some() in <a href="https://redirect.github.com/honojs/hono/pull/5391">honojs/hono#5391</a></li> <li>chore(deps): upgrade vite-plus to 1.0.0 in <a href="https://redirect.github.com/honojs/hono/pull/5464">honojs/hono#5464</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/honojs/hono/compare/v4.13.11...v4.13.12">https://github.com/honojs/hono/compare/v4.13.11...v4.13.12</a></p> <h2>v4.13.11</h2> <h2>Security fixes</h2> <h3><code>serveStatic</code> decodes the request path a second time, leading to bypass of middleware on static paths</h3> <p>Affects: <code>hono/serve-static</code> and the adapters built on it (<code>hono/bun</code>, <code>hono/deno</code>, <code>hono/cloudflare-workers</code>, <code>@hono/bun</code>, <code>@hono/deno</code>, <code>@hono/cloudflare-workers</code>). Fixes <code>serveStatic</code> decoding an already-decoded path, where a crafted request could be routed as one path and served as another, skipping middleware mounted on a static prefix. GHSA-5r4p-p66f-jhc7</p> <p><code>serveStatic</code> now rejects request paths that still contain <code>%</code> after decoding. To serve files whose names contain a literal <code>%</code>, set <code>allowPercentInPath: true</code>.</p> <p>The same fix ships in <code>@hono/node-server</code> v2.1.3.</p> <h2>v4.13.10</h2> <h2>Adapters are now separate packages</h2> <p>The runtime adapters are now published as their own packages: <code>@hono/bun</code>, <code>@hono/deno</code>, <code>@hono/cloudflare-workers</code>, <code>@hono/aws-lambda</code>, <code>@hono/lambda-edge</code>, <code>@hono/netlify</code>, <code>@hono/vercel</code>, and <code>@hono/service-worker</code>. <code>@hono/deno</code> is also on JSR.</p> <p><code>hono/<adapter></code> still works in v4 but is deprecated and will be removed in v5. Migrating is an import change:</p> <pre lang="diff"><code>- import { serveStatic } from 'hono/bun' + import { serveStatic } from '@hono/bun' </code></pre> <p>hono/cloudflare-pages is deprecated without a replacement package; Cloudflare recommends Workers with static assets.</p> <h2>What's Changed</h2> <ul> <li>chore: migrate the package manager from bun to pnpm in <a href="https://redirect.github.com/honojs/hono/pull/5433">honojs/hono#5433</a></li> <li>chore(package.json): invoke package scripts through pnpm instead of bun in <a href="https://redirect.github.com/honojs/hono/pull/5434">honojs/hono#5434</a></li> <li>chore: replace prettier with oxfmt in <a href="https://redirect.github.com/honojs/hono/pull/5435">honojs/hono#5435</a></li> <li>chore(deps): upgrade vitest to 5.0.1 in <a href="https://redirect.github.com/honojs/hono/pull/5437">honojs/hono#5437</a></li> <li>chore: let oxfmt sort imports instead of eslint in <a href="https://redirect.github.com/honojs/hono/pull/5442">honojs/hono#5442</a></li> <li>chore: replace eslint with oxlint in <a href="https://redirect.github.com/honojs/hono/pull/5443">honojs/hono#5443</a></li> <li>chore: introduce Vite+ in <a href="https://redirect.github.com/honojs/hono/pull/5444">honojs/hono#5444</a></li> <li>fix(types): allow returning a Blob as a response body in <a href="https://redirect.github.com/honojs/hono/pull/5446">honojs/hono#5446</a></li> <li>chore: convert build scripts into plugins in <a href="https://redirect.github.com/honojs/hono/pull/5448">honojs/hono#5448</a></li> <li>chore: stop editorconfig-checker from checking Markdown indent size in <a href="https://redirect.github.com/honojs/hono/pull/5455">honojs/hono#5455</a></li> <li>ci: remove empty step left in <code>cr.yml</code> by the pnpm migration in <a href="https://redirect.github.com/honojs/hono/pull/5456">honojs/hono#5456</a></li> <li>chore(deps): upgrade vite-plus to <code>1.0.0-rc.1</code> in <a href="https://redirect.github.com/honojs/hono/pull/5459">honojs/hono#5459</a></li> <li>feat(adapters): add <code>@hono/bun</code> as a workspace package in <a href="https://redirect.github.com/honojs/hono/pull/5447">honojs/hono#5447</a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/honojs/hono/commit/6abd35b0a5f35f67b6417627d5b0a6c2d266ac04"><code>6abd35b</code></a> 4.13.12</li> <li><a href="https://github.com/honojs/hono/commit/95eb860473d26bd303a2a29716e61a77c8c5e5f5"><code>95eb860</code></a> chore(deps): upgrade vite-plus to 1.0.0 (<a href="https://redirect.github.com/honojs/hono/issues/5464">#5464</a>)</li> <li><a href="https://github.com/honojs/hono/commit/afb2068c1c53a530528ac5f237e245793dd03c07"><code>afb2068</code></a> fix(combine): return a Response from a short-circuiting middleware in some() ...</li> <li><a href="https://github.com/honojs/hono/commit/e5bb2062a3ddd645e3a16d1300bd76d6c9af33a5"><code>e5bb206</code></a> fix(jsx): add px to numeric gridGap, gridRowGap and gridColumnGap (<a href="https://redirect.github.com/honojs/hono/issues/5487">#5487</a>)</li> <li><a href="https://github.com/honojs/hono/commit/c3053ccf14f1c4c70a0c2888eed6121b9f6c8f95"><code>c3053cc</code></a> fix(etag): correctly match mixed-case header name in retainedHeader option (#...</li> <li><a href="https://github.com/honojs/hono/commit/c437d7569219a21e45fa81797749e8f34fe19dcc"><code>c437d75</code></a> test(build): type-check the bundled declarations from a consumer project (<a href="https://redirect.github.com/honojs/hono/issues/5486">#5486</a>)</li> <li><a href="https://github.com/honojs/hono/commit/be1f7498fed269544a3c544db0a5a727048462a8"><code>be1f749</code></a> fix(build): keep internal types private in bundled d.ts and avoid a self-refe...</li> <li><a href="https://github.com/honojs/hono/commit/37ce06904e732d4bc11c9075adf362c76049a594"><code>37ce069</code></a> 4.13.11</li> <li><a href="https://github.com/honojs/hono/commit/1e1207cabfcd154146bf6ac9fd6a0b646f7fc484"><code>1e1207c</code></a> test(serve-static): fix the test (<a href="https://redirect.github.com/honojs/hono/issues/5479">#5479</a>)</li> <li><a href="https://github.com/honojs/hono/commit/8b05c774ef1555c70f5ba5e2991b7e77cc3635cc"><code>8b05c77</code></a> Merge commit from fork</li> <li>Additional commits viewable in <a href="https://github.com/honojs/hono/compare/v4.13.5...v4.13.12">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ColeMurray/background-agents/network/alerts). </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
**Summary**
Add team-scoped encrypted secrets for team-owned work, with precedence
`global -> team -> environment or repositories`. Workspace-owned
sessions retain their existing secret scopes.
- Add a Zod-validated `TeamSecretsStore` using the existing encryption
key, validation, and per-scope key cap.
- Add human-only team secret GET/PUT/DELETE routes authorized by
`requireTeam("canManageSecrets")` in every enforcement mode.
- Batch secret mutations with `team.secret_set` / `team.secret_deleted`
audit rows containing key names only.
- Resolve sandbox team secrets from current D1 session ownership and
retain the `team` source in the managed-provider filter.
- Include the environment owner's team secrets in environment builds,
never in repository-shared builds.
- Add a capability-gated Secrets tab, reuse the existing editor, and add
encoded browser proxy routes.
- Update audit labels, route policy counts/snapshots, and the operator
changelog.
Issue: https://linear.app/colemurray/issue/COL-207
**Checkpoint Report**
1. Commands and results
| Command | Result |
| --- | --- |
| `npm run build -w @open-inspect/shared` | Passed |
| `npm run typecheck` | Passed across every TypeScript workspace; rerun
after final editor changes |
| `npm run lint:fix` | Passed; rerun after final changes |
| `npm run lint:sql-portability` | Passed: `SQL portability: clean (24
baselined occurrence(s) across 4 file(s)).` |
| `npm test -w @open-inspect/control-plane -- --maxWorkers=1` | 338
files, 5,653 tests passed |
| `npm run test:integration -w @open-inspect/control-plane --
--maxWorkers=1` | 130 files passed; 1,669 tests passed, 1 skipped |
| `npm test -w @open-inspect/web -- --maxWorkers=1` | Final rerun: 260
files, 2,476 tests passed |
| `npm test -w @open-inspect/shared -- --maxWorkers=1
src/types/audit-events.test.ts` | 32 tests passed |
| `npm test -w @open-inspect/control-plane -- --maxWorkers=1
src/session/session-target-secrets.test.ts
src/session/user-env-resolver.test.ts src/image-builds/scope.test.ts` |
Red before resolution wiring: 6 failed, 60 passed. Green afterward: 66
passed |
| `npm run test:integration -w @open-inspect/control-plane --
--maxWorkers=1 test/integration/team-secrets.test.ts
test/integration/team-secrets-routes.test.ts
test/integration/team-secrets-store.test.ts` | Initial run: 9 failed, 38
passed. Six resolution failures before wiring, three invalid bot-actor
fixtures corrected |
| `npm run test:integration -w @open-inspect/control-plane --
--maxWorkers=1 test/integration/team-secrets.test.ts
test/integration/team-secrets-routes.test.ts
test/integration/team-secrets-store.test.ts
test/integration/hono-route-catalog-conformance.test.ts
test/integration/route-admission-matrix.test.ts` | 61 tests passed |
| `npm test -w @open-inspect/web -- --maxWorkers=1
src/components/teams/team-secrets.test.tsx
src/components/teams/teams-pages.test.tsx
src/components/settings/audit-log-settings.test.tsx
"src/app/api/teams/[id]/secrets/route.test.ts"
"src/app/api/teams/[id]/secrets/[key]/route.test.ts"` | Initial targeted
frontend run: 78 tests passed |
| `npm test -w @open-inspect/web -- --maxWorkers=1
src/components/teams/team-secrets.test.tsx` | Final targeted editor run:
13 tests passed, including a scope-switch mutation regression test |
| `git ls-files --modified --others --exclude-standard -z \| xargs -0
npx prettier --write --ignore-unknown` | Formatted intended changes |
| `git diff --check` and `git diff --check origin/main...HEAD` | Passed
|
Verbatim red unit assertion excerpts:
```text
FAIL src/image-builds/scope.test.ts > loadScopeBuildSecrets > loads the environment owner's team before environment secrets
AssertionError: expected "vi.fn()" to be called with arguments: [ 'team_build' ]
Number of calls: 0
FAIL src/session/session-target-secrets.test.ts > buildSessionTargetSecretSources > merges team after global and before target secrets for target null
AssertionError: expected [ 'global', 'acme/web' ] to deeply equal [ 'global', 'team', 'acme/web' ]
FAIL src/session/session-target-secrets.test.ts > buildSessionTargetSecretSources > merges team after global and before target secrets for target env_team
AssertionError: expected [ 'global', 'environment' ] to deeply equal [ 'global', 'team', 'environment' ]
FAIL src/session/session-target-secrets.test.ts > buildSessionTargetSecretSources > includes team secrets in an ad-hoc session without repositories
AssertionError: expected { SHARED: 'global' } to deeply equal { SHARED: 'team' }
FAIL src/session/user-env-resolver.test.ts > UserEnvResolver > session-target secret fold > includes team broker secrets from the current public D1 session ownership
AssertionError: expected { SHARED: 'global' } to deeply equal { SHARED: 'team', …(1) }
FAIL src/session/user-env-resolver.test.ts > UserEnvResolver > session-target secret fold > fails closed when the authoritative session index row is missing
AssertionError: promise resolved "undefined" instead of rejecting
Test Files 3 failed (3)
Tests 6 failed | 60 passed (66)
```
Verbatim initial integration assertion excerpts:
```text
FAIL test/integration/team-secrets-routes.test.ts > team secrets routes > denies members, nonmembers, bots and suspended leads in off mode
FAIL test/integration/team-secrets-routes.test.ts > team secrets routes > denies members, nonmembers, bots and suspended leads in shadow mode
FAIL test/integration/team-secrets-routes.test.ts > team secrets routes > denies members, nonmembers, bots and suspended leads in on mode
AssertionError: github-bot/slack:U-SECRET-ACTOR/GET: expected 401 to be 403 // Object.is equality
FAIL test/integration/team-secrets.test.ts > team secret resolution > gives environment precedence over team and global and retains team broker credentials
AssertionError: expected { SHARED: 'environment', …(2) } to deeply equal { SHARED: 'environment', …(4) }
FAIL test/integration/team-secrets.test.ts > team secret resolution > gives team precedence over global without letting member repositories change the broker
AssertionError: expected { SHARED: 'global', …(2) } to deeply equal { SHARED: 'team-a', …(4) }
FAIL test/integration/team-secrets.test.ts > team secret resolution > keeps primary repository precedence over team secrets
AssertionError: expected 'global' to be 'team-a' // Object.is equality
FAIL test/integration/team-secrets.test.ts > team secret resolution > never injects another team's secrets or any team secrets into workspace sessions
AssertionError: expected { SHARED: 'global', …(2) } to deeply equal { SHARED: 'team-b', …(3) }
FAIL test/integration/team-secrets.test.ts > team secret resolution > uses current D1 ownership after a session moves teams
AssertionError: expected undefined to be 'b' // Object.is equality
FAIL test/integration/team-secrets.test.ts > team secret resolution > adds the environment owner's team to builds but never to repository-shared builds
AssertionError: expected { SHARED: 'environment', …(2) } to deeply equal { SHARED: 'environment', …(4) }
Test Files 2 failed | 1 passed (3)
Tests 9 failed | 38 passed (47)
```
The bot fixture used a Slack actor for GitHub and Linear credentials.
Fixtures now use valid service-specific actors and still assert exactly
403; production authentication was not changed.
The passing full integration suite emitted NDJSON decoding warnings and
diagnostics from deliberate eviction/invalid-D1 fixtures. The passing
web suite emitted a `TimeoutNaNWarning` and jsdom navigation warning.
2. Verified facts and drift
Branched from `main` at `19e7993`, still matching `origin/main` before
opening. Existing D1 migration `0083` supplies `team_secrets` and
`environments.owner_team_id`; no migration was added. The main session
fold, broker filter, separate build fold, encryption helpers, key cap,
and human team capability admission match the cited behavior. Catalog
baseline was 202 routes / 154 paths; this adds three routes / two paths,
resulting in 205 / 156.
The DO `SessionRow` has no team ownership field
(`session/types.ts:36-65`). Ownership is authoritative in D1
(`db/session-index.ts:80,448-452`), so the resolver reads the existing
index by public session name on each resolution instead of adding DO
state or a migration. Tests cover ownership changes and a missing index
row.
3. Deliberately excluded / limitations
- No new migration, schema field, ownership mutation, repository
grant/token change, or edits to the parallel team/environment/planner
work.
- `OAuthSecretScope` remains `environment | repo | global`; no team
OAuth persistence/write-back was added. Team-only legacy OAuth refresh
tokens are not end-to-end supported: the requested managed-source fold
retains the team layer, but existing OpenAI/xAI brokers still read only
target/global scopes (`session/openai-token-refresh-service.ts:43-46`,
`session/xai-token-refresh-service.ts:78-85`). Team API keys and
ordinary secrets use the new precedence normally. The broker paths were
deliberately left unchanged rather than expanding the
credential/write-back design.
- No new snapshot or prebuilt-image invalidation guarantees. Current
resolution is not revocation of credentials already baked into existing
artifacts. Per-scope capacity retains the existing stores'
read-before-write concurrency behavior.
- Visual verification used the real Next team page with local synthetic
auth/API fixtures, not a production login or remote persistence.
**Visual Verification**
Opened `http://127.0.0.1:3000/teams/platform` as a team lead, verified
tab visibility, list metadata, create/update/delete, clearing saved
values, and no horizontal overflow at mobile width. Uploaded viewport
captures:
- Desktop 1440x1000: artifact `55a3727e9b76c72f089518af573c4e60`.
- Mobile 390x844, sidebar closed: artifact
`4891293b8afb43836a4f8de6bcacfeb4`.
---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/97c7dd352452cdbbe444bffb0c0b0c97)*
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Team members with secret-management access can manage encrypted team
secrets from the team’s Secrets tab, including adding, updating, and
deleting secrets.
* Team-owned sessions and environment image builds can use team secrets;
repository-shared image builds do not. Team secrets override global
secrets, while environment or repository secrets take precedence where
applicable.
* Audit logs identify team-secret changes without exposing secret
values, and secret lists show key metadata rather than stored values.
* Team-secret changes supersede affected environment images and trigger
best-effort rebuild scheduling for eligible team environments.
* **Bug Fixes**
* Environment image builds now fail rather than proceed when team
secrets cannot be read or decrypted.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…rray#2186) Follow-ups from the review of ColeMurray#2184. That PR was merged before these could be pushed to it. ## Behavior changes - **Collaborators on team-owned sessions must be team members.** `PUT /sessions/:id/collaborators/:userId` now returns `409 not_team_member` when the session is team-owned and the target user isn't in the owning team. Previously the grant returned `updated` but couldn't be used, because the `not_member` check from ColeMurray#2184 denies every non-read action to non-members. Re-adding an existing non-member collaborator also returns the 409 now, instead of `unchanged`. Sessions without an owning team behave as before. - **The collaborator picker only lists the owning team.** For team-owned sessions, `GET /sessions/:id/collaborator-candidates` returns only members of the owning team. `UserStore.listCollaboratorCandidates` takes an optional `teamId` for this. - **Batch archive reports `not_member`.** Sessions skipped because of team membership are reported as `not_member` instead of `missing_permission`, so admins who have `sessions.lifecycle` see the real cause. The shared `sessionBatchArchiveResponseSchema` gains this value. The web route only passes the response through, so no deployed client parses it. ## No behavior change - `effectiveSessionCapabilities` reuses the results `sessionCapabilities` already computed, instead of calling `checkSessionAccess` again for each action. This runs on every session-list and inbox row. - `updateSessionScope` no longer refetches `/api/me/teams`. That refetch was only needed by the removed move-and-join-team flow. - Removed the unused `TeamStore.isActive` and made `TeamMembershipStore.bindAddIfJoinable` private. ## Testing - New integration tests cover rejecting non-member collaborators on team-owned sessions, filtering the picker, and the `not_member` batch-archive reason in `off`, `shadow` and `on` modes. - `npm run typecheck`, ESLint and Prettier pass. - Control-plane unit tests (5690), the full control-plane integration suite (1648 passed, 1 skipped) and the web `session-scope` tests (88) pass. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/b6f9227a231685a574df1b97128c1176)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Collaborator suggestions for team-owned sessions are limited to active members of the owning team, and adding a non-member is rejected. * Batch archive results identify sessions skipped because the requester is not a team member. * **Bug Fixes** * Collaborators on team-owned private sessions can access them only while they remain members of the owning team. Leaving or being removed from the team ends access, while the collaborator record remains. * Batch archive results distinguish missing sessions, team-membership restrictions, and permission issues. * Updating session scope no longer triggers an unnecessary membership-data refresh. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…ray#2187) Follow-up to ColeMurray#2186. Route handlers should not contain raw SQL. ## Changes - Added `UserStore.getCollaboratorEligibility(userId, teamId)`. It runs the existing user, role assignment and team membership lookup, and returns one of `"eligible" | "not_found" | "inactive" | "not_team_member"`. It sits next to `listCollaboratorCandidates`, which applies the same rules (active user, optionally restricted to a team). - `changeCollaborator` in `routes/session-scope.ts` now calls the store and maps each outcome to the same HTTP responses as before (`404`, `409 user_inactive`, `409 not_team_member`). The route no longer contains raw SQL or the zod row schema. No behavior change: the SQL, the eligibility rules and the responses are unchanged. ## Testing - New `UserStore` integration test covering every outcome: missing, suspended, no role assignment, eligible without a team, not a team member, and team member. - The existing route-level tests for `session-scope-routes`, `session-access-routes` and `collaborator-candidates` pass unchanged. - Control-plane typecheck and ESLint pass. Control-plane unit tests pass (5718). --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/b6f9227a231685a574df1b97128c1176)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Collaborator changes now consistently check whether the selected person is eligible, active, and—when applicable—a member of the specified team. * Existing error responses remain in place for missing users, inactive users, and people who are not team members. * **Tests** * Added coverage for eligibility outcomes, including active users with and without team membership. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…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 -->
…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>
…2515) ## Why Reviewing agent proposals in **Settings › Memories / Shared memories** takes one click per record, which is slow when many proposals are waiting. This adds selecting several records and applying one action to all of them. It also covers bulk "delete": memories have no hard delete by design (`docs/MEMORY.md`, which keeps historical session manifests valid), so bulk removal means bulk **archive**, or bulk **reject** for proposals. ## What changed **Shared contract** (`packages/shared/src/types/memories.ts`) - `bulkMemoryActionSchema`: `{ action, memories: [{ id, revisionId }], archiveNote? }`. It holds 1 to `MEMORY_BULK_ACTION_MAX_RECORDS` records (one max page, 100), and IDs must be unique. `archiveNote` is accepted only for actions that archive (`reject`, `archive`), matching the single-record body schemas. - `bulkMemoryActionResponseSchema`: one `{ id, ok: true, memory }` or `{ id, ok: false, error }` per record, in request order. **Control plane** - New `POST /memories/bulk` (same `activeSelf` admission and `private, no-store` as the other memory routes). - Loads all targets in one query (`MemoryRecordStore.getMany`). - Authorizes them with the new `MemoryManagementPolicy.authorizeRecords`. This loads environment rows and repository grant owners once for the whole request; the shared-catalog path now uses the same preload. - Applies the **existing** `store.transition` to each record in order. Revision checks, lifecycle guards (replacement family, pending-proposal quota, predecessor) and the per-record audit event are unchanged. - A record that is missing, concealed (another user's personal record), forbidden or stale fails on its own and the rest still apply. Unexpected errors are still thrown. **Web** - BFF route `/api/memories/bulk` and an `applyBulkMemoryAction` client helper. - Records with at least one granted action get a checkbox. A toolbar offers "Select all on this page" (with an indeterminate state), a selected count, and the actions every selected record allows. - Approve and Restore run right away. - Reject and Archive open a confirmation panel with one optional shared archive note. It uses the collection's existing single-panel model, so it replaces any open card panel. - After a bulk action the page refreshes and shows a summary ("Approved 38 of 40 memories."). Any failure is listed by title with the server's reason. - The selection resets when the status tab, page or search changes, and records that leave the page drop out of it. **Docs**: `docs/MEMORY.md` (API table and management behavior). ## Testing - `memories-bulk-actions.test.ts` (new integration test, real D1): - Mixed outcomes in one request: managed repo, managed environment and own personal records succeed. These fail: a readable-only repo, an ungranted repo, another user's personal record, a stale revision, a wrong status and a missing ID. Results come back in request order with the right messages. - Audit events are written only for the records that applied. - Admission is loaded once (`listOwnersForRepositories` and `getByIds` each called once, and no per-repo lookups). - Archive with a shared note, then restore. - Malformed requests get a 400 before any `transition` runs. - Unit tests: - `MemoryManagementPolicy.authorizeRecords` (decision order, preload before evaluation, no name resolution). - The shared schema bounds. - `applyBulkMemoryAction` request shape. - `bulkActionSummary`. - UI flows: selection, select-all, bulk approve with a partial failure, bulk archive confirmation with a note, selection reset on tab change. - Route catalog and admission snapshots updated (`POST /memories/bulk owner=400`, `anonymous=401`), and route counts bumped. - Ran: shared memory tests; web memory settings, hook, lib and settings-page tests; the full control-plane unit suite; the memory and route-conformance integration suites. Typecheck (shared, control-plane, web) and ESLint pass. Not done: I didn't check the UI in a running browser. Layout is covered only by the component tests. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/090962ce9d84a1cd08f8bbf585d4856a)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Select eligible memories on the current page to approve, reject, archive, or restore in bulk. * Optionally add a shared note when archiving selected memories. * **Improvements** * Review per-record results, including errors when an action fails; successful changes aren’t blocked by other failures. * Actions are checked against each memory’s reviewed version and your permissions. * Larger selections are processed in batches. * Selection clears when you change the view, such as switching status tabs, searching, or paging. <!-- 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>
…ay#2516) Follow-up to ColeMurray#2509, which merged 29 seconds before its [review](ColeMurray#2509 (review)) was posted. The merged code is identical to the reviewed head, `4c8b388`, so every finding applies to `main`. This PR acts on the validated findings in two commits: a refactor with no behavior change, then a separate fix that can be reviewed or dropped on its own. ### 1. `refactor: return SandboxAlarmResult from fatal-report termination` (no behavior change) **One result type for both recovery paths.** `FatalTerminationOutcome` mapped one-to-one onto the documented `SandboxAlarmResult`: | removed | now | |---|---| | `not_terminated` | `"no_action"` | | `stop_failed`, or a held retained boot | `"sandbox_failed"` ("held for recovery, so pending prompts wait") | | `terminated` | `"sandbox_terminated"` | | `retries_exhausted` | `{ kind: "fatal_report_unrecoverable"; reason }` | `terminateFailedSandbox` now returns `SandboxAlarmResult`, which gains the `fatal_report_unrecoverable` variant. The exported enum is deleted; nothing in production code ever checked `stop_failed`. **One applier.** `handleFatalSandboxFailure` had re-implemented the alarm handler's "re-drive on termination, fail the boot prompt on an abandoned boot" block. Both now call `applySandboxRecovery` in the new `session/sandbox-recovery.ts`, and the alarm handler's comments moved into its doc. **No `countedOutcome` sentinel.** The fatal result was decided across five returns plus a mutable fallback. That fallback existed only because the catch-all outlived its original reason when the provider stop moved to `stopProviderSandboxSafely`, which never throws. Now: - The work after the failure is counted (publish, detach, stop) lives in `tearDownFailedSandbox`, which logs and returns `false` instead of throwing. - The result is decided at one return site, from `held`, `exhausted` and `tornDown`. **Count and check the breaker in one step.** `recordSpawnFailure` now returns whether the breaker is open. This replaces three copies of "record the failure, then call `isCircuitBreakerOpen`", and removes `isCircuitBreakerOpen` from `WatchdogEffectsDependencies`. The answer is the same: right after counting at `now`, the breaker is open exactly when `failureCount >= threshold`. Test changes in this commit are mechanical outcome renames. The two held-retained-boot assertions in `manager-shutdown.test.ts` change from `no_action` to `sandbox_failed`; the queue treats those two results identically. ### 2. `fix: settle the boot prompt when a fatal report's teardown fails` (behavior change) **Before:** after a fatal report on a booting sandbox, if the provider stop failed (or cleanup threw) while retries remained, the row was `failed` and nothing re-drove the queue. - `handleAlarm()` sees a terminal row. - `redriveIdleQueue()` only wakes `ready` runtimes. So a Slack, Linear or GitHub prompt stayed `pending`, and its caller never got `notifyComplete`, until someone sent another message. That's the same bug ColeMurray#2509 fixed for the breaker-open case. The connecting-timeout watchdog already fails the prompt when its stop fails ("instead of waiting with nothing scheduled to retry it"). **After:** an unfinished teardown resolves `fatal_report_unrecoverable`, so the boot prompt fails with the fatal reason. A held retained boot still resolves `sandbox_failed` and fails nothing. **Regression test:** `sandbox-fatal-report-recovery.test.ts` gains "fails a pending prompt when the failed sandbox cannot be stopped for a replacement". It stubs `ModalSandboxProvider.stopSandbox` to fail. Without the fix it fails (`{ status: "pending" }`); with it, it passes. ### Validation ```sh npm run build -w @open-inspect/shared # ColeMurray#2510 changed shared types npm run typecheck -w @open-inspect/control-plane # clean npx vitest run src/sandbox/lifecycle/ src/session/message-queue.test.ts \ src/session/http/handlers/sandbox-error.handler.test.ts src/session/sandbox-shutdown.test.ts --maxWorkers=1 # 535 passed npx vitest run --config vitest.integration.config.ts test/integration/sandbox-fatal-report-recovery.test.ts \ test/integration/session-lifecycle-alarm-recovery.test.ts test/integration/sandbox-early-connect.test.ts \ test/integration/sandbox-state-retention.test.ts test/integration/sandbox-shutdown.test.ts --maxWorkers=1 # 74 passed (the 73 from ColeMurray#2509 plus the new regression test) ``` ESLint and Prettier are clean on the changed files. ### Not changed - **Boot-prompt guard.** `handleFatalSandboxFailure` still skips the boot prompt when a prompt is processing or awaiting stop confirmation, while the alarm handler uses a bare `getNextPendingMessage()`. Because `onPromptDispatched()` resets the breaker, that guard only matters if the reset write fails or a `processing` row is orphaned. Unifying the two definitions changes which prompt fails in those cases (a CodeRabbit review on ColeMurray#2509 asked for the guard), so that decision is left for a separate change. - **ColeMurray#2509 description.** It still says `SandboxLifecycle` exposes `isCircuitBreakerOpen()` (reverted in 4fbed3a), and that prompts are re-driven "while retries remain" (only when the stop succeeds). The squash commit `593a6a2` carries that text, so this PR body is the correction on record. - **Possible overlap with ColeMurray#2462:** it also edits `message-queue.ts`. This PR only touches `handleFatalSandboxFailure` there. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/8da2a45b72a5af5316b10704bb075341)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Bug Fixes** * Improved recovery from fatal sandbox errors: successful termination resumes execution, while failed termination reports an unrecoverable error and fails the pending prompt with the fatal error. * Avoided duplicate recovery actions when a sandbox is already stopped, termination is underway, or shutdown/checkpoint handling is in progress. * Improved retry handling for sandbox startup failures so exhausted retries are reported consistently. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…ray#2521) The scheduler still combines invocation orchestration with session setup and first-prompt delivery. Extract those workflows into constructor-injected services, reducing `scheduler.ts` from 2,259 to 1,764 lines while preserving admission and launch ordering. ## Changes - `AutomationSessionService` resolves session inputs, refines the deadline from the session claim time, rechecks authority and team visibility, selects skills/memory, and initializes the session. - `AutomationRunLaunchService` claims the session, queues its first prompt, releases held follow-ups, and classifies launch failures. Initialization failures remain eligible for retries; failed or ambiguous prompt delivery does not start a second session. - Share team-target and provider-snapshot admission functions between first launches and retries. Centralize launch contracts and deadline/backoff policy, with construction in `createScheduler`. - Move first-prompt assembly and its stable request ID into the existing prompt module. Preserve compatibility exports and existing SQL/store operations. - Add 17 direct boundary tests while retaining scheduler and real-D1 integration coverage. This is PR 2 of the scheduler refactor, following ColeMurray#2520. Invocation admission, failure accounting, and recovery orchestration remain for PR 3; event handling follows in PR 4. ## Validation - 244 focused unit tests passed across nine files. - 200 integration tests passed across 11 affected suites. Workerd emitted the same teardown warning observed during PR 1; the run exited successfully. - All control-plane typechecks, targeted ESLint, formatting, and Worker/Node builds passed. - Compared retained scheduler methods and extracted launch/session bodies against the merged baseline: changes are dependency access and argument packaging, with workflow order preserved. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e2f5c5eeea5f7221537fb48246f45eaf)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Automation runs now initialize sessions using the applicable target, settings, shared skills, and provider authentication. * Runs receive execution deadlines based on their sandbox settings, and their initial instructions are queued with a stable request ID. * **Reliability** * Launch failures are recorded with retry handling for eligible errors. Authorization and provider configuration are checked before launch, and authorization failures are marked accordingly. * Follow-up work can be released after a run starts; errors during release no longer fail the launch. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…rray#2522) The scheduler still combines source orchestration with invocation admission, retry recovery, and failure accounting. Extract those workflows into injected services, reducing `scheduler.ts` from 1,765 to 915 lines while preserving existing claim and write ordering. ## Changes - `AutomationInvocationService` prepares and admits firings, accepts webhook receipts after insertion, resolves lazy launch inputs, launches children, and accounts for immediate failures. Its separate `retryDue` workflow prepares and conditionally claims retries, retaining post-insert attribution for first launches and pre-claim attribution/provider resolution for retries. - `AutomationRecoveryService` repairs orphaned/expired runs and missed accounting, with no launch-admission dependencies. The scheduler retains recovery → held prompts → retries → due firings; retry results still count claimed attempts, including failed attempts. - `AutomationFailureService` owns the invocation failure claim, consecutive-failure counter, pause threshold, and success reset. Errors still propagate to each caller's existing containment policy. - Wire collaborators through `createScheduler`; share launch budgets, bounded concurrency, owner-principal resolution, and SCM attribution without introducing new persistence operations. - Add 18 focused tests for admission/retry boundaries, failure accounting, and tick sequencing. Keep completion ordering and source-specific results in `Scheduler`. This is PR 3 of the scheduler refactor, following ColeMurray#2521. Event handling and final cleanup remain for PR 4. Existing accounting/publication crash windows are unchanged. ## Validation - 193 targeted unit tests passed across invocation-services, scheduler, launch-services, and factory. - 80 real-D1 integration tests passed across automation-launch-retries, automation-invocations, and scheduler. - All control-plane typechecks, targeted ESLint, formatting, and Worker/Node builds passed. - Compared extracted and retained method bodies against the merged baseline: workflow bodies match after dependency/delegation substitutions. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e2f5c5eeea5f7221537fb48246f45eaf)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Improvements** * Automation runs now handle duplicate triggers and overlapping executions more consistently. * Failed runs can be retried or recovered automatically, including runs that stall during startup or exceed their execution deadline. * Repeated failures can pause an automation, while successful runs reset its failure streak. * Automation launches and retries are limited per scheduler cycle, with concurrent launches capped to help manage activity. * GitHub attribution is included when it can be resolved reliably. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…urray#2524) ## Summary - Skip the final whole-repo typecheck in `.openinspect/setup.sh` only when `OPENINSPECT_BOOT_MODE=fresh`, preserving dependency installation, the shared build, hook checks, and Python setup. - Keep typechecking for image builds and local runs, including safe handling of an unset boot-mode variable and the existing warning-only failure behavior. - Enable inherited TypeScript incremental caching for all four control-plane projects. - Clarify reusable TypeScript caches and boot-mode guards in the prebuilt-image documentation. ## Validation - `bash -n .openinspect/setup.sh` passed. - Smoke-tested the actual setup script with stubbed provisioning commands for fresh, build, unset, and empty boot modes, plus typecheck failures. Confirmed the preceding setup steps still run and setup completes. - `npm run typecheck -w @open-inspect/control-plane` passed twice: **35.84 s cold**, **9.74 s warm**, about **3.7x faster** on the second run. - Confirmed all four expected `.tsbuildinfo` files exist and are ignored by Git. - Whole-repo `npm run typecheck` passed. - Targeted Prettier checks and `git diff --check` passed. Resolves COL-284. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/246e37bc53d84d4d41def5a6fe2de4f1)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Performance** * Fresh session setup now skips type checking, helping sessions start faster. Type checks continue during other setup modes. * TypeScript compilation can reuse cached results. * **Documentation** * Updated image prebuild guidance to explain reusable tool caches and when to warm them. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
Scheduler still owned event matching, Slack thread continuation, and webhook receipt leasing alongside tick and completion orchestration. Extract `AutomationEventHandler`, `SlackEventHandler`, and `WebhookEventHandler`, reducing `scheduler.ts` from 915 to 351 lines. - Construct each source handler with its own collaborators in the factory. The dispatcher receives only their `handle(event)` interfaces; continuation-policy builders are private methods of the source handlers. - Resolve SCM provider configuration at construction. Source handlers use `SessionContinuityService`, shared `AutomationAttributionService`, stores, and the existing clock/crypto APIs instead of raw function dependencies. Actor authorization and attribution remain lazy and run at the original admission points. - Keep Slack continuation before trigger conditions and its lazy actor/context/settings reads local to each delivery, including when a handler instance is reused. - Keep webhook receipt reconciliation before conditions, continuation after conditions, and initial receipt acceptance inside invocation admission. Uncertain delivery retains its lease. - Bind stores, clocks, IDs, GitHub admission, and continuity adapters in the factory. Retry admission remains with `AutomationInvocationService`. - Share event result accounting and prompt composition, remove temporary scheduler re-exports, and cover Linear/Sentry candidate filtering and condition-gated admission directly. Validation: 191 focused unit tests and 52 real-D1 Slack/webhook/retry integration tests passed. All four control-plane TypeScript configurations, changed-file ESLint/Prettier checks, Worker and Node builds, and `git diff --check` passed. The integration runner emitted a teardown RPC warning after passing tests; it exited successfully. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e2f5c5eeea5f7221537fb48246f45eaf)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Added event-driven automation handling for GitHub, Slack, webhooks, Linear, and Sentry. * Slack and webhook events can continue existing sessions, and webhook deliveries are checked to prevent duplicate processing. * Event context is separated from automation instructions, with safeguards against context overriding trusted instructions. * **Bug Fixes** * Invalid trigger conditions and skipped or refused event deliveries are handled and reported more consistently. <!-- 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>
Scheduler services still accepted standalone clock, ID-generation, and held-prompt callbacks after the event handlers moved to explicit collaborators. Apply the same clock and crypto dependencies to scheduling, invocation admission, recovery, run launch, and Slack publication. Expose held-prompt release and sweeping through `SessionContinuityService`, allowing the factory to inject one continuity service throughout. Preserve timestamp/ID generation points, recovery → held prompts → retries → due firings ordering, and first-prompt marking before follow-up release. Validation: 214 targeted unit tests and 96 real-D1 integration tests passed. All four control-plane TypeScript configurations, changed-file ESLint/Prettier checks, Worker and Node builds, and `git diff --check` passed. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e2f5c5eeea5f7221537fb48246f45eaf)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Refactor** * Standardized how scheduler operations handle timestamps, identifier generation, and session continuity. Scheduling and recovery behavior remains unchanged. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - Add Boat base pruning using the existing API transport and `/saved-snapshots` endpoints. Require explicit deployed/replaced references and validate both before deleting anything. - Derive the rollback reference from confirmed Cloudflare Worker deployment history, not artifact creation times. Verify the active deployment and target, skip same-base redeploys, and fail closed on ambiguous history. - Restrict cleanup to exact `<prefix>-<16 lowercase hex>` names and reserve `oi-img`/`oi-img-*` base prefixes so prebuilt-image names cannot overlap. Manual builds, saving snapshots, and other prefixes remain untouched. - Run cleanup after the Worker deployment, with snapshot name, API URL and organization in the non-secret triggers. No destroy-time cleanup is added. - Cover reverts followed by another deploy, undeployed ready artifacts, retries, namespace collisions, history safety checks and target changes; document manual previews and rollback limits. ## Verification - Sandbox-images pytest suite: 161 passed. - Ruff lint and formatting checks passed for sandbox-images and Boat tooling. - Terraform 1.14.8 validation and targeted Boat backend tests: 9 passed. - Recursive Terraform formatting, Prettier and diff checks passed. A read-only dry run against the real wallet remains required before merge; this sandbox has no Boat credentials or deployed snapshot configuration. No live API calls or snapshot deletions were performed. Deployments, direct Worker rollbacks and pruning must remain serialized. Fixes [COL-285](https://linear.app/colemurray/issue/COL-285/delete-superseded-boat-base-snapshots-after-each-base-deploy-keeping). --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/b9c78237e1458a81e3a7dc3a9048c3d3)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Automatically prunes eligible superseded Boat base snapshots after the Worker switches to a verified snapshot, retaining the deployed base and its confirmed predecessor for rollback. * A dry run previews cleanup without deleting snapshots. Cleanup is skipped when deployment history cannot confirm a predecessor; manual snapshots, prebuilt images, and snapshots outside the configured naming pattern are excluded. * **Documentation** * Clarified snapshot naming, wallet-based allowance usage, cleanup behavior, and rollback considerations. Use a unique prefix for each deployment sharing a Boat wallet to avoid treating their snapshots as belonging to the same deployment. <!-- 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>
Slack and webhook event handlers assembled continuation behavior as callback objects that captured their dependencies. Replace them with `SlackContinuationPolicy` and `WebhookContinuationPolicy`, implementing the existing policy contract with constructor-injected collaborators and ordinary methods. Two small source-specific factories create delivery-scoped policies. Slack shares a lazy actor lookup across candidates within one delivery, including null results and failures, while prompt construction receives each candidate's automation metadata. Webhook policies retain the delivery claim and stable prompt ID; receipt acceptance/release, authorization, hashing timing, and held-follow-up ordering remain unchanged. The handlers no longer receive policy-only identity and attribution dependencies. Validation: - 184 targeted unit tests passed across event handlers, scheduler, and factory, including 10 new policy cases. - 35 real-D1 integration tests passed across Slack events, team steering, and webhook sessions, covering immediate and held follow-ups. The runner logged an RPC teardown error but reported all tests passed and exited 0. - All four control-plane TypeScript configurations passed. - Changed-file ESLint and Prettier checks, Worker/Node builds, and `git diff --check` passed. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e2f5c5eeea5f7221537fb48246f45eaf)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Improvements** * Slack and webhook-triggered automations now provide more consistent session continuations, including actor attribution and delivery tracking. * Slack identity lookups are reused during event handling. Webhook requests retain consistent identifiers when none are provided. * Slack prompts include thread context, and webhook continuations use the automation’s canonical owner when available. * **Tests** * Added coverage for continuation behavior, identity resolution, attribution, request identifiers, and webhook delivery handling. <!-- 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>
GitHub automation admission duplicated the event counters, invocation ID collection, and logging already shared by the other event handlers. Reuse `recordEventLaunch` and `logEventResult` after GitHub-specific grant and retry decisions settle, so event outcome semantics have one owner. The bounded retry loop, fresh authority reads, and grant-denied history remain in place. Intermediate retry outcomes are not counted. Added coverage for deduplication, recorded/unrecorded overlap, blocked overlap, failed launches, and final logging. Validation: 41 focused unit tests and 22 real-D1 GitHub routing integration tests passed. All four control-plane TypeScript configurations, changed-file ESLint/Prettier, and `git diff --check` passed. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e2f5c5eeea5f7221537fb48246f45eaf)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Bug Fixes** * Improved consistency when processing duplicate or overlapping GitHub events, including blocked and unsuccessful launches. * Avoided a spurious warning when coverage returns before a post-admission denial is recorded, and improved the accuracy of event outcome reporting. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
SessionContinuityService previously forwarded every operation into a separate free-function implementation, which also constructed authorization collaborators and read the global clock and ID generator. Move that implementation directly into the existing service, with internal methods private and the public continuation contracts retained separately. The composition root now injects authorization, clock, and crypto collaborators. The store dependency is narrowed to its nine used operations. Authorization remains lazy and is read afresh when resolving an actor or releasing held prompts; Slack's delivery-scoped actor cache and per-session admission checks retain their existing ownership. Remove the temporary firstPromptRequestId re-export and import it from session-prompt in its remaining test caller. The control flow, receipt handling, historical lookups, and held-prompt ordering are preserved. New focused tests exercise interface-only dependencies, revoked/unavailable authorization at held-prompt release, and deterministic prompt IDs/timestamps. This PR is independent of ColeMurray#2529 and targets main directly. Validation: - 211 targeted unit tests passed across continuity, event handlers, scheduler, launch services, and factory, including eight new dependency tests. - 35 real-D1 Slack events, team steering, and webhook session integration tests passed. The runner logged an RPC EnvironmentTeardownError but reported all tests passed and exited 0. - All four control-plane TypeScript configurations passed. - Changed-file ESLint/Prettier, Worker/Node builds, and git diff --check passed. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e2f5c5eeea5f7221537fb48246f45eaf)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Bug Fixes** * Session continuation now checks current authorization before accepting collaboration. Suspended actors and actors without collaboration access are denied. * Held follow-ups are removed when authorization is revoked and retained if authorization cannot be verified. * **Improvements** * Recent sessions can continue for up to seven days. Held follow-ups are retried after two minutes, with each retry sweep limited to 50 runs. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
Completion coordination tests currently construct the entire scheduler factory and depend on unrelated module mocks. Add a small, typed constructor-injected harness for `Scheduler.runComplete` so failures identify the scheduler's own contract: guarded writes, first-prompt ordering, accounting, publication, duplicates, timeout correction, and error propagation. Move the repository-free Slack label assertion to the Slack service tests and preserve sibling-accounting cases in the failure service tests. Retain composed factory coverage for duplicate publication after preparation failure and late timeout correction, alongside the existing D1 completion/accounting tests. Production code is unchanged. Validation: - 199 targeted unit tests passed across scheduler completion, scheduler, invocation services, and Slack service; the nine new completion cases also passed after the typed fixture correction. - Eight real-D1 tests passed for run completion and sibling finalization (`scheduler.test.ts`, filtered). - All four control-plane TypeScript configurations passed. - Changed-file ESLint, Prettier, and `git diff --check` passed. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e2f5c5eeea5f7221537fb48246f45eaf)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Tests** * Expanded verification of run completion outcomes, including successful and failed runs, duplicate or missing runs, and late timeout corrections. * Added coverage for partial invocation failures and Slack notifications when repository details are unavailable. * Updated scheduler test coverage for completion handling and related failure-accounting behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
Recurring automations require a cron expression, so users cannot schedule a prompt to run once at a future time. This adds a one-shot variant to the existing schedule trigger and scheduler. Addresses ColeMurray#1596. - Add nullable `schedule_once_at` storage and a millisecond `scheduleOnceAt` API field. Creation requires either cron or a future timestamp; schedule mode is fixed after creation. A paused one-shot can be rescheduled and resumed explicitly. - Atomically record the scheduled invocation/runs and consume the one-shot slot. Keep it enabled while its runs are active or retrying, then disable it once they settle. Check the current row during admission so cancellation and concurrent or stale tick delivery cannot launch a second session. Keep an overlapping slot due until its skip is durably recorded. - Exclude disabled one-shots with completed/skipped scheduled invocations in SQL before cursor pagination, using the existing derived invocation status. Preserve active, failed, partial-failed, unauthorized, and never-fired entries, and retain history/detail reads. - Add the Run once date/time choice, cancellation guidance, and list/detail descriptions. Preserve an unchanged timestamp during edits, including seconds and daylight-saving offsets. Preserve the configured timezone on edits and share schedule descriptions between list and detail. - One-shot and recurring schedules share launch retries. Retried attempts reuse the same run and invocation, recheck authorization, and respect pause/delete/retry-limit changes. Rescheduling a one-shot withdraws retries for its old slot. Keep admission, skip history, and slot consumption in shared atomic statement binders; recovery repairs missed final disables. ### Before/after evidence At main `02a6bbf1c2720699444d91867026e8b0111ed6fc`, the executable creation regression below failed: expected HTTP 201, received HTTP 400. It now passes and asserts the exact stored timestamp and next run time. ```sh npm test -w @open-inspect/control-plane -- src/routes/automation-create.test.ts --maxWorkers=1 -t 'creates a one-shot schedule' ``` ### Validation - 326 targeted control-plane unit tests passed across scheduler, invocation services, store, and create/update/lifecycle routes. - 122 real-D1 integration tests passed across one-shot scheduling, launch retries, scheduler behavior, and invocation admission. The retry suite runs against both recurring and one-shot schedules, including concurrency, cancellation, rescheduling, retry exhaustion, authorization, and missed-finalization recovery. - 72 web form and policy tests passed, including retry selection for one-shots and timezone/instant preservation. - Control-plane and web type checks, Worker/Node builds, ESLint/Prettier, SQL portability, and `git diff --check` passed. - Verified the local Run once form with fixture API responses at a 1512×982 viewport: selected Up to 2 retries and a valid time; creation became enabled. Uploaded screenshot artifact `2b4c02a5e069ef415d3aba17638ade20`. No live automation was submitted. ### Limits The existing scheduler runs every minute: a one-shot becomes eligible at its timestamp and launches on an eligible tick, subject to the existing work budget. It does not add an exact-second timer. D1 tests exercise real database/session-index writes with a session-runtime stand-in; they do not start a Modal sandbox. No deployment or issue-state change was performed. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/1bc40d46c8b4c6d689b402979f062652)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Scheduled automations can now run once at a chosen date and time, in addition to recurring schedules. * Create and edit forms support one-time scheduling, show times in the configured timezone, and validate that new schedule times are in the future. * One-time schedules support retries and can be canceled by pausing or deleting the automation. They disable after active runs and retries finish. * **Bug Fixes** * Resuming a one-time schedule preserves its configured time and reports conflicts if the schedule changes during resume. * One-time schedules no longer launch repeatedly after their scheduled run is consumed. * **Documentation** * Updated scheduling guidance to cover one-time schedules, retries, cancellation, and rescheduling. <!-- 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>
The scheduler factory currently assembles an ad hoc admission object whose methods forward to functions with a separate dependency bundle. Replace that layer with a constructor-injected `AutomationLaunchAdmissionService` that owns target snapshotting, team-grant checks, and provider-auth validation. Invocation handling continues to depend on the public two-method `LaunchAdmissionOperations` contract. The existing low-level adapters remain in the factory. Initial-launch and retry admission keep their distinct ordering, including grant-version capture before target resolution, initial attribution after insertion, and retry attribution/provider resolution before claiming. Migrate admission tests to the service and add coverage for fresh provider routing on successive admissions and provider-resolution failures. The extracted algorithm bodies are unchanged apart from accessing constructor dependencies. Validation: - 188 targeted unit tests passed across launch services, invocation services, scheduler, and factory. - 27 real-D1 integration tests passed across automation launch retries and team grants. - All four control-plane TypeScript configurations passed. - Worker and Node builds, changed-file ESLint/Prettier, and `git diff --check` passed. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e2f5c5eeea5f7221537fb48246f45eaf)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Refactor** * Updated automation launch processing to resolve targets and check repository access and provider configuration before execution. Existing access denials and configuration errors continue to be handled, and provider routing is refreshed for each admission. These changes do not add new end-user features. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - Stop idle automation, GitHub bot, and agent-child sandboxes after 2 minutes by default, while preserving the regular 10-minute default for user, Slack, and Linear sessions. - Resolve the session source on each alarm, inactivity scheduling call, and idle-queue redrive; sessions without a persisted row use the regular timeout. - Add `SANDBOX_UNATTENDED_INACTIVITY_TIMEOUT_MS` with a 60-second minimum. Invalid values warn and fall back to the default; unattended sessions use the smaller of this setting and the regular timeout, allowing the shorter policy to be disabled. - Preserve connected session-page extensions, processing-heartbeat activity renewal, and follow-up sandbox recovery. - Emit `sandbox.inactivity_timeout` before provider-specific shutdown with `spawn_source`, `timeout_ms`, and `idle_ms`. - Wire the setting through Node, Terraform, and both deployment workflow jobs; update the example environment, lifecycle/operator docs, debugging reference, runtime comment, and changelog. ## Verification - 184 targeted control-plane unit tests passed across lifecycle decisions, manager scheduling/redrive, inactivity effects, cross-path effects, reconnects, and Node configuration. - 53 control-plane integration tests passed across sandbox events, runtime composition/configuration, and shutdown recovery. Includes a quiet processing turn kept alive by heartbeats beyond the unattended timeout. - 23 docs content-policy/inventory tests and 27 Terraform workflow-contract tests passed. - Repository-wide `npm run typecheck` and `npm run lint` passed. - Prettier, `git diff --check`, and recursive `terraform fmt -check` passed. - `terraform validate` and all 5 targeted `sandbox_inactivity.tftest.hcl` tests passed. ## Operational Notes - Only the session page's WebSocket grants connected-client extensions; direct terminal, VS Code, or VNC access alone does not. - Existing sessions resolve their source-aware timeout on their next alarm check. - Inactivity dashboards using the old unmanaged-only `sandbox.timeout` event should migrate to `sandbox.inactivity_timeout`. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/997a31ce8d14b6f31b18de9ae15a1359)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Automation, GitHub bot, and agent sessions stop after 2 minutes of inactivity by default; user, Slack, and Linear sessions retain the 10-minute default. * Configure the unattended-session timeout with `SANDBOX_UNATTENDED_INACTIVITY_TIMEOUT_MS` (minimum 1 minute). The effective timeout is capped at the regular inactivity timeout. * Connected session pages and active runtime heartbeats extend activity. Follow-up prompts can resume an existing session after an inactivity stop; direct terminal, VS Code, and VNC connections alone do not extend the timer. * **Documentation** * Updated setup, lifecycle, and deployment guides with timeout settings and 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 - Move Boat snapshot saves, reads, and deletes in the control-plane client and base builder to `/saved-snapshots`, and rename the associated helpers, types, schemas, and constants. - Update callers, mocks, response-envelope fixtures, documentation, Terraform wording, and the changelog to use saved-snapshot terminology. The allowance error now directs operators to add money to the Boat wallet. - Add regression coverage for the Python builder's request paths, payload, and save polling, plus preservation of 402 `credit_required`, 404, and 409 `save_in_progress` handling. ## Compatibility and Rollout - Request bodies, timeouts, snapshot naming formats, Terraform variable/output/resource names, and the `BOAT_BASE_SNAPSHOT` binding are unchanged. - Existing snapshots need no migration or deployment ordering. The existing prune logic is untouched. - The changed build inputs trigger a base snapshot rebuild on the next Terraform deployment. That save needs available allowance or wallet funding before the Workers switch. - Historical changelog entries retain their original wording; the legacy terminology scan finds no other occurrences. ## Validation - Shared package build and control-plane typecheck passed. - Targeted Boat REST client, lifecycle, provider, image-build resources, and adapter tests: 126 passed. - Python provider-adapter, deployment-retention, and bundle/hash tests: 137 passed. - Production Terraform validation and `terraform test -filter=tests/boat_backend.tftest.hcl`: 9 passed. - ESLint, Ruff lint/format, Prettier, changed-file Terraform formatting, shell syntax, and `git diff --check` passed. - No live deployment or Boat API mutation was performed. Fixes [COL-287](https://linear.app/colemurray/issue/COL-287/move-boat-snapshot-calls-off-the-deprecated-named-snapshots-endpoints). --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/7989122426c06aaddae49c935ad4fb76)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Boat snapshots are now saved and managed through the saved-snapshot API, including for base builds and prebuilt images. * Existing snapshots require no migration; the next deployment rebuilds the base snapshot once. * **Documentation** * Updated Boat setup, configuration, and troubleshooting guidance to use “saved snapshot” terminology. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - Materialize the shared `eligible_sessions` CTE once so recursive re-rooting can index the filtered set instead of rescanning the viewer's session history for every recursive row. - Require materialization for snapshot, category-page, and subtree-marking queries through the existing shared helper; leave unscoped bulk marking flattenable. A short comment documents the recursive planner requirement. - Add a real-SQLite regression test using the full D1 migration set and one batched session insert. It pins materialization, rejects base-session searches/scans directly inside the recursive step, and verifies categories, re-rooting, unread state, descendants, and pagination on a known shape. - No migrations, indexes, write-path changes, or polling changes. ## Local Benchmark Measured on a synthetic in-memory SQLite database with 2,026 sessions, including roughly 200 children beneath an archived parent and nested descendants. Both variants ran the snapshot SQL captured from `SessionInboxStore` with identical `mine`/all-scope bindings against the same database. The throwaway benchmark script is outside the repository. | Variant | Median Query Time | | --- | --- | | Inlined eligible CTE | 45.10 ms | | Materialized eligible CTE | 6.24 ms | **7.23x faster** across 20 measured runs per variant after three warmups, alternating execution order. All 25 returned rows and their order were identical. Measurements include statement preparation and execution, not API or metadata-loading overhead. SQLite version: 3.53.4. The original recursive step searches `sessions` through the viewer's user index. With the fix it scans `rerooted_sessions` and searches `child` using an automatic covering index on `parent_session_id`. ## Validation - Confirmed the new query-plan regression fails on the missing materialization assertion when `MATERIALIZED` is removed, then restored the fix. - `npm run test -w @open-inspect/control-plane -- src/db/session-inbox-store.plan.test.ts src/routes/session-index.test.ts --maxWorkers=1`: 70 tests passed. - `npm run test:integration -w @open-inspect/control-plane -- test/integration/session-inbox.test.ts test/integration/session-inbox-scoping.test.ts test/integration/session-inbox-mark-read.test.ts test/integration/session-scope-routes.test.ts test/integration/session-shadow-audit.test.ts --maxWorkers=1`: 97 tests passed, with existing suites unchanged. - `npm run typecheck -w @open-inspect/control-plane`: all four TypeScript programs passed. - `npm run lint -w @open-inspect/control-plane`: passed. - `npm run lint:sql-portability`: passed. - `npm run format:check`: passed on a clean export of the staged commit. In the live sandbox, this command reports an unrelated formatting issue in the pre-existing, Git-excluded generated `.opencode/tool/_memory-contract.js`; that tooling was left untouched. - Confirmed snapshot, page, and subtree-marking statements materialize the eligible CTE, while unscoped bulk marking does not. The expanded real-SQLite regression rejects both materialization and the extra eligible-set scan for unscoped marking, checks the written read state, and fails with the previous unconditional fence. ## Post-Deploy Verification Operator verification requires Cloudflare credentials and was not run. Check D1 query analytics for lower inbox rows read and engine duration, lower `GET /sessions/inbox` SQL-engine and request durations, and reduced p90 contention on unrelated routes during inbox refreshes. Tracks [COL-290](https://linear.app/colemurray/issue/COL-290/inbox-query-materialize-eligible-sessions-so-re-rooting-stops). --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/62ebc7136fccfdbf13e38a2718eb7c7f)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Performance** * Improved the efficiency of loading session inbox snapshots and marking sessions as read. Session groups, pagination behavior, and read-state results remain unchanged. <!-- 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 - Start the sidebar inbox from the hydrated stored team selection once current-user authorization is available, without waiting for memberships. - Validate provisional team IDs with the shared query parser and derive All teams access from role and suspension state. - Switch to validated filters when memberships resolve, preserving the existing SWR key when the selection is unchanged and refetching once when a stale selection is corrected. - Offer Mark all as read only after memberships validate the scope its confirmation names. - Reconcile a stored Workspace or All teams selection to All my teams when the user has no active team. Their lists were already unfiltered and the switcher is hidden, so the provisional request now corrects once instead of on every load. - Preserve membership-error handling, creator filtering, pagination, and scoped mark-read actions. ## Tests - The new regression tests fail before the fix, and the strengthened membership-failure cases fail with the original loading gate restored. The archived-only Workspace and All teams handoffs, the zero-team storage check, and the pending-memberships Mark all as read case also fail before their fixes. - `npm run test -w @open-inspect/web -- src/hooks/use-active-team.test.tsx src/hooks/use-sidebar-sessions.test.tsx src/components/session-sidebar.test.tsx --maxWorkers=1` passed: 115 tests. - `npm run test -w @open-inspect/web` passed: 2,735 tests across 226 files. - `npm run typecheck -w @open-inspect/web` passed. - `npm run lint` and `npm run format:check` passed. - `git diff --check` passed. ## Post-Deploy Verification - Confirm a cold-load inbox request starts alongside the memberships request rather than after it. - Confirm a valid stored selection causes one inbox request. - Confirm a stale stored team, or a stored Workspace selection for a user with no active team, causes one provisional request followed by one unfiltered request, then storage is corrected to All my teams. - Confirm a non-admin with All teams stored never sends `scope=all`. - Confirm Mark all as read appears only after the team selection resolves. Tracks [COL-291](https://linear.app/colemurray/issue/COL-291/sidebar-fetch-the-inbox-without-waiting-for-apimeteams). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Bug Fixes** * Inbox and sidebar sessions can now appear while team information is still loading, using the saved team or workspace selection where access allows. * Session results update to reflect validated team access once memberships load; unavailable or invalid selections fall back to unfiltered results. * “Mark all as read” is hidden until team loading finishes. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
) ## Problem The composer's session-target picker confirms its default target only after both `/api/repos` and `/api/environments` return. Three read-only requests wait for that confirmation, even though their keys are usually known from the start: - the default repository's branch list, - the image-build feed (`/api/image-builds`), - the skill resolution preview. So on a cold home-page load they run as a second serial tier after the catalogs, and the branch list is typically the last request to settle. ## Change **`use-session-target-picker.ts`** - `readStoredSessionTarget()` reads the stored last selection with a try/catch. It accepts only a `none` target, an environment with a non-empty id, or a repository whose full name passes `parseRepositoryFullName`. - `anticipateDefaultTarget(...)` returns the effect's first two choices in order: the team default environment, then the stored target (never "none" inside a team). A catalog that is still loading counts as containing the candidate. It returns null when the default would be `repos[0]`, because that can't be known before the catalog loads. - The default-target effect now calls the same helper with the loaded catalogs, followed by its existing `repos[0]` / "none" fallbacks. The precedence lives in one place, and the effect's storage read is now guarded too. - `anticipatedTarget = hasExplicitSelection ? sessionTarget : sessionTarget ?? anticipateDefaultTarget(...)`. A loaded catalog that confirms the guess keeps returning it, so request keys don't flip to null while the effect commits. - Branches are keyed on the anticipated repository. The branch selector still renders only for a confirmed repository, and once a target is confirmed the two are the same. - The image-build feed is enabled when there is an anticipated target, or when either catalog has entries. - A new `anticipatedRequestFields` is exposed for read-only prefetches. **`page.tsx`** - The skill preview is requested for `picker.anticipatedRequestFields`. The selector, the count and the prompt suggestions show it only once the confirmed target exists. Before that they get the same null / not-loading / empty-ready values as today. - Warming and launching still use only `buildRequestFields()`, the confirmed target. ### Small deviation from the ticket `anticipateDefaultTarget` takes `loadingRepos` and `loadingEnvironments` separately instead of a single `catalogsLoading` flag. Each candidate is treated as available only while *its own* catalog is loading. This avoids guessing a stored repository that the already-loaded repository catalog has ruled out. ## Wrong guesses SWR caches each response under its own key, and every consumer reads the current key, so nothing needs aborting; all three requests are read-only and authorized by the server. A stale stored target costs at most one branch-list request and one skill-preview request. After that the effect picks the fallback and saves it to storage. The anticipated target is never used to warm or launch a session. ## Tests - `use-session-target-picker.test.ts`: `useBranches` and `useImageBuilds` are now `vi.fn`s. New cases cover: - a stored repository while the catalogs load; - a team default environment while loading; - a stored repository that no longer exists, switching to the loaded fallback; - no empty branch key while the catalogs confirm the guess; - an explicit selection made during loading; - an unparseable stored value and throwing storage; - a stored "No repository" in a team context. - `page.test.tsx`: the fixture's `useSkillResolutionPreview` is now a `vi.fn`. A new test checks that the preview is requested for the stored target while repositories load, and that the selector shows no count until the target is confirmed. - With the source changes reverted, all 9 new tests fail. Checks run: - `npm run test -w @open-inspect/web -- src/hooks/use-session-target-picker.test.ts "src/app/(app)/(sidebar)/page.test.tsx" "src/app/(app)/(sidebar)/page-team-context.test.tsx"` - `npm run test -w @open-inspect/web` (226 files, 2744 tests pass) - `npm run typecheck -w @open-inspect/web` - `npm run lint` - `npm run format:check` ## How to verify after deploy (operator) In the web request logs for a cold load of `/` with a stored selection, `/api/image-builds`, `/api/repos/<owner>/<name>/branches` and `/api/skills/resolve-preview` should start in the same batch as `/api/repos` and `/api/environments`, not after them. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/9a5c313ff3f989ce2dc6bf54c9982b6b)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Improvements** * Skill previews and related data can begin loading while repository and environment choices are still being restored. * The composer keeps showing an unresolved preview until the selected target is confirmed, and shows the skill count once the repository is available. * Default and restored target choices are handled more consistently while catalogs load. <!-- 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>
Adds `CHANGELOG.md` entries for significant features merged since October 14 that didn't add their own. Bug fixes, refactors, dependency bumps, and small UI polish are left out on purpose. | Date | Entry | PR | |---|---|---| | Oct 25 | One-shot scheduled automations (D1 migration 0091) | ColeMurray#2411 | | Oct 25 | Launch retries for scheduled runs (D1 migration 0090) | ColeMurray#2510 | | Oct 25 | Review memories in bulk, plus the shared memories **All** view | ColeMurray#2515, ColeMurray#2503 | | Oct 24 | Codex sessions report their cost | ColeMurray#2466 | | Oct 24 | Mermaid diagrams in assistant messages | ColeMurray#2407 | | Oct 24 | Duplicate an automation | ColeMurray#2499 | | Oct 22 | Merge duplicate member accounts | ColeMurray#2477 | | Oct 20 | OpenComputer sandbox provider removed (operator upgrade note) | ColeMurray#2450 | Each entry goes under its PR's merge date (UTC) and follows the existing style. The wording is based on each PR's description and the public docs pages it updated. The file was formatted with Prettier. Not included, but could be added: Codex plan updates and error surfacing (ColeMurray#2459, ColeMurray#2460), marking a parent session and its children read (ColeMurray#2464), PR state on session search results (ColeMurray#2461), and the `bridge.harness_attached` → `bridge.harness_attach` log rename (ColeMurray#2385). --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/2fbb13e7b0bc8531ab561204055075c2)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Added one-shot scheduled automations, bulk memory review, and automation duplication. * Added Codex cost reporting and Mermaid diagram rendering. * Added account merging, with permission requirements. * Updated shared-memory scope. * **Improvements** * Scheduled automations now support bounded launch retries. * **Breaking Changes** * OpenComputer support has been removed. Terraform validation fails for deployments that still use it. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - Delegate authentication to existing control-plane requests instead of performing a separate server-side session lookup first. - Preserve authentication-before-provider-disclosure for unsupported repo images in a shared server-only helper. - Preserve 401 responses in the image-build feed and Slack channel route, including mixed image-feed failures, while retaining Slack's non-authentication fallback. - Reject attachment uploads without a session cookie before reading their bodies, without making a network request. - Retain explicit session lookups for the login page and local unsupported-provider responses, preserve route-specific cache/error behavior, and remove obsolete websocket auth timing. ## Validation - `npm run build -w @open-inspect/shared` - `npm run test -w @open-inspect/web -- src/app/api src/lib/server-auth-boundary-eslint.test.ts --maxWorkers=1` (180 tests passed) - `npm run test -w @open-inspect/web -- --maxWorkers=1` (2,780 tests passed) - `npm run typecheck -w @open-inspect/web` - `npm run lint` - `npm run format:check -- '!.opencode/tool/_memory-contract.js'` passed. The unfiltered command flags this unrelated, git-ignored sandbox-generated helper; it was left unchanged. - Confirmed the new regressions fail against the original route implementations and pass with these changes. - Confirmed no `app/api/**/route.ts` references `getServerAuthSession`. ## Behavior Notes - Malformed anonymous requests can now encounter local validation before control-plane authentication. - Some unauthorized responses use the route's existing control-plane error mapping; clients continue to receive HTTP 401. - No control-plane, schema, infrastructure, or login-page behavior changes. ## Post-Deploy Verification Operator verification requires production access and was not performed here. Compare function durations for environment, repository, image-build, and branch requests, and confirm control-plane session-lookup traffic decreases. Browser-owned session refresh requests should remain. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/55490229132405b59a3ab1d1b29793ba)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **API Behavior** * Many API requests now rely on the control plane to determine authentication and return authorization errors. * Session attachment uploads require a valid browser-session cookie; existing validation and size limits remain. * Repository image operations retain unsupported-provider restrictions, and image-build authorization errors are preserved. * Session inbox read-state requests require valid JSON and a matching request origin. * Sandbox access requests return an empty response when access is unavailable. * Invalid resource paths are rejected, and resource identifiers are safely encoded when forwarded. * Automation key regeneration rejects request bodies over 16 KiB. * **Tests** * Updated API tests cover forwarded authorization responses and revised request handling. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…oleMurray#2550) ## Summary Two scheduling changes were documented on the public site (`automations/schedules.mdx`) but not in the internal reference `docs/AUTOMATIONS.md`, which still described schedules as cron-only: - ColeMurray#2411: one-shot scheduled automations - ColeMurray#2510: retrying scheduled runs whose launch fails before the prompt This brings `docs/AUTOMATIONS.md` in line: - **Intro and trigger tables:** schedules run once or on a cron cadence, and the schedule fields now include **Retries**. - **Schedule Options:** split into **Run Once** and **Recurring Schedules**. Run Once covers: - the **Run at** field and the `scheduleOnceAt` API field - when the slot is consumed - auto-disable once its runs settle - cancelling, rescheduling and resuming - fired one-shots disappearing from the list - **New Launch Retries section:** - which failures are retried and which never are - the 2-minute doubling backoff - attempts and re-authorization - how retries count toward auto-pause - cancellation - **Pause/Resume, Edit, Run Statuses and Limits:** one-shot behavior, the **Retrying** status, and the retry limit. ## Verification - Checked against: - `MAX_AUTOMATION_RETRIES`, `scheduleOnceAt` and the `retrying` status in `packages/shared/src/types/automations.ts` - `LAUNCH_RETRY_BASE_DELAY_MS` in `scheduler/launch-policy.ts` - the **Schedule**, **Run at** and **Retries** fields in `automation-trigger-fields.tsx` - the public `schedules.mdx` - Prettier is clean (lint-staged ran on commit). --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/0d530639e2aec3634685f351386e9616)* --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com> Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary ColeMurray#2407 made the session timeline render completed ` ```mermaid ` blocks in assistant responses as diagrams, but nothing told users. This adds a short paragraph below the timeline table in `sessions/session-page.mdx`: - what renders - the "View diagram source" disclosure - that a diagram which can't be rendered stays as code - a tip to ask the agent for a diagram when a flow is easier to read as a picture It's a paragraph rather than a table cell so Prettier doesn't reflow the whole table. `mermaid-code-block.tsx` is added to `relatedCode`. ## Verification - Label and fallback behavior checked against `packages/web/src/components/mermaid-code-block.tsx`. - `npm test -w @open-inspect/docs` passes. Prettier is clean. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/0d530639e2aec3634685f351386e9616)* Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary ColeMurray#2499 added a **Duplicate** button to the automation detail page, but no docs mentioned it. This adds it in two places: - `automations/first-automation.mdx`: a **Duplicate** bullet in the "Pause, resume, edit, duplicate, or delete" step. - `docs/AUTOMATIONS.md`: a **Duplicate** section under Managing Automations. Both cover what matters when making a copy: - The copy opens in the create form as `<name> (copy)` and is saved only on **Create Automation**. It starts with no run history and with you as executor. - Webhook keys are never copied (new ones are issued), and the form asks for the Sentry client secret again. - If you can't create automations in the source's team, the copy starts in your current scope with no targets. Repositories or environments you can't use are left out. A notice above the form explains either case. - The button only appears when you can create automations. ## Verification - Checked against `automation-form-policy.ts` (`createAutomationDuplicate`, the `(copy)` suffix, keys and secret not copied), `use-automation-duplicate.ts`, the create page's notices, and the detail page's `useCanCreateAutomation` gate. - `npm test -w @open-inspect/docs` passes. Prettier is clean. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/0d530639e2aec3634685f351386e9616)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Documentation** * Updated the automation guides to explain how duplication opens a prefilled form and saves a separate automation without run history. * Clarified that copies use the current user as executor, regenerate webhook keys, and require the Sentry client secret to be entered again. * Documented how the form handles unavailable teams, repositories, and environments, including notices when it uses a different scope or omits unavailable targets. <!-- 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
Codex sub-agents now show and behave the way the Claude and OpenCode
harnesses' sub-agents already do. Each sub-agent appears as a `task` row
with its activity grouped underneath, and the prompt finishes only once
its sub-agents have finished. All changes are in `harness/codex/`; the
bridge contract, control plane and web app are unchanged.
### Why this is needed now
Six of the eight Codex models we offer (`gpt-6.1-sol` (the default),
`gpt-6-sol`, `gpt-6-astra`, `gpt-6-luna`, `gpt-5.6-sol`,
`gpt-5.6-terra`) declare Codex's v2 multi-agent runtime. For these
models Codex offers the `collaboration` tools (`spawn_agent`,
`wait_agent`, `followup_task`, …) regardless of
`features.multi_agent=false`.
So on `main`, a Codex session can already spawn sub-agents. Today:
- their activity and token usage are dropped;
- their OpenInspect tool calls are rejected;
- the prompt can finish while they keep running in the sandbox.
This was verified against Codex 0.163.0 with a local mock Responses API,
including `model/list` output and the harness's exact launch arguments.
No setting found in 0.163.0 turns the v2 tools off.
## Behaviour
- **Task rows.** A parent-thread `subAgentActivity` (`started`) becomes
a `tool_call` with `tool: "task"`:
- `callId` is `codex:<turn>:agent:<thread>`;
- `args` are `{description: <task name>, task_id: <agent path>}`;
- `childSessionId` is the sub-agent's thread id.
- **Grouping.** The sub-agent's own tool calls use the existing
projection, tagged `isSubtask` / `taskCallId` / `childSessionId`, so the
timeline groups them under the task. Its assistant text is not streamed;
its final message becomes the task's `output`. A failed or interrupted
sub-agent marks the task as `error`.
- **Settlement.** The prompt finishes only after the parent turn and
every sub-agent turn rooted at it (0.163.0's `rootTurnId`) have ended.
This matches the Claude harness, which disables background sub-agents.
If the parent turn is interrupted or fails, its running sub-agents are
interrupted. `abort()` interrupts both the parent and its sub-agents.
Containment through `stop_execution` is unchanged, because sub-agents
run inside the same app-server process.
- **Usage.** Each sub-agent turn reports its own `step_start` /
`step_finish` (subtask fields, `stepId codex:<thread>:<turn>`), which
lands in `step_usage` with `is_subtask`. Usage is tracked per thread:
- a newly spawned thread starts from zero;
- a sub-agent re-tasked by a later prompt is billed only for new work;
- an unknown or regressing baseline is not billed, the same rule the
parent follows.
- **OpenInspect tools in sub-agents.** `item/tool/call` from a running
sub-agent turn of the current prompt is now accepted, as sub-agents can
in the other harnesses. A tool call can arrive before the notification
that starts its turn has been processed, so ownership waits briefly for
it.
- **Limits.**
- **Depth.** One level, matching the timeline's one-level grouping. A
sub-agent's own sub-agents are interrupted as soon as their turn starts.
In v2, Codex ignores `agents.max_depth`, and in testing a chain of
sub-agents recursed to roughly 50 levels.
- **Concurrency.**
`features.multi_agent_v2.max_concurrent_threads_per_session=4`, which
counts the parent thread. This pins today's default explicitly; setting
it does not enable v2 for other models.
- **v1 runtime.** It stays disabled (`features.multi_agent=false`). The
comment now explains what the flag does and doesn't control. `gpt-5.5`
and `gpt-5.6-luna` get no sub-agents, as before.
## Changes
- `harness/codex/subagents.py` (new): `CodexSubagents`, a pure projector
for one prompt's sub-agents (registry, task and nested events,
per-thread usage, refusal of nested sub-agents, ownership).
- `harness/codex/events.py`: the tool-item projection and usage math are
extracted into `tool_call_event()` and `project_usage()` so the parent
and sub-agents share them. The parent's behaviour is unchanged, and the
existing 97 event tests pass. `failure_message` is now public.
- `harness/codex/__init__.py`: the turn loop runs until sub-agents
settle; adds interrupt fan-out, sub-agent tool ownership, `abort()`
fan-out, and distrust of sub-agent usage after an abnormal exit.
- `harness/codex/env.py`: thread cap and flag comment.
- Tests:
- `tests/test_codex_subagents.py`: projector.
- `tests/test_codex_subagent_lifecycle.py`: settlement, interrupts,
nested-sub-agent refusal, tool ownership race, abort, abnormal exit.
- A native end-to-end test in `tests/test_codex_native.py`. The scripted
Responses fixture can now route items to sub-agent requests using the
`x-openai-subagent` header.
## Testing
`OI_TEST_CODEX_BINARY=/usr/local/bin/codex uv run --extra dev pytest
tests/test_codex_*.py tests/test_bridge_harness_lifecycle.py` against
the pinned **0.163.0** binary: 270 passed, 1 skipped. The skip is the
existing cross-VM restore test, which needs a snapshot fixture. `ruff
check` and `ruff format` are clean.
The native test runs a real spawn → sub-agent OpenInspect tool call →
`wait_agent` cycle. It checks:
- the completed task row with the sub-agent's answer;
- the nested tool call;
- 240 sub-agent tokens reported separately from the parent's 360.
## Known limits / follow-ups
- **No "Instructions" disclosure.** The sub-agent's prompt is not
available in v2 events: there is no `userMessage` item on the child
thread, and the `spawn_agent.message` parameter is declared encrypted.
- **Untested against a real model.** All verification used a scripted
Responses endpoint, not a real model or credential.
- **Tool wording.** The shared `spawn-child` description still tells the
model to use "the Agent tool" for sub-agent requests. A neutral wording
would change the Codex tool catalog hash, which resets native threads,
so it is left for a separate change.
---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/ab2ae12059bf8cea2e409c285b681e7b)*
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->
## Summary by CodeRabbit
* **New Features**
* Codex sessions now support up to four concurrent agents, with agent
activity and nested tool calls shown as tasks and steps.
* Prompt usage and cost totals include supported agent activity, with
available token and cost details shown for individual steps.
* **Improvements**
* Prompts wait for active agents to finish, and stopping a prompt also
requests interruption of its agents.
* Tool calls are accepted only after their turn is recognized and
associated with the active prompt.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary - Add `web_serverless_region` for the Vercel web project, retaining the `iad1` default and leaving the docs project's region unchanged. - Add an optional session Durable Object location hint, validate it during Cloudflare environment composition, and share the hinted raw dispatch between runtime calls and WebSocket upgrades. Invalid hints warn and fall back to automatic placement. - Wire both settings through Terraform plan/apply and document placement selection, verification, redeployment requirements, and rollback. - Keep the Worker-only setting out of the Node host's configuration reader while preserving its exhaustive compile-time configuration check. Closes COL-305. ## Validation - Confirmed new runtime, parser, WebSocket, workflow, and Terraform wiring/validation regressions fail without their corresponding changes. - Terraform recursive formatting check, backend-disabled initialization, validation, and all production Terraform tests: passed (91 tests). - `npm run test:terraform-workflow-contract`: passed (29 tests). - `npm run build -w @open-inspect/shared`: passed. - Targeted control-plane tests, including Cloudflare dispatch and Node configuration contracts: passed (34 tests). - Worker lifecycle boundary integration tests: passed (6 tests). - Real sandbox WebSocket upgrade integration test: passed, preserving the `101` response. - Full `npm run typecheck -w @open-inspect/control-plane` and `npm run lint`: passed. - `npm run format:check` reports a pre-existing ignored generated helper, `.opencode/tool/_memory-contract.js`. Checking formatting with only that helper excluded passes; the helper is not part of this PR. ## Operator Verification No production settings were changed. Enable one setting at a time after merge and deployment. 1. Apply the web region override, then redeploy the web app so its build pulls the updated Vercel project settings. Verify the function location in Vercel Runtime Logs (Function details) or `proxy.lambdaRegion` in log drains and compare browser/request timings and Worker telemetry (`$workers.event.request.cf.colo` on `cf-worker-event`). 2. Apply the session location hint, create a new session, and verify its placement using GraphQL `durableObjectsPeriodicGroups` grouped by `coloCode` and filtered by `objectId`. Compare new and existing session timings, including sandbox event latency. 3. To roll back, unset the relevant override, apply, and redeploy the web app if its region changed. Existing Durable Objects do not move; location hints are best-effort and affect only newly created objects. Worker Smart Placement, D1 placement/replication, and sandbox-provider regions remain out of scope. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/32633e150d6b263f16f8a997c8f92d63)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit - **New Features** - Added configurable deployment regions for the web application and session services. The web application defaults to `iad1`; session placement remains automatic unless a supported location is configured. - Session location settings apply to newly created session objects. Existing objects stay in their current locations, including after the setting is changed or removed. - **Documentation** - Added guidance on supported region values, deployment timing, verification, and rollback 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>
…eMurray#2538) ## Summary - Cache validated branch lists per case-insensitive repository key using the existing engine-neutral cache store and SCM identity fingerprint. - Serve entries fresh for 60 seconds, retain them for one hour, and refresh stale entries in the background. Cold requests await the same refresh promise registered with the background-task context so a disconnected caller can still populate the cache. - Include `cached` and `cachedAt` in branch responses and record `kv_read_ms` plus `scm_api_ms` when fetching. Provider failures are never cached; background failures leave stale data intact, and synchronous errors retain their existing responses. - Use GitHub's first-page `Link` last-page number to fetch later pages concurrently, preserving page order and the 500-branch limit. Keep sequential pagination when no usable last-page link exists. Link URLs are never followed. ## Behavior And Scope - Newly pushed branches become visible after roughly the 60-second fresh window plus a request to trigger revalidation and refresh completion. Cross-location KV propagation can add delay. The picker still searches only fetched branches. - Route authorization remains ahead of cache access. Repository-list caching behavior and GitLab pagination are unchanged. Concurrent cold/stale branch refreshes share one in-flight promise per repository and SCM identity within each isolate; entries are released on success or failure. - SQL cache writes reclaim expired rows, including abandoned repository keys. Reads remain non-writing, and live/permanent entries are retained. Idle caches retain expired rows until their next write but cannot grow while idle. - A dedicated `repos_branches_cache.fetch` event reports SCM duration independently of HTTP request completion, including background and failed fetches. ## Validation - Passed: 131 focused tests across branch routes, cache entries, GitHub pagination/token caching, SQL expiry reclamation, Node host/cache initialization, and Node cache conformance (`--maxWorkers=1`). - New concurrent-refresh, expiry-reclamation, and background-timing regressions reproduced before their fixes. - Confirmed the new cache-hit/stale-refresh/identity behavior tests and concurrent-pagination regression tests fail against the original implementations before applying the fixes. - Passed: `npm run typecheck -w @open-inspect/control-plane` (all four TypeScript programs). - Passed: `npm run lint -w @open-inspect/control-plane`. - Ran `npm run format:check`; its only failure is the pre-existing ignored runtime tooling file `.opencode/tool/_memory-contract.js`, which was left untouched. Prettier checks pass for all changed files. - Passed: `git diff --check` and commit hooks. - Passed: `npm run test:integration -w @open-inspect/control-plane -- test/integration/cache-store-conformance.test.ts --maxWorkers=1` (26 passed, one expected KV-expiry skip). - Attempted the full integration suite with one worker; it exceeded the 10-minute command limit after workerd teardown/reset errors. The isolated cache integration run passed. ## Post-Deploy Verification Production verification requires operator credentials. Check that repeated branch requests within the fresh window omit `scm_api_ms`, that stale requests return promptly while refreshing, and that branch-route and home-page completion latency improve without changing authorization or error behavior. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/c0a5ab55a8272edaad5a8cdf50464290)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Repository branch listings now support caching for GitHub and GitLab. Fresh results can be reused, while older cached results remain available during background refreshes. * Branch listing responses now include whether results came from cache and when they were cached. * **Improvements** * GitHub branch listings now retrieve multiple pages, up to 500 branches, to include more results for repositories with large branch lists. * Expired cache entries are reclaimed during successful writes, helping limit stale cache data. <!-- 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>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Terraform Validation ResultsValidation job: success
Pushed by: @jasoncuriano, Action: |
ScottKirschner
approved these changes
Oct 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Routine upstream sync per
upstream-sync-runbook.md. Brings the fork up to0c7bc0dd.upstream/main— no fork-local changespackage-lock.jsonuntouched (zero version/resolved/integrity changes)Verified locally before push:
npm run build(all packages)npm run typechecknpm test -w @open-inspect/control-planedist/index.js× 5Notes:
packages/docspackage.terraform/modules/opencomputer-inframodule — review the Terraform plan before applying.🤖 Generated with Claude Code