Skip to content

chore: sync fork with upstream (ColeMurray/background-agents) - #23

Merged
jasoncuriano merged 12 commits into
mainfrom
sync-upstream-d017795a
Sep 11, 2026
Merged

jasoncuriano merged 12 commits into
mainfrom
sync-upstream-d017795a

Conversation

@jasoncuriano

Copy link
Copy Markdown
Collaborator

Routine upstream sync per upstream-sync-runbook.md. Brings the fork up to d017795a.

  • 11 upstream commits, 267 files changed
  • Merged tree is byte-for-byte identical to upstream/main — no fork-local changes
  • package-lock.json untouched (zero version/resolved/integrity changes)

Verified locally before push:

Check Result
npm run build (all packages) pass
npm run typecheck pass
npm test -w @open-inspect/control-plane 4218 passed / 285 files
Worker dist/index.js × 5 present

Notes:

  • One new @open-inspect/shared subpath export (./harnesses) — clean shared rebuild required, per the runbook.
  • Two new D1 migrations land here (0075_session_harness, 0076_anthropic_provider_accounts); Terraform applies them on deploy.

🤖 Generated with Claude Code

ColeMurray and others added 12 commits September 9, 2026 15:05
## Summary

- add `anthropic/claude-fable-5-1` to the shared model catalog with low
through max reasoning and a high default
- expose Fable 5.1 through model selectors and Linear's
`model:fable-5-1` override
- update model documentation, changelog, and the frozen OpenCode
reasoning catalog metadata and checksums

## Validation

- `npm run build -w @open-inspect/shared`
- `npm test -w @open-inspect/shared -- --run` (867 tests)
- `npm test -w @open-inspect/linear-bot -- --run` (235 tests)
- `npm test -w @open-inspect/web -- --run
src/hooks/use-enabled-models.test.tsx
src/components/model-reasoning-selector.test.tsx
src/components/settings/models-settings.test.tsx` (23 tests)
- `npm test -w @open-inspect/slack-bot -- --run
src/app-home/models.test.ts` (2 tests)
- `npm run typecheck`
- `npm run lint`
- `uv run --extra dev ruff check
tests/test_opencode_reasoning_contract.py`
- `uv run --extra dev ruff format --check
tests/test_opencode_reasoning_contract.py`
- `uv run --extra dev pytest tests/test_opencode_reasoning_contract.py
tests/test_reasoning_config.py -v` (1 passed, 3 opt-in wire tests
skipped)

## Verification note

The opt-in wire suite was also attempted with OpenCode 1.18.29. The
binary reported the expected version, but its isolated health endpoint
timed out before test execution in this environment, so live wire
serialization remains unverified here. The non-wire reasoning
configuration test passes, and the fixture metadata matches the current
public OpenCode catalog.

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

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

- **New Features**
- Added Claude Fable 5.1 (`anthropic/claude-fable-5-1`) to the available
model catalog and model picker.
- Added adaptive reasoning controls with effort levels from low to max,
defaulting to high.
- Added support for selecting Claude Fable 5.1 using the
`model:fable-5-1` override label.

- **Bug Fixes**
- Improved model label handling so versioned and provider-qualified
Claude model labels resolve correctly.

- **Documentation**
- Updated model support tables and available-model documentation to
include Claude Fable 5.1.
  - Updated integration guidance with the new model override label.
<!-- 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>
…slot skill test (ColeMurray#1848)

## Problem

`managed-skills.test.ts > validates more assigned environments than the
engine has parameter slots` intermittently fails with `Test timed out in
5000ms`.

## Root cause

The test seeded 101 environments with one `EnvironmentStore.create()`
call per environment. Each `create()` is its own `db.batch()`, so the
test issued ~115 sequential D1 round-trips before its two
`SkillStore.create()` assertions even started.

In isolation that is cheap. Under the full parallel integration run —
one workerd instance per test file across all cores — every round-trip
waits on a starved pool, and the accumulated latency crosses vitest's 5s
default `testTimeout`.

The `validateAssignments` chunking from ColeMurray#1556 is not implicated. This
test was simply the only one in the suite doing a three-digit sequential
seed.

Measured on a 16-core machine, with the full integration suite running
concurrently for load:

| Seeding method | 101 environments |
| --- | --- |
| Sequential `create()` loop | 28–32 ms |
| Single `env.DB.batch()` | 5 ms |

## Fix

Seed the rows through one `env.DB.batch()` of `bindEnvironmentInsert`
statements, matching the pattern the file's existing `seedGlobalCatalog`
helper already uses. `bindEnvironmentInsert` is the same statement
builder `EnvironmentStore.create()` composes, so the rows are identical;
the loop only added transaction boundaries.

The test still asserts the same behavior: 101 assignments persist, and a
create with one missing environment among many is still rejected with
`environments do not exist`.

## Verification

- The target test passes at 22ms inside a full 103-file / 1228-test
integration run, down from a 5000ms timeout.
- `tsc --noEmit -p tsconfig.test.json`, eslint, and prettier all clean.

## Note for reviewers

The integration config leaves vitest's 5s default `testTimeout` in place
while running one workerd instance per file across all cores, so any
test's first case can stall past it on a loaded machine. A separate run
here hit exactly that in `events-messages-list.test.ts` (passes in
isolation, all cases under 40ms, no seeding loop). If these load-only
timeouts recur across unrelated files, raising `testTimeout` globally is
the follow-up — deliberately out of scope here, since it is not what
caused this failure.

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

- **Tests**
- Streamlined test setup for scenarios involving a large number of
assigned environments.
- Preserved the existing test data, validation behavior, and assertions
while improving setup efficiency.
  - No changes to exported or public functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary

Moves OpenCode behind an `AgentHarness` seam in the sandbox runtime so a
second agent can be added without touching the bridge's session, event
and prompt-queue logic. No behaviour change for OpenCode sessions.

- `harness/base.py` defines the two halves of the seam: `AgentHarness`
(bridge half: open, close, create-or-resume a session, run a prompt,
abort) and `HarnessProcessOwner` (supervisor half: start and stop the
vendor process).
- `harness/opencode.py` implements both for OpenCode.
`opencode_client.py` and `prompt_stream.py` move under `harness/` as
`opencode_client.py` and `opencode_stream.py`; `test_prompt_stream.py`
keeps its name.
- `bridge.py` drops its OpenCode-specific code (about 260 lines) and
drives the harness through the Protocol. `entrypoint.py` and
`supervisor.py` pick the process owner from the session config's
`harness` field, which only accepts `opencode` today.
- The second commit declares `session_id` on the Protocol, renames a
local that shadowed the turn outcome, and widens `OpenCodeServer.start`
to `Sequence`, which keeps `mypy src/` at the same five pre-existing
errors `main` has.
- The third commit is the review round: harness startup sits inside the
same try/finally as the run loop and a failing `close()` can no longer
replace a `HarnessStartError`; the harness owns `session_id` through
`resume_session` and `create_session` (startup only resumes, the first
prompt creates, as before the seam); `TurnOutcome` rejects contradictory
states; only deployable harness ids are listed, so
`parse_harness_id("claude")` is rejected until that harness lands; the
`ready` and `snapshot_ready` events emit exactly what the shared schema
on `main` declares; registry imports are at module scope.
`test_bridge_harness_lifecycle.py` covers the start-error,
cleanup-failure, invalid-resume and contract cases.

No runtime manifest bump: the image ships the same Python package.

Part 1 of the Claude Agent harness stack. Next: the harness
discriminator on sessions and automations.

## Testing

- `packages/sandbox-runtime`: `uv run python -m pytest` (950 passed, 3
skipped), `ruff check`, `ruff format --check`, `mypy src/` unchanged
from `main`, `node --test tests/*.test.mjs` (21 passed).


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

* **New Features**
* Added configurable agent selection for sessions, defaulting to
OpenCode.
* Added agent-aware session creation, resumption, prompting, stopping,
and status reporting.
  * Added clearer reporting for unrecoverable startup failures.

* **Bug Fixes**
  * Improved restoration when saved sessions are unavailable.
* Prevented cleanup errors from obscuring the original startup failure.
* Improved handling of agent-process crashes and deterministic failures.
* Ensured failed startup cleanup completes without masking the
underlying error.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…rray#1851)

## Summary

Adds `harness` to sessions and automations end to end, so a session
records which agent it runs on. Stacked on ColeMurray#1850.

- `@open-inspect/shared/harnesses`: the harness catalog, display labels,
and the one compatibility rule applied wherever a model or provider-auth
selection enters a session. The catalog lists exactly the harnesses the
runtime on the branch can boot, so here it is `opencode` alone; ColeMurray#1853
adds `claude` together with the runtime that boots it. A provider a
harness has no auth row for resolves to the API key rather than binding
an installation default the harness cannot use. The rule's auth half
runs wherever modes are known: session create, automation create and
update (explicit selections, or the stored pins when harness or model
changes), and child spawn (the inherited modes against the child's
model).
- D1 migration `0075` adds a nullable `sessions.harness` column;
automations gain the same column. Both read as `opencode` when null.
- The session Durable Object migration renames `opencode_session_id` to
`agent_session_id` with a dual read of the legacy key, so existing
sessions resume unchanged.
- Sandbox spawn passes `harness` to the runtime and `web_api.py`
forwards it into the sandbox environment. Child sessions inherit the
parent's harness; a child cannot switch.
- The `ready` bridge event reports which harness booted, and the session
Durable Object logs a `sandbox.harness_mismatch` warning when it differs
from the session's harness (image or config drift, never reconciled
silently).
- A harness that never suggests a title would leave the session
untitled, so once the first turn settles the session offers the prompt's
first line; the only-if-unset write decides whether it lands, so a
vendor suggestion that arrived mid-turn wins.

**Inbox join-order pin.** Adding a column to `sessions` flipped SQLite's
join order on the inbox list query so it read every session row before
filtering. The `CROSS JOIN` in `session-inbox-store.ts` pins the order
the query was written for. A regression test that measures the read
bound belongs to inbox work not yet on `main` and will arrive with it.

The fixture commit adds `harness: "opencode"` to the unit and
integration fixtures that build sessions and automations; reviewers can
skip it.

## Testing

- `npm run typecheck` (every package, including `test/integration`),
`npm run lint`, `npm run lint:sql-portability`.
- control-plane: unit 4121 passed; integration 1227 passed, 1 skipped.
- shared 877 passed; web 1581 passed; modal-infra 238 passed.


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

- **New Features**
- Added harness support for sessions and automations, including child
sessions, scheduled runs, sandbox execution, and restores.
- Added compatibility checks for selected models and provider
authentication, with clear errors for unsupported combinations.
- Sessions now receive an automatic fallback title when none is
provided.
  - Session state now reports the agent session identifier and harness.

- **Bug Fixes**
  - Improved session inbox query performance.
- Added handling for snapshot-ready events and harness mismatch
warnings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…oleMurray#1852)

## Summary

The in-process tool server the Claude Agent harness will use, plus the
SDK it depends on. Nothing wires the module yet; the next PR does.
Stacked on ColeMurray#1851.

- `harness/claude_tools.py` implements the seven Open Inspect tools as
one in-process MCP server named `oi`: `spawn-child`,
`send-child-prompt`, `cancel-child`, `get-child-status`,
`create-pull-request`, `slack-notify`, `upload-media`. They are the same
tools OpenCode gets as `.opencode/tool/*.js` plugins, over the same
control-plane side channels, and they run inside the bridge process so
no credential has to reach the child for them to work. Tools are gated
the way the OpenCode plugins are (Slack only when the session has a
Slack thread).
- `claude-agent-sdk==0.2.152` pinned exactly in
`sandbox-runtime/pyproject.toml`: the wheel bundles the `claude` CLI
(2.1.259) whose message shapes the harness translates. The three lock
files (`sandbox-runtime/uv.lock`, `modal-infra/uv.lock` through its path
dependency, `sandbox-images/locks/runtime.txt`) resolve the pin; that is
where the line count comes from.

No runtime manifest bump: the dependency is inert until the harness
lands.

**Contract review.** Seven places where the port had drifted from the
JavaScript tools and the control-plane routes were corrected: the
child-detail formatter reads the `session` envelope; trajectory paging
uses `cursor`; a single-repository session reads its branch from the
checkout, not the bridge's cwd; absent pull-request optionals are
omitted rather than sent as `null`; pull-request creation outlasts the
server's push budget; `viewport`/`dimensions` are `{width, height}`
objects serialized to the JSON strings the media endpoint parses; video
uploads require `caption`. `test_claude_tools.py` now pins those wire
shapes.

## Testing

- sandbox-runtime: pytest 965 passed, 3 skipped (`test_claude_tools.py`
exercises `build_tool_server` and the wire contracts), ruff, mypy
unchanged, `uv lock --check`.
- modal-infra: pytest 238 passed, `uv lock --check`.
- sandbox-images: `cli.py lock --check`, pytest 56 passed.


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

* **New Features**
* Added Claude harness tools for managing child sessions, including
spawning and checking status.
* Added pull request creation with repository and current-branch
detection.
  * Added Slack notifications with actionable status and error feedback.
* Added media upload support with file-type validation and structured
image/video metadata.
* Tools are automatically limited based on repository availability and
Slack configuration.

* **Improvements**
* Child session details now include sandbox information, recent events,
and trajectory continuation.
* Pull request requests now enforce a dedicated timeout and omit
unavailable optional fields.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary

The Claude Agent SDK harness behind the seam from ColeMurray#1850, on the tool
server from ColeMurray#1852.

- `harness/claude.py`: the `AgentHarness` for the SDK client. Creates or
resumes a session by UUID, streams SDK messages into bridge events
(text, tool calls, results, cost), maps reasoning effort onto the SDK's
thinking options, and aborts a running turn. The `oi` tool server from
ColeMurray#1852 is registered as an MCP server and allow-listed as `mcp__oi__*`.
- `claude_stager.py`: the `HarnessProcessOwner` for Claude. There is no
resident process (the SDK spawns the child inside the bridge), so
`start()` only prepares what the child will see: a per-sandbox
`CLAUDE_CONFIG_DIR` outside every repository where no credential is ever
written, bundled and managed skills, the standalone bin scripts, and a
small JSON handoff.
- `harness/claude_env.py` and `sandbox_bin.py`: the child runs with the
sandbox environment OpenCode sees, minus the Anthropic credential family
of the other auth mode, so it holds exactly one credential and never
both. The sentinel test in `test_claude_env.py` is the one to read.
- `credentials/provider_credential_client.py`: fetches a connected
subscription's stored provider secret (the `stored_provider_secret`
credential kind, as opposed to a brokered access token) from the control
plane's sandbox-only runtime-credential endpoint on every bridge start
(the route lands in ColeMurray#1854), so restarts and snapshot restores each
produce an issuance record; the token lives in process memory only.
Without a connected account the harness uses `ANTHROPIC_API_KEY`.
- `@open-inspect/shared/harnesses`: `claude` enters the catalog here,
with the runtime that boots it: Anthropic models only, API key or a
connected Anthropic account (that account mode gets its control-plane
route in ColeMurray#1854). Providers the harness has no auth row for resolve to
the API-key fallback.
- Runtime manifest: generation 64, `v64-claude-harness`, rebuild
generation 64, global compatibility floor unchanged at 62, and a
per-harness floor `harnessMinimumGeneration: { claude: 64 }`. Image
selection applies the session harness's floor, so a Claude session never
boots a prebuilt image from before its runtime existed, and no OpenCode
image or snapshot is retired by this change.

**Turn contract.** Prompts are stamped `origin: human` and the harness
consumes the SDK message stream turn-aware: a turn the session injected
is skipped from its opening user message through its result (only those
two carry an origin), so none of its output reaches the timeline and its
result never ends this prompt; the child also runs with
`CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1`, so an `Agent` call returns
when its sub-agent finishes (several launched in one message still run
concurrently, as OpenCode's `task` tool does), and Claude's `Agent` tool
is reported as the runtime-neutral `task` tool the timeline groups
under. One prompt budget covers connect, submit, reads and emits, each
read keeps the inactivity budget, and both cleanup after a timeout and
`abort()` are bounded by the cleanup budget, dropping the child when the
interrupt hangs. A conversation reset rotates the vendor session id,
which the harness adopts and the bridge persists after the turn. A turn
without a running total leaves the cost baseline unknown so the next
total charges nothing rather than the missing turn's cost; failed
connects spend the reconnect budget.

**Handoff and staging.** The supervisor's handoff to the bridge carries
decisions only (workdir, config dir, repository presence), written
owner-only and atomically; MCP configuration reaches the bridge through
its own `SESSION_CONFIG`. A `CLAUDE_CONFIG_DIR` inside the workspace or
a checkout falls back to the default, decided once in the composition
root so managed skills (materialized before the stager runs) and the
stager use the same directory; local MCP packages are preinstalled at
staging as at OpenCode boot. The credential client treats 408/425/429, a
retryable 409 and an undecodable success body as transient.

One follow-up from the ColeMurray#1852 review rides at the bottom of this PR since
that one merged first: the upload tool refuses `hasAudio: true` locally
and its schema says so, instead of sending the file and relaying the
endpoint's 400.

Three bugs found running real sessions are kept as separate commits so
they read apart from the harness itself: resume looked for transcripts
under the bridge's config dir instead of the child's; the handoff writer
choked on the frozen session config; the child's allow-listed
environment stripped the session context the sandbox helpers need.

## Testing

- sandbox-runtime: pytest 1033 passed, 3 skipped; ruff; mypy unchanged
from `main`; node tests 21 passed.
- control-plane unit 4124 passed; shared 882 passed; typecheck and lint
across every package.
- modal-infra: pytest 238 passed. sandbox-images: lock check, pytest 56
passed.


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

## Summary by CodeRabbit

* **New Features**
  * Added Claude Agent as a supported harness for Anthropic models.
* Added isolated Claude configuration, managed skills, MCP setup, and
secure credential handling.
* Added automatic session recovery and persistence when conversations
reconnect or rotate session IDs.
* Added harness-specific runtime compatibility checks for prebuilt
sandbox images.
* **Bug Fixes**
* Improved provider account fallback when harness-specific
authentication is unavailable.
  * Video uploads now reject files containing audio tracks.
* **Tests**
* Expanded coverage for Claude execution, authentication, reconnects,
tools, staging, and runtime compatibility.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
… delivery (ColeMurray#1854)

## Summary

An Anthropic provider account: connect a Claude subscription by pasting
the output of `claude setup-token`, and deliver that stored provider
secret to a sandbox at boot without writing it to disk. Builds on the
runtime from ColeMurray#1853; the browser consent flow follows in ColeMurray#1855 and the
settings dialog in ColeMurray#1857.

### Account model

- `auth/model-provider-account-anthropic-adapter.ts`: the third provider
adapter. A setup token is a static credential: it does not refresh, it
cannot be verified against the provider (`supportsVerification: false`,
enforced by `ModelProviderAccountService.verify()` with a 409 before
anything is read or claimed), and a pasted account carries no external
identity, so two pasted tokens are never de-duplicated. A pasted value
the adapter cannot read is a 400 on create and reconnect. The adapter
declares `runtimeCredentialKind: "stored_provider_secret"`, the
counterpart of the brokered access tokens OpenAI and xAI mint per
request.
- `auth/anthropic.ts` and the authorization-code capability on the
adapter: the PKCE start/exchange helpers and the capability interface
ColeMurray#1855 wires into routes. They sit here so the adapter is complete as a
unit; no route calls them in this PR.
- `@open-inspect/shared/types/provider-accounts`: Anthropic joins the
subscription providers, with the connect/reconnect request schemas
(`setupToken`), the authorization-code request/response schemas for
ColeMurray#1855, `STATIC_CREDENTIAL_PROVIDER_IDS`, and the per-provider connection
method.

### Credential delivery

- `POST /sessions/:id/provider-auth/anthropic/runtime-credential`
(sandbox-authenticated, `no-store`): the sandbox's bridge calls it on
every start. The route reads the session's binding, requires an active
unarchived account, decrypts only after every check, and returns `{
kind, secret, credentialVersion, expiresAt }`. A credential within an
hour of its recorded expiry fences the account to `reconnect_required`
instead; that fence is a compare-and-set on the credential version the
request inspected, and a lost race (a reconnect rotated it in between)
returns a 409 marked `retryable: true`, which the runtime client from
ColeMurray#1853 retries. Every other 409 describes the account and is final.
- The existing access-token route refuses providers whose runtime
credential kind is not a brokered token, so the setup token only leaves
through the route above.
- The sandbox principal now carries the authenticated sandbox id: the
session runtime's token verification returns it, route admission stores
it on the principal, and the runtime-credential route rejects a caller
whose `X-Sandbox-ID` names a different sandbox.

### Session wiring

- `provider-account-resolution.ts`: when nothing is selected and no
default exists, OpenAI and xAI still fall back to the legacy
scoped-OAuth path their plugins understand; Anthropic never had one, so
it falls back to the API key (`api_key_fallback`) rather than a
placeholder.
- `user-env-resolver.ts`: a pre-spawn check. A session bound to a
connected account that has since been disabled, archived or fenced fails
the prompt in the queue with reconnect guidance, before a sandbox is
spawned into a credential denial. The resolver now returns the bound
account id per provider for that check.
- `managed-provider-env.ts`: the Anthropic entry of the per-provider env
fold. In account mode the sandbox gets `ANTHROPIC_OAUTH_MANAGED=1` and
any user-supplied `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL` or
`CLAUDE_CODE_OAUTH_*` values are stripped so a stale user key or gateway
setting cannot shadow the managed credential. The Anthropic platform key
arrives out of band on Modal, so an api_key session is not validated
from the assembled env the way OpenAI and xAI are.
- The harness auth rule from ColeMurray#1851 now has a provider it can bite on:
automation create and update reject a pinned Anthropic account for an
OpenCode harness, and a child on an Anthropic model cannot inherit one
from an OpenCode parent.

### What disabling, archiving or reconnecting does

It stops new hand-outs immediately: the next bridge start on a bound
session is refused, and the next prompt fails the pre-spawn check with
reconnect guidance. A sandbox that already holds the token keeps it in
memory until it exits. Open-Inspect does not chase running sandboxes:
disconnecting never revokes the token at Anthropic, so stopping our own
sandboxes would only shorten a window it cannot close. Revocation is
done at Anthropic, and the docs in ColeMurray#1858 say so. (An earlier revision of
this PR carried an issuance ledger, a cleanup outbox, triggers and a
scheduled job for that chase; they were removed in 35aec41, see the PR
comment.)

### Migration `0076`

- Rebuilds `model_provider_account_authorizations` to widen the provider
check to Anthropic and add `authorization_kind` (`device` |
`authorization_code`, default `device`); existing rows are copied over
as `device`. Nothing in this PR reads or writes the column; ColeMurray#1855 does.
- Backfills an `anthropic` / `api_key` / `legacy_migration` row into
`session_model_provider_auth` for every existing session, which is what
those sessions were already using for Anthropic models.
- Re-creates the session seed trigger from 0064 with that same third
row, so sessions the previous worker inserts during the deploy window
come out complete. `sql-portability-baseline.json` records the trigger.
- **Deploy note:** the new worker requires exactly one
`session_model_provider_auth` row per subscription provider and the old
worker writes two, so session create and resume on subscription
providers and the Provider Accounts page are unavailable between the
migration apply and the worker deploy (a few minutes on the standard
pipeline). Rollback is fix-forward.

### Web

The settings page renders every provider in the shared catalog, so this
PR adds the Anthropic icon and lets the page list a provider it cannot
connect yet (its Add entry is disabled, Verify is hidden for
static-credential providers). The connection dialog arrives with ColeMurray#1857.

## Testing

At the PR head:

- `npm run typecheck`, `npm run lint`, `npm run lint:sql-portability`.
- shared 882 passed; control-plane unit 4137 passed; control-plane
integration 1235 passed, 1 skipped (route catalog and admission
snapshots updated for the one new route); web 1581 passed;
sandbox-runtime pytest 1033 passed, 3 skipped.
- Integration coverage for the delivery route: token delivered to the
bound sandbox, wrong sandbox id refused, unbound session refused,
brokered provider refused on this route, stored-secret provider refused
on the access-token route, expiry fence, fence lost to a rotated
credential, disabled account refused from then on.
…ount hardening (ColeMurray#1855)

## Summary

The browser path for connecting a Claude account: a PKCE
authorization-code flow against Anthropic's consent page, with the code
pasted back. Stacked on ColeMurray#1854.

- `model-provider-accounts/authorization-code-service.ts`: start (mint
the PKCE verifier and state, return the authorization URL), status,
complete (exchange the pasted `code#state`, persist the account, never
persist the refresh token) and cancel.
- `authorization-transaction.ts` holds the lifecycle both completion
kinds share: reservation (reconnect target checks, attempt limit),
ownership, durable expiry, stale-claim detection, cancellation and the
connected response. Ownership is by user, provider **and**
`authorization_kind`, so the device and authorization-code route
families cannot read, claim, settle or cancel each other's rows. The
processing lease is 90s, pinned by a test to stay above every provider
call made under a claim.
- Exchange outcomes cross the capability boundary as
`ProviderAuthorizationCodeExchangeError` with a provider-neutral
classification: `rejected` (the provider's verdict; terminal),
`retry_safe` (429; the row returns to pending and the same code can be
pasted again, three times), `ambiguous` (timeout, transit failure, 5xx;
the one-use code may be spent, so the transaction fails and the user
starts a fresh authorization). The Anthropic adapter maps its reasons
onto these.
- Routes: `POST`, `GET` and `DELETE` on
`/model-provider-accounts/anthropic/authorization-codes[/:id]` and `POST
.../:id/complete`, all `private, no-store`.
- Identity: the exchange names the granting Claude account, so browser
slots carry it (duplicate creates converge, a reconnect from another
account is refused). A pasted slot has no identity; it adopts one the
first time a browser reconnect names it, unless that account already has
a slot. A slot with an identity can be reconnected only through the
browser, so a pasted token can never replace the credential behind a
verified identity. The atomic writer fences the reconnect on the
identity the slot held when the transaction started and compares with
`IS` so null matches.
- Three fixes from running the real exchange, kept as separate commits:
the token endpoint rejects requests without a User-Agent (Cloudflare
error 1010, and a Worker's outbound fetch sends none); the OAuth hosts
are `claude.com/cai/oauth/authorize` and
`platform.claude.com/v1/oauth/token` with the one-year `expires_in`; the
exchange names the granting account.

The integration config mocks the token endpoint with the real response
shape. Token ids in fixtures are synthetic.

## Testing

- `npm run typecheck`, `npm run lint`, `git diff --check`.
- control-plane: unit 4218 passed; integration 1253 passed, 1 skipped,
including `provider-account-authorization-codes.test.ts` (wrong-kind
routes under the same provider, ambiguous and throttled exchanges,
identity adoption and its refusal, pasted reconnect refused on a
browser-named slot).


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

## Summary by CodeRabbit

- **New Features**
- Added an authorization-code OAuth flow for Anthropic accounts,
including authorization, status, completion, and cancellation.
- Anthropic connections now retain additional account and organization
details from authorization.
- Identity-less accounts can adopt an account identity during reconnect
when appropriate.
- **Bug Fixes**
- Improved handling of invalid, expired, ambiguous, rate-limited, and
temporarily unavailable authorization attempts.
- Added safe retry behavior for transient failures while preventing
reuse of potentially consumed authorization codes.
- Improved validation and conflict detection during account reconnects.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…urray#1856)

## Summary

The web side of choosing a harness. Stacked on ColeMurray#1855; it needs only
ColeMurray#1851 to compile and ColeMurray#1853 for a session to run.

- New sessions: the model control becomes one agent-and-model control.
The trigger reads as one phrase (vendor mark, `Claude Agent:`, model,
effort); the menu has Agent, Model and Effort rows, as submenus on
desktop and drill-in views on mobile. Picking an agent whose model list
excludes the current model swaps to a compatible one.
- One harness-aware model selection (`resolveHarnessModelSelection`)
feeds the home composer, the session composer and the automation form.
The picker, the submitted model and the submit gate all come from it, so
a harness with no compatible enabled model blocks submission with a
message instead of resolving to a hidden fallback.
- Switching harness drops provider-account pins the new harness cannot
use (`reconcileProviderSelectionsForHarness` in the shared catalog, the
same policy the control plane validates with), and the auth controls
offer no account where the harness cannot select one.
- Session page: the same trigger with the harness fixed and no Agent
row. The header gets no badge.
- Automations: the form gains a harness select and the list shows it.
- Timeline: tool formatters for Claude's tool names (`Read`, `Edit`,
`Bash`, `MultiEdit`, …) and external MCP tools
(`mcp__<server>__<tool>`), so Claude Agent turns render like OpenCode
turns. The runtime strips its own MCP qualification from first-party
tools (`create-pull-request`, `slack-notify`, child status) at the event
boundary, the same place it already maps its sub-agent tool to `task`,
so those reach the existing renderers unchanged. The formatters have
their own test file.
- `HarnessIcon` and `HarnessName` render the vendor marks;
`OpenCodeIcon` is the pixel "O" from opencode.ai's favicon redrawn in
`currentColor`.
- The session snapshot server message and reader carry `harness`; the
shared schema defaults it, so readers see a required field.

The product name is "Claude Agent" everywhere user-facing, and a test
asserts it.

## Testing

- `npm run typecheck`, `npm run lint`.
- web 1611 passed; shared 885 passed; sandbox-runtime 1034 passed;
control-plane unit suite green.


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

## Summary by CodeRabbit

* **New Features**
* Added agent/harness selection across sessions and automations, with
provider-specific labels and icons.
* Harness choices are saved and restored, with compatible models and
reasoning options shown automatically.
* Automation forms now support selecting, editing, and submitting an
agent harness.
  * Session and automation details display the selected agent.
* Improved formatting and grouping for Claude Agent, MCP, and additional
tool activity.

* **Bug Fixes**
* Incompatible model selections now fall back to supported alternatives.
  * Provider authentication options now reflect harness compatibility.
* Clear feedback is shown when no enabled model supports the selected
harness.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…oleMurray#1857)

## Summary

Settings > Provider Accounts learns to connect a Claude account. Stacked
on ColeMurray#1856; it depends on ColeMurray#1855 for the routes.

- `provider-authorization-code-dialog.tsx`: two entry points, one
mounted at a time. Browser: open the authorization URL, paste the
`code#state` value the consent page shows (the `#state` half is
required), and the dialog completes the transaction. Setup token: paste
the output of `claude setup-token`. Switching to the setup token
unmounts the browser flow and cancels its transaction; a save in flight
locks the inputs, Back, Cancel and dismissal, so two credential writes
never overlap.
- `use-provider-authorization-code.ts`: the transaction lifecycle
(start, complete, reconcile, cancel) as one phase machine, validated
with the shared schemas. A completion is abortable; when its outcome is
unknown (another request owns the claim, a lost response, or the
deadline aborted it) the hook polls the durable status for a bounded
window before it decides. Only 404/410 mean the transaction is gone.
Settled states use the control plane's vocabulary, so a provider
rejection (`denied`) offers a fresh transaction.
- The settings page's connection strategies are a complete record again:
Anthropic maps to the authorization-code dialog and Verify is hidden for
static-credential providers. The reconnect dialog explains that sessions
which already received the credential keep it until their sandbox exits.
A form closes as soon as its write succeeds and the list refreshes
best-effort, so a refresh failure cannot resubmit an identity-less
setup-token account.
- The authorization-code BFF routes accept only providers that connect
by that method.

## Testing

- `npm run typecheck`, `npm run lint`.
- web 1648 passed. The hook tests cover start, completion, 409
reconciliation, a never-settling completion aborted at the deadline,
unmount abort, expiry and reconnect; the dialog tests cover both entry
points, cancel with a pasted code, Start over after denial, and the save
lock; the settings tests cover a connect whose refresh fails.


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

## Summary by CodeRabbit

- **New Features**
- Added Anthropic account connection and reconnection through an
authorization-code flow.
- Added support for connecting with a Claude setup token where
applicable.
- Added progress, countdown, cancellation, retry, and failure states
during authorization.
  - Added clearer handling for denied or expired authorization attempts.
- **Bug Fixes**
- Improved account refresh and error feedback after connection actions.
- Added credential-rotation warnings and restricted unsupported account
actions for Anthropic connections.

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

Documentation for the Claude Agent harness. Stacked on ColeMurray#1857.

- `docs/CLAUDE_AGENT.md`: choosing a harness, connecting a Claude
account (browser and setup token), how the credential reaches the
sandbox, the lifecycle of disable, archive and reconnect, a runbook for
revoking a setup token, and the migration window to plan for when
deploying.
- CHANGELOG release note, including the deploy-window note for
migrations 0075 and 0076 and the runtime manifest bump.
- README, HOW_IT_WORKS and AVAILABLE_MODELS mention the second harness.

## Testing

- `prettier --check` on the five files.


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

## Summary by CodeRabbit

- **New Features**
- Added support for connecting Anthropic/Claude accounts through browser
authorization or setup tokens.
- Added connection and reconnection flows with expiration tracking,
retry handling, cancellation, and credential-rotation warnings.
- Added Claude Agent harness support alongside OpenCode, including
selectable harness and authentication options.

- **Documentation**
- Expanded setup, authentication, harness, model, sandbox, migration,
and operational guidance for Claude Agent.
- Updated architecture documentation and credits to include the Claude
Agent SDK.

- **Bug Fixes**
- Improved connection handling so successful saves close forms reliably
while refresh issues are reported separately.

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

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate
Tests

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @jasoncuriano, Action: pull_request

@jasoncuriano
jasoncuriano merged commit 0e23d78 into main Sep 11, 2026
22 checks passed
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.

3 participants