chore: sync with upstream 2026-09-02 (conflicts) - #124
Draft
NicolasWalter wants to merge 98 commits into
Draft
Conversation
…gest closure bags (ColeMurray#1608) ## What First PR of the deps-style normalization campaign (follow-through on the ColeMurray#1594–ColeMurray#1604 decomposition): replace the composition root's three biggest closure-bag literals with composition classes, per the house deps standard from the ColeMurray#1045-series (pass collaborators directly with full types; give a closure group that shares collaborators a named class). Behavior-preserving — no port changes, no call-flow changes. ## Changes - **`DurableObjectSandboxStorage`** (new `session/sandbox-lifecycle-adapters.ts`) implements the lifecycle manager's `SandboxStorage` port over its four real collaborators: `SandboxRepository`, `SessionCoreRepository`, `UserEnvResolver`, and the secrets encryption key. Replaces the 28-property literal in the root. The encrypt-before-store rule (code-server/VNC/ttyd secrets), previously copy-pasted three times inline, is one private `encryptIfConfigured` method. - **`LifecycleSocketAdapter`** (same file) implements the manager's `WebSocketManager` port over `SessionWebSocketManager` — the name translation and the no-socket send branch get a typed home instead of a literal. - **`SessionClientCommandFacade`** (new `session/client-command-facade.ts`) implements the message router's `SessionClientCommands<WebSocket, ClientInfo>` port with the four services as constructor deps. The port itself stays generic — that genericity is what lets the server stack unit-test over string connections, so the facade is the production binding, not a port rewrite. The router's client-message type aliases are now exported (they are referenced by the exported port, so naming them outside the module was already implied). Net: 39 function-valued props removed from `components.ts`; the root now constructs objects in these three spots instead of authoring behavior inline. ## Tests New `sandbox-lifecycle-adapters.test.ts` covers the pieces with real logic, which previously lived untested inside the root literal: the encrypt-when-configured branch (round-trips via `decryptToken`), the plaintext-passthrough branch, the repository-shape defaults (`baseBranch` → `"main"`, missing row → `baseSha: null`), the `setLastSpawnError` → `updateSandboxSpawnError` rename, and both `sendToSandbox` branches. Pure forwards stay covered through the manager and server suites. ## Queue context Next in the campaign (separate PRs): handler deps-bags → classes (normalizing the 7-factory/5-class split), vestigial thunk removal (`getLogger: () => log` first), and the `test/integration` typecheck spike. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Refactor** - Improved session command handling for prompts, execution controls, typing indicators, presence, subscriptions, and history. - Improved sandbox lifecycle and WebSocket handling for more consistent session connectivity. - **Security** - Sandbox access credentials can now be encrypted when configured, while retaining compatibility with existing setups. - **Tests** - Added coverage for credential storage, sandbox startup errors, repository behavior, and WebSocket communication. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…lve the storage middle-man (ColeMurray#1609) ## What Campaign item 2, combining two agreed decisions: **the secrets encryption key is required** (it always was operationally — Terraform declares it with no default — but the code treated it as optional and silently fell back to storing plaintext), and **the storage middle-man from ColeMurray#1608 is dissolved** (its ~25 one-line pass-throughs were the smell that prompted the design discussion). ## Encryption key is required - New `requireRepoSecretsEncryptionKey(env)`: the session graph throws at construction when the key is absent (the ColeMurray#1602 eager posture — a misconfigured deployment fails every request at initialization instead of running degraded), and the five MCP-server routes validate the same way. - Every plaintext-**write** fallback is deleted: the sandbox access-secret stores, `McpServerStore`'s keyless branch, and `UserEnvResolver`'s "skip secret loading" branch. `isManagedSecretsConfigured` reduces to `Boolean(db)`. - Plaintext-**read** fallbacks stay: pre-encryption legacy rows still decrypt-or-degrade exactly as before (`McpServerStore`'s catch fallback, access values resolving to null on decrypt failure). - The integration environment already provides a test key in its miniflare bindings, so no test-infra changes were needed. ## Encryption is owned by persistence; the middle-man is gone - `SandboxRepository` takes the key at construction and encrypts code-server/VNC/ttyd secrets inside its write methods — the same pattern the D1 stores already use. No caller can persist an access secret in the clear, structurally. - The manager's conflated port is **split into two roles** — the root cause behind both the ColeMurray#1608 forwarding layer and an interim inheritance design. `SandboxStorage` shrinks to the sandbox-row contract, which `SandboxRepository` now satisfies **structurally** (no adapter, no subclass, and no manager-port import in the repository — the structural check happens at the composition boundary). The three session-context reads become their own `SessionContextReader` port, implemented by a small `LifecycleSessionContext` facade over `SessionCoreRepository` + `UserEnvResolver` — an honest adapter: it spans two collaborators and owns the repository-shape defaults. `DurableObjectSandboxStorage` is deleted. - The shared test mock already implements both ports, so the manager's test harness changes are mechanical: the same fake is passed for both parameters at every constructor site. - `updateSandboxSpawnError` is renamed `setLastSpawnError` to match the port vocabulary, removing the last name translation. ## Tests Encryption round-trips (via `decryptToken`) now live in `sandbox-repository.test.ts` with the logic; the adapter tests pin the context mapping and the inheritance wiring ("sandbox writes hit SQL with no forwarding layer"). Deleted-behavior tests are deleted with their behavior: the keyless verbatim-read test, the resolver's skip-secret-loading test, and ColeMurray#1608's synchronous-keyless-persist test (that branch no longer exists — with the key required, every secret write takes the same WebCrypto await it always took on real deployments). `McpServerStore` tests construct keyed; their plaintext-seeded rows now exercise the legacy-read fallback, which is exactly what such rows are. ## Behavior change (intended) A deployment without `REPO_SECRETS_ENCRYPTION_KEY` now fails loudly at session initialization and on MCP routes, instead of silently persisting secrets unencrypted. Valid deployments are unaffected. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Security** * Repository secrets encryption is now required for control-plane operations. * Sandbox passwords, tokens, credentials, and stored environment secrets are encrypted before persistence. * Encryption keys are strictly validated for required format and length. * **Bug Fixes** * Improved handling of unavailable or empty stored secrets. * Reduced unnecessary decryption errors for empty credentials. * Improved sandbox error reporting. * **Refactor** * Streamlined sandbox lifecycle and session-context handling for more consistent behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - move the six Python CI jobs into a dedicated `CI (Python)` workflow - keep the seven Node.js/TypeScript jobs in `CI (TypeScript)` - trigger each workflow only for its package and root-tooling dependency surface - preserve the Markdown-only exclusions added in ColeMurray#1590 ## Motivation The main CI workflow currently runs both ecosystems for every code change. This split prevents Python-only changes from allocating TypeScript runners and TypeScript-only changes from allocating Python runners, while preserving all existing job commands and dependencies. This is the ecosystem-level step before introducing narrower package-aware filtering in follow-up PRs. ## Validation - `npx prettier --check .github/workflows/ci.yml .github/workflows/ci-python.yml` - parsed both workflows and verified all 13 original job definitions remain present - `git diff --check` `actionlint` and Go were unavailable in the local environment. The repository-wide `npm run format:check` also reports a pre-existing formatting issue in `.opencode/package.json`; both changed workflow files pass their targeted formatting check. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/6a9584bdf48356904b0771920dcb9482)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Added dedicated continuous integration checks for Python linting, formatting, type checking, and tests. * Updated TypeScript validation to run through a dedicated workflow. * Refined workflow triggers to focus on relevant code and configuration changes, excluding documentation-only updates. * Expanded validation coverage for runtime, deployment, and infrastructure changes. * Added concurrency controls to cancel outdated runs and strengthened workflow security settings. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
while working on ColeMurray#1037 i noticed that the e2b sandboxes started by the current template were failing to run bun despite being installed by the dockerfile. The Dockerfile previously ran the installer like this: `BUN_INSTALL=/usr/local curl ... | bash` That environment variable applied to `curl`, not the `bash` process running the installer. Bun therefore used its default install location, which was outside the runtime user's PATH. This change passes `BUN_INSTALL=/usr/local` to `bash` and also adds `command -v bun` to the template readiness check. ### Before <img width="1228" height="755" alt="e2b-bun-issue-before" src="https://github.com/user-attachments/assets/781533c4-5983-4262-bcf8-acb0cdddcf26" /> ### After <img width="1231" height="782" alt="e2b-bun-issue-after" src="https://github.com/user-attachments/assets/7258ce2b-8f53-42f3-9a98-2a8603181fa5" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Template readiness checks now verify that Bun is available before finalization. * **Chores** * Improved the Bun installation setup during environment creation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…asses (ColeMurray#1612) ## Summary Item 3 of the deps-style normalization campaign (follow-up to ColeMurray#1608/ColeMurray#1609): the seven session HTTP handlers still built as `createXHandler(deps)` factories over deps-bags become classes with direct constructor collaborators, matching the `SessionDiffsHandler` (ColeMurray#1047) and `AttachmentsHandler` precedents. One prerequisite commit makes `TOKEN_ENCRYPTION_KEY` required, mirroring ColeMurray#1609's treatment of the repo-secrets key. The deps-bags were where most of the composition root's pure same-name forwards lived — closures like `getSession: () => sessionCoreRepository.getSession()` that exist only because a bag can't hold the repository itself. Net effect in `components.ts`: 43 function-valued closure lines removed, 8 added back as named per-request adapters (−35), and all seven `XHandlerDeps` interfaces deleted. ## `TOKEN_ENCRYPTION_KEY` is now required (first commit) Terraform already requires the key (no default, `sensitive`) and the `Env` type declares it non-optional — the three falsy-guards were silent-degradation branches: - `identity.ts` silently dropped stored SCM tokens from GitHub enrichment, - the session graph silently skipped constructing the user token store, - session init silently discarded a plaintext SCM token instead of encrypting it. `requireTokenEncryptionKey(env)` shares the AES-256 material validator with `requireRepoSecretsEncryptionKey` (strict base64, exactly 32 decoded bytes) and is thrown at session-graph construction, so a misconfigured deployment fails every request at init rather than degrading. Plaintext-read paths are untouched. ## Conversion rules (uniform across all seven) - **Collaborators become constructor params with their real types** — repositories, services, messenger. `deps.getSession()` → `this.sessionCoreRepository.getSession()`. - **Constant thunks become data** — `getDurableObjectId: () => durableObjectId` → `durableObjectId: string`; `isManagedSecretsConfigured: () => Boolean(db)` → `managedSecretsConfigured: boolean` (fixed at composition). - **Module functions re-wrapped only to bind composition-time values are called directly** — `resolvePublicSessionId(session, this.durableObjectId)`, `parseArtifactMetadata(artifact, this.log)`, `validateReasoningEffort(model, effort, this.log)`; same instances, same arguments as the deleted closures. - **Genuine adapters stay function-typed params** (8 total): the three per-request token/credential service factories on `SandboxHandler`, the request-log-scoped `createPullRequest` factory + `getSessionUrl` + background `triggerPullRequestRefresh` on `PullRequestHandler`, and `scheduleWarmSandbox` + `cancelSession` on `SessionLifecycleHandler`. - **Seams stay functions without eta-expansion** — the root passes `generateId`/`hashToken`/`encryptToken`/`isValidSandboxToken` as bare module references; `now` defaults to `Date.now` per the `AttachmentsHandler` precedent. - **The class replaces the same-named interface**, so the internal route table (`components.ts` tier 9) is untouched — those wrappers adapt the uniform route signature to method arities and are not forwards. - `SessionLifecycleHandler`'s cancel path reuses the lifecycle `WebSocketManager` port via a `LifecycleSocketAdapter` instance (ColeMurray#1608) instead of two raw socket forwards; the adapter's `sendToSandbox` performs the identical resolve-then-send. - `PullRequestHandler`'s local result-union aliases were byte-identical to `ParticipantService`'s declared return types and are deleted. ## Behavior notes - Behavior-preserving except the deliberate key-requirement change above. - Tests now exercise the real `resolvePublicSessionId` (via `session_name` fixtures) and the real `validateReasoningEffort` (whose catalog answers match what the old stubs returned) instead of stubs. - One commit per handler group; every commit is independently green. ## Testing - `tsc --noEmit` (prod + test configs), ESLint, Prettier - Unit: 205 files / 3186 tests green - Integration (workerd + real D1): green <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added validation for the token encryption key used to protect OAuth tokens. * Token-based identity enrichment now requires valid encryption-key configuration. * **Bug Fixes** * Improved configuration errors for missing, malformed, or incorrectly sized encryption keys. * **Refactor** * Updated session and HTTP request handling for more consistent dependency management without changing endpoint behavior. * **Tests** * Expanded coverage for encryption-key validation and token-related session flows. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary Behavior-preserving follow-up to ColeMurray#1608/ColeMurray#1609/ColeMurray#1612 (deps-style normalization, per the ColeMurray#1045–ColeMurray#1049 standard): drop the vestigial logger thunks. Five sites took the session logger as a zero-arg function (`getLogger: () => Logger` / `getLog: () => Logger`) and called it on every use; all five are fed a value that is constant after composition, so they now take `log: Logger` directly. The thunks existed for the DO-era log swap: `SessionDO` used to reassign its logger once the public session id resolved, so anything that captured a logger by value at construction time kept logging the stale id. That mechanism is gone — the composition root builds one session-scoped logger whose `session_id` is injected **per emit** through the latched resolver (`components.ts`: "for every component in the graph, however early it captured the logger"). The comment in `sandbox-events.ts` justifying its getter ("The DO swaps its logger for a request-scoped child during fetch()") described behavior that no longer exists. ## Changes | Site | Before | After | | --- | --- | --- | | `SessionHttpDispatcher` deps | `getLogger: () => Logger` | `log: Logger` | | `SessionMessageRouter` deps | `getLogger: () => Logger` | `log: Logger` | | `SessionDisconnectHandler` deps | `getLogger: () => Logger` | `log: Logger` | | `SessionSandboxEventProcessor` ctor | `getLog: () => Logger` + `private get log()` accessor | `private readonly log: Logger` (accessor deleted; internal `this.log` uses unchanged) | | `createCloudflareBackgroundTasks` | `getLogger: () => Logger = () => log` | `logger: Logger = log` (worker/scheduler callers use the default, unchanged) | Composition root: the three `getLogger: () => log` props and two `() => log` arguments become `log`. ## What deliberately stays a function Everything that is genuinely dynamic, per the campaign's classification: - **Latched resolvers** — `getSessionId` (DO id until the session row exists, public id after). - **Live queries** — `getStatus`, `getAuthenticatedClients`, `getSandboxSocket`, `getProcessingMessageAuthor`, `isSpawning`. - **Post-init freshness reads** — `getExecutionTimeoutMs`. - **The SCM provider cell** — `() => scmProvider` reads a mutable `let` that live-DO integration tests substitute after graph construction. - **Clock/id seams and adapters** — `now`, `generateId`, action-shaped deps. ## Testing - `npm run typecheck -w @open-inspect/control-plane` (both tsconfigs) clean - `npm run lint -w @open-inspect/control-plane` clean - Unit: 3187 passed; integration: 1002 passed <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Updated session and background task components to receive logging instances directly. * Streamlined error, request, message, disconnect, and sandbox-event logging. * Preserved existing session handling, cleanup, reconnection, and close behavior. * **Tests** * Updated automated tests and test setup to match the simplified logging configuration. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary `test/integration/**` (91 files) was never typechecked — eslint covers `src/` only, and the tsconfigs excluded the directory. Store-signature drift there has repeatedly survived until runtime (`D1_TYPE_ERROR` mid-suite; most recently a stale `SandboxRepository` construction found during ColeMurray#1609). This PR adds `tsconfig.integration.json`, fixes everything it surfaced (1,033 errors initially, most from one root cause), and wires it into `npm run typecheck` so CI enforces it from now on. ## The config - Extends the production tsconfig with `types: ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers/types"]` — the integration files execute inside workerd, so they compile against workers types **without Node globals** (same boundary rationale as the prod config; Node-context files like `vitest.integration.config.ts` run in the Vite host and are not part of this program). - The pool's `cloudflare:test` declarations live at the package's `./types` subpath export (v0.16 layout). The old root-package reference silently loads nothing — which is why the existing `env.d.ts` was augmenting a `ProvidedEnv` interface that no longer exists. - `env.d.ts` rewritten to the v0.16 contract: merge the worker's real `Env` (plus `TEST_MIGRATIONS`) into the `Cloudflare.Env` placeholder that `env` from `cloudflare:test` is typed as. This one fix collapsed ~900 of the initial errors. - An experiment narrowing `SESSION` to `DurableObjectNamespace<SessionDO>` inside the augmentation was reverted: it makes `Cloudflare.Env` unassignable to the production `Env` at every `handleRequest(env)` call site. The production `Env` cannot be narrowed either — importing the DO class from `types.ts` is exactly what the only-`index.ts`-imports-the-adapter lint exists to prevent. Instead, stub typing happens at one seam: ## New test seams (all in existing helper files) | Helper | Why | | --- | --- | | `runInSessionDO(stub, cb)` | `runInDurableObject` with the stub typed as the session DO — the single cast asserting what the SESSION namespace hosts (43 call sites converted) | | `ctxOf(instance)` | the DO's `ctx` is `protected` on the `DurableObject` base class; storage seeding/assertions go through this one cast | | `sqlDatabase(env.DB)` | plain assignment (no cast) viewing D1 through the engine-neutral `SqlDatabase` interface, so tests can `batch()` store-bound statements (21 sites) | | `getSetCookies(headers)` | workerd implements `Headers.getSetCookie()` but this workers-types version doesn't declare it — same cast `src/routes/browser-auth.ts` carries | ## Latent drift the checker caught (the point of the exercise) All fixed behavior-preservingly: - **`AutomationRow` fixtures still carried `repo_owner`/`repo_name`/`base_branch`/`repo_id`** (6 files) — dead since repos moved to the `automation_repositories` junction table; linkage in the affected tests already flows through `replaceRepositories(...)`. - **Run fixtures set `concurrency_key`** — it lives on invocations now, so the seeded value never reached any table. Note for a follow-up: the scheduler-events "does not block a different concurrency key" test seeds its active run without any key either way, so it doesn't currently distinguish per-key scoping from no-key blocking (left as-is; runtime unchanged). - **Browser-auth router tests passed a raw `ExecutionContext` where the router now takes `BackgroundTasks`** (3 files) — worked only because the failure path never ran. Now wrapped with `createCloudflareBackgroundTasks`, mirroring `index.ts`. - **`stubSourceControlProvider` was missing `resolveCommit`/`listTree`/`readBlob`** — the provider read-surface added for skills import; stubbed with the suite's existing `notUsedHere` idiom. - **A session fixture wrote status `"initializing"`** — removed from the status vocabulary (ColeMurray#1554); now `"active"`. - **`generateId({ model: "user" })`** — Better Auth's canonical generator takes no arguments; the argument was silently ignored. - **`ensureInitialized` still passed in a `SessionPlatform` stub** — unthreaded by ColeMurray#1604. - **Repository skill assignments missing the now-required `baseBranch`**, and **image-build correlation contexts missing the required `trace_id`**. Plus mechanical strictness fixes (WebCrypto union narrowing in the Google id-token helper, `json<T>()` typing, non-null assertions where `subscribe: true` guarantees replay messages). `session-do-access.ts`'s old comment — "test/integration/** is never typechecked (eslint + grep are the only static gates here)" — is retired. ## Testing - `npm run typecheck` (now three programs) clean - Unit: 3187 passed; integration: 1002 passed — no behavioral change - Prettier over the touched files <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Improved integration-test coverage and type-checking across authentication, sessions, automations, scheduling, webhooks, and Durable Object workflows. * Updated test infrastructure for more reliable cookie handling, database batching, background tasks, and session state access. * Refined fixtures and assertions to reflect current repository, concurrency, and session behavior. * **Chores** * Updated test TypeScript configurations and runtime type definitions for improved validation and editor support. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - count bridge heartbeats as sandbox activity while a message is processing - keep idle heartbeats liveness-only so abandoned sandboxes still reach inactivity cleanup - add unit and Durable Object integration coverage for both states ## Motivation A long-running tool call can emit no agent events for longer than the sandbox inactivity timeout even though the bridge remains healthy. Previously, bridge heartbeats refreshed only heartbeat liveness, so the lifecycle alarm could classify the sandbox as idle and stop it mid-execution. The sandbox event processor already owns which incoming events count as activity. While a message is processing, a live bridge heartbeat now renews the existing activity timestamp. After processing finishes, heartbeats no longer renew activity and ordinary idle cleanup remains unchanged. This is a deliberately narrow alternative to ColeMurray#1601. It does not change execution-timeout recovery, provider stop behavior, queue recovery, schema, or cleanup semantics. ## Validation - npm test -w @open-inspect/control-plane — 205 files, 3,188 tests passed - npm run test:integration -w @open-inspect/control-plane — 81 files, 1,002 tests passed - npm run typecheck -w @open-inspect/control-plane - npm run lint --workspace=@open-inspect/control-plane -- --no-fix - Prettier check for all changed files - git diff --check origin/main...HEAD <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved heartbeat tracking so idle heartbeats maintain liveness without incorrectly extending activity timers. * Heartbeats received while processing a message now correctly refresh activity status. * Heartbeat events continue to be excluded from stored event history. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary Closes out the deps-style normalization campaign (ColeMurray#1608/ColeMurray#1609/ColeMurray#1612/ColeMurray#1615/ColeMurray#1616): the last-resort `"main"` base-branch fallback was written as a literal at seven independent sites. Per the repo convention ("define each default value exactly once — extract to a named constant and import everywhere"), it is now `DEFAULT_BASE_BRANCH` in `src/repos/default-branch.ts`, imported at all seven. Deferred from the ColeMurray#1608 review round. ## The seven sites All express the same concept — the branch assumed only when neither the caller nor the SCM provider's repository metadata supplies one; configured per-repo defaults (ColeMurray#757) always win: - `repos/resolve.ts` — `input.baseBranch?.trim() || access.defaultBranch || …` - `automation/repository.ts` — same shape for automation repo selections - `routes/session-child-spawn.ts` — spawn-context fallback - `session/initialize.ts` and `session/http/handlers/session-lifecycle.handler.ts` — init-payload fallback - `session/snapshot-reader.ts` and `session/sandbox-lifecycle-adapters.ts` — legacy repository rows persisted before `base_branch` was stored Test fixtures keep their literals (they are inputs, not the default's definition). No behavior change: the constant's value is `"main"`. ## Testing - `npm run typecheck` (all three programs) clean; ESLint clean - Unit + integration batteries green - `rg '\?\? "main"|\|\| "main"' src` (non-test) → no matches <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Standardized repository branch fallback behavior across session initialization, automation, repository resolution, and child sessions. * Repositories without a configured or provider-supplied base branch now consistently use the default `main` branch. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - keep directly automated and GitHub bot sessions hidden from the Mine inbox - allow user-attributed agent children with automation lineage to appear as re-rooted Mine entries - add integration coverage for an automation root with a user-attributed child ## Root cause The Mine inbox rejected every session with a non-null `automation_id`. Child sessions inherit that ID from an automation parent, so even children created after a user follow-up were filtered out. ## Verification - `npm run test:integration -w @open-inspect/control-plane -- session-inbox.test.ts` - `npm test -w @open-inspect/control-plane -- src/routes/session-index.test.ts src/db/session-index.test.ts` - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint -w @open-inspect/control-plane` - focused Prettier check - `git diff --check` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/115a7540a10e9695039d22afac46028d)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Updated the “Mine” inbox view to include agent sessions spawned from automated sessions. * Clarified the option used to exclude automated sessions. * **Bug Fixes** * Improved inbox filtering so directly automated and GitHub Bot sessions are excluded while eligible child sessions remain visible. * **Tests** * Expanded integration coverage for automated sessions, their child sessions, and user-owned sessions in the “Mine” view. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - queues eligible GitHub PR comments and submitted reviews after signed webhook validation - re-reads authoritative GitHub state, correlates the owning session, and applies repository policy - records durable decisions and atomically admits one idempotent message into the existing SessionDO queue - enforces the rolling per-PR attempt cap and recovers ambiguous or duplicate deliveries - keeps Autofix default-off and preserves explicit mention behavior - uses D1 migration 0058 without colliding with current main ## Stack 1. This PR: human and explicitly allowlisted review feedback foundation 2. ColeMurray#1183: producer-agnostic Open Inspect App reviews 3. ColeMurray#1184: configuration, timeline, queue health, and dogfood operations ## Validation - all required GitHub checks pass - full control-plane, web, bot, shared, Python, build, typecheck, lint, format, integration, and Terraform validation jobs pass - targeted D1 Autofix integration passes ## Rollout Autofix remains disabled by default. This PR does not enable any production repository. Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - accepts actionable submitted reviews authored by the exact configured Open Inspect App login and Bot actor type - keeps the dedicated Open Inspect review setting independent from third-party bot allowlists - rejects App-authored PR comments, approved reviews, empty reviews, and matching human logins without normal write permission - requires no producer-session metadata, publication receipt, special sandbox tool, or reviewer prompt change ## Why Autofix consumes authoritative GitHub reviews. Built-in review sessions and custom automations can continue publishing reviews through their existing GitHub mechanisms. Eligibility depends on the provider-read App identity and repository setting, not on which Open Inspect workflow produced the review. ## Stack - Depends on ColeMurray#1182 - Base branch: pr-feedback-autofix-human - Next: ColeMurray#1184 configuration, timeline, queue health, and dogfood operations ## Validation - repository typecheck, lint, and format check - full affected shared, control-plane, GitHub bot, and web suites - focused own-App eligibility and ingress tests - targeted D1 Autofix integration - Terraform format check ## Rollout Open Inspect review Autofix remains disabled by default. Existing review producers require no change. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved pull request feedback processing to recognize authoritative reviews from the configured Open Inspect app. * Actionable reviews can now be queued without an additional permission check. * Inline-only review comments are supported. * **Bug Fixes** * Improved filtering for unauthorized bots, bot comments, disabled review handling, non-actionable reviews, and reviewers without write permission. * Removed an incorrect attribution-based rejection case. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - adds global and repository-override Autofix settings with default-off behavior - explains that exact Open Inspect App reviews are eligible regardless of producer workflow - warns operators before trusting third-party bot input or raising attempt limits - labels admitted feedback with the existing generic review origin in the session timeline - adds primary Queue and DLQ health inspection without delaying scheduled work - documents producer-neutral dogfood, triage, and kill-switch procedures - makes warranted originating-PR outcome responses explicit ## Stack - Depends on ColeMurray#1183 - Base branch: pr-feedback-autofix-open-inspect-review - Final PR in the stack ## Validation - all required GitHub checks pass - full control-plane, web, bot, shared, Python, build, typecheck, lint, format, integration, and Terraform validation jobs pass - independent thermo review and closure re-review pass - independent revised-plan adherence review passes with no deviations ## Dogfood gates This PR does not enable a repository. Before dogfood: - configure external alert routing for Queue and DLQ health events - exercise both the built-in reviewer and an existing custom review automation - verify duplicate delivery, timeline provenance, and attempt-cap behavior - explicitly accept the absence of an authoritative spend budget or add that platform capability first <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added GitHub PR feedback Autofix settings, including review/comment triggers, approved bot accounts, and attempt limits. * Added per-repository Autofix overrides. * Session timelines now show whether work resumed from a human or bot comment/review, with a link to the feedback. * GitHub avatars now use stable profile images. * **Bug Fixes** * Improved Autofix queue monitoring and operational alerts. * **Documentation** * Added a rollout and troubleshooting runbook for PR Feedback Autofix. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - replace the generic `create-pull-request` argument/output disclosure with the selected pull request preview treatment - render agent-authored PR bodies as sanitized Markdown without assuming Summary or Verification sections - parse current created, updated, draft, manual, pending, and failure output variants while preserving unknown output verbatim - validate external PR links and keep long descriptions progressively disclosed - add focused coverage for rendering, lifecycle states, unsafe URLs, arbitrary body formats, and case-insensitive tool dispatch ## Verification - `npm test -w @open-inspect/web -- src/components/create-pull-request-event.test.tsx src/components/tool-call-item.test.tsx` - `npm run lint -w @open-inspect/web` - `npm run typecheck -w @open-inspect/web` - `git diff --check` ## Testing note - the full web suite completed all 1,226 assertions successfully, but Vitest exited nonzero because the pre-existing `sandbox-settings.test.tsx` timeout callback fired after jsdom teardown (`window is not defined`) --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/6e4947f5c6a40da91e6ca16c2823cbb7)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added rich pull-request timeline events for creation, updates, drafts, pending states, failures, and manual creation. * Added expandable descriptions with Markdown support, branch details, links, and status indicators. * Added safe handling for external links and unrecognized pull-request output. * **Bug Fixes** * Pull-request tool calls now consistently use the specialized display, including mixed-case names. * **Tests** * Added comprehensive coverage for pull-request states, expansion behavior, link safety, and fallback rendering. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - replace the Autofix session HTTP handler factory with a class - inject `SessionAutofixService` directly through the constructor - update session composition and handler tests to use the class API - preserve the existing route adapter, validation, logging, and response behavior ## Context This aligns the Autofix endpoint with the class-based session HTTP handler pattern established in ColeMurray#1612. ## TDD - changed the handler test to instantiate `AutofixHandler`, confirming the red state with `AutofixHandler is not a constructor` - implemented the class and reran the focused test to green ## Validation - `npm run build -w @open-inspect/shared` - focused Autofix handler tests: 2 passed - `npm test -w @open-inspect/control-plane`: 3,253 passed - `npm run test:integration -w @open-inspect/control-plane`: 1,006 passed - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint -w @open-inspect/control-plane` - targeted Prettier check - `npm run build -w @open-inspect/control-plane` - `git diff --check` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/a531a7ca7d9557ead0ead1e406f70652)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Maintained autofix request handling, validation, error responses, and service dispatch behavior. * Updated internal handler wiring without changing the user-visible autofix experience. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…urray#1620) ## Summary - replace `Response` return values from Scheduler tick, event, manual trigger, run completion, and health operations with operation-specific typed results - remove the synthetic `Scheduler.dispatch()` HTTP router after confirming it had no production callers - serialize Scheduler outcomes only in the real automation and webhook HTTP adapters while preserving their status codes and JSON bodies - make in-process automation completion acknowledgement and retryable failure outcomes explicit, retaining the existing two-attempt retry policy without interpreting HTTP statuses - update Scheduler unit and integration tests to invoke typed application methods directly, while retaining route/webhook HTTP contract coverage ## External Contract Preservation - manual trigger success remains `201` with `{ invocationId, runs }` - active manual runs remain `409` with `{ error: "A run is already active for this automation" }` - trigger launch failures and authoritative lookup/validation failures remain wrapped as `500` by the public route - normalized event, generic automation webhook, and Sentry webhook success bodies remain `{ ok: true, triggered, skipped, steered }` - event forwarding exceptions remain `502` at the normalized event adapter - request validation and authentication continue to run before Scheduler invocation ## Completion And Retry Behavior - completed and ignored run callbacks are explicit acknowledged outcomes - invalid callback input is an explicit retryable Scheduler failure, preserving the previous behavior where the callback service retried a non-2xx Scheduler response - thrown D1/application failures still retry once and remain distinct from typed Scheduler rejections - completion remains best-effort after both attempts, matching existing notification behavior ## Dispatch Removal Evidence Repository-wide call inspection found `Scheduler.dispatch()` only in Scheduler unit/integration test shims. Production invokes `tick()`, `event()`, `trigger()`, and `runComplete()` directly, and there is no external Scheduler service or Durable Object binding. The fake router and its unknown-route tests were therefore removed rather than retained as a compatibility layer. ## Verification - `npm test -w @open-inspect/control-plane -- src/scheduler/scheduler.test.ts src/routes/automations.test.ts src/session/callback-notification-service.test.ts src/webhooks/automation-event.test.ts src/webhooks/automation-webhook.test.ts` (229 tests) - `npm run test:integration -w @open-inspect/control-plane -- test/integration/scheduler.test.ts test/integration/scheduler-events.test.ts test/integration/scheduler-slack-events.test.ts test/integration/webhooks.test.ts test/integration/webhooks-slack.test.ts test/integration/webhooks-github-pr-lifecycle.test.ts` (85 tests) - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint -w @open-inspect/control-plane` - `npm run build -w @open-inspect/control-plane` - code-simplifier review completed; no generic result framework or compatibility adapter was introduced ## Migration Impact No database, shared-package, deployment, or external API migration is required. This is an internal control-plane application boundary change; direct TypeScript callers now consume discriminated results instead of decoding synthetic HTTP responses. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/2576829fb50115431a5a2451edc7128f)* --------- Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary - replace the storage-shaped image-build status DTO with a camelCase public API contract - expose `repositoryShas` as validated `RepositoryShaEntry[] | null` instead of leaking the D1 JSON string - keep snake_case rows and `repository_shas` internal to control-plane persistence - decode each status row once at the control-plane response boundary and map malformed historical provenance to `null` - move the canonical repository provenance Zod schemas into `@open-inspect/shared` and reuse them for callback and stored-row validation - remove the web JSON parser and consume typed provenance directly while preserving status folding, fingerprint filtering, primary SHA display, and duration formatting ## HTTP Contract Image-build status records now use public camelCase names, including `scopeKind`, `scopeId`, `repositoriesFingerprint`, `runtimeVersion`, `buildDurationSeconds`, `errorMessage`, and `createdAt`. `repositoryShas` is a decoded array or `null`; `repository_shas` and all other D1 encodings are no longer exposed. Malformed historical `repository_shas` values do not fail the status feed. They map to `repositoryShas: null`. Internal rebuild and finalization paths continue reading the raw row and retain their existing invalid-provenance behavior. ## TDD Evidence ### Red Tests were changed before production code and produced the expected failures: - shared DTO tests rejected the new camelCase structured record and `repositoryShaEntrySchema` was not exported - the control-plane mapper test failed because `status-view` did not exist - status integration tests observed snake_case keys, a JSON-encoded `repository_shas`, and no nullable decoded field - web folding returned no statuses because it still read snake_case fields - primary SHA extraction returned `null` because it still expected a JSON string ### Green The minimum implementation added the shared schema, internal storage-row type, one response mapper, and typed web consumption. Focused shared, control-plane, integration, and web tests then passed. ### Refactor After green, the code-simplifier pass removed a duplicate inherited storage field and consolidated imports. The focused suites remained green. ## Compatibility All in-repo HTTP consumers are updated atomically in this monorepo. No temporary dual-field response is included: retaining `repository_shas` would continue exposing the storage encoding and conflict with the A03 contract, while there is no external consumer evidence requiring it. Shared-package changes trigger both affected deployment paths; a brief mixed-version rolling window remains the normal risk for this intentional contract change, but adding a second wire shape would not eliminate that risk without preserving the deprecated leak. ## Validation - `npm run build -w @open-inspect/shared` - shared tests: 50 files, 697 tests passed - control-plane unit tests: 213 files, 3,257 tests passed - control-plane `image-builds.test.ts` integration: 51 tests passed - web tests: 163 files, 1,231 tests passed - `npm run typecheck` - ESLint on all changed files - Prettier check on all changed files - `git diff --check` The first parallel full web run had two unrelated ESLint-boundary test timeouts under concurrent load; the isolated full web rerun passed all 1,231 tests. Repository-wide `npm run lint` and `npm run format:check` remain blocked by pre-existing, untouched `.opencode` lint errors and `.opencode/package.json` formatting drift; all changed files pass both checks. ## Migration And Risk - no D1 schema or data migration is required - malformed persisted provenance is represented safely only at the public response boundary - no image callback lifecycle or provider behavior was refactored - the intentional HTTP DTO change is the primary compatibility risk --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/529a68bb06a61cfc493c4f4414bee068)* --------- Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…y#1625) ## Summary - remove `SessionAutofixService`, which only forwarded two commands to `SessionMessageQueue` - give `AutofixHandler` a consumer-owned two-method queue surface - dispatch admission and recovery commands directly at the validated HTTP boundary - move both dispatch cases into the handler test and delete the duplicate service suite ## Context This addresses the second Autofix refactor finding after ColeMurray#1624: the session path no longer inserts a behavior-free service between the HTTP handler and message queue. ## TDD - changed the handler tests to inject queue capabilities directly and added recovery lookup coverage - confirmed the red state for both valid command variants at the old `service.handle` seam - removed the service and implemented direct narrow-port dispatch ## Validation - `npm run build -w @open-inspect/shared` - focused Autofix handler tests: 3 passed - `npm test -w @open-inspect/control-plane`: 3,252 passed - `npm run test:integration -w @open-inspect/control-plane`: 1,006 passed - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint -w @open-inspect/control-plane` - targeted Prettier check - `npm run build -w @open-inspect/control-plane` - `git diff --check` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/a531a7ca7d9557ead0ead1e406f70652)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Autofix requests now correctly enqueue new feedback and retrieve results for recovery lookups. * Invalid autofix commands continue to return a validation error without triggering queue operations. * Autofix responses now consistently reflect whether feedback was accepted, duplicated, rejected, found, or unavailable. * **Tests** * Expanded coverage for feedback enqueueing, recovery lookups, invalid-command handling, and response outcomes. <!-- 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 - exclude the session-injected `.opencode` directory from the root ESLint scan - keep generated local tooling from producing environment-specific `no-undef` and unused-variable failures ## Verification - `npm run lint` - `npx prettier --check eslint.config.js` - `git diff --check` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/b34c42382069ae3b2941c82dc52bbe17)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Improved linting coverage for OpenCode configuration and scripts. * Updated lint checks to recognize Node.js environments and handle intentionally unused parameters 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>
…eMurray#1629) Phase B of the collaborator-arity program: `SessionSandboxEventProcessor` was a 19-parameter dispatch table — 13+ event types, each branch using a different collaborator subset. This splits it into a thin router plus per-family handlers, mirroring the HTTP-route decomposition. Behavior-preserving: the existing `sandbox-events` suite (37 tests) passes with **zero assertion changes** — only the construction helper changed, and it now builds the real family composition. ## Shape `src/session/sandbox-events/`: | Class | Params | Owns | | --- | --- | --- | | `SessionSandboxEventProcessor` (router) | 8 | arrival logging, per-event context (one `Date.now()`, one message-attribution resolution), dispatch, **the ack contract** | | `SandboxStreamingEventHandler` | 6 | `token`, `context_compacted`, `step_start`/`step_finish`, `tool_call` + the generic timeline path (`tool_result`, `error`, `warning`, `user_message`, unknown) | | `SandboxArtifactEventHandler` | 4 | `artifact` | | `SandboxExecutionEventHandler` | 12 | `execution_complete` — the settle-a-turn convergence point | | `SandboxRuntimeEventHandler` | 7 | `heartbeat`, `session_title`, `ready`, `git_sync` | | `SandboxPushCoordinator` | 4 + resolver state | `pushBranchToRemote` and `push_complete`/`push_error` — one unit, because the terminal events settle state the request side created | The ack contract is now a single post-dispatch line in the router; family handlers never see `ackId`. Ack ordering is unchanged — critical events ack after their handler finishes, exactly where the old branches acked (`execution_complete` after `processMessageQueue`, push/tail events after broadcast). The execution handler is deliberately still wide (12): every param is a distinct role in settling a finished turn. The status-owner campaign is expected to absorb `projectTerminalMessage` and parts of `statusService` into one projection surface; the class doc says to re-measure then rather than split further now. ## Inventory findings (charted before cutting) - `error` and `snapshot_ready` had no dedicated branches — the old fall-through tail was really a *timeline-observer* path (persist → broadcast → ack-if-critical). That path is now `recordTimelineEvent` on the streaming handler, with the router's `default` case routing to it. - `ready` did its side effects early and then **fell through** to the tail (persist + broadcast). It's now fully owned by the runtime handler with the same effect order. - `snapshot_ready` in `CRITICAL_EVENT_TYPES` is unreachable: it's not in the `sandboxEventSchema` union (both entry paths validate against it) and the Modal bridge never emits it. Left inert here — flagging for a separate cleanup rather than changing semantics in a refactor. One non-observable ordering note: the router computes context (two pure reads) before dispatch, so for `ready` the `getProcessingMessage` read now precedes `pinBaselines` instead of following it; the two touch disjoint state. ## Verification - `tsc` ×3 programs (src, test, integration) clean; ESLint clean - Unit battery 3253/3253; integration battery 1006/1006 (includes `session-do-collaborator-wiring.test.ts`, which patches `pushBranchToRemote` through the DO — the router keeps that method as a delegate to the coordinator so the seam still intercepts) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Improved processing of sandbox activity, including streaming updates, artifacts, runtime events, and execution completion. * Improved reliability of branch push operations, including completion tracking, error handling, timeouts, and support for multiple pending pushes. * Preserved delivery acknowledgements for critical sandbox events. * **Bug Fixes** * Improved session activity, status updates, notifications, and timeline synchronization during sandbox operations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- add strict Pydantic request models for interactive sandbox create and
snapshot restore
- validate repository owner/name pairs and nested multi-repository
identities at the HTTP boundary
- parse create/restore requests once and construct manager inputs from
typed values
- centralize authentication, timing, HTTP exception tracking, generic
exception mapping, and `modal.http_request` logging in a small async
context manager
- map unexpected internal failures to sanitized HTTP 500 responses
instead of HTTP 200 `{ success: false }` payloads
- preserve explicit build-session not-found handling and all
endpoint-specific success response shapes
## Compatibility
The existing rolling-deployment policy is preserved independently from
strict field typing:
- unknown top-level request fields remain ignored through
`_ModalRequestModel` (`extra="ignore"`)
- unknown nested restore `session_config` fields remain preserved
(`extra="allow"`) so snapshots can round-trip fields introduced by newer
control-plane deployments
- known fields use strict types, so values such as `"false"` are
rejected rather than coerced to truthy booleans
- optional no-repository sessions remain supported, while partial
repository identities are rejected
- default timeout and VNC behavior, repo-image create behavior, snapshot
clone-token compatibility, environment variables, settings,
code-server/VNC/Slack flags, multi-repository session configuration, and
structured correlation IDs are preserved
No control-plane changes were necessary. Its Modal client already
handles non-2xx responses explicitly, and successful response payloads
are unchanged.
## Error Envelope
The shared endpoint execution seam owns:
- bearer authentication before request and control-plane URL validation
- request timing and success/error outcome tracking
- propagation of known `HTTPException` status/detail values
- logging unexpected exceptions server-side and mapping them to bounded
`500 Internal server error` responses
- final `modal.http_request` logging, including endpoint-specific
trace/request/session/sandbox/build identifiers
Control-plane URL validation no longer reflects the submitted URL in
client-visible errors.
## TDD Evidence
Red:
- added focused tests before production changes
- initial focused run: `11 failed, 30 passed`
- expected failures showed string booleans being accepted, malformed
typed fields reaching Modal/domain code, and generic create/restore
failures returning normally instead of raising HTTP 500
Green:
- added the create/restore request models and applied the minimal
execution seam to those handlers
- focused create/restore run: `41 passed`
Refactor:
- extracted all remaining authenticated endpoint envelopes onto the
tested seam
- combined focused create/build API run after extraction: `74 passed`
- applied the code-simplifier review and removed only redundant
execution-path state and an unreachable error mapping
- reran focused and full verification after refactoring
## Verification
- `uv run pytest tests/test_web_api_create_sandbox.py
tests/test_web_api_build_sandbox.py -q` -> 74 passed
- `uv run pytest tests/ -q` -> 210 passed
- `uv run ruff check src/web_api.py
tests/test_web_api_create_sandbox.py` -> passed
- `uv run ruff format --check src/web_api.py
tests/test_web_api_create_sandbox.py` -> passed
- `git diff --check` -> passed
An additional `uv run mypy src/web_api.py` was attempted and reports 16
existing strict-typing issues in this legacy module, primarily
pre-existing unparameterized endpoint `dict` annotations and dynamically
re-exported constants. This check is not part of the requested Modal
validation set and no new mypy-specific scope was added.
## Risks
- malformed create/restore payloads that previously reached domain code
or were silently coerced now receive HTTP 400 errors
- unexpected failures now correctly produce non-2xx responses; callers
relying on the erroneous HTTP-200 error object behavior will observe the
corrected contract
- unknown-field handling remains intentionally permissive for rolling
deployments as described above
## Scope
This change is limited to audit finding A21. It does not include A22's
`SandboxProvider` capability/launch-contract refactor, provider adapter
consolidation, or image-build lifecycle changes.
---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/14275a8cddd1b305bd607af44c6f6ba0)*
---------
Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…ound the request (ColeMurray#1408) ## Problem The Slack and Linear bots classify each inbound message to decide which repository or environment a coding session should target. Both classifiers are pinned to Anthropic: - `packages/slack-bot/src/classifier/index.ts` builds an Anthropic client and forces a `classify_target` tool call. - `packages/linear-bot/src/classifier/index.ts` calls `api.anthropic.com/v1/messages` directly and **hardcodes** `claude-haiku-4-5` with no env override at all. Two consequences: 1. **Single-provider coupling.** An Anthropic outage, rate limit, or billing lapse degrades routing on every deployment, with no way to point the classifier elsewhere — even for deployments whose coding agents already run OpenAI models. We hit exactly this: an Anthropic billing lapse dropped both bots to "pick a target yourself" until it was noticed. 2. **Unbounded requests.** Neither classifier passes an abort signal, so a stalled or queued provider request holds the Slack thread / Linear webhook open until the platform kills the invocation. The classifiers already fail soft to a target picker, so a *fast* failure is cheap — it was the unbounded wait that hurt. ## What this does Lets an operator pick the classifier's provider, requires **only that provider's** credential, and binds exactly one provider key to the bots. | `classification_model` | Provider | Credential required | |---|---|---| | `anthropic/<x>` or bare `claude-*` (default) | Anthropic, existing tool-calling request | `classification_anthropic_api_key`, falling back to `anthropic_api_key` | | `openai/<x>` or bare `gpt-*` | OpenAI Chat Completions, strict `json_schema` | `classification_openai_api_key` | The prefix rule reuses the convention already encoded in `normalizeModelId`/`MODEL_CATALOG` in `packages/shared/src/models.ts`, so there is no second setting that can disagree with the model id. The bare id is sent to the provider. An unrecognised prefix throws into each classifier's existing `catch`, which already degrades to asking the user to pick — no new failure mode. Both providers funnel through the existing validators (`normalizeModelResponse` in slack-bot, `classifyToolInputSchema` in linear-bot), so the downstream contract is untouched. `CLASSIFICATION_REQUEST_TIMEOUT_MS = 15_000` now bounds **both** providers, following the existing convention (`REPOS_FETCH_TIMEOUT_MS`, `OUTBOUND_REQUEST_TIMEOUT_MS`): milliseconds in the name, defined once, and asserted in tests by identity of the signal object rather than just its shape. ### Scope of the credential choice — please read This is deliberately **classifier-scoped**, not a deployment-wide provider switch. `anthropic_api_key` is left exactly as it is on `main` (`nullable = false`, non-blank validation) because it has consumers unrelated to classification: the Modal sandbox's `llm-api-keys` secret (`modal.tf`) that Claude coding sessions use, and the opencomputer control-plane path. The diff to `variables.tf` is purely additive — it does not touch that variable. So: choosing the OpenAI classifier means you supply `classification_openai_api_key` and the bots receive **only** that key. It does not make the deployment OpenAI-only, and this PR makes no claim to. Making sandbox provider credentials uniformly optional is a separate, larger change tied to the default coding model, and I have not attempted it here. ## Backward compatibility **Nothing changes for an existing deployment that sets no new value.** - `classification_model` defaults to `claude-haiku-4-5` — today's value. - `classification_anthropic_api_key` defaults to blank and falls back to `anthropic_api_key`, so existing deployments keep working untouched. - The Anthropic request body is unchanged; the timeout is passed as `messages.create(body, { signal })`, so the body itself is untouched. - `ANTHROPIC_API_KEY` stays required, the `@anthropic-ai/sdk` dependency stays, `CLASSIFY_TARGET_TOOL` stays. - No Claude entries removed anywhere — `packages/linear-bot/src/model-resolution.ts` (`MODEL_LABEL_MAP`) is untouched, so `model:opus`-style Linear labels keep working. - Anthropic-classifier deployments keep exactly the bot secret bindings they had; no empty secret is introduced and no worker version churns from this change. - The Anthropic SDK client is now constructed lazily, so an OpenAI-configured deployment never reaches `new Anthropic({ apiKey: undefined })`. The Linear bot gains a `CLASSIFICATION_MODEL` binding it never had; its default makes the previously hardcoded `claude-haiku-4-5` explicit, so the effective model is unchanged. ## Configuration ```hcl # Default — Anthropic, using the key you already supply # classification_model = "claude-haiku-4-5" # Or classify on OpenAI; the bots then receive only this key classification_model = "gpt-5.4-mini" classification_openai_api_key = "sk-proj-..." ``` Each provider's key is validated non-blank **when that provider is selected and a classifier bot is enabled** — so an OpenAI deployment is never asked for an Anthropic classifier key, a deployment running neither bot is never asked for either, and a selected provider can't ship credential-less. That last guard matters because GitHub Actions renders an unset secret as an empty string, which would otherwise plan and apply cleanly and leave a classifier rejecting every message. For the same reason the workflow maps the model with an explicit fallback (`${{ vars.CLASSIFICATION_MODEL || 'claude-haiku-4-5' }}`, matching the existing `ENABLE_SLACK_BOT || 'true'` pattern), and the configuration additionally refuses a blank override rather than silently treating it as "use the default". ## Verification Terraform (`terraform test`, mock providers) — **18 passed, 0 failed**, including a new `tests/classifier_provider.tftest.hcl` whose 8 runs cover every branch: - Anthropic default binds `ANTHROPIC_API_KEY` and **no** `OPENAI_API_KEY` on both bots (the backward-compatibility guarantee, asserted rather than assumed) - OpenAI model binds `OPENAI_API_KEY` and **no** `ANTHROPIC_API_KEY` — exactly one provider credential reaches the bots, asserted in both polarities - `gpt-5.4-mini` and `openai/gpt-5.4-mini` both resolve to OpenAI; `anthropic/claude-haiku-4-5` resolves to Anthropic - OpenAI model with a blank key → plan **fails** - OpenAI model with both bots disabled and a blank key → plan **succeeds** - unknown provider prefix → plan **fails**; blank model → plan **fails** The pre-existing `anthropic_api_key_blank` guard in `tests/auth_provider_configuration.tftest.hcl` still passes unchanged. `terraform fmt -check -recursive` clean; `terraform validate` success. TypeScript: `npm run typecheck` exit 0; `eslint --max-warnings 0` clean on both changed packages. Unit suites (clean upstream-main baseline → this branch): slack-bot 421 → **425**, linear-bot 223 → **230**; unchanged elsewhere: shared **601**, github-bot **130**, control-plane **2518**, web **956**. Control-plane integration (workerd + real D1): **778 passed**. New tests per bot cover: the OpenAI request contract (`max_completion_tokens` present, `max_tokens` absent, `temperature: 0`, `strict: true`, bare model id, `additionalProperties: false`, all fields `required`, nullable id typed `["string","null"]`), non-2xx degrading to the picker, the timeout signal being the exact `AbortSignal.timeout` object, the Anthropic default path still firing when nothing is set, and an unrecognised prefix degrading without calling either provider. ## Notes for reviewers - **`max_completion_tokens` is required and `max_tokens` is rejected** by the gpt-5 family (`Unsupported parameter: 'max_tokens' is not supported with this model`) — verified against the live API, and pinned by a test in each bot so it cannot regress silently. - Each bot implements its own small OpenAI request function rather than sharing one: two call sites with different schemas, and it keeps each Worker self-contained. Happy to extract into `packages/shared` if you would prefer that. - The provider is derived from the model id rather than a separate `CLASSIFICATION_PROVIDER` variable, to avoid a setting that can disagree with the model. If you would rather support OpenAI-compatible gateways (Azure, OpenRouter, proxies) whose ids are not `gpt-*`, an explicit provider override is the natural follow-up — happy to add it here or later. - `classification_anthropic_api_key` exists mainly so the two providers are symmetric and the classifier's credential is separable from the sandbox's. If you would rather the Anthropic classifier just always read `anthropic_api_key` and drop that variable, that is a one-line simplification — say which you prefer. - The `docs/GETTING_STARTED.md` diff looks larger than it is: adding `CLASSIFICATION_ANTHROPIC_API_KEY` widened the Actions-secret table's first column, so Prettier (which your `lint-staged` runs on Markdown) realigned every row. `git diff -w` on that file shows only the six sample lines, the two new table rows, and the widened separator. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added configurable classification model selection for Slack and Linear bots. * Added OpenAI and Anthropic classification support with provider-specific credentials. * Added structured response validation and 15-second request timeouts. * Added graceful handling for unsupported models, provider errors, and missing credentials. * **Documentation** * Updated setup and deployment guidance for models and API keys. * **Tests** * Expanded coverage for provider selection, validation, timeouts, credentials, and fallback behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
) This is an automated nightly unsafe-cast remediation sweep. It fixes three current default-branch findings by replacing unsafe boundary/persisted-data assertions with Zod parsing or existing schema parsing, following the TypeScript Coding Standards unsafe-cast / parse-don't-assert guidance and the Zod boundary-validation pattern established in PR ColeMurray#807. | file:line | risk | cast removed | fix | | --- | --- | --- | --- | | `packages/slack-bot/src/classifier/index.ts:141` / `:152` / `:175` | High | External LLM tool payload cast to `Record<string, unknown>` and confidence cast to `ClassificationResult["confidence"]` | Added local `llmResponseSchema` and `safeParse` at the model-output boundary; invalid output preserves the existing low-confidence clarification fallback. | | `packages/control-plane/src/db/automation-model-provider-auth.ts:30` | High | Persisted provider auth rows assembled and cast to `ModelProviderSelections`, bypassing existing schema | Runs `modelProviderSelectionsSchema.parse` after row assembly so the shared Zod schema remains the source of truth. | | `packages/control-plane/src/db/mcp-servers.ts:66`, `:79`, `:94`, `:237` | Medium | Persisted MCP JSON/type fields cast to `Record<string, string>` and `"local" | "remote"` | Added package-local Zod parsers for MCP server type, command arrays, and env/header maps at D1 decode sites. | Verification: | command | result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run build -w @open-inspect/control-plane` | Passed | | `npm run build -w @open-inspect/slack-bot` | Passed | | `npm run typecheck` | Passed | | `npm run format` | Passed | | `npm run lint -w @open-inspect/control-plane` | Passed | | `npm run lint -w @open-inspect/slack-bot` | Passed | | `npm test -w @open-inspect/control-plane` | Passed | | `npm test -w @open-inspect/slack-bot` | Passed | | `npm run lint` | Failed on pre-existing `.opencode/**/*.js` `no-undef` errors outside this sweep's allowed touch set; package lint for changed code passed. | --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/c7e806bd601ed64888d77b3ed7ec687e)* --------- Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
This is an automated nightly unsafe-cast remediation sweep. It replaces selected unsafe TypeScript casts at boundary/persisted-data sites with parse-don't-assert validation, following the TypeScript Coding Standards unsafe-cast guidance and the Zod boundary-validation pattern established in PR ColeMurray#807. | Finding | Risk | Cast removed | Fix | | --- | --- | --- | --- | | `packages/slack-bot/src/classifier/repos.ts:224` | HIGH | KV fallback `cached as SlackRoutingRule[]`, bypassing the existing shared routing-rule schema | Uses `z.array(slackRoutingRuleSchema).safeParse(cached)` before `normalizeRoutingRules`; malformed cached routing rules fail open to the existing empty fallback. | | `packages/control-plane/src/session/event-stream.ts:119` | MEDIUM | persisted event `JSON.parse(event.data) as Record<string, unknown>` | Adds a local Zod `persistedEventDataSchema` and validates parsed event data before returning the HTTP event response. | | `packages/control-plane/src/routes/session-children.ts:127` | LOW | child response `(await response.clone().json()) as { messageId?: unknown }` | Replaces the assertion with a plain object/property guard; malformed best-effort response payloads continue to be ignored. | Verification: | Command | Result | | --- | --- | | `npm run format` | Passed | | `npm test -w @open-inspect/control-plane -- src/session/event-stream.test.ts src/routes/session-children.test.ts` | Passed, 2 files / 19 tests | | `npm test -w @open-inspect/slack-bot -- src/classifier/repos.test.ts` | Passed, 1 file / 23 tests | | `npm run build -w @open-inspect/shared` | Passed | | `npm run build -w @open-inspect/control-plane` | Passed | | `npm run build -w @open-inspect/slack-bot` | Passed | | `npm test -w @open-inspect/control-plane` | Passed, 168 files / 2568 tests | | `npm test -w @open-inspect/slack-bot` | Passed, 34 files / 423 tests | | `npm run typecheck` | Passed | | `npm run lint -w @open-inspect/control-plane` | Passed | | `npm run lint -w @open-inspect/slack-bot` | Passed | | `git diff --check` | Passed | | `npm run lint` | Failed on pre-existing `.opencode/` helper files (`no-undef` for `process`, `fetch`, `Headers`, `URL`, etc.), unrelated to the files touched by this sweep. | Reference: TypeScript Coding Standards unsafe-cast / parse-don't-assert guidance and the Zod webhook normalizer pattern from PR ColeMurray#807. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/db6e4a50d71c0639ad6c7d522af6683f)* --------- Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
This is an automated nightly unsafe-cast remediation sweep. It replaces qualifying unsafe TypeScript assertions at trust boundaries with parse-don't-assert style guards, following the TypeScript Coding Standards for unsafe casts and the Zod boundary-validation pattern established in PR ColeMurray#807. This PR is draft because the exact root `npm run lint` gate fails in this sandbox on untracked local `.opencode/` tooling files outside the repository-tracked source changes. | Finding | Risk | Cast Removed | Fix | | --- | --- | --- | --- | | `packages/control-plane/src/sandbox/e2b-rest-client.ts:183` | High | External E2B Connect end-stream body cast to `{ error?: { message?: string } }` | Inline `isRecord` guard before reading `error.message` | | `packages/control-plane/src/sandbox/e2b-rest-client.ts:190` | High | External E2B Connect event body cast to `{ event?: Record<string, { status?: string }> }` | Inline `isRecord` guards before reading `event.end.status` | | `packages/control-plane/src/webhooks/automation-event.ts:56` and `:83` | High | Normalized webhook envelope body cast to `Record<string, unknown>` before schema validation | Inline `isRecord` guard before source/eventType reads; existing `automationEventSchema.safeParse` remains authoritative | | `packages/web/src/app/api/sessions/[id]/title/parse-request.ts:4` | Medium | Request body cast to `{ title?: unknown }` | Existing object guard plus `"title" in body` one-field access | Verification: | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run build -w @open-inspect/control-plane` | Passed | | `NODE_ENV=production npm run build -w @open-inspect/web` | Passed | | `npm run typecheck` | Passed | | `npm run format` | Passed | | `npm test -w @open-inspect/control-plane` | Passed: 204 files, 3184 tests | | `npm test -w @open-inspect/web -- src/app/api/sessions/[id]/title/route.test.ts` | Passed: 1 file, 3 tests | | `npm test -w @open-inspect/web` | Passed on retry: 162 files, 1214 tests | | `npm run lint -w @open-inspect/control-plane && npm run lint -w @open-inspect/web` | Passed | | `npm run lint` | Failed: ESLint includes untracked local `.opencode/` tooling files with `no-undef` errors; none are tracked or modified by this PR | Notes: - The first `npm run build -w @open-inspect/web` failed with this sandbox's non-standard `NODE_ENV`; rerunning with `NODE_ENV=production` passed. - No dependencies were added. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/84e35873f963231ea86abe832d6fc1bb)* --------- Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
This is an automated nightly unsafe-cast remediation sweep. It replaces three unsafe casts of opaque SQLite PRAGMA rows with a local parse/guard path, following the TypeScript Coding Standards for unsafe casts and parse-don't-assert. The selected boundary is package-local and trivial, so this uses inline runtime guards instead of Zod; this is consistent with the Zod boundary-validation pattern established in PR ColeMurray#807 for structured external payloads while keeping one-field SQLite row parsing minimal. | Finding | Risk | Cast removed | Fix | | --- | --- | --- | --- | | `packages/control-plane/src/session/schema.ts:386` | Medium | `PRAGMA table_info(participants).toArray() as Array<{ name: string }>` | Inline `isRecord`/`parseSqlColumnNames` guard before building the column set | | `packages/control-plane/src/session/schema.ts:422` | Medium | `PRAGMA table_info(${table}).toArray() as Array<{ name: string }>` | Inline `isRecord`/`parseSqlColumnNames` guard before checking for `scm_provider` | | `packages/control-plane/src/session/schema.ts:436` | Medium | `PRAGMA table_info(session).toArray() as Array<{ name: string }>` | Inline `isRecord`/`parseSqlColumnNames` guard before building the column set | Verification: | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run build -w @open-inspect/control-plane` | Passed | | `npm run typecheck` | Passed | | `npm run format` | Passed, no additional changes | | `npm run lint -w @open-inspect/control-plane` | Passed | | `npm test -w @open-inspect/control-plane` | Passed, 203 files / 3167 tests | | `npm run lint` | Failed on pre-existing `.opencode/**` no-undef issues outside this sweep's allowed file scope | --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/c23740fe74b7a02f5cf2c5a127178219)* --------- Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
Automated nightly unsafe-cast remediation sweep. This PR fixes two remaining web-package unsafe cast sites by parsing or narrowing boundary/opaque data instead of asserting, following the TypeScript Coding Standards unsafe-cast / parse-don't-assert guidance and the Zod boundary-validation pattern established in PR ColeMurray#807. | Finding | Risk | Cast removed | Fix | | --- | --- | --- | --- | | `packages/web/src/lib/tasks.ts:41` | Medium | `latestTodoWrite.args as TodoWriteArgs` for opaque sandbox tool-call args | Added a local Zod schema for the consumed TodoWrite args and `safeParse`; malformed args preserve the existing empty-list behavior. | | `packages/web/src/components/settings/data-controls-settings.tsx:72` | High | `await res.json()` trusted as `SessionListResponse` for archived-session pagination | Added a canonical session-list response schema and shared fetcher used by initial and load-more requests; malformed responses hit the existing catch/log path. | Verification: | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run build -w @open-inspect/web` | Passed | | `npm run typecheck` | Passed | | `npm run format` | Passed | | `npm test -w @open-inspect/web -- --run src/lib/tasks.test.ts src/components/settings/data-controls-settings.test.tsx` | Passed | | `npm test -w @open-inspect/web` | Passed: 157 files, 1159 tests | | `npm run lint -w @open-inspect/web` | Passed | | `npm run lint` | Failed on pre-existing `.opencode/` JavaScript globals (`Headers`, `fetch`, `process`, etc.) outside the touched files; PR opened as draft per sweep instructions. | --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/7c0bca8b4624321b48bb19ce9a137ee6)* --------- Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com> Co-authored-by: Codex <codex@openai.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
This is an automated nightly unsafe-cast remediation sweep. It replaces selected unsafe TypeScript assertions over persisted or loose boundary data with runtime narrowing, preserving existing null/skip behavior for malformed values and leaving valid inputs unchanged. The fixes follow the TypeScript Coding Standards unsafe-cast / parse-don't-assert guidance and the Zod boundary-validation pattern established in PR ColeMurray#807; these particular findings were simple persisted-data shapes, so lightweight inline guards were sufficient and no dependency changes were made. | Finding | Risk | Cast removed | Fix | | --- | --- | --- | --- | | `packages/control-plane/src/session/tunnel-urls.ts:28` | Medium | `parsed as Record<string, string>` after parsing stored `sandbox.tunnel_urls` JSON | Inline guard builds a fresh `Record<string, string>` only after validating every entry | | `packages/control-plane/src/session/pr-artifacts.ts:20` | Medium | `parsed as { repoOwner?: unknown; repoName?: unknown }` after parsing stored PR artifact metadata | Inline `isRecord` guard before reading repo identity fields; malformed metadata still returns `null` | | `packages/control-plane/src/sandbox/lifecycle/image-selection.ts:125` | Medium | `primary as { baseSha?: unknown }` after parsing stored `repository_shas` JSON | Inline `isRecord` guard before reading `baseSha`; malformed provenance still yields `null` | | `packages/web/src/lib/session-socket/artifact-metadata.ts:65` | Medium | `artifact.metadata as Record<string, unknown> | null` from loose session artifact wire metadata | Inline `isRecord` guard before UI metadata narrowing; non-object metadata is ignored | Verification: | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run build -w @open-inspect/control-plane` | Passed | | `NODE_ENV=production npm run build -w @open-inspect/web` | Passed | | `npm run typecheck` | Passed | | `npm run lint -- --ignore-pattern '.opencode/**'` | Passed; `.opencode` is untracked local tooling in this workspace and is excluded from the PR | | `npm run lint -w @open-inspect/control-plane` | Passed | | `npm run lint -w @open-inspect/web` | Passed | | `npm test -w @open-inspect/control-plane` | Passed | | `npm test -w @open-inspect/web` | Passed when run isolated; concurrent run with control-plane tests timed out in two existing ESLint-boundary tests, then passed on isolated rerun | | `npm run format` | Passed | | `git diff --check` | Passed | --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/9c22735b7a63f6e49a3d58042e10a5bd)* --------- Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
| Finding | Risk | Cast removed | Fix |
| --- | --- | --- | --- |
| `packages/control-plane/src/routes/session-ws-token.ts:23` | HIGH |
`parseJsonBody<{ scmLogin?: string; scmName?: string; scmEmail?: string
}>` generic request-body assertion for an auth/session token path |
Added a local Zod schema and `safeParse` after preserving raw-body
identity enforcement |
| `packages/control-plane/src/routes/image-builds.ts:339` | HIGH |
`parseJsonBody<{ enabled?: unknown }>` generic request-body assertion
feeding repo image-build persistence | Parsed JSON as `unknown` and used
an inline record/boolean guard before persistence |
| `packages/control-plane/src/routes/session-child-spawn.ts:97` | MEDIUM
| `(await spawnContextRes.json()) as { error?: unknown }` on an opaque
session-runtime response | Parsed as `unknown` and used an inline
record/string guard, preserving the existing fallback message |
Verification:
| Command | Result |
| --- | --- |
| `npm run format` | Passed |
| `npm test -w @open-inspect/control-plane` | Passed, 172 files / 2598
tests |
| `npm run build -w @open-inspect/shared` | Passed |
| `npm run build -w @open-inspect/control-plane` | Passed |
| `npm run typecheck` | Passed |
| `npm run lint -w @open-inspect/control-plane` | Passed |
| `git diff --check` | Passed |
| `npm run lint` | Failed on pre-existing `.opencode` files (`process`,
`fetch`, `Headers`, etc. reported as undefined), unrelated to this PR |
No dependency changes.
---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/6347ee0b9042691211c410eacb804bcd)*
---------
Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
This is an automated nightly unsafe-cast remediation sweep. It replaces selected high-risk unsafe TypeScript casts with parse-don't-assert validation at trust boundaries, following the TypeScript Coding Standards for unsafe casts and the Zod boundary-validation pattern established in PR ColeMurray#807. | Finding | Risk | Cast removed | Fix | | --- | --- | --- | --- | | `packages/slack-bot/src/callbacks.ts:353` | HIGH | `payload as AutomationSkipPayload` after `request.json()` | Added a local Zod `automationSkipSchema` and uses `safeParse` before signature validation and async handling. | | `packages/control-plane/src/scheduler/durable-object.ts:864` | HIGH | `event as SlackAutomationEvent` after `automationEventSchema.safeParse` | Replaced the cast with discriminant narrowing from the already-validated automation event union. | Verification: | Command | Result | | --- | --- | | `npm test -w @open-inspect/slack-bot` | Passed: 34 files, 422 tests. | | `npm test -w @open-inspect/control-plane` | Passed: 161 files, 2540 tests. | | `npm run build -w @open-inspect/shared` | Passed. | | `npm run build -w @open-inspect/control-plane` | Passed. | | `npm run build -w @open-inspect/slack-bot` | Passed. | | `npm run format` | Passed. | | `npm run typecheck` | Passed. | | `npm run lint -w @open-inspect/control-plane` | Passed. | | `npm run lint -w @open-inspect/slack-bot` | Passed. | | `npm run lint -- --ignore-pattern .opencode/` | Passed for the tracked repository tree. | | `git diff --check` | Passed. | Reference: TypeScript Coding Standards unsafe-cast / parse-don't-assert guidance and the Zod webhook normalizer pattern from PR ColeMurray#807. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/13cffa9a6e1265b60e4deb0ebffcb302)* Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com> Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
## Summary - instruct the GitHub reviewer to accumulate findings before publishing - submit the review summary and all inline findings in one `/reviews` API request - prevent regressions to standalone inline comment requests and unsafe shell interpolation - preserve `COMMENT` reviews for bot-authored pull requests ## Why Standalone review comments are not part of the final submitted review consumed by Autofix. Batching them into the review's `comments` array ensures GitHub emits one complete submitted review and Autofix receives all findings in one attempt. ## Validation - `npm test -w @open-inspect/github-bot` - `npm run typecheck -w @open-inspect/github-bot` - `npm run lint -w @open-inspect/github-bot` - Prettier check on changed files - `git diff --check` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/5ce0f93565c54f68464ad4d5db973bbc)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Streamlined automated pull request reviews by submitting summaries and inline comments together in a single review. * Improved support for repositories with nested owners when creating reviews. * Clarified the supported review outcomes for automated code reviews. * **Bug Fixes** * Reduced the risk of incomplete or split review feedback by consolidating submissions into one operation. <!-- 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 explicit audit outcomes and structured before/requested/after metadata - record default Member role assignments atomically from the database trigger - treat repeated role and suspension requests as audited no-ops without touching state or sessions - improve Owner bootstrap and user-merge provenance, including affected role and suspension state - preserve the existing atomic coupling between successful RBAC mutations and their audit records ## Why The initial RBAC audit events identified who acted and which user was targeted, but did not record the actual authorization change. Repeated writes were also indistinguishable from real changes, default grants were absent, and operator-driven user merges lacked unique provenance. ## Testing - `npm run test:integration -w @open-inspect/control-plane` (87 files, 1075 tests) - `npm test -w @open-inspect/control-plane -- src/db/authorization-store.test.ts` - `npm run test:rbac-bootstrap-owner` - `npm run test:user-merge-cli` - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint -w @open-inspect/control-plane` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/97badb97b9dc770a3214927e8ed1bd16)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Authorization updates now report when a request makes no changes. * Audit records include operation results and structured before, requested, and after state details. * User merges and automatic role assignments provide richer audit information. * **Bug Fixes** * Identical role or status updates no longer alter user state or authentication sessions. * Audit failures now prevent related user changes from being saved. * Existing audit data is preserved and upgraded during migration. * Bootstrap and migration processes now record complete role-assignment audit details. <!-- 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 - append an authoritative trust-boundary guardrail after event-specific automation context - preserve the stable instruction-first prefix used for provider prompt caching - clarify that untrusted event content cannot override configured automation or workspace instructions - update prompt composition and Slack scheduler assertions for the secured ordering ## Security impact Slack, Sentry, and other event-derived content no longer occupies the final instruction position in unattended automation prompts. The trailing trusted reminder reduces the risk that prompt injection in event data overrides the automation's configured behavior. ## Source Ported from ColeMurray/open-inspect-claude-prod#133 with the original commit authorship preserved. ## Testing - `npm test -w @open-inspect/control-plane -- src/scheduler/compose-automation-prompt.test.ts src/scheduler/scheduler.test.ts` (84 passed) - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint -w @open-inspect/control-plane` - `git diff --check origin/main...HEAD` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/34bd317e0e5ab003e4a82bf4d16fe64e)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Security** * Added a prompt safeguard that clearly distinguishes trusted instructions from event context. * Event-provided content can no longer override or modify trusted automation instructions. * **Tests** * Updated automated checks to verify the safeguard appears consistently across supported event-handling scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - increase the per-session unfinished prompt limit from 10 to 50 - retain the existing centralized admission checks and queue-full behavior ## Testing - `npm run build -w @open-inspect/shared` - `npm test -w @open-inspect/shared` - `npm test -w @open-inspect/control-plane -- src/session/message-queue.test.ts src/session/message-repository.test.ts` - `npm run test:integration -w @open-inspect/control-plane -- test/integration/websocket-client.test.ts test/integration/prompt-enqueue.test.ts` - `npx prettier --check packages/shared/src/types/prompts.ts` - `npx eslint packages/shared/src/types/prompts.ts` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/9493ea6b540d830f764d2ae560c79043)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Improvements** - Increased the maximum number of unfinished prompts that can be retained from 10 to 50. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - persist denied RBAC decisions at the centralized HTTP authorization boundary - persist allowed decisions for protected mutations and sensitive managed reads - include canonical actor/service snapshots, permission requirements, request and trace IDs, route, response status, and denial reason - preserve the original response when audit persistence fails, while emitting an operational error - atomically audit stale-actor denials, missing-resource rejections, and last-Owner conflicts in member mutations - keep member writes gated exclusively by the matching applied audit event ## Why The RBAC rollout enforced authorization centrally but left denied attempts and most high-impact allowed requests visible only through generic request logs. Rejected member mutations also produced no durable record. This adds durable decision-level coverage without duplicating audit calls across every route handler. ## Dependency Stacked on ColeMurray#1687, which adds the audit outcome and structured metadata fields used here. ## Testing - `npm test -w @open-inspect/control-plane` (225 files, 3405 tests) - focused integration suites (3 files, 28 tests) - `npm run typecheck -w @open-inspect/control-plane` - ESLint and Prettier on changed files The full integration run reached 86 passing files and 1035 passing tests before the existing force-eviction test crashed one workerd pool with `ECONNRESET`; all directly affected integration suites pass. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/97badb97b9dc770a3214927e8ed1bd16)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added comprehensive authorization audit events for approved, denied, rejected, and no-op requests. * Audit records now include request details, required permissions, principal information, response status, and decision outcomes. * Added configurable auditing for route authorization policies, including service and sandbox access. * Default role assignment actions now generate audit records. * **Bug Fixes** * Improved audit accuracy by recording the state actually applied during role and membership changes. * Audit persistence failures no longer interrupt request processing. * Corrected permission reporting for bypassed authorization checks. <!-- 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 - attribute actorless Linear `created` agent sessions to the verified installed Linear app user - use the same actor for session creation and the initial prompt so RBAC permits both requests - keep human preferences and issue transitions tied to an actual human creator - add regression coverage for the signed actor headers while preserving body identity restrictions ## Root cause Automation-created Linear agent sessions can omit both `agentSession.comment.userId` and `agentSession.creatorId`. The Linear bot consequently omitted `X-OpenInspect-Actor` from `POST /sessions`, which the control plane correctly rejected with `403 service_actor_required`. ## Validation - `npm test -w @open-inspect/linear-bot` (233 tests passed) - `npm run typecheck -w @open-inspect/linear-bot` - `npm run lint -w @open-inspect/linear-bot` - `npm run build -w @open-inspect/linear-bot` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/8fdbd5d2c64650f69b2cb0987e7c4281)* Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary - remove genuinely unused provider-account helpers and request types - narrow module APIs by making internal constants, functions, and types private - update Knip configuration to analyze sandbox runtime entry points without false positives - document the intentional keyboard shortcut request/response schema alias ## Verification - `npm run knip` - `npm run typecheck` - `npm test -w @open-inspect/control-plane` (3,399 tests) - `npm test -w @open-inspect/web` (1,397 tests) - `npm test -w @open-inspect/slack-bot` (432 tests) - `git diff --check` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/eabe5991dd06163d2962b482487e4063)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Reduced the public API surface by making internal types, helpers, constants, and schemas private. * Streamlined type re-exports and removed unused public interfaces. * Consolidated keyboard-shortcut validation around a single schema without changing behavior. * **Chores** * Simplified workspace and dependency-analysis configuration. * Updated duplicate-type handling and dependency ignore rules for cleaner project checks. <!-- 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>
) <!-- open-inspect-react-doctor-owner: nightly-automation --> <!-- react-doctor-base-sha: 72fb7fc --> <!-- react-doctor-bucket: web-safe-local-fixes --> <!-- react-doctor-diagnostic-manifest: react-doctor/no-array-index-as-key@src/components/automations/condition-builder.tsx:151;react-doctor/only-export-components@src/components/automations/condition-builder.tsx:36 --> ## Summary Fixes **2 root-cause tasks** from the full `packages/web` React Doctor scan. 1. **`react-doctor/no-array-index-as-key`** in `src/components/automations/condition-builder.tsx`: condition editor rows used their array position as React identity. Removing or reordering conditions could transfer component state to the wrong row. The row now uses the condition semantic key, which is already enforced as unique by the builder. 2. **`react-doctor/only-export-components`** in `src/components/automations/condition-builder.tsx`: the component module also exported the label map, preventing a clean Fast Refresh boundary and potentially forcing state-losing reloads during development. The map now lives in `condition-labels.ts` and both consumers import it there. Both findings were ungrouped diagnostics and therefore count as one task each. No selected finding had a non-null `fixGroupId`; grouped findings elsewhere were left intact. ## React Doctor Results - Scanner: React Doctor `0.9.12`, schema version 3, full scope, `@open-inspect/web` - Before: **90 total** diagnostics, 2 errors, 88 warnings, score 62 - After: **88 total** diagnostics, 2 errors, 86 warnings, score 63 - `no-array-index-as-key`: 7 to 6 - `only-export-components`: 1 to 0 - Raw diagnostics cleared: **2** - Changed-scope regression scan: **no issues found** - No new full-scan rule/file/message findings were introduced. Diagnostic IDs in touched files shifted with line numbers only. ## Validation - `npx vitest run src/components/automations/condition-builder.test.tsx`: passed, 29 tests - `npm run typecheck -w @open-inspect/web`: passed - `npm run lint -w @open-inspect/web`: passed - `npx prettier --check packages/web`: passed - `npm test -w @open-inspect/web`: passed, 1,287 tests - `npx -y react-doctor@latest . --json --json-out /tmp/react-doctor-after.json --yes --blocking none`: completed, selected findings removed - `npx -y react-doctor@latest . --verbose --scope changed --base origin/main --yes --blocking none`: passed, no issues - `npm run build -w @open-inspect/web`: reproduces the pre-existing `/_global-error` prerender failure, `TypeError: Cannot read properties of null (reading 'useContext')`, digest `3074926929`; compilation and TypeScript complete first Baseline tests initially had two 5-second authentication-boundary test timeouts. They were transient and the complete post-change test run passes, so no test failure remains. ## Deferred Findings - The two error-severity `effect-needs-cleanup` findings are detector false positives: the provider authorization timers are cleared by the effect teardown, and the session WebSocket is closed by its mount-effect teardown. - State synchronization findings in auth-adjacent integration settings, secrets, sidebar persistence, and automation forms require broader lifecycle or UX judgment. - Giant components, reducer migrations, dynamic chart imports, image optimization, locale formatting, iframe sandboxing, and performance-only loop rewrites require broader refactors, runtime evidence, security review, or visual/product decisions. - Remaining index-key findings lack stable IDs or are append-only data; they were not changed speculatively. - No visual verification was run because these changes do not alter rendered styling or layout. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/5524f71cd31217e89b652a450068bbe8)* Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary - keep a sticky session timeline pinned to the physical bottom when its viewport changes, including terminal preference restoration and panel resizing - keep the timeline pinned while virtual row measurements correct the synthetic content height - preserve the current position after a user intentionally scrolls away from the bottom - add a regression experiment covering viewport shrink, delayed content growth, and user-scroll preservation ## Root cause Timeline virtualization scrolls against estimated row heights. Measurements can update the synthetic content height after the existing `[events, isProcessing]` layout effect has run. Restoring an open terminal after hydration also shrinks the timeline viewport without changing either dependency, leaving the session above its new bottom. ## Validation - `npm test -w @open-inspect/web -- src/components/session-timeline-scroll.test.tsx src/components/session-timeline.test.tsx src/lib/timeline-virtual-rows.test.ts` (44 tests passed) - `npm run typecheck -w @open-inspect/web` - ESLint on changed files - full web suite: 1,396 tests passed; two concurrent resource-related timeouts passed when rerun independently - `git diff --check` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/12148d875927d2f52aa3a39f9b69d1af)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved session timeline scrolling so it stays anchored at the bottom as the viewport or content resizes. * Preserved a user’s manual scroll position when they move away from the bottom. * Improved scrolling behavior as timeline content grows or new activity is appended. <!-- 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 the `workspace.audit.read` permission and shared schemas for durable audit event pages - expose a human-user-only, permission-gated `GET /audit-events` endpoint with strict query validation, no-store caching, and opaque newest-first keyset pagination - add the supporting D1 index plus a web BFF, validated SWR hook, and dedicated responsive Workspace Audit Log settings page - show outcome, timestamp, actor/resource snapshots, request/reason context, expandable structured metadata, and Previous/Next navigation ## Authorization and behavior - Owner and Administrator inherit audit-read access; Member and Viewer do not - custom roles may be granted audit-read access - successful audit reads are not recursively audited, while denied reads remain recorded - historical snapshot values are displayed directly without joining mutable user records ## Testing - `npm test -w @open-inspect/shared` (798 tests) - `npm test -w @open-inspect/control-plane` (3,415 tests) - `npm run test:integration -w @open-inspect/control-plane` (1,082 tests) - focused audit web tests (20 tests) - web auth-boundary tests individually (12 tests) - `npm run typecheck` - `npm run lint` - `npm run lint:complexity` - `NODE_ENV=production npm run build -w @open-inspect/web` - changed-file Prettier check and `git diff --check` ## Verification note The aggregate web suite passed 1,406 tests but its two ESLint-boundary tests exceeded their existing 5-second per-test timeout under full-suite load; both files passed when rerun individually. Browser verification of the authenticated panel was not possible locally because the sandbox has no configured OAuth/control-plane session. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/97badb97b9dc770a3214927e8ed1bd16)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a workspace **Audit log** view for authorized users. * Display event details, including timestamps, outcomes, actors, resources, request IDs, reason codes, and expandable structured details. * Added cursor-based pagination with Previous and Next controls, loading, empty, and retry states. * Added a protected audit-events API endpoint and workspace audit-read permission. * **Bug Fixes** * Improved validation for pagination parameters and malformed cursors. * **Tests** * Added coverage for authorization, pagination, validation, rendering, and error 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>
## Summary - normalize proxied Codex calls to a `Request` before rewriting the endpoint - preserve source request methods, bodies, headers, signals, and other Fetch options - replace the dummy authorization value with broker-provided account credentials - add regression coverage for a POST supplied as a source `Request` ## Testing - `node --test tests/*.test.mjs` (19 passed) - `uv run --extra dev pytest tests/test_codex_auth_plugin_setup.py -q` (6 passed) - `npx prettier --check packages/sandbox-runtime/src/sandbox_runtime/plugins/codex-auth-plugin.js packages/sandbox-runtime/tests/codex-auth-plugin.test.mjs` - `git diff --check` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e0f189f3c5f4602c3e3cab2b983e7587)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved authentication handling for proxied API requests. - Preserved caller-provided authorization credentials for non-OAuth requests. - Ensured OAuth requests use refreshed credentials and account information correctly. - Preserved request methods, custom headers, and request bodies when routing requests through the proxy. - **Tests** - Added coverage for OAuth credential replacement and request rewriting. - Verified non-OAuth requests retain their original authorization headers. <!-- 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 - document notable merged changes from August 28 through September 1 - group related RBAC, audit, settings, Autofix, automation security, Slack setup, and prompt queue improvements - use each feature group's final pull request merge date ## Validation - `npx prettier --check CHANGELOG.md` - `git diff --check` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/c4a18123f22753172231107ae35c6439)* Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary - keep child session action triggers measurable while their portaled dropdown is open - preserve the existing hover/focus visibility behavior without removing the trigger from layout - add regression coverage for the open child-session menu anchor state ## Root cause The desktop child-session trigger used `display: none` outside the row's hover/focus states. Dropdown content is rendered in a portal, so moving into the open menu could clear those row states, hide the trigger, and leave Radix without a measurable anchor. The menu would then reposition at the far-left viewport origin. ## Verification - `npm test -w @open-inspect/web -- src/components/session-list-item.test.tsx` - `npm test -w @open-inspect/web -- --testTimeout=15000` (185 files, 1,414 tests) - `npm run typecheck -w @open-inspect/web` - `npx eslint src/components/session-list-item.tsx src/components/session-list-item.test.tsx` - `npx prettier --check src/components/session-list-item.tsx src/components/session-list-item.test.tsx` - `git diff --check` ## Visual verification The local app reached its authentication boundary, but no local control-plane credentials/session were configured, so an authenticated child-session sidebar could not be opened in the browser. The interaction is covered at the component level by asserting that the open Radix trigger remains rendered and measurable. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/9be673f97d1332c19681e2b4264a07b6)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved the desktop child-session actions button so it remains properly positioned and measurable when hidden. * The button now appears reliably on hover, focus, or when its menu is open. * Mobile 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 a shared `AnalyticsDashboardResponse` contract with explicit `generatedAt` and half-open `[startAt, endAt)` window semantics - add `GET /analytics/dashboard`, authorized once with `analytics.read`, that composes all 12 analytics statements into one D1 batch - add a matching web BFF route and replace five independently polling SWR resources with one dashboard resource - retain existing component props and specialist analytics endpoints - remove the duplicate session-derived `PRs Created` summary card so the PR funnel is the dashboard's canonical top-level PR-created metric ## Snapshot semantics The route calls `Date.now()` once. That value is the response `generatedAt`, the window `endAt`, and the anchor for current open-PR age calculations. `startAt` is derived once from the selected day range. All session summary, timeseries, repository/user breakdown, and pull-request statements execute in one `SqlDatabase.batch`, whose D1 contract provides one consistent persisted-database snapshot and positional results. The consistency boundary is D1 state at batch execution. It does not make upstream provider events or asynchronously denormalized session/PR data atomic before they reach D1. ## Endpoint compatibility Repository-wide call-site inspection found the five specialist endpoints are currently consumed only by the web dashboard. They are retained because they are existing public control-plane contracts and may have external consumers. Their response shapes and filtering semantics remain unchanged; the stores now expose statement preparation and result decoding so the dashboard coordinator can reuse the same SQL without duplication. ## R10 handling The dependency was confirmed: `AnalyticsSummaryResponse.totalPrs` comes from `sessions.pr_count`, is windowed by session creation, and excludes automation, while `pullRequests.funnel.created` comes from `session_pull_requests`, is windowed by provider PR creation, and includes automation. They can legitimately disagree. For compatibility, the legacy `totalPrs` field remains on the summary contract and specialist endpoint. The dashboard no longer renders its duplicate `PRs Created` card. The dedicated PR funnel, backed by `session_pull_requests`, is now the sole canonical top-level PR-created metric. Repository/user PR columns remain as contextual session-breakdown metrics. ## Validation - `npm run build -w @open-inspect/shared` - shared typecheck, full test suite (799 tests), and lint - control-plane full unit suite (3,419 tests), focused workerd integration suites (11 tests), typecheck, lint, and build - web focused analytics/BFF/page/component tests, typecheck, lint, and production build - full web suite completed 1,416 tests; two unrelated ESLint-boundary tests timed out under parallel load and both passed when rerun alone (12 tests) - Prettier check and `git diff --check` ## Risks - dashboard refreshes now issue one 12-statement D1 batch instead of five HTTP requests containing multiple independent database reads; this improves coherence but makes the dashboard response depend on the entire batch succeeding - specialist endpoints can still produce independently anchored snapshots when called directly; only the new dashboard endpoint guarantees cross-section coherence - the legacy summary `totalPrs` field remains available for compatibility and should not be treated as equivalent to the PR funnel metric --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/b21bcf5402eb44e3c3e257b697a6c003)* Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
) ## Summary - replace one point query per repository with set queries containing up to 50 `(repo_owner, repo_name)` predicates - normalize and deduplicate identities before querying, reducing SQL statement count from one per input to `ceil(unique repositories / 50)` - derive chunk capacity from the authoritative `MAX_D1_QUERY_PARAMETERS` limit so every statement binds at most 100 parameters - preserve the existing lowercase-keyed `Map` interface, missing-row behavior, mixed-case matching, and nested owners as opaque strings - leave `routes/repos.ts` unchanged because the store interface remains stable ## Tests - add boundary coverage for 50 tuples in one query and 51 tuples across two queries - verify duplicate mixed-case identities bind only once - verify nested owners and lowercase output keys - verify found and missing rows across chunk boundaries - `npm test -w @open-inspect/control-plane -- src/db/repo-metadata.test.ts` - `npm test -w @open-inspect/control-plane` (227 files, 3,418 tests) - `npm run typecheck -w @open-inspect/control-plane` - ESLint and Prettier checks for both changed files ## Risk Low. The query uses parenthesized `OR` predicates supported by SQLite/D1 rather than relying on row-value `IN`. No schema migration or caller change is required. The main behavioral change is fewer SQL statements; result keys and omission semantics are unchanged. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/02786a8acba38fdc214ab0fde70f1060)* Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary - extract the existing race-safe session read-state item transform so legacy lists and inbox caches share the newer-message guard - reconcile read-state changes across retained sidebar pages, string-keyed inbox snapshots/pages, and tuple-keyed pagination caches - dispatch a browser-local reconciliation event after legacy list cache updates and route explicit sidebar mark-read actions through that shared path - revalidate canonical inbox state while removing attention hierarchies only after their root and descendants are all read ## Preserved behavior This intentionally brings over only the cache synchronization portion of ColeMurray#1695. It does not change session-page observer ownership, `TerminalMessageReadObserver`, `SessionTimeline` observer props, virtualized scrolling, or visible-message read semantics from ColeMurray#1694. ## Verification - `npm test -w @open-inspect/web -- src/lib/session-list.test.ts src/lib/session-inbox-api.test.ts src/lib/session-read-state.test.ts src/hooks/use-sidebar-sessions.test.tsx src/components/session-timeline.test.tsx src/components/session-timeline-scroll.test.tsx` (99 tests passed) - `npm test -w @open-inspect/web` (1,407 tests passed) - `npm run lint -w @open-inspect/web` - `npm run typecheck -w @open-inspect/web` - `NODE_ENV=production npm run build -w @open-inspect/web` - `git diff --check main...HEAD` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/88dd42370a25b83c45c2b65f8f484c48)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved synchronization of session read status across the sidebar, inbox, and paginated results. - Prevented stale unread indicators from reappearing after session updates or remounts. - Preserved newer message and read-state information when updating cached session data. - Updated pagination when read sessions change categories, ensuring refreshed results use the correct position. - **Tests** - Added coverage for read-state reconciliation, pagination resets, category transitions, caching, and protection against stale updates. <!-- 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 audit recommendation R4 by making environment create/update use the same ordered repository-set resolver as session launch. - Persist provider-canonical `repoOwner`, `repoName`, `repoId`, and `baseBranch` - Preserve request order so repository position 0 remains the primary repository - Reuse launch-time branch defaults, deterministic resolution errors, canonical duplicate detection, and checkout-path collision detection - Keep nested repository owners opaque throughout resolution and persistence - Remove the environment route's parallel per-repository resolution implementation ## Motivation Environment saves previously resolved each repository independently but persisted the requested owner/name. Providers can return canonical identities after redirects or renames, allowing saved environments and session launch to disagree after canonicalization. R4 requires one canonical ordered set model for both paths. ## Tests - Added focused environment resolver tests for redirect-style canonicalization, nested owners, branch defaults, input/primary order, canonical duplicate identities, and checkout-name collisions - `npm test -w @open-inspect/control-plane -- src/routes/environments.test.ts src/repos/resolve.test.ts` - `npm test -w @open-inspect/control-plane` (228 files, 3,418 tests) - `npm run test:integration -w @open-inspect/control-plane -- test/integration/environments-routes.test.ts test/integration/session-from-environment.test.ts` - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint -w @open-inspect/control-plane` - Prettier check on changed files ## Risks Environment repository resolution now intentionally inherits session launch's set-level HTTP errors and all-failure aggregation. This aligns save-time acceptance with launch-time acceptance and may reject provider-canonical duplicates or checkout collisions that were previously persisted and failed later. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/842fc886367bbc1ce486173caab19598)* Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary
Fixes **1 root-cause task** from the nightly React Doctor scan.
1. **`react-doctor/only-export-components`** —
`packages/web/src/components/settings/audit-log-settings.tsx:24`
- Problem: `auditActionLabel` was publicly exported from a component
module even though it has no external consumers.
- Impact: the extra non-component export prevents the module from being
a clean Fast Refresh boundary, which can cause component state to reset
during development updates.
- Human severity: **Warning**.
- Fix: keep the helper module-private.
## Counting
A task unit is one non-null `fixGroupId` group or one ungrouped
diagnostic. This task was one ungrouped diagnostic, so it counts as **1
root-cause task**. No `fixGroupId` was split.
## React Doctor Results
- Scanner: React Doctor `0.9.12`, schema version 3, full `packages/web`
scope
- Before: **101 total** (`2 errors`, `99 warnings`, 56 affected files),
score 62
- After: **100 total** (`2 errors`, `98 warnings`, 56 affected files),
score 63
- `react-doctor/only-export-components`: **1 -> 0**
- Raw diagnostics cleared: **1**
- New diagnostics introduced: **0**
- Changed-scope scan against `origin/main`: **no issues found**
## Validation
- `npm run typecheck` in `packages/web`: passed
- `npm run lint` in `packages/web`: passed
- `npm test` in `packages/web`: passed, 185 files / 1,413 tests
- `npx vitest run src/components/settings/audit-log-settings.test.tsx`:
passed, 8 tests
- `npx prettier --check
"packages/web/**/*.{ts,tsx,js,jsx,json,css,md}"`: passed
- `npx -y react-doctor@latest . --json --json-out
/tmp/react-doctor-after.json --yes --blocking none`: passed; selected
task absent and no new finding
- `npx -y react-doctor@latest . --verbose --scope changed --base
origin/main --yes --blocking none`: passed; no changed-scope issues
- `npm run build` in `packages/web`: pre-existing failure remains,
detailed below
## Pre-existing Failures
- Production build still fails while prerendering `/_global-error` with
`TypeError: Cannot read properties of null (reading 'useContext')`,
digest `2693865170`, after repeated pre-existing missing-key warnings
for Next-generated boundaries. The same fingerprint occurred before this
edit.
- The baseline full test run had two 5-second timeout failures in
`client-auth-boundary-eslint.test.ts` and
`server-auth-boundary-eslint.test.ts`; the post-change full run passed
all 1,413 tests, so those timeouts did not remain.
## Deferred Findings
- Both `effect-needs-cleanup` errors were deferred as detector false
positives: authorization timers already have teardown cleanup, and the
WebSocket is closed by its owning effect cleanup.
- `no-loading-flag-reset-outside-finally` was deferred as stale because
the reset already occurs in `finally`.
- Performance-only findings were deferred where canonical recipes
require runtime measurement unavailable in this run.
- Array-index keys, locale formatting, client redirects, image
migration, state/effect restructuring, iframe sandboxing, and
giant-component findings were deferred because they require stable
identity, UX/runtime, security, or architecture judgment, or
migration-scale work.
## Visual Verification
Not run. The change only removes an unused export modifier and does not
alter rendered UI or runtime behavior.
<!-- react-doctor-owner: nightly-cleanup -->
<!-- react-doctor-bucket: web-safe-local -->
<!-- react-doctor-base-sha: bf395e2 -->
<!-- react-doctor-diagnostic-manifest:
react-doctor/only-export-components|src/components/settings/audit-log-settings.tsx|24|ungrouped
-->
---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/2644ecfb89a9532a564c85c2d72d5b16)*
Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
) ## Summary Adds focused regression coverage for recently changed permission-sensitive logic: - Automation execution authorization now verifies fail-closed behavior when the SQL guard returns denied or no row for both automation execution and principal checks. - Web sandbox access hook tests now cover protected tunnel/dashboard metadata, rolling-deploy defaults for older control-plane responses, malformed access payloads, and clear/refresh cache behavior that prevents stale privileged credentials from lingering. ## Verification - `npm test -w @open-inspect/control-plane -- authorization-guard.test.ts` - `npm test -w @open-inspect/web -- use-sandbox-access.test.tsx` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/d001e765a1596decf40c3609f7b558b0)* Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
<!-- react-doctor-owner: open-inspect[bot] --> <!-- react-doctor-bucket: nightly-web-2026-08-30 --> <!-- react-doctor-base-sha: 9e88529 --> <!-- react-doctor-diagnostics: react-doctor/only-export-components@src/components/automations/condition-builder.tsx;react-doctor/no-array-index-as-key@src/components/automations/condition-builder.tsx;react-doctor/no-array-index-as-key@src/components/automations/condition-summary.tsx --> ## Summary Fixes 3 root-cause React Doctor tasks in the web package. 1. `react-doctor/no-array-index-as-key` in `src/components/automations/condition-builder.tsx`: replaced the positional key on editable condition rows with the condition's unique semantic key. This prevents React from reusing controlled editor state for the wrong condition after removal or reordering. Human severity: medium. 2. `react-doctor/no-array-index-as-key` in `src/components/automations/condition-summary.tsx`: replaced positional summary keys with semantic condition keys. This preserves stable element identity when conditions are reordered or filtered. Human severity: low. 3. `react-doctor/only-export-components` in `src/components/automations/condition-builder.tsx`: moved the condition-label object to a non-component module. This restores a clean component refresh boundary so edits do not unnecessarily invalidate component state during development. Human severity: low. All 3 diagnostics were ungrouped and therefore counted as one task each. No non-null `fixGroupId` was split or included. ## Diagnostic Delta React Doctor 0.9.12, schema 3, full web scan: - Before: 92 total, 2 errors, 90 warnings, 48 affected files, score 63 - After: 89 total, 2 errors, 87 warnings, 47 affected files, score 63 - Raw diagnostics cleared: 3 - `react-doctor/no-array-index-as-key`: 7 -> 5 - `react-doctor/only-export-components`: 1 -> 0 - No new diagnostic fingerprints or rule-count increases - Changed scope against `origin/main`: no issues found ## Validation - `npm test -- src/components/automations/condition-builder.test.tsx src/components/automations/condition-summary.test.tsx`: passed, 34 tests - `npm run typecheck`: passed - `npm run lint`: passed - `npx prettier --check packages/web`: passed - `npm test`: passed, 1,317 tests across 170 files - `npx -y react-doctor@latest . --json --json-out /tmp/react-doctor-after.json --yes --blocking none`: passed with a complete JSON report - `npx -y react-doctor@latest . --verbose --scope changed --base origin/main --yes --blocking none`: passed, no issues found - `npm run build`: reproduces the pre-existing `/_global-error` prerender failure (`TypeError: Cannot read properties of null (reading 'useContext')`) after compilation and TypeScript succeed. The same generated React key warnings and failure fingerprint occurred before editing; this PR adds no new failing path or rule site. The first parallel full-test attempt had one unrelated 5-second timeout in `server-auth-boundary-eslint.test.ts`; that test passed in isolation (5/5), and the subsequent full suite passed (1,317/1,317). ## Deferred - Two error-level `effect-needs-cleanup` findings remain because source inspection proves existing teardown: device authorization clears timers/aborts polling/cancels the transaction, and session transport closes its ref-owned socket on unmount. No suppression was added. - Locale formatting findings need a timezone/display UX decision or hydration strategy. - Native image findings need remote-host and runtime URL policy decisions. - The terminal iframe sandbox finding needs an explicit capability review. - Remaining positional-key findings lack stable IDs and require data-model changes. - State/effect findings require hydration or component-lifecycle design work. - Performance findings require runtime or production-profile evidence. - Giant-component and reducer findings are migration-scale maintainability work left for later batches. ## Visual Verification Not run: these changes preserve rendered markup and styling; they only stabilize React identity and module refresh boundaries. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/8d8659bf37b678487a428605c197043f)* Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
Automated nightly unsafe-cast remediation sweep. This PR replaces three unsafe casts of persisted integration settings with parsing through the existing shared Zod schemas, preserving the current warn-and-fallback behavior for malformed stored data. It follows the TypeScript Coding Standards for unsafe-cast / parse-don't-assert remediation and the Zod boundary-validation pattern established in PR ColeMurray#807. | Finding | Risk | Cast removed | Fix | | --- | --- | --- | --- | | `packages/control-plane/src/session/integration-settings-resolution.ts:36` | MEDIUM | `settings as CodeServerSettings` for persisted code-server settings | Parse with existing `codeServerSettingsSchema.parse(settings)` | | `packages/control-plane/src/session/integration-settings-resolution.ts:61` | MEDIUM | `settings as VncSettings` for persisted VNC settings | Parse with existing `vncSettingsSchema.parse(settings)` | | `packages/control-plane/src/session/integration-settings-resolution.ts:108` | MEDIUM | `settings as SandboxSettings` for persisted sandbox settings | Parse with existing `sandboxSettingsSchema.parse(settings)` | Verification: | Command | Result | | --- | --- | | `npm run build -w @open-inspect/shared` | Passed | | `npm run build -w @open-inspect/control-plane` | Passed | | `npm run typecheck` | Passed | | `npm run lint` | Passed | | `npm run format` | Passed | | `npm test -w @open-inspect/control-plane -- integration-settings-resolution.test.ts` | Passed | | `npm test -w @open-inspect/control-plane` | Passed | Existing open PRs labeled `automation:unsafe-cast` were checked before selecting findings; this change avoids files already covered there. --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/2107fb318c98b3df1e15f6c59a5a3bdb)* Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…oleMurray#1711) ## Why `SessionTerminalMessageProjection` writes each completed turn's terminal message onto the D1 session row that the inbox categories and read state are computed from. When that write failed twice inline it was logged as `session_terminal_message.projection_failed` and dropped. The session then showed no unread state and stayed in the wrong inbox category until its next turn happened to succeed. This was the one server-side gap found in the read-state architecture review that produced ColeMurray#1710. ## What changes - **Pending projection is persisted.** New Durable Object table `terminal_message_projection_pending` (migration 47) holds the single newest message awaiting projection. The projection is monotonic by `(created_at, id)`, so an older pending message would be a no-op and is never kept over a newer one. - **Retry runs from the alarm.** After the inline attempts fail, the projection persists the message and arms the shared alarm slot. `createAlarmHandler` calls `flushPending()` first. Backoff starts at 5s, doubles, caps at 5min, and gives up after 8 alarm attempts with `session_terminal_message.projection_abandoned`. - **Deadline survives other alarms and restarts.** The alarm slot is shared with lifecycle checks, and `beginDelivery` clears the pending deadline when any alarm fires, so `flushPending` re-schedules its own deadline when it runs early. `rearm()` runs on Durable Object rehydration. - **A newer message landing inline clears the pending one.** `clearThrough` only drops an entry at or below the message that landed. - `SessionTerminalMessageProjection` now takes a deps object (`sessionIndex`, `getSessionId`, `store`, `alarmScheduler`, `now`, `log`). Log events: `projection_deferred` (warn), `projection_retry_scheduled` (warn), `projection_recovered` (info), `projection_abandoned` (error). ## Tests - `terminal-message-projection.test.ts`: inline retry unchanged; defer + arm on double failure; newer inline success clears the pending entry, older does not; `flushPending` no-op / re-arm when not due / recover / backoff / cap / abandon; `rearm`. - `terminal-message-projection-store.test.ts`: real SQLite via `node:sqlite` against `initSchema`, covering newest-wins upsert and `clearThrough`. - `alarm/handler.test.ts`: `flushPending` runs before stop-confirmation recovery. - Control-plane unit (3444) and integration (1084) suites, typecheck, lint, prettier all green. Independent of ColeMurray#1710; both branch from `main`. https://claude.ai/code/session_017wKwqfn4aE7BjV9PgdwraF <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved reliability of terminal messages when immediate delivery fails. - Failed terminal-message updates are retried automatically with increasing delays. - Pending updates resume after service restarts and are processed before stop-confirmation recovery. - Retry metadata remains associated with the correct pending message. - Retry attempts are capped, while successful updates clear pending state. - Stop-confirmation timeouts remain scheduled when earlier alarms fire. - **Maintenance** - Added persistent tracking for terminal-message updates awaiting delivery, including retry status and scheduling information. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…ay#1710) ## Why Read state has had a run of client-side incidents (ColeMurray#1228, ColeMurray#1488, ColeMurray#1628, ColeMurray#1694, ColeMurray#1695, ColeMurray#1699). The server model (per-user cursor, monotonic projection, server-computed categories) has held up; the client has not. Two structural causes this PR removes: - **No order in the contract.** The server orders terminal messages by `latest_terminal_message_created_at` but never exposed it, so the client guessed with "different message ID, skip" and the sidebar reset whole pagination chains when a cached row and a PATCH result disagreed. - **Mark-read tied to DOM geometry.** `TerminalMessageReadObserver` waited for a virtualized row to intersect by 48px in a focused document, with a retry counter that reset on scroll. For a product with one terminal message per turn, opening the session is the read signal (the same semantic Devin and Codex ship). ## What changes **Contract.** `SessionReadState` and the PATCH `/sessions/:id/read-state` result gain `version: number`: the projected creation time of the latest terminal message, `0` before any turn completes. Rule: a higher version supersedes a lower one; within one version, read is final. The D1 schema, inbox SQL, cursor format and route are unchanged. Clients that ignore the field keep working. **Trigger.** `useMarkSessionRead(sessionId, latestTerminalMessageId)` on the session page acknowledges the latest `execution_complete` once per message ID while the document is visible. A hidden tab waits for `visibilitychange`. Focus is not required (the terminal pane holds focus for much of a working session). `not_latest` is settled rather than retried, since the newer message reaches the client and is acknowledged in turn. Only `no_terminal_message` retries, with the existing 2s/4s/8s backoff, and 4xx stops. The observer component, the `terminal` virtual row that existed to host it, and the timeline's read-observation props are deleted. **Caches.** Nothing renders unread from the flat `/api/sessions` caches, so read results no longer mutate them. The sidebar's unreconcilable-row branch (reset attention + destination chains on an ID mismatch) is gone; the version rule makes an in-place update either correct or a no-op. ## Behaviour change to be aware of Opening a session marks its latest message read even if the timeline is behind the diff pane, the media viewer or the details sheet. Previously those overlays suppressed the read. This is intentional. ## Tests - Control plane: read-state and inbox integration suites assert `version` on list rows and PATCH results. - Web: new `use-mark-session-read.test.tsx` (once per ID, hidden-tab wait, no focus requirement, backoff and give-up, 4xx stop, `not_latest` settles, unmount cancels); `readStateSupersedes` / `applySessionReadStateToItem` unit tests replace the flat-list tests; the sidebar test that exercised the ID-mismatch reset now passes through the version rule. - `npm run typecheck`, `npm run lint`, `npm run knip`, web suite (1447), control-plane unit (417) and the two read-state integration suites all green. ## Follow-ups (separate PRs) - Durable retry for the terminal-message projection (today a double D1 failure is logged and swallowed). - Phase 2 of the read-state plan: loaded pages in React state only, render-time read overlay, revalidate instead of relocating. https://claude.ai/code/session_017wKwqfn4aE7BjV9PgdwraF <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added version tracking to session read states for reliable synchronization of read and unread status. - Session read status now automatically acknowledges the latest terminal message while viewing a session. - Transient read-state failures are retried when the page is visible. - **Bug Fixes** - Prevented stale read-state updates from overwriting newer session information. - Improved ordering and reconciliation when messages share the same version. - Improved sidebar and session-list consistency across message updates and archived sessions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary - preserve GitHub review comment `in_reply_to_id` in Autofix feedback - skip Open Inspect App reviews composed entirely of thread replies - continue admitting App reviews that contain top-level findings, including mixed finding/reply reviews - add provider mapping and Autofix eligibility regression coverage ## Why GitHub associates App-authored inline thread responses such as “Fixed” with new submitted review IDs. Autofix treated each reply-only review as new actionable feedback, causing response loops and serialized session backlogs. ## Verification - `npm test -w @open-inspect/control-plane` (230 files, 3,450 tests) - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint -w @open-inspect/control-plane` --- *Created with [Open-Inspect](https://open-inspect-prod.vercel.app/session/e25e3d6a3c59c23e963719d27425faae)* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * GitHub review comments now preserve reply relationships. * Reviews with a non-empty body and only thread replies continue to be processed and queued. * **Bug Fixes** * Reviews containing only replies to the bot’s own empty review are skipped to avoid unnecessary processing. * Reviews containing both top-level findings and replies continue to be processed correctly. <!-- 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 - normalize manifest-less legacy sessions on demand in `SessionSkillStore` - keep human and sandbox skills routes as direct projections of the canonical store result - reject new session initialization unless it resolves or inherits exactly one manifest - preserve `404 Not Found` for unknown sessions ## Testing - `npm run test:integration -w @open-inspect/control-plane -- managed-skills.test.ts session-provider-auth.test.ts` - `npm test -w @open-inspect/control-plane -- src/session/initialize.test.ts` - `npm run typecheck -w @open-inspect/control-plane` - focused ESLint and Prettier checks <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Session initialization now rejects requests with missing or conflicting managed-skills manifest sources. * Skills views and sandbox installations now return 404 when no stored session manifest is available. * Manifest responses and cache identifiers now consistently reflect the stored manifest metadata. * Nonexistent sessions continue to return a 404 response. <!-- 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>
Conflict markers committed. Resolve them in this PR before merging.
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueComment |
Terraform Validation Results
Pushed by: @NicolasWalter, Action: |
Terraform Plan ResultsStatus: ✅ Success Show Planmodule.modal_app[0].null_resource.modal_secrets[0]: Refreshing state... [id=8757687985279342629]
null_resource.control_plane_build: Refreshing state... [id=9105849033611783886]
null_resource.github_bot_build[0]: Refreshing state... [id=8271231830927734747]
null_resource.slack_bot_build[0]: Refreshing state... [id=5379137651876318417]
terraform_data.sign_in_provider_gate: Refreshing state... [id=7f4a67d1-6978-0b23-899d-c2a9004643bd]
terraform_data.cloudflare_custom_domain_gate: Refreshing state... [id=fa456fac-6c14-16e4-a484-3338d1a3718d]
null_resource.web_app_cloudflare_build[0]: Refreshing state... [id=5024517857365059396]
random_bytes.provider_accounts_encryption_key: Refreshing state...
null_resource.linear_bot_build[0]: Refreshing state... [id=956366907826137814]
terraform_data.access_control_gate: Refreshing state... [id=841ab6bc-98a7-018c-1031-ebad4f8b62bc]
random_password.service_auth_secret_web: Refreshing state... [id=none]
random_password.service_auth_secret_github_bot: Refreshing state... [id=none]
random_password.service_auth_secret_linear_bot: Refreshing state... [id=none]
random_password.image_callback_token_pepper: Refreshing state... [id=none]
random_password.service_auth_secret_slack_bot: Refreshing state... [id=none]
module.github_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=87dbfaa1d9ce4a37a42e04c57c434a72]
cloudflare_queue.slack_completion_delivery[0]: Refreshing state... [id=56ef0f3e13bd46a3a39f30c79ec547fa]
module.slack_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=729b357dbb5e4c9d99ec9212cc45766e]
cloudflare_queue.image_build_finalization: Refreshing state... [id=1ca823a150c54578a9ad1814325147a3]
cloudflare_queue.slack_completion_delivery_dlq[0]: Refreshing state... [id=06ce03d2663f4aea937b0c0c1c379c17]
module.session_index_kv.cloudflare_workers_kv_namespace.this: Refreshing state... [id=7f18644fbed34121bbe3a196f373ea93]
data.external.modal_source_hash[0]: Reading...
cloudflare_r2_bucket.media: Refreshing state... [id=open-inspect-media-primo]
cloudflare_d1_database.main: Refreshing state... [id=dba95b03-ace9-47a8-81e9-6e39d8d694c5]
local_file.web_app_wrangler_production[0]: Refreshing state... [id=71b0d3758fc7d34e75fd9a3abe59e2c1b6dadeea]
module.linear_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=d003f1ad81384910a1f48a0a33f18c09]
cloudflare_queue.image_build_finalization_dlq: Refreshing state... [id=cbbc2d8794c04396a550996e7f0cc129]
module.linear_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=33782d80e8ff4af9b30b92870084b674]
module.slack_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=5200e96d69804ea296e1f3a6b39e4243]
data.external.modal_source_hash[0]: Read complete after 0s [id=-]
module.modal_app[0].null_resource.modal_deploy: Refreshing state... [id=5768192879312552892]
null_resource.d1_migrations: Refreshing state... [id=5980374278680462129]
module.linear_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=a55bcc0f-a65c-41a5-8441-05d7b4af3966]
module.slack_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=e87b85ce-d5a2-4b3a-8fbe-23e6200e98fe]
module.linear_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=6986a9e1-4d49-41fa-a780-f4ad5e481b34]
module.slack_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=ba95e23c-c5c2-40d9-a17e-823adba5df2a]
module.control_plane_worker.cloudflare_worker.this: Refreshing state... [id=c208a60c393e45e38eb502346bb7ce1e]
cloudflare_queue_consumer.slack_completion_delivery[0]: Refreshing state...
module.control_plane_worker.cloudflare_worker_version.this: Refreshing state... [id=e480d777-f058-4246-86ef-dc36a6fa28fe]
module.control_plane_worker.cloudflare_workers_deployment.this: Refreshing state... [id=6edc103c-bb0f-471e-8224-9f17597d658a]
module.control_plane_worker.cloudflare_workers_cron_trigger.this[0]: Refreshing state... [id=open-inspect-control-plane-primo]
cloudflare_queue_consumer.image_build_finalization: Refreshing state...
null_resource.web_app_cloudflare_deploy[0]: Refreshing state... [id=1320732786606345683]
module.github_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=4b5e2696491a41eaaa124f4e2a9855f2]
null_resource.web_app_cloudflare_secrets[0]: Refreshing state... [id=8867783181576424643]
module.github_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=21ef4d5d-ec56-47a1-8646-dbfcd4af510b]
module.github_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=0a0e290a-7d4b-42f1-a5e0-49385187b3c5]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
~ update in-place
-/+ destroy and then create replacement
Terraform will perform the following actions:
# cloudflare_queue.github_autofix[0] will be created
+ resource "cloudflare_queue" "github_autofix" {
+ account_id = "bf66240843ed90d19b82e4b90916d29a"
+ consumers = (known after apply)
+ consumers_total_count = (known after apply)
+ created_on = (known after apply)
+ id = (known after apply)
+ modified_on = (known after apply)
+ producers = (known after apply)
+ producers_total_count = (known after apply)
+ queue_id = (known after apply)
+ queue_name = "open-inspect-github-autofix-primo"
+ settings = (known after apply)
}
# cloudflare_queue.github_autofix_dlq[0] will be created
+ resource "cloudflare_queue" "github_autofix_dlq" {
+ account_id = "bf66240843ed90d19b82e4b90916d29a"
+ consumers = (known after apply)
+ consumers_total_count = (known after apply)
+ created_on = (known after apply)
+ id = (known after apply)
+ modified_on = (known after apply)
+ producers = (known after apply)
+ producers_total_count = (known after apply)
+ queue_id = (known after apply)
+ queue_name = "open-inspect-github-autofix-dlq-primo"
+ settings = (known after apply)
}
# cloudflare_queue_consumer.github_autofix[0] will be created
+ resource "cloudflare_queue_consumer" "github_autofix" {
+ account_id = "bf66240843ed90d19b82e4b90916d29a"
+ consumer_id = (known after apply)
+ created_on = (known after apply)
+ dead_letter_queue = "open-inspect-github-autofix-dlq-primo"
+ queue_id = (known after apply)
+ queue_name = (known after apply)
+ script_name = "open-inspect-control-plane-primo"
+ settings = {
+ batch_size = 1
+ max_concurrency = 5
+ max_retries = 4
+ max_wait_time_ms = 1000
+ retry_delay = 30
+ visibility_timeout_ms = (known after apply)
}
+ type = "worker"
}
# local_file.web_app_wrangler_production[0] will be created
+ resource "local_file" "web_app_wrangler_production" {
+ content = <<-EOT
name = "open-inspect-web-primo"
main = ".open-next/worker.js"
compatibility_date = "2025-08-15"
compatibility_flags = ["nodejs_compat", "global_fetch_strictly_public"]
# A custom-domain deployment has one canonical browser origin.
workers_dev = true
[vars]
CONTROL_PLANE_URL = "https://open-inspect-control-plane-primo.primo-bf6.workers.dev"
NEXT_PUBLIC_WS_URL = "wss://open-inspect-control-plane-primo.primo-bf6.workers.dev"
NEXT_PUBLIC_SANDBOX_PROVIDER = "modal"
NEXT_PUBLIC_APP_NAME = "Primo"
NEXT_PUBLIC_APP_ICON_URL = ""
[assets]
directory = ".open-next/assets"
binding = "ASSETS"
[[services]]
binding = "CONTROL_PLANE_WORKER"
service = "open-inspect-control-plane-primo"
EOT
+ content_base64sha256 = (known after apply)
+ content_base64sha512 = (known after apply)
+ content_md5 = (known after apply)
+ content_sha1 = (known after apply)
+ content_sha256 = (known after apply)
+ content_sha512 = (known after apply)
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "../../..//packages/web/wrangler.production.toml"
+ id = (known after apply)
}
# null_resource.control_plane_build must be replaced
-/+ resource "null_resource" "control_plane_build" {
~ id = "9105849033611783886" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-08-26T12:04:31Z" -> (known after apply)
}
}
# null_resource.d1_migrations must be replaced
-/+ resource "null_resource" "d1_migrations" {
~ id = "5980374278680462129" -> (known after apply)
~ triggers = { # forces replacement
~ "migrations_sha" = "177eee0e2901d7ed13c268ff3734192a648a7c7ed0f08db7d5568f5277847087" -> "5f840c6ba6171045d35df0170478fbacf4a13d6e5c039e405f8a8bac9b29b191"
# (1 unchanged element hidden)
}
}
# null_resource.github_bot_build[0] must be replaced
-/+ resource "null_resource" "github_bot_build" {
~ id = "8271231830927734747" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-08-26T12:04:31Z" -> (known after apply)
}
}
# null_resource.linear_bot_build[0] must be replaced
-/+ resource "null_resource" "linear_bot_build" {
~ id = "956366907826137814" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-08-26T12:04:31Z" -> (known after apply)
}
}
# null_resource.slack_bot_build[0] must be replaced
-/+ resource "null_resource" "slack_bot_build" {
~ id = "5379137651876318417" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-08-26T12:04:31Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_build[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_build" {
~ id = "5024517857365059396" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-08-26T12:04:31Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_deploy[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_deploy" {
~ id = "1320732786606345683" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-08-26T12:05:09Z" -> (known after apply)
}
}
# module.control_plane_worker.cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "c208a60c393e45e38eb502346bb7ce1e"
name = "open-inspect-control-plane-primo"
~ observability = {
~ logs = {
+ destinations = (known after apply)
# (4 unchanged attributes hidden)
}
~ traces = {
+ destinations = (known after apply)
# (3 unchanged attributes hidden)
}
# (2 unchanged attributes hidden)
}
~ references = {
~ dispatch_namespace_outbounds = [] -> (known after apply)
~ domains = [] -> (known after apply)
~ durable_objects = [
- {
- namespace_id = "4c77239db3614a6aac69a90e1fbd8955" -> null
- namespace_name = "open-inspect-control-plane-primo_SessionDO" -> null
- worker_id = "c208a60c393e45e38eb502346bb7ce1e" -> null
- worker_name = "open-inspect-control-plane-primo" -> null
},
] -> (known after apply)
~ queues = [
- {
- queue_consumer_id = "648d35d2a8064e8ea79899e946a65334" -> null
- queue_id = "1ca823a150c54578a9ad1814325147a3" -> null
- queue_name = "open-inspect-image-build-finalization-primo" -> null
},
] -> (known after apply)
~ workers = [
- {
- id = "33782d80e8ff4af9b30b92870084b674" -> null
- name = "open-inspect-linear-bot-primo" -> null
},
- {
- id = "5200e96d69804ea296e1f3a6b39e4243" -> null
- name = "open-inspect-slack-bot-primo" -> null
},
- {
- id = "ac07332f8b0f4cdfa4ca04f966a9fa61" -> null
- name = "open-inspect-web-primo" -> null
},
- {
- id = "4b5e2696491a41eaaa124f4e2a9855f2" -> null
- name = "open-inspect-github-bot-primo" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-08-26T12:04:33Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.control_plane_worker.cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
~ annotations = {
+ workers_message = (known after apply)
+ workers_tag = (known after apply)
~ workers_triggered_by = "create_version_api" -> (known after apply)
} -> (known after apply)
~ bindings = (sensitive value) # forces replacement
~ created_on = "2026-08-26T12:04:38Z" -> (known after apply)
~ id = "e480d777-f058-4246-86ef-dc36a6fa28fe" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
~ migration_tag = "v1" -> (known after apply)
~ modules = [
- { # forces replacement
- content_file = "../../..//packages/control-plane/dist/index.js" -> null
- content_sha256 = "4ac20254b2f6c06558a76dfc57261274427cf88b501a0f3645025100bee072e6" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/control-plane/dist/index.js"
+ content_sha256 = "0d648431ae5c0ad3eb884d5f3d04c118d511ef497e639a8946522d8201e1f4ab"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 65 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 124 -> (known after apply)
~ urls = [] -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.control_plane_worker.cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
~ annotations = {
+ workers_message = (known after apply)
~ workers_triggered_by = "deployment" -> (known after apply)
} -> (known after apply)
~ author_email = "nicolas@primo.la" -> (known after apply)
~ created_on = "2026-08-26T12:04:40Z" -> (known after apply)
~ id = "6edc103c-bb0f-471e-8224-9f17597d658a" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "e480d777-f058-4246-86ef-dc36a6fa28fe" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
# module.github_bot_worker[0].cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "4b5e2696491a41eaaa124f4e2a9855f2"
name = "open-inspect-github-bot-primo"
~ observability = {
~ logs = {
+ destinations = (known after apply)
# (4 unchanged attributes hidden)
}
~ traces = {
+ destinations = (known after apply)
# (3 unchanged attributes hidden)
}
# (2 unchanged attributes hidden)
}
~ references = {
~ dispatch_namespace_outbounds = [] -> (known after apply)
~ domains = [] -> (known after apply)
~ durable_objects = [] -> (known after apply)
~ queues = [] -> (known after apply)
~ workers = [] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-08-26T12:04:40Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.github_bot_worker[0].cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
~ annotations = {
+ workers_message = (known after apply)
+ workers_tag = (known after apply)
~ workers_triggered_by = "create_version_api" -> (known after apply)
} -> (known after apply)
~ bindings = (sensitive value) # forces replacement
~ created_on = "2026-08-26T12:04:41Z" -> (known after apply)
~ id = "21ef4d5d-ec56-47a1-8646-dbfcd4af510b" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
+ migration_tag = (known after apply)
~ modules = [
- { # forces replacement
- content_file = "../../..//packages/github-bot/dist/index.js" -> null
- content_sha256 = "54510ead747ee6d78a3d7db31ac2cd9ffeeafb439c037df965ee7c51ccdeda05" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/github-bot/dist/index.js"
+ content_sha256 = "531f52a75d5e025c08759401abc41c92b456ef56ebfad84e9a9266e9ee8f86c2"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 49 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 35 -> (known after apply)
~ urls = [
- "https://21ef4d5d-open-inspect-github-bot-primo.primo-bf6.workers.dev",
] -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.github_bot_worker[0].cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
~ annotations = {
+ workers_message = (known after apply)
~ workers_triggered_by = "deployment" -> (known after apply)
} -> (known after apply)
~ author_email = "nicolas@primo.la" -> (known after apply)
~ created_on = "2026-08-26T12:04:41Z" -> (known after apply)
~ id = "0a0e290a-7d4b-42f1-a5e0-49385187b3c5" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "21ef4d5d-ec56-47a1-8646-dbfcd4af510b" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
# module.linear_bot_worker[0].cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "33782d80e8ff4af9b30b92870084b674"
name = "open-inspect-linear-bot-primo"
~ observability = {
~ logs = {
+ destinations = (known after apply)
# (4 unchanged attributes hidden)
}
~ traces = {
+ destinations = (known after apply)
# (3 unchanged attributes hidden)
}
# (2 unchanged attributes hidden)
}
~ references = {
~ dispatch_namespace_outbounds = [] -> (known after apply)
~ domains = [] -> (known after apply)
~ durable_objects = [] -> (known after apply)
~ queues = [] -> (known after apply)
~ workers = [
- {
- id = "c208a60c393e45e38eb502346bb7ce1e" -> null
- name = "open-inspect-control-plane-primo" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-08-26T12:04:32Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.linear_bot_worker[0].cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
~ annotations = {
+ workers_message = (known after apply)
+ workers_tag = (known after apply)
~ workers_triggered_by = "create_version_api" -> (known after apply)
} -> (known after apply)
~ bindings = (sensitive value) # forces replacement
~ created_on = "2026-08-26T12:04:32Z" -> (known after apply)
~ id = "a55bcc0f-a65c-41a5-8441-05d7b4af3966" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
+ migration_tag = (known after apply)
~ modules = [
- { # forces replacement
- content_file = "../../..//packages/linear-bot/dist/index.js" -> null
- content_sha256 = "cdd5ab4993778450482ea7956889b7ffd9de4c45960b12976b384f16b5b539bf" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/linear-bot/dist/index.js"
+ content_sha256 = "6c9db2425bbdba7e45c788b4a24bcc5872f61aa1de756d2c8f27b07594b015a5"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 68 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 42 -> (known after apply)
~ urls = [
- "https://a55bcc0f-open-inspect-linear-bot-primo.primo-bf6.workers.dev",
] -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.linear_bot_worker[0].cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
~ annotations = {
+ workers_message = (known after apply)
~ workers_triggered_by = "deployment" -> (known after apply)
} -> (known after apply)
~ author_email = "nicolas@primo.la" -> (known after apply)
~ created_on = "2026-08-26T12:04:33Z" -> (known after apply)
~ id = "6986a9e1-4d49-41fa-a780-f4ad5e481b34" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "a55bcc0f-a65c-41a5-8441-05d7b4af3966" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
# module.modal_app[0].null_resource.modal_deploy must be replaced
-/+ resource "null_resource" "modal_deploy" {
~ id = "5768192879312552892" -> (known after apply)
~ triggers = { # forces replacement
~ "source_hash" = "7dea31102eed27234e88e6b28e35595256d20715e6ae85a373d2dfd7edb707e3" -> "9b9b10e226266cccd65a8dd6e236c92d1baa75a22ec38fb5ba00f46be0984da6"
# (3 unchanged elements hidden)
}
}
# module.slack_bot_worker[0].cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "5200e96d69804ea296e1f3a6b39e4243"
name = "open-inspect-slack-bot-primo"
~ observability = {
~ logs = {
+ destinations = (known after apply)
# (4 unchanged attributes hidden)
}
~ traces = {
+ destinations = (known after apply)
# (3 unchanged attributes hidden)
}
# (2 unchanged attributes hidden)
}
~ references = {
~ dispatch_namespace_outbounds = [] -> (known after apply)
~ domains = [] -> (known after apply)
~ durable_objects = [] -> (known after apply)
~ queues = [
- {
- queue_consumer_id = "a755a290fd92417fb11c298f9c1d1f40" -> null
- queue_id = "56ef0f3e13bd46a3a39f30c79ec547fa" -> null
- queue_name = "open-inspect-slack-completion-primo" -> null
},
] -> (known after apply)
~ workers = [
- {
- id = "c208a60c393e45e38eb502346bb7ce1e" -> null
- name = "open-inspect-control-plane-primo" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-08-26T12:04:31Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.slack_bot_worker[0].cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
~ annotations = {
+ workers_message = (known after apply)
+ workers_tag = (known after apply)
~ workers_triggered_by = "create_version_api" -> (known after apply)
} -> (known after apply)
~ bindings = (sensitive value) # forces replacement
~ created_on = "2026-08-26T12:04:32Z" -> (known after apply)
~ id = "e87b85ce-d5a2-4b3a-8fbe-23e6200e98fe" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
+ migration_tag = (known after apply)
~ modules = [
- { # forces replacement
- content_file = "../../..//packages/slack-bot/dist/index.js" -> null
- content_sha256 = "6911837e8156b867d1069efd3fbac032f460149d1226b260fddb6d37d5233cee" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/slack-bot/dist/index.js"
+ content_sha256 = "63459d874c954bb360fbe7d97e1862e50c7f18fdbdcb6211ff49d59f5f38153e"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 71 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 99 -> (known after apply)
~ urls = [
- "https://e87b85ce-open-inspect-slack-bot-primo.primo-bf6.workers.dev",
] -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.slack_bot_worker[0].cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
~ annotations = {
+ workers_message = (known after apply)
~ workers_triggered_by = "deployment" -> (known after apply)
} -> (known after apply)
~ author_email = "nicolas@primo.la" -> (known after apply)
~ created_on = "2026-08-26T12:04:33Z" -> (known after apply)
~ id = "ba95e23c-c5c2-40d9-a17e-823adba5df2a" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "e87b85ce-d5a2-4b3a-8fbe-23e6200e98fe" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
Plan: 20 to add, 4 to change, 16 to destroy.
Changes to Outputs:
+ d1_database_name = "open-inspect-primo"
+ slack_bot_events_url = "https://open-inspect-slack-bot-primo.primo-bf6.workers.dev/events"
+ slack_bot_interactions_url = "https://open-inspect-slack-bot-primo.primo-bf6.workers.dev/interactions"
+ slack_bot_worker_url = "https://open-inspect-slack-bot-primo.primo-bf6.workers.dev"
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"Pushed by: @NicolasWalter |
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.
Automated upstream sync
Upstream:
ColeMurray/background-agents@mainThis PR was opened automatically by
.github/workflows/sync-upstream.yml.