Skip to content

feat(security): run a thread unattended inside a hardened container - #2348

Merged
jonathanKingston merged 75 commits into
mainfrom
claude/copse-container-execution-nw4ln8
Sep 9, 2026
Merged

jonathanKingston merged 75 commits into
mainfrom
claude/copse-container-execution-nw4ln8

Conversation

@jonathanKingston

@jonathanKingston jonathanKingston commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Run a Copse thread unattended inside a disposable, hardened local Docker container with no user prompts, per the plan docs/plans/thread-in-container.md (the loop runs in the guest, not on the desktop), startable from the app, able to run key-capable ACP agents (Claude, Codex, Gemini) in the guest under a vendor API key — or, for Codex and Gemini, on the user's desktop sign-in when they opt in per run — and, since the later commits, a turn on the thread that launched it, with follow-ups that continue the run.

Experimental and off by default. Settings › Experimental › Unattended container runs (containerRunsEnabled) turns it on. The run service refuses to start anything while it is off, whatever the renderer asks, and the composer's menu entry and target picker exist only once it is on. Needs Docker.

The runtime and the gate

  • Contained-effect gate branch. ensureShellCommandPermitted takes one new branch when the run is an armed unattended run and the runtime declared container: the harm gate still runs (hard denies stay hard), then decideContainedShellEffect (@copse/shell-guard/container-effects.ts) refuses host escapes, defers outward effects (git push, GitHub writes, publishes, HTTP writes, cloud CLIs) under the new prompt cause shell-outward-effect, and allows everything else.
  • Runtime containment declaration (security/runtime-containment.ts): the host writes an attestation of the hardening it applied; the guest declares from it and refuses anything short of the bar (unprivileged uid, read-only rootfs, cap-drop ALL, no-new-privileges, no unmediated network, no host mount outside /run/copse).
  • Unattended-run ledger (security/unattended-run.ts): session-only, per-thread, beside Guarded YOLO and mutually exclusive with it; arming begins deferral mode and requires budgets.
  • Container worker: the product's runHeadlessAgent in the guest with a fail-closed approval handler that counts prompts (must be zero); workspace carried in as a git bundle and carried out as commits under refs/copse/runs/<id>. The container is the sandbox (no nested bubblewrap).
  • Host runner: hardened docker run argv (the same source as the attestation), --network none plus one broker socket and a token-gated HTTP CONNECT proxy in the guest with a pattern allowlist (host:port, *.suffix:port), a connection log, a secret canary check, idempotent teardown, a managed-container sweep, and a JSON review record per run. A pnpm store volume is shared across installing runs.
  • Follow-up refactors merged into this branch (refactor(container): share validated run and attestation contracts #2598, refactor(container): report lifecycle phases as structured events #2599, refactor(agent): share transcript content and tool updates #2602, refactor(container): track external runs with the task supervisor #2603): the run request and attestation cross the host/guest boundary as shared validated schemas; the worker reports its lifecycle phases as structured events; transcript content and tool updates share one reducer with the desktop agent; runs are tracked by the task supervisor.

From the app

  • resolveContainerProvider adapts the desktop's own provider resolution for the guest: describeProviderProviderDescription (protocol, endpoint, tuned parameters, OpenRouter privacy and OpenAI transport settings; keys apart) travels in run.json with the desktop's context window, and the guest builds the same client from it. A loopback endpoint is renamed to a broker alias; LM Studio's WebSocket transport becomes its OpenAI-compatible endpoint.
  • ContainerRunService owns one run per thread, refuses SSH projects before touching Docker, records the arming as a decision, and pushes ContainerRunProgress snapshots over container:* channels; preload and the demo API expose api.container.
  • A run is a turn on its thread: the prompt is the thread's user message and the run is one container_run tool call whose subagent session holds the guest's transcript (folded with the desktop's own text planner) and the review record; the dialog closes on start and a banner shows the phase. The composer gets a target picker: to container continues the run (carrying in its commits, or a fresh snapshot when it made none, with what it was asked and reported prefixed to the prompt); to thread talks to the desktop model, which is told the run's outcome through the thread's model history. Apply N commits to this checkout cherry-picks the run's commits onto HEAD.
  • ACP agents in the guest: pinned agents baked into the image, the run's one key handed to the agent under its own variable, contained permission policy (outward effects refused and recorded, never deferred), the resolver's verdict per picker row, and the per-run sign-in opt-in for Codex and Gemini.

Changes to shared paths (the merge risk)

  • The desktop's buildProvider now goes through describeProvider + buildProviderFromDescription: the same branches split in two, covered by provider-selection.test.ts.
  • The worktree backup, the container carry-in and the remote e2e push share one working-tree snapshot (git-snapshot.ts). A clean tree's backup ref now points at HEAD rather than an identical fresh commit.
  • runHeadlessAgent returns the turn's turnOutcome.
  • Security pins moved past today's advisories: @xmldom/xmldom 0.8.15, sharp 0.35.4, js-yaml 4.3.2 (main carries the same).
  • Two e2e specs that assert the footer overflow menu (developer-mode, footer-overflow-bounds) seed the experimental setting on, since the container entry is hidden by default.

Validation

  • pnpm run check green (typecheck, lint, format, dead code, 8970 unit tests); CI green on every head since the review rounds.
  • Unit: the gate matrix, attestation shortfalls, Docker argv, carry-in/out and the carry-out branch rule, the image fingerprint, judgeRun, the provider description and plan, the run service (key hidden and blanked, stop before the container exists, continuation from memory, disk and the card), the egress grammar/broker/proxy over real sockets (including close during a dial), the guest transcript fold, the turn's failure and token tally, the thread history record, the composer target and the experimental gate.
  • Docker integration (COPSE_THREAD_CONTAINER_E2E=1 pnpm test -- thread-container.integration, acp-container.integration): a scripted model and a scripted ACP agent behind the broker; not run in CI.
  • Real runs: Codex on the author's sign-in, several times; each run's findings are fixed in the later commits (beachball on start, no live progress, run-on report text, follow-ups to thread and container, a volume that went away under the container).

Not in this PR (recorded in the plan)

Attaching from the desktop (ACP role + lease), approving a deferral from the dialog, the macOS broker variant.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv

github-actions Bot added a commit that referenced this pull request Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🖥️ PR preview

@jonathanKingston

Copy link
Copy Markdown
Collaborator Author

Readiness review: retaining this as a prototype draft. Its own product gate still lists unproven real-provider execution, no deferred-action approval/replay surface, and no startup orphan reconciliation/TTL. The scripted container test and UI shots establish useful plumbing, but do not discharge those security/lifecycle requirements; I am not merging unattended execution on that evidence alone.

@jonathanKingston

Copy link
Copy Markdown
Collaborator Author

Additional review findings against current head 5e2abcd, beyond the previously noted readiness gaps:

  1. Snapshot the thread checkout, not the project root. ContainerRunService.start resolves only getProjectRoot(request.projectId) (container-run-service.ts:92). A thread running in an isolated worktree therefore snapshots the project checkout instead, potentially using the wrong branch and omitting its edits. Resolve and validate the thread-specific checkout before carry-in; cover a project and thread worktree with different commits and uncommitted edits.

  2. Do not report successful completion when result retrieval or teardown fails. The service chooses finished solely from the presence of a result and its stopReason (container-run-service.ts:185-189). Carry-out fetch failures are only logged (thread-container.ts:748-754), while teardown failure and container exit are not considered. The UI can consequently say commits are back when no result ref was fetched, or report completion without surfacing failed cleanup as a failure/warning. Track these outcomes explicitly and test failed fetch and failed teardown records.

  3. Invalidate the worker image when the shipped worker changes. ensureImage only checks whether the fixed copse-worker:local tag exists (container-run-service.ts:40-44). Once built, an image survives app upgrades and can keep running old worker code, including old security behavior. Key image reuse to a worker/runtime build fingerprint or version and rebuild when it differs.

  4. Make the wall-clock deadline independently bounded. waitForContainer starts docker stop at the deadline but swallows its failure and resolves only when docker wait closes (thread-container.ts:518-540). If stop fails or hangs while wait remains open, the run can wait indefinitely and never reach the cleanup finally block. Bound the Docker subprocesses and deadline settlement independently, attempt cleanup, and explicitly report cleanup failures; cover a failed/hung stop with a still-pending wait.

These are source-review findings, not newly executed reproductions. No code changes were made during this follow-up review.

Copy link
Copy Markdown
Collaborator Author

All four findings are fixed in 4bfd03c, each with the coverage the review asked for.

1. Snapshot the thread checkout. The service now resolves the checkout through resolveThreadExecutionContext — the cold resolver long-task-wake.ts and ci-watch-service.ts already inject — rather than getProjectRoot. A broken worktree fails the arming instead of falling back to the project root, and a root git cannot snapshot is refused up front with a readable reason rather than a raw git rev-parse failure after the image build. start became async, so it claims the thread's slot before its first await (two clicks cannot start two containers) and releases it when resolution fails. The record and the dialog now name which checkout ran. Test: a real project checkout plus a real linked worktree at a different commit with its own uncommitted edits, asserting the worktree is what gets carried in; plus a broken-resolver case and a non-git case.

2. No successful completion when retrieval or teardown fails. carryOutRef became carryOut { expected, ref, error }, and the record gained cleanupError; judgeRun decides the phase from the whole record. Commits produced but not fetched, a container that would not stop or reap, and a leaked secret canary all keep a run out of finished — as the failure reason, with cleanup problems also listed as warnings in the dialog. The banner now reads "made but NOT fetched" rather than "back" when there is no ref. Tests cover the failed-fetch and failed-teardown records.

3. Image invalidation. Reuse is keyed to a fingerprint of the guest bundle, the Dockerfile, the entrypoint, the worker uid and the sandbox-runtime version, written as a dev.copse.worker-fingerprint image label and compared before reuse, so an app upgrade rebuilds instead of running the previous worker. Verified on a real build; test covers the fingerprint changing with the bundle and with the base image.

4. Independently bounded deadline. docker stop now has its own timeout, and a bounded grace period settles the wait either way, so a failed or hung stop with a still-pending wait can no longer leave the run pending and skip its cleanup block. The abandoned wait is killed, the failure is reported as cleanupError instead of swallowed, and docker rm --force in teardown remains the next line of defence. Both Docker calls are injectable; tests cover a rejecting stop with a hung wait, a successful stop with a hung wait, and a wait that cannot spawn.

Validation: pnpm run check green (8305 unit tests), the Docker integration test re-run green against a real daemon with a rebuilt image, and the browser-tier screenshots refreshed (the result view now shows the checkout row).

These are fixes to the findings only — the readiness gaps you listed earlier (real-provider execution, deferred-action approval/replay, orphan reconciliation/TTL) are unchanged and still recorded as not-in-this-PR in docs/plans/thread-in-container.md.


Generated by Claude Code

github-actions Bot added a commit that referenced this pull request Sep 5, 2026
github-actions Bot added a commit that referenced this pull request Sep 5, 2026

Copy link
Copy Markdown
Collaborator Author

Followed up on 5ddeb89 (the stricter judgeRun). Two things.

What I verified. The new containerExit !== 0 rule depends on a clean guest actually exiting 0, which no unit test can establish and which CI never exercises — the Docker integration test is opt-in. I ran waitForContainer against real containers on a live daemon:

guest waitForContainer
finishes normally (main() resolves, as worker-entry.ts does) { exit: 0, timedOut: false }
fails (main().catch sets exitCode = 1) { exit: 1, timedOut: false }
outlives its budget, stopped by the host { exit: 137, timedOut: true }

So the happy path still judges clean, and the two failure shapes are now caught. The entrypoint is exec node /app/worker.cjs, so the container's status is the worker's own — nothing in between can launder a non-zero exit into a zero.

One ordering problem, fixed in d0e9813. The canary is pushed onto warnings before the cleanup branch is tested, so a run that both leaked the canary and failed to reap its container reported

The container could not be removed; remove it by hand before the next run.

as the failure, with the leak demoted to a warning underneath. A container left behind is a chore; a credential that escaped is an incident. The canary verdict now sits above the cleanup branch (still in warnings too, so the dialog's warning strip is unchanged), with a test for the combined case. pnpm run check green.

What I could not check. The full Docker integration test cannot run in this session: the network policy blocks deb.debian.org, so the worker image's apt-get layer (bubblewrap, git, ripgrep, socat) will not build here, through the proxy or the gcr mirror. The last real end-to-end run was against 4bfd03c; the judgeRun changes since then are covered by unit tests and the container-level check above, not by a full run.


Generated by Claude Code

@jonathanKingston

Copy link
Copy Markdown
Collaborator Author

Current-head validation is green and current with main: full CI passes, the diff is whitespace-clean, and the four concrete source-review findings recorded above are addressed. This still should remain a prototype draft. The remaining gates are product/security evidence, not obvious code nits: a real-provider end-to-end run, deferred-action approval/replay, and startup orphan reconciliation/runtime TTL. The existing plan and this PR already track those gaps; a new issue would only be useful if this prototype is split into independently owned implementation slices.

@jonathanKingston
jonathanKingston force-pushed the claude/copse-container-execution-nw4ln8 branch from d0e9813 to cf22cbb Compare September 6, 2026 09:19
github-actions Bot added a commit that referenced this pull request Sep 6, 2026
@jonathanKingston jonathanKingston added the ci-full Force the full e2e suite (bypass the test-oracle thinning) on this PR label Sep 6, 2026 — with Claude
@jonathanKingston
jonathanKingston force-pushed the claude/copse-container-execution-nw4ln8 branch from cf22cbb to 3ad842a Compare September 6, 2026 11:28
github-actions Bot added a commit that referenced this pull request Sep 6, 2026
claude and others added 8 commits September 6, 2026 11:41
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
@jonathanKingston
jonathanKingston force-pushed the claude/copse-container-execution-nw4ln8 branch from 3ad842a to 59e0bc0 Compare September 6, 2026 11:49
github-actions Bot added a commit that referenced this pull request Sep 6, 2026
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
github-actions Bot added a commit that referenced this pull request Sep 9, 2026
…up continue from the card alone

Two more from the follow-ups tried after the fixes:

To the thread: the turn had history now, but Codex asked what "try
again" meant. An ACP agent's turn is replayed the thread as text, and
the run's turn in the history was a tool call with no assistant text.
The turn recorded for a settled run now carries the review record as
the assistant's own message as well as the tool result.

To the container: still refused. The continuation could answer only
from the run the main process held or its record on disk, and a record
the sweep removed, or one an earlier build wrote under the guest's own
thread id, left it nothing. The thread's card says what the run was
asked and reported and where its commits are, so the follow-up sends
that along and the continuation falls back to it when the record is
unusable; the card's arguments gain the agent's last words for it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
claude and others added 2 commits September 9, 2026 01:00
…file

A real run's install died with EROFS on a file copy into the checkout,
the worker went on regardless, and the sign-in restore then died with
ENOENT on a mkdir under the home; the host could not remove the
container either. The volume had gone from under the container (the
disk Docker keeps its volumes on full, or the mount lost), so every path
under /workspace fell through to the read-only rootfs, and neither error
named that. The worker now probes the checkout's volume after a failed
install, refuses to go on when it no longer takes writes, and says the
same beside any filesystem error its fatal handler sees.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
Jonathan Kingston and others added 3 commits September 9, 2026 03:03
…default

The feature is reachable from every thread's composer and needs Docker,
copies a sign-in into the guest when asked, and has fixed something on
each real run so far. It joins the other opt-in features under
Settings › Experimental: a containerRunsEnabled setting, off by default,
with a toggle that says what a run does and what it carries. The run
service refuses to start anything while it is off, whatever the renderer
asks; the composer's menu entry and its target picker appear only once
it is on. The demo scenarios and the e2e spec that exercise the feature
turn it on in their seeds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzkY3kYdcQvYk8EH3EW8uv
github-actions Bot added a commit that referenced this pull request Sep 9, 2026
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
@jonathanKingston
jonathanKingston marked this pull request as ready for review September 9, 2026 02:38
claude and others added 5 commits September 9, 2026 03:09
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-full Force the full e2e suite (bypass the test-oracle thinning) on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants