@jini-ai/daemon 0.3.2 → 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 +133 -3
- package/dist/agent-executor.d.ts.map +1 -1
- package/dist/agent-executor.js +265 -11
- package/dist/agent-executor.js.map +1 -1
- package/dist/artifacts/index.d.ts +14 -0
- package/dist/artifacts/index.d.ts.map +1 -0
- package/dist/artifacts/index.js +14 -0
- package/dist/artifacts/index.js.map +1 -0
- package/dist/artifacts/manifest.d.ts +90 -0
- package/dist/artifacts/manifest.d.ts.map +1 -0
- package/dist/artifacts/manifest.js +229 -0
- package/dist/artifacts/manifest.js.map +1 -0
- package/dist/artifacts/publication-guard.d.ts +28 -0
- package/dist/artifacts/publication-guard.d.ts.map +1 -0
- package/dist/artifacts/publication-guard.js +77 -0
- package/dist/artifacts/publication-guard.js.map +1 -0
- package/dist/artifacts/runtime-compat.d.ts +37 -0
- package/dist/artifacts/runtime-compat.d.ts.map +1 -0
- package/dist/artifacts/runtime-compat.js +33 -0
- package/dist/artifacts/runtime-compat.js.map +1 -0
- package/dist/artifacts/store.d.ts +85 -0
- package/dist/artifacts/store.d.ts.map +1 -0
- package/dist/artifacts/store.js +92 -0
- package/dist/artifacts/store.js.map +1 -0
- package/dist/artifacts/stub-guard.d.ts +73 -0
- package/dist/artifacts/stub-guard.d.ts.map +1 -0
- package/dist/artifacts/stub-guard.js +198 -0
- package/dist/artifacts/stub-guard.js.map +1 -0
- package/dist/artifacts/text-suppression.d.ts +68 -0
- package/dist/artifacts/text-suppression.d.ts.map +1 -0
- package/dist/artifacts/text-suppression.js +184 -0
- package/dist/artifacts/text-suppression.js.map +1 -0
- package/dist/run-lifecycle.d.ts +20 -0
- package/dist/run-lifecycle.d.ts.map +1 -1
- package/dist/run-lifecycle.js +20 -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 +5 -5
package/dist/agent-executor.js
CHANGED
|
@@ -849,6 +849,44 @@ 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
|
+
}
|
|
852
890
|
/**
|
|
853
891
|
* Removes any pre-existing `[mcp_servers.{@link JINI_MCP_SERVER_KEY}]` table — and its
|
|
854
892
|
* `[mcp_servers.{@link JINI_MCP_SERVER_KEY}.*]` subtables (e.g. `.env`) — from a real Codex
|
|
@@ -856,10 +894,34 @@ export function buildCodexMcpServerToml(entry) {
|
|
|
856
894
|
* without producing the duplicate TOML key Codex's parser rejects at startup.
|
|
857
895
|
*
|
|
858
896
|
* Line-oriented, not a real TOML parser (matching {@link buildCodexMcpServerToml}'s own
|
|
859
|
-
* no-TOML-dependency constraint)
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
*
|
|
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.
|
|
863
925
|
* @param existingRaw - The real Codex home's `config.toml` content, already known to be defined
|
|
864
926
|
* (callers pass `''` for a missing file).
|
|
865
927
|
* @returns `existingRaw` with any jini table/subtable removed.
|
|
@@ -870,9 +932,9 @@ function stripExistingJiniMcpServerTable(existingRaw) {
|
|
|
870
932
|
const kept = [];
|
|
871
933
|
let skipping = false;
|
|
872
934
|
for (const line of existingRaw.split('\n')) {
|
|
873
|
-
const header = /^\s*\[([^[\]]+)\]\s
|
|
935
|
+
const header = /^\s*\[([^[\]]+)\]\s*(#.*)?$/.exec(line);
|
|
874
936
|
if (header) {
|
|
875
|
-
const key = (header[1] ?? '').
|
|
937
|
+
const key = splitTomlDottedKey(header[1] ?? '').join('.');
|
|
876
938
|
skipping = key === jiniTableKey || key.startsWith(`${jiniTableKey}.`);
|
|
877
939
|
if (skipping)
|
|
878
940
|
continue;
|
|
@@ -1122,6 +1184,114 @@ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
|
|
|
1122
1184
|
},
|
|
1123
1185
|
};
|
|
1124
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
|
+
}
|
|
1125
1295
|
/**
|
|
1126
1296
|
* Default ceiling on the `'until-close'` stdout accumulator (see `RuntimeStdoutPolicy` in
|
|
1127
1297
|
* `@jini-ai/agent-runtime`), in bytes of received UTF-8.
|
|
@@ -1303,6 +1473,13 @@ function wireChildLifecycle(ctx) {
|
|
|
1303
1473
|
// `content` below already carries the whole raw output, a pre-existing, unrelated gap), so
|
|
1304
1474
|
// there is no `remainder` to thread back in — only the extracted blocks are used here.
|
|
1305
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);
|
|
1306
1483
|
try {
|
|
1307
1484
|
const result = await continuation.toolExecutor.execute(continuation.principal, run, toolUse.name, toolUse.input);
|
|
1308
1485
|
content = resultContent(result);
|
|
@@ -1313,6 +1490,9 @@ function wireChildLifecycle(ctx) {
|
|
|
1313
1490
|
content = errorMessage(error);
|
|
1314
1491
|
isError = true;
|
|
1315
1492
|
}
|
|
1493
|
+
finally {
|
|
1494
|
+
lifecycle.resumeSlowRunNotice(runId);
|
|
1495
|
+
}
|
|
1316
1496
|
await lifecycle.emit(runId, {
|
|
1317
1497
|
event: 'agent',
|
|
1318
1498
|
data: {
|
|
@@ -1916,7 +2096,7 @@ export async function resolveMcpBridgeForRun(input, deps) {
|
|
|
1916
2096
|
* @complexity O(1) plus `mergeEnvContentMcpConfig`'s and `mergeEnvContentInstructions`'s own `JSON.parse`/`JSON.stringify` cost.
|
|
1917
2097
|
* @overallScore 100/100
|
|
1918
2098
|
*/
|
|
1919
|
-
export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptEnvOverrides, stagedInstructionsFile) {
|
|
2099
|
+
export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptEnvOverrides, stagedInstructionsFile, claudeConfigDir) {
|
|
1920
2100
|
const envContentApplied = mcpBridge?.kind === 'env-content'
|
|
1921
2101
|
? {
|
|
1922
2102
|
...spawnEnv,
|
|
@@ -1936,7 +2116,12 @@ export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptE
|
|
|
1936
2116
|
[stagedInstructionsFile.varName]: mergeEnvContentInstructions(envPassthroughApplied[stagedInstructionsFile.varName], stagedInstructionsFile.path),
|
|
1937
2117
|
};
|
|
1938
2118
|
const codexHomeApplied = codexHomeDir === undefined ? instructionsApplied : { ...instructionsApplied, CODEX_HOME: codexHomeDir };
|
|
1939
|
-
|
|
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 };
|
|
1940
2125
|
}
|
|
1941
2126
|
/**
|
|
1942
2127
|
* Phase 6b: the `RuntimeContext` `buildArgs` receives — `undefined` unless a file, bridge path, or
|
|
@@ -1990,16 +2175,23 @@ function computeSystemPromptOverlay(promptAugmenter, agentId, runtimeContext) {
|
|
|
1990
2175
|
turnIndex: runtimeContext?.hasPriorAssistantTurn ? 1 : 0,
|
|
1991
2176
|
});
|
|
1992
2177
|
}
|
|
1993
|
-
/** 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. */
|
|
1994
2179
|
export function buildAgentBuildArgsOptions(input, systemPromptOverlay) {
|
|
1995
2180
|
const hasOverlay = systemPromptOverlay !== undefined && systemPromptOverlay !== null;
|
|
1996
|
-
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) {
|
|
1997
2187
|
return undefined;
|
|
1998
2188
|
}
|
|
1999
2189
|
return {
|
|
2000
2190
|
...(input.model !== undefined ? { model: input.model } : {}),
|
|
2001
2191
|
...(input.reasoning !== undefined ? { reasoning: input.reasoning } : {}),
|
|
2002
2192
|
...(input.permissionMode !== undefined ? { permissionMode: input.permissionMode } : {}),
|
|
2193
|
+
...(input.disallowedTools !== undefined ? { disallowedTools: input.disallowedTools } : {}),
|
|
2194
|
+
...(input.allowedTools !== undefined ? { allowedTools: input.allowedTools } : {}),
|
|
2003
2195
|
...(hasOverlay ? { systemPromptOverlay } : {}),
|
|
2004
2196
|
};
|
|
2005
2197
|
}
|
|
@@ -2210,6 +2402,41 @@ export async function prepareCodexHomeIfNeeded(input, deps) {
|
|
|
2210
2402
|
return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a CODEX_HOME for agent "${input.def.id}": ${errorMessage(err)}`);
|
|
2211
2403
|
}
|
|
2212
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
|
+
}
|
|
2213
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. */
|
|
2214
2441
|
export async function guardWindowsCommandLineBudget(input, deps) {
|
|
2215
2442
|
const windowsBudgetError = checkWindowsCmdShimCommandLineBudget(input.def, input.launchPath, input.args) ??
|
|
@@ -2350,6 +2577,8 @@ export function createAgentExecutor(options) {
|
|
|
2350
2577
|
const continuation = options.continuation;
|
|
2351
2578
|
const classifyFailure = options.classifyFailure;
|
|
2352
2579
|
const mcpJsonInjection = options.mcpJsonInjection;
|
|
2580
|
+
const claudeConfigDirIsolation = options.claudeConfigDirIsolation;
|
|
2581
|
+
const claudeConfigDirIsolationEnabled = options.claudeConfigDirIsolationEnabled ?? false;
|
|
2353
2582
|
const promptAugmenter = options.promptAugmenter;
|
|
2354
2583
|
/**
|
|
2355
2584
|
* Transitions `runId` to `'failed'` (idempotent, never resumable — no
|
|
@@ -2458,6 +2687,13 @@ export function createAgentExecutor(options) {
|
|
|
2458
2687
|
* Only the `'codex-toml'` mechanism stages a directory at all.
|
|
2459
2688
|
*/
|
|
2460
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;
|
|
2461
2697
|
/**
|
|
2462
2698
|
* Set once `prepareSystemPromptOverlayFileIfNeeded` has actually staged this run's overlay file
|
|
2463
2699
|
* for a `'config-instructions-file'` def, so `cleanupStagedFiles` knows there is a temp
|
|
@@ -2481,6 +2717,11 @@ export function createAgentExecutor(options) {
|
|
|
2481
2717
|
preparedCodexHome = null;
|
|
2482
2718
|
await codexHomeToRemove.cleanup();
|
|
2483
2719
|
}
|
|
2720
|
+
if (preparedClaudeConfigDir) {
|
|
2721
|
+
const claudeConfigDirToRemove = preparedClaudeConfigDir;
|
|
2722
|
+
preparedClaudeConfigDir = null;
|
|
2723
|
+
await claudeConfigDirToRemove.cleanup();
|
|
2724
|
+
}
|
|
2484
2725
|
if (preparedSystemPromptOverlayFile) {
|
|
2485
2726
|
const overlayFileToRemove = preparedSystemPromptOverlayFile;
|
|
2486
2727
|
preparedSystemPromptOverlayFile = null;
|
|
@@ -2537,6 +2778,19 @@ export function createAgentExecutor(options) {
|
|
|
2537
2778
|
// so — unlike the `.mcp.json` staging above — this can run after `buildArgs` with no ordering
|
|
2538
2779
|
// constraint of its own; it is placed here only to keep the two staging steps adjacent.
|
|
2539
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
|
+
});
|
|
2540
2794
|
// `'config-instructions-file'`'s one effect — stage the overlay to a temp file so
|
|
2541
2795
|
// `computeChildEnv` below has a real path to merge into that def's `instructions` array. A
|
|
2542
2796
|
// no-op (`null`) for every other def/strategy or a run with no overlay at all. Independent of
|
|
@@ -2551,7 +2805,7 @@ export function createAgentExecutor(options) {
|
|
|
2551
2805
|
// here ever read `childEnv`, so moving the call cost nothing.
|
|
2552
2806
|
const childEnv = computeChildEnv(spawnEnv, mcpBridge, preparedCodexHome?.path, systemPromptEnvOverrides, preparedSystemPromptOverlayFile && def.systemPromptDelivery?.strategy === 'config-instructions-file'
|
|
2553
2807
|
? { varName: def.systemPromptDelivery.varName, path: preparedSystemPromptOverlayFile.path }
|
|
2554
|
-
: undefined);
|
|
2808
|
+
: undefined, preparedClaudeConfigDir?.path);
|
|
2555
2809
|
// Post-buildArgs guard for argv-bound defs whose resolved binary is a
|
|
2556
2810
|
// Windows .cmd/.bat shim or a direct .exe: a prompt under the raw byte
|
|
2557
2811
|
// budget can still expand past CreateProcess's command-line cap once
|