Promote main to release - #2631
Merged
Merged
Conversation
`prototypes/side-highlight` inventoried ten accent rails across six stylesheets doing three unrelated jobs — containment in the transcript, selection in the sidebar, nesting under a tool card — and proposed "M, the mix", split by what each block *is* rather than by what it looks like. This implements it. Only nesting keeps the bar, which is literally what a nesting guide depicts; `accent-rails.test.ts` now holds that list closed. Containment becomes a plate, in two materials (base.css): - **Flat** — GitHub alerts and blockquotes. Prose the agent wrote is part of the answer, so it gets a plain surface and the hue moves to the title. These are styled by `@copse/streaming-markdown`, so markdown.css overrides at the package's own specificity and wins on source order; no upstream change. - **Hatched** — thinking, review, comparison. Copse annotating its own turn, and a texture says so without spending a fourth hue or a fourth shape. It degrades to the flat plate under `prefers-reduced-transparency` and `prefers-contrast: more`. The VNC pane takes the gutter instead: a separate pane whose status hue has to survive on one line, where a plate would just box three of them. Selection — chat rows, Settings nav, VNC tabs — becomes the full-bleed fill plus weight. Answering the prototype's open questions: - **Moiré is settled by the number.** 8 is a multiple of 4, so at every quarter-step display scale the hatch period lands on a whole count of device pixels and there is no beat frequency. The pitch is load bearing. - **Two new hues.** Five alert kinds need five, and the palette had three. `--info` and `--important` join the semantic tokens with derived light values. `--important` is pushed bluer than a true violet so it does not read as the pink accent; a user whose accent *is* purple still collapses that, which is written down rather than fixed. - **Callout glyphs are solid**, the only filled shapes in an outline set, with distinct silhouettes so the kind reads before the hue. Their optical-centring nudges are baked into each mask's viewBox — measured once by the prototype, not recomputed at runtime. The tuning lives in base.css but the colour is mixed at each use site, because a custom property is substituted where it is *declared*: one holding the whole `color-mix(… var(--sev) …)` freezes the root's grey into its computed value and inherits that grey everywhere, whatever each block sets --sev to. Every plate came out the same neutral, and a screenshot is not how you catch that — the new `callout-surfaces` e2e spec is, and it did. `.thread-proposal` (#2334) landed after the prototype's inventory and keeps its rail, allowlisted with the reason: a standing offer is an *ask*, a fourth job the bar was not doing when this was surveyed. Worth revisiting deliberately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI's full e2e tier found what the stylesheet-level guard could not: four specs assert the old design directly, so removing the rails made them fail rather than pass. Each is repointed at what the surface means now, not just at a new number. - `conversation-visual-hierarchy` pinned review and comparison to a 2px left border, a zero radius and a fading `linear-gradient` wash. They take the hatched plate now, and the radius is the part a rail could never have had — it would have bowed around the corner. - `markdown-streaming-blockquote` used the rail's width as its proof that the stylesheet had reached a *pending* quote at all. That proof is the plate now: a filled background and a radius, plus an explicit `0px` border so the rail cannot creep back on the streaming path alone. - `thread-sidebar-selection` and `ui-polish` both asserted a rail — trailing on a chat row, leading on Settings nav. Both are the full-bleed fill plus weight, and in `ui-polish` the two now being *identical* is the assertion: one selection gesture wherever the list sits, where the old spec required them to differ. Unrelated: `thread-terminal-rename-archive` also failed on shard 8 with "expected renamed terminal label", a wait on the terminal tab's rename. It passes locally against this build and touches nothing this branch changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Prototype for docs/plans/thread-in-container.md: the product's headless agent loop runs inside a disposable local Docker container with no user prompts. - Contained-effect gate branch: when an unattended run is armed and the runtime attests to container containment, the shell gate allows in-guest effects, defers outward effects (git push, GitHub writes, publishes) under the new prompt cause `shell-outward-effect`, and refuses host escapes. Harm-gate hard denies stay hard. - Runtime containment declaration from a host-written attestation; refused when short of the bar (unprivileged uid, read-only rootfs, cap-drop ALL, no-new-privileges, brokered-or-no network, no foreign mounts). - Unattended-run ledger beside Guarded YOLO, mutually exclusive both ways; arming begins deferral mode and requires budgets. - Container worker image and entry; host runner with git-bundle carry-in and carry-out to refs/copse/runs/<id>, --network none plus a per-origin unix-socket egress broker, secret canary, idempotent teardown, a managed container sweep, and a JSON review record. `pnpm run thread:container`. - Unit tests for classification, attestation, the ledger, the gate matrix, Docker argv and the bundle round trip; an opt-in end-to-end test against a real daemon with a scripted OpenAI-compatible model behind the broker. - Plan document, plans index row, and a cross-reference from unattended-runs.md recording where this diverges from its Decisions 1 and 3. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
Move the container runner into the main process and let the UI trigger it. - The runner, egress broker and guest worker live under src/main/services/container-runtime/; the worker is a standalone main bundle (dist/main/thread-container-worker.cjs) with node-pty stubbed and only the sandbox runtime external, which the image context stages by copying the package from the app's own node_modules (no npm at run time). The Dockerfile and entrypoint are embedded strings. The CLI wrapper now bundles its entry the way the autonomy runner does. - resolveContainerProvider maps a product model id to the guest's provider (OpenAI-compatible URL for local servers, OpenAI, OpenRouter and extra providers; the product's own resolver in the guest for Anthropic), the one egress origin, and the key, which travels as a run-scoped environment variable and is blanked once the guest holds it. - ContainerRunService owns one run per thread, refuses SSH projects and unresolvable models before Docker, records the arming as a mode-arming decision, and pushes ContainerRunProgress snapshots over container:* IPC; preload and the demo API expose api.container. - The composer footer gains "Run unattended in a container…": one dialog that is the arming form (draft prefilled, budgets, model and reachable origin) before a run and the status/review view (phase, log tail, deferrals, commits and their ref, canary, teardown) during and after; a banner over the composer mirrors the phase and a toast announces the end. - Visual evidence: browser-tier spec over a seeded finished run and an Electron spec through real IPC, with screenshots; unit tests for the provider plan and the service; the Docker integration test still passes with the moved runner. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
Four findings from the readiness review of the container run, each with the
test the review asked for.
**Snapshot the thread's checkout, not the project root.** A thread in an
isolated worktree has its own branch and its own uncommitted edits, so the
service resolved the wrong tree. It now resolves the checkout through
`resolveThreadExecutionContext` — the cold resolver the supervisor already
injects — refuses a root git cannot snapshot with a readable reason instead of
a raw `git rev-parse` failure mid-run, and never falls back to the project root
when a worktree is broken. `start` claims the thread's slot before its first
await so two clicks cannot start two containers, and releases it when
resolution fails. The record and the dialog name which checkout ran.
**Never report a clean finish when retrieval or cleanup failed.** The record
now carries `carryOut { expected, ref, error }` and `cleanupError` instead of a
bare ref, and `judgeRun` decides the phase: commits that were produced but
could not be fetched, a container that would not stop or reap, and a leaked
secret canary keep a run out of `finished` and surface as the failure reason or
a warning. The banner no longer says commits are back when no ref was fetched.
**Key image reuse to the worker build.** `copse-worker:local` survives app
upgrades, so tag existence alone kept running the previous guest — including
its permission behaviour. The image is labelled with a fingerprint of the guest
bundle, the Dockerfile, the entrypoint, the uid and the sandbox-runtime
version, and is rebuilt when the shipped worker differs.
**Bound the wall-clock deadline independently.** `docker stop` failing or
hanging while `docker wait` stayed open could leave a run pending forever,
never reaching its cleanup block. The stop has its own timeout and a bounded
grace period settles the wait either way; the abandoned wait is killed and the
failure is reported rather than swallowed, with `docker rm --force` in teardown
as the next line of defence. Both Docker calls are injectable so a failed and a
hung stop are covered by unit tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
The plan quoted `container:runThread` / `container:runChanged`; the merge renamed both to the kebab-case wire names the frozen protocol derives from the facade. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
`judgeRun` pushed the canary onto `warnings` and then tested the cleanup branch first, so a run that both leaked the secret canary and failed to reap its container reported "The container could not be removed" as the failure and buried the leak in the warnings list. A container left behind is a chore; a credential that escaped the run is an incident, and it should be the line the user reads first. Moves the canary verdict above the cleanup branch. The canary stays in `warnings` too, so the dialog's warning strip still shows it either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
Reconstructs the adaptation the branch's `Merge origin/main` commit carried. A rebase drops that merge, so the rename it performed has to land as its own commit or the preload facade would invoke channels the main process never registers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCcGaKPRL3fmRpLR3rK5WX
`thread-container.ts` carried its own `isRecord`, byte-identical to the one #2407 consolidated into `@shared/unknown-value.ts`. #2419 then made the type predicate inventory shrink-only, so the duplicate registered as a new unlisted predicate and failed `check` on the merge with main. The file already imports through `@shared`, so the alias resolves on every path this module is built for, the guest bundle included. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
The composer footer gained "Run unattended in a container…", and two e2e specs pin the overflow roster: `footer-overflow-bounds` by count and `developer-mode` by exact labels, in both its default and developer-mode cases. Only the unit-level roster in `input-bar.test.ts` was updated when the action landed, so these three assertions still described a six-item menu. They went unnoticed because the e2e tier never ran on this PR: the `e2e` job is gated on `draft == false` or the `ci-full` label, and this PR is a draft. Labelling it `ci-full` dispatched the shards and they failed on exactly these. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
Screenshot candidates rendered for parent PR #2348 at `bc51f9dad83792e849cb20e25a269806479c5f2b` by [CI run 34031724395](https://github.com/copse-dev/agent-pane/actions/runs/34031724395). Review GitHub's image diffs, then merge this PR (or enable auto-merge) to apply the accepted references to `claude/copse-container-execution-nw4ln8`. This branch contains only PNG candidates from the immutable `reference-screenshot-candidates-34031724395` artifact and never targets `main`. If the parent branch has advanced beyond the source SHA above, do not merge this PR; the successful CI run for the new head will replace it. Co-authored-by: jonathanKingston <338988+jonathanKingston@users.noreply.github.com>
Starting a run on macOS failed before the container existed: egress socket path is too long for a unix socket (104 > 100): /var/folders/r5/qll_28695_q_2qr2kk7lv5gm0000gn/T/copse-egress-run-mtpvs161-9a6506/openrouter.ai_443.sock A unix socket path is capped by `sun_path` — 104 bytes on macOS, 108 on Linux — and macOS gives each process a per-user temp root around 49 bytes before we add anything. Both of our contributions grew without a bound: the directory spelled the whole runtime id, and the file name spelled the hostname, so an extra allowlisted origin on a long host could overflow on Linux too. Both are now digests: `copse-cx-<10 hex of the runtime id>` and `<10 hex of host:port>.sock`, a flat 15 bytes per socket whatever the origin. Worst case on that same macOS root is 83 bytes. The run id and the origins stay readable in `run.json` and in the broker's log lines, which is where they are actually looked up. The guest can no longer derive the file name, so `COPSE_EGRESS_ORIGINS` carries it: `host:port=socket`. The entrypoint refuses a name containing a separator rather than pasting it into a path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
The dialog asked for the task in an editable textarea prefilled from the composer draft. Both ways into it — an existing thread, the new-thread input — mean the user typed that sentence a moment ago, so the field was asking them to confirm their own text in a second box. It is now a confirmation. The task is the composer draft, shown read-only; an editable field appears only when the composer is empty, because then there is genuinely nothing to run, and Start stays disabled until it has something (the IPC schema requires a non-empty prompt, so an empty start only ever produced a validation error). What the dialog is actually for is the part the composer cannot say: this runs unwatched, on this model, until one of two budgets stops it. So the model becomes a choice rather than a line of hint text — a select over the provider-backed models (agents are filtered out; the guest runs a model in-process, not an ACP session), defaulting to the thread's and applying to this run only. `ContainerRunProgress` gains `prompt`. It is part of what a run is: the review record can now say what was asked, which it could not before, and "Start another run" can offer the same task again — with the composer empty it would otherwise reopen a blank form it refuses to submit. Tests: `fillModelSelect` is extracted and unit-tested for the shapes the option list arrives in — empty, grouped, and missing the current model — because neither e2e fixture can populate it (the Electron project seeds no provider keys and the demo API serves no LM Studio models), and asserting otherwise there would have been an assertion that could never fail honestly. The browser tier covers both faces of the sheet, quoted and empty; the Electron tier covers the live preload path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
…ot run Three things, all one thread. **The picker is the app's picker.** The run dialog had a hand-rolled `<select>` with its own population code — a second, worse way to choose a model in the same app, with no search. It now mounts `mountModelSelectPicker` over a hidden native select, the way Settings does, so the roster is the same searchable menu as the composer's and the default is the thread's current model. The bespoke `fillModelSelect` and its test go with it. The picker has to mount after the field exists: `mountModelSelectPicker` inserts its trigger with `select.after(...)`, which silently does nothing while the select has no parent. That is what the first attempt got wrong. **Agent models are shown, not hidden, and not offered.** ACP, remote and plugin agents are separate programs that authenticate as the user — an OAuth login in `$HOME` or their own vendor key — and an unattended container is given neither by design (`unattended-runs.md` decision 3), with a secret canary checking it stayed that way. So they cannot run in the guest. Hiding them makes this picker quietly different from every other one; offering them ends in the service refusing the run. They are listed disabled and labelled "needs its own login" instead. The runnable set is derived from the product's own `includeAgentModels: false`, not a hardcoded prefix list, so it stays right as that set moves. **The refusal explains itself.** `resolveContainerProvider` answered an `acp:` id with "cannot resolve a provider for model …", which reads as an internal miss. It now says the model runs as its own agent process signed in as you, and that the container is not given your credentials. The four container-run reference screenshots are refreshed here rather than in a follow-up, and that placement is load-bearing. `filter-screenshots` holds them as "contested: deliberately committed on this branch", so CI never proposes them and they have to be re-rendered by hand — but a commit touching *only* `tests/e2e/screenshots/` is a screenshot-only head, which `ci.yml` skips e2e for, before the `ci-full` label is even consulted. Landed separately they would have skipped the very e2e run that should check them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
…t use
The greyed-out agent rows said "needs its own login", which was wrong twice
over. The agent is usually already signed in — that is what "on this device"
in its group heading means — and signing in again would change nothing,
because the container is given no credentials at all, not because a
credential is missing. It sent people off to fix the wrong thing.
The row now says "not available in a container", and a note under the model
field carries the part that actually unblocks them: agent models sign in as
you on this device, an unattended container is given no credentials, so pick
the same model from its provider instead. That last clause matters — the
provider-backed twin is usually sitting one group up in the same list, and it
runs.
The note appears only once the roster actually contains a disabled row, since
a disabled option cannot be clicked to ask why.
`loadRunModelOptions` now takes the bound fetcher rather than the api and the
current model, which drops a `{} as ModelOptionsApi` cast from the test and
lets it assert the real thing: the reason names the container, and never
implies a sign-in would help. The first version of that test asserted a
string literal it had written itself, which would have passed against any
implementation at all.
Screenshots are refreshed in this commit rather than after it, for the reason
recorded in e17ee4f: a screenshot-only head skips the e2e run that checks it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
`carry-in / carry-out over git bundles` asserted that `writeCarryInBundle` and a fresh `createSnapshotCommit` produce the same commit sha. A commit sha folds in the committer timestamp at one-second granularity, so the assertion held only while both calls landed in the same second — it passed locally for days and failed on a CI runner that crossed a second boundary between them. The intent was that the bundle carries the same snapshot, which is a statement about content: compare the trees, which are timestamp-independent. Verified the mechanism directly — two commits of an identical tree a second apart differ in sha and agree in tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
The run dialog offers agent-backed models greyed out as "not available in a container". This records why, and what it would take to change that, as a section of the plan with its own decisions (A1–A6), phases (A-0 to A-4) and test rows. Three facts make the guest unable to run one today, each with its file: an agent authenticates as the user from its own store, and the guest is given no credentials by design; the broker cannot express the wildcard origins the catalogue declares, and two origins on 443 collide in the entrypoint's per-origin socat scheme; and an agent runs its own tool loop, so the deferral guarantee — the run's headline claim — would not hold. The decisions: a vendor key scoped to the run and never the login (A1); one broker socket behind a guest CONNECT proxy with a pattern allowlist, which also fixes the 443 collision (A2); the record names the harness and outward effects are denied rather than deferred, because an agent's own tool call cannot be replayed from the host (A3); binaries baked, pinned and in the image fingerprint (A4); the container is the sandbox, no nested seatbelt (A5); scope is the key-capable agents only (A6). Every phase before A-3 is inert behind the existing refusal. A-1, the egress rework, is worth landing on its own for the collision it fixes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
The nested AGENTS.md walk was cached for one second, so a turn re-walked up to 10,000 directories before nearly every file tool call. A turn now creates a discovery memo (`createNestedInstructionTurn`) that the system prompt build seeds with one fresh walk; every `activateNestedInstructionSources` call of that turn reuses it. Callers outside a turn (the composer estimate) share a 30 s cache, and Settings still forces a reload. An edit tool whose path is an AGENTS.md invalidates both the memo and the shared cache after it runs, so the next file tool call sees the file the agent wrote, moved, or removed. `run_shell` writes are not observed. Each mid-turn activation now adds a one-line transcript notice naming the file, emitted as a text chunk at the next step boundary rather than between a tool call and its result (a text chunk there would start a new assistant bubble and strand the tool card). Discovery records when it stopped at a directory, file, or depth cap: project sources carry `discoveryTruncated` and the walk logs a warning. A nested file that repeats an already-listed source stays listed and is marked `duplicateOf` instead of being dropped; its content is still injected once. The latest-activation record for Settings is now keyed per thread under the project root, so concurrent threads no longer overwrite each other; Settings reads the most recent thread. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011jaQbed96h8aSZLijfiHNB
…urces `instructions:list` passes `duplicateOf` and `discoveryTruncated` through to the renderer. Sources → Instruction files marks a nested file whose text repeats a listed one with a quiet "duplicate" badge and says which file carries it, and appends a single note under the list when nested discovery stopped at its cap, so a missing deep file no longer looks unwritten. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011jaQbed96h8aSZLijfiHNB
Name the built-in file tools that bring a path into context and say that `run_shell` writes, ACP agents, and subagent reads do not activate a nested AGENTS.md. Describe the once-per-turn discovery, the re-walk after the agent writes an AGENTS.md, the transcript notice, the duplicate badge, and the truncation note in Sources. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011jaQbed96h8aSZLijfiHNB
The container's egress was one unix socket and one guest-side socat listener per allowlisted origin, bound by name through --add-host and a sysctl for privileged ports. That could not express a wildcard, and two origins on the same port both tried to bind it in the guest, so the second listener died backgrounded and its origin silently blackholed. Now the host mounts a single broker socket and the worker starts a small HTTP proxy on its own loopback, advertised through HTTPS_PROXY/HTTP_PROXY (Node's fetch honours them under NODE_USE_ENV_PROXY). Each request opens the socket, writes `CONNECT host:port`, and the host answers OK or DENY against a pattern allowlist — exact `host:port` and `*.suffix:port`, matched on the dot boundary. TLS stays end to end; refusals are logged with the target, so a destination the guest asked for and did not get is in the review record. The provider path is unchanged: plain-HTTP origins go absolute-form through the proxy, re-chunked and streamed so server-sent events arrive as they are sent. - egress-rules.ts: the grammar, pure, shared by both sides. - egress-broker.ts: the socket, the one-line protocol, the log; a `resolve` remap may carry a port so a stand-in origin can play 443. - guest-egress-proxy.ts: the loopback proxy, dependency-free, in the worker bundle. - --add-host, the sysctl and socat are gone from the run and the image. - The integration test now reaches the model on guest port 443 through a wildcard rule and checks the rule is named in the log. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
…s shape `ProjectInstructionSummary` gains a required `trusted: boolean` alongside the optional `scopePath` / `duplicateOf` / `discoveryTruncated`. A new *optional* result field would have been additive, but a required one is not, so `gen-api-protocol.mts --compare-ref` classifies `instructions:list` and `client.instructions.list` as breaking and fails the gate at version 2. The gate did not exist when this branch forked; it arrived on `main` with the protocol freeze, so this is the first run that has had to answer for the shape change. Bumping is the right side of the trade: `trusted` is a real property of every instruction source, and weakening it to optional purely to stay additive would put a lie in the type. The manifest is regenerated with `gen:api-protocol`, not edited: only its `version` moves, because the manifest records channel arity and binding names, which this change leaves alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCcGaKPRL3fmRpLR3rK5WX
The run dialog offered every agent model greyed out: an agent signs in as the user on the desktop, and the guest is given no credentials. That is still true of the login. It is not true of a vendor API key, which is the one credential a run already carries, scoped to the run, by value. So the agents with a documented key path — Claude, Codex, Gemini — can run in the guest, on their own harness, under that key; the rest stay greyed out with a reason that says what would make them run, or that nothing would. Plumbing (A-0): the image takes an ACP_AGENTS build argument of pinned package@version specs (src/shared/container-acp-agents.ts) and installs them globally before dropping to the worker user; the specs join the image fingerprint. The run spec carries the agent config; the worker registers that one agent in its settings overlay so getAcpAgent resolves it and nothing else. The result and record gain `harness` and `denials`. Credentials and policy (A-2): the user's own env map and desktop command path never cross; the guest gives the agent exactly one variable, the run's key under the agent's own name. The container is the sandbox, so the agent spawns without a nested seatbelt and the session host is not staged. Inside a contained run the ACP permission responder treats the agent as contained, and its shell requests run the contained gate with a new option, outwardEffects: 'deny': an agent's own command cannot be replayed by Copse, so an outward effect is refused and recorded rather than queued. A host escape's throw is answered to the agent as a rejection. Any other kind that would reach a dialog is refused and recorded. The worker reads the refusals back from the decision log. Resolver (A-3, code): resolveContainerProvider returns an `acp` plan for a registered key-capable agent whose key is in Settings — the full selection as the model, the harness, the key, and the catalogue's allowedDomains on 443 as egress rules. Others are refused per agent. Dialog (A-4): agent rows enable when the vendor key exists, with the per-agent reason otherwise; the note names which agents run and on what; the record shows the harness and the refusals. No new IPC — renderer and resolver read one shared table. Not run here: the image build with the real agents baked in (no npm registry from a Docker build in this sandbox), the scripted-agent integration test (needs Docker), and a real agent run (needs a vendor key). The plan records each as the next thing to run where it can. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
Main bumped the same three security pins (xmldom under a 0.8 range, the merge keeps that spelling), refreshed reference screenshots this branch had also re-rendered (main's are taken; the container feature is off by default now, so the footer menus it re-rendered match main's), and moved Electron to 44.1. Main's new design lint keeps the accent rail for nesting alone, so the run dialog's warning box becomes a plate on the danger hue, as the callout surfaces in styles/global/base.css prescribe. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
chore(models): sync intellect scores from Artificial Analysis
fix(ssh): combine recursive and force removal flags correctly
…dexing fix(files): resolve explicit references before indexing completes
Fix callout glyphs in forced colors and correct prototype guidance
Add durable event automation admission and recovery
…parity Show PR comments, reviews, and individual CI checks
…rch-failure fix(search): fall back when indexed search exits unsuccessfully
Add bulk worktree cleanup without rescanning other checkouts
…paths fix(files): treat recursive listing paths as literal directories
…t-extensions fix(attachments): preserve extensions when shortening filenames
…lity fix(sandbox): keep zsh heredoc files in allowed scratch
…econcile-worktrees post-mortem Three findings from the 2026-09-09 post-mortem of /reconcile-worktrees stalling inside Copse when driven by Codex over ACP. 1. Skill discovery ignored ~/.codex/skills. `.codex` joins the container list at both scopes, last in precedence; project-scope roots are now sorted by that precedence instead of readdir order so the documented order is true. `userSkillRoots()` and a test-only home override make the user scope testable without the developer's real skills leaking in. 2. run_shell could not read the invoked skill's directory or the primary checkout. Invoking a skill now grants that thread read-only sandbox access to the skill directory plus validated `paths` frontmatter entries (thread-read-roots.ts / skill-read-roots.ts); the seatbelt overlay adds them to allowRead only and the shell-scope classifier treats them as contained read roots, so read-only commands naming them auto-run in the sandbox. `sed` is admitted as structurally read-only in its provable filter shape. A linked-worktree thread can now read (never write) the shared primary checkout, with the shared .git, its own nested worktree and nested sibling worktrees carved out per child so Linux bind ordering cannot shadow write binds; the primary's HEAD, index and sibling admin dirs are readable so `git worktree list` and `git -C <primary>` work. 3. An abandoned bridged MCP call cancelled its pending approval. The bridge now aborts an abandoned call (socket close or notifications/cancelled) with an AbandonedCallAbort reason; the approval service parks the prompt instead of cancelling it, rejects the caller with ApprovalPendingError telling the agent to retry the same call, and replays the eventual verdict once to an identical request (same thread, execution root and prompt) within ten minutes. Turn end still dismisses parked prompts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rted The experimental setting hides the "Run unattended in a container…" entry by default, so the developer-mode and footer-overflow-bounds specs seed it on, matching container-run-dialog. The developer-mode helpers take an optional settings bag for this. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
refactor(container): share validated run and attestation contracts
refactor(container): report lifecycle phases as structured events
refactor(agent): share transcript content and tool updates
refactor(container): track external runs with the task supervisor
Support directory-scoped AGENTS.md instructions
…ion-nw4ln8 feat(security): run a thread unattended inside a hardened container
…stmortem Fix skill discovery, sandbox reads and abandoned approvals (reconcile-worktrees post-mortem)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Daily automated promotion of
mainto the release-readyreleasebranch.The repository ruleset requires the full
CI Passedgate. This PR willmerge automatically when that gate is green; failures leave it open
until the underlying issue is fixed on
main.