@cat-factory/executor-harness 1.132.3 → 1.135.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/agent-capabilities.d.ts +21 -24
- package/dist/agent-capabilities.js +22 -50
- package/dist/agent-env.d.ts +17 -0
- package/dist/agent-env.js +47 -0
- package/dist/agent-runner.d.ts +18 -2
- package/dist/agent-runner.js +29 -231
- package/dist/agent-shared.d.ts +14 -5
- package/dist/agent-shared.js +14 -5
- package/dist/agent.d.ts +0 -11
- package/dist/agent.js +7 -138
- package/dist/captured-command.d.ts +1 -1
- package/dist/captured-command.js +3 -2
- package/dist/claude-cli.d.ts +90 -0
- package/dist/claude-cli.js +181 -0
- package/dist/claude-home.d.ts +41 -0
- package/dist/claude-home.js +159 -0
- package/dist/coding-agent.d.ts +35 -0
- package/dist/coding-agent.js +213 -41
- package/dist/docker-status.d.ts +89 -0
- package/dist/docker-status.js +147 -0
- package/dist/frontend-infra.js +4 -3
- package/dist/git.d.ts +48 -5
- package/dist/git.js +93 -26
- package/dist/guard-driver.d.ts +71 -0
- package/dist/guard-driver.js +171 -0
- package/dist/harness-server.js +13 -0
- package/dist/infra-standup.d.ts +69 -0
- package/dist/infra-standup.js +182 -0
- package/dist/job.d.ts +10 -0
- package/dist/multi-repo-coding.d.ts +17 -0
- package/dist/multi-repo-coding.js +61 -16
- package/dist/pi-workspace.d.ts +11 -0
- package/dist/pi-workspace.js +126 -57
- package/dist/pi.d.ts +8 -0
- package/dist/pi.js +16 -9
- package/dist/progress-guard.d.ts +56 -10
- package/dist/progress-guard.js +84 -22
- package/dist/runner.d.ts +1 -1
- package/dist/salvage.d.ts +180 -0
- package/dist/salvage.js +289 -0
- package/dist/workspace-probe.d.ts +85 -0
- package/dist/workspace-probe.js +124 -0
- package/package.json +4 -4
- package/src/agent-capabilities.ts +25 -51
- package/src/agent-env.ts +49 -0
- package/src/agent-runner.ts +40 -267
- package/src/agent-shared.ts +16 -5
- package/src/agent.ts +7 -164
- package/src/captured-command.ts +3 -2
- package/src/claude-cli.ts +217 -0
- package/src/claude-home.ts +233 -0
- package/src/coding-agent.ts +252 -44
- package/src/docker-status.ts +201 -0
- package/src/frontend-infra.ts +4 -3
- package/src/git.ts +104 -26
- package/src/guard-driver.ts +203 -0
- package/src/harness-server.ts +13 -0
- package/src/infra-standup.ts +218 -0
- package/src/job.ts +10 -0
- package/src/multi-repo-coding.ts +65 -16
- package/src/pi-workspace.ts +161 -57
- package/src/pi.ts +27 -12
- package/src/progress-guard.ts +110 -34
- package/src/runner.ts +1 -1
- package/src/salvage.ts +407 -0
- package/src/workspace-probe.ts +155 -0
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ accepts the job and returns immediately with a `jobId`; the driver then polls
|
|
|
14
14
|
- [Job protocol](#job-protocol)
|
|
15
15
|
- [What a job does](#what-a-job-does)
|
|
16
16
|
- [No secrets in the image](#no-secrets-in-the-image)
|
|
17
|
+
- [Local infra: the container's Docker daemon](#local-infra-the-containers-docker-daemon)
|
|
17
18
|
- [Layout](#layout)
|
|
18
19
|
- [Runner lifecycle knobs](#runner-lifecycle-knobs)
|
|
19
20
|
- [Build / test](#build--test)
|
|
@@ -316,6 +317,44 @@ signed, model-locked LLM-proxy **session token** in the request body. Pi reaches
|
|
|
316
317
|
models only through the Worker proxy, which injects the real provider key (qwen /
|
|
317
318
|
Kimi / DeepSeek) and meters spend. The provider key never enters the container.
|
|
318
319
|
|
|
320
|
+
## Local infra: the container's Docker daemon
|
|
321
|
+
|
|
322
|
+
The Tester's local-mode infra stand-up runs `docker compose up --wait` INSIDE this container, so
|
|
323
|
+
the container needs a daemon of its own. It runs rootless, as the unprivileged `harness` user:
|
|
324
|
+
Cloudflare Containers (and most managed runners) give no root and no privileged mode, and a host
|
|
325
|
+
Docker socket would hand the container root on the host.
|
|
326
|
+
|
|
327
|
+
`entrypoint.sh` starts it, waits for it in the background, and RECORDS the verdict
|
|
328
|
+
(`src/docker-status.ts`). Two things consume that record and nothing else does:
|
|
329
|
+
|
|
330
|
+
- `GET /health` reports it, so an operator (and a boot-time probe) can see what the container
|
|
331
|
+
concluded about itself.
|
|
332
|
+
- The compose stand-up REFUSES on a decided absence and says why, instead of running compose
|
|
333
|
+
against nothing and handing the agent a connection error to interpret. The refusal rides back on
|
|
334
|
+
the Tester step as `infraSetup.dockerAvailable: false` with the cause.
|
|
335
|
+
|
|
336
|
+
The verdict is three-valued, and that is the point. `false` is a decided absence. `undefined` is
|
|
337
|
+
"nothing decided" — the probe is still in flight, or nothing recorded anything at all, which is the
|
|
338
|
+
normal state under the native host transport (`LOCAL_NATIVE_AGENTS`) where the harness runs on a
|
|
339
|
+
developer's machine with no entrypoint. Undecided attempts the stand-up; only a decided absence
|
|
340
|
+
refuses it.
|
|
341
|
+
|
|
342
|
+
What is recorded describes BOOT, and a container outlives its boot: a warm pool serves many jobs
|
|
343
|
+
from one, and a sidecar daemon that took longer to come up than the entrypoint's bounded wait
|
|
344
|
+
allows is serving perfectly well by the second job. So a recorded absence is a hypothesis, not the
|
|
345
|
+
refusal: `resolveDockerVerdict` re-checks it against a live daemon at the moment a stand-up is
|
|
346
|
+
about to run, and the record supplies what only the record holds, the cause and the daemon's own
|
|
347
|
+
log tail. `GET /health` deliberately keeps reporting the boot record rather than probing per poll,
|
|
348
|
+
since it is not the surface that acts on the answer.
|
|
349
|
+
|
|
350
|
+
Why it is written down at all: the image shipped for months with `docker-ce-rootless-extras` (the
|
|
351
|
+
wrappers that START a daemon) and no `docker-ce` (the daemon itself), and no `iproute2` for the
|
|
352
|
+
network rootlesskit builds. The entrypoint backgrounded the start in a subshell where its exit
|
|
353
|
+
status could not be observed, so every local-infra Tester run degraded silently to a no-infra run
|
|
354
|
+
whose only trace was a compose error in a prompt note. A capability that reports itself present and
|
|
355
|
+
then degrades in silence is worse than one that is absent, so the daemon is installed AND the
|
|
356
|
+
verdict is stated.
|
|
357
|
+
|
|
319
358
|
## Layout
|
|
320
359
|
|
|
321
360
|
| File | Responsibility |
|
|
@@ -329,11 +368,17 @@ Kimi / DeepSeek) and meters spend. The provider key never enters the container.
|
|
|
329
368
|
| `src/pi-reduction.ts` | Reducing a Pi event stream to what the run PRODUCED (summary, stats, diagnostics, terminal failure), FOLDED as records stream rather than over a retained array — memory is O(largest record), not O(records). The array-taking entry points offline tooling uses are defined in terms of the same reducer. |
|
|
330
369
|
| `src/tool-silence.ts` | The tool-silence watchdog (F13) and the `ToolProgressWindow` an agent stream opens, beats and closes. Separate from the phase marker on purpose: a window is only meaningful while something able to reset it is running. |
|
|
331
370
|
| `src/git.ts` | clone / branch / commit / push (lease-guarded: [The work-branch push is CHECKPOINTED, so it is lease-guarded](#the-work-branch-push-is-checkpointed-so-it-is-lease-guarded)) + GitHub PR creation; bootstrap history reset + force-push. |
|
|
371
|
+
| `src/progress-guard.ts` | The live anti-rabbithole bounds every agent run is held to, plus the tool-name vocabulary they classify calls with. PURE and SYNCHRONOUS: it spawns nothing and reads nothing off disk, so it can be driven over a fixed event sequence in a unit test. Shared by both runners, because two copies of a bound are two bounds. |
|
|
372
|
+
| `src/workspace-probe.ts` | The working-tree answer to "has this run actually changed the repository": a dirty tree, or HEAD moved off the sha the pass began at. What the no-edit bound decides on, since the tool names it can see are a fact about which tool the model picked and not about the repo (an agent writing everything through `bash` heredocs read as making no edits at all). Gitignored paths are excluded by git, which is what keeps a dependency install from reading as progress. Order carries the "is this a repository at all" question: the status runs first and is never caught, while a missing HEAD is the from-scratch case rather than a failure. A run whose cwd is a workspace of sibling checkouts composes one probe over them, where a checkout that could not be probed makes the answer inconclusive rather than clean. |
|
|
373
|
+
| `src/guard-driver.ts` | The bridge between the synchronous guard and the async evidence one of its bounds needs. Both runners feed the guard from a sync stream handler, so the driver owns the probe's lifetime: at most one probe per run, a positive answer satisfying the bound permanently, a negative one aborting with the evidence quoted, and a THROWN one inconclusive (re-arm and warn, never kill). Also hosts the claude-code stream's tool_use/tool_result pairing. |
|
|
374
|
+
| `src/salvage.ts` | Committing the new, untracked files an agent left behind, under a dependency/build deny-list and file-count + byte bounds that refuse ALL-or-nothing rather than truncating. Coding modes only. A credential-bearing name (`.env`, a private key, `.npmrc`) is a THIRD disposition, not a fourth junk entry: it is withheld like the rest but NAMED on the outcome, because for a secret the deny-list's usual trade inverts (a missed file is recoverable, a leaked key is not) and someone has to decide whether to rotate it. Every message here states the salvage's own provenance: a commit arriving with no explanation is indistinguishable from work someone chose to keep, and nobody chose this. |
|
|
332
375
|
| `src/bootstrap.ts` | The `/bootstrap` handler (clone-or-empty → adapt → reinit + force-push). |
|
|
333
376
|
| `src/blueprint.ts` | The `/blueprint` handler (decompose → render `blueprints/` → commit on branch). |
|
|
334
377
|
| `src/embed.ts` | Bundled assets/templates written into the workspace. |
|
|
335
378
|
| `src/package-registries.ts` | Private-registry (npm) auth: renders the job's allowlisted entries into an npmrc; the user `~/.npmrc` in a container, a per-job file pointed at by `npm_config_userconfig` for a native job. |
|
|
336
379
|
| `src/agent-runner.ts` | The subscription-harness runners (`runClaudeCode` / `runCodex`): talk direct to the vendor with a leased OAuth token, lift per-turn usage/telemetry off the CLI event stream. |
|
|
380
|
+
| `src/claude-cli.ts` | The claude-code CLI's INVOCATION surface: the built-in tools a run DECLARES with `--tools` (the CLI's headless default has no `Grep`/`Glob` and no plan tools, and does carry a dozen an ephemeral container can act on none of), the argv that declares them, and the read-back of the CLI's `init` event that warns when a required capability was granted no tool. The list is over-inclusive on purpose, which `--tools` makes safe: an unknown name is dropped silently and a RETIRED one is an alias onto its successor, so one pinned image can face several CLI versions. The web tools are unconditional, being served by the vendor the leased subscription pays rather than by this deployment's search proxy. The same list also rides the `--allowedTools` re-grant, which is ADDITIVE rather than inert, so the two are one value threaded rather than two lists. |
|
|
381
|
+
| `src/claude-home.ts` | The PER-RUN claude-code config home (`codex-home.ts`'s sibling): the isolated `CLAUDE_CONFIG_DIR` outside the checkout, its onboarding pre-seed, the run's natively-installed skills, its `--mcp-config`, the child env carrying the leased credential, and the teardown that lifts the session transcripts out before deleting it. |
|
|
337
382
|
| `src/claude-call-aggregator.ts` | Folds Claude Code's per-CONTENT-BLOCK `stream-json` envelopes back into the model calls they belong to (by `message.id`), reconstructs each call's request transcript, and routes subagent turns off the parent's chain. **Exported as the `./claude-call-aggregator` subpath and driven by the BACKEND too** (`runtimes/local`, for an inline step running on the developer's host `claude`), so it stays the ONE implementation: the per-envelope over-count it fixes inflated a measured 1.47M tokens to 5.53M, and both drivers have to learn that only once. That second driver is why the transcript is retained only to `MAX_TRANSCRIPT_CHARS` (stating what it stopped retaining) and why assembling bodies at all is a `bodies` switch: in a container the reconstruction is one job's memory in a box sized for it, in the backend it is per concurrent inline step in the orchestrator process. Unlike the compile-only `./embed`, this subpath is a `dist` import, which is why the package emits declarations, and why a consumer's typecheck depends on Turbo's `^build` edge having built this package first (see `tsconfig.json`'s `comment:buildOrder`). |
|
|
338
383
|
| `src/usage-attribution.ts` | Reconciles a subscription CLI's TWO token channels: the per-turn usage its stream narrates and the cumulative total its terminal event reports. They disagree routinely and in one direction (Claude Code's per-turn `output_tokens` is the message-START snapshot, single digits), so whatever the turns did not account for becomes ONE extra metric standing for the job (`standsForJob`, filed with a null turn index) rather than tokens grafted onto a real turn, which would make a derived number read as a measured one. Reconciled against the PARENT loop's calls alone, since the terminal cumulative covers only that conversation. |
|
|
339
384
|
| `src/transcript-retention.ts` | Lifts the CLI session transcripts (`projects/` / `sessions/`) out of the isolated, credential-bearing config home before it is deleted, and prunes them on a TTL (debugging artifact retention). |
|
|
@@ -350,6 +395,8 @@ Kimi / DeepSeek) and meters spend. The provider key never enters the container.
|
|
|
350
395
|
| `src/codex-images.ts` | Codex's own `image_gen` output, staged where the agent can reach it: creates `$CODEX_HOME/generated_images` as a symlink into `.cat-context/binary-output/generated/` before the CLI starts, sweeps anything a failed redirect left behind, and unlinks (never follows) the redirect at teardown — a failed unlink is REPORTED, because that unlink is what stops the recursive delete reaching the checkout. Exists because codex exposes no path for what it generated AND `$CODEX_HOME` holds the run's decrypted credential, so neither asking the agent nor sending it there is available. |
|
|
351
396
|
| `src/agent-shared.ts` | The few helpers every agent MODE shares (effort-report folding, the capability fields forwarded to `runAgentInWorkspace`). |
|
|
352
397
|
| `src/logger.ts` | Structured logging. |
|
|
398
|
+
| `src/docker-status.ts` | This container's own verdict about its Docker daemon, as recorded by `entrypoint.sh`. Three-valued on purpose: a daemon that FAILED and a daemon nobody asked about are different facts, and only a DECIDED absence refuses a stand-up. See [Local infra: the container's Docker daemon](#local-infra-the-containers-docker-daemon). |
|
|
399
|
+
| `src/agent-env.ts` | The env for anything the harness spawns into the agent's CHECKOUT: its own environment minus the variables that are facts about the HARNESS. Today that is `NODE_ENV` — the harness runs in production mode, and an inherited `NODE_ENV=production` makes npm omit devDependencies in a checkout that never asked for it. |
|
|
353
400
|
|
|
354
401
|
## Runner lifecycle knobs
|
|
355
402
|
|
|
@@ -373,6 +420,8 @@ runner):
|
|
|
373
420
|
| `REPRODUCTION_TOTAL_BUDGET_MS` | `2700000` (45m) | Wall-clock ceiling on the WHOLE proof phase (every attempt, both trees, setup included). Attempts multiply two full tree runs each and the heartbeat above deliberately stops the inactivity watchdog from firing, so this is what bounds the phase. Checked at phase boundaries; exceeding it settles `inconclusive`, never a run failure. |
|
|
374
421
|
| `HARNESS_TRANSCRIPT_TTL_MS` | `259200000` (3d) | How long lifted subscription-CLI session transcripts are kept before the retention sweep prunes them. |
|
|
375
422
|
| `HARNESS_TRANSCRIPT_ROOT` | `<tmpdir>/cf-agent-transcripts` | Where retained session transcripts are moved to (one dir per run). Meaningful only on a reused (warm-pool) container; a per-run container is torn down with the job. The TTL sweep deletes only dirs it created (each carries a `.cf-retained` marker), so pointing this at a shared directory never touches unrelated content, though a dedicated dir is still recommended. An override on a different filesystem than the config home falls back to copy-then-remove. |
|
|
423
|
+
| `HARNESS_DOCKER_READY_TIMEOUT_SECONDS` | `60` | How long `entrypoint.sh` waits for the container's Docker daemon before recording it unavailable. Only a HUNG daemon pays this in full: the wait ends early both when the socket answers and when the daemon process is gone. It runs in the BACKGROUND, so it never delays the container's boot. |
|
|
424
|
+
| `HARNESS_DOCKER_STATUS_FILE` | `/tmp/harness-docker-status.json` | Where that verdict is recorded. `entrypoint.sh` writes it and the harness reads it, so an override must be set for BOTH (they share one process env). |
|
|
376
425
|
|
|
377
426
|
## Build / test
|
|
378
427
|
|
|
@@ -173,39 +173,36 @@ export declare function parseMcpServerSpecs(value: unknown): McpServerSpec[] | u
|
|
|
173
173
|
export declare function claudeMcpConfig(servers: McpServerSpec[]): {
|
|
174
174
|
mcpServers: Record<string, Record<string, unknown>>;
|
|
175
175
|
};
|
|
176
|
-
/**
|
|
177
|
-
* The claude-code CLI's own tools, named so an `--allowedTools` list can never take them away.
|
|
178
|
-
*
|
|
179
|
-
* An allow-list is whole-session: it does not scope itself to MCP just because every entry we
|
|
180
|
-
* generate happens to be an `mcp__*` pattern. So the moment one tool server narrows its tools, the
|
|
181
|
-
* list has to re-grant the agent's built-in file/bash/search tools or the run is handed a narrowed
|
|
182
|
-
* MCP surface AND no way to read, edit or build anything.
|
|
183
|
-
*
|
|
184
|
-
* Bias this list toward OVER-inclusion. A name the CLI does not have is inert; a name it has and
|
|
185
|
-
* this list lacks is a tool silently removed from a run — which surfaces as an agent that cannot
|
|
186
|
-
* do its work, far from the registration that caused it. Historical/renamed spellings are kept for
|
|
187
|
-
* the same reason: the harness image is pinned per workspace, so one image faces several CLI
|
|
188
|
-
* versions. When the CLI gains a tool, add it here.
|
|
189
|
-
*/
|
|
190
|
-
export declare const CLAUDE_BUILT_IN_TOOLS: readonly string[];
|
|
191
176
|
/**
|
|
192
177
|
* The tool-name list for `--allowedTools`: every declared server's tools in the CLI's
|
|
193
|
-
* `mcp__<server>__<tool>` convention, PLUS
|
|
194
|
-
* restriction contributes the whole-server pattern, so
|
|
178
|
+
* `mcp__<server>__<tool>` convention, PLUS the built-in tools this run declared with `--tools`
|
|
179
|
+
* (`CLAUDE_TOOL_SET`). A server with no restriction contributes the whole-server pattern, so
|
|
180
|
+
* an allow-list stays one entry per server.
|
|
195
181
|
*
|
|
196
182
|
* Returns undefined when NO server restricts its tools — there is then nothing to narrow, and the
|
|
197
183
|
* safest list is the one we never send.
|
|
198
184
|
*
|
|
185
|
+
* An allow-list is whole-session, not MCP-scoped: it does not confine itself to MCP just because
|
|
186
|
+
* every entry we generate happens to be an `mcp__*` pattern. So the moment one tool server narrows
|
|
187
|
+
* its tools, the list has to carry the built-in file/bash/search tools too or the run is handed a
|
|
188
|
+
* narrowed MCP surface AND no way to read, edit or build anything.
|
|
189
|
+
*
|
|
190
|
+
* And carrying them is not merely a re-grant. MEASURED against CLI 2.1.245, the list is ADDITIVE:
|
|
191
|
+
* `--allowedTools "Bash,Grep"` yields the CLI's default set PLUS `Glob` and `Grep`, and an EMPTY
|
|
192
|
+
* list yields the default set plus `Glob`, `Grep` and the four `Task*` tools. A name here UNLOCKS
|
|
193
|
+
* a tool. That is why `builtIns` is the run's OWN declared set passed by reference rather than a
|
|
194
|
+
* constant re-read here: a separately-derived list would silently re-grant exactly what the
|
|
195
|
+
* `--tools` declaration withheld, and only on the runs that happen to wire a narrowing tool
|
|
196
|
+
* server.
|
|
197
|
+
*
|
|
199
198
|
* Whether the CLI ENFORCES this list is permission-mode dependent and not a contract we control:
|
|
200
199
|
* the run uses `--permission-mode bypassPermissions` (the container is the sandbox and no human is
|
|
201
|
-
* there to approve a call), under which an allow-list grants rather than gates.
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* scoping, not as a security boundary: a server the agent must not reach fully should not be
|
|
206
|
-
* wired for that kind at all.
|
|
200
|
+
* there to approve a call), under which an allow-list grants rather than gates. The always-present
|
|
201
|
+
* channel is the PROMPT, which states each server's permitted tool names on every harness. Treat
|
|
202
|
+
* `allowedTools` as scoping, not as a security boundary: a server the agent must not reach fully
|
|
203
|
+
* should not be wired for that kind at all.
|
|
207
204
|
*/
|
|
208
|
-
export declare function claudeAllowedToolPatterns(servers: McpServerSpec[]): string[] | undefined;
|
|
205
|
+
export declare function claudeAllowedToolPatterns(servers: McpServerSpec[], builtIns: readonly string[]): string[] | undefined;
|
|
209
206
|
/**
|
|
210
207
|
* The `[mcp_servers.<id>]` TOML block Codex reads from its `CODEX_HOME/config.toml`. Codex's MCP
|
|
211
208
|
* client is stdio-only, so an `http` server is skipped here.
|
|
@@ -418,68 +418,40 @@ export function claudeMcpConfig(servers) {
|
|
|
418
418
|
}
|
|
419
419
|
return { mcpServers };
|
|
420
420
|
}
|
|
421
|
-
/**
|
|
422
|
-
* The claude-code CLI's own tools, named so an `--allowedTools` list can never take them away.
|
|
423
|
-
*
|
|
424
|
-
* An allow-list is whole-session: it does not scope itself to MCP just because every entry we
|
|
425
|
-
* generate happens to be an `mcp__*` pattern. So the moment one tool server narrows its tools, the
|
|
426
|
-
* list has to re-grant the agent's built-in file/bash/search tools or the run is handed a narrowed
|
|
427
|
-
* MCP surface AND no way to read, edit or build anything.
|
|
428
|
-
*
|
|
429
|
-
* Bias this list toward OVER-inclusion. A name the CLI does not have is inert; a name it has and
|
|
430
|
-
* this list lacks is a tool silently removed from a run — which surfaces as an agent that cannot
|
|
431
|
-
* do its work, far from the registration that caused it. Historical/renamed spellings are kept for
|
|
432
|
-
* the same reason: the harness image is pinned per workspace, so one image faces several CLI
|
|
433
|
-
* versions. When the CLI gains a tool, add it here.
|
|
434
|
-
*/
|
|
435
|
-
export const CLAUDE_BUILT_IN_TOOLS = [
|
|
436
|
-
'Agent',
|
|
437
|
-
'Bash',
|
|
438
|
-
'BashOutput',
|
|
439
|
-
'Edit',
|
|
440
|
-
'ExitPlanMode',
|
|
441
|
-
'Glob',
|
|
442
|
-
'Grep',
|
|
443
|
-
'KillBash',
|
|
444
|
-
'KillShell',
|
|
445
|
-
'ListMcpResources',
|
|
446
|
-
'MultiEdit',
|
|
447
|
-
'NotebookEdit',
|
|
448
|
-
'NotebookRead',
|
|
449
|
-
'Read',
|
|
450
|
-
'ReadMcpResource',
|
|
451
|
-
'SlashCommand',
|
|
452
|
-
'Skill',
|
|
453
|
-
'Task',
|
|
454
|
-
'TaskCreate',
|
|
455
|
-
'TaskUpdate',
|
|
456
|
-
'TodoWrite',
|
|
457
|
-
'WebFetch',
|
|
458
|
-
'WebSearch',
|
|
459
|
-
'Write',
|
|
460
|
-
];
|
|
461
421
|
/**
|
|
462
422
|
* The tool-name list for `--allowedTools`: every declared server's tools in the CLI's
|
|
463
|
-
* `mcp__<server>__<tool>` convention, PLUS
|
|
464
|
-
* restriction contributes the whole-server pattern, so
|
|
423
|
+
* `mcp__<server>__<tool>` convention, PLUS the built-in tools this run declared with `--tools`
|
|
424
|
+
* (`CLAUDE_TOOL_SET`). A server with no restriction contributes the whole-server pattern, so
|
|
425
|
+
* an allow-list stays one entry per server.
|
|
465
426
|
*
|
|
466
427
|
* Returns undefined when NO server restricts its tools — there is then nothing to narrow, and the
|
|
467
428
|
* safest list is the one we never send.
|
|
468
429
|
*
|
|
430
|
+
* An allow-list is whole-session, not MCP-scoped: it does not confine itself to MCP just because
|
|
431
|
+
* every entry we generate happens to be an `mcp__*` pattern. So the moment one tool server narrows
|
|
432
|
+
* its tools, the list has to carry the built-in file/bash/search tools too or the run is handed a
|
|
433
|
+
* narrowed MCP surface AND no way to read, edit or build anything.
|
|
434
|
+
*
|
|
435
|
+
* And carrying them is not merely a re-grant. MEASURED against CLI 2.1.245, the list is ADDITIVE:
|
|
436
|
+
* `--allowedTools "Bash,Grep"` yields the CLI's default set PLUS `Glob` and `Grep`, and an EMPTY
|
|
437
|
+
* list yields the default set plus `Glob`, `Grep` and the four `Task*` tools. A name here UNLOCKS
|
|
438
|
+
* a tool. That is why `builtIns` is the run's OWN declared set passed by reference rather than a
|
|
439
|
+
* constant re-read here: a separately-derived list would silently re-grant exactly what the
|
|
440
|
+
* `--tools` declaration withheld, and only on the runs that happen to wire a narrowing tool
|
|
441
|
+
* server.
|
|
442
|
+
*
|
|
469
443
|
* Whether the CLI ENFORCES this list is permission-mode dependent and not a contract we control:
|
|
470
444
|
* the run uses `--permission-mode bypassPermissions` (the container is the sandbox and no human is
|
|
471
|
-
* there to approve a call), under which an allow-list grants rather than gates.
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
* scoping, not as a security boundary: a server the agent must not reach fully should not be
|
|
476
|
-
* wired for that kind at all.
|
|
445
|
+
* there to approve a call), under which an allow-list grants rather than gates. The always-present
|
|
446
|
+
* channel is the PROMPT, which states each server's permitted tool names on every harness. Treat
|
|
447
|
+
* `allowedTools` as scoping, not as a security boundary: a server the agent must not reach fully
|
|
448
|
+
* should not be wired for that kind at all.
|
|
477
449
|
*/
|
|
478
|
-
export function claudeAllowedToolPatterns(servers) {
|
|
450
|
+
export function claudeAllowedToolPatterns(servers, builtIns) {
|
|
479
451
|
if (!servers.some((s) => s.allowedTools?.length))
|
|
480
452
|
return undefined;
|
|
481
453
|
const mcp = servers.flatMap((s) => s.allowedTools?.length ? s.allowedTools.map((t) => `mcp__${s.id}__${t}`) : [`mcp__${s.id}`]);
|
|
482
|
-
return [...mcp, ...
|
|
454
|
+
return [...mcp, ...builtIns];
|
|
483
455
|
}
|
|
484
456
|
/** Escape a string as a TOML basic string (Codex config is TOML, not JSON). */
|
|
485
457
|
function tomlString(value) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variables of the HARNESS PROCESS that must not reach the agent's checkout.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately short, and it stays short: the bar is a variable whose value is a fact about the
|
|
5
|
+
* harness that a tool in the checkout will silently act on. It is not a sandbox (an agent can set
|
|
6
|
+
* whatever it likes in its own shell) and not a secret filter (the harness holds per-job secrets
|
|
7
|
+
* in `agentEnv`, never in `process.env`).
|
|
8
|
+
*/
|
|
9
|
+
export declare const HARNESS_ONLY_ENV_NAMES: readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* The child env for a command run in the agent's checkout: the harness's own environment minus
|
|
12
|
+
* {@link HARNESS_ONLY_ENV_NAMES}, with each layer merged over it in order.
|
|
13
|
+
*
|
|
14
|
+
* A layer may still SET a stripped name — a job that explicitly asks for `NODE_ENV` gets it. The
|
|
15
|
+
* strip removes what was merely INHERITED, which is the thing nobody chose.
|
|
16
|
+
*/
|
|
17
|
+
export declare function agentChildEnv(...layers: (Record<string, string | undefined> | undefined)[]): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The environment the harness hands to everything it spawns INTO the agent's checkout: the agent
|
|
2
|
+
// CLI itself, the captured commands (dependency prepopulation, validation checks, the reproduction
|
|
3
|
+
// proof) and the frontend build/serve.
|
|
4
|
+
//
|
|
5
|
+
// The rule this exists for: the harness process and the agent's checkout are two different
|
|
6
|
+
// programs, and a few of the harness's own environment variables are actively wrong for the
|
|
7
|
+
// second. `NODE_ENV=production` is the one that bit: npm reads it as `omit=dev`, so `npm install`
|
|
8
|
+
// in a checkout silently skips devDependencies, leaving the agent with no test runner, no linter
|
|
9
|
+
// and no build tool. One measured coder run spent six of its forty budgeted tool calls
|
|
10
|
+
// discovering and undoing that (install, `npm ls`, `npm config get omit`, reinstall with
|
|
11
|
+
// `--include=dev`, re-check the bin directory, approve an install script) — all of it caused by a
|
|
12
|
+
// variable the platform set, on a project the platform knows nothing about.
|
|
13
|
+
//
|
|
14
|
+
// Stripping it at THIS seam rather than in the image is what makes it true everywhere: the
|
|
15
|
+
// container gets `NODE_ENV=production` from `entrypoint.sh` (so the harness itself still runs in
|
|
16
|
+
// production mode) and the native host transport sets the same variable on the harness process it
|
|
17
|
+
// spawns, so an image-only fix would have left the developer's own machine leaking it.
|
|
18
|
+
//
|
|
19
|
+
// Per-job env NEVER goes through `process.env` (CLAUDE.md, "Harness rules"): the native transport
|
|
20
|
+
// serves every concurrent `ambientAuth` job from one long-lived process, so a mutation here would
|
|
21
|
+
// be a cross-job leak. This function only READS the process env and returns a fresh object.
|
|
22
|
+
/**
|
|
23
|
+
* Variables of the HARNESS PROCESS that must not reach the agent's checkout.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately short, and it stays short: the bar is a variable whose value is a fact about the
|
|
26
|
+
* harness that a tool in the checkout will silently act on. It is not a sandbox (an agent can set
|
|
27
|
+
* whatever it likes in its own shell) and not a secret filter (the harness holds per-job secrets
|
|
28
|
+
* in `agentEnv`, never in `process.env`).
|
|
29
|
+
*/
|
|
30
|
+
export const HARNESS_ONLY_ENV_NAMES = ['NODE_ENV'];
|
|
31
|
+
/**
|
|
32
|
+
* The child env for a command run in the agent's checkout: the harness's own environment minus
|
|
33
|
+
* {@link HARNESS_ONLY_ENV_NAMES}, with each layer merged over it in order.
|
|
34
|
+
*
|
|
35
|
+
* A layer may still SET a stripped name — a job that explicitly asks for `NODE_ENV` gets it. The
|
|
36
|
+
* strip removes what was merely INHERITED, which is the thing nobody chose.
|
|
37
|
+
*/
|
|
38
|
+
export function agentChildEnv(...layers) {
|
|
39
|
+
const env = { ...process.env };
|
|
40
|
+
for (const name of HARNESS_ONLY_ENV_NAMES)
|
|
41
|
+
delete env[name];
|
|
42
|
+
for (const layer of layers) {
|
|
43
|
+
if (layer)
|
|
44
|
+
Object.assign(env, layer);
|
|
45
|
+
}
|
|
46
|
+
return env;
|
|
47
|
+
}
|
package/dist/agent-runner.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { type Logger } from './logger.js';
|
|
|
2
2
|
import { type ToolProgressWindow } from './tool-silence.js';
|
|
3
3
|
import { type HarnessCallMetric, type PiRunOutcome, type TodoProgress, type ToolSpan } from './pi.js';
|
|
4
4
|
import { type McpServerSpec, type ObservedMcpServer, type SkillSpec } from './agent-capabilities.js';
|
|
5
|
-
import {
|
|
5
|
+
import type { ProgressGuardLimits } from './progress-guard.js';
|
|
6
|
+
import type { WorkspaceProbe } from './workspace-probe.js';
|
|
6
7
|
import { type SliceReview } from './subagents.js';
|
|
7
8
|
/** Which subscription harness to run (the Pi harness uses `runPi` directly). */
|
|
8
9
|
export type SubscriptionHarness = 'claude-code' | 'codex';
|
|
@@ -65,7 +66,7 @@ export interface SubscriptionRunOptions {
|
|
|
65
66
|
generateImages?: boolean;
|
|
66
67
|
/**
|
|
67
68
|
* Extra environment for the CLI child, scoped to this job (the tester's secrets, a
|
|
68
|
-
* private-registry npmrc pointer). Merged over the inherited
|
|
69
|
+
* private-registry npmrc pointer). Merged over the inherited env at spawn (`agentChildEnv`), so the
|
|
69
70
|
* agent and its shell tools see them without the harness mutating its OWN environment — which
|
|
70
71
|
* is shared by every concurrent job under the native host-process transport. See
|
|
71
72
|
* `RunOptions.agentEnv`.
|
|
@@ -84,6 +85,14 @@ export interface SubscriptionRunOptions {
|
|
|
84
85
|
guardLimits?: ProgressGuardLimits;
|
|
85
86
|
/** Whether this run is expected to edit files (false for assess-only runs); gates the no-edit bound. */
|
|
86
87
|
expectsEdits?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Probes the working tree for evidence the agent changed the repository. The guard's no-edit
|
|
90
|
+
* bound asks that question and can only see TOOL NAMES, so an agent writing files through
|
|
91
|
+
* `bash` reads as making no edits at all; this is what settles it before anything is killed.
|
|
92
|
+
* Injected so the guard stays pure, and consulted at most once per run (only when the bound is
|
|
93
|
+
* about to abort). Omitted ⇒ the bound falls back to its tool-name-only judgement.
|
|
94
|
+
*/
|
|
95
|
+
workspaceProbe?: WorkspaceProbe;
|
|
87
96
|
/** Called on every chunk of CLI output, so the watchdog sees the agent is alive. */
|
|
88
97
|
onActivity?: () => void;
|
|
89
98
|
/** Called with the latest subtask counts each time the CLI updates its todo/plan list. */
|
|
@@ -152,6 +161,13 @@ export declare function carryClaudeSystemPrompt(systemPrompt: string, userPrompt
|
|
|
152
161
|
prompt: string;
|
|
153
162
|
folded: boolean;
|
|
154
163
|
};
|
|
164
|
+
/**
|
|
165
|
+
* Run the Claude Code CLI headlessly against `opts.cwd`, authenticated with the
|
|
166
|
+
* leased subscription OAuth token (CLAUDE_CODE_OAUTH_TOKEN), talking direct to
|
|
167
|
+
* api.anthropic.com. Streams `--output-format stream-json`, mapping the
|
|
168
|
+
* `TodoWrite` tool calls onto subtask progress and the terminal `result` event
|
|
169
|
+
* onto the summary + usage.
|
|
170
|
+
*/
|
|
155
171
|
export declare function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRunOutcome>;
|
|
156
172
|
/**
|
|
157
173
|
* Run the Codex CLI headlessly against `opts.cwd`, authenticated with the leased
|