@cat-factory/executor-harness 1.134.0 → 1.137.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 +48 -0
- package/dist/agent-capabilities.d.ts +21 -24
- package/dist/agent-capabilities.js +22 -50
- package/dist/agent-runner.d.ts +7 -0
- package/dist/agent-runner.js +26 -183
- package/dist/agent-shared.d.ts +14 -5
- package/dist/agent-shared.js +14 -5
- package/dist/agent.js +22 -9
- 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/environment-inventory.d.ts +158 -0
- package/dist/environment-inventory.js +412 -0
- package/dist/multi-repo-coding.js +6 -8
- package/dist/pi-workspace.js +80 -58
- package/package.json +2 -2
- package/src/agent-capabilities.ts +25 -51
- package/src/agent-runner.ts +26 -214
- package/src/agent-shared.ts +16 -5
- package/src/agent.ts +23 -9
- package/src/claude-cli.ts +217 -0
- package/src/claude-home.ts +233 -0
- package/src/environment-inventory.ts +512 -0
- package/src/multi-repo-coding.ts +6 -8
- package/src/pi-workspace.ts +90 -58
package/README.md
CHANGED
|
@@ -116,6 +116,51 @@ Bootstrap differs at the ends: it may start from an empty dir, and **resets
|
|
|
116
116
|
history to one commit and force-pushes** the default branch instead of opening a
|
|
117
117
|
PR. Blueprint **commits onto a branch** (no history reset) and returns the tree.
|
|
118
118
|
|
|
119
|
+
### The environment is probed once, not by the agent
|
|
120
|
+
|
|
121
|
+
Before any mode branches, `handleAgent` probes the machine it is about to run on and appends an
|
|
122
|
+
`ENVIRONMENT INVENTORY` block to the job's system prompt (`src/environment-inventory.ts`). It names
|
|
123
|
+
the toolchain that answered and its versions, the curated list of tools that did not, and whether a
|
|
124
|
+
Docker DAEMON is actually reachable.
|
|
125
|
+
|
|
126
|
+
It exists because the platform used to ask every agent to find this out for itself, and every agent
|
|
127
|
+
did: in one measured run an architect ran `for c in docker kubectl helm kustomize; …` and then
|
|
128
|
+
`docker info`, and the coder it handed off to rediscovered both answers thirty calls later. Four
|
|
129
|
+
calls out of a forty-call budget for facts this process holds before the agent's first turn. The
|
|
130
|
+
backend cannot hold them (it composes its prompt before a transport is chosen, and the same body
|
|
131
|
+
reaches this image, a deployment's own image variant and, under `LOCAL_NATIVE_AGENTS`, the
|
|
132
|
+
developer's own machine), so it states the POLICY and names no tooling at all.
|
|
133
|
+
|
|
134
|
+
Three rules bind anything added to it:
|
|
135
|
+
|
|
136
|
+
- **A failed probe is not an absence.** Only `ENOENT` means "not installed"; a timeout or a refused
|
|
137
|
+
spawn renders on its own line as could-not-be-determined, because the two lead an agent to
|
|
138
|
+
opposite next moves.
|
|
139
|
+
- **An unlisted tool is unknown too**, which the block's last line says. The probe list is curated,
|
|
140
|
+
so silence about `terraform` must not read as its absence.
|
|
141
|
+
- **The Docker daemon is answered by running `docker info`**, never by finding the CLI. The CLI is
|
|
142
|
+
installed in this image unconditionally, `entrypoint.sh` starts the rootless daemon best-effort
|
|
143
|
+
and execs the server without waiting for it, so at job start this probe is the only thing that
|
|
144
|
+
knows how that went.
|
|
145
|
+
- **A daemon that is STARTING is not a daemon that is absent.** Because the entrypoint does not
|
|
146
|
+
wait, the backend dispatches seconds before there is a socket, and `docker info` is then refused
|
|
147
|
+
at once rather than slowly. So a refusal is read against `DOCKER_HOST`, which the entrypoint sets
|
|
148
|
+
whenever something is meant to serve a daemon here: unset means nothing was coming and the
|
|
149
|
+
absence is stated definitively, set means one short retry and then could-not-be-determined. The
|
|
150
|
+
absent wording tells an agent `docker compose up` "will fail here whatever the CLI reports", which
|
|
151
|
+
is a prohibition, so it may only be reached where nothing is going to answer.
|
|
152
|
+
- **A tool the platform did not provide is not installable system-wide either**, since the job runs
|
|
153
|
+
unprivileged. The line says that instead of banning installation outright: `pnpm` is absent from
|
|
154
|
+
this image (only the UI variant carries it), so it is routinely the package manager the job's own
|
|
155
|
+
repository declares, and a flat prohibition pushed agents onto `npm install` against a pnpm
|
|
156
|
+
lockfile. Reaching a project's own manager for that project alone is allowed and named.
|
|
157
|
+
|
|
158
|
+
Composed at exactly ONE point, onto the job's own `systemPrompt`, which every mode already forwards
|
|
159
|
+
and all three CLIs already carry (claude-code's `--append-system-prompt` and its oversized-argv
|
|
160
|
+
fallback, Codex's fold, Pi's `AGENTS.md`). `test/environment-inventory.coverage.test.ts` pins that:
|
|
161
|
+
a mode that folded its own copy would state the machine twice, and one that folded none would leave
|
|
162
|
+
its agent probing, with nothing failing either way.
|
|
163
|
+
|
|
119
164
|
### The work-branch push is CHECKPOINTED, so it is lease-guarded
|
|
120
165
|
|
|
121
166
|
Step 8's push is not the run's first: every `JOB_CHECKPOINT_INTERVAL_MS` (60s) the harness pushes
|
|
@@ -377,6 +422,8 @@ verdict is stated.
|
|
|
377
422
|
| `src/embed.ts` | Bundled assets/templates written into the workspace. |
|
|
378
423
|
| `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. |
|
|
379
424
|
| `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. |
|
|
425
|
+
| `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. |
|
|
426
|
+
| `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. |
|
|
380
427
|
| `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`). |
|
|
381
428
|
| `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. |
|
|
382
429
|
| `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). |
|
|
@@ -391,6 +438,7 @@ verdict is stated.
|
|
|
391
438
|
| `src/bootstrap-mode.ts` | The repo-bootstrap MODE: clone-a-reference-or-scaffold → run the agent → refuse to push an empty tree → reinit + force-push to the pre-created target repo. |
|
|
392
439
|
| `src/artifact-upload.ts` | The OUTBOUND half of the artifact seam: parses the body's `artifactUpload` and projects it onto the agent's env as `ARTIFACT_UPLOAD_URL` / `ARTIFACT_UPLOAD_TOKEN`, registering the token for redaction first. Passes through what the body carries and decides nothing: which kinds get the seam is the backend's call. |
|
|
393
440
|
| `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. |
|
|
441
|
+
| `src/environment-inventory.ts` | What the MACHINE holds, probed once per job and appended to the agent's system prompt as an ENVIRONMENT INVENTORY block. The only layer that can state it: the backend composes its prompt before a transport is chosen, and the same body serves this image, a deployment's own variant and the developer's laptop under `LOCAL_NATIVE_AGENTS`. Three-valued on purpose, so a probe that failed renders as unknown rather than as an absence, and the Docker DAEMON is answered by running `docker info` rather than by finding the CLI, which is installed here either way. See [The environment is probed once, not by the agent](#the-environment-is-probed-once-not-by-the-agent). |
|
|
394
442
|
| `src/agent-shared.ts` | The few helpers every agent MODE shares (effort-report folding, the capability fields forwarded to `runAgentInWorkspace`). |
|
|
395
443
|
| `src/logger.ts` | Structured logging. |
|
|
396
444
|
| `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). |
|
|
@@ -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) {
|
package/dist/agent-runner.d.ts
CHANGED
|
@@ -161,6 +161,13 @@ export declare function carryClaudeSystemPrompt(systemPrompt: string, userPrompt
|
|
|
161
161
|
prompt: string;
|
|
162
162
|
folded: boolean;
|
|
163
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
|
+
*/
|
|
164
171
|
export declare function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRunOutcome>;
|
|
165
172
|
/**
|
|
166
173
|
* Run the Codex CLI headlessly against `opts.cwd`, authenticated with the leased
|
package/dist/agent-runner.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { join } from 'node:path';
|
|
5
3
|
import { claudeAssistantContent, isObject, numberOf, redactBody } from './claude-stream.js';
|
|
6
4
|
import { claudeUsage, unaccountedUsageCall } from './usage-attribution.js';
|
|
7
5
|
import { createClaudeRunTelemetry, subagentDispatchId, } from './claude-call-aggregator.js';
|
|
@@ -9,7 +7,8 @@ import { ToolCallTracker, recordClaudeToolResults, } from './tool-trajectory.js'
|
|
|
9
7
|
import { log } from './logger.js';
|
|
10
8
|
import { NO_TOOL_WINDOW } from './tool-silence.js';
|
|
11
9
|
import { publishCallMetric, } from './pi.js';
|
|
12
|
-
import {
|
|
10
|
+
import { observeClaudeMcpInit, } from './agent-capabilities.js';
|
|
11
|
+
import { openClaudeRunHome } from './claude-home.js';
|
|
13
12
|
import { codexImageGapNote, createCodexHome, disposeCodexHome } from './codex-home.js';
|
|
14
13
|
import { createClaudeProgressGuard } from './guard-driver.js';
|
|
15
14
|
import { BoundedTail, JsonlLineReader } from './jsonl-stream.js';
|
|
@@ -17,11 +16,10 @@ import { killChildProcess, spawnDetached } from './process.js';
|
|
|
17
16
|
import { agentChildEnv } from './agent-env.js';
|
|
18
17
|
import { abortReasonOf } from './failure.js';
|
|
19
18
|
import { describeProcessExit } from './process-exit.js';
|
|
20
|
-
import { redact,
|
|
19
|
+
import { redact, secretsToRedact } from './redact.js';
|
|
21
20
|
import { createSliceTracker, startSubagentWatcher } from './subagents.js';
|
|
22
21
|
import { createTaskPlanTracker, mergeProgress, normalizeStatus, pickProgress, toProgress, todosToProgress, } from './progress.js';
|
|
23
|
-
import {
|
|
24
|
-
import { retainSessionTranscripts } from './transcript-retention.js';
|
|
22
|
+
import { assertClaudeToolsCurrent, claudeCliArgs, CLAUDE_TOOL_SET } from './claude-cli.js';
|
|
25
23
|
/**
|
|
26
24
|
* Drive one CLI subprocess to completion, streaming LF-framed JSONL from stdout
|
|
27
25
|
* through `onEvent`. Mirrors `runPi`'s lifecycle: prompt over stdin (out-of-band,
|
|
@@ -256,80 +254,6 @@ export function carryClaudeSystemPrompt(systemPrompt, userPrompt) {
|
|
|
256
254
|
// ---------------------------------------------------------------------------
|
|
257
255
|
// Claude Code
|
|
258
256
|
// ---------------------------------------------------------------------------
|
|
259
|
-
/**
|
|
260
|
-
* Run the Claude Code CLI headlessly against `opts.cwd`, authenticated with the
|
|
261
|
-
* leased subscription OAuth token (CLAUDE_CODE_OAUTH_TOKEN), talking direct to
|
|
262
|
-
* api.anthropic.com. Streams `--output-format stream-json`, mapping the
|
|
263
|
-
* `TodoWrite` tool calls onto subtask progress and the terminal `result` event
|
|
264
|
-
* onto the summary + usage.
|
|
265
|
-
*/
|
|
266
|
-
/**
|
|
267
|
-
* Write a repo-sourced skill as a NATIVE Claude Code skill under `<skillsRoot>/<name>/`: a
|
|
268
|
-
* `SKILL.md` (YAML frontmatter `name`/`description` + the instructions body, the format the CLI
|
|
269
|
-
* expects) plus every resource file at its path within the skill directory. Resource sub-paths
|
|
270
|
-
* were sanitized at the job boundary (no traversal), so nested dirs are created as needed.
|
|
271
|
-
*
|
|
272
|
-
* The frontmatter `name`/`description` values are emitted as JSON-encoded (double-quoted) YAML
|
|
273
|
-
* scalars, not bare plain scalars: an author's description routinely contains `: ` (colon-space)
|
|
274
|
-
* or a leading YAML indicator (`#`, `-`, `[`, `{`, `"`, …), which is invalid as a plain scalar and
|
|
275
|
-
* would make the CLI fail to parse the frontmatter and silently skip the skill. A JSON string is a
|
|
276
|
-
* valid YAML double-quoted scalar, so quoting makes the manifest robust to arbitrary text.
|
|
277
|
-
*/
|
|
278
|
-
async function writeNativeSkill(skillsRoot, skill) {
|
|
279
|
-
const dir = join(skillsRoot, skill.name);
|
|
280
|
-
await mkdir(dir, { recursive: true });
|
|
281
|
-
const name = JSON.stringify(skill.name);
|
|
282
|
-
const description = JSON.stringify(skill.description.replace(/\r?\n/g, ' '));
|
|
283
|
-
const frontmatter = `---\nname: ${name}\ndescription: ${description}\n---\n`;
|
|
284
|
-
await writeFile(join(dir, 'SKILL.md'), `${frontmatter}\n${skill.instructions}\n`, 'utf8');
|
|
285
|
-
for (const resource of skill.resources) {
|
|
286
|
-
const dest = join(dir, resource.relPath);
|
|
287
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
288
|
-
await writeFile(dest, resource.content, 'utf8');
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Prepare the Claude Code CLI's MCP wiring for one run: write the servers to a PER-RUN config and
|
|
293
|
-
* return the argv that points the CLI at it, plus the cleanup for a directory we had to mint.
|
|
294
|
-
*
|
|
295
|
-
* Two decisions live here. `--strict-mcp-config` makes that file the ONLY source of servers, so an
|
|
296
|
-
* ambient run on a developer's own machine can never silently hand the agent their personal ones.
|
|
297
|
-
* And `--allowedTools` is passed ONLY when a server actually narrows its tools — an allow-list is
|
|
298
|
-
* whole-session, not MCP-scoped, so `claudeAllowedToolPatterns` re-grants the CLI's built-in
|
|
299
|
-
* file/bash tools in the same list; see it for why that holds whichever way the run's permission
|
|
300
|
-
* mode treats an allow-list.
|
|
301
|
-
*
|
|
302
|
-
* The config carries this job's resolved credentials, so it goes in the isolated config home when
|
|
303
|
-
* we own one and a throwaway per-JOB directory otherwise — never the checkout (it would land in a
|
|
304
|
-
* commit) and never a shared HOME path (a concurrent job would clobber it).
|
|
305
|
-
*/
|
|
306
|
-
async function setUpClaudeMcp(servers, configHome) {
|
|
307
|
-
const noop = { args: [], cleanup: async () => { } };
|
|
308
|
-
if (!servers?.length)
|
|
309
|
-
return noop;
|
|
310
|
-
// Before anything can spawn: a failing MCP server echoes its own argv/headers into stderr, and
|
|
311
|
-
// that tail is carried onto the step's diagnostics.
|
|
312
|
-
registerKnownSecrets(mcpServerSecretValues(servers));
|
|
313
|
-
const home = configHome ?? (await mkdtemp(join(tmpdir(), 'cf-claude-mcp-')));
|
|
314
|
-
const owned = home === configHome ? undefined : home;
|
|
315
|
-
const cleanup = async () => {
|
|
316
|
-
if (owned)
|
|
317
|
-
await rm(owned, { recursive: true, force: true }).catch(() => { });
|
|
318
|
-
};
|
|
319
|
-
const configPath = await writeClaudeMcpConfig(home, servers);
|
|
320
|
-
if (!configPath)
|
|
321
|
-
return { args: [], cleanup };
|
|
322
|
-
const allowedTools = claudeAllowedToolPatterns(servers);
|
|
323
|
-
return {
|
|
324
|
-
args: [
|
|
325
|
-
'--mcp-config',
|
|
326
|
-
configPath,
|
|
327
|
-
'--strict-mcp-config',
|
|
328
|
-
...(allowedTools?.length ? ['--allowedTools', allowedTools.join(',')] : []),
|
|
329
|
-
],
|
|
330
|
-
cleanup,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
257
|
/**
|
|
334
258
|
* The LIVE publishers of a claude-code run: everything the stream has revealed so far that the
|
|
335
259
|
* backend should see before the run ends, rather than only in its terminal result.
|
|
@@ -475,6 +399,13 @@ function openClaudeCallCapture(opts, stream) {
|
|
|
475
399
|
},
|
|
476
400
|
};
|
|
477
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Run the Claude Code CLI headlessly against `opts.cwd`, authenticated with the
|
|
404
|
+
* leased subscription OAuth token (CLAUDE_CODE_OAUTH_TOKEN), talking direct to
|
|
405
|
+
* api.anthropic.com. Streams `--output-format stream-json`, mapping the
|
|
406
|
+
* `TodoWrite` tool calls onto subtask progress and the terminal `result` event
|
|
407
|
+
* onto the summary + usage.
|
|
408
|
+
*/
|
|
478
409
|
export async function runClaudeCode(opts) {
|
|
479
410
|
const stats = { toolCalls: 0, assistantChars: 0 };
|
|
480
411
|
let summary = '';
|
|
@@ -491,6 +422,9 @@ export async function runClaudeCode(opts) {
|
|
|
491
422
|
bytes: Buffer.byteLength(opts.systemPrompt, 'utf8'),
|
|
492
423
|
});
|
|
493
424
|
}
|
|
425
|
+
// The built-in tools this run declares, named ONCE: the same list rides `--tools` and the
|
|
426
|
+
// `--allowedTools` re-grant, which is additive rather than inert (see `claudeAllowedToolPatterns`).
|
|
427
|
+
const tools = CLAUDE_TOOL_SET;
|
|
494
428
|
const secrets = opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : [];
|
|
495
429
|
const capture = openClaudeCallCapture(opts, { prompt, folded, secrets });
|
|
496
430
|
const telemetry = capture.telemetry;
|
|
@@ -532,6 +466,9 @@ export async function runClaudeCode(opts) {
|
|
|
532
466
|
const onEvent = (event, meta) => {
|
|
533
467
|
const type = event.type;
|
|
534
468
|
reportToolServerStartup(event, opts.onToolServers);
|
|
469
|
+
// The same startup event answers what the CLI granted of what we asked for; a capability it
|
|
470
|
+
// named no tool for is a silent capability loss otherwise (see `assertClaudeToolsCurrent`).
|
|
471
|
+
assertClaudeToolsCurrent(event, tools, opts.log);
|
|
535
472
|
// A subagent's turns ride the parent's stdout tagged with the dispatch that spawned them;
|
|
536
473
|
// `telemetry` routes them off the parent's chain (and decides who bills them). Progress, slice
|
|
537
474
|
// tracking, the guard and `stats` below deliberately see EVERY event: a subagent grinding on
|
|
@@ -593,7 +530,7 @@ export async function runClaudeCode(opts) {
|
|
|
593
530
|
terminalReport = claudeResultReport(event) || terminalReport;
|
|
594
531
|
}
|
|
595
532
|
};
|
|
596
|
-
const home = await openClaudeRunHome(opts);
|
|
533
|
+
const home = await openClaudeRunHome(opts, tools);
|
|
597
534
|
const { configHome } = home;
|
|
598
535
|
// ADR 0026 D3 (path corrected by ADR 0027 Defect A): while the run is live, tail the CLI's
|
|
599
536
|
// subagent `*.jsonl` transcripts so a parallel-subagent review keeps the inactivity
|
|
@@ -623,22 +560,7 @@ export async function runClaudeCode(opts) {
|
|
|
623
560
|
try {
|
|
624
561
|
const { stderrTail } = await streamCli({
|
|
625
562
|
command: 'claude',
|
|
626
|
-
args:
|
|
627
|
-
'-p',
|
|
628
|
-
'--output-format',
|
|
629
|
-
'stream-json',
|
|
630
|
-
'--verbose',
|
|
631
|
-
// The per-run container IS the sandbox, and the run is fully headless (no one
|
|
632
|
-
// to approve a tool call) — so bypass permissions entirely. `acceptEdits`
|
|
633
|
-
// would auto-accept file edits but still gate Bash, which in `-p` mode is then
|
|
634
|
-
// denied, leaving the agent unable to run builds/tests/git to verify its work.
|
|
635
|
-
'--permission-mode',
|
|
636
|
-
'bypassPermissions',
|
|
637
|
-
'--model',
|
|
638
|
-
opts.model,
|
|
639
|
-
...home.mcpArgs,
|
|
640
|
-
...appendArgs,
|
|
641
|
-
],
|
|
563
|
+
args: claudeCliArgs({ model: opts.model, tools, mcpArgs: home.mcpArgs, appendArgs }),
|
|
642
564
|
}, prompt, { ...opts, signal: runSignal }, home.env, opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : [], onEvent);
|
|
643
565
|
// The stream has ended, so the last call has no successor envelope to complete it.
|
|
644
566
|
telemetry.flush();
|
|
@@ -687,91 +609,6 @@ export async function runClaudeCode(opts) {
|
|
|
687
609
|
await home.dispose();
|
|
688
610
|
}
|
|
689
611
|
}
|
|
690
|
-
async function openClaudeRunHome(opts) {
|
|
691
|
-
// Native (ambient) mode: run the developer's installed `claude` with its OWN login —
|
|
692
|
-
// no isolated config home, no injected credential, no onboarding pre-seed. Otherwise,
|
|
693
|
-
// Claude Code persists user config/credentials under its config dir; point that at an
|
|
694
|
-
// isolated, per-run temp dir OUTSIDE the cloned checkout (`opts.cwd`). Otherwise the
|
|
695
|
-
// agents that finish with `git add -A` (blueprint/requirements/bootstrap) could stage a
|
|
696
|
-
// stray `.claude/` directory — and any cached credential in it — into the pushed branch.
|
|
697
|
-
// Mirrors the Codex CODEX_HOME isolation below; removed by `dispose`.
|
|
698
|
-
if (!opts.ambientAuth && !opts.subscriptionToken) {
|
|
699
|
-
throw new Error('claude-code harness requires a subscription token (or ambientAuth)');
|
|
700
|
-
}
|
|
701
|
-
const configHome = opts.ambientAuth ? undefined : await mkdtemp(join(tmpdir(), 'cf-claude-'));
|
|
702
|
-
// The config dir is brand-new every run, so Claude Code would otherwise treat this
|
|
703
|
-
// as a first launch and BLOCK on the interactive onboarding / "trust this folder" /
|
|
704
|
-
// bypass-permissions acknowledgement prompts — which never get answered headlessly,
|
|
705
|
-
// hanging the job until the watchdog kills it. Pre-seed the config that marks those
|
|
706
|
-
// as already accepted so `-p` starts straight into the run. Best-effort: written
|
|
707
|
-
// before the CLI starts; unknown keys are harmless if a CLI version ignores them.
|
|
708
|
-
// (Ambient mode skips this — the developer's own config is already onboarded.)
|
|
709
|
-
// ADR 0026 D4: assert the pinned onboarding keys landed and log them with the CLI
|
|
710
|
-
// version, so a future first-run gate this set doesn't cover (which looks identical to
|
|
711
|
-
// a healthy-but-quiet subagent start) is diffable when the cold-start watchdog fires.
|
|
712
|
-
if (configHome) {
|
|
713
|
-
await writeOnboardingPreseed(configHome);
|
|
714
|
-
await assertOnboardingKeysCurrent(configHome, process.env.CLAUDE_CLI_VERSION, opts.log);
|
|
715
|
-
}
|
|
716
|
-
// Skills: install each as a native skill under the config dir's `skills/<name>/` so the CLI
|
|
717
|
-
// discovers and can invoke it. ONLY into the isolated per-run config home — never the
|
|
718
|
-
// developer's own `~/.claude` (ambient/native mode), where it would persist in their personal
|
|
719
|
-
// setup after the run and two concurrent jobs carrying same-named skills would clobber each
|
|
720
|
-
// other. An ambient run reads the skills from the checkout instead (`.cat-context/skill/<name>/`,
|
|
721
|
-
// materialised by the caller). Best-effort: a write failure must not wedge the run — the prompt
|
|
722
|
-
// still names the skills.
|
|
723
|
-
if (configHome) {
|
|
724
|
-
for (const skill of opts.skills ?? []) {
|
|
725
|
-
await writeNativeSkill(join(configHome, 'skills'), skill).catch(() => { });
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
// Tool servers (MCP): the CLI is pointed at a per-run config rather than discovering an ambient
|
|
729
|
-
// one. See `setUpClaudeMcp` for why that matters and what has to be cleaned up afterwards.
|
|
730
|
-
const mcp = await setUpClaudeMcp(opts.mcpServers, configHome);
|
|
731
|
-
return {
|
|
732
|
-
configHome,
|
|
733
|
-
mcpArgs: mcp.args,
|
|
734
|
-
env: buildClaudeEnv(opts, configHome),
|
|
735
|
-
dispose: async () => {
|
|
736
|
-
// The ambient-mode MCP config dir (credential-bearing) never outlives the run.
|
|
737
|
-
await mcp.cleanup();
|
|
738
|
-
if (!configHome)
|
|
739
|
-
return;
|
|
740
|
-
// Lift the CLI session transcripts (`projects/`) out for short-lived retention BEFORE the
|
|
741
|
-
// home is deleted — the credential lives at the home root, never in `projects/`, so this
|
|
742
|
-
// keeps the debugging artifact without leaking the token. Best-effort; never throws.
|
|
743
|
-
await retainSessionTranscripts(configHome, ['projects'], {
|
|
744
|
-
label: 'claude-code',
|
|
745
|
-
...(opts.log ? { log: opts.log } : {}),
|
|
746
|
-
});
|
|
747
|
-
// Never leave the config dir (and any cached credential) on disk past the run.
|
|
748
|
-
await rm(configHome, { recursive: true, force: true }).catch(() => { });
|
|
749
|
-
},
|
|
750
|
-
};
|
|
751
|
-
}
|
|
752
|
-
/**
|
|
753
|
-
* Build the child-process env for the `claude` CLI: an isolated config home plus subscription
|
|
754
|
-
* auth (Anthropic OAuth token, or an Anthropic-compatible base URL + auth token for a
|
|
755
|
-
* non-Anthropic Claude-Code vendor like GLM/Kimi/DeepSeek), or an empty env in ambient mode
|
|
756
|
-
* (the developer's own logged-in `~/.claude` is used). Extracted from {@link runClaudeCode} to
|
|
757
|
-
* keep its cyclomatic complexity down; behaviour is a straight move of the original expression.
|
|
758
|
-
*/
|
|
759
|
-
function buildClaudeEnv(opts, configHome) {
|
|
760
|
-
// The job-scoped env rides along in BOTH modes; the credential/config vars below are what
|
|
761
|
-
// ambient mode drops (the developer's own logged-in `~/.claude` is used instead).
|
|
762
|
-
if (opts.ambientAuth)
|
|
763
|
-
return { ...opts.extraEnv };
|
|
764
|
-
return {
|
|
765
|
-
...opts.extraEnv,
|
|
766
|
-
CLAUDE_CONFIG_DIR: configHome,
|
|
767
|
-
...(opts.subscriptionBaseUrl
|
|
768
|
-
? {
|
|
769
|
-
ANTHROPIC_BASE_URL: opts.subscriptionBaseUrl,
|
|
770
|
-
ANTHROPIC_AUTH_TOKEN: opts.subscriptionToken,
|
|
771
|
-
}
|
|
772
|
-
: { CLAUDE_CODE_OAUTH_TOKEN: opts.subscriptionToken }),
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
612
|
/**
|
|
776
613
|
* Merge the parent-loop telemetry with the subagents' out-of-band usage + per-call metrics into
|
|
777
614
|
* the run outcome. INVARIANT (do not "fix" this into a double count): the run total is the parent
|
|
@@ -924,6 +761,12 @@ export async function runCodex(opts) {
|
|
|
924
761
|
'exec',
|
|
925
762
|
'--json',
|
|
926
763
|
'--skip-git-repo-check',
|
|
764
|
+
// No `--tools` analogue here, and its absence is a FINDING rather than an oversight:
|
|
765
|
+
// codex has no flag that declares a built-in tool set, because it has no set to choose
|
|
766
|
+
// from. Its surface is shell + apply_patch + the plan tool, and the optional extras are
|
|
767
|
+
// individual `CODEX_HOME/config.toml` switches the harness already sets deliberately
|
|
768
|
+
// (`[features] image_generation`, see `codex-home.ts`). So there is nothing here that
|
|
769
|
+
// silently drifts with a CLI version the way claude-code's headless default did.
|
|
927
770
|
// The per-run container IS the sandbox; let Codex write files and reach the
|
|
928
771
|
// vendor unrestricted, with no approval prompts (the run is headless).
|
|
929
772
|
'--dangerously-bypass-approvals-and-sandbox',
|
package/dist/agent-shared.d.ts
CHANGED
|
@@ -7,11 +7,18 @@ import type { EffortReport } from './effort.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function mergeEffort(result: AgentResult, effortReport: EffortReport | undefined): AgentResult;
|
|
9
9
|
/**
|
|
10
|
-
* The agent-capability fields (skills, tool servers, reference designs) every
|
|
11
|
-
* forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow
|
|
12
|
-
* cannot silently be the one that drops a kind's declared playbook, tool server
|
|
13
|
-
* gallery: the failure mode is invisible (the agent simply works without
|
|
14
|
-
* up as degraded output.
|
|
10
|
+
* The agent-capability fields (skills, tool servers, reference designs, web research) every
|
|
11
|
+
* agent-running flow forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow
|
|
12
|
+
* spread, so a flow cannot silently be the one that drops a kind's declared playbook, tool server,
|
|
13
|
+
* reference gallery or web access: the failure mode is invisible (the agent simply works without
|
|
14
|
+
* it) and would only show up as degraded output.
|
|
15
|
+
*
|
|
16
|
+
* Web research joined the helper after the conflict-resolver and bootstrap flows were found to be
|
|
17
|
+
* forwarding neither half of it: both build their own spec literal, and the two web fields were
|
|
18
|
+
* hand-written at the four sites that remembered them. That is exactly the drift this helper
|
|
19
|
+
* exists to make unrepresentable, so they are read here rather than at each call site. Both halves
|
|
20
|
+
* travel together on purpose: the guidance NAMES the tools, so a flow carrying one without the
|
|
21
|
+
* other either describes tools the run was never given or hands it tools nothing introduced.
|
|
15
22
|
*/
|
|
16
23
|
export declare function agentCapabilities(job: AgentJob): {
|
|
17
24
|
skills?: SkillSpec[];
|
|
@@ -19,4 +26,6 @@ export declare function agentCapabilities(job: AgentJob): {
|
|
|
19
26
|
generateImages?: boolean;
|
|
20
27
|
referenceScreenshots?: ImageManifestSpec;
|
|
21
28
|
designImages?: ImageManifestSpec;
|
|
29
|
+
webSearchProxy?: boolean;
|
|
30
|
+
webToolsGuidance?: string;
|
|
22
31
|
};
|
package/dist/agent-shared.js
CHANGED
|
@@ -10,11 +10,18 @@ export function mergeEffort(result, effortReport) {
|
|
|
10
10
|
return effortReport ? { ...result, effortReport } : result;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* The agent-capability fields (skills, tool servers, reference designs) every
|
|
14
|
-
* forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow
|
|
15
|
-
* cannot silently be the one that drops a kind's declared playbook, tool server
|
|
16
|
-
* gallery: the failure mode is invisible (the agent simply works without
|
|
17
|
-
* up as degraded output.
|
|
13
|
+
* The agent-capability fields (skills, tool servers, reference designs, web research) every
|
|
14
|
+
* agent-running flow forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow
|
|
15
|
+
* spread, so a flow cannot silently be the one that drops a kind's declared playbook, tool server,
|
|
16
|
+
* reference gallery or web access: the failure mode is invisible (the agent simply works without
|
|
17
|
+
* it) and would only show up as degraded output.
|
|
18
|
+
*
|
|
19
|
+
* Web research joined the helper after the conflict-resolver and bootstrap flows were found to be
|
|
20
|
+
* forwarding neither half of it: both build their own spec literal, and the two web fields were
|
|
21
|
+
* hand-written at the four sites that remembered them. That is exactly the drift this helper
|
|
22
|
+
* exists to make unrepresentable, so they are read here rather than at each call site. Both halves
|
|
23
|
+
* travel together on purpose: the guidance NAMES the tools, so a flow carrying one without the
|
|
24
|
+
* other either describes tools the run was never given or hands it tools nothing introduced.
|
|
18
25
|
*/
|
|
19
26
|
export function agentCapabilities(job) {
|
|
20
27
|
return {
|
|
@@ -23,5 +30,7 @@ export function agentCapabilities(job) {
|
|
|
23
30
|
...(job.generateImages ? { generateImages: true } : {}),
|
|
24
31
|
...(job.referenceScreenshots ? { referenceScreenshots: job.referenceScreenshots } : {}),
|
|
25
32
|
...(job.designImages ? { designImages: job.designImages } : {}),
|
|
33
|
+
...(job.webSearch ? { webSearchProxy: true } : {}),
|
|
34
|
+
...(job.webToolsGuidance ? { webToolsGuidance: job.webToolsGuidance } : {}),
|
|
26
35
|
};
|
|
27
36
|
}
|