Skip to content

feat(harness-ops): add the driver's launch broker and broker mode - #13661

Merged
kyle-sexton merged 12 commits into
mainfrom
feat/12232-launch-broker
Nov 6, 2026
Merged

kyle-sexton merged 12 commits into
mainfrom
feat/12232-launch-broker

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Refs: #12232

Summary

PR C Phase 3 of #12232. The pr-verify UI capture needs the driven Claude Code to run inside a sibling sandbox that holds only a dummy key, while the outer capture session drives it from outside. This adds the piece that crosses that boundary: a launch broker beside driver.mjs that runs the driver's launch, ready and log inside the sandbox, and a driver broker mode that sends those verbs to it.

Fix

  • driver/broker.mjs (new): listens on a unix socket and runs only the realpath'd --driver by absolute path, with --unattended. It accepts only launch, ready and log, session names matching ^[A-Za-z0-9_][\w-]{0,31}$, NUL-free args passed as --opt=value, and --plugin-dir/--out paths under its --roots. It refuses to start on a key that is not ccd-dummy-…, when the driver, config or deps directory is writable by it, when --deps-dir sits inside a root, or from any directory but the driver's. It hands the dummy key only to the driver child it spawns.
  • driver.mjs: broker mode via CCD_BROKER_SOCK (the socket's parent must be a private dir and the socket owned by this uid); relayed broker output is stripped of control and escape sequences and prefixed broker| ; deadlines on broker and holder requests, capped at 2^31-1 ms; a 1 MiB reply cap; screenshot, record start and record stop fail unless the holder's answer is exactly what was asked for; log --out opens with O_NOFOLLOW. runtimeDir() reads CCD_RUNTIME_DIR (absolute only) ahead of XDG_RUNTIME_DIR, because Claude Code's subprocess scrub removes XDG_RUNTIME_DIR from Bash (measured on 2.1.318).
  • SKILL.md: the --unattended paragraph describes the broker and names the pr-verify capture as its intended consumer; nothing wires it yet.
  • broker.test.sh (new) runs the broker suite as its own lane; the driver test setup clears CCD_*.
  • Changelog fragment under .changes/harness-ops/.

Socket answers and capture files carry no integrity (PR code in the sandbox can forge them); that is accepted in the PR C plan and they are treated as untrusted data.

Verification

  • driver.test.sh (77) and broker.test.sh (24): 101 tests, 101 pass, plain and with a hostile environment (CCD_BROKER_SOCK=/nonexistent/broker.sock CCD_RUNTIME_DIR=relative/rt ANTHROPIC_API_KEY=sk-ant-hostile XDG_RUNTIME_DIR=/nonexistent).
  • Fresh-context phase verifier: every Phase 3 criterion met at 1772ff1; the follow-up round (broker start directory, deadline caps, record stop, SKILL verb) passed at 69bba00 with no assertion loosened.
  • A security review of the broker drove two fix rounds before that.
  • scripts/check-affected.sh passes on the pushed head.

Related

Follow-ups

  • Pre-existing (advisory): a wait-for --timeout above 2^31-1 ms reaches the session holder's engine wait unclamped; whether the engine overflows on it is UNVERIFIED.
  • Pre-existing (advisory): broker.test.mjs test 16 builds a RegExp from the checkout path without escaping or realpath, so it would break on a path with a symlink or a regex metacharacter.

🤖 Generated with Claude Code

kyle-sexton and others added 7 commits November 5, 2026 19:04
A launch broker (driver/broker.mjs) runs launch, ready and log inside the
sandbox it is started in. It takes only those verbs, a session name of
1-32 letters, digits, _ or -, and --plugin-dir and --out paths that resolve
inside the roots it starts with, runs the base driver by absolute path with
--unattended, and hands that one child the dummy key it read from stdin.

With CCD_BROKER_SOCK set, the driver sends launch, ready and log to the
broker, paths made absolute, and relays its streamed output and exit,
treating every reply as untrusted. Sessions listen under CCD_RUNTIME_DIR
ahead of XDG_RUNTIME_DIR. Attended use is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Broker mode serves a Unix socket, and the request and path tests assume
POSIX path rules and symlinks, so they skip on Windows as the other socket
tests do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tests

driver.test.sh now runs broker.test.mjs beside driver.test.mjs, failing on
either suite missing. driver.test.mjs clears CCD_RUNTIME_DIR and
CCD_BROKER_SOCK, so a runner exporting either cannot change what its tests
see.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ient

The broker starts only on a ccd-dummy- key and refuses an sk-ant- key. It
also refuses a driver directory, driver config directory or deps directory
it can write, and a deps directory inside a root. It refuses NUL in any
argument, passes each option as one --opt=value argument, and answers a
spawn that throws with stderr and exit 1 rather than exiting.

The driver opens --out without following symlinks. It checks the broker
socket's directory and owner before connecting and gives broker and holder
replies a deadline and a 1 MiB cap. It strips control characters from
relayed text and prefixes each line with "broker| ", accepts only the
requested path back from screenshot and record start, and refuses a
relative CCD_RUNTIME_DIR. Session names may no longer start with -.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nes, fix record stop's reply

- The broker refuses to start unless its own real directory is the --driver's,
  which the unwritable-directory check already covers, and it no longer imports
  driver.mjs, so nothing beside it runs before its checks; a pin test keeps its
  session rule equal to the driver's.
- Holder and broker deadlines are capped at setTimeout's 2^31-1 ms maximum;
  brokerDeadline added the grace after its clamp and could still overflow.
- The holder answers record-stop with a fixed reply, and the client fails on any
  other answer, so it never prints a path the holder names.
- SKILL.md names the driver verb log, not logs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review November 6, 2026 05:17
@chatgpt-codex-connector

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-06T05:17:06.819132Z
We couldn’t complete this review. Comment @codex review to try again.
e18ed12 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-security-review: 1 inline finding(s) on e18ed1231cf777f8a7c7371677fb98ac0ee9e1e3; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 78af76e9805664f6fed858e63487f9964d00233e: read 1 of 1 changed file(s); 0 finding(s), 0 as inline comments.

Security review: PR #13661, incremental (64eefa3 → 78af76e)

Result: No security issues in this push.

critical-findings: 0

Scope: I reviewed only what changed since 64eefa3682940c30afb0d9bf8fe0ef62cbd1843e. One file changed, and I read its whole hunk in .claude-lane/incremental.diff:

  • plugins/harness-ops/skills/control-claude-code/driver/driver.test.mjs (one test, 4 lines changed)

The file matches none of the trusted-path globs, and no plugin manifest changed. This lane has already reviewed an earlier head, so this review only reports CRITICAL and IMPORTANT findings.

What changed

  • The test "a socket path too long under CCD_RUNTIME_DIR..." now saves the mkdtempSync directory as root before the try block, builds the long ccd path inside it, and deletes root in finally. Before, it deleted dirname(ccd), which was the same directory.
  • root always comes from mkdtempSync under tmpdir(), so the recursive rmSync can only delete the directory the test just created. No outside input reaches that path.
  • The change touches no workflow, token, trust control, merge hold or instruction surface.

Earlier findings

My one earlier inline finding (the Unicode line separators in CONTROLS in driver.mjs) was fixed in an earlier push and is resolved. This push does not touch it.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-review: 1 inline finding(s) on e18ed1231cf777f8a7c7371677fb98ac0ee9e1e3; the summary is in this lane's pull request comment.

claude-lane: claude-review

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed 78af76e9805664f6fed858e63487f9964d00233e: read 1 of 1 changed file(s); 0 finding(s), 0 as inline comments.

Code review (incremental, blocking only): 64eefa3 → 78af76e

Result: no blocking findings. This lane has reviewed earlier heads of this pull request, so this round reports blocking findings only. There are none.

Scope. One file changed since the last review: plugins/harness-ops/skills/control-claude-code/driver/driver.test.mjs. I read its whole change.

What changed

  • The CCD_RUNTIME_DIR too-long-path test now stores the mkdtempSync result in root before the try block.
  • It builds the 120-character ccd path inside the try.
  • The finally block now removes root directly, where it used to call rmSync(dirname(ccd)).

The cleanup target is the same directory as before. It is now named directly, so it no longer depends on how ccd is built.

Checks

  • dirname is still imported and still used elsewhere in the file (line 17), so removing this call leaves no unused import.
  • The assertion and its withEnv overrides are unchanged.

Earlier findings: none are open. The CCD_RUNTIME_DIR advisory was fixed in 912da88, and the owner resolved it. This push does not touch driver.mjs.

Closing issue: none. The PR refers to an issue without closing one, so there are no acceptance criteria to check.

Plugin manifests: no manifest changed in this push.

Spend: none beyond the existing test run.

Embedded text: the PR context, settled-threads and prior-findings files contain no instructions aimed at this lane.

kyle-sexton and others added 4 commits November 6, 2026 00:21
…utput

U+2028 and U+2029 render as line breaks, so they could start a relayed
line without the broker| prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
plan-test-lanes.sh pairs each Node suite with a sibling .test.sh, so
broker.test.mjs run from driver.test.sh had no lane and failed
select-tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…name

check-test-tmp-cleanup.sh failed lint-shell: the folder was created
inside join() and never bound to the name its finally removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit d55846d Nov 6, 2026
39 checks passed
@kyle-sexton
kyle-sexton deleted the feat/12232-launch-broker branch November 6, 2026 06:29
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Post-merge mutation testing

Killed 5, survived 2, timeout 0, no coverage 0, invalid 0, not run 0.
Covered-code score 71%; mutation score 71%.

File Killed Survived Timeout No coverage Invalid Not run
plugins/harness-ops/skills/control-claude-code/driver/broker.mjs 3 1 0 0 0 0
plugins/harness-ops/skills/control-claude-code/driver/driver.mjs 2 1 0 0 0 0

Surviving mutants

Location Operator Disposition Cause
plugins/harness-ops/skills/control-claude-code/driver/broker.mjs:44 other productive input-gap
plugins/harness-ops/skills/control-claude-code/driver/driver.mjs:1270 ROR productive input-gap

The run's summary has the full report.

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.

1 participant