@cat-factory/executor-harness 1.134.0 → 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 +2 -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 +0 -6
- 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/multi-repo-coding.js +6 -8
- package/dist/pi-workspace.js +80 -58
- package/package.json +1 -1
- 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 +0 -6
- package/src/claude-cli.ts +217 -0
- package/src/claude-home.ts +233 -0
- package/src/multi-repo-coding.ts +6 -8
- package/src/pi-workspace.ts +90 -58
package/dist/pi-workspace.js
CHANGED
|
@@ -132,6 +132,80 @@ export async function checkoutHasBlueprints(dir, multiRepo) {
|
|
|
132
132
|
const checks = await Promise.all(legs.filter((e) => e.isDirectory()).map((e) => isBlueprintDir(join(dir, e.name))));
|
|
133
133
|
return checks.some(Boolean);
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Run one pass on a SUBSCRIPTION harness (Claude Code / Codex): the leased-credential path, which
|
|
137
|
+
* shares only the checkout preparation with the Pi one.
|
|
138
|
+
*
|
|
139
|
+
* Split out of {@link runAgentInWorkspace} for its cyclomatic budget. It is also the honest seam:
|
|
140
|
+
* everything here is a decision about what the vendor's own CLI is handed, while everything left
|
|
141
|
+
* behind is about the proxy-backed Pi run.
|
|
142
|
+
*/
|
|
143
|
+
async function runSubscriptionInWorkspace(harness, spec, opts, prepared) {
|
|
144
|
+
const { contextFiles, imageGuidance, workspaceProbe } = prepared;
|
|
145
|
+
// Ambient (native) mode authenticates with the developer's own CLI login, so no
|
|
146
|
+
// leased token is required; otherwise the leased subscription token is mandatory.
|
|
147
|
+
if (!spec.ambientAuth && !spec.subscriptionToken) {
|
|
148
|
+
throw new Error(`The ${harness} harness requires a subscription token`);
|
|
149
|
+
}
|
|
150
|
+
const subOutcome = await runSubscriptionHarness(harness, {
|
|
151
|
+
cwd: spec.dir,
|
|
152
|
+
model: spec.model,
|
|
153
|
+
systemPrompt: `${subscriptionSystemPrompt(spec.systemPrompt, contextFiles)}${imageGuidance}`,
|
|
154
|
+
userPrompt: spec.userPrompt,
|
|
155
|
+
...(spec.subscriptionToken ? { subscriptionToken: spec.subscriptionToken } : {}),
|
|
156
|
+
subscriptionBaseUrl: spec.subscriptionBaseUrl,
|
|
157
|
+
...(spec.ambientAuth ? { ambientAuth: true } : {}),
|
|
158
|
+
...(spec.skills?.length ? { skills: spec.skills } : {}),
|
|
159
|
+
...(spec.mcpServers?.length ? { mcpServers: spec.mcpServers } : {}),
|
|
160
|
+
// Codex's own image tool. Passed for both subscription harnesses because the option lives on
|
|
161
|
+
// the shared run options; `runClaudeCode` ignores it, since claude-code has no such tool and
|
|
162
|
+
// (unlike an MCP server) there is nothing to report as unservable — the backend never
|
|
163
|
+
// resolves a codex-served generator onto a claude-code step, because admission refuses it.
|
|
164
|
+
...(spec.generateImages ? { generateImages: true } : {}),
|
|
165
|
+
// `spec.webSearchProxy` is deliberately NOT forwarded. It states whether OUR PROXY serves web
|
|
166
|
+
// research for this run's account, which is what Pi's tools ride and what they would fail
|
|
167
|
+
// without; neither subscription CLI touches that proxy. Claude Code's `WebSearch`/`WebFetch`
|
|
168
|
+
// are served by the vendor the leased subscription already pays and are declared
|
|
169
|
+
// unconditionally (see `CLAUDE_TOOL_SET`), and Codex's surface is per-tool config rather than
|
|
170
|
+
// a list. Passing the proxy's availability here would withhold working tools on the strength
|
|
171
|
+
// of an unrelated deployment's wiring.
|
|
172
|
+
...(opts.agentEnv ? { extraEnv: opts.agentEnv } : {}),
|
|
173
|
+
signal: opts.signal,
|
|
174
|
+
// Run the SAME no-progress guard Pi gets (previously claude-code/codex had none): env
|
|
175
|
+
// defaults merged loosen-only with the kind's tuning + the backend's complexity-scaled
|
|
176
|
+
// no-edit allowance, so a claude-code run that stops making progress is killed early
|
|
177
|
+
// instead of burning the full wall-clock budget. The claude runner consumes it; codex
|
|
178
|
+
// ignores it for now (its stream isn't wired to the guard).
|
|
179
|
+
guardLimits: mergeGuardLimits(progressGuardLimitsFromEnv(), spec.guardLimits),
|
|
180
|
+
expectsEdits: spec.expectsEdits ?? true,
|
|
181
|
+
// What the guard's no-edit bound actually decides on (see `buildWorkspaceProbe`).
|
|
182
|
+
workspaceProbe,
|
|
183
|
+
onActivity: opts.onActivity,
|
|
184
|
+
onProgress: opts.onProgress,
|
|
185
|
+
// The run's tool-call trajectory, the same hook the Pi path feeds — so a subscription run
|
|
186
|
+
// and a proxied one produce the same evidence rather than one of them producing none.
|
|
187
|
+
onSpan: opts.onSpan,
|
|
188
|
+
// The tool-silence window (stuck-run audit F13), opened by whichever CLI actually runs.
|
|
189
|
+
// Wired for BOTH subscription harnesses: each reports tool activity on its own stream, so
|
|
190
|
+
// each can beat the window it opens.
|
|
191
|
+
beginToolWindow: opts.beginToolWindow,
|
|
192
|
+
// Per-slice review capture, so a parallel review's finished slices are persisted as they
|
|
193
|
+
// land rather than only in the terminal output. Only the subscription runners fan work out
|
|
194
|
+
// across subagents, so this is the only path that can produce it.
|
|
195
|
+
onSliceReviews: opts.onSliceReviews,
|
|
196
|
+
// What the CLI reported about the tool servers it loaded. Wired for BOTH subscription
|
|
197
|
+
// harnesses even though only claude-code's stream carries the report today: the hook is a
|
|
198
|
+
// pass-through, and a codex run that never calls it leaves the backend's record honestly
|
|
199
|
+
// absent rather than claiming every server it wired failed to start.
|
|
200
|
+
onToolServers: opts.onToolServers,
|
|
201
|
+
// Stream this run's per-call telemetry to the job's live drain. The subscription
|
|
202
|
+
// harnesses are the only producers of `callMetrics` (Pi's calls are metered by the LLM
|
|
203
|
+
// proxy as they happen), so this is the only path that needs the hook.
|
|
204
|
+
onCallMetric: opts.onCallMetric,
|
|
205
|
+
...(opts.log ? { log: opts.log } : {}),
|
|
206
|
+
});
|
|
207
|
+
return withEffortReport(spec.dir, subOutcome);
|
|
208
|
+
}
|
|
135
209
|
/**
|
|
136
210
|
* Write Pi's global agent context (`~/.pi/agent/AGENTS.md`) + provider config,
|
|
137
211
|
* then run Pi once in `spec.dir` and return its summary/stats/stderr. The context
|
|
@@ -181,67 +255,15 @@ export async function runAgentInWorkspace(spec, opts = {}) {
|
|
|
181
255
|
// bootstrap) has no HEAD to read; the probe then rides on the dirty-tree half alone, which is
|
|
182
256
|
// the half that matters there anyway.
|
|
183
257
|
const workspaceProbe = await buildWorkspaceProbe(spec, opts.signal);
|
|
184
|
-
// Subscription harnesses (Claude Code / Codex) authenticate with the leased
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// push, watchdogs) is unchanged.
|
|
258
|
+
// Subscription harnesses (Claude Code / Codex) authenticate with the leased token and talk
|
|
259
|
+
// direct to the vendor: no proxy config, no AGENTS.md. The system prompt is passed straight to
|
|
260
|
+
// the CLI; everything around this (clone, push, watchdogs) is unchanged.
|
|
188
261
|
if (spec.harness === 'claude-code' || spec.harness === 'codex') {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
throw new Error(`The ${spec.harness} harness requires a subscription token`);
|
|
193
|
-
}
|
|
194
|
-
const subOutcome = await runSubscriptionHarness(spec.harness, {
|
|
195
|
-
cwd: spec.dir,
|
|
196
|
-
model: spec.model,
|
|
197
|
-
systemPrompt: `${subscriptionSystemPrompt(spec.systemPrompt, contextFiles)}${imageGuidance}`,
|
|
198
|
-
userPrompt: spec.userPrompt,
|
|
199
|
-
...(spec.subscriptionToken ? { subscriptionToken: spec.subscriptionToken } : {}),
|
|
200
|
-
subscriptionBaseUrl: spec.subscriptionBaseUrl,
|
|
201
|
-
...(spec.ambientAuth ? { ambientAuth: true } : {}),
|
|
202
|
-
...(spec.skills?.length ? { skills: spec.skills } : {}),
|
|
203
|
-
...(spec.mcpServers?.length ? { mcpServers: spec.mcpServers } : {}),
|
|
204
|
-
// Codex's own image tool. Passed for both subscription harnesses because the option lives on
|
|
205
|
-
// the shared run options; `runClaudeCode` ignores it, since claude-code has no such tool and
|
|
206
|
-
// (unlike an MCP server) there is nothing to report as unservable — the backend never
|
|
207
|
-
// resolves a codex-served generator onto a claude-code step, because admission refuses it.
|
|
208
|
-
...(spec.generateImages ? { generateImages: true } : {}),
|
|
209
|
-
...(opts.agentEnv ? { extraEnv: opts.agentEnv } : {}),
|
|
210
|
-
signal: opts.signal,
|
|
211
|
-
// Run the SAME no-progress guard Pi gets (previously claude-code/codex had none): env
|
|
212
|
-
// defaults merged loosen-only with the kind's tuning + the backend's complexity-scaled
|
|
213
|
-
// no-edit allowance, so a claude-code run that stops making progress is killed early
|
|
214
|
-
// instead of burning the full wall-clock budget. The claude runner consumes it; codex
|
|
215
|
-
// ignores it for now (its stream isn't wired to the guard).
|
|
216
|
-
guardLimits: mergeGuardLimits(progressGuardLimitsFromEnv(), spec.guardLimits),
|
|
217
|
-
expectsEdits: spec.expectsEdits ?? true,
|
|
218
|
-
// What the guard's no-edit bound actually decides on (see `buildWorkspaceProbe`).
|
|
262
|
+
return await runSubscriptionInWorkspace(spec.harness, spec, opts, {
|
|
263
|
+
contextFiles,
|
|
264
|
+
imageGuidance,
|
|
219
265
|
workspaceProbe,
|
|
220
|
-
onActivity: opts.onActivity,
|
|
221
|
-
onProgress: opts.onProgress,
|
|
222
|
-
// The run's tool-call trajectory, the same hook the Pi path feeds — so a subscription run
|
|
223
|
-
// and a proxied one produce the same evidence rather than one of them producing none.
|
|
224
|
-
onSpan: opts.onSpan,
|
|
225
|
-
// The tool-silence window (stuck-run audit F13), opened by whichever CLI actually runs.
|
|
226
|
-
// Wired for BOTH subscription harnesses: each reports tool activity on its own stream, so
|
|
227
|
-
// each can beat the window it opens.
|
|
228
|
-
beginToolWindow: opts.beginToolWindow,
|
|
229
|
-
// Per-slice review capture, so a parallel review's finished slices are persisted as they
|
|
230
|
-
// land rather than only in the terminal output. Only the subscription runners fan work out
|
|
231
|
-
// across subagents, so this is the only path that can produce it.
|
|
232
|
-
onSliceReviews: opts.onSliceReviews,
|
|
233
|
-
// What the CLI reported about the tool servers it loaded. Wired for BOTH subscription
|
|
234
|
-
// harnesses even though only claude-code's stream carries the report today: the hook is a
|
|
235
|
-
// pass-through, and a codex run that never calls it leaves the backend's record honestly
|
|
236
|
-
// absent rather than claiming every server it wired failed to start.
|
|
237
|
-
onToolServers: opts.onToolServers,
|
|
238
|
-
// Stream this run's per-call telemetry to the job's live drain. The subscription
|
|
239
|
-
// harnesses are the only producers of `callMetrics` (Pi's calls are metered by the LLM
|
|
240
|
-
// proxy as they happen), so this is the only path that needs the hook.
|
|
241
|
-
onCallMetric: opts.onCallMetric,
|
|
242
|
-
...(opts.log ? { log: opts.log } : {}),
|
|
243
266
|
});
|
|
244
|
-
return withEffortReport(spec.dir, subOutcome);
|
|
245
267
|
}
|
|
246
268
|
if (!spec.proxyBaseUrl || !spec.sessionToken) {
|
|
247
269
|
throw new Error('The Pi harness requires proxyBaseUrl and sessionToken');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/executor-harness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.135.0",
|
|
4
4
|
"description": "Container payload: a thin TypeScript wrapper that runs the Pi coding agent against a cloned repo and opens a PR. Runs in the Cloudflare Container (and, in local native mode, as a host process); carries no secrets.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -519,70 +519,44 @@ export function claudeMcpConfig(servers: McpServerSpec[]): {
|
|
|
519
519
|
return { mcpServers }
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
-
/**
|
|
523
|
-
* The claude-code CLI's own tools, named so an `--allowedTools` list can never take them away.
|
|
524
|
-
*
|
|
525
|
-
* An allow-list is whole-session: it does not scope itself to MCP just because every entry we
|
|
526
|
-
* generate happens to be an `mcp__*` pattern. So the moment one tool server narrows its tools, the
|
|
527
|
-
* list has to re-grant the agent's built-in file/bash/search tools or the run is handed a narrowed
|
|
528
|
-
* MCP surface AND no way to read, edit or build anything.
|
|
529
|
-
*
|
|
530
|
-
* Bias this list toward OVER-inclusion. A name the CLI does not have is inert; a name it has and
|
|
531
|
-
* this list lacks is a tool silently removed from a run — which surfaces as an agent that cannot
|
|
532
|
-
* do its work, far from the registration that caused it. Historical/renamed spellings are kept for
|
|
533
|
-
* the same reason: the harness image is pinned per workspace, so one image faces several CLI
|
|
534
|
-
* versions. When the CLI gains a tool, add it here.
|
|
535
|
-
*/
|
|
536
|
-
export const CLAUDE_BUILT_IN_TOOLS: readonly string[] = [
|
|
537
|
-
'Agent',
|
|
538
|
-
'Bash',
|
|
539
|
-
'BashOutput',
|
|
540
|
-
'Edit',
|
|
541
|
-
'ExitPlanMode',
|
|
542
|
-
'Glob',
|
|
543
|
-
'Grep',
|
|
544
|
-
'KillBash',
|
|
545
|
-
'KillShell',
|
|
546
|
-
'ListMcpResources',
|
|
547
|
-
'MultiEdit',
|
|
548
|
-
'NotebookEdit',
|
|
549
|
-
'NotebookRead',
|
|
550
|
-
'Read',
|
|
551
|
-
'ReadMcpResource',
|
|
552
|
-
'SlashCommand',
|
|
553
|
-
'Skill',
|
|
554
|
-
'Task',
|
|
555
|
-
'TaskCreate',
|
|
556
|
-
'TaskUpdate',
|
|
557
|
-
'TodoWrite',
|
|
558
|
-
'WebFetch',
|
|
559
|
-
'WebSearch',
|
|
560
|
-
'Write',
|
|
561
|
-
]
|
|
562
|
-
|
|
563
522
|
/**
|
|
564
523
|
* The tool-name list for `--allowedTools`: every declared server's tools in the CLI's
|
|
565
|
-
* `mcp__<server>__<tool>` convention, PLUS
|
|
566
|
-
* restriction contributes the whole-server pattern, so
|
|
524
|
+
* `mcp__<server>__<tool>` convention, PLUS the built-in tools this run declared with `--tools`
|
|
525
|
+
* (`CLAUDE_TOOL_SET`). A server with no restriction contributes the whole-server pattern, so
|
|
526
|
+
* an allow-list stays one entry per server.
|
|
567
527
|
*
|
|
568
528
|
* Returns undefined when NO server restricts its tools — there is then nothing to narrow, and the
|
|
569
529
|
* safest list is the one we never send.
|
|
570
530
|
*
|
|
531
|
+
* An allow-list is whole-session, not MCP-scoped: it does not confine itself to MCP just because
|
|
532
|
+
* every entry we generate happens to be an `mcp__*` pattern. So the moment one tool server narrows
|
|
533
|
+
* its tools, the list has to carry the built-in file/bash/search tools too or the run is handed a
|
|
534
|
+
* narrowed MCP surface AND no way to read, edit or build anything.
|
|
535
|
+
*
|
|
536
|
+
* And carrying them is not merely a re-grant. MEASURED against CLI 2.1.245, the list is ADDITIVE:
|
|
537
|
+
* `--allowedTools "Bash,Grep"` yields the CLI's default set PLUS `Glob` and `Grep`, and an EMPTY
|
|
538
|
+
* list yields the default set plus `Glob`, `Grep` and the four `Task*` tools. A name here UNLOCKS
|
|
539
|
+
* a tool. That is why `builtIns` is the run's OWN declared set passed by reference rather than a
|
|
540
|
+
* constant re-read here: a separately-derived list would silently re-grant exactly what the
|
|
541
|
+
* `--tools` declaration withheld, and only on the runs that happen to wire a narrowing tool
|
|
542
|
+
* server.
|
|
543
|
+
*
|
|
571
544
|
* Whether the CLI ENFORCES this list is permission-mode dependent and not a contract we control:
|
|
572
545
|
* the run uses `--permission-mode bypassPermissions` (the container is the sandbox and no human is
|
|
573
|
-
* there to approve a call), under which an allow-list grants rather than gates.
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
* scoping, not as a security boundary: a server the agent must not reach fully should not be
|
|
578
|
-
* wired for that kind at all.
|
|
546
|
+
* there to approve a call), under which an allow-list grants rather than gates. The always-present
|
|
547
|
+
* channel is the PROMPT, which states each server's permitted tool names on every harness. Treat
|
|
548
|
+
* `allowedTools` as scoping, not as a security boundary: a server the agent must not reach fully
|
|
549
|
+
* should not be wired for that kind at all.
|
|
579
550
|
*/
|
|
580
|
-
export function claudeAllowedToolPatterns(
|
|
551
|
+
export function claudeAllowedToolPatterns(
|
|
552
|
+
servers: McpServerSpec[],
|
|
553
|
+
builtIns: readonly string[],
|
|
554
|
+
): string[] | undefined {
|
|
581
555
|
if (!servers.some((s) => s.allowedTools?.length)) return undefined
|
|
582
556
|
const mcp = servers.flatMap((s) =>
|
|
583
557
|
s.allowedTools?.length ? s.allowedTools.map((t) => `mcp__${s.id}__${t}`) : [`mcp__${s.id}`],
|
|
584
558
|
)
|
|
585
|
-
return [...mcp, ...
|
|
559
|
+
return [...mcp, ...builtIns]
|
|
586
560
|
}
|
|
587
561
|
|
|
588
562
|
/** Escape a string as a TOML basic string (Codex config is TOML, not JSON). */
|
package/src/agent-runner.ts
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 {
|
|
@@ -25,14 +23,12 @@ import {
|
|
|
25
23
|
} from './pi.js'
|
|
26
24
|
import type { PiRunStats } from './pi-reduction.js'
|
|
27
25
|
import {
|
|
28
|
-
claudeAllowedToolPatterns,
|
|
29
|
-
mcpServerSecretValues,
|
|
30
26
|
observeClaudeMcpInit,
|
|
31
|
-
writeClaudeMcpConfig,
|
|
32
27
|
type McpServerSpec,
|
|
33
28
|
type ObservedMcpServer,
|
|
34
29
|
type SkillSpec,
|
|
35
30
|
} from './agent-capabilities.js'
|
|
31
|
+
import { openClaudeRunHome } from './claude-home.js'
|
|
36
32
|
import { codexImageGapNote, createCodexHome, disposeCodexHome } from './codex-home.js'
|
|
37
33
|
import type { ProgressGuardLimits } from './progress-guard.js'
|
|
38
34
|
import { createClaudeProgressGuard } from './guard-driver.js'
|
|
@@ -42,7 +38,7 @@ import { killChildProcess, spawnDetached } from './process.js'
|
|
|
42
38
|
import { agentChildEnv } from './agent-env.js'
|
|
43
39
|
import { abortReasonOf } from './failure.js'
|
|
44
40
|
import { describeProcessExit } from './process-exit.js'
|
|
45
|
-
import { redact,
|
|
41
|
+
import { redact, secretsToRedact } from './redact.js'
|
|
46
42
|
import { createSliceTracker, startSubagentWatcher, type SliceReview } from './subagents.js'
|
|
47
43
|
import {
|
|
48
44
|
createTaskPlanTracker,
|
|
@@ -52,8 +48,7 @@ import {
|
|
|
52
48
|
toProgress,
|
|
53
49
|
todosToProgress,
|
|
54
50
|
} from './progress.js'
|
|
55
|
-
import {
|
|
56
|
-
import { retainSessionTranscripts } from './transcript-retention.js'
|
|
51
|
+
import { assertClaudeToolsCurrent, claudeCliArgs, CLAUDE_TOOL_SET } from './claude-cli.js'
|
|
57
52
|
|
|
58
53
|
// The alternate (subscription) harness runners. The Pi harness reaches models
|
|
59
54
|
// through the LLM proxy with a model-locked session token; the Claude Code and
|
|
@@ -481,82 +476,6 @@ export function carryClaudeSystemPrompt(
|
|
|
481
476
|
// Claude Code
|
|
482
477
|
// ---------------------------------------------------------------------------
|
|
483
478
|
|
|
484
|
-
/**
|
|
485
|
-
* Run the Claude Code CLI headlessly against `opts.cwd`, authenticated with the
|
|
486
|
-
* leased subscription OAuth token (CLAUDE_CODE_OAUTH_TOKEN), talking direct to
|
|
487
|
-
* api.anthropic.com. Streams `--output-format stream-json`, mapping the
|
|
488
|
-
* `TodoWrite` tool calls onto subtask progress and the terminal `result` event
|
|
489
|
-
* onto the summary + usage.
|
|
490
|
-
*/
|
|
491
|
-
/**
|
|
492
|
-
* Write a repo-sourced skill as a NATIVE Claude Code skill under `<skillsRoot>/<name>/`: a
|
|
493
|
-
* `SKILL.md` (YAML frontmatter `name`/`description` + the instructions body, the format the CLI
|
|
494
|
-
* expects) plus every resource file at its path within the skill directory. Resource sub-paths
|
|
495
|
-
* were sanitized at the job boundary (no traversal), so nested dirs are created as needed.
|
|
496
|
-
*
|
|
497
|
-
* The frontmatter `name`/`description` values are emitted as JSON-encoded (double-quoted) YAML
|
|
498
|
-
* scalars, not bare plain scalars: an author's description routinely contains `: ` (colon-space)
|
|
499
|
-
* or a leading YAML indicator (`#`, `-`, `[`, `{`, `"`, …), which is invalid as a plain scalar and
|
|
500
|
-
* would make the CLI fail to parse the frontmatter and silently skip the skill. A JSON string is a
|
|
501
|
-
* valid YAML double-quoted scalar, so quoting makes the manifest robust to arbitrary text.
|
|
502
|
-
*/
|
|
503
|
-
async function writeNativeSkill(skillsRoot: string, skill: SkillSpec): Promise<void> {
|
|
504
|
-
const dir = join(skillsRoot, skill.name)
|
|
505
|
-
await mkdir(dir, { recursive: true })
|
|
506
|
-
const name = JSON.stringify(skill.name)
|
|
507
|
-
const description = JSON.stringify(skill.description.replace(/\r?\n/g, ' '))
|
|
508
|
-
const frontmatter = `---\nname: ${name}\ndescription: ${description}\n---\n`
|
|
509
|
-
await writeFile(join(dir, 'SKILL.md'), `${frontmatter}\n${skill.instructions}\n`, 'utf8')
|
|
510
|
-
for (const resource of skill.resources) {
|
|
511
|
-
const dest = join(dir, resource.relPath)
|
|
512
|
-
await mkdir(dirname(dest), { recursive: true })
|
|
513
|
-
await writeFile(dest, resource.content, 'utf8')
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
/**
|
|
518
|
-
* Prepare the Claude Code CLI's MCP wiring for one run: write the servers to a PER-RUN config and
|
|
519
|
-
* return the argv that points the CLI at it, plus the cleanup for a directory we had to mint.
|
|
520
|
-
*
|
|
521
|
-
* Two decisions live here. `--strict-mcp-config` makes that file the ONLY source of servers, so an
|
|
522
|
-
* ambient run on a developer's own machine can never silently hand the agent their personal ones.
|
|
523
|
-
* And `--allowedTools` is passed ONLY when a server actually narrows its tools — an allow-list is
|
|
524
|
-
* whole-session, not MCP-scoped, so `claudeAllowedToolPatterns` re-grants the CLI's built-in
|
|
525
|
-
* file/bash tools in the same list; see it for why that holds whichever way the run's permission
|
|
526
|
-
* mode treats an allow-list.
|
|
527
|
-
*
|
|
528
|
-
* The config carries this job's resolved credentials, so it goes in the isolated config home when
|
|
529
|
-
* we own one and a throwaway per-JOB directory otherwise — never the checkout (it would land in a
|
|
530
|
-
* commit) and never a shared HOME path (a concurrent job would clobber it).
|
|
531
|
-
*/
|
|
532
|
-
async function setUpClaudeMcp(
|
|
533
|
-
servers: McpServerSpec[] | undefined,
|
|
534
|
-
configHome: string | undefined,
|
|
535
|
-
): Promise<{ args: string[]; cleanup: () => Promise<void> }> {
|
|
536
|
-
const noop = { args: [], cleanup: async () => {} }
|
|
537
|
-
if (!servers?.length) return noop
|
|
538
|
-
// Before anything can spawn: a failing MCP server echoes its own argv/headers into stderr, and
|
|
539
|
-
// that tail is carried onto the step's diagnostics.
|
|
540
|
-
registerKnownSecrets(mcpServerSecretValues(servers))
|
|
541
|
-
const home = configHome ?? (await mkdtemp(join(tmpdir(), 'cf-claude-mcp-')))
|
|
542
|
-
const owned = home === configHome ? undefined : home
|
|
543
|
-
const cleanup = async (): Promise<void> => {
|
|
544
|
-
if (owned) await rm(owned, { recursive: true, force: true }).catch(() => {})
|
|
545
|
-
}
|
|
546
|
-
const configPath = await writeClaudeMcpConfig(home, servers)
|
|
547
|
-
if (!configPath) return { args: [], cleanup }
|
|
548
|
-
const allowedTools = claudeAllowedToolPatterns(servers)
|
|
549
|
-
return {
|
|
550
|
-
args: [
|
|
551
|
-
'--mcp-config',
|
|
552
|
-
configPath,
|
|
553
|
-
'--strict-mcp-config',
|
|
554
|
-
...(allowedTools?.length ? ['--allowedTools', allowedTools.join(',')] : []),
|
|
555
|
-
],
|
|
556
|
-
cleanup,
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
479
|
/**
|
|
561
480
|
* The LIVE publishers of a claude-code run: everything the stream has revealed so far that the
|
|
562
481
|
* backend should see before the run ends, rather than only in its terminal result.
|
|
@@ -736,6 +655,13 @@ function openClaudeCallCapture(
|
|
|
736
655
|
}
|
|
737
656
|
}
|
|
738
657
|
|
|
658
|
+
/**
|
|
659
|
+
* Run the Claude Code CLI headlessly against `opts.cwd`, authenticated with the
|
|
660
|
+
* leased subscription OAuth token (CLAUDE_CODE_OAUTH_TOKEN), talking direct to
|
|
661
|
+
* api.anthropic.com. Streams `--output-format stream-json`, mapping the
|
|
662
|
+
* `TodoWrite` tool calls onto subtask progress and the terminal `result` event
|
|
663
|
+
* onto the summary + usage.
|
|
664
|
+
*/
|
|
739
665
|
export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRunOutcome> {
|
|
740
666
|
const stats: PiRunStats = { toolCalls: 0, assistantChars: 0 }
|
|
741
667
|
let summary = ''
|
|
@@ -754,6 +680,10 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
754
680
|
})
|
|
755
681
|
}
|
|
756
682
|
|
|
683
|
+
// The built-in tools this run declares, named ONCE: the same list rides `--tools` and the
|
|
684
|
+
// `--allowedTools` re-grant, which is additive rather than inert (see `claudeAllowedToolPatterns`).
|
|
685
|
+
const tools = CLAUDE_TOOL_SET
|
|
686
|
+
|
|
757
687
|
const secrets = opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : []
|
|
758
688
|
const capture = openClaudeCallCapture(opts, { prompt, folded, secrets })
|
|
759
689
|
const telemetry = capture.telemetry
|
|
@@ -798,6 +728,9 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
798
728
|
const onEvent = (event: Record<string, unknown>, meta?: { final?: boolean }): void => {
|
|
799
729
|
const type = event.type
|
|
800
730
|
reportToolServerStartup(event, opts.onToolServers)
|
|
731
|
+
// The same startup event answers what the CLI granted of what we asked for; a capability it
|
|
732
|
+
// named no tool for is a silent capability loss otherwise (see `assertClaudeToolsCurrent`).
|
|
733
|
+
assertClaudeToolsCurrent(event, tools, opts.log)
|
|
801
734
|
// A subagent's turns ride the parent's stdout tagged with the dispatch that spawned them;
|
|
802
735
|
// `telemetry` routes them off the parent's chain (and decides who bills them). Progress, slice
|
|
803
736
|
// tracking, the guard and `stats` below deliberately see EVERY event: a subagent grinding on
|
|
@@ -853,7 +786,7 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
853
786
|
}
|
|
854
787
|
}
|
|
855
788
|
|
|
856
|
-
const home = await openClaudeRunHome(opts)
|
|
789
|
+
const home = await openClaudeRunHome(opts, tools)
|
|
857
790
|
const { configHome } = home
|
|
858
791
|
|
|
859
792
|
// ADR 0026 D3 (path corrected by ADR 0027 Defect A): while the run is live, tail the CLI's
|
|
@@ -887,22 +820,7 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
887
820
|
const { stderrTail } = await streamCli(
|
|
888
821
|
{
|
|
889
822
|
command: 'claude',
|
|
890
|
-
args:
|
|
891
|
-
'-p',
|
|
892
|
-
'--output-format',
|
|
893
|
-
'stream-json',
|
|
894
|
-
'--verbose',
|
|
895
|
-
// The per-run container IS the sandbox, and the run is fully headless (no one
|
|
896
|
-
// to approve a tool call) — so bypass permissions entirely. `acceptEdits`
|
|
897
|
-
// would auto-accept file edits but still gate Bash, which in `-p` mode is then
|
|
898
|
-
// denied, leaving the agent unable to run builds/tests/git to verify its work.
|
|
899
|
-
'--permission-mode',
|
|
900
|
-
'bypassPermissions',
|
|
901
|
-
'--model',
|
|
902
|
-
opts.model,
|
|
903
|
-
...home.mcpArgs,
|
|
904
|
-
...appendArgs,
|
|
905
|
-
],
|
|
823
|
+
args: claudeCliArgs({ model: opts.model, tools, mcpArgs: home.mcpArgs, appendArgs }),
|
|
906
824
|
},
|
|
907
825
|
prompt,
|
|
908
826
|
{ ...opts, signal: runSignal },
|
|
@@ -959,118 +877,6 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
959
877
|
}
|
|
960
878
|
}
|
|
961
879
|
|
|
962
|
-
/**
|
|
963
|
-
* The isolated, per-run home the `claude` CLI runs against: a temp config dir OUTSIDE the cloned
|
|
964
|
-
* checkout, pre-seeded past the first-launch prompts, carrying the run's native skills and MCP
|
|
965
|
-
* config, plus the child env pointing the CLI at it. {@link ClaudeRunHome.dispose} is the other
|
|
966
|
-
* half of the same concern — the leased credential must never outlive the run — so acquisition
|
|
967
|
-
* and teardown are defined together rather than split across a `finally` forty lines away.
|
|
968
|
-
*
|
|
969
|
-
* Ambient (native) mode has NO home: the developer's installed CLI uses its own `~/.claude`
|
|
970
|
-
* login, so nothing is created, nothing is pre-seeded, and `dispose` only clears the MCP config.
|
|
971
|
-
*/
|
|
972
|
-
interface ClaudeRunHome {
|
|
973
|
-
/** The per-run config dir; `undefined` in ambient mode (the developer's own login is used). */
|
|
974
|
-
configHome: string | undefined
|
|
975
|
-
/** The CLI argv selecting the run's tool servers; empty when it has none. */
|
|
976
|
-
mcpArgs: string[]
|
|
977
|
-
/** The child-process env (see {@link buildClaudeEnv}). */
|
|
978
|
-
env: Record<string, string>
|
|
979
|
-
dispose: () => Promise<void>
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
async function openClaudeRunHome(opts: SubscriptionRunOptions): Promise<ClaudeRunHome> {
|
|
983
|
-
// Native (ambient) mode: run the developer's installed `claude` with its OWN login —
|
|
984
|
-
// no isolated config home, no injected credential, no onboarding pre-seed. Otherwise,
|
|
985
|
-
// Claude Code persists user config/credentials under its config dir; point that at an
|
|
986
|
-
// isolated, per-run temp dir OUTSIDE the cloned checkout (`opts.cwd`). Otherwise the
|
|
987
|
-
// agents that finish with `git add -A` (blueprint/requirements/bootstrap) could stage a
|
|
988
|
-
// stray `.claude/` directory — and any cached credential in it — into the pushed branch.
|
|
989
|
-
// Mirrors the Codex CODEX_HOME isolation below; removed by `dispose`.
|
|
990
|
-
if (!opts.ambientAuth && !opts.subscriptionToken) {
|
|
991
|
-
throw new Error('claude-code harness requires a subscription token (or ambientAuth)')
|
|
992
|
-
}
|
|
993
|
-
const configHome = opts.ambientAuth ? undefined : await mkdtemp(join(tmpdir(), 'cf-claude-'))
|
|
994
|
-
|
|
995
|
-
// The config dir is brand-new every run, so Claude Code would otherwise treat this
|
|
996
|
-
// as a first launch and BLOCK on the interactive onboarding / "trust this folder" /
|
|
997
|
-
// bypass-permissions acknowledgement prompts — which never get answered headlessly,
|
|
998
|
-
// hanging the job until the watchdog kills it. Pre-seed the config that marks those
|
|
999
|
-
// as already accepted so `-p` starts straight into the run. Best-effort: written
|
|
1000
|
-
// before the CLI starts; unknown keys are harmless if a CLI version ignores them.
|
|
1001
|
-
// (Ambient mode skips this — the developer's own config is already onboarded.)
|
|
1002
|
-
// ADR 0026 D4: assert the pinned onboarding keys landed and log them with the CLI
|
|
1003
|
-
// version, so a future first-run gate this set doesn't cover (which looks identical to
|
|
1004
|
-
// a healthy-but-quiet subagent start) is diffable when the cold-start watchdog fires.
|
|
1005
|
-
if (configHome) {
|
|
1006
|
-
await writeOnboardingPreseed(configHome)
|
|
1007
|
-
await assertOnboardingKeysCurrent(configHome, process.env.CLAUDE_CLI_VERSION, opts.log)
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// Skills: install each as a native skill under the config dir's `skills/<name>/` so the CLI
|
|
1011
|
-
// discovers and can invoke it. ONLY into the isolated per-run config home — never the
|
|
1012
|
-
// developer's own `~/.claude` (ambient/native mode), where it would persist in their personal
|
|
1013
|
-
// setup after the run and two concurrent jobs carrying same-named skills would clobber each
|
|
1014
|
-
// other. An ambient run reads the skills from the checkout instead (`.cat-context/skill/<name>/`,
|
|
1015
|
-
// materialised by the caller). Best-effort: a write failure must not wedge the run — the prompt
|
|
1016
|
-
// still names the skills.
|
|
1017
|
-
if (configHome) {
|
|
1018
|
-
for (const skill of opts.skills ?? []) {
|
|
1019
|
-
await writeNativeSkill(join(configHome, 'skills'), skill).catch(() => {})
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
// Tool servers (MCP): the CLI is pointed at a per-run config rather than discovering an ambient
|
|
1024
|
-
// one. See `setUpClaudeMcp` for why that matters and what has to be cleaned up afterwards.
|
|
1025
|
-
const mcp = await setUpClaudeMcp(opts.mcpServers, configHome)
|
|
1026
|
-
|
|
1027
|
-
return {
|
|
1028
|
-
configHome,
|
|
1029
|
-
mcpArgs: mcp.args,
|
|
1030
|
-
env: buildClaudeEnv(opts, configHome),
|
|
1031
|
-
dispose: async () => {
|
|
1032
|
-
// The ambient-mode MCP config dir (credential-bearing) never outlives the run.
|
|
1033
|
-
await mcp.cleanup()
|
|
1034
|
-
if (!configHome) return
|
|
1035
|
-
// Lift the CLI session transcripts (`projects/`) out for short-lived retention BEFORE the
|
|
1036
|
-
// home is deleted — the credential lives at the home root, never in `projects/`, so this
|
|
1037
|
-
// keeps the debugging artifact without leaking the token. Best-effort; never throws.
|
|
1038
|
-
await retainSessionTranscripts(configHome, ['projects'], {
|
|
1039
|
-
label: 'claude-code',
|
|
1040
|
-
...(opts.log ? { log: opts.log } : {}),
|
|
1041
|
-
})
|
|
1042
|
-
// Never leave the config dir (and any cached credential) on disk past the run.
|
|
1043
|
-
await rm(configHome, { recursive: true, force: true }).catch(() => {})
|
|
1044
|
-
},
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
/**
|
|
1049
|
-
* Build the child-process env for the `claude` CLI: an isolated config home plus subscription
|
|
1050
|
-
* auth (Anthropic OAuth token, or an Anthropic-compatible base URL + auth token for a
|
|
1051
|
-
* non-Anthropic Claude-Code vendor like GLM/Kimi/DeepSeek), or an empty env in ambient mode
|
|
1052
|
-
* (the developer's own logged-in `~/.claude` is used). Extracted from {@link runClaudeCode} to
|
|
1053
|
-
* keep its cyclomatic complexity down; behaviour is a straight move of the original expression.
|
|
1054
|
-
*/
|
|
1055
|
-
function buildClaudeEnv(
|
|
1056
|
-
opts: SubscriptionRunOptions,
|
|
1057
|
-
configHome: string | undefined,
|
|
1058
|
-
): Record<string, string> {
|
|
1059
|
-
// The job-scoped env rides along in BOTH modes; the credential/config vars below are what
|
|
1060
|
-
// ambient mode drops (the developer's own logged-in `~/.claude` is used instead).
|
|
1061
|
-
if (opts.ambientAuth) return { ...opts.extraEnv }
|
|
1062
|
-
return {
|
|
1063
|
-
...opts.extraEnv,
|
|
1064
|
-
CLAUDE_CONFIG_DIR: configHome!,
|
|
1065
|
-
...(opts.subscriptionBaseUrl
|
|
1066
|
-
? {
|
|
1067
|
-
ANTHROPIC_BASE_URL: opts.subscriptionBaseUrl,
|
|
1068
|
-
ANTHROPIC_AUTH_TOKEN: opts.subscriptionToken!,
|
|
1069
|
-
}
|
|
1070
|
-
: { CLAUDE_CODE_OAUTH_TOKEN: opts.subscriptionToken! }),
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
880
|
/**
|
|
1075
881
|
* Merge the parent-loop telemetry with the subagents' out-of-band usage + per-call metrics into
|
|
1076
882
|
* the run outcome. INVARIANT (do not "fix" this into a double count): the run total is the parent
|
|
@@ -1257,6 +1063,12 @@ export async function runCodex(opts: SubscriptionRunOptions): Promise<PiRunOutco
|
|
|
1257
1063
|
'exec',
|
|
1258
1064
|
'--json',
|
|
1259
1065
|
'--skip-git-repo-check',
|
|
1066
|
+
// No `--tools` analogue here, and its absence is a FINDING rather than an oversight:
|
|
1067
|
+
// codex has no flag that declares a built-in tool set, because it has no set to choose
|
|
1068
|
+
// from. Its surface is shell + apply_patch + the plan tool, and the optional extras are
|
|
1069
|
+
// individual `CODEX_HOME/config.toml` switches the harness already sets deliberately
|
|
1070
|
+
// (`[features] image_generation`, see `codex-home.ts`). So there is nothing here that
|
|
1071
|
+
// silently drifts with a CLI version the way claude-code's headless default did.
|
|
1260
1072
|
// The per-run container IS the sandbox; let Codex write files and reach the
|
|
1261
1073
|
// vendor unrestricted, with no approval prompts (the run is headless).
|
|
1262
1074
|
'--dangerously-bypass-approvals-and-sandbox',
|
package/src/agent-shared.ts
CHANGED
|
@@ -18,11 +18,18 @@ export function mergeEffort(
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* The agent-capability fields (skills, tool servers, reference designs) every
|
|
22
|
-
* forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow
|
|
23
|
-
* cannot silently be the one that drops a kind's declared playbook, tool server
|
|
24
|
-
* gallery: the failure mode is invisible (the agent simply works without
|
|
25
|
-
* up as degraded output.
|
|
21
|
+
* The agent-capability fields (skills, tool servers, reference designs, web research) every
|
|
22
|
+
* agent-running flow forwards to {@link runAgentInWorkspace}. One helper rather than a per-flow
|
|
23
|
+
* spread, so a flow cannot silently be the one that drops a kind's declared playbook, tool server,
|
|
24
|
+
* reference gallery or web access: the failure mode is invisible (the agent simply works without
|
|
25
|
+
* it) and would only show up as degraded output.
|
|
26
|
+
*
|
|
27
|
+
* Web research joined the helper after the conflict-resolver and bootstrap flows were found to be
|
|
28
|
+
* forwarding neither half of it: both build their own spec literal, and the two web fields were
|
|
29
|
+
* hand-written at the four sites that remembered them. That is exactly the drift this helper
|
|
30
|
+
* exists to make unrepresentable, so they are read here rather than at each call site. Both halves
|
|
31
|
+
* travel together on purpose: the guidance NAMES the tools, so a flow carrying one without the
|
|
32
|
+
* other either describes tools the run was never given or hands it tools nothing introduced.
|
|
26
33
|
*/
|
|
27
34
|
export function agentCapabilities(job: AgentJob): {
|
|
28
35
|
skills?: SkillSpec[]
|
|
@@ -30,6 +37,8 @@ export function agentCapabilities(job: AgentJob): {
|
|
|
30
37
|
generateImages?: boolean
|
|
31
38
|
referenceScreenshots?: ImageManifestSpec
|
|
32
39
|
designImages?: ImageManifestSpec
|
|
40
|
+
webSearchProxy?: boolean
|
|
41
|
+
webToolsGuidance?: string
|
|
33
42
|
} {
|
|
34
43
|
return {
|
|
35
44
|
...(job.skills?.length ? { skills: job.skills } : {}),
|
|
@@ -37,5 +46,7 @@ export function agentCapabilities(job: AgentJob): {
|
|
|
37
46
|
...(job.generateImages ? { generateImages: true } : {}),
|
|
38
47
|
...(job.referenceScreenshots ? { referenceScreenshots: job.referenceScreenshots } : {}),
|
|
39
48
|
...(job.designImages ? { designImages: job.designImages } : {}),
|
|
49
|
+
...(job.webSearch ? { webSearchProxy: true } : {}),
|
|
50
|
+
...(job.webToolsGuidance ? { webToolsGuidance: job.webToolsGuidance } : {}),
|
|
40
51
|
}
|
|
41
52
|
}
|
package/src/agent.ts
CHANGED
|
@@ -495,8 +495,6 @@ async function runExploreMode(job: AgentJob, opts: RunOptions): Promise<AgentRes
|
|
|
495
495
|
// Read-only: it inspects and reports, making no edits — so the no-progress
|
|
496
496
|
// guard's no-edit bound must not fire on its legitimately edit-free run.
|
|
497
497
|
expectsEdits: false,
|
|
498
|
-
webToolsGuidance: job.webToolsGuidance,
|
|
499
|
-
webSearchProxy: job.webSearch,
|
|
500
498
|
contextFiles: job.contextFiles,
|
|
501
499
|
guardLimits: job.guardLimits,
|
|
502
500
|
...agentCapabilities(job),
|
|
@@ -746,8 +744,6 @@ async function runMultiRepoExplore(job: AgentJob, opts: RunOptions): Promise<Age
|
|
|
746
744
|
sessionToken: job.sessionToken,
|
|
747
745
|
// Read-only: no edits expected, so the no-progress guard's no-edit bound must not fire.
|
|
748
746
|
expectsEdits: false,
|
|
749
|
-
webToolsGuidance: job.webToolsGuidance,
|
|
750
|
-
webSearchProxy: job.webSearch,
|
|
751
747
|
...(job.contextFiles ? { contextFiles: job.contextFiles } : {}),
|
|
752
748
|
guardLimits: job.guardLimits,
|
|
753
749
|
...agentCapabilities(job),
|
|
@@ -868,8 +864,6 @@ export function buildSingleRepoCodingSpec(
|
|
|
868
864
|
proxyPhasePath: job.proxyPhasePath,
|
|
869
865
|
sessionToken: job.sessionToken,
|
|
870
866
|
commitMessage: job.commitMessage ?? job.pr?.title ?? 'Agent changes',
|
|
871
|
-
webToolsGuidance: job.webToolsGuidance,
|
|
872
|
-
webSearchProxy: job.webSearch,
|
|
873
867
|
guardLimits: job.guardLimits,
|
|
874
868
|
...(job.persistentCheckout ? { persistentCheckout: true } : {}),
|
|
875
869
|
...(job.streamFollowUps ? { streamFollowUps: true } : {}),
|