Skip to content

CONTRACT: fix(backends): use the host app exec wrapper - #2397

Merged
schlessera merged 9 commits into
mainfrom
fix/1429-host-exec-k6w3
Oct 22, 2026
Merged

schlessera merged 9 commits into
mainfrom
fix/1429-host-exec-k6w3

Conversation

@schlessera

@schlessera schlessera commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Refs #1429

What changed

Each app passes its resolved exec configuration through startup probes, backend construction, Claude per-turn probes and SDK workers, and Pi grep/bash, including the tools inside Pi workers. Host A/C stays authoritative over ambient B. Explicit {} disables the optional wrapper. Omitted standalone options keep the late ambient fallback, which Pi resolves once when each worker starts.

This head also redoes the worker-boundary corrections from the unpublished d6e1e889 candidate, which this session could not recover:

  • Helper visibility. Pi binds each configured wrapper and killer file read-only into ordinary (governed/strict) and restricted worker views, never the directory holding it. A regular-file symlink alias keeps working.
  • Ordering. Pi selects and validates the exec before any relay, reservation or worker is acquired. A relative ambient wrapper therefore no longer starts the restricted relay.
  • Refusal. A wrapper or killer that is not an absolute path to a regular file is refused before any relay or worker. That covers a directory and a symlink to a directory. The ordinary Claude worker applies the same check to its wrapper, so a directory is never bound into a worker below its masks. The shared check is workerExecHelpers, exported from ui-sdk/internal.

Why this shape

The existing backend context and factory options carry the additive configuration (#1429, #1343). No new seam is added. Helpers go through the launcher's existing readPaths handling, so the mask and alias behavior is the one runtime paths already have. Host-owned helper targets must stay stable through launch; no TOCTOU guarantee is claimed. Permissions, provider history, argv semantics, private masks and deadlines are unchanged. History is preserved: this branch merges origin/main normally on top of the published head f462a8f84, without a rebase or force-push. That merge includes #2377.

Proof

Base aa8c8966b (origin/main), head f5684cedd.

New failing-first receipt. packages/ui-backend-pi/tests/worker-host-exec.test.ts launches real bubblewrap workers in the governed, strict and restricted profiles. Each worker runs the actual worker-local grep/bash tools through a masked, aliased wrapper and cancels through the killer. The file also has 10 refusal cases. It was run against the published worker-session.ts with only the tests added:

  • 12 intended reds:
    • governed/strict/restricted each failed HOST_HELPER_VISIBLE_IN_WORKER with ENOENT … posix_spawn '…/private/links/wrap'.
    • 8 directory and symlink-directory cases (wrapper and killer, ordinary and restricted) received worker launched instead of the refusal.
    • The restricted relative-ambient case failed NO_RELAY_BEFORE_HELPER_REFUSAL (1 envelope call).
  • 1 control passed both before and after: the ordinary relative-ambient refusal.
  • Fixed: 13 pass / 54 assertions.

Mutations. Each was applied, ran red on the named assertion, then was restored byte-for-byte:

Mutation Result
Drop helpers from the ordinary sandbox readPaths governed+strict fail HOST_HELPER_VISIBLE_IN_WORKER
Drop helpers from the restricted readPaths restricted fails HOST_HELPER_VISIBLE_IN_WORKER
Bind the helpers' grandparent directory instead of the files governed+strict fail HELPER_SIBLINGS_STAY_MASKED
Remove the Claude ordinary wrapper check fails CLAUDE_WORKER_WRAPPER_REGULAR
Remove host propagation at the Pi init call site (exec = ambientExecConfig()) fails PI_WORKER_HOST_EXEC
Remove host propagation at the Claude SDK call site fails SDK_WORKER_HOST_ARGV

Suites.

  • bun run test packages/ui-backend-claude packages/ui-backend-pi: 1173 pass / 0 fail / 5817 assertions on f5684cedd.
  • tsc --noEmit and bun run lint are clean.
  • bun run api-report: the only delta is the new internal export workerExecHelpers in ui-sdk.txt. The three optional public exec fields are unchanged from the original candidate.
  • tests/decision-citations.test.ts: 44 pass after the citation reconciliation.
bun run check:pr --base origin/main

Default local preflight on f5684cedd, three runs:

  1. Hit my own outer 3,500 s shell timeout during feedback (exit 124; feedback child 137) on a host running four concurrent preflights. Fast invariants were 744 pass / 0 fail, and no test failure was recorded. This is a harness cutoff, not a gate result.

  2. Lint/leakage, environment, changeset and fast invariants passed (744 / 0). Import-graph feedback (296 files) finished 3,501 pass / 2 fail. Both failures were in ui-server/tests/brain-client.test.ts: the combined process hit maintenance admission state (EPERM fchown and temporary storage is busy). The file passes alone (35 / 0) and paired with sync-runtime (46 / 0). Filed as ui-server: brain-client version-pin and wrapper cases fail in combined feedback on maintenance admission state #2655; this diff does not touch that code.

  3. Feedback finished 3,502 pass / 1 fail: hygiene-review.test.ts "Actions production cap…" overran its 30 s deadline (32.8 s). It passes alone (36 / 0). Recorded on ui-server: maintenance release reports missing fixture state after refresh timeout #2412, which covers the same overrun class.

  4. Passed (exit 0): lint/leakage, environment, changeset, 744 fast invariants / 0 fail, and import-graph feedback 3,503 pass / 0 fail (296 files).

No deadline, guard or test was weakened between runs, and the head did not change.

History. The earlier receipts keep their own identities. The original f462 preflight failed (2 native exit-137), and d6e1e889's R1/R2 and draft-runtime setup-hook receipts belong to that unpublished tree. None of them is transferred to this head.

Checklist

  • Focused local preflight/behavioral receipts and complete selected hosted proof passed. Local preflight passed on f5684cedd (run 4). Hosted ready-candidate proof is pending (the coordinator owns confirmation); draft gates are green on this head.
  • Acceptance criteria are met. Implementation is complete; required hosted verification is still outstanding.
  • Milestone 0.44.0 is on the PR and backends: resolve the exec wrapper from the host's app config instead of process.env #1429 (read back below). It must be re-verified before merge.
  • Changeset: .changeset/host-backend-exec.md, a four-package minor, updated for the helper behavior.
  • No personal data; the leakage gate is part of check:pr.
  • Integration contract: the package-api.md component describes helper visibility and refusal, and the index entry is retained. The commit is prefixed CONTRACT:.
  • Docs: citations moved by this change are updated (worker-spawn.ts, exec-wrapper.ts, worker-session.ts, app.ts, agent/backend.ts, backend-module.ts).
  • Provider history: no provider-facing text changed, and the sandbox note does not list read paths. Both provider-history suites are inside the backend run above.

Anything left open

Complete selected hosted proof (all six categories plus pack qualification) on the matching head, ready-candidate confirmation and merge. These are coordinator-owned.

🤖 Generated with Claude Code

Carry optional host exec through startup and per-turn probes, backend construction, Claude SDK spawning and Pi worker-local tools. Preserve explicit-empty and omitted-host compatibility. Add keyless runtime and propagation mutation coverage; include the additive contract, minor changeset, API report and moved citations.
…ions

Keep explicitly supplied host configuration copied per backend, including empty configuration. Preserve omission through standalone construction and resolve the host fallback for each Pi worker turn. Add intended-red controls for ambient changes after construction and correct the additive contract and changeset wording.
@schlessera schlessera added this to the 0.44.0 milestone Oct 18, 2026
@schlessera schlessera added type: fix Something is broken and must behave the way it is documented. area: ui-sdk Wire protocol, runtime schemas, the server and client SDK surfaces. area: ui-server Hono app factory: turn coordinator, auth, session catalog, routes, cron. area: backends AgentBackend implementations and the seam they share. contract Touches the integration contract: CONTRACT: commit, contract doc updated in the same commit. security Auth, isolation, egress, secrets, or anything in the threat model. labels Oct 18, 2026
schlessera and others added 3 commits October 22, 2026 11:43
Reconciles moved app.ts, agent/backend.ts and backend-module.ts citations.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pi serializes the host exec into worker-local grep/bash, so the wrapper and
killer must exist inside the worker's view. Bind each configured helper file
read-only into ordinary and restricted views, never its directory, and select
the exec once per worker before any relay, reservation or worker is acquired.
Refuse a wrapper or killer that is not an absolute regular file (directly or
through a symlink), and refuse a nonregular wrapper for an ordinary Claude
worker, so a directory is never bound into a worker below its masks.

Real bubblewrap governed/strict/restricted workers run the real worker-local
tools through a masked, aliased wrapper and cancel through the killer.

Refs #1429

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Includes #2377. Reconciles moved app.ts citations.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: backends AgentBackend implementations and the seam they share. area: ui-sdk Wire protocol, runtime schemas, the server and client SDK surfaces. area: ui-server Hono app factory: turn coordinator, auth, session catalog, routes, cron. contract Touches the integration contract: CONTRACT: commit, contract doc updated in the same commit. security Auth, isolation, egress, secrets, or anything in the threat model. type: fix Something is broken and must behave the way it is documented.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant