@jini-ai/daemon 0.6.8 → 0.6.9
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/CHANGELOG.md +4 -0
- package/dist/agent-executor/antigravity-permissions.d.ts +11 -0
- package/dist/agent-executor/antigravity-permissions.js +64 -0
- package/dist/agent-executor/contracts.d.ts +31 -6
- package/dist/agent-executor/event-translation.d.ts +6 -1
- package/dist/agent-executor/event-translation.js +1 -1
- package/dist/agent-executor/events.d.ts +1 -0
- package/dist/agent-executor/events.js +85 -74
- package/dist/agent-executor/launch.d.ts +4 -2
- package/dist/agent-executor/launch.js +306 -217
- package/dist/agent-executor/mcp-bridge-probe.js +51 -35
- package/dist/agent-executor/mcp-bridge.d.ts +18 -8
- package/dist/agent-executor/mcp-bridge.js +105 -65
- package/dist/agent-executor/run-output.d.ts +20 -0
- package/dist/agent-executor/run-output.js +80 -0
- package/dist/attachment-content.js +18 -15
- package/dist/core/read-only-tools.d.ts +15 -0
- package/dist/core/read-only-tools.js +29 -1
- package/dist/core/tool-audit.d.ts +3 -1
- package/dist/core/tool-audit.js +35 -22
- package/dist/core/tool-gaps.d.ts +60 -0
- package/dist/core/tool-gaps.js +199 -0
- package/dist/delegated-tool-bridge.d.ts +1 -1
- package/dist/delegated-tool-bridge.js +161 -122
- package/dist/event-log.d.ts +45 -2
- package/dist/event-log.js +83 -4
- package/dist/http/attachments.d.ts +26 -1
- package/dist/http/attachments.js +129 -65
- package/dist/http/connectors.d.ts +1 -32
- package/dist/http/connectors.js +13 -68
- package/dist/http/delegated-tools.d.ts +5 -1
- package/dist/http/delegated-tools.js +29 -29
- package/dist/http/host-tools.d.ts +1 -0
- package/dist/http/host-tools.js +64 -59
- package/dist/http/index.d.ts +2 -2
- package/dist/http/index.js +1 -1
- package/dist/http/media.js +48 -69
- package/dist/http/memory.js +45 -36
- package/dist/http/model-proxy-fields.d.ts +23 -0
- package/dist/http/model-proxy-fields.js +29 -0
- package/dist/http/model-proxy.js +193 -395
- package/dist/http/research-tavily.adapter.js +72 -49
- package/dist/http/research.js +33 -23
- package/dist/http/resource-request.d.ts +37 -0
- package/dist/http/resource-request.js +59 -0
- package/dist/http/routines.js +49 -90
- package/dist/http/runs.d.ts +7 -5
- package/dist/http/runs.js +21 -7
- package/dist/http/terminals.js +23 -48
- package/dist/http/tool-catalog.d.ts +5 -0
- package/dist/http/xai.d.ts +11 -1
- package/dist/http/xai.js +125 -124
- package/dist/legacy-data-migration.js +41 -24
- package/dist/run/core/retry.js +51 -35
- package/dist/run/diagnostics/diagnostics.js +5 -17
- package/dist/run-credentials.js +49 -27
- package/dist/run-lifecycle.d.ts +12 -2
- package/dist/run-lifecycle.js +47 -26
- package/dist/store/agent-sessions/sqlite.d.ts +6 -0
- package/dist/store/agent-sessions/sqlite.js +6 -4
- package/dist/surface-exchanges.js +2 -1
- package/dist/terminal-session.d.ts +1 -1
- package/dist/terminal-session.js +26 -33
- package/dist/tool-audit.d.ts +1 -0
- package/dist/tool-audit.js +1 -0
- package/dist/tool-executor.d.ts +24 -3
- package/dist/tool-executor.js +276 -139
- package/dist/tool-recovery/recovery.js +64 -35
- package/dist/tool-recovery/stack.d.ts +2 -1
- package/dist/tool-recovery/stack.js +1 -1
- package/package.json +10 -10
|
@@ -3,6 +3,43 @@ import { createInterface } from 'node:readline';
|
|
|
3
3
|
import { redactSecrets } from '@jini-ai/core';
|
|
4
4
|
import { buildAgentEnv } from './agent-env.js';
|
|
5
5
|
import { errorMessage, isRecord } from './values.js';
|
|
6
|
+
const STDERR_TAIL_BYTES = 2000;
|
|
7
|
+
/** Bounded stderr tail: keeps only the last STDERR_TAIL_BYTES bytes, redacted when read. */
|
|
8
|
+
function createStderrTail() {
|
|
9
|
+
let stderr = Buffer.alloc(0);
|
|
10
|
+
return {
|
|
11
|
+
append: (chunk) => {
|
|
12
|
+
stderr = Buffer.concat([stderr, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]).subarray(-STDERR_TAIL_BYTES);
|
|
13
|
+
},
|
|
14
|
+
text: () => redactSecrets({ input: stderr.toString('utf8') }, {
|
|
15
|
+
extraPatterns: [{ name: 'daemon_token', regex: /JINI_DAEMON_TOKEN\s*=\s*\S+/gi }],
|
|
16
|
+
}).trim(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Resolve on the first stdout line that is the initialize (id 1) result. */
|
|
20
|
+
function watchInitializeReply(lines, onReply) {
|
|
21
|
+
lines.on('line', (line) => {
|
|
22
|
+
try {
|
|
23
|
+
const reply = JSON.parse(line);
|
|
24
|
+
if (isRecord(reply) && reply.id === 1 && Object.hasOwn(reply, 'result'))
|
|
25
|
+
onReply();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Startup chatter is not an initialize response; keep waiting for the matching reply.
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Always reap the probe child; cleanup failures never replace the probe's own diagnostic. */
|
|
33
|
+
function reapProbe({ timer, lines, child }) {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
lines?.close();
|
|
36
|
+
try {
|
|
37
|
+
child?.kill('SIGKILL');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// A process already gone or refusing cleanup must not discard the original probe diagnostic.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
6
43
|
/** Initialize a disposable bridge without a run credential; always reap it after the bounded probe.
|
|
7
44
|
* Spawn/errors/exits/timeouts return diagnostics rather than rejecting. Stderr retains 2000 bytes.
|
|
8
45
|
* @complexity O(n) in received bytes; stderr storage is bounded independently of child output.
|
|
@@ -15,63 +52,42 @@ export async function probeMcpBridge({ entry }, { spawn = nodeSpawn, baseEnv = b
|
|
|
15
52
|
let child;
|
|
16
53
|
let lines;
|
|
17
54
|
let timer;
|
|
18
|
-
|
|
19
|
-
const appendStderr = (chunk) => {
|
|
20
|
-
stderr = Buffer.concat([stderr, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]).subarray(-2000);
|
|
21
|
-
};
|
|
55
|
+
const stderr = createStderrTail();
|
|
22
56
|
const result = (ok, exitCode = null, signal = null) => ({
|
|
23
|
-
ok, exitCode, signal,
|
|
24
|
-
stderrTail: redactSecrets({ input: stderr.toString('utf8') }, {
|
|
25
|
-
extraPatterns: [{ name: 'daemon_token', regex: /JINI_DAEMON_TOKEN\s*=\s*\S+/gi }],
|
|
26
|
-
}).trim(),
|
|
27
|
-
elapsedMs: now() - startedAt,
|
|
57
|
+
ok, exitCode, signal, stderrTail: stderr.text(), elapsedMs: now() - startedAt,
|
|
28
58
|
});
|
|
59
|
+
let outcome;
|
|
29
60
|
try {
|
|
30
61
|
child = spawn(entry.command, [...entry.args], { env, stdio: 'pipe' });
|
|
31
62
|
const bridge = child;
|
|
32
63
|
lines = bridge.stdout ? createInterface({ input: bridge.stdout, crlfDelay: Infinity }) : undefined;
|
|
33
|
-
|
|
64
|
+
const reader = lines;
|
|
65
|
+
outcome = await new Promise((resolve) => {
|
|
34
66
|
const fail = (error) => {
|
|
35
|
-
|
|
67
|
+
stderr.append(`\n${errorMessage(error)}`);
|
|
36
68
|
resolve(result(false));
|
|
37
69
|
};
|
|
38
70
|
timer = setTimeout(() => resolve(result(false)), timeoutMs);
|
|
39
71
|
bridge.once('error', fail);
|
|
40
72
|
bridge.once('exit', (code, signal) => resolve(result(false, code, signal)));
|
|
41
73
|
bridge.once('close', (code, signal) => resolve(result(false, code, signal)));
|
|
42
|
-
bridge.stderr?.on('data',
|
|
74
|
+
bridge.stderr?.on('data', stderr.append);
|
|
43
75
|
bridge.stdin?.once('error', fail);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const reply = JSON.parse(line);
|
|
49
|
-
if (isRecord(reply) && reply.id === 1 && Object.hasOwn(reply, 'result'))
|
|
50
|
-
resolve(result(true));
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
// Startup chatter is not an initialize response; keep waiting for the matching reply.
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
76
|
+
reader?.once('error', fail);
|
|
77
|
+
if (reader)
|
|
78
|
+
watchInitializeReply(reader, () => resolve(result(true)));
|
|
57
79
|
bridge.stdin?.end(`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {
|
|
58
80
|
protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'jini-bridge-probe', version: '0' },
|
|
59
81
|
} })}\n`);
|
|
60
82
|
});
|
|
61
83
|
}
|
|
62
84
|
catch (error) {
|
|
63
|
-
|
|
64
|
-
|
|
85
|
+
stderr.append(`\n${errorMessage(error)}`);
|
|
86
|
+
outcome = result(false);
|
|
65
87
|
}
|
|
66
88
|
finally {
|
|
67
|
-
|
|
68
|
-
lines?.close();
|
|
69
|
-
try {
|
|
70
|
-
child?.kill('SIGKILL');
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
// A process already gone or refusing cleanup must not discard the original probe diagnostic.
|
|
74
|
-
}
|
|
89
|
+
reapProbe({ timer, lines, child });
|
|
75
90
|
}
|
|
91
|
+
return outcome;
|
|
76
92
|
}
|
|
77
93
|
//# sourceMappingURL=mcp-bridge-probe.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpBridgeProbeResult } from './mcp-bridge-probe.js';
|
|
2
|
+
import { ensureAntigravityMcpPermission } from './antigravity-permissions.js';
|
|
2
3
|
import type { AcpMcpServerInput, RuntimeAgentDef } from '@jini-ai/agent-runtime';
|
|
3
4
|
import type { McpJsonInjectionOptions, McpJsonServerEntry, McpBridgeDelivery, PreparedCodexHome, PreparedClaudeConfigDir, ClaudeConfigDirIsolationOptions, FailBeforeSpawn } from './contracts.js';
|
|
4
5
|
/** The run-error code a run gets when its CLI reports the injected `jini` bridge as anything but connected. */
|
|
@@ -25,15 +26,14 @@ export declare function bridgeUnavailableMessage(status: string, probe?: McpBrid
|
|
|
25
26
|
* `writeMcpJsonForRun`'s job, which keeps the effect out of this function.
|
|
26
27
|
*
|
|
27
28
|
* @param runId - The run this entry scopes its child to.
|
|
28
|
-
* @param options -
|
|
29
|
+
* @param options - Launch settings and independent readers of diagnostic and visible startup IDs.
|
|
29
30
|
* @param credential - The already-resolved bearer token, or `undefined` to omit `JINI_DAEMON_TOKEN`
|
|
30
31
|
* entirely. Omitting produces byte-identical output to before this parameter existed.
|
|
31
|
-
* @complexity O(
|
|
32
|
-
* @overallScore 100/100
|
|
32
|
+
* @complexity O(t) output space/time for t launch environment and registered ID bytes.
|
|
33
33
|
*/
|
|
34
34
|
export declare function buildMcpJsonServerEntry({ runId, options }: {
|
|
35
35
|
readonly runId: string;
|
|
36
|
-
readonly options: Pick<McpJsonInjectionOptions, 'command' | 'args' | 'daemonUrl' | 'env'>;
|
|
36
|
+
readonly options: Pick<McpJsonInjectionOptions, 'command' | 'args' | 'daemonUrl' | 'env' | 'diagnosticToolIds' | 'startupToolIds'>;
|
|
37
37
|
}, { credential }?: {
|
|
38
38
|
readonly credential?: string;
|
|
39
39
|
}): McpJsonServerEntry;
|
|
@@ -177,7 +177,9 @@ export declare function buildCodexMcpServerToml({ entry }: {
|
|
|
177
177
|
* install carries — model choice, sandbox policy, the trusted-project list, the operator's own
|
|
178
178
|
* other MCP servers — must survive a spawn byte-for-byte. Only the one table this driver itself
|
|
179
179
|
* owns (`jini` is this integration's own reserved server name — see {@link JINI_MCP_SERVER_KEY}) is
|
|
180
|
-
*
|
|
180
|
+
* removed by default, via the narrow line-oriented scan above, never a full TOML parse. Explicit
|
|
181
|
+
* per-run OFF choices additionally omit personal MCP tables or retain only those tables when
|
|
182
|
+
* user settings are off. Inline root MCP declarations fail closed when isolation is requested.
|
|
181
183
|
* @param existingRaw - The real Codex home's `config.toml` content, or `undefined` when it does not
|
|
182
184
|
* exist (a fresh Codex install — degrades to "start from just this run's block", matching
|
|
183
185
|
* {@link mergeMcpJsonContent}'s own "missing file" handling).
|
|
@@ -185,10 +187,12 @@ export declare function buildCodexMcpServerToml({ entry }: {
|
|
|
185
187
|
* @returns The full text to write to the scratch `CODEX_HOME`'s `config.toml`.
|
|
186
188
|
* @complexity O(n) in the existing config's length.
|
|
187
189
|
*/
|
|
188
|
-
export declare function buildCodexHomeConfigToml({ existingRaw, entry }: {
|
|
190
|
+
export declare function buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp, inheritUserSettings }: {
|
|
189
191
|
readonly existingRaw: string | undefined;
|
|
190
192
|
readonly entry: McpJsonServerEntry;
|
|
191
|
-
|
|
193
|
+
readonly inheritUserMcp?: boolean;
|
|
194
|
+
readonly inheritUserSettings?: boolean;
|
|
195
|
+
}, _optional?: Record<string, never>): string;
|
|
192
196
|
/**
|
|
193
197
|
* Where `'codex-toml'` reads the operator's REAL Codex config from, to seed a run's scratch copy —
|
|
194
198
|
* never where it writes. Resolved against the daemon HOST process's own environment (`hostEnv`,
|
|
@@ -250,7 +254,8 @@ export declare function defaultRemoveMcpJsonFile({ path }: {
|
|
|
250
254
|
export declare function resolveSourceClaudeConfigDir({ hostEnv }: {
|
|
251
255
|
readonly hostEnv: NodeJS.ProcessEnv;
|
|
252
256
|
}): string;
|
|
253
|
-
/** Phase 5:
|
|
257
|
+
/** Phase 5: resolve credentials and delivery, then await agy's scoped MCP permission setup.
|
|
258
|
+
* Setup errors release staged files and fail before spawn. @complexity O(settings bytes) for agy. */
|
|
254
259
|
export declare function resolveMcpBridgeForRun({ input, deps }: {
|
|
255
260
|
readonly input: {
|
|
256
261
|
readonly runId: string;
|
|
@@ -261,6 +266,7 @@ export declare function resolveMcpBridgeForRun({ input, deps }: {
|
|
|
261
266
|
readonly mcpJsonInjection: McpJsonInjectionOptions | undefined;
|
|
262
267
|
readonly cleanupStagedFiles: () => Promise<void>;
|
|
263
268
|
readonly failBeforeSpawn: FailBeforeSpawn;
|
|
269
|
+
readonly ensureAntigravityMcpPermission?: typeof ensureAntigravityMcpPermission;
|
|
264
270
|
};
|
|
265
271
|
}): Promise<McpBridgeDelivery | null>;
|
|
266
272
|
/** Phase 10: mechanism 1 of 5's one effect — stages this run's own `.mcp.json`, returning the path `cleanupStagedFiles` should later remove (`undefined` for every other mechanism / unconfigured host). */
|
|
@@ -268,6 +274,8 @@ export declare function writeMcpJsonIfNeeded({ input, deps }: {
|
|
|
268
274
|
readonly input: {
|
|
269
275
|
readonly runId: string;
|
|
270
276
|
readonly cwd: string;
|
|
277
|
+
readonly nativeToolAccess?: boolean | undefined;
|
|
278
|
+
readonly nativeCapabilities?: Partial<import('@jini-ai/agent-runtime').NativeCapabilities>;
|
|
271
279
|
readonly def: RuntimeAgentDef;
|
|
272
280
|
readonly mcpBridge: McpBridgeDelivery | null;
|
|
273
281
|
};
|
|
@@ -290,6 +298,8 @@ export declare function writeMcpJsonIfNeeded({ input, deps }: {
|
|
|
290
298
|
export declare function prepareCodexHomeIfNeeded({ input, deps }: {
|
|
291
299
|
readonly input: {
|
|
292
300
|
readonly runId: string;
|
|
301
|
+
readonly nativeToolAccess?: boolean | undefined;
|
|
302
|
+
readonly nativeCapabilities?: Partial<import('@jini-ai/agent-runtime').NativeCapabilities>;
|
|
293
303
|
readonly def: RuntimeAgentDef;
|
|
294
304
|
readonly mcpBridge: McpBridgeDelivery | null;
|
|
295
305
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { promises as fsPromises, } from 'node:fs';
|
|
2
|
+
import { ensureAntigravityMcpPermission } from './antigravity-permissions.js';
|
|
2
3
|
import { homedir, tmpdir, } from 'node:os';
|
|
3
4
|
import { join, resolve, } from 'node:path';
|
|
4
5
|
import { isRecord, errorMessage, } from './values.js';
|
|
@@ -40,11 +41,10 @@ export function bridgeUnavailableMessage(status, probe) {
|
|
|
40
41
|
* `writeMcpJsonForRun`'s job, which keeps the effect out of this function.
|
|
41
42
|
*
|
|
42
43
|
* @param runId - The run this entry scopes its child to.
|
|
43
|
-
* @param options -
|
|
44
|
+
* @param options - Launch settings and independent readers of diagnostic and visible startup IDs.
|
|
44
45
|
* @param credential - The already-resolved bearer token, or `undefined` to omit `JINI_DAEMON_TOKEN`
|
|
45
46
|
* entirely. Omitting produces byte-identical output to before this parameter existed.
|
|
46
|
-
* @complexity O(
|
|
47
|
-
* @overallScore 100/100
|
|
47
|
+
* @complexity O(t) output space/time for t launch environment and registered ID bytes.
|
|
48
48
|
*/
|
|
49
49
|
export function buildMcpJsonServerEntry({ runId, options }, { credential } = {}) {
|
|
50
50
|
const hostEnv = Object.entries(options.env ?? {}).filter(([key]) => !key.startsWith('JINI_'));
|
|
@@ -55,6 +55,8 @@ export function buildMcpJsonServerEntry({ runId, options }, { credential } = {})
|
|
|
55
55
|
...Object.fromEntries(hostEnv),
|
|
56
56
|
JINI_RUN_ID: runId,
|
|
57
57
|
JINI_DAEMON_URL: options.daemonUrl,
|
|
58
|
+
...(options.diagnosticToolIds ? { JINI_DIAGNOSTIC_TOOL_IDS: JSON.stringify(options.diagnosticToolIds()) } : {}),
|
|
59
|
+
...(options.startupToolIds ? { JINI_STARTUP_TOOL_IDS: JSON.stringify(options.startupToolIds({ runId })) } : {}),
|
|
58
60
|
...(credential !== undefined ? { JINI_DAEMON_TOKEN: credential } : {}),
|
|
59
61
|
},
|
|
60
62
|
};
|
|
@@ -313,30 +315,43 @@ export function buildCodexMcpServerToml({ entry }) {
|
|
|
313
315
|
* @complexity O(n) in the length of `rawKey`.
|
|
314
316
|
*/
|
|
315
317
|
function splitTomlDottedKey(rawKey) {
|
|
316
|
-
const
|
|
317
|
-
let current = '';
|
|
318
|
-
let quote = null;
|
|
318
|
+
const scan = { segments: [], current: '', quote: null };
|
|
319
319
|
for (const ch of rawKey) {
|
|
320
|
-
if (quote)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
current
|
|
320
|
+
if (scan.quote === null)
|
|
321
|
+
scanBareTomlKeyChar(scan, ch);
|
|
322
|
+
else if (ch === scan.quote)
|
|
323
|
+
scan.quote = null;
|
|
324
|
+
else
|
|
325
|
+
scan.current += ch;
|
|
326
|
+
}
|
|
327
|
+
scan.segments.push(scan.current.trim());
|
|
328
|
+
return scan.segments;
|
|
329
|
+
}
|
|
330
|
+
/** One key character outside quotes: opens a quote, ends the open segment at a `.`, or extends it. */
|
|
331
|
+
function scanBareTomlKeyChar(scan, ch) {
|
|
332
|
+
if (ch === '"' || ch === "'")
|
|
333
|
+
scan.quote = ch;
|
|
334
|
+
else if (ch === '.') {
|
|
335
|
+
scan.segments.push(scan.current.trim());
|
|
336
|
+
scan.current = '';
|
|
337
337
|
}
|
|
338
|
-
|
|
339
|
-
|
|
338
|
+
else
|
|
339
|
+
scan.current += ch;
|
|
340
|
+
}
|
|
341
|
+
// An inline or dotted root `mcp_servers` declaration, which the table scanner below cannot rewrite.
|
|
342
|
+
const INLINE_MCP_SERVERS = /^\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*(?:\.|=)/;
|
|
343
|
+
// `[<key>]` optionally followed only by a `# comment`; `[[...]]` never matches (see below).
|
|
344
|
+
const TOML_TABLE_HEADER = /^\s*\[([^[\]]+)\]\s*(#.*)?$/;
|
|
345
|
+
/**
|
|
346
|
+
* Whether {@link stripExistingJiniMcpServerTable} drops the table named `key`: always the jini
|
|
347
|
+
* table and its subtables; with `allServers` every MCP table; with `mcpOnly` every non-MCP table.
|
|
348
|
+
*/
|
|
349
|
+
function dropsTomlTable(key, optional) {
|
|
350
|
+
const jiniTableKey = `mcp_servers.${JINI_MCP_SERVER_KEY}`;
|
|
351
|
+
if (key === jiniTableKey || key.startsWith(`${jiniTableKey}.`))
|
|
352
|
+
return true;
|
|
353
|
+
const isMcp = key === 'mcp_servers' || key.startsWith('mcp_servers.');
|
|
354
|
+
return (optional.allServers === true && isMcp) || (optional.mcpOnly === true && !isMcp);
|
|
340
355
|
}
|
|
341
356
|
/**
|
|
342
357
|
* Removes any pre-existing `[mcp_servers.{@link JINI_MCP_SERVER_KEY}]` table — and its
|
|
@@ -378,18 +393,19 @@ function splitTomlDottedKey(rawKey) {
|
|
|
378
393
|
* @returns `existingRaw` with any jini table/subtable removed.
|
|
379
394
|
* @complexity O(n) in the number of lines.
|
|
380
395
|
*/
|
|
381
|
-
function stripExistingJiniMcpServerTable(existingRaw) {
|
|
382
|
-
const jiniTableKey = `mcp_servers.${JINI_MCP_SERVER_KEY}`;
|
|
396
|
+
function stripExistingJiniMcpServerTable(existingRaw, optional = {}) {
|
|
383
397
|
const kept = [];
|
|
384
|
-
let skipping =
|
|
398
|
+
let skipping = optional.mcpOnly === true;
|
|
385
399
|
for (const line of existingRaw.split('\n')) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (skipping)
|
|
391
|
-
continue;
|
|
400
|
+
// The existing table scanner cannot safely rewrite inline/dotted root server values. Refuse
|
|
401
|
+
// that uncommon shape when isolating servers rather than accidentally preserving a grant.
|
|
402
|
+
if (optional.allServers && INLINE_MCP_SERVERS.test(line)) {
|
|
403
|
+
throw new Error('Cannot turn off personal MCP servers: the Codex config.toml declares mcp_servers inline (a dotted key or `mcp_servers = {...}`), which cannot be removed safely; move each server into its own [mcp_servers.<name>] table.');
|
|
392
404
|
}
|
|
405
|
+
const header = TOML_TABLE_HEADER.exec(line);
|
|
406
|
+
// The header's key capture group is not optional, so `header[1]` is always set on a match.
|
|
407
|
+
if (header)
|
|
408
|
+
skipping = dropsTomlTable(splitTomlDottedKey(header[1]).join('.'), optional);
|
|
393
409
|
if (!skipping)
|
|
394
410
|
kept.push(line);
|
|
395
411
|
}
|
|
@@ -407,7 +423,9 @@ function stripExistingJiniMcpServerTable(existingRaw) {
|
|
|
407
423
|
* install carries — model choice, sandbox policy, the trusted-project list, the operator's own
|
|
408
424
|
* other MCP servers — must survive a spawn byte-for-byte. Only the one table this driver itself
|
|
409
425
|
* owns (`jini` is this integration's own reserved server name — see {@link JINI_MCP_SERVER_KEY}) is
|
|
410
|
-
*
|
|
426
|
+
* removed by default, via the narrow line-oriented scan above, never a full TOML parse. Explicit
|
|
427
|
+
* per-run OFF choices additionally omit personal MCP tables or retain only those tables when
|
|
428
|
+
* user settings are off. Inline root MCP declarations fail closed when isolation is requested.
|
|
411
429
|
* @param existingRaw - The real Codex home's `config.toml` content, or `undefined` when it does not
|
|
412
430
|
* exist (a fresh Codex install — degrades to "start from just this run's block", matching
|
|
413
431
|
* {@link mergeMcpJsonContent}'s own "missing file" handling).
|
|
@@ -415,8 +433,8 @@ function stripExistingJiniMcpServerTable(existingRaw) {
|
|
|
415
433
|
* @returns The full text to write to the scratch `CODEX_HOME`'s `config.toml`.
|
|
416
434
|
* @complexity O(n) in the existing config's length.
|
|
417
435
|
*/
|
|
418
|
-
export function buildCodexHomeConfigToml({ existingRaw, entry }) {
|
|
419
|
-
const base = stripExistingJiniMcpServerTable(existingRaw ?? '');
|
|
436
|
+
export function buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp = true, inheritUserSettings = true }, _optional = {}) {
|
|
437
|
+
const base = stripExistingJiniMcpServerTable(existingRaw ?? '', { allServers: !inheritUserMcp, mcpOnly: !inheritUserSettings });
|
|
420
438
|
const separator = base.length === 0 ? '' : base.endsWith('\n') ? '\n' : '\n\n';
|
|
421
439
|
return `${base}${separator}${buildCodexMcpServerToml({ entry: entry })}`;
|
|
422
440
|
}
|
|
@@ -537,12 +555,12 @@ function mcpJsonPathForRun(cwd, runId) {
|
|
|
537
555
|
* @complexity O(1) plus one `readFile`/`writeFile` round trip.
|
|
538
556
|
* @overallScore 100/100
|
|
539
557
|
*/
|
|
540
|
-
async function writeMcpJsonForRun(cwd, delivery, options) {
|
|
558
|
+
async function writeMcpJsonForRun(cwd, delivery, options, inheritUserConfig = true) {
|
|
541
559
|
const readFileFn = options.readFile ?? defaultReadMcpJsonFile;
|
|
542
560
|
const writeFileFn = options.writeFile ?? defaultWriteMcpJsonFile;
|
|
543
561
|
let existingRaw;
|
|
544
562
|
try {
|
|
545
|
-
existingRaw = await readFileFn({ path: join(cwd, '.mcp.json') });
|
|
563
|
+
existingRaw = inheritUserConfig ? await readFileFn({ path: join(cwd, '.mcp.json') }) : undefined;
|
|
546
564
|
}
|
|
547
565
|
catch {
|
|
548
566
|
// No existing file (ENOENT — the common case) or unreadable for any other reason: both
|
|
@@ -558,14 +576,24 @@ function defaultRemoveCodexHomeDir({ path }) {
|
|
|
558
576
|
return fsPromises.rm(path, { recursive: true, force: true });
|
|
559
577
|
}
|
|
560
578
|
/** Config credentials stay run-scoped, but rollouts must survive both completion and cancellation.
|
|
561
|
-
* Native
|
|
562
|
-
*
|
|
563
|
-
|
|
579
|
+
* Native links keep Codex as the sole writer of sessions and refreshed authentication.
|
|
580
|
+
* Parity also links skills/rules/plugins and other home entries; config.toml alone stays staged
|
|
581
|
+
* so run-scoped bearer tokens never reach the real config. Cleanup removes links, not targets.
|
|
582
|
+
* O(h) I/O for h top-level home entries; locked runs link only sessions and authentication. */
|
|
583
|
+
async function defaultLinkCodexSessionStore({ runHome, sourceHome }, optional = {}) {
|
|
564
584
|
for (const name of ['sessions', 'archived_sessions']) {
|
|
565
585
|
const target = resolve(sourceHome, name);
|
|
566
586
|
await fsPromises.mkdir(target, { recursive: true, mode: 0o700 });
|
|
567
587
|
await fsPromises.symlink(target, join(runHome, name), 'junction');
|
|
568
588
|
}
|
|
589
|
+
await fsPromises.symlink(join(sourceHome, 'auth.json'), join(runHome, 'auth.json'), 'file');
|
|
590
|
+
if (optional.inheritUserConfig !== false) {
|
|
591
|
+
for (const entry of await fsPromises.readdir(sourceHome, { withFileTypes: true })) {
|
|
592
|
+
if (['config.toml', 'auth.json', 'sessions', 'archived_sessions'].includes(entry.name))
|
|
593
|
+
continue;
|
|
594
|
+
await fsPromises.symlink(join(sourceHome, entry.name), join(runHome, entry.name), entry.isDirectory() ? 'junction' : 'file');
|
|
595
|
+
}
|
|
596
|
+
}
|
|
569
597
|
}
|
|
570
598
|
function resolveCodexHomeSeams(options) {
|
|
571
599
|
return {
|
|
@@ -583,15 +611,15 @@ function resolveCodexHomeSeams(options) {
|
|
|
583
611
|
* - `config.toml`: the real Codex home's own config (read best-effort — see
|
|
584
612
|
* {@link buildCodexHomeConfigToml}'s "missing file" handling) with this run's
|
|
585
613
|
* `[mcp_servers.jini]` table appended.
|
|
586
|
-
* - `auth.json`: a
|
|
614
|
+
* - `auth.json`: a link to the real Codex home's stored login, so the spawned CLI is
|
|
587
615
|
* still authenticated. Best-effort is safe here, not merely convenient: a real headless spawn
|
|
588
616
|
* against a `CODEX_HOME` with no `auth.json` at all was confirmed (against installed Codex CLI
|
|
589
617
|
* 0.151.0) to fail fast with a structured `401 Unauthorized` stream event, never an interactive
|
|
590
618
|
* login prompt or a hang — see `defs/codex.ts`'s module doc for the full transcript summary.
|
|
591
619
|
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
620
|
+
* Never changes the real config. Authentication is linked so a CLI token refresh survives cleanup;
|
|
621
|
+
* copying rotating credentials lost refreshes and could log the operator out. Deleting scratch
|
|
622
|
+
* state must not erase linked native state. The run-bound MCP credential stays in scratch config.
|
|
595
623
|
*
|
|
596
624
|
* A failure after the directory is created (a rejecting `writeFile`, most plausibly) does not leak
|
|
597
625
|
* it: the directory may already hold a partial `config.toml` or a copied credential, so the
|
|
@@ -603,10 +631,10 @@ function resolveCodexHomeSeams(options) {
|
|
|
603
631
|
* @param sourceCodexHomeDir - Where to read the real install's `config.toml`/`auth.json` from — see {@link resolveSourceCodexHomeDir}.
|
|
604
632
|
* @param seams - Injectable mkdtemp/readFile/writeFile/removeDir/linkSessionStore, real filesystem by default.
|
|
605
633
|
* @throws Whatever staging rejects with — the caller ({@link prepareCodexHomeIfNeeded}) turns that into a pre-spawn `AGENT_SPAWN_FAILED` failure, matching {@link writeMcpJsonForRun}'s own contract.
|
|
606
|
-
* @complexity O(
|
|
634
|
+
* @complexity O(h) filesystem operations for h top-level user-home entries; one staged config write.
|
|
607
635
|
* @overallScore 100/100
|
|
608
636
|
*/
|
|
609
|
-
async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
|
|
637
|
+
async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams, inheritUserConfig = true, inheritUserMcp = true) {
|
|
610
638
|
// Stricter than `@jini-ai/agent-runtime`'s `prepareAgentLogFile`/`preparePromptFileForAgent`
|
|
611
639
|
// labels (which keep dots): this prefix stages a directory that ends up holding a copied Codex
|
|
612
640
|
// login credential, so it gets `mcpJsonPathForRun`'s tighter discipline instead — dots stripped
|
|
@@ -617,23 +645,15 @@ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
|
|
|
617
645
|
try {
|
|
618
646
|
let existingConfigRaw;
|
|
619
647
|
try {
|
|
620
|
-
existingConfigRaw = await seams.readFile({ path: join(sourceCodexHomeDir, 'config.toml') });
|
|
648
|
+
existingConfigRaw = inheritUserConfig || inheritUserMcp ? await seams.readFile({ path: join(sourceCodexHomeDir, 'config.toml') }) : undefined;
|
|
621
649
|
}
|
|
622
650
|
catch {
|
|
623
651
|
// No config yet (fresh Codex install) or unreadable — start from just this run's block,
|
|
624
652
|
// matching writeMcpJsonForRun's identical "missing file" handling.
|
|
625
653
|
existingConfigRaw = undefined;
|
|
626
654
|
}
|
|
627
|
-
await seams.writeFile({ path: join(dir, 'config.toml'), content: buildCodexHomeConfigToml({ existingRaw: existingConfigRaw, entry: entry }) });
|
|
628
|
-
|
|
629
|
-
const authRaw = await seams.readFile({ path: join(sourceCodexHomeDir, 'auth.json') });
|
|
630
|
-
await seams.writeFile({ path: join(dir, 'auth.json'), content: authRaw });
|
|
631
|
-
}
|
|
632
|
-
catch {
|
|
633
|
-
// No stored login (or unreadable) — the spawned CLI runs unauthenticated. Confirmed above:
|
|
634
|
-
// this fails the run fast and observably, never as a hang.
|
|
635
|
-
}
|
|
636
|
-
await seams.linkSessionStore({ runHome: dir, sourceHome: sourceCodexHomeDir }, {});
|
|
655
|
+
await seams.writeFile({ path: join(dir, 'config.toml'), content: buildCodexHomeConfigToml({ existingRaw: existingConfigRaw, entry: entry, inheritUserMcp, inheritUserSettings: inheritUserConfig }) });
|
|
656
|
+
await seams.linkSessionStore({ runHome: dir, sourceHome: sourceCodexHomeDir }, { inheritUserConfig });
|
|
637
657
|
}
|
|
638
658
|
catch (err) {
|
|
639
659
|
await seams.removeDir({ path: dir }).catch(() => {
|
|
@@ -756,25 +776,34 @@ async function prepareClaudeConfigDirForRun(runId, sourceConfigDir, seams) {
|
|
|
756
776
|
},
|
|
757
777
|
};
|
|
758
778
|
}
|
|
759
|
-
/** Phase 5:
|
|
779
|
+
/** Phase 5: resolve credentials and delivery, then await agy's scoped MCP permission setup.
|
|
780
|
+
* Setup errors release staged files and fail before spawn. @complexity O(settings bytes) for agy. */
|
|
760
781
|
export async function resolveMcpBridgeForRun({ input, deps }) {
|
|
761
782
|
try {
|
|
762
783
|
// Awaited here rather than inside `buildMcpBridgeDelivery` so that function stays pure and
|
|
763
784
|
// synchronous. `undefined` when the host supplied no resolver, which omits the token entirely.
|
|
764
785
|
const credential = deps.mcpJsonInjection !== undefined ? await deps.mcpJsonInjection.credential?.({ runId: input.runId }) : undefined;
|
|
765
|
-
|
|
786
|
+
const delivery = buildMcpBridgeDelivery({
|
|
766
787
|
cwd: input.cwd,
|
|
767
788
|
runId: input.runId,
|
|
768
789
|
strategy: input.def.externalMcpInjection,
|
|
769
790
|
options: deps.mcpJsonInjection,
|
|
770
791
|
credential,
|
|
771
792
|
});
|
|
793
|
+
if (input.def.id === 'antigravity' && delivery?.kind === 'env-passthrough') {
|
|
794
|
+
await (deps.ensureAntigravityMcpPermission ?? ensureAntigravityMcpPermission)({
|
|
795
|
+
settingsPath: join(homedir(), '.gemini', 'antigravity-cli', 'settings.json'),
|
|
796
|
+
serverName: JINI_MCP_SERVER_KEY,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
return delivery;
|
|
772
800
|
}
|
|
773
801
|
catch (err) {
|
|
774
802
|
// Spawning a child that cannot authenticate would produce a run whose every bridged tool call
|
|
775
|
-
// 401s, so a rejecting credential resolver fails the run before spawn instead.
|
|
803
|
+
// 401s, so a rejecting credential resolver fails the run before spawn instead. Permission setup
|
|
804
|
+
// failure is fatal for the same reason: the agent would start without its tools.
|
|
776
805
|
await deps.cleanupStagedFiles();
|
|
777
|
-
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not resolve the MCP bridge credential for agent "${input.def.id}": ${errorMessage(err)}` });
|
|
806
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not resolve the MCP bridge credential or permissions for agent "${input.def.id}": ${errorMessage(err)}` });
|
|
778
807
|
}
|
|
779
808
|
}
|
|
780
809
|
/** Phase 10: mechanism 1 of 5's one effect — stages this run's own `.mcp.json`, returning the path `cleanupStagedFiles` should later remove (`undefined` for every other mechanism / unconfigured host). */
|
|
@@ -783,7 +812,7 @@ export async function writeMcpJsonIfNeeded({ input, deps }) {
|
|
|
783
812
|
return undefined;
|
|
784
813
|
}
|
|
785
814
|
try {
|
|
786
|
-
await writeMcpJsonForRun(input.cwd, input.mcpBridge, deps.mcpJsonInjection);
|
|
815
|
+
await writeMcpJsonForRun(input.cwd, input.mcpBridge, deps.mcpJsonInjection, input.nativeToolAccess !== false && input.nativeCapabilities?.userMcp !== false);
|
|
787
816
|
return input.mcpBridge.mcpJsonPath;
|
|
788
817
|
}
|
|
789
818
|
catch (err) {
|
|
@@ -791,6 +820,11 @@ export async function writeMcpJsonIfNeeded({ input, deps }) {
|
|
|
791
820
|
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not write .mcp.json for agent "${input.def.id}": ${errorMessage(err)}` });
|
|
792
821
|
}
|
|
793
822
|
}
|
|
823
|
+
/** True when a Codex run turns off the operator's personal MCP servers or settings, which only a staged home can do. */
|
|
824
|
+
function codexTurnsOffPersonalConfig(input) {
|
|
825
|
+
const capabilities = input.nativeCapabilities;
|
|
826
|
+
return input.def.id === 'codex' && (capabilities?.userMcp === false || capabilities?.userSettings === false);
|
|
827
|
+
}
|
|
794
828
|
/**
|
|
795
829
|
* Phase 10b: mechanism 5 of 5's one effect — stages this run's scratch `CODEX_HOME` directory,
|
|
796
830
|
* returning the prepared handle `cleanupStagedFiles` should later release (`null` for every other
|
|
@@ -803,10 +837,16 @@ export async function writeMcpJsonIfNeeded({ input, deps }) {
|
|
|
803
837
|
*/
|
|
804
838
|
export async function prepareCodexHomeIfNeeded({ input, deps }) {
|
|
805
839
|
if (input.mcpBridge?.kind !== 'codex-toml' || deps.mcpJsonInjection === undefined) {
|
|
840
|
+
if (codexTurnsOffPersonalConfig(input)) {
|
|
841
|
+
await deps.releaseStagedResources();
|
|
842
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'NATIVE_TOOL_POLICY_UNSUPPORTED',
|
|
843
|
+
message: 'AgentExecutor: disabling personal Codex configuration requires run-scoped home staging.' });
|
|
844
|
+
}
|
|
806
845
|
return null;
|
|
807
846
|
}
|
|
847
|
+
const nativeAllowed = input.nativeToolAccess !== false;
|
|
808
848
|
try {
|
|
809
|
-
return await prepareCodexHomeForRun(input.runId, input.mcpBridge.serverEntry, resolveSourceCodexHomeDir({ hostEnv: deps.hostEnv }), resolveCodexHomeSeams(deps.mcpJsonInjection));
|
|
849
|
+
return await prepareCodexHomeForRun(input.runId, input.mcpBridge.serverEntry, resolveSourceCodexHomeDir({ hostEnv: deps.hostEnv }), resolveCodexHomeSeams(deps.mcpJsonInjection), nativeAllowed && input.nativeCapabilities?.userSettings !== false, nativeAllowed && input.nativeCapabilities?.userMcp !== false);
|
|
810
850
|
}
|
|
811
851
|
catch (err) {
|
|
812
852
|
await deps.releaseStagedResources();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TerminalRunOutcome } from '../close-status.js';
|
|
2
|
+
import type { RunLifecycle } from '../run-lifecycle.js';
|
|
3
|
+
/**
|
|
4
|
+
* Observe canonical run events, including delegated MCP tools, and reject empty successes.
|
|
5
|
+
* Diagnostics are bounded to 8 KiB; raw progress and private thinking are not assistant output.
|
|
6
|
+
* Returns a close-time guard that unsubscribes and emits a readable error before terminal failure.
|
|
7
|
+
* @complexity O(received bytes), with bounded diagnostic storage.
|
|
8
|
+
*/
|
|
9
|
+
export declare function observeRunOutput({ lifecycle, runId, agentName }: {
|
|
10
|
+
lifecycle: RunLifecycle;
|
|
11
|
+
runId: string;
|
|
12
|
+
agentName: string;
|
|
13
|
+
}, { plainStdout }?: {
|
|
14
|
+
plainStdout?: boolean;
|
|
15
|
+
}): {
|
|
16
|
+
settle(input: {
|
|
17
|
+
status: TerminalRunOutcome;
|
|
18
|
+
}, options?: Record<string, never>): Promise<TerminalRunOutcome>;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=run-output.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
2
|
+
// Only a terminal-prefixed failure overrides real output; ordinary assistant explanations
|
|
3
|
+
// can mention empty output or headless permissions without being failed runs themselves.
|
|
4
|
+
const DIAGNOSTIC_LINE = /^(?:jetski|agy|error|fatal):\s*(?:no (?:output|response) produced(?:\s*[—–:-]|[.!]\s*$)|(?:error:\s*)?permission denied\b.*headless|headless\b.*auto-denied)/i;
|
|
5
|
+
/** Records whether the event is assistant text, tool activity or a run error. */
|
|
6
|
+
function noteActivity(seen, event) {
|
|
7
|
+
if (event.kind === 'error')
|
|
8
|
+
seen.error = true;
|
|
9
|
+
if (event.kind !== 'agent')
|
|
10
|
+
return;
|
|
11
|
+
const payload = event.payload;
|
|
12
|
+
if (payload.type === 'text_delta' && payload.delta.trim())
|
|
13
|
+
seen.text = true;
|
|
14
|
+
if (payload.type === 'tool_use' || payload.type === 'tool_result')
|
|
15
|
+
seen.tool = true;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Appends a watched output chunk to its 8 KiB tail and keeps the latest failure line found there.
|
|
19
|
+
* stderr is always watched; stdout only for plain-stdout agents, whose stdout is the CLI's own text.
|
|
20
|
+
*/
|
|
21
|
+
function noteDiagnostic({ seen, tails, event, plainStdout }) {
|
|
22
|
+
if (event.kind !== 'stderr' && !(plainStdout && event.kind === 'stdout'))
|
|
23
|
+
return;
|
|
24
|
+
const channel = event.kind === 'stderr' ? 'stderr' : 'stdout';
|
|
25
|
+
tails[channel] = (tails[channel] + event.payload.chunk).slice(-8192);
|
|
26
|
+
const line = tails[channel].split(/\r?\n/).find(value => DIAGNOSTIC_LINE.test(value.trim()));
|
|
27
|
+
if (line)
|
|
28
|
+
seen.diagnostic = line;
|
|
29
|
+
}
|
|
30
|
+
/** True when the run's own outcome stands; false when the observed output turns it into a failure. */
|
|
31
|
+
function keepsOutcome(status, seen) {
|
|
32
|
+
if (status === 'cancelled')
|
|
33
|
+
return true;
|
|
34
|
+
if (seen.diagnostic)
|
|
35
|
+
return false;
|
|
36
|
+
return status === 'failed' || (!seen.error && (seen.text || seen.tool));
|
|
37
|
+
}
|
|
38
|
+
/** The readable reason recorded when an apparently finished run is turned into a failure. */
|
|
39
|
+
function failureReason(agentName, seen) {
|
|
40
|
+
if (seen.diagnostic) {
|
|
41
|
+
const cause = /headless|permission|auto-denied/i.test(seen.diagnostic) ? 'permission denied in headless mode' : 'no output produced';
|
|
42
|
+
return `${agentName} failed: ${cause}. ${seen.diagnostic}`;
|
|
43
|
+
}
|
|
44
|
+
return seen.error ? `${agentName} ended with an error.` : `${agentName} returned no assistant text or tool activity. Please retry.`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Observe canonical run events, including delegated MCP tools, and reject empty successes.
|
|
48
|
+
* Diagnostics are bounded to 8 KiB; raw progress and private thinking are not assistant output.
|
|
49
|
+
* Returns a close-time guard that unsubscribes and emits a readable error before terminal failure.
|
|
50
|
+
* @complexity O(received bytes), with bounded diagnostic storage.
|
|
51
|
+
*/
|
|
52
|
+
export function observeRunOutput({ lifecycle, runId, agentName }, { plainStdout = false } = {}) {
|
|
53
|
+
const seen = { text: false, tool: false, error: false, diagnostic: undefined };
|
|
54
|
+
const tails = { stdout: '', stderr: '' };
|
|
55
|
+
const subscription = lifecycle.stream({ runId, onEvent: event => {
|
|
56
|
+
noteActivity(seen, event);
|
|
57
|
+
noteDiagnostic({ seen, tails, event, plainStdout });
|
|
58
|
+
} }).catch(error => {
|
|
59
|
+
// An observer failure is a daemon diagnostic, not evidence that the agent failed.
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.warn(`[@jini-ai/daemon] agent-executor: output observer unavailable for run "${runId}"`, redactSecrets({ input: error instanceof Error ? error.message : String(error) }));
|
|
62
|
+
return null;
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
async settle({ status }, _options = {}) {
|
|
66
|
+
const observer = await subscription;
|
|
67
|
+
// Without observation, missing events cannot prove an empty run; keep its real outcome.
|
|
68
|
+
if (observer?.kind !== 'ok')
|
|
69
|
+
return status;
|
|
70
|
+
observer.unsubscribe();
|
|
71
|
+
if (keepsOutcome(status, seen))
|
|
72
|
+
return status;
|
|
73
|
+
const message = redactSecrets({ input: failureReason(agentName, seen).slice(0, 1200) });
|
|
74
|
+
// A watchdog can already have settled the run; its terminal state must still win.
|
|
75
|
+
await lifecycle.emit({ runId, input: { event: 'error', data: { message } } }).catch(() => { });
|
|
76
|
+
return 'failed';
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=run-output.js.map
|