Skip to content

chore: sync with upstream 2026-08-29 (conflicts) - #120

Draft
NicolasWalter wants to merge 60 commits into
mainfrom
sync/upstream-2026-08-29
Draft

chore: sync with upstream 2026-08-29 (conflicts)#120
NicolasWalter wants to merge 60 commits into
mainfrom
sync/upstream-2026-08-29

Conversation

@NicolasWalter

Copy link
Copy Markdown

Automated upstream sync

⚠️ Conflicts detected — resolve markers before merging.

Upstream: ColeMurray/background-agents@main

This PR was opened automatically by .github/workflows/sync-upstream.yml.

ColeMurray and others added 30 commits August 25, 2026 22:56
…gest closure bags (ColeMurray#1608)

## What

First PR of the deps-style normalization campaign (follow-through on the
ColeMurray#1594ColeMurray#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#1045ColeMurray#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>
open-inspect Bot and others added 27 commits August 27, 2026 13:25
This is an automated nightly unsafe-cast remediation sweep. It replaces
selected unsafe automation request-body casts with parse-don't-assert
validation at trust boundaries, 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/control-plane/src/routes/automations.ts:472` | HIGH |
`parseJsonBody<CreateAutomationRequest & ...>(request)` for automation
creation, bypassing the existing shared request schema before
identity/persistence | Parses as `unknown`, runs identity enforcement
against the raw pre-Zod body, then validates with a route-local
extension of `createAutomationRequestSchema.safeParse`; existing custom
parsers still handle repositories, environments, and trigger config to
preserve current 400 behavior. |
| `packages/control-plane/src/routes/automations.ts:763` | HIGH |
`parseJsonBody<UpdateAutomationRequest>(request)` for automation
updates, bypassing the existing shared request schema before persistence
| Parses as `unknown`, validates with a route-local extension of
`updateAutomationRequestSchema.safeParse`, and attaches `triggerConfig`
only after the existing parser accepts it. |
| `packages/control-plane/src/routes/automations.ts:1232` | HIGH |
`parseJsonBody<{ sentryClientSecret?: string }>(request)` before
regenerating Sentry automation credentials | Parses as `unknown` and
uses an inline object/string guard before encrypting and persisting the
secret. |

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 -w @open-inspect/control-plane` | Passed |
| `npm run format` | Passed |
| `npm test -w @open-inspect/control-plane --
src/routes/automations.test.ts` | Passed: 92 tests |
| `npm test -w @open-inspect/control-plane` | Passed: 205 files / 3184
tests |
| `git diff --check` | Passed |
| `npm run lint` | Failed on unrelated pre-existing `.opencode/**`
no-undef / unused-var errors (`Headers`, `fetch`, `URL`, `process`,
`console`, etc.), outside this sweep's touched files. |

No dependency changes.

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/0b3529c1afa61ac5b07cae4ca06f0691)*

---------

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 persisted D1 settings casts with Zod-backed parsing at the
storage boundary, 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/control-plane/src/db/integration-settings.ts:98` | MEDIUM |
`JSON.parse(row.settings) as IntegrationSettingsMap[K]["global"]` for
persisted global integration settings | Shared Zod integration global
settings schemas + `safeParse` in `parseStoredGlobalSettings` |
| `packages/control-plane/src/db/integration-settings.ts:158` | MEDIUM |
`JSON.parse(row.settings) as IntegrationSettingsMap[K]["repo"]` for
persisted repo integration settings | Shared Zod integration repo
settings schemas + `safeParse` in `parseStoredRepoSettings` |
| `packages/control-plane/src/db/integration-settings.ts:227` | MEDIUM |
`JSON.parse(row.settings) as IntegrationSettingsMap[K]["repo"]` for
persisted environment integration settings | Shared Zod integration repo
settings schemas + `safeParse` in `parseStoredRepoSettings` |

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 -w @open-inspect/shared` | Passed |
| `npm run lint -w @open-inspect/control-plane` | Passed |
| `npm run format` | Passed |
| `npm test -w @open-inspect/shared` | Passed |
| `npm test -w @open-inspect/control-plane` | Passed |

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/5761693cf0e4781cdb4a2f838ed2fcc4)*

---------

Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
Automated unsafe-cast remediation for secret-write and SCM-settings
trust boundaries.

- Uses one canonical secret request schema across repository, global,
and environment secret routes.
- Preserves every own JSON secret key (including `__proto__`) while
validating string values before persistence.
- Uses shared strict SCM schemas for route and store validation, trims
labels, rejects comma-separated labels, and keeps SCM integrated with
the canonical settings-schema registry.
- Adds focused route, persistence, and shared-schema regression
coverage.

The branch was semantically reconciled with the latest `main` after the
related secret-validation and integration-schema PRs merged.

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/17476d818897488f4edc3343c0abfd63)*

---------

Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
…urray#1632)

Phase C of the collaborator-arity program — with a plan correction the
inventory forced. The plan proposed a shared "state-assembly reader" for
`SessionLifecycleHandler` and `ChildSessionsHandler`, on the theory that
the repository sextet recurred because state assembly was duplicated
inline. Measured against the code, that theory is false:

- `getState()` is thirty lines over two repos, and every collaborator it
touches is used by other routes — a reader would remove **zero**
constructor params from the lifecycle handler.
- `getChildSummary()`'s assembly is already extracted into pure builders
(`child-session-summary.ts`); the handler only gathers rows.
- Each assembly has exactly one consumer, so a reader would be a move,
not a de-duplication — failing the plan's own rule for composition
classes (nameable concept, real behavior, **multiple consumers**).

What the per-method collaborator chart actually shows is two stark
**exclusive clusters**:

| Class | Finding |
| --- | --- |
| `SessionLifecycleHandler` (14) | `init` exclusively owns 5 params
(`tokenEncryptionKey`, `encryptToken`, `generateId`,
`scheduleWarmSandbox`, `now`) — bootstrap is a different responsibility
from read/transition |
| `ChildSessionsHandler` (10) | `getChildSummary` exclusively owns 5
params (`eventRepository`, `artifactRepository`, `sandboxRepository`,
`durableObjectId`, `log`) — the parent-facing read model is a different
responsibility from spawn/prompt plumbing |

So this PR applies decision rule 1 (split when methods partition over
disjoint collaborator subsets) twice:

- **`SessionInitHandler`** (new, 9 params): `/internal/init` — the
DO-side session bootstrap (aggregate transaction + warm-spawn trigger),
with its request schema. `SessionLifecycleHandler` drops to **9**
(getState/updateTitle/archive/expireDraft/unarchive/cancel — read +
transition the aggregate).
- **`ChildSummaryHandler`** (new, 7 params): `/internal/child-summary` —
the parent-facing read model, still delegating assembly to the pure
builders. `ChildSessionsHandler` drops to **5**.

Route wiring is thunk-based, so only two thunk targets in
`components.ts` changed. Both test suites keep every assertion: the
harnesses now compose the split pair exactly as `components.ts` does,
and the bound `handler` objects keep all call sites intact.

Also closed out in this phase, per the plan's deferred decision:
`SandboxHandler` (15) was charted and **accepted** — eight small routes
each using 1–3 collaborators with no exclusive cluster; splitting would
remove line count, not coupling.

Net: 14 → 9 + 9, 10 → 5 + 7, and every handler class in
`session/http/handlers/` now sits at or below 9 constructor params.

## Verification

- `tsc` ×3 programs clean; ESLint + Prettier clean
- Handler suites 196/196 with assertions unchanged; full unit battery
3254/3254; integration 1003/1003


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added child-session summaries with validation, detailed session
information, filtering, pagination, and clear error responses.
* Added a dedicated session initialization flow that validates requests,
securely processes tokens, creates required session records, and begins
sandbox preparation.

* **Refactor**
* Improved session request routing by separating initialization and
child-summary handling from general session lifecycle operations.
* Improved handling of session setup and child-session detail requests
for more consistent responses.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary

- add `workflow_run.completed` as a GitHub automation event with
workflow-name and event-specific conclusion filters
- normalize workflow metadata, preserve run-attempt deduplication, and
scope concurrency to the logical workflow run
- make GitHub condition availability event-aware across shared
validation, API updates, and the automation form
- preserve rolling compatibility for existing check-suite
`checkConclusion` events and unchanged persisted legacy conditions
- make event changes reversible in the form and clear stale
dropped-condition feedback
- document the opt-in GitHub bot, webhook subscriptions, and required
App permissions

This PR contains the contributor work from ColeMurray#1426 plus follow-up fixes
for its unresolved review findings and compatibility issues.

## Validation

- `npm run typecheck`
- `npm test -w @open-inspect/shared` (718 passed before final focused
additions)
- `npm test -w @open-inspect/control-plane` (3043 passed)
- `npm test -w @open-inspect/web` (1232 passed before final focused
additions)
- `npm test -w @open-inspect/github-bot` (136 passed)
- focused final suites: shared 104 passed, control-plane automation
routes 96 passed, web automation components 74 passed
- targeted ESLint and Prettier checks for all changed files
- `git diff --check origin/main...HEAD`

## Known unrelated validation issues

- the full control-plane integration run timed out after existing
session provider-auth/sandbox failures outside the automation paths
- the full production build compiled all worker bundles and the web app,
then hit the existing Next.js `/_global-error` prerender failure
- repository-wide lint/format checks include pre-existing `.opencode`
errors; targeted checks for this PR pass

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/f326a53ef1f4471b0ff232e5f6b66380)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added opt-in GitHub event automations, including completed GitHub
Actions workflow runs.
* Added workflow name and conclusion filters with event-specific
condition options.
* Workflow context includes run details such as name, status, branch,
commit, and URL.
* **Improvements**
* Incompatible conditions are removed when changing GitHub event types,
with clear feedback.
* Improved validation, check-suite compatibility, and workflow rerun
handling.
* **Documentation**
* Updated setup and automation guides with required permissions, webhook
subscriptions, and configuration steps.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Roelof Blom <roelof@rb2.nl>
Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary
- increase the GitHub Autofix default attempt cap from 10 to 30 per PR
per 24 hours
- compare the elevated-cap warning against the shared default rather
than a duplicated literal
- update the integration settings UI assertions for the new default

## Verification
- `npm run build -w @open-inspect/shared`
- `npm test -w @open-inspect/shared`
- `npm test -w @open-inspect/web --
src/components/settings/integrations/github-integration-settings.test.tsx`
- `npm run typecheck -w @open-inspect/web`

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/39af83bae1a46f4f8a82c6966edcdd4d)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Enhancements**
* Increased the default GitHub Autofix limit to 30 attempts per pull
request within 24 hours.
  * Updated settings warnings and validation to reflect the new limit.
* **Tests**
* Updated GitHub integration coverage for the increased Autofix attempt
limit.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary
- remove the `SLACK_TRIGGERS_ENABLED` runtime guard so eligible channel
messages are always processed
- remove the Slack bot environment type and Terraform variable/binding
for the retired flag
- simplify trigger tests and update automation, Slack integration, and
changelog documentation

## Verification
- `npm test -w @open-inspect/slack-bot` (432 tests passed)
- `npm run typecheck`
- `npx eslint packages/slack-bot/src/channel-trigger.ts
packages/slack-bot/src/types/index.ts
packages/slack-bot/src/channel-trigger.test.ts`
- `npx prettier --check packages/slack-bot/src/channel-trigger.ts
packages/slack-bot/src/types/index.ts
packages/slack-bot/src/channel-trigger.test.ts
docs/integrations/SLACK.md docs/AUTOMATIONS.md CHANGELOG.md`
- `git diff --check origin/main...HEAD`

## Note
- Terraform formatting could not be checked locally because the
Terraform CLI is not installed in the session environment; the Terraform
changes only delete the retired variable and binding.

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/f9b21c7dba04f8fe44611b69ae5a9969)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Updates**
- Slack channel message triggers no longer require a separate enablement
switch.
- Configured Slack channel messages can be processed directly by
automations.
- Updated production configuration and setup guidance to reflect the
streamlined activation process.
- Revised troubleshooting and security documentation for the current
configuration workflow.
- **Tests**
- Updated coverage to reflect trigger processing without the former
enablement control.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary

- replace the repeated array-wide completed-turn lookup with a forward
scan from the current user message
- preserve existing completed, in-flight, and partial-history grouping
behavior
- keep the change local to timeline derivation with no new state or
abstractions

## Performance

Local consecutive-run microbenchmarks:

| Events | Before | After | Change |
| ---: | ---: | ---: | ---: |
| 1,000 | 0.364 ms | 0.309 ms | -15.1% |
| 5,000 | 2.859 ms | 1.527 ms | -46.6% |
| 100,000 | not previously measured | 69.633 ms | baseline established |

Scaling from 1,000 to 5,000 events improved from 7.85x to 4.95x, closely
matching the 5x input increase.

## Verification

- `npm test -w @open-inspect/web` (1,254 tests)
- `npm run typecheck -w @open-inspect/web`
- `npm run lint -w @open-inspect/web`
- `NODE_ENV=production npm run build -w @open-inspect/web`

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/ba5b7c0d26d8e9234dd5cd821eb9ab18)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Improved internal timeline processing without changing visible
behavior or functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…oleMurray#1635)

## Summary

Phase D item deferred from ColeMurray#1609's review round. The sandbox row's three
URL+secret access artifacts — code-server, VNC, ttyd — each had their
own update / clear / clear-URL-only methods: 8 near-identical methods on
`SandboxRepository`, 8 matching entries on the `SandboxStorage` port,
and 8 mock implementations. This PR replaces each set with one
kind-parameterized trio:

- `updateSandboxAccess(kind, url, secret)` — encrypts the secret before
writing, as before
- `clearSandboxAccess(kind)` — clears URL + secret
- `clearSandboxAccessUrl?(kind)` — clears the URL, preserving the stored
secret

`SandboxAccessKind = "codeServer" | "vnc" | "ttyd"` lives next to
`SandboxRow` (whose column families it names) and matches the keys the
access endpoint already serves. The column names come from a
repo-private record keyed by that closed union — the same interpolation
pattern `updateSandboxForSpawn` already uses.

Tunnel URLs are deliberately **not** a fourth kind: they're a single
JSON column with no secret — a different shape, and forcing them into
the trio would trade real uniformity for fake uniformity.

## Behavior preservation

- The SQL emitted per (kind, operation) is byte-identical to the
per-kind methods it replaces; the repo suite still pins encrypt-at-rest
per kind and the exact `SET` clauses.
- `clearSandboxAccessState`'s sequence is unchanged in both branches
(code-server, VNC, tunnels, ttyd, broadcast), including the
provider-managed-stop distinction: URL-only clears for code-server/VNC
(passwords survive persistent resume), full clear for ttyd (its JWT is
minted per sandbox).
- One port narrowing worth calling out: the old port had two independent
optional URL-clears with a nested fallback, so a storage could implement
`clearSandboxCodeServerUrl` but not `clearSandboxVncUrl`. That partial
state is no longer expressible — a storage either provides
`clearSandboxAccessUrl` for all kinds or the manager falls back to full
clears for both. The only real storage (`SandboxRepository`) implements
everything, so composed behavior is identical; the fallback test now
exercises the remaining meaningful case.
- The trio incidentally gives ttyd a URL-only clear. Nothing calls it —
the manager keeps ttyd on the full clear for the reason above.

## Verification

- Typecheck: main + `tsconfig.test.json` + `test/integration` programs
all clean
- ESLint + Prettier on all touched files
- Unit battery 3304/3304, integration battery 1006/1006

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Unified sandbox access artifact management for code-server, VNC, and
terminal access.
* Standardized storing, clearing, and URL-only clearing of sandbox
access details.
  * Preserved encrypted secret handling and existing tunnel behavior.
* **Tests**
* Updated lifecycle and repository coverage for the unified access
management flow.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- simplify healthy provider-account rows by moving maintenance actions
into the overflow menu
- surface only the relevant primary recovery action for disabled or
reconnect-required accounts
- replace raw status labels with user-facing health labels and clarify
automation defaults
- add contextual explanations for accounts that cannot start new
sessions
- update account-settings tests for the revised action hierarchy and
recovery states

## Verification
- `npm test -w @open-inspect/web -- --run
"src/components/settings/provider-accounts-settings.test.tsx"`
- `npx prettier --check
"packages/web/src/components/settings/provider-accounts-settings.tsx"
"packages/web/src/components/settings/provider-accounts-settings.test.tsx"`
- `npx eslint
"packages/web/src/components/settings/provider-accounts-settings.tsx"
"packages/web/src/components/settings/provider-accounts-settings.test.tsx"`
- `npm run build -w @open-inspect/shared`
- `npm run typecheck -w @open-inspect/web`
- `git diff --check origin/main...HEAD`

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/3c43ceac7f30f69c8c4924a321088042)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Redesigned provider account rows with clearer status, default,
verification, and usage information.
  * Consolidated account management actions into a “More actions” menu.
* Added contextual Reconnect and Enable options for inactive or
reconnect-required accounts.
* Clarified when an account is the default for automation based on the
selected mode.
* **Bug Fixes**
  * Improved action availability based on account status.
* Added clearer messaging for disabled accounts and accounts requiring
reconnection.
<!-- 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
- virtualize session timeline rows with TanStack Virtual and dynamic
element measurement
- preserve history prepend anchoring, near-bottom append following,
terminal read observation, and expansion state across row unmounts
- build a renderable row model that excludes hidden events and coalesces
terminal output/completion
- stabilize tool group identity when older history is prepended
- add bounded-DOM and row-model regression coverage

## Performance validation
A temporary local route rendered 25,000 source events in a 1512x982
browser viewport:
- 22 virtual rows mounted
- 252 total DOM elements
- initial position opened at the latest turn
- scrolling to the first turn worked
- document height remained constrained to the viewport

The temporary validation route and benchmark tooling are not included in
this PR.

## Verification
- `npm test -w @open-inspect/web` (165 files, 1,278 tests)
- `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`

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/ba5b7c0d26d8e9234dd5cd821eb9ab18)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Improved session timeline performance by rendering only visible
events, including large histories.
* Added collapsible sections for task details, instructions, and
results.
  * Improved handling of loading, processing, and terminal states.

* **Bug Fixes**
  * Preserved tool-group identity when older events are added.
* Improved timeline scrolling and loading behavior when viewing earlier
history.
* Ensured older-history loading remains available after timeline
placeholders disappear.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary

Fixes **3 React Doctor root-cause task units** in the web package while
preserving existing provider preferences and prompt autocomplete
behavior.

## Fixed tasks

1. **`react-doctor/client-localstorage-no-version`** —
`packages/web/src/app/(app)/page.tsx:150`
- Problem: reconciled provider selections were serialized to an
unversioned local-storage key.
- Impact: future schema changes could read stale persisted data without
a clean version boundary.
- Fix: write to `open-inspect-last-provider-selections:v1` and migrate
valid data from the legacy key once.
   - Human severity: **Medium** correctness warning.

2. **`react-doctor/client-localstorage-no-version`** —
`packages/web/src/app/(app)/page.tsx:219`
- Problem: user-selected provider authentication was serialized to the
same unversioned key.
- Impact: newly saved preferences would remain coupled to the original
schema indefinitely.
- Fix: route this write through the versioned key covered by the same
migration path.
   - Human severity: **Medium** correctness warning.

3. **`react-doctor/role-supports-aria-props`** —
`packages/web/src/components/prompt-skill-autocomplete.tsx:180`
- Problem: the native textarea exposed `aria-expanded`, which is
unsupported by its implicit `textbox` role and ignored by assistive
technology.
- Impact: accessibility metadata claimed popup state on the wrong
semantic role.
- Fix: remove only the unsupported attribute while retaining list
autocomplete, controls, active-descendant, focus, and keyboard behavior.
   - Human severity: **Medium** accessibility warning.

## Task accounting

All three selected diagnostics had a null `fixGroupId`, so each
occurrence counts as one task unit. The two storage diagnostics share
one underlying key and were fixed atomically, but are counted separately
under the requested ungrouped-diagnostic rule. No non-null `fixGroupId`
was split.

## React Doctor results

React Doctor version: `0.9.12`, schema version: `3`, full web-package
scope.

| Metric | Before | After |
| --- | ---: | ---: |
| Total diagnostics | 89 | 86 |
| Errors | 2 | 2 |
| Warnings | 87 | 84 |
| Affected files | 45 | 44 |
| Score | 62 | 63 |
| `client-localstorage-no-version` | 2 | 0 |
| `role-supports-aria-props` | 1 | 0 |

Raw diagnostics cleared: **3**. The final changed-scope scan reports **0
findings** across the 4 changed files, and the full scan introduced no
new diagnostic.

## Validation

- `npm run typecheck` — passed.
- `npm run lint` — passed.
- `npx prettier --check src` — passed.
- `npx vitest run 'src/app/(app)/page.test.tsx'` — passed, 17 tests.
- `npx vitest run src/components/prompt-skill-autocomplete.test.tsx` —
passed, 8 tests.
- `npm test` — passed, 162 files and 1,214 tests.
- `NODE_ENV=production npm run build` — passed.
- `npx -y react-doctor@latest . --json --json-out
/tmp/react-doctor-after.json --yes --blocking none` — passed, 86
diagnostics.
- `npx -y react-doctor@latest . --verbose --scope changed --base
origin/main --yes --blocking none` — passed, no issues found.
- `git diff --check` — passed.

## Baseline failures

No pre-existing failure remains in final validation.

- The pre-edit `npx prettier --check .` included generated `.next/types`
output and flagged `.next/types/cache-life.d.ts`, `root-params.d.ts`,
`routes.d.ts`, and `validator.ts`. Source-only formatting passes.
- The pre-edit full test run had transient 5-second timeouts in
`client-auth-boundary-eslint.test.ts` and
`server-auth-boundary-eslint.test.ts` while baseline checks ran
concurrently. The final uncontended full suite passes all 1,214 tests.
- The pre-edit build inherited a nonstandard `NODE_ENV` and failed
prerendering `/_global-error` with `Cannot read properties of null
(reading 'useContext')`. The production-environment build passes.

## Deferred findings

- The two error-level `effect-needs-cleanup` findings were validated as
detector false positives: device authorization already clears
timers/aborts polling in its effect cleanup, and session transport
closes its WebSocket in the owning mount-effect cleanup. No suppression
was added.
- Performance rules requiring runtime measurement were deferred,
including lazy state initialization, stable default references,
handler-only state, combined iterations, map/set lookups, dynamic
imports, and SVG precision.
- Migration-scale component decomposition and state architecture
findings were deferred, including giant components, reducer migrations,
derived/adjusted state, effect chains, and SSR hydration patterns.
- Image, locale-formatting, list-key, iframe sandbox, and other findings
requiring UX, runtime, identity, or security judgment were deferred.
- The test-only JSON clone diagnostic is intentional because
serialization dropping `undefined` is the behavior under test.

## Visual verification

Browser visual verification was not run because the patch makes no
visual output change. The touched autocomplete was rendered in focused
jsdom tests; its listbox, keyboard selection, focus, and remaining ARIA
relationships pass.

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/c708ff3ceba29f95ab4ff214858e95d9)*

---------

Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary

- document notable merged changes since the August 22 changelog entry
- cover managed skill imports, long-running execution reliability, PR
Autofix, workflow automations, bot classifier configuration, richer PR
timeline events, and timeline virtualization
- date each section using the corresponding pull request's GitHub merge
date

## Validation

- `npx prettier --check CHANGELOG.md`
- `git diff --check`

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/68cdc24e75646e86ce8d9d86d12d69b1)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Improved performance for virtualized session timelines.
  * Added Autofix support for pull-request feedback.
  * Added GitHub Actions workflow automations.
  * Added configurable Slack and Linear bot classification.
  * Added richer pull-request timeline events.
  * Improved reliability for long-running tool calls.
* Added repository-managed skill imports with validation and provenance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…leMurray#1639)

## Summary

Two real defects in the prebuilt-images surfaces, found in the
2026-08-04 image-build post-merge review and re-verified against current
code:

**1. Feed errors were swallowed.** Both `ImagesSettings` and
`EnvironmentImageStatus` destructured only `data` from `useSWR`. The
global SWR fetcher throws on any non-OK response, so a transient feed
failure left `data` undefined and:

- the repo settings page rendered every prebuild toggle **unchecked**
with "No image" — inviting a user to "re-enable" repos that are already
enabled, which fires real state-changing PUT calls;
- the environment status chip rendered "No image", suggesting a
build-less environment and inviting an unneeded manual rebuild.

Now the repo page renders an error banner instead of a misleading list
when the feed fails with no cached data (SWR's default retry + focus
revalidation recover it automatically), and the environment chip shows
"Status unavailable", distinguishing a failed fetch from a genuinely
build-less environment. Stale-while-revalidate is preserved: a
background revalidation failure with cached data keeps showing the data.

**2. "Building…" never advanced.** Neither feed had a `refreshInterval`,
so a building row only updated on a manual refresh or an unrelated
mutate. Both feeds now use a shared conditional interval —
`imageBuildPollInterval` in `lib/image-builds.ts` — that polls every 30s
while any visible row is `building` and not at all once the feed is
all-terminal (terminal transitions only happen through user actions,
which already mutate the key). Same shape as the existing
conditional-poll pattern in `child-sessions-section.tsx`.

## Not included

The rest of the review's web-tier items (folding the per-environment
images route into the unified feed, settings-vs-picker status folding)
are separate concerns; several were already fixed by later PRs.

## Verification

- `imageBuildPollInterval` unit-tested (building → 30s, all-terminal →
0, unloaded → 0); the feed-failure path component-tested (banner shown,
no switches rendered). The polling wiring itself is a one-line
`refreshInterval` option per component — exercised indirectly, not
timer-tested, to keep the suite deterministic.
- Web typecheck, ESLint, Prettier clean; full web suite 1278/1278.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Image build status now refreshes automatically while builds are in
progress.
- Completed build feeds continue refreshing periodically to discover new
builds.
- Environment and session image statuses now use consistent, centralized
build information.
- Settings pages display a clear “Status unavailable” message when image
build information cannot be loaded.

- **Bug Fixes**
- Prevented repository controls from appearing when image build data
fails to load without cached results.
- Improved handling of unavailable environment image status information.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- Added focused control-plane unit tests for `persistMediaArtifact`.
- Covers successful runtime persistence, cleanup after runtime
rejection, 4xx error propagation, 5xx error redaction, raw non-JSON
error bodies, and cleanup failures.

## Why
Media uploads are a high-risk path because an artifact object is stored
before the session runtime records its metadata. These tests lock down
cleanup and client-facing error behavior so rejected metadata does not
leave orphaned objects or expose internal 5xx details.

## Testing
- `npm test -w @open-inspect/control-plane --
session-media-artifacts.test.ts`
- `npm test -w @open-inspect/control-plane`

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/7fa47b80e0db765015394e0cb4532777)*

---------

Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
…#1643)

## Summary

Behavior-preserving collapse of accumulated indirection in the
image-build subsystem, from the recorded post-merge review of the
standardization arc. Every finding was re-verified against current code
before implementation; one was found to be load-bearing and is
deliberately skipped (below).

**Dead middleman error class.** `ImageBuildCallbackAuthError` existed
only to carry a `(failure, cause)` pair from
`authorizeCompletionCallback` to `loggedCallbackAuthError`, which read
exactly those two fields and returned a *different* error; its message
was always discarded and its `provider?` log param never supplied. The
logger helper now takes the `"rejected" | "misconfigured"` discriminant
directly; the class is deleted (the `ImageBuildCallbackAuthFailure` type
stays). Log delta: the always-`undefined` `provider` field is gone from
`image_build.callback_auth_failed`; everything else is unchanged.

**Dead authorization state.** The store's `"fresh" | "accepted"`
discriminant was computed and never read anywhere (the republish
decision uses `build.status === "building"`), and
`ImageBuildCallbackBuild.providerSessionId` had zero readers.
`authorizeCompletionCallback` now returns `ImageBuildCallbackBuild |
null`; the still-authorizable-after-acceptance branch is kept and now
documented inline, and the integration test still pins that replay
behavior (asserting on the returned build instead of the deleted
discriminant).

**Workflow no longer owns a reaper it only forwarded.**
`workflow.cleanupImages` was a one-line delegate whose sole caller was
the scheduler. The scheduler now takes the reaper as a collaborator
(constructed in `runImageBuildScheduler`, injected in tests — same seam
shape the tests already used via the workflow mock); the workflow drops
the field, the method, and the import. The cleanup suite moved from
`workflow.test.ts` to a new `reaper.test.ts` with its own lean fixture,
assertions unchanged.

**Queue adapter.** The `Parameters<NonNullable<...>>[0]` wrapper is
gone; the binding is passed directly. One correction to the original
finding: the wrapper *was* load-bearing under current workers-types
(`Queue.send` returns `Promise<QueueSendResponse>`, not `void`), so
instead of re-wrapping, the port's `send` now returns `Promise<unknown>`
— callers only await delivery. The scheduler's republish path now builds
its job via `republishedImageBuildFinalizationJob` in
`finalization-job.ts`, so the `version: 1` shape has one owner.

**Repo-standards items.** The `ImageBuildPlannerLike = Pick<...>`
projection became an explicit `ImageBuildPlannerPort` interface (with
`ImageBuildPlanRequest` naming the planBuild input) that
`ImageBuildPlanner` implements. `BaseImageBuildPlan` (single-extension
leftover) is inlined into `ImageBuildPlan`. The reaper's two best-effort
helpers with zero external callers are now private.

**Small collapses.** `errorMessage` was byte-identical in five modules —
now one export in `errors.ts`. The finalizer's `markFailed → getBuild →
cleanupTerminalBuild → completed()` ritual (×3) is now `failAndCleanup`.
The unused-outside-its-own-test re-export of
`ImageBuildFinalizationAttemptError` from `finalizer.ts` is gone (the
test imports from `finalization-error.ts`). The `startAdapter` shadow of
the in-scope `adapter` is gone. The `orphan_sweep` scheduler-tick log
field — static narration about a sweep that no longer exists — is
deleted.

**Result-union simplification.** `ImageBuildWorkflowResult` had two
variants, each produced by exactly one method, forcing an
unreachable-default switch in routes.
`acceptBuildComplete`/`acceptBuildFailed` now return `void` and the two
callback routes return their (byte-identical) 202 bodies directly.

## Deliberately skipped

`ImageBuildRegistration.callbackTokenHash?`/`callbackTokenExpiresAt?`:
the one production caller always supplies both, but ~45 test call sites
intentionally register token-less builds to exercise the
unauthorizable-row paths — the optionality is load-bearing test surface,
not dead generality.

## Verification

Net −33 lines with a new 180-line test file (i.e. ~−200 in src).
Typecheck across all three programs (main, unit-test,
`test/integration`), ESLint + Prettier clean, unit battery 3322/3322,
integration battery 1006/1006. All HTTP responses, SQL, and log events
byte-identical except the two documented log-field deletions
(`orphan_sweep`, always-undefined `provider`).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Completion callbacks can be safely republished, including after a
prior acceptance.
  - Finalization jobs support reliable retry processing.
- Image build planning supports provider-neutral plans and clone
authorization states.

- **Bug Fixes**
- Improved cleanup of failed and superseded image artifacts, including
timeouts and provider deletion failures.
  - Standardized error handling across image build operations.
  - Callback responses remain consistent after completion or failure.

- **Tests**
- Expanded coverage for cleanup retries, concurrency limits, timeouts,
and idempotent behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- handle a failed sandbox WebSocket send instead of waiting for the
360-second push timeout
- remove the pending push resolver and clear its timeout before
rejecting the operation
- add regression coverage for the immediate failure, timer cleanup, and
resolver cleanup

The accepted legacy behavior for ambiguous identity-less terminal events
with multiple pending pushes is unchanged.

## Validation
- `npm test -w @open-inspect/control-plane` (3,323 tests)
- `npm run typecheck -w @open-inspect/control-plane`
- `npm run lint -w @open-inspect/control-plane`
- `git diff --check`

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/265065807e86f6de8115291d2eb340c2)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Push operations now fail immediately with a clear error when delivery
to the sandbox is unsuccessful.
- Prevented unnecessary timeout waiting after an undelivered push
command.
- Improved handling of late, mismatched, or ambiguous push completion
events to avoid incorrect status updates.
- Push failures now consistently return a failed result with an
actionable error message.
<!-- 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
- focus the new-session prompt automatically when the authenticated home
page loads
- add a regression test for the initial focus behavior

## Testing
- `npm test -w @open-inspect/web -- "src/app/(app)/page.test.tsx"`
- `npx eslint "src/app/(app)/page.tsx" "src/app/(app)/page.test.tsx"`

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/494241ec99e2c86f17d2bfe8898abfa2)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* The prompt input is now automatically focused when the home page
loads, allowing users to start typing immediately.

* **Tests**
* Added coverage to verify the prompt input receives focus on initial
load.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
## Summary

- replace the global session sidebar on `/settings*` with a dedicated
settings surface and explicit return-to-app navigation
- centralize settings labels, descriptions, keywords, availability, and
grouping into one searchable registry used by desktop and mobile
navigation
- preserve query-string deep links and provider-gated image settings
while improving mobile root/detail transitions, URL state, focus
management, and close/back actions
- keep the dedicated settings rail on desktop integration detail routes
and make Appearance controls wrap cleanly on mobile

## Reviewer Guide

### Desktop settings

- Confirm the settings rail replaces the session list rather than
appearing beside it.
- Check the Personal, Sessions, Workspace, and System groupings, active
state, independent rail scrolling, and centered `max-w-3xl` detail
content.
- Verify search matches category labels, descriptions, and keywords,
including multi-term queries.
- Confirm `Back to app` returns to the main application.

### Mobile settings

- Check the root header, close action, search, grouped category cards,
descriptions, chevrons, and touch-sized rows.
- Selecting a category should open its existing production panel without
changing panel behavior.
- `Back to settings` should restore `/settings`, return focus to the
Settings heading, and keep deep links such as `/settings?tab=appearance`
working.
- Verify Appearance controls stack below their descriptions rather than
compressing text.

### Integrations and behavior preservation

- Confirm desktop integration details retain the settings rail, search,
and active Integrations state.
- Verify integration back/close navigation on mobile and desktop.
- Existing panel APIs, mutations, provider gating, nested editors, and
authenticated route boundaries are unchanged.

## Validation

- `npm test -w @open-inspect/web` (167 files, 1,292 tests)
- `npm run typecheck -w @open-inspect/web`
- `npm run lint -w @open-inspect/web -- --no-fix`
- targeted Prettier check and `git diff --check`
- clean-checkout build: `npm run build -w @open-inspect/shared` then
`NODE_ENV=production npm run build -w @open-inspect/web`

## Visual Evidence

- Desktop settings root, 1512x982: artifact
`e390eb7c3abe5b31f2ad4afc6f3f9f05`
- Mobile settings root, 390x844: artifact
`b7f9bf1eb55dae612ec845f511e7410e`
- Final mobile Appearance detail, 390x844: artifact
`f89a016fd17789e87781bfa6c176bb2b`
- Desktop GitHub integration detail, 1512x982: artifact
`2c0c0ac07a443035f9d521b559ad9ba7`

The captures use the local mock control plane, so unavailable
integration data in individual panels is expected.

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/3c43ceac7f30f69c8c4924a321088042)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added analytics dashboards with date-range filtering, charts,
summaries, and sortable user data.
* Added automation management for browsing, creating, editing, viewing
runs, and controlling automations.
* Added session detail views with loading states, activity timelines,
terminals, and change previews.
* Introduced responsive settings navigation with grouped categories,
search, mobile back navigation, and improved layouts.
* Improved session creation with persistent selections, repository and
environment targets, skills, and file attachments.

* **Bug Fixes**
* Improved error handling, retry actions, missing-item states, and
navigation consistency across new pages.
<!-- 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
- remove the hard-coded 50-attempt ceiling and accept any positive safe
integer
- add an explicit `No limit` policy that persists as `null` and skips
rolling-limit admission checks
- keep a local text draft for the number input so users can clear,
backspace, and replace its value
- add UI, settings validation, command schema, and runtime admission
coverage

## Testing
- `npm test -w @open-inspect/web -- --run
src/components/settings/integrations/github-integration-settings.test.tsx`
- `npm test -w @open-inspect/control-plane -- --run
src/db/integration-settings.test.ts
src/session/message-repository.test.ts
src/session/http/handlers/autofix.handler.test.ts`
- `npm run typecheck`
- `npm run lint`

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/a31e45edb59c2483bdf4689ef6626136)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a “No limit” option for GitHub Autofix attempts per pull
request.
  * Attempt limits can now be set above 50, including values such as 75.
  * Unlimited settings are preserved when saved and applied.

* **Bug Fixes**
  * Autofix no longer checks attempt counts when no limit is configured.
* Improved validation for invalid, non-positive, fractional, or unsafe
limits.
* Repository-specific settings now preserve only explicitly configured
overrides.
<!-- 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 a reusable Slack app manifest for the existing bot behavior,
including Agent view, App Home, writable Messages tab, OAuth scopes,
event subscriptions, and interactivity URL placeholders
- expose Slack worker, events, and interactions URLs as Terraform
outputs
- update onboarding and setup documentation to use the manifest and
generated endpoint outputs

## Validation

- `npm test -w @open-inspect/slack-bot` (432 tests)
- `npm run typecheck -w @open-inspect/slack-bot`
- Prettier and `git diff --check`

## Notes

- No Slack bot runtime source or test files differ from `main`.
- `chat:write.public`, `message.mpim`, and MPIM scopes are intentionally
excluded.
- Terraform/OpenTofu validation was not run locally because neither
binary is installed; the PR includes a Terraform assertion for the new
URL outputs.

---------

Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
## Summary

- expose every available settings category as a direct destination in
the global command menu
- search settings by label, description, group, and keywords while
preserving existing fuzzy session and command search
- reuse the canonical settings registry for destination metadata and
provider-gated availability
- reset settings filtering whenever the controlled dialog closes

## Reviewer Guide

- Open the command menu and confirm the Settings group lists direct
destinations with descriptions and category labels.
- Search terms such as `theme`, `github`, `pull request`, or `request
source` should surface the expected setting without unrelated fuzzy
settings matches.
- Selecting a destination should close the command menu and navigate to
`/settings?tab=<category>`.
- Confirm the existing root Settings link, New session, Home,
Automations, and session search remain available.
- Repository Images should remain absent when the active sandbox
provider does not support image builds.

## Validation

- `npm test -w @open-inspect/web` (169 files, 1,301 tests)
- `npm run typecheck -w @open-inspect/web`
- `npm run lint -w @open-inspect/web -- --no-fix`
- Prettier and `git diff --check`
- clean-checkout build: `npm run build -w @open-inspect/shared` then
`NODE_ENV=production npm run build -w @open-inspect/web`

## Visual Evidence

- Precise `theme` keyword result, viewport 1512x982: artifact
`9194d504919262da53cdb3267109787b`

The capture uses the local mock control plane; the unrelated
repository-loading warning in the background is expected.

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/3c43ceac7f30f69c8c4924a321088042)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Added a Settings section to the global command menu.
  * Search settings by name, description, and keywords.
  * Select a result to navigate directly to the relevant settings page.
* Unavailable options, such as repository image settings, are hidden
automatically.
  * Updated the command menu search prompt for clearer guidance.

* **Bug Fixes**
  * Search filters now clear when the command menu closes.
<!-- 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
- translate expected sandbox-access `409 Conflict` responses to `204 No
Content` at the browser-facing BFF
- preserve unexpected control-plane errors such as missing sessions
- update the sandbox-access hook contract and add route coverage

## Why
Sandbox access is unavailable during normal lifecycle transitions such
as startup and snapshotting. The client handled the control plane's
`409` correctly, but browsers still displayed every response as a
console error. Returning `204` from the browser-facing route preserves
the no-access state without creating misleading console noise.

## Verification
- `npm test -w @open-inspect/web -- --run
'src/hooks/use-sandbox-access.test.tsx'
'src/app/api/sessions/[id]/sandbox-access/route.test.ts'`
- `npm run typecheck -w @open-inspect/web`
- focused ESLint checks for changed files
- `git diff --check`

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/f8d2001318da2e00477b7981d3f32db0)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved sandbox access handling when access is unavailable, returning
an empty response without displaying an error.
  * Requests now wait until the sandbox is ready before checking access.
* Updated the application to recognize unavailable and not-found
responses as having no sandbox access.
* Preserved expected error responses and response headers for unexpected
conditions.

* **Tests**
* Added coverage for sandbox readiness, unavailable access responses,
and preserved 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
- add a sandbox-authenticated `POST /sessions/:id/sandbox-error`
control-plane route
- mark fatally failed sandboxes as failed and persist/broadcast the
failure reason
- update the sandbox supervisor to report against the session-scoped
endpoint with encoded session IDs
- skip the session callback for image-build sandboxes that do not have a
session ID

## Testing
- `npm test -w @open-inspect/control-plane --
src/session/http/routes.test.ts
src/session/http/handlers/sandbox.handler.test.ts
src/routes/session-runtime-proxy.test.ts`
- `npm run typecheck -w @open-inspect/control-plane`
- `PYTHONPATH=src uv run --extra dev pytest
tests/test_supervisor_monitor.py tests/test_runtime_config.py -q`
- `uv run --extra dev ruff check src/sandbox_runtime/supervisor.py
src/sandbox_runtime/runtime_config.py tests/test_supervisor_monitor.py
tests/test_runtime_config.py`
- Prettier checks for changed TypeScript files

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/e59e7948d2e827ccf2ce73dd38c0e2d3)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Added authenticated sandbox fatal-error reporting to sessions.
* Fatal errors now terminate failed sandboxes, preserve their reason,
and resume processing safely.
* Added retry handling with backoff and bounded error messages for
transient failures.
  * Added validation for control-plane URLs and session configuration.

* **Bug Fixes**
* Improved handling of missing, invalid, or mismatched sandbox
credentials.
  * Empty, oversized, or malformed reports are rejected appropriately.
  * Reports for stopped or stale sandboxes are safely ignored.
  * Error reporting remains disabled when no session is configured.

* **Tests**
* Added coverage for authorization, retries, lifecycle handling, and
invalid reports.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Cole Murray <2492022+ColeMurray@users.noreply.github.com>
Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
…oleMurray#1649)

Tier 4 (modal-infra hygiene) of the image-build cleanup review,
re-verified against current main before touching anything. Two of the
six recorded findings turned out to be already fixed by intervening
work. Net −373 lines of dead surface. The deep review surfaced a real
capability the first revision missed (see item 1); adopting it is
deliberately deferred to ColeMurray#1658 rather than folded in here.

## Already fixed on main — no changes

- **Request-logging scaffold ×7 in web_api.py**: consolidated since the
review into `_execute_endpoint` / `_EndpointExecution`; all eight
endpoints use it and `_log_build_http_request` is gone.
- **`create_sandbox` / `restore_from_snapshot` ~130-line duplication**:
already extracted into `_launch_sandbox` + `_SandboxLaunchSpec` with
typed image-source variants. The hot-path item this review deferred to
last no longer exists.

## Implemented

### 1. The no-op image-delete round trip is removed; real deletion is
deferred to ColeMurray#1658

`api_delete_provider_image` never deleted anything — it logged
`image.delete_requested` and answered `"deleted": true` — but cost a
deployed function plus an authenticated HTTP request per
reaper/finalizer cleanup. `ModalSandboxProvider.deleteProviderImage` is
now a local no-op; the `ModalImageBuildProvider` interface and adapter
delegation are unchanged, so E2B/Vercel/OpenComputer deletions are
untouched, and the reaper/finalizer callers keep logging each attempt
and outcome. Deleted: the Modal endpoint + request model,
`ModalClient.deleteProviderImage` with its types/schema/URL wiring, and
the associated tests; the README row is gone. Verified first: nothing
consumes the `image.delete_requested` log line, and terraform has no
reference to the endpoint URL.

The deep review correctly flagged that the "Modal doesn't have an
explicit delete API" comment this cleanup relied on is stale: the locked
SDK (modal 1.4.3) exposes `modal.experimental.image_delete` (verified in
the locked venv; `@synchronizer.create_blocking` provides the `.aio`
form), and the reaper treats a fulfilled delete as proof it can clear
the row carrying `provider_image_id` — so with fake success, reaped
Modal images are retained provider-side untracked. Two things to note
about that:

- **This PR does not change retention behavior in either direction** —
the previous endpoint body was equally fake success, so the leak
predates it.
- **Adopting the real deletion is deliberately deferred to ColeMurray#1658**:
`image_delete` is an experimental interface (its own docstring warns the
stable form may differ), and we want to validate it before wiring the
cleanup path to it. Commit `40ba25674` (reverted in this PR) preserves a
complete, tested reference implementation — real endpoint with
idempotent `NotFoundError` handling and error propagation, restored
client/provider round trip, and coverage — to restore once validation
passes. The no-op's comment documents the deferral.

The auth-before-validation test that used the delete endpoint as its
vehicle now runs against `api_terminate_build_sandbox` (renamed
accordingly; the invariant it guards is endpoint-independent). The
400-log-fields assertion it duplicated is already covered on another
endpoint.

### 2. Dead module surface in the sandbox package

All verified zero-caller (both this repo and downstream): the
`get_manager`/`get_sandbox_config`/`get_sandbox_handle` lazy accessors,
the module-global `sandbox_manager` instance,
`SandboxHandle.get_logs`/`terminate`, and the fabricated `snap-…` id in
`take_snapshot` that was logged once and discarded (the
`sandbox.snapshot` log keeps `sandbox_id`/`image_id`, which are the
queryable identifiers). `SandboxHandle.snapshot_id` — the
restore-provenance field asserted by the launch-spec tests — stays.

### 3. Dead pre-bridge event protocol in sandbox-runtime

`SandboxEvent` and its seven pydantic subclasses (`HeartbeatEvent` …
`ArtifactEvent`) are relics of a protocol the bridge replaced with plain
dicts; zero constructors or importers outside the two `__init__.py`
re-exports. `GitSyncStatus` went with them — its only reader was
`GitSyncEvent` (the control plane's `GitSyncStatus` is a separate TS
type in `@open-inspect/shared`).
`GitUser`/`SessionConfig`/`McpServerConfig`/`SandboxStatus` are live and
stay.

### 4. Residue

- `websockets>=13.0` dropped from modal-infra's dependencies: nothing in
modal-infra imports it. The sandbox image's pip list in `images/base.py`
and sandbox-runtime's own dependency (the real importer) are unaffected.
Lockfile regenerated.
- `api_snapshot_sandbox` no longer reads or echoes
`session_id`/`reason`: the control plane's response schema reads only
`image_id`, and endpoint logging uses the correlation headers, so the
body fields were dead in both directions. The client stops sending them;
`SnapshotSandboxRequest.reason` is gone (provider-level
`SnapshotConfig.reason` stays — OpenComputer embeds it in checkpoint
names). The generic-snapshot identity test still passes unchanged, since
a crafted `reason` now can't influence anything by construction.
- README: `src/sandbox/` listing described files that moved to
`packages/sandbox-runtime` long ago and claimed a "warm" operation the
manager doesn't have; now matches the real module layout.

## Deliberately kept

The `modal>=1.4.3` floor and its `with_options()` comment: the review
flagged the justification as stale, but the floor has since been
re-justified — `Function.with_options()` per-call timeout override
genuinely requires it.

## Verification

- modal-infra pytest 217/217, sandbox-runtime pytest 775/775, ruff clean
- control-plane unit 3319/3319 and integration 1006/1006, workspace
typecheck + both test tsconfig programs clean
- Deleted-symbol grep across the full tree returns only the intended
survivors (interface member + no-op implementation)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Changes**
- Simplified sandbox snapshot requests and responses by removing session
and reason details.
  - Removed provider-image deletion support from the sandbox APIs.
- Reduced public sandbox runtime exports to supported configuration and
status types.
- Removed deprecated sandbox event models and unsupported sandbox handle
operations.

- **Documentation**
- Updated sandbox component documentation to reflect the current runtime
structure, lifecycle terminology, and removal of the image-deletion
endpoint.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary

- preserve mobile settings search and scroll context while opening
detail panels, and restore focus to the originating category after
browser or in-app Back navigation
- use shared input/button focus treatments, expose active mobile
navigation semantics, announce empty search results, and increase mobile
header actions to 44px touch targets
- avoid rendering the desktop settings structure during mobile hydration
by sharing the resolved settings viewport through the route shell
- correct integration detail heading hierarchy and make Slack routing
rules stack cleanly on narrow screens
- add regression coverage for focus restoration, retained search state,
focus treatments, heading levels, and responsive routing controls

## Validation

- `npm run build -w @open-inspect/shared`
- `npm run typecheck -w @open-inspect/web`
- `npm run lint -w @open-inspect/web -- --no-fix`
- `npm test -w @open-inspect/web -- --maxWorkers=4` (170 files, 1,308
tests)
- `NODE_ENV=production npm run build -w @open-inspect/web`
- `git diff --check`

## Manual Verification

Tested against a local mock control plane with an authenticated user:

- 390x844 mobile settings search → Appearance detail → browser Back
- confirmed the search query remains populated, focus returns to
Appearance, and `aria-current` remains accurate
- confirmed the Appearance controls stack without compression
- confirmed the Slack integration at 320x844 has no horizontal overflow
and follows `h1` → `h2` → `h3` heading order

Visual recording artifact: `7fe72d4f66822633d4cbc0aea64646bb`

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/813b354124416569fd45908400e81c6c)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Accessibility**
* Improved heading structure across integration settings for clearer
screen-reader navigation.
  * Enhanced focus restoration when navigating mobile settings.
* Improved accessibility semantics for active navigation items and
mobile actions.

* **Mobile Experience**
* Improved navigation between settings category lists and detail views.
  * Preserved mobile search state during browser-history navigation.
  * Updated mobile controls with larger, more consistent buttons.

* **Layout**
  * Improved Slack routing-rule layout on smaller screens.

* **Reliability**
  * Added a smoother loading state while settings finish initializing.
<!-- 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.
@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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

@github-actions

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate
Tests

Pushed by: @NicolasWalter, Action: pull_request

@github-actions

Copy link
Copy Markdown

Terraform Plan Results

Status: ✅ Success

Show Plan
terraform_data.sign_in_provider_gate: Refreshing state... [id=7f4a67d1-6978-0b23-899d-c2a9004643bd]
terraform_data.access_control_gate: Refreshing state... [id=841ab6bc-98a7-018c-1031-ebad4f8b62bc]
terraform_data.cloudflare_custom_domain_gate: Refreshing state... [id=fa456fac-6c14-16e4-a484-3338d1a3718d]
local_file.web_app_wrangler_production[0]: Refreshing state... [id=71b0d3758fc7d34e75fd9a3abe59e2c1b6dadeea]
data.external.modal_source_hash[0]: Reading...
null_resource.slack_bot_build[0]: Refreshing state... [id=5379137651876318417]
null_resource.web_app_cloudflare_build[0]: Refreshing state... [id=5024517857365059396]
random_password.service_auth_secret_slack_bot: Refreshing state... [id=none]
random_password.service_auth_secret_web: Refreshing state... [id=none]
random_bytes.provider_accounts_encryption_key: Refreshing state...
random_password.service_auth_secret_github_bot: Refreshing state... [id=none]
random_password.image_callback_token_pepper: Refreshing state... [id=none]
random_password.service_auth_secret_linear_bot: Refreshing state... [id=none]
module.modal_app[0].null_resource.modal_secrets[0]: Refreshing state... [id=8757687985279342629]
null_resource.linear_bot_build[0]: Refreshing state... [id=956366907826137814]
null_resource.control_plane_build: Refreshing state... [id=9105849033611783886]
null_resource.github_bot_build[0]: Refreshing state... [id=8271231830927734747]
module.github_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=87dbfaa1d9ce4a37a42e04c57c434a72]
module.linear_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=d003f1ad81384910a1f48a0a33f18c09]
cloudflare_r2_bucket.media: Refreshing state... [id=open-inspect-media-primo]
module.session_index_kv.cloudflare_workers_kv_namespace.this: Refreshing state... [id=7f18644fbed34121bbe3a196f373ea93]
cloudflare_queue.image_build_finalization_dlq: Refreshing state... [id=cbbc2d8794c04396a550996e7f0cc129]
module.slack_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=729b357dbb5e4c9d99ec9212cc45766e]
cloudflare_d1_database.main: Refreshing state... [id=dba95b03-ace9-47a8-81e9-6e39d8d694c5]
cloudflare_queue.image_build_finalization: Refreshing state... [id=1ca823a150c54578a9ad1814325147a3]
cloudflare_queue.slack_completion_delivery_dlq[0]: Refreshing state... [id=06ce03d2663f4aea937b0c0c1c379c17]
data.external.modal_source_hash[0]: Read complete after 0s [id=-]
cloudflare_queue.slack_completion_delivery[0]: Refreshing state... [id=56ef0f3e13bd46a3a39f30c79ec547fa]
module.modal_app[0].null_resource.modal_deploy: Refreshing state... [id=5768192879312552892]
module.slack_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=5200e96d69804ea296e1f3a6b39e4243]
module.linear_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=33782d80e8ff4af9b30b92870084b674]
null_resource.d1_migrations: Refreshing state... [id=5980374278680462129]
module.slack_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=e87b85ce-d5a2-4b3a-8fbe-23e6200e98fe]
module.linear_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=a55bcc0f-a65c-41a5-8441-05d7b4af3966]
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]
null_resource.web_app_cloudflare_deploy[0]: Refreshing state... [id=1320732786606345683]
cloudflare_queue_consumer.image_build_finalization: Refreshing state...
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" -> "2634877bac226aff76346d1db31648d3a4d14db4827981a6e00663b4502b07af"
            # (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   = "ac07332f8b0f4cdfa4ca04f966a9fa61" -> null
                  - name = "open-inspect-web-primo" -> null
                },
              - {
                  - id   = "4b5e2696491a41eaaa124f4e2a9855f2" -> null
                  - name = "open-inspect-github-bot-primo" -> null
                },
              - {
                  - id   = "33782d80e8ff4af9b30b92870084b674" -> null
                  - name = "open-inspect-linear-bot-primo" -> null
                },
              - {
                  - id   = "5200e96d69804ea296e1f3a6b39e4243" -> null
                  - name = "open-inspect-slack-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 = "42f6fdc57fc78ae643353dd936b077065e118908a52db442092b12d1d883fa20"
              + 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 = "8325a54fe2d5e52fd066738d73b13a9199519cec33ab811f6f851c42b7083c8e"
              + 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 = "ccd7ff4c5a000ff0db9b7949a8fd2049e1fd50c7bc6e186ec7f89b0fdcefbdbd"
              + 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" -> "2f6727937c35409a1eedcbda6a69fbcd87df04e4ca0b489a9a2734b18c897b17"
            # (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 = "89b02eabc86f7e83251be01ce69cb4e9189bef0fe2be2faadecb555204562e40"
              + 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:
  + 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants