@jini-ai/daemon 0.3.1 → 0.3.6
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/dist/agent-executor.d.ts +197 -25
- package/dist/agent-executor.d.ts.map +1 -1
- package/dist/agent-executor.js +385 -52
- package/dist/agent-executor.js.map +1 -1
- package/dist/run-lifecycle.d.ts +56 -1
- package/dist/run-lifecycle.d.ts.map +1 -1
- package/dist/run-lifecycle.js +109 -0
- package/dist/run-lifecycle.js.map +1 -1
- package/dist/tool-result-surfaces.d.ts +1 -1
- package/dist/tool-result-surfaces.js +1 -1
- package/package.json +3 -3
package/dist/agent-executor.js
CHANGED
|
@@ -849,31 +849,123 @@ export function buildCodexMcpServerToml(entry) {
|
|
|
849
849
|
return serverTable;
|
|
850
850
|
return `${serverTable}\n[mcp_servers.${JINI_MCP_SERVER_KEY}.env]\n${envLines.join('\n')}\n`;
|
|
851
851
|
}
|
|
852
|
+
/**
|
|
853
|
+
* Splits a TOML table header's dotted key into its individual segments, honoring quoted parts
|
|
854
|
+
* (`"basic"` or `'literal'`) that may themselves contain a literal `.` — a bare `.` only
|
|
855
|
+
* separates segments outside of quotes. Each segment is trimmed and, if quoted, unwrapped.
|
|
856
|
+
*
|
|
857
|
+
* Not a full TOML parser: it does not resolve escape sequences (`\"`, `\u...`) inside basic
|
|
858
|
+
* strings. That is deliberately out of scope — see {@link stripExistingJiniMcpServerTable}'s doc
|
|
859
|
+
* for why it is safe to skip for this specific comparison.
|
|
860
|
+
* @param rawKey - The raw text between a table header's `[` and `]`.
|
|
861
|
+
* @returns The dotted key's segments, dequoted and trimmed.
|
|
862
|
+
* @complexity O(n) in the length of `rawKey`.
|
|
863
|
+
*/
|
|
864
|
+
function splitTomlDottedKey(rawKey) {
|
|
865
|
+
const segments = [];
|
|
866
|
+
let current = '';
|
|
867
|
+
let quote = null;
|
|
868
|
+
for (const ch of rawKey) {
|
|
869
|
+
if (quote) {
|
|
870
|
+
if (ch === quote)
|
|
871
|
+
quote = null;
|
|
872
|
+
else
|
|
873
|
+
current += ch;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (ch === '"' || ch === "'") {
|
|
877
|
+
quote = ch;
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (ch === '.') {
|
|
881
|
+
segments.push(current.trim());
|
|
882
|
+
current = '';
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
current += ch;
|
|
886
|
+
}
|
|
887
|
+
segments.push(current.trim());
|
|
888
|
+
return segments;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Removes any pre-existing `[mcp_servers.{@link JINI_MCP_SERVER_KEY}]` table — and its
|
|
892
|
+
* `[mcp_servers.{@link JINI_MCP_SERVER_KEY}.*]` subtables (e.g. `.env`) — from a real Codex
|
|
893
|
+
* `config.toml`'s raw text, so {@link buildCodexHomeConfigToml} can append this run's own table
|
|
894
|
+
* without producing the duplicate TOML key Codex's parser rejects at startup.
|
|
895
|
+
*
|
|
896
|
+
* Line-oriented, not a real TOML parser (matching {@link buildCodexMcpServerToml}'s own
|
|
897
|
+
* no-TOML-dependency constraint), but wide enough to survive a hand-edited config, which is the
|
|
898
|
+
* scenario this whole function exists for. A table header line is recognized when, after
|
|
899
|
+
* trimming, it is `[<key>]` optionally followed only by a `# comment` (TOML's own grammar allows
|
|
900
|
+
* nothing else there) — so it tolerates leading indentation, whitespace inside the brackets, a
|
|
901
|
+
* trailing line comment, and a dotted key written with quoted segments (`["mcp_servers"."jini"]`),
|
|
902
|
+
* via {@link splitTomlDottedKey}. Once such a header's key matches the jini table or one of its
|
|
903
|
+
* subtables, every following line is dropped until the next table header (of any name) or EOF.
|
|
904
|
+
* Every other table, key, comment-only, and blank line is passed through untouched.
|
|
905
|
+
*
|
|
906
|
+
* Deliberately unhandled, and why it is safe to leave that way:
|
|
907
|
+
* - **Escape sequences inside quoted key segments** (e.g. a segment containing `\"`) — neither
|
|
908
|
+
* `mcp_servers` nor {@link JINI_MCP_SERVER_KEY} ever needs escaping, and this driver's own
|
|
909
|
+
* writer ({@link buildCodexMcpServerToml}) never emits a quoted form at all, so no real
|
|
910
|
+
* `config.toml` this driver produced can exercise this gap; a hand-edit that goes out of its
|
|
911
|
+
* way to escape a character inside a key that is supposed to spell "jini" would fail to match
|
|
912
|
+
* and fall back to today's pre-fix behavior (append a duplicate) rather than silently doing
|
|
913
|
+
* something worse.
|
|
914
|
+
* - **`[[array-of-tables]]` syntax** — Codex's schema has no array-of-tables shape for
|
|
915
|
+
* `mcp_servers`, and the header regex's `[^[\]]+` capture cannot match a line starting with a
|
|
916
|
+
* second `[`, so `[[mcp_servers.jini]]` is not recognized as a header before or after this fix
|
|
917
|
+
* — unchanged, not a new gap.
|
|
918
|
+
* - **A single quoted segment that happens to spell the same characters with the dot included**
|
|
919
|
+
* (e.g. `["mcp_servers.jini"]`, which in real TOML names one key literally containing a `.`,
|
|
920
|
+
* not two nested tables) — this collapses to the same joined string as the real nested-table
|
|
921
|
+
* spelling and is therefore treated as a match. This is a false positive in the conservative
|
|
922
|
+
* direction the reported bug calls for: a missed detection duplicates a key and crashes Codex,
|
|
923
|
+
* so on this axis over-matching a spelling nobody would plausibly hand-write for an MCP server
|
|
924
|
+
* table is preferable to under-matching the real one.
|
|
925
|
+
* @param existingRaw - The real Codex home's `config.toml` content, already known to be defined
|
|
926
|
+
* (callers pass `''` for a missing file).
|
|
927
|
+
* @returns `existingRaw` with any jini table/subtable removed.
|
|
928
|
+
* @complexity O(n) in the number of lines.
|
|
929
|
+
*/
|
|
930
|
+
function stripExistingJiniMcpServerTable(existingRaw) {
|
|
931
|
+
const jiniTableKey = `mcp_servers.${JINI_MCP_SERVER_KEY}`;
|
|
932
|
+
const kept = [];
|
|
933
|
+
let skipping = false;
|
|
934
|
+
for (const line of existingRaw.split('\n')) {
|
|
935
|
+
const header = /^\s*\[([^[\]]+)\]\s*(#.*)?$/.exec(line);
|
|
936
|
+
if (header) {
|
|
937
|
+
const key = splitTomlDottedKey(header[1] ?? '').join('.');
|
|
938
|
+
skipping = key === jiniTableKey || key.startsWith(`${jiniTableKey}.`);
|
|
939
|
+
if (skipping)
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (!skipping)
|
|
943
|
+
kept.push(line);
|
|
944
|
+
}
|
|
945
|
+
return kept.join('\n');
|
|
946
|
+
}
|
|
852
947
|
/**
|
|
853
948
|
* Builds the full `config.toml` a run's scratch `CODEX_HOME` gets: the real Codex home's own
|
|
854
|
-
* config,
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
* to an operator for their own config — rather than solved with a full TOML parser for one
|
|
866
|
-
* collision case.
|
|
949
|
+
* config, with any pre-existing `[mcp_servers.jini]` table removed (see
|
|
950
|
+
* {@link stripExistingJiniMcpServerTable}), then this run's own table appended.
|
|
951
|
+
*
|
|
952
|
+
* **Append-only by design, not a parse-and-merge, for everything but the jini table itself.**
|
|
953
|
+
* `mergeMcpJsonContent`/`mergeEnvContentMcpConfig` above can safely parse-merge-reserialize because
|
|
954
|
+
* their formats have a JS-native parser (`JSON.parse`); this driver has no general TOML parser in
|
|
955
|
+
* its dependency graph (see `buildCodexMcpServerToml`'s doc), and every other setting a real Codex
|
|
956
|
+
* install carries — model choice, sandbox policy, the trusted-project list, the operator's own
|
|
957
|
+
* other MCP servers — must survive a spawn byte-for-byte. Only the one table this driver itself
|
|
958
|
+
* owns (`jini` is this integration's own reserved server name — see {@link JINI_MCP_SERVER_KEY}) is
|
|
959
|
+
* ever removed, via the narrow line-oriented scan above, never a full TOML parse.
|
|
867
960
|
* @param existingRaw - The real Codex home's `config.toml` content, or `undefined` when it does not
|
|
868
961
|
* exist (a fresh Codex install — degrades to "start from just this run's block", matching
|
|
869
962
|
* {@link mergeMcpJsonContent}'s own "missing file" handling).
|
|
870
963
|
* @param entry - The shared bridge entry.
|
|
871
964
|
* @returns The full text to write to the scratch `CODEX_HOME`'s `config.toml`.
|
|
872
965
|
* @complexity O(n) in the existing config's length.
|
|
873
|
-
* @overallScore 100/100
|
|
874
966
|
*/
|
|
875
967
|
export function buildCodexHomeConfigToml(existingRaw, entry) {
|
|
876
|
-
const base = existingRaw ?? '';
|
|
968
|
+
const base = stripExistingJiniMcpServerTable(existingRaw ?? '');
|
|
877
969
|
const separator = base.length === 0 ? '' : base.endsWith('\n') ? '\n' : '\n\n';
|
|
878
970
|
return `${base}${separator}${buildCodexMcpServerToml(entry)}`;
|
|
879
971
|
}
|
|
@@ -1092,6 +1184,114 @@ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
|
|
|
1092
1184
|
},
|
|
1093
1185
|
};
|
|
1094
1186
|
}
|
|
1187
|
+
function defaultMkdtempClaudeConfigDir(prefix) {
|
|
1188
|
+
return fsPromises.mkdtemp(join(tmpdir(), prefix));
|
|
1189
|
+
}
|
|
1190
|
+
function defaultRemoveClaudeConfigDir(path) {
|
|
1191
|
+
return fsPromises.rm(path, { recursive: true, force: true });
|
|
1192
|
+
}
|
|
1193
|
+
function resolveClaudeConfigDirSeams(options) {
|
|
1194
|
+
return {
|
|
1195
|
+
mkdtemp: options?.mkdtemp ?? defaultMkdtempClaudeConfigDir,
|
|
1196
|
+
readFile: options?.readFile ?? defaultReadMcpJsonFile,
|
|
1197
|
+
writeFile: options?.writeFile ?? defaultWriteMcpJsonFile,
|
|
1198
|
+
removeDir: options?.removeDir ?? defaultRemoveClaudeConfigDir,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Where `claude`'s own config resolution reads the operator's REAL config from, to seed a run's
|
|
1203
|
+
* scratch copy — never where it writes. Mirrors {@link resolveSourceCodexHomeDir}'s exact reasoning
|
|
1204
|
+
* and resolution order: `CLAUDE_CONFIG_DIR` is not in `BASELINE_AGENT_ENV_KEYS`, so a spawned child
|
|
1205
|
+
* never inherits it anyway — the whole point is finding wherever the *operator's actual* Claude Code
|
|
1206
|
+
* install lives, a host-machine fact resolved against the daemon HOST process's own environment.
|
|
1207
|
+
* @param hostEnv - The daemon process's own environment.
|
|
1208
|
+
* @returns `hostEnv.CLAUDE_CONFIG_DIR` when set to a non-blank value (matching Claude Code's own
|
|
1209
|
+
* resolution order, confirmed against installed Claude Code 2.1.263), else the CLI's documented
|
|
1210
|
+
* default, `~/.claude`.
|
|
1211
|
+
* @complexity O(1).
|
|
1212
|
+
*/
|
|
1213
|
+
export function resolveSourceClaudeConfigDir(hostEnv) {
|
|
1214
|
+
const override = hostEnv.CLAUDE_CONFIG_DIR;
|
|
1215
|
+
return override !== undefined && override.trim().length > 0 ? override : join(homedir(), '.claude');
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Stages a fresh, randomly-named `CLAUDE_CONFIG_DIR` directory (same non-determinism requirement as
|
|
1219
|
+
* {@link McpJsonInjectionOptions.mkdtemp}'s own doc — `os.tmpdir()` is a shared location on a
|
|
1220
|
+
* multi-user host, so a guessable name is a real pre-plant/symlink target).
|
|
1221
|
+
*
|
|
1222
|
+
* **Deliberately empty by default** — unlike {@link prepareCodexHomeForRun}, which copies the real
|
|
1223
|
+
* `config.toml` wholesale (Codex has no personal-data problem in that file), this directory gets
|
|
1224
|
+
* NOTHING written into it beyond a best-effort copy of `.credentials.json` (see below). That is the
|
|
1225
|
+
* fix: the operator's real `skills`/`plugins`/`agents`/`memory-path index`/`settings.json` must NOT
|
|
1226
|
+
* carry over implicitly. `claude` runs correctly against a config directory holding nothing at all —
|
|
1227
|
+
* it falls back to its own built-in defaults, not an error.
|
|
1228
|
+
*
|
|
1229
|
+
* **Login preservation, verified rather than assumed** (per this task's own instruction — "prove
|
|
1230
|
+
* login still resolves; do not assume"): confirmed live (2026-09-07, installed Claude Code 2.1.263,
|
|
1231
|
+
* macOS) that `claude auth status` reports `loggedIn: false` against ANY `CLAUDE_CONFIG_DIR` other
|
|
1232
|
+
* than the operator's real one — including the real `HOME` with only `CLAUDE_CONFIG_DIR` swapped —
|
|
1233
|
+
* and confirmed against Claude Code's own docs (code.claude.com/docs/en/authentication) why: "If
|
|
1234
|
+
* you've set the CLAUDE_CONFIG_DIR environment variable, Claude Code keeps the .credentials.json
|
|
1235
|
+
* file under that directory instead, including the file the macOS fallback writes, and keys the
|
|
1236
|
+
* macOS Keychain entry to that directory too, so a session with a different CLAUDE_CONFIG_DIR reads
|
|
1237
|
+
* a different entry." So a scratch directory is never logged in by default on ANY platform, not just
|
|
1238
|
+
* the ones with no Keychain at all. This function only closes the *portable* case: when the source
|
|
1239
|
+
* directory holds a file-based `.credentials.json` (Linux, Windows, or a Keychain-locked macOS
|
|
1240
|
+
* fallback — none of which this function can distinguish, and does not need to), it is copied
|
|
1241
|
+
* best-effort into the scratch directory, exactly `prepareCodexHomeForRun`'s `auth.json` copy. When
|
|
1242
|
+
* it does not (a normal macOS Keychain-only install, confirmed the common case on this codebase's
|
|
1243
|
+
* own dev machine), this function does NOT attempt to read the macOS Keychain itself — that would
|
|
1244
|
+
* mean this daemon process extracting a live OAuth secret out of an OS-managed credential store into
|
|
1245
|
+
* a plaintext file, a materially different and larger security surface than forwarding an
|
|
1246
|
+
* already-resolved credential the host handed it (which `credentialEnv`/`ANTHROPIC_API_KEY` already
|
|
1247
|
+
* does, safely, today — see `AgentExecutorRunInput.credentialEnv`'s own doc). In that case this
|
|
1248
|
+
* mirrors `prepareCodexHomeForRun`'s own accepted outcome for a missing credential file verbatim:
|
|
1249
|
+
* "the spawned CLI runs unauthenticated" is documented, existing, precedented behavior in this file,
|
|
1250
|
+
* not a new failure mode invented here. A host that needs the isolated child to stay logged in on
|
|
1251
|
+
* such an install must supply a credential explicitly via `AgentExecutorRunInput.credentialEnv`
|
|
1252
|
+
* (`ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN` — both outrank Keychain-based subscription login
|
|
1253
|
+
* in Claude Code's own auth precedence, so the isolated child authenticates without ever needing the
|
|
1254
|
+
* operator's personal Keychain entry at all) — see this fix's own handoff report for the operational
|
|
1255
|
+
* consequence on a host with no such credential configured yet.
|
|
1256
|
+
*
|
|
1257
|
+
* **Never touches the real config directory.** `sourceConfigDir` is read-only throughout.
|
|
1258
|
+
* @param runId - Embedded in the temp-dir prefix for traceability, same sanitization discipline as {@link prepareCodexHomeForRun}'s `safeRunId`.
|
|
1259
|
+
* @param sourceConfigDir - Where to read a possible real `.credentials.json` from — see {@link resolveSourceClaudeConfigDir}.
|
|
1260
|
+
* @param seams - Injectable mkdtemp/readFile/writeFile/removeDir, real filesystem by default.
|
|
1261
|
+
* @throws Whatever `mkdtemp` rejects with — the caller ({@link prepareClaudeConfigDirIfNeeded}) turns that into a pre-spawn `AGENT_SPAWN_FAILED` failure, matching {@link prepareCodexHomeForRun}'s own contract.
|
|
1262
|
+
* @complexity O(1) plus one directory creation and up to one best-effort file read/write round trip.
|
|
1263
|
+
*/
|
|
1264
|
+
async function prepareClaudeConfigDirForRun(runId, sourceConfigDir, seams) {
|
|
1265
|
+
const safeRunId = runId.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 80) || 'run';
|
|
1266
|
+
const dir = await seams.mkdtemp(`jini-claude-config-${safeRunId}-`);
|
|
1267
|
+
try {
|
|
1268
|
+
const credentialsPath = join(sourceConfigDir, '.credentials.json');
|
|
1269
|
+
let credentialsRaw;
|
|
1270
|
+
try {
|
|
1271
|
+
credentialsRaw = await seams.readFile(credentialsPath);
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
// No file-based credential to copy (the common macOS-Keychain-only case) — see this
|
|
1275
|
+
// function's own doc for why that is an accepted, documented outcome, not a failure here.
|
|
1276
|
+
credentialsRaw = undefined;
|
|
1277
|
+
}
|
|
1278
|
+
if (credentialsRaw !== undefined) {
|
|
1279
|
+
await seams.writeFile(join(dir, '.credentials.json'), credentialsRaw);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
catch (err) {
|
|
1283
|
+
await seams.removeDir(dir).catch(() => {
|
|
1284
|
+
// Best-effort only — the original error below is what the caller must see either way.
|
|
1285
|
+
});
|
|
1286
|
+
throw err;
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
path: dir,
|
|
1290
|
+
cleanup: async () => {
|
|
1291
|
+
await seams.removeDir(dir);
|
|
1292
|
+
},
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1095
1295
|
/**
|
|
1096
1296
|
* Default ceiling on the `'until-close'` stdout accumulator (see `RuntimeStdoutPolicy` in
|
|
1097
1297
|
* `@jini-ai/agent-runtime`), in bytes of received UTF-8.
|
|
@@ -1273,6 +1473,13 @@ function wireChildLifecycle(ctx) {
|
|
|
1273
1473
|
// `content` below already carries the whole raw output, a pre-existing, unrelated gap), so
|
|
1274
1474
|
// there is no `remainder` to thread back in — only the extracted blocks are used here.
|
|
1275
1475
|
let media = [];
|
|
1476
|
+
// Suspend the slow-run watchdog for the duration of this daemon-awaited execution: the daemon
|
|
1477
|
+
// knows exactly why the run is quiet here (it dispatched the tool itself and is waiting on it),
|
|
1478
|
+
// so a legitimately long tool (an install, a build, a repo-wide scan) must not be mistaken for
|
|
1479
|
+
// the CPU-starved-and-silent condition the watchdog exists to catch. Resumed in `finally` so a
|
|
1480
|
+
// genuinely stalled stretch *after* this tool settles is still caught — see
|
|
1481
|
+
// `RunLifecycle.suspendSlowRunNotice`'s own doc.
|
|
1482
|
+
lifecycle.suspendSlowRunNotice(runId);
|
|
1276
1483
|
try {
|
|
1277
1484
|
const result = await continuation.toolExecutor.execute(continuation.principal, run, toolUse.name, toolUse.input);
|
|
1278
1485
|
content = resultContent(result);
|
|
@@ -1283,6 +1490,9 @@ function wireChildLifecycle(ctx) {
|
|
|
1283
1490
|
content = errorMessage(error);
|
|
1284
1491
|
isError = true;
|
|
1285
1492
|
}
|
|
1493
|
+
finally {
|
|
1494
|
+
lifecycle.resumeSlowRunNotice(runId);
|
|
1495
|
+
}
|
|
1286
1496
|
await lifecycle.emit(runId, {
|
|
1287
1497
|
event: 'agent',
|
|
1288
1498
|
data: {
|
|
@@ -1886,7 +2096,7 @@ export async function resolveMcpBridgeForRun(input, deps) {
|
|
|
1886
2096
|
* @complexity O(1) plus `mergeEnvContentMcpConfig`'s and `mergeEnvContentInstructions`'s own `JSON.parse`/`JSON.stringify` cost.
|
|
1887
2097
|
* @overallScore 100/100
|
|
1888
2098
|
*/
|
|
1889
|
-
export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptEnvOverrides, stagedInstructionsFile) {
|
|
2099
|
+
export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptEnvOverrides, stagedInstructionsFile, claudeConfigDir) {
|
|
1890
2100
|
const envContentApplied = mcpBridge?.kind === 'env-content'
|
|
1891
2101
|
? {
|
|
1892
2102
|
...spawnEnv,
|
|
@@ -1906,7 +2116,12 @@ export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptE
|
|
|
1906
2116
|
[stagedInstructionsFile.varName]: mergeEnvContentInstructions(envPassthroughApplied[stagedInstructionsFile.varName], stagedInstructionsFile.path),
|
|
1907
2117
|
};
|
|
1908
2118
|
const codexHomeApplied = codexHomeDir === undefined ? instructionsApplied : { ...instructionsApplied, CODEX_HOME: codexHomeDir };
|
|
1909
|
-
|
|
2119
|
+
// Finding 1 of SEC-assistant-env-isolation-2026-09-07: same "only set when staged" shape as
|
|
2120
|
+
// codexHomeApplied above — mutually exclusive with it in practice (codexHomeDir is only ever set
|
|
2121
|
+
// for a 'codex-toml' bridge, claudeConfigDir only ever for a `claude`-id run), so both existing
|
|
2122
|
+
// side by side here is a documentation convenience, not a real collision risk.
|
|
2123
|
+
const claudeConfigDirApplied = claudeConfigDir === undefined ? codexHomeApplied : { ...codexHomeApplied, CLAUDE_CONFIG_DIR: claudeConfigDir };
|
|
2124
|
+
return systemPromptEnvOverrides === undefined ? claudeConfigDirApplied : { ...claudeConfigDirApplied, ...systemPromptEnvOverrides };
|
|
1910
2125
|
}
|
|
1911
2126
|
/**
|
|
1912
2127
|
* Phase 6b: the `RuntimeContext` `buildArgs` receives — `undefined` unless a file, bridge path, or
|
|
@@ -1960,16 +2175,23 @@ function computeSystemPromptOverlay(promptAugmenter, agentId, runtimeContext) {
|
|
|
1960
2175
|
turnIndex: runtimeContext?.hasPriorAssistantTurn ? 1 : 0,
|
|
1961
2176
|
});
|
|
1962
2177
|
}
|
|
1963
|
-
/** Phase 9a: the def's `buildArgs` 4th argument — `undefined` when the run selects no model/reasoning/permissionMode/overlay at all (byte-identical to omitting the argument). Pure. */
|
|
2178
|
+
/** Phase 9a: the def's `buildArgs` 4th argument — `undefined` when the run selects no model/reasoning/permissionMode/overlay/tool-restriction at all (byte-identical to omitting the argument). Pure. */
|
|
1964
2179
|
export function buildAgentBuildArgsOptions(input, systemPromptOverlay) {
|
|
1965
2180
|
const hasOverlay = systemPromptOverlay !== undefined && systemPromptOverlay !== null;
|
|
1966
|
-
if (input.model === undefined
|
|
2181
|
+
if (input.model === undefined
|
|
2182
|
+
&& input.reasoning === undefined
|
|
2183
|
+
&& input.permissionMode === undefined
|
|
2184
|
+
&& input.disallowedTools === undefined
|
|
2185
|
+
&& input.allowedTools === undefined
|
|
2186
|
+
&& !hasOverlay) {
|
|
1967
2187
|
return undefined;
|
|
1968
2188
|
}
|
|
1969
2189
|
return {
|
|
1970
2190
|
...(input.model !== undefined ? { model: input.model } : {}),
|
|
1971
2191
|
...(input.reasoning !== undefined ? { reasoning: input.reasoning } : {}),
|
|
1972
2192
|
...(input.permissionMode !== undefined ? { permissionMode: input.permissionMode } : {}),
|
|
2193
|
+
...(input.disallowedTools !== undefined ? { disallowedTools: input.disallowedTools } : {}),
|
|
2194
|
+
...(input.allowedTools !== undefined ? { allowedTools: input.allowedTools } : {}),
|
|
1973
2195
|
...(hasOverlay ? { systemPromptOverlay } : {}),
|
|
1974
2196
|
};
|
|
1975
2197
|
}
|
|
@@ -2008,16 +2230,18 @@ export function buildAgentBuildArgsOptions(input, systemPromptOverlay) {
|
|
|
2008
2230
|
* @param input.prompt - The composed prompt `buildArgs` would otherwise receive verbatim.
|
|
2009
2231
|
* @param input.resumeSessionId - This run's `RuntimeContext.resumeSessionId`; presence means an
|
|
2010
2232
|
* existing session is being continued, not created.
|
|
2011
|
-
* @returns
|
|
2012
|
-
*
|
|
2013
|
-
*
|
|
2233
|
+
* @returns A {@link SystemPromptOverlayDelivery}: the prefix this run's prompt text must carry
|
|
2234
|
+
* (`''` unless the fallback applies), that prefix already applied to `input.prompt`, any extra argv
|
|
2235
|
+
* to append to whatever `buildArgs` itself returns, and any env var overrides to merge into the
|
|
2236
|
+
* spawn env (`{}` for every strategy but `'env-var'`). Every one of `run()`'s four prompt
|
|
2237
|
+
* transports consumes this same result — see the interface's own no-double-delivery note.
|
|
2014
2238
|
* @complexity O(n) in the overlay/prompt lengths — string concatenation only, no I/O.
|
|
2015
2239
|
* @overallScore 100/100
|
|
2016
2240
|
*/
|
|
2017
2241
|
export function resolveSystemPromptOverlayDelivery(input) {
|
|
2018
2242
|
const { defId, systemPromptDelivery, resumesSessionViaCli, resumesSessionViaAcpLoad, overlay, prompt, resumeSessionId } = input;
|
|
2019
2243
|
if (typeof overlay !== 'string' || overlay.length === 0) {
|
|
2020
|
-
return { prompt, extraArgs: [], envOverrides: {} };
|
|
2244
|
+
return { promptPrefix: '', prompt, extraArgs: [], envOverrides: {} };
|
|
2021
2245
|
}
|
|
2022
2246
|
if (systemPromptDelivery?.strategy === 'append-flag') {
|
|
2023
2247
|
const capabilityKey = systemPromptDelivery.capabilityKey;
|
|
@@ -2028,14 +2252,14 @@ export function resolveSystemPromptOverlayDelivery(input) {
|
|
|
2028
2252
|
// (e.g. `pi`'s existing `--append-system-prompt`, trusted unconditionally) always passes, same
|
|
2029
2253
|
// as an absent key.
|
|
2030
2254
|
const capabilityOk = capabilityKey === undefined || agentCapabilities.get(defId)?.[capabilityKey] !== false;
|
|
2031
|
-
return { prompt, extraArgs: capabilityOk ? [systemPromptDelivery.flag, overlay] : [], envOverrides: {} };
|
|
2255
|
+
return { promptPrefix: '', prompt, extraArgs: capabilityOk ? [systemPromptDelivery.flag, overlay] : [], envOverrides: {} };
|
|
2032
2256
|
}
|
|
2033
2257
|
if (systemPromptDelivery?.strategy === 'env-var') {
|
|
2034
2258
|
// No capability gate, unlike `'append-flag'`: an unrecognized env var is inert to a CLI (it
|
|
2035
2259
|
// simply never reads it), never a fatal "unknown option" exit — there is no equivalent hazard
|
|
2036
2260
|
// to probe-gate against here. Set verbatim, not merged with any existing value — a dedicated
|
|
2037
2261
|
// single-purpose var, not a shared config channel (see this field's own `types.ts` doc).
|
|
2038
|
-
return { prompt, extraArgs: [], envOverrides: { [systemPromptDelivery.varName]: overlay } };
|
|
2262
|
+
return { promptPrefix: '', prompt, extraArgs: [], envOverrides: { [systemPromptDelivery.varName]: overlay } };
|
|
2039
2263
|
}
|
|
2040
2264
|
if (systemPromptDelivery?.strategy === 'config-instructions-file') {
|
|
2041
2265
|
// Delivered elsewhere, not here: unlike `'append-flag'`/`'env-var'`, this mechanism needs real
|
|
@@ -2047,13 +2271,13 @@ export function resolveSystemPromptOverlayDelivery(input) {
|
|
|
2047
2271
|
// This branch's only job is to make sure the universal prefix fallback below does NOT ALSO run
|
|
2048
2272
|
// for a def that already has this strategy declared — the same "no double delivery" concern
|
|
2049
2273
|
// `imageDelivery`'s doc calls out for its own native-vs-fallback split.
|
|
2050
|
-
return { prompt, extraArgs: [], envOverrides: {} };
|
|
2274
|
+
return { promptPrefix: '', prompt, extraArgs: [], envOverrides: {} };
|
|
2051
2275
|
}
|
|
2052
2276
|
const isContinuingExistingSession = (resumesSessionViaCli === true || resumesSessionViaAcpLoad === true) &&
|
|
2053
2277
|
typeof resumeSessionId === 'string' &&
|
|
2054
2278
|
resumeSessionId.length > 0;
|
|
2055
2279
|
if (isContinuingExistingSession) {
|
|
2056
|
-
return { prompt, extraArgs: [], envOverrides: {} };
|
|
2280
|
+
return { promptPrefix: '', prompt, extraArgs: [], envOverrides: {} };
|
|
2057
2281
|
}
|
|
2058
2282
|
// KNOWN TRADE-OFF, deliberate: for a resume-capable def with no `'append-flag'`/`'env-var'`
|
|
2059
2283
|
// mechanism yet (`codex`, `codebuddy`, `opencode`, `amr` — all four presently on this fallback),
|
|
@@ -2070,22 +2294,22 @@ export function resolveSystemPromptOverlayDelivery(input) {
|
|
|
2070
2294
|
// is part of what a resumed session replays), which removes this limitation entirely for that
|
|
2071
2295
|
// def. See `reasonix.ts`'s and `opencode.ts`'s module docs for the two already-identified,
|
|
2072
2296
|
// not-yet-wired native mechanisms.
|
|
2073
|
-
|
|
2297
|
+
const promptPrefix = `${overlay}\n\n---\n\n`;
|
|
2298
|
+
return { promptPrefix, prompt: `${promptPrefix}${prompt}`, extraArgs: [], envOverrides: {} };
|
|
2074
2299
|
}
|
|
2075
|
-
/**
|
|
2300
|
+
/**
|
|
2301
|
+
* Phase 9b: calls the def's `buildArgs`, releasing staged resources and failing the run on a throw.
|
|
2302
|
+
*
|
|
2303
|
+
* @param input.overlayDelivery - This run's already-resolved overlay decision, computed once in
|
|
2304
|
+
* `run()` rather than here. It is resolved upstream because `buildArgs` is only ONE of the four
|
|
2305
|
+
* channels that can carry the prompt: the staged prompt file is written *before* this function
|
|
2306
|
+
* runs, and stdin/ACP/pi-rpc send theirs *after* spawn. A decision made inside this function could
|
|
2307
|
+
* therefore only ever reach the 7 defs whose `buildArgs` reads its first argument at all — the
|
|
2308
|
+
* other 17 declare it `_prompt` and discard it, which is exactly how the overlay used to go missing.
|
|
2309
|
+
*/
|
|
2076
2310
|
export async function buildRunArgs(input, deps) {
|
|
2077
2311
|
try {
|
|
2078
|
-
|
|
2079
|
-
// overlay already prefixed into `prompt` — see `resolveSystemPromptOverlayDelivery`'s own doc.
|
|
2080
|
-
const delivery = resolveSystemPromptOverlayDelivery({
|
|
2081
|
-
defId: input.def.id,
|
|
2082
|
-
systemPromptDelivery: input.def.systemPromptDelivery,
|
|
2083
|
-
resumesSessionViaCli: input.def.resumesSessionViaCli,
|
|
2084
|
-
resumesSessionViaAcpLoad: input.def.resumesSessionViaAcpLoad,
|
|
2085
|
-
overlay: input.systemPromptOverlay,
|
|
2086
|
-
prompt: input.imageDelivery.prompt,
|
|
2087
|
-
resumeSessionId: input.runtimeContext?.resumeSessionId,
|
|
2088
|
-
});
|
|
2312
|
+
const delivery = input.overlayDelivery;
|
|
2089
2313
|
const args = input.def.buildArgs(delivery.prompt, [...(input.imagePaths ?? [])], input.imageDelivery.extraAllowedDirs === undefined ? undefined : [...input.imageDelivery.extraAllowedDirs], buildAgentBuildArgsOptions(input.runInput, input.systemPromptOverlay), input.runtimeContext);
|
|
2090
2314
|
// `'append-flag'` delivery's extra argv (empty for every other def/strategy) is appended after
|
|
2091
2315
|
// whatever the def's own `buildArgs` returned — safe because it is only ever non-empty for a
|
|
@@ -2178,6 +2402,41 @@ export async function prepareCodexHomeIfNeeded(input, deps) {
|
|
|
2178
2402
|
return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a CODEX_HOME for agent "${input.def.id}": ${errorMessage(err)}`);
|
|
2179
2403
|
}
|
|
2180
2404
|
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Finding 1 of SEC-assistant-env-isolation-2026-09-07's one effect — stages this run's scratch
|
|
2407
|
+
* `CLAUDE_CONFIG_DIR` directory, returning the prepared handle `cleanupStagedFiles` should later
|
|
2408
|
+
* release (`null` for every def other than `claude` — see {@link prepareClaudeConfigDirForRun}'s
|
|
2409
|
+
* own doc for why this is unconditional for `claude` runs, unlike {@link prepareCodexHomeIfNeeded}'s
|
|
2410
|
+
* gate on a host-configured MCP bridge strategy).
|
|
2411
|
+
*
|
|
2412
|
+
* Gated on `def.id === 'claude'` directly rather than on `externalMcpInjection === 'claude-mcp-json'`
|
|
2413
|
+
* (which `codebuddy` also declares): isolating the operator's personal `~/.claude` is specific to
|
|
2414
|
+
* the real `claude` CLI's own config resolution, not to every def that happens to share its `.mcp.
|
|
2415
|
+
* json` delivery shape. Matches the existing `USER`-for-claude-login special case already singled
|
|
2416
|
+
* out by id in `BASELINE_AGENT_ENV_KEYS`'s own doc, a few hundred lines above.
|
|
2417
|
+
*
|
|
2418
|
+
* ALSO gated on `deps.enabled` (`CreateAgentExecutorOptions.claudeConfigDirIsolationEnabled`, default
|
|
2419
|
+
* `false`) since this task's own fix — see that field's doc for why it defaults off (Keychain login
|
|
2420
|
+
* is `CLAUDE_CONFIG_DIR`-keyed and no caller was supplying a credential when this was unconditional).
|
|
2421
|
+
* `def.id === 'claude'` is still checked first and independently: a host that flips this flag on
|
|
2422
|
+
* should not suddenly stage a directory for `codebuddy` or any other def.
|
|
2423
|
+
* @param input.def - Used for both the `id` gate and the failure message.
|
|
2424
|
+
* @param deps.enabled - The isolation on/off switch — see this function's own doc above.
|
|
2425
|
+
* @param deps.hostEnv - The daemon's own environment, threaded through to {@link resolveSourceClaudeConfigDir} rather than read from a module-level `process.env`, matching {@link prepareCodexHomeIfNeeded}'s identical testability reasoning.
|
|
2426
|
+
* @complexity O(1) plus {@link prepareClaudeConfigDirForRun}'s own cost.
|
|
2427
|
+
*/
|
|
2428
|
+
export async function prepareClaudeConfigDirIfNeeded(input, deps) {
|
|
2429
|
+
if (input.def.id !== 'claude' || !deps.enabled) {
|
|
2430
|
+
return null;
|
|
2431
|
+
}
|
|
2432
|
+
try {
|
|
2433
|
+
return await prepareClaudeConfigDirForRun(input.runId, resolveSourceClaudeConfigDir(deps.hostEnv), resolveClaudeConfigDirSeams(deps.claudeConfigDirIsolation));
|
|
2434
|
+
}
|
|
2435
|
+
catch (err) {
|
|
2436
|
+
await deps.releaseStagedResources();
|
|
2437
|
+
return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a CLAUDE_CONFIG_DIR for agent "${input.def.id}": ${errorMessage(err)}`);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2181
2440
|
/** Phase 11: post-`buildArgs` guard for argv-bound defs whose resolved binary is a Windows shim/.exe — a no-op off-Windows and for non-argv-bound defs. */
|
|
2182
2441
|
export async function guardWindowsCommandLineBudget(input, deps) {
|
|
2183
2442
|
const windowsBudgetError = checkWindowsCmdShimCommandLineBudget(input.def, input.launchPath, input.args) ??
|
|
@@ -2318,6 +2577,8 @@ export function createAgentExecutor(options) {
|
|
|
2318
2577
|
const continuation = options.continuation;
|
|
2319
2578
|
const classifyFailure = options.classifyFailure;
|
|
2320
2579
|
const mcpJsonInjection = options.mcpJsonInjection;
|
|
2580
|
+
const claudeConfigDirIsolation = options.claudeConfigDirIsolation;
|
|
2581
|
+
const claudeConfigDirIsolationEnabled = options.claudeConfigDirIsolationEnabled ?? false;
|
|
2321
2582
|
const promptAugmenter = options.promptAugmenter;
|
|
2322
2583
|
/**
|
|
2323
2584
|
* Transitions `runId` to `'failed'` (idempotent, never resumable — no
|
|
@@ -2359,13 +2620,46 @@ export function createAgentExecutor(options) {
|
|
|
2359
2620
|
// defs' own native protocol already delivers the image and must never also get this
|
|
2360
2621
|
// treatment (the double-delivery hazard this mechanism exists to avoid).
|
|
2361
2622
|
const imageDelivery = await resolveImageDeliveryAndArgvBudget({ runId: input.runId, def, prompt: input.prompt, imagePaths: input.imagePaths, extraAllowedDirs: input.extraAllowedDirs }, { failBeforeSpawn });
|
|
2623
|
+
// Phase 8, hoisted deliberately above EVERY step that writes or sends the prompt. Four
|
|
2624
|
+
// different channels carry a prompt in this driver, and they do not all run at the same point:
|
|
2625
|
+
// the `promptViaFile` staging below writes its file BEFORE `buildArgs`, `buildArgs` itself runs
|
|
2626
|
+
// mid-`run()`, and stdin/ACP/pi-rpc all send theirs AFTER spawn. Resolving the overlay once,
|
|
2627
|
+
// here, is what lets all four consume the same decision; resolving it later (as this used to,
|
|
2628
|
+
// inside `buildRunArgs`) could only ever reach `buildArgs`, so the 17 defs that discard that
|
|
2629
|
+
// argument, plus grok-build's staged file, silently received no overlay at all.
|
|
2630
|
+
//
|
|
2631
|
+
// `computeSystemPromptOverlay`'s third argument is the pre-staging `RuntimeContext`: it reads
|
|
2632
|
+
// only `hasPriorAssistantTurn`, which `computeRuntimeContext` never populates from any of the
|
|
2633
|
+
// staged-file/MCP inputs added to the fuller context built further down, so nothing between
|
|
2634
|
+
// here and there can change the overlay this returns. Passing the narrower context makes that
|
|
2635
|
+
// independence explicit rather than relying on the ordering staying lucky.
|
|
2636
|
+
//
|
|
2637
|
+
// `turnIndex` is a coarse 0/1 proxy (no exact turn counter exists on this driver) — sufficient
|
|
2638
|
+
// because every `PromptAugmenter.systemOverlay()` implementation this seam has today wants the
|
|
2639
|
+
// same overlay on every turn, not a first-turn-only one; a caller that needs finer-grained turn
|
|
2640
|
+
// numbering can track it itself and ignore this arg.
|
|
2641
|
+
const preStagingRuntimeContext = computeRuntimeContext(null, null, null, input.resumeSessionId, input.newSessionId);
|
|
2642
|
+
const systemPromptOverlay = computeSystemPromptOverlay(promptAugmenter, def.id, preStagingRuntimeContext);
|
|
2643
|
+
const overlayDelivery = resolveSystemPromptOverlayDelivery({
|
|
2644
|
+
defId: def.id,
|
|
2645
|
+
systemPromptDelivery: def.systemPromptDelivery,
|
|
2646
|
+
resumesSessionViaCli: def.resumesSessionViaCli,
|
|
2647
|
+
resumesSessionViaAcpLoad: def.resumesSessionViaAcpLoad,
|
|
2648
|
+
overlay: systemPromptOverlay,
|
|
2649
|
+
prompt: imageDelivery.prompt,
|
|
2650
|
+
resumeSessionId: preStagingRuntimeContext?.resumeSessionId,
|
|
2651
|
+
});
|
|
2362
2652
|
const resolvedEnv = resolveRunEnv(input, process.env);
|
|
2363
2653
|
const launch = await resolveLaunch({ runId: input.runId, def, resolvedEnv }, { resolveAgentLaunch: resolveAgentLaunchFn, failBeforeSpawn });
|
|
2364
2654
|
const spawnEnv = applyAgentLaunchEnvFn({ ...resolvedEnv }, launch);
|
|
2365
2655
|
// Stage a promptViaFile def's (grok-build) prompt to a temp file before buildArgs runs — its
|
|
2366
2656
|
// buildArgs throws without runtimeContext.promptFilePath. A no-op (returns null) for every
|
|
2367
2657
|
// def without promptViaFile: true (preparePromptFileForAgent's own guard).
|
|
2368
|
-
|
|
2658
|
+
//
|
|
2659
|
+
// `overlayDelivery.prompt`, not `imageDelivery.prompt`: for a `promptViaFile` def this file IS
|
|
2660
|
+
// the prompt transport — its `buildArgs` declares `_prompt` and passes only the path — so the
|
|
2661
|
+
// overlay has to be in the bytes written here or the CLI never sees it at all.
|
|
2662
|
+
const preparedPromptFile = await stagePromptFile({ runId: input.runId, def, prompt: overlayDelivery.prompt }, { preparePromptFileForAgent: preparePromptFileForAgentFn, failBeforeSpawn });
|
|
2369
2663
|
// Stage a needsAgentLogFile def's (antigravity) diagnostic-log path, on the same terms and at
|
|
2370
2664
|
// the same point as the prompt file above: before buildArgs, since buildArgs is what turns the
|
|
2371
2665
|
// path into a `--log-file <path>` argument. A no-op (returns null) for every def without
|
|
@@ -2393,6 +2687,13 @@ export function createAgentExecutor(options) {
|
|
|
2393
2687
|
* Only the `'codex-toml'` mechanism stages a directory at all.
|
|
2394
2688
|
*/
|
|
2395
2689
|
let preparedCodexHome = null;
|
|
2690
|
+
/**
|
|
2691
|
+
* Set once `prepareClaudeConfigDirIfNeeded` has actually staged this run's scratch
|
|
2692
|
+
* `CLAUDE_CONFIG_DIR`, so `cleanupStagedFiles` knows there is a directory (possibly holding a
|
|
2693
|
+
* copied login credential) to remove. Cleared as it is consumed, matching `preparedCodexHome`'s
|
|
2694
|
+
* identical single-removal discipline. Only a `claude`-id run stages a directory this way.
|
|
2695
|
+
*/
|
|
2696
|
+
let preparedClaudeConfigDir = null;
|
|
2396
2697
|
/**
|
|
2397
2698
|
* Set once `prepareSystemPromptOverlayFileIfNeeded` has actually staged this run's overlay file
|
|
2398
2699
|
* for a `'config-instructions-file'` def, so `cleanupStagedFiles` knows there is a temp
|
|
@@ -2416,6 +2717,11 @@ export function createAgentExecutor(options) {
|
|
|
2416
2717
|
preparedCodexHome = null;
|
|
2417
2718
|
await codexHomeToRemove.cleanup();
|
|
2418
2719
|
}
|
|
2720
|
+
if (preparedClaudeConfigDir) {
|
|
2721
|
+
const claudeConfigDirToRemove = preparedClaudeConfigDir;
|
|
2722
|
+
preparedClaudeConfigDir = null;
|
|
2723
|
+
await claudeConfigDirToRemove.cleanup();
|
|
2724
|
+
}
|
|
2419
2725
|
if (preparedSystemPromptOverlayFile) {
|
|
2420
2726
|
const overlayFileToRemove = preparedSystemPromptOverlayFile;
|
|
2421
2727
|
preparedSystemPromptOverlayFile = null;
|
|
@@ -2452,12 +2758,6 @@ export function createAgentExecutor(options) {
|
|
|
2452
2758
|
releaseRuntimeLock();
|
|
2453
2759
|
await cleanupStagedFiles();
|
|
2454
2760
|
};
|
|
2455
|
-
// Computed once per `run()`, not per-token/per-event: a system-prompt overlay is a spawn-time
|
|
2456
|
-
// CLI arg, not something that varies mid-run. `turnIndex` is a coarse 0/1 proxy (no exact turn
|
|
2457
|
-
// counter exists on this driver) — sufficient because every `PromptAugmenter.systemOverlay()`
|
|
2458
|
-
// implementation this seam has today wants the same overlay on every turn, not a first-turn-only
|
|
2459
|
-
// one; a caller that needs finer-grained turn numbering can track it itself and ignore this arg.
|
|
2460
|
-
const systemPromptOverlay = computeSystemPromptOverlay(promptAugmenter, def.id, runtimeContext);
|
|
2461
2761
|
// Guarded, like every other step between staging and spawn: a `runtimeLock` def's `buildArgs` is
|
|
2462
2762
|
// guarded precisely *because* it performs real filesystem writes (antigravity writes its model
|
|
2463
2763
|
// choice into a shared settings file), so EACCES on a read-only home, ENOSPC, or a malformed
|
|
@@ -2465,7 +2765,7 @@ export function createAgentExecutor(options) {
|
|
|
2465
2765
|
// `Error` — breaking this driver's "never a bare throw, always an `AgentExecutorError`" contract
|
|
2466
2766
|
// — and left the run `'running'` forever while still holding the process-global mutex and both
|
|
2467
2767
|
// staged files, so no later run of that def could ever acquire the lock either.
|
|
2468
|
-
const { args, envOverrides: systemPromptEnvOverrides } = await buildRunArgs({ runId: input.runId, def, imageDelivery, imagePaths: input.imagePaths, runInput: input, systemPromptOverlay, runtimeContext }, { releaseStagedResources, failBeforeSpawn });
|
|
2768
|
+
const { args, envOverrides: systemPromptEnvOverrides } = await buildRunArgs({ runId: input.runId, def, imageDelivery, imagePaths: input.imagePaths, runInput: input, systemPromptOverlay, overlayDelivery, runtimeContext }, { releaseStagedResources, failBeforeSpawn });
|
|
2469
2769
|
// Mechanism 1 of 5's one effect — stage this run's own MCP config file (run-scoped, see
|
|
2470
2770
|
// `mcpJsonPathForRun`) before spawn so the `--mcp-config <path>` argv buildArgs just produced
|
|
2471
2771
|
// points at a real file. Skipped entirely for the other four mechanisms and whenever no bridge
|
|
@@ -2478,6 +2778,19 @@ export function createAgentExecutor(options) {
|
|
|
2478
2778
|
// so — unlike the `.mcp.json` staging above — this can run after `buildArgs` with no ordering
|
|
2479
2779
|
// constraint of its own; it is placed here only to keep the two staging steps adjacent.
|
|
2480
2780
|
preparedCodexHome = await prepareCodexHomeIfNeeded({ runId: input.runId, def, mcpBridge }, { mcpJsonInjection, hostEnv: process.env, releaseStagedResources, failBeforeSpawn });
|
|
2781
|
+
// Finding 1 of SEC-assistant-env-isolation-2026-09-07's one effect — stage this run's scratch
|
|
2782
|
+
// `CLAUDE_CONFIG_DIR` directory. Unconditional for a `claude`-id run (unlike CODEX_HOME above,
|
|
2783
|
+
// this does not depend on `mcpJsonInjection` being configured at all — see
|
|
2784
|
+
// `prepareClaudeConfigDirIfNeeded`'s own doc for why). Placed here only to stay adjacent to the
|
|
2785
|
+
// other pre-`computeChildEnv` staging steps; `claude.ts`'s `buildArgs` needs no argv change for
|
|
2786
|
+
// this (CLAUDE_CONFIG_DIR is an env var, not a flag), same as CODEX_HOME.
|
|
2787
|
+
preparedClaudeConfigDir = await prepareClaudeConfigDirIfNeeded({ runId: input.runId, def }, {
|
|
2788
|
+
enabled: claudeConfigDirIsolationEnabled,
|
|
2789
|
+
claudeConfigDirIsolation,
|
|
2790
|
+
hostEnv: process.env,
|
|
2791
|
+
releaseStagedResources,
|
|
2792
|
+
failBeforeSpawn,
|
|
2793
|
+
});
|
|
2481
2794
|
// `'config-instructions-file'`'s one effect — stage the overlay to a temp file so
|
|
2482
2795
|
// `computeChildEnv` below has a real path to merge into that def's `instructions` array. A
|
|
2483
2796
|
// no-op (`null`) for every other def/strategy or a run with no overlay at all. Independent of
|
|
@@ -2492,7 +2805,7 @@ export function createAgentExecutor(options) {
|
|
|
2492
2805
|
// here ever read `childEnv`, so moving the call cost nothing.
|
|
2493
2806
|
const childEnv = computeChildEnv(spawnEnv, mcpBridge, preparedCodexHome?.path, systemPromptEnvOverrides, preparedSystemPromptOverlayFile && def.systemPromptDelivery?.strategy === 'config-instructions-file'
|
|
2494
2807
|
? { varName: def.systemPromptDelivery.varName, path: preparedSystemPromptOverlayFile.path }
|
|
2495
|
-
: undefined);
|
|
2808
|
+
: undefined, preparedClaudeConfigDir?.path);
|
|
2496
2809
|
// Post-buildArgs guard for argv-bound defs whose resolved binary is a
|
|
2497
2810
|
// Windows .cmd/.bat shim or a direct .exe: a prompt under the raw byte
|
|
2498
2811
|
// budget can still expand past CreateProcess's command-line cap once
|
|
@@ -2545,7 +2858,14 @@ export function createAgentExecutor(options) {
|
|
|
2545
2858
|
runId: input.runId,
|
|
2546
2859
|
agentId: def.id,
|
|
2547
2860
|
child,
|
|
2548
|
-
|
|
2861
|
+
// `overlayDelivery.promptPrefix` applied to `input.prompt`, NOT `overlayDelivery.prompt`:
|
|
2862
|
+
// this call site deliberately sends the raw input prompt rather than the image-delivery
|
|
2863
|
+
// rewrite (see `resolveImageDeliveryAndArgvBudget`'s call site above — an ACP def delivers
|
|
2864
|
+
// images natively and must never also get `'prompt-path'`'s appended paths), and the prefix
|
|
2865
|
+
// is the part of the overlay decision that applies to whatever prompt text a transport was
|
|
2866
|
+
// already sending. `''` for `reasonix` (env-var) and any resumed ACP session, so those keep
|
|
2867
|
+
// sending byte-identical text and never receive the overlay twice.
|
|
2868
|
+
prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
|
|
2549
2869
|
cwd: input.cwd,
|
|
2550
2870
|
model: input.model,
|
|
2551
2871
|
imagePaths: input.imagePaths ?? [],
|
|
@@ -2572,7 +2892,10 @@ export function createAgentExecutor(options) {
|
|
|
2572
2892
|
runId: input.runId,
|
|
2573
2893
|
agentId: def.id,
|
|
2574
2894
|
child,
|
|
2575
|
-
|
|
2895
|
+
// Same reasoning as the ACP call site above. `pi` declares an `'append-flag'` strategy,
|
|
2896
|
+
// so its prefix is `''` and this stays byte-identical to the raw prompt — the overlay
|
|
2897
|
+
// rides `--append-system-prompt` instead, exactly once.
|
|
2898
|
+
prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
|
|
2576
2899
|
cwd: input.cwd,
|
|
2577
2900
|
model: input.model,
|
|
2578
2901
|
imagePaths: input.imagePaths ?? [],
|
|
@@ -2592,7 +2915,17 @@ export function createAgentExecutor(options) {
|
|
|
2592
2915
|
});
|
|
2593
2916
|
return;
|
|
2594
2917
|
}
|
|
2595
|
-
|
|
2918
|
+
// The overlay reaches stdin only for a def that declares stdin as its prompt transport. The
|
|
2919
|
+
// four `plain`-format defs that do not (`aider`/`antigravity`/`deepseek` put the prompt in
|
|
2920
|
+
// argv, `grok-build` in a staged file) are still written to and closed here exactly as before,
|
|
2921
|
+
// because this driver always spawns with `stdio: ['pipe','pipe','pipe']` and their CLIs need
|
|
2922
|
+
// the EOF — but their prompt already carried the overlay through argv or the staged file, so
|
|
2923
|
+
// sending the overlaid text here too would deliver it twice. This is the one place the
|
|
2924
|
+
// resolver's own `''`-prefix rule is not sufficient on its own: those four defs are on the
|
|
2925
|
+
// fallback strategy, so their prefix is genuinely non-empty; what makes stdin the wrong
|
|
2926
|
+
// channel for them is the def's declared transport, not the strategy.
|
|
2927
|
+
const stdinPrompt = def.promptViaStdin === true ? overlayDelivery.prompt : imageDelivery.prompt;
|
|
2928
|
+
writePromptToStdin(def, child, stdinPrompt, stdinHandle);
|
|
2596
2929
|
}
|
|
2597
2930
|
return { run };
|
|
2598
2931
|
}
|