@jini-ai/daemon 0.3.8 → 0.5.1
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 +124 -0
- package/README.md +50 -8
- package/dist/agent-executor/contracts.d.ts +736 -0
- package/dist/agent-executor/contracts.js +28 -0
- package/dist/agent-executor/event-translation.d.ts +87 -0
- package/dist/agent-executor/event-translation.js +230 -0
- package/dist/agent-executor/events.d.ts +190 -0
- package/dist/agent-executor/events.js +848 -0
- package/dist/agent-executor/handoff.d.ts +18 -0
- package/dist/agent-executor/handoff.js +16 -0
- package/dist/agent-executor/index.d.ts +98 -0
- package/dist/agent-executor/index.js +98 -0
- package/dist/agent-executor/launch.d.ts +375 -0
- package/dist/agent-executor/launch.js +1001 -0
- package/dist/agent-executor/mcp-bridge.d.ts +338 -0
- package/dist/agent-executor/mcp-bridge.js +835 -0
- package/dist/agent-executor/values.d.ts +5 -0
- package/dist/agent-executor/values.js +13 -0
- package/dist/agent-executor.d.ts +375 -176
- package/dist/agent-executor.js +388 -335
- package/dist/close-status.d.ts +11 -8
- package/dist/close-status.js +31 -19
- package/dist/continuation/continuation-transport.d.ts +3 -1
- package/dist/continuation/continuation-transport.js +1 -1
- package/dist/continuation/journal.d.ts +12 -5
- package/dist/continuation/journal.js +4 -4
- package/dist/continuation/run-scoped-context-store.d.ts +13 -4
- package/dist/continuation/run-scoped-context-store.js +6 -6
- package/dist/continuation/run-start-handler.d.ts +1 -1
- package/dist/continuation/run-start-handler.js +3 -16
- package/dist/delegated-tool-bridge.d.ts +7 -3
- package/dist/delegated-tool-bridge.js +32 -31
- package/dist/event-log.d.ts +7 -5
- package/dist/event-log.js +12 -11
- package/dist/frontend-capability-tools.d.ts +1 -1
- package/dist/frontend-capability-tools.js +3 -2
- package/dist/frontend-session-registry.d.ts +32 -9
- package/dist/frontend-session-registry.js +12 -11
- package/dist/http/active-context.d.ts +105 -0
- package/dist/http/active-context.js +67 -0
- package/dist/http/agents.d.ts +68 -0
- package/dist/http/agents.js +12 -0
- package/dist/http/attachments.d.ts +618 -0
- package/dist/http/attachments.js +1034 -0
- package/dist/http/cancel-owned-runs.d.ts +49 -0
- package/dist/http/cancel-owned-runs.js +35 -0
- package/dist/http/component-catalog.d.ts +84 -0
- package/dist/http/component-catalog.js +64 -0
- package/dist/http/connectors.d.ts +209 -0
- package/dist/http/connectors.js +409 -0
- package/dist/http/daemon-status.d.ts +76 -0
- package/dist/http/daemon-status.js +55 -0
- package/dist/http/db-ops.d.ts +123 -0
- package/dist/http/db-ops.js +189 -0
- package/dist/http/delegated-tools.d.ts +113 -0
- package/dist/http/delegated-tools.js +230 -0
- package/dist/http/frontend-control.d.ts +136 -0
- package/dist/http/frontend-control.js +79 -0
- package/dist/http/frontend-sessions.d.ts +110 -0
- package/dist/http/frontend-sessions.js +180 -0
- package/dist/http/health.d.ts +95 -0
- package/dist/http/health.js +40 -0
- package/dist/http/host-tools.d.ts +214 -0
- package/dist/http/host-tools.js +329 -0
- package/dist/http/index.d.ts +52 -0
- package/dist/http/index.js +27 -0
- package/dist/http/media.d.ts +146 -0
- package/dist/http/media.js +346 -0
- package/dist/http/memory.d.ts +265 -0
- package/dist/http/memory.js +328 -0
- package/dist/http/model-proxy.d.ts +49 -0
- package/dist/http/model-proxy.js +642 -0
- package/dist/http/remote-run-events.d.ts +70 -0
- package/dist/http/remote-run-events.js +224 -0
- package/dist/http/research.d.ts +48 -0
- package/dist/http/research.js +315 -0
- package/dist/http/route-manifest.d.ts +35 -0
- package/dist/http/route-manifest.js +103 -0
- package/dist/http/routines.d.ts +120 -0
- package/dist/http/routines.js +298 -0
- package/dist/http/run-credentials.d.ts +73 -0
- package/dist/http/run-credentials.js +71 -0
- package/dist/http/runs.d.ts +91 -0
- package/dist/http/runs.js +221 -0
- package/dist/http/terminals.d.ts +138 -0
- package/dist/http/terminals.js +325 -0
- package/dist/http/tool-catalog.d.ts +78 -0
- package/dist/http/tool-catalog.js +66 -0
- package/dist/http/workspace-root.d.ts +77 -0
- package/dist/http/workspace-root.js +63 -0
- package/dist/http/xai.d.ts +90 -0
- package/dist/http/xai.js +655 -0
- package/dist/image-prompt-delivery.d.ts +13 -3
- package/dist/image-prompt-delivery.js +5 -5
- package/dist/index.d.ts +556 -4
- package/dist/index.js +556 -4
- package/dist/legacy-data-migration.d.ts +59 -8
- package/dist/legacy-data-migration.js +38 -33
- package/dist/read-only-tools.d.ts +130 -0
- package/dist/read-only-tools.js +115 -0
- package/dist/remote-tool-bridge.d.ts +11 -5
- package/dist/remote-tool-bridge.js +15 -15
- package/dist/routines/index.d.ts +1 -1
- package/dist/routines/index.js +1 -1
- package/dist/routines/routine-store.d.ts +34 -12
- package/dist/routines/routine-store.js +21 -20
- package/dist/routines/schedule.d.ts +48 -10
- package/dist/routines/schedule.js +21 -21
- package/dist/routines/scheduler.d.ts +32 -11
- package/dist/routines/scheduler.js +61 -57
- package/dist/routines/types.d.ts +10 -3
- package/dist/run/core/result.d.ts +6 -2
- package/dist/run/core/result.js +3 -3
- package/dist/run/core/retry.d.ts +17 -4
- package/dist/run/core/retry.js +7 -11
- package/dist/run/diagnostics/diagnostics.d.ts +14 -4
- package/dist/run/diagnostics/diagnostics.js +7 -7
- package/dist/run/index.d.ts +1 -1
- package/dist/run/index.js +1 -1
- package/dist/run-credentials.d.ts +138 -0
- package/dist/run-credentials.js +152 -0
- package/dist/run-lifecycle.d.ts +92 -33
- package/dist/run-lifecycle.js +81 -74
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.js +2 -0
- package/dist/session-coordination.d.ts +115 -0
- package/dist/session-coordination.js +111 -0
- package/dist/store/agent-sessions/errors.d.ts +8 -0
- package/dist/store/agent-sessions/errors.js +10 -0
- package/dist/store/agent-sessions/index.d.ts +12 -0
- package/dist/store/agent-sessions/index.js +19 -0
- package/dist/store/agent-sessions/pglite.d.ts +12 -0
- package/dist/store/agent-sessions/pglite.js +7 -0
- package/dist/store/agent-sessions/ports.d.ts +24 -0
- package/dist/store/agent-sessions/ports.js +2 -0
- package/dist/store/agent-sessions/postgres.d.ts +12 -0
- package/dist/store/agent-sessions/postgres.js +7 -0
- package/dist/store/agent-sessions/schema.d.ts +3 -0
- package/dist/store/agent-sessions/schema.js +23 -0
- package/dist/store/agent-sessions/sql.d.ts +35 -0
- package/dist/store/agent-sessions/sql.js +59 -0
- package/dist/store/agent-sessions/sqlite.d.ts +117 -0
- package/dist/store/agent-sessions/sqlite.js +128 -0
- package/dist/store/event-log/sqlite.d.ts +75 -0
- package/dist/store/event-log/sqlite.js +230 -0
- package/dist/surface-exchanges.d.ts +97 -0
- package/dist/surface-exchanges.js +153 -0
- package/dist/terminal-session.d.ts +43 -11
- package/dist/terminal-session.js +22 -20
- package/dist/tokens.js +4 -4
- package/dist/tool-audit.d.ts +114 -0
- package/dist/tool-audit.js +95 -0
- package/dist/tool-executor.d.ts +22 -7
- package/dist/tool-executor.js +16 -16
- package/dist/tool-result-media.d.ts +3 -1
- package/dist/tool-result-media.js +1 -1
- package/dist/tool-result-surfaces.d.ts +3 -1
- package/dist/tool-result-surfaces.js +1 -1
- package/package.json +127 -9
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import { type spawn as nodeSpawn, type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { type Principal } from '@jini-ai/core';
|
|
3
|
+
import { type RunAgentPayload, type RunErrorPayload } from '@jini-ai/protocol';
|
|
4
|
+
import { type applyAgentLaunchEnv, type ensureAgentCapabilities, type getAgentDef, type resolveAgentLaunch, type attachAcpSession, type attachPiRpcSession, type prepareAgentLogFile, type preparePromptFileForAgent, type AcpMcpServerInput, type AcpPermissionHandler, type PromptAugmenter } from '@jini-ai/agent-runtime';
|
|
5
|
+
import { type createCommandInvocation, type listProcessSnapshots, type ProcessSnapshot, type StopProcessesResult } from '@jini-ai/platform';
|
|
6
|
+
import { type RunByteJournal } from '../continuation/journal.js';
|
|
7
|
+
import { type RunRetrySideEffectState } from '../run/core/index.js';
|
|
8
|
+
import { type ToolExecutor } from '../tool-executor.js';
|
|
9
|
+
import { type RunLifecycle } from '../run-lifecycle.js';
|
|
10
|
+
/**
|
|
11
|
+
* A parsed, loosely-typed event as produced by one of `@jini-ai/agent-runtime`'s
|
|
12
|
+
* four stream-parser factories — each parser's `onEvent` callback receives
|
|
13
|
+
* `Record<string, unknown>` with a `type` discriminant, not a typed union.
|
|
14
|
+
*/
|
|
15
|
+
export type StreamHandler = {
|
|
16
|
+
feed(args: {
|
|
17
|
+
chunk: string;
|
|
18
|
+
}): void;
|
|
19
|
+
flush(): void;
|
|
20
|
+
};
|
|
21
|
+
export declare const SUPPORTED_STREAM_FORMATS: readonly ["claude-stream-json", "json-event-stream", "copilot-stream-json", "qoder-stream-json", "acp-json-rpc", "pi-rpc", "plain"];
|
|
22
|
+
/** The families of the registry's stream-format this driver implements — see index.ts module doc. */
|
|
23
|
+
export type SupportedStreamFormat = (typeof SUPPORTED_STREAM_FORMATS)[number];
|
|
24
|
+
export type JsonStreamFormat = Exclude<SupportedStreamFormat, 'acp-json-rpc' | 'pi-rpc' | 'plain'>;
|
|
25
|
+
/**
|
|
26
|
+
* `streamFormat` values `wireChildLifecycle` drives directly off raw
|
|
27
|
+
* `child.stdout` `'data'` events: the 4 JSON-stream-parser formats (fed
|
|
28
|
+
* through a real `feed()`/`flush()` state machine via
|
|
29
|
+
* {@link createStreamHandlerForDef}) plus `'plain'` (no parser at all —
|
|
30
|
+
* each chunk is forwarded verbatim as a `text_delta`, see that function's
|
|
31
|
+
* doc). Distinct from `'acp-json-rpc'`/`'pi-rpc'`, which own their own
|
|
32
|
+
* JSON-RPC prompt/event protocol and get their own
|
|
33
|
+
* `wireAcpLifecycle`/`wirePiRpcLifecycle` wiring instead.
|
|
34
|
+
*/
|
|
35
|
+
export type ChildDrivenStreamFormat = JsonStreamFormat | 'plain';
|
|
36
|
+
/**
|
|
37
|
+
* Whether `run()` can drive a given def, and — when it can — its `streamFormat` already narrowed for
|
|
38
|
+
* the dispatch logic that follows. The narrowing rides along deliberately: it is what lets `run()`
|
|
39
|
+
* delegate every compatibility guard here without then re-checking the format to satisfy the type
|
|
40
|
+
* system, which would leave an unreachable branch behind.
|
|
41
|
+
*/
|
|
42
|
+
export type AgentExecutorCompatibility = {
|
|
43
|
+
readonly supported: true;
|
|
44
|
+
readonly streamFormat: SupportedStreamFormat;
|
|
45
|
+
} | {
|
|
46
|
+
readonly supported: false;
|
|
47
|
+
readonly reason: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Result of translating one parsed stream event into this engine's
|
|
51
|
+
* vocabulary. `'agent'` is the common case (forward as a `RunAgentPayload`
|
|
52
|
+
* via the `'agent'` run event); `'error'` and `'turn-end'` are the two
|
|
53
|
+
* type values `run()` handles specially rather than passing through (see
|
|
54
|
+
* index.ts module doc); `'ignored'` covers anything the 4 parsers never actually
|
|
55
|
+
* produce plus defensively malformed/non-record input.
|
|
56
|
+
*
|
|
57
|
+
* `'agent'`'s optional `sessionId` (gap 5, session resume — see
|
|
58
|
+
* `RunEndPayload.sessionRef`'s doc in `@jini-ai/protocol`) is a daemon-internal
|
|
59
|
+
* side channel, not part of the `RunAgentPayload` wire payload itself:
|
|
60
|
+
* OpenCode's `sessionID`/Codex's `thread_id`/Qoder's and Claude's
|
|
61
|
+
* `session_id` all arrive on a `'status'` event alongside fields
|
|
62
|
+
* `RunAgentPayload`'s `'status'` variant already models (`label`/`model`/
|
|
63
|
+
* `ttftMs`/`detail`) but has no room for a session id itself — surfacing it
|
|
64
|
+
* here lets a lifecycle-wiring function capture it into a local variable and
|
|
65
|
+
* thread it into its own terminal `finish()` call, without widening the
|
|
66
|
+
* public wire protocol just to carry a value that only this module reads.
|
|
67
|
+
*
|
|
68
|
+
* `'turn-end'`'s optional `stopReason` (gap 3, capability-routed
|
|
69
|
+
* continuation transport) is the same kind of internal side channel: the
|
|
70
|
+
* claude-stream parser deliberately emits `stopReason` *after* every
|
|
71
|
+
* `tool_use` block in the same assistant message has already been
|
|
72
|
+
* translated (so a caller can decide whether to keep stdin open before
|
|
73
|
+
* closing it), but v1 (pre-gap-3) discarded it and closed stdin
|
|
74
|
+
* unconditionally on any `turn-end` — see `packages/daemon/archived provenance ledger`'s
|
|
75
|
+
* "Design decision 2" note. `wireChildLifecycle` now reads it to decide
|
|
76
|
+
* whether `stop_reason: 'tool_use'` means "inject a tool result and keep
|
|
77
|
+
* going" (gap 3, gated — see `ContinuationOptions`) or "close stdin as
|
|
78
|
+
* before" (the unconditional default when no continuation is configured).
|
|
79
|
+
*/
|
|
80
|
+
export type AgentRuntimeEventTranslation = {
|
|
81
|
+
readonly kind: 'agent';
|
|
82
|
+
readonly payload: RunAgentPayload;
|
|
83
|
+
readonly sessionId?: string;
|
|
84
|
+
} | {
|
|
85
|
+
readonly kind: 'error';
|
|
86
|
+
readonly payload: RunErrorPayload;
|
|
87
|
+
} | {
|
|
88
|
+
readonly kind: 'turn-end';
|
|
89
|
+
readonly stopReason?: string;
|
|
90
|
+
} | {
|
|
91
|
+
readonly kind: 'ignored';
|
|
92
|
+
};
|
|
93
|
+
/** Machine-readable failure reasons `run()` can reject with — every one is preceded by a `lifecycle.finish({status:'failed'})` call (see index.ts module doc's Invariant section). */
|
|
94
|
+
export type AgentExecutorErrorCode = 'AGENT_PERMISSION_MODE_INVALID' | 'AGENT_NOT_FOUND' | 'AGENT_RUNTIME_UNSUPPORTED' | 'AGENT_BINARY_NOT_RESOLVED' | 'AGENT_SPAWN_FAILED' | 'AGENT_PROMPT_TOO_LARGE';
|
|
95
|
+
/** Thrown by `AgentExecutor.run()` on every failure path — never a bare `Error`, so callers can branch on `.code` instead of parsing `.message`. */
|
|
96
|
+
export declare class AgentExecutorError extends Error {
|
|
97
|
+
readonly code: AgentExecutorErrorCode;
|
|
98
|
+
constructor({ code, message }: {
|
|
99
|
+
readonly code: AgentExecutorErrorCode;
|
|
100
|
+
readonly message: string;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export interface AgentExecutorRunInput {
|
|
104
|
+
readonly runId: string;
|
|
105
|
+
readonly agentId: string;
|
|
106
|
+
readonly prompt: string;
|
|
107
|
+
readonly cwd: string;
|
|
108
|
+
/** Optional host-selected model id, forwarded to every runtime transport. */
|
|
109
|
+
readonly model?: string;
|
|
110
|
+
/** Optional host-selected reasoning effort, forwarded to runtime argv builders. */
|
|
111
|
+
readonly reasoning?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Forwarded verbatim to `RuntimeBuildOptions.permissionMode` (see `@jini-ai/agent-runtime`'s
|
|
114
|
+
* `types.ts`). Every def that has an auto-approve flag (`bypassPermissions` / `--yolo` /
|
|
115
|
+
* `--dangerously-skip-permissions`) historically enabled it when omitted because a spawned
|
|
116
|
+
* subprocess has no TTY for interactive approval. That rationale explains the old behavior;
|
|
117
|
+
* the daemon now fails closed with 'restricted'. A host must explicitly select 'bypass' to
|
|
118
|
+
* delegate auto-approval, and any unknown mode is rejected before launching a process.
|
|
119
|
+
*/
|
|
120
|
+
readonly permissionMode?: 'bypass' | 'restricted';
|
|
121
|
+
/**
|
|
122
|
+
* Host-validated image files. How each one actually reaches the model
|
|
123
|
+
* depends on `def.imageDelivery` (see `@jini-ai/agent-runtime`'s `types.ts`):
|
|
124
|
+
* a `'native'` def forwards these through its own protocol (argv, ACP
|
|
125
|
+
* `resource_link` blocks, or pi-rpc's base64 `images` field); a
|
|
126
|
+
* `'prompt-path'` def (currently just `claude`) instead gets them named in
|
|
127
|
+
* the prompt text and its allowed directories widened, via
|
|
128
|
+
* `image-prompt-delivery.ts#applyImagePromptDelivery`, called once near
|
|
129
|
+
* the top of `run()` in `launch.ts`.
|
|
130
|
+
*/
|
|
131
|
+
readonly imagePaths?: readonly string[];
|
|
132
|
+
/** Additional host-validated directories the runtime may read. */
|
|
133
|
+
readonly extraAllowedDirs?: readonly string[];
|
|
134
|
+
/** Trusted root that must contain pi-rpc image paths after realpath resolution. */
|
|
135
|
+
readonly uploadRoot?: string;
|
|
136
|
+
/**
|
|
137
|
+
* Credential(s) this run's selected agent/provider needs (e.g. `{ ANTHROPIC_API_KEY: '...' }`),
|
|
138
|
+
* delegated explicitly by the host and merged into the baseline-allowlisted env below. Never
|
|
139
|
+
* read implicitly from `process.env` — see SEC-001.
|
|
140
|
+
*/
|
|
141
|
+
readonly credentialEnv?: Record<string, string>;
|
|
142
|
+
/**
|
|
143
|
+
* Explicit escape hatch: when supplied, used verbatim as the spawned subprocess's entire
|
|
144
|
+
* environment (no allowlist filtering) — for tests and hosts that have already done their own
|
|
145
|
+
* scoping. When omitted (the default), the subprocess gets only `BASELINE_AGENT_ENV_KEYS` from
|
|
146
|
+
* the host's real env plus `credentialEnv`, never a full `process.env` passthrough. A spawned
|
|
147
|
+
* coding-agent CLI is prompt-influenced and must be treated as potentially adversarial; it must
|
|
148
|
+
* not inherit secrets the daemon process happens to hold for unrelated reasons. See SEC-001
|
|
149
|
+
* (`ADS-memory/reports/proposals/PROP-agent-subprocess-env-allowlist-2026-07-21.md`) and locked
|
|
150
|
+
* architecture decision C8 (`ADS-memory/reports/jini-port/extraction-plan.md`).
|
|
151
|
+
*/
|
|
152
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
153
|
+
/**
|
|
154
|
+
* Stored session id for this (conversation, agent) pair from a prior run's
|
|
155
|
+
* `RunEndPayload.sessionRef` (see `@jini-ai/protocol`'s doc on that field) — set when the host
|
|
156
|
+
* wants this run to continue that CLI session instead of starting cold. Forwarded verbatim into
|
|
157
|
+
* `RuntimeContext.resumeSessionId`; a `resumesSessionViaCli` def (see
|
|
158
|
+
* `@jini-ai/agent-runtime`'s `types.ts`) reads it to pass its CLI's own resume flag, in which case
|
|
159
|
+
* the host should send only the latest user turn as `prompt`, not the full transcript. `null` and
|
|
160
|
+
* omitted are equivalent: no resume target for this run.
|
|
161
|
+
*/
|
|
162
|
+
readonly resumeSessionId?: string | null;
|
|
163
|
+
/**
|
|
164
|
+
* A fresh id the host mints and persists when starting a session it wants resumable on a later
|
|
165
|
+
* turn (i.e. no `resumeSessionId` is available yet). Forwarded verbatim into
|
|
166
|
+
* `RuntimeContext.newSessionId`; a `resumesSessionViaCli` def passes it to its CLI's own
|
|
167
|
+
* "start with this id" flag so a later turn's `resumeSessionId` can continue the same underlying
|
|
168
|
+
* CLI session. Ignored by defs that don't declare `resumesSessionViaCli`.
|
|
169
|
+
*/
|
|
170
|
+
readonly newSessionId?: string;
|
|
171
|
+
/**
|
|
172
|
+
* Forwarded verbatim to `RuntimeBuildOptions.disallowedTools` (see `@jini-ai/agent-runtime`'s
|
|
173
|
+
* `types.ts`) — Finding 2 of SEC-assistant-env-isolation-2026-09-07. A mechanism only: this
|
|
174
|
+
* package bakes in no opinion about which tools to name; the host decides. `undefined`/empty
|
|
175
|
+
* means no restriction, byte-identical to today's behavior.
|
|
176
|
+
*/
|
|
177
|
+
readonly disallowedTools?: readonly string[];
|
|
178
|
+
/** Same mechanism as {@link disallowedTools}, forwarded to `RuntimeBuildOptions.allowedTools`. */
|
|
179
|
+
readonly allowedTools?: readonly string[];
|
|
180
|
+
/** Forwarded verbatim to `RuntimeBuildOptions.settingSources` (an empty list = load none of the CLI's settings layers). */
|
|
181
|
+
readonly settingSources?: readonly string[];
|
|
182
|
+
/** Forwarded verbatim to `RuntimeBuildOptions.settings` (extra settings JSON or file path for this run). */
|
|
183
|
+
readonly settings?: string;
|
|
184
|
+
}
|
|
185
|
+
export interface AgentExecutor {
|
|
186
|
+
/**
|
|
187
|
+
* Spawns `agentId`'s CLI for `runId`, wires its stdout/stderr into
|
|
188
|
+
* `lifecycle.emit()`, and resolves once the child process is confirmed
|
|
189
|
+
* spawned (fire-and-forget from there — see index.ts module doc). Callers await
|
|
190
|
+
* `lifecycle.waitForTerminal(runId)` separately for completion.
|
|
191
|
+
* @throws {@link AgentExecutorError} on every failure path — the
|
|
192
|
+
* underlying run is always already transitioned to `'failed'` via
|
|
193
|
+
* `lifecycle.finish()` before this rejects (see index.ts module doc's Invariant).
|
|
194
|
+
*/
|
|
195
|
+
run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "permissionMode" | "imagePaths" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings">): Promise<void>;
|
|
196
|
+
}
|
|
197
|
+
/** Process discovery is a getter; process collection and stopping accept explicit host inputs. */
|
|
198
|
+
export type CollectProcessTreePidsPort = (args: {
|
|
199
|
+
readonly processes: ProcessSnapshot[];
|
|
200
|
+
readonly rootPids: Array<number | null | undefined>;
|
|
201
|
+
}) => number[];
|
|
202
|
+
export type StopProcessesPort = (args: {
|
|
203
|
+
readonly pids: Array<number | null | undefined>;
|
|
204
|
+
}) => Promise<StopProcessesResult>;
|
|
205
|
+
export interface TerminateChildTreeDeps {
|
|
206
|
+
readonly listProcessSnapshots: () => Promise<ProcessSnapshot[]>;
|
|
207
|
+
readonly collectProcessTreePids: CollectProcessTreePidsPort;
|
|
208
|
+
readonly stopProcesses: StopProcessesPort;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Which step reported a contained failure through `onCleanupFailure` (SEC-007).
|
|
212
|
+
*
|
|
213
|
+
* The first three come from {@link terminateChildTreeBestEffort} (process-tree teardown). The last
|
|
214
|
+
* two are the two fallible steps that sit between a child's `'close'` and `finish()` — staged-file
|
|
215
|
+
* removal and the host's own `classifyFailure` — neither of which may prevent the terminal
|
|
216
|
+
* transition, and neither of which may fail silently either. See each close handler.
|
|
217
|
+
*/
|
|
218
|
+
export type AgentCleanupFailurePhase = 'cancel' | 'acp-attach-failure' | 'pi-rpc-attach-failure' | 'staged-file-cleanup' | 'failure-classification';
|
|
219
|
+
export interface AgentCleanupFailureContext {
|
|
220
|
+
readonly runId: string;
|
|
221
|
+
readonly phase: AgentCleanupFailurePhase;
|
|
222
|
+
/**
|
|
223
|
+
* The child's pid. `undefined` only for the post-close phases on a child that never had one
|
|
224
|
+
* assigned (a spawn that produced no process): those phases are about this run's own bookkeeping
|
|
225
|
+
* rather than about signalling a process, so an absent pid is reportable rather than a
|
|
226
|
+
* contradiction.
|
|
227
|
+
*/
|
|
228
|
+
readonly pid: number | undefined;
|
|
229
|
+
readonly error: unknown;
|
|
230
|
+
}
|
|
231
|
+
/** A small handle `writePromptToStdin` uses to close stdin exactly once, shared with the `turn_end`-triggered close inside {@link wireChildLifecycle}. */
|
|
232
|
+
export interface StdinCloseHandle {
|
|
233
|
+
closeStdinOnce(): void;
|
|
234
|
+
/** Journals a sent-to-stdin byte chunk, queued through the same FIFO {@link wireChildLifecycle} already uses for emitted events. No-op when no journal was configured (see `CreateAgentExecutorOptions.journal`). */
|
|
235
|
+
recordSentBytes(content: string): void;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Gap 3 (capability-routed continuation transport) — host-owned config for the
|
|
239
|
+
* `'stdin-injection'` transport (claude/codebuddy only; see
|
|
240
|
+
* `resolveContinuationTransport`'s doc). **Absent by default, and absent means
|
|
241
|
+
* zero behavior change**: with no `ContinuationOptions`, every `turn_end`
|
|
242
|
+
* closes stdin exactly as it always has (v1 behavior, unconditionally).
|
|
243
|
+
*
|
|
244
|
+
* `autonomousToolNames` is this task's answer to the debate's Unresolved
|
|
245
|
+
* Delta (how does the loop distinguish "the agent is continuing
|
|
246
|
+
* autonomously" from "the agent is waiting on a human"): rather than
|
|
247
|
+
* inferring intent from the stream, the *host* pre-declares which tool
|
|
248
|
+
* names are safe to auto-resolve and re-inject without a human in the loop.
|
|
249
|
+
* A `tool_use` whose name is not in this set is left exactly as it was
|
|
250
|
+
* before gap 3 — stdin closes, the run proceeds to its normal terminal
|
|
251
|
+
* state — even though a `stopReason: 'tool_use'` was observed. This sidesteps
|
|
252
|
+
* building unproven intent-detection: nothing auto-continues unless a host
|
|
253
|
+
* has explicitly vetted that specific tool as autonomous-safe. A
|
|
254
|
+
* human-facing "ask the user a question" tool is simply never added to this
|
|
255
|
+
* set; `packages/chat-core/src/question-form.ts`'s existing text-tag
|
|
256
|
+
* mechanism (a new `Run` per turn, not mid-turn injection) already covers
|
|
257
|
+
* that case without needing this transport at all.
|
|
258
|
+
*/
|
|
259
|
+
export interface ContinuationOptions {
|
|
260
|
+
/** Injected tool results are authorized through this — the same deny-by-default gate every other tool execution path in this codebase uses. No parallel authorization path. */
|
|
261
|
+
readonly toolExecutor: ToolExecutor;
|
|
262
|
+
/** The principal an injected tool call is authorized as. */
|
|
263
|
+
readonly principal: Principal;
|
|
264
|
+
/** Tool names this host has pre-classified as safe to auto-resolve without human involvement. See this interface's own doc for why this — not stream-inferred intent — is gap 3's answer to the human-in-the-loop pause question. */
|
|
265
|
+
readonly autonomousToolNames: ReadonlySet<string>;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Gap 3, part 2 (MCP-callback continuation transport — the spawn-time half the spike's own
|
|
269
|
+
* commit message named as undone: "Item 4 ... NOT done yet"). `resolveContinuationTransport`
|
|
270
|
+
* already resolves `'mcp-callback'` for every def with `externalMcpInjection !== undefined`, but
|
|
271
|
+
* the executor did not originally *act* on that resolution — `execute_delegated_tool`
|
|
272
|
+
* (`@jini-ai/mcp`'s `../server/tools/delegated-tool.ts`) only does anything useful once the spawned
|
|
273
|
+
* CLI's own client actually launches `jini-mcp` as its MCP server subprocess.
|
|
274
|
+
*
|
|
275
|
+
* **All six declared strategies are wired.** These options describe *one* bridge server
|
|
276
|
+
* (`command`/`args`/`daemonUrl`/`credential`); which transport carries it to a given child is that
|
|
277
|
+
* def's own `externalMcpInjection` declaration, and each of the six has exactly one
|
|
278
|
+
* implementation in `mcp-bridge.ts` — see {@link buildMcpBridgeDelivery}, which is the single dispatch point.
|
|
279
|
+
* The interface name predates the other five mechanisms and is kept for API compatibility with
|
|
280
|
+
* `@jini-ai/server`'s `agentExecutor` passthrough; it is no longer `.mcp.json`-specific.
|
|
281
|
+
*
|
|
282
|
+
* **Host-resolved, not this package's to know.** `command`/`daemonUrl` have no default the way
|
|
283
|
+
* `journal`/`continuation`/`classifyFailure` don't either — there is no "real" install layout or
|
|
284
|
+
* loopback URL this package could assume on a caller's behalf (matching every other seam on this
|
|
285
|
+
* interface that defaults to *nothing* rather than a real implementation).
|
|
286
|
+
*/
|
|
287
|
+
export interface McpJsonInjectionOptions {
|
|
288
|
+
/** Absolute path (or PATH-resolvable name) to the `jini-mcp` bin entry (`packages/mcp/src/bin/serve.ts`) this driver tells the spawned CLI to launch as its own MCP server subprocess. */
|
|
289
|
+
readonly command: string;
|
|
290
|
+
/** Extra argv for `command`. @default [] */
|
|
291
|
+
readonly args?: readonly string[];
|
|
292
|
+
/**
|
|
293
|
+
* Extra env for the bridge child, for hosts whose `command` is not a plain Node binary. An Electron
|
|
294
|
+
* host passes `{ ELECTRON_RUN_AS_NODE: '1' }`: its `process.execPath` is the app itself, which boots
|
|
295
|
+
* as a GUI app without that flag and never speaks MCP. `buildAgentEnv`'s allowlist strips the
|
|
296
|
+
* host's own copy from the CLI, so it can only reach the bridge through here. `JINI_*` keys are
|
|
297
|
+
* ignored: the run id, callback URL and token are this driver's to set. @default {}
|
|
298
|
+
*/
|
|
299
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
300
|
+
/** The daemon's own loopback base URL the spawned `jini-mcp` process calls back into via `JINI_DAEMON_URL` (see `packages/mcp/src/bin/serve.ts`'s `DAEMON_URL_ENV_VAR`). */
|
|
301
|
+
readonly daemonUrl: string;
|
|
302
|
+
/**
|
|
303
|
+
* Mints the bearer credential this run's `jini-mcp` child presents on its callbacks, delivered to
|
|
304
|
+
* the child through `JINI_DAEMON_TOKEN`. Omit it and the child is spawned exactly as before, with
|
|
305
|
+
* no token env var at all — so this is additive for every existing host.
|
|
306
|
+
*
|
|
307
|
+
* **A resolver, not a string, and deliberately so.** A host's `McpJsonInjectionOptions` is built
|
|
308
|
+
* once when it composes its executor — before any run exists. A plain string field could therefore
|
|
309
|
+
* only ever carry one boot-wide secret shared by every run, which defeats the point: the reason to
|
|
310
|
+
* hand the child a credential at all is that it can be scoped to the one run it was spawned for and
|
|
311
|
+
* stop working when that run ends. Taking `runId` here is what makes a per-run credential
|
|
312
|
+
* expressible.
|
|
313
|
+
*
|
|
314
|
+
* May be async so a host can mint through a keystore or signing service. Resolution happens in
|
|
315
|
+
* `writeMcpJsonForRun`, which is already async and already effectful; `buildMcpJsonServerEntry`
|
|
316
|
+
* stays pure and synchronous and receives the resolved value.
|
|
317
|
+
*
|
|
318
|
+
* Never hand this the host's own inbound API token. The child is the least-trusted participant in
|
|
319
|
+
* the run — it is reachable by whatever the spawned CLI does — so its credential should authorize
|
|
320
|
+
* its own callback route and nothing else.
|
|
321
|
+
*
|
|
322
|
+
* @throws Anything the host's own minting throws. `run()` turns a rejection into a pre-spawn
|
|
323
|
+
* `AGENT_SPAWN_FAILED` failure rather than spawning a child that cannot authenticate.
|
|
324
|
+
*/
|
|
325
|
+
readonly credential?: ({ runId }: {
|
|
326
|
+
readonly runId: string;
|
|
327
|
+
}) => string | Promise<string>;
|
|
328
|
+
/** Reads the project's own `cwd/.mcp.json` so this driver merges its servers in rather than dropping them. Rejecting (ENOENT or otherwise) is treated as "no existing file" — see `writeMcpJsonForRun`. This file is only ever *read*. @default the real `fs.promises.readFile` (utf8) */
|
|
329
|
+
readonly readFile?: ({ path }: {
|
|
330
|
+
readonly path: string;
|
|
331
|
+
}) => Promise<string>;
|
|
332
|
+
/** Writes the merged content to this run's own config path (see {@link mcpJsonPathForRun}), never to the project's `.mcp.json`. @default the real `fs.promises.writeFile` (utf8) */
|
|
333
|
+
readonly writeFile?: ({ path, content }: {
|
|
334
|
+
readonly path: string;
|
|
335
|
+
readonly content: string;
|
|
336
|
+
}) => Promise<void>;
|
|
337
|
+
/**
|
|
338
|
+
* Removes this run's config file once the run is over — it holds a live per-run bearer token, so
|
|
339
|
+
* leaving it behind is the same class of confidentiality gap as a leaked prompt file (see
|
|
340
|
+
* `WireChildLifecycleContext.cleanupStagedFiles`). Called on the close handler and on every
|
|
341
|
+
* pre-spawn/spawn-failure path, and a rejection is reported rather than allowed to strand the run.
|
|
342
|
+
* @default `fs.promises.rm(path, { force: true })` — already-gone is success, not an error.
|
|
343
|
+
*/
|
|
344
|
+
readonly removeFile?: ({ path }: {
|
|
345
|
+
readonly path: string;
|
|
346
|
+
}) => Promise<void>;
|
|
347
|
+
/**
|
|
348
|
+
* `'codex-toml'` only. Creates a fresh, randomly-named directory `prepareCodexHomeForRun` stages
|
|
349
|
+
* as a run's scratch `CODEX_HOME`. **Must be non-deterministic (a real `mkdtemp`, not a
|
|
350
|
+
* caller-computed path)** — unlike `mcpJsonPathForRun`'s deterministic path inside the run's own
|
|
351
|
+
* `cwd`, this directory holds a copy of the operator's real Codex login credential, and
|
|
352
|
+
* `os.tmpdir()` is a shared location on a multi-user host: a guessable name there is a real
|
|
353
|
+
* pre-plant/symlink target for another local user. `fs.mkdtemp`'s random suffix plus its `0700`
|
|
354
|
+
* directory mode is the actual confidentiality control, matching the same reasoning
|
|
355
|
+
* `@jini-ai/agent-runtime`'s `log-file.ts`/`prompt-file.ts` already apply to their own staged temp
|
|
356
|
+
* dirs.
|
|
357
|
+
* @param prefix - A caller-composed, run-id-derived prefix (already sanitized) for the mkdtemp
|
|
358
|
+
* template; the real suffix mkdtemp appends is what makes the path unpredictable.
|
|
359
|
+
* @default `fs.mkdtemp(path.join(os.tmpdir(), prefix))`
|
|
360
|
+
*/
|
|
361
|
+
readonly mkdtemp?: ({ prefix }: {
|
|
362
|
+
readonly prefix: string;
|
|
363
|
+
}) => Promise<string>;
|
|
364
|
+
/**
|
|
365
|
+
* `'codex-toml'` only. Recursively removes the scratch `CODEX_HOME` directory `mkdtemp` above
|
|
366
|
+
* created — the directory-level analogue of `removeFile`, needed because this mechanism stages a
|
|
367
|
+
* whole directory (`config.toml` plus a copied `auth.json`), not one file.
|
|
368
|
+
* @default `fs.rm(path, { recursive: true, force: true })` — already-gone is success, not an error.
|
|
369
|
+
*/
|
|
370
|
+
readonly removeDir?: ({ path }: {
|
|
371
|
+
readonly path: string;
|
|
372
|
+
}) => Promise<void>;
|
|
373
|
+
}
|
|
374
|
+
/** One `.mcp.json` `mcpServers` entry — the shape Claude Code's own config schema expects. */
|
|
375
|
+
export interface McpJsonServerEntry {
|
|
376
|
+
readonly command: string;
|
|
377
|
+
readonly args: string[];
|
|
378
|
+
readonly env: Readonly<Record<string, string>> & {
|
|
379
|
+
readonly JINI_RUN_ID: string;
|
|
380
|
+
readonly JINI_DAEMON_URL: string;
|
|
381
|
+
/** Present only when the host supplied a `credential` resolver — see {@link McpJsonInjectionOptions.credential}. */
|
|
382
|
+
readonly JINI_DAEMON_TOKEN?: string;
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* What one run's MCP bridge turns into, discriminated by the delivery mechanism its def declared.
|
|
387
|
+
* Exactly one variant is produced per run, and each variant carries only what its own consumer
|
|
388
|
+
* needs — so a consumer cannot accidentally read another mechanism's payload.
|
|
389
|
+
*/
|
|
390
|
+
export type McpBridgeDelivery =
|
|
391
|
+
/** `'claude-mcp-json'` (claude, codebuddy): a `.mcp.json` staged into the run cwd, whose path the def's `buildArgs` passes as `--mcp-config`. */
|
|
392
|
+
{
|
|
393
|
+
readonly kind: 'claude-mcp-json';
|
|
394
|
+
readonly mcpJsonPath: string;
|
|
395
|
+
readonly serverEntry: McpJsonServerEntry;
|
|
396
|
+
}
|
|
397
|
+
/** `'acp-merge'` (the 9 ACP-native defs): `mcpServers` entries for the ACP `session/new` params. */
|
|
398
|
+
| {
|
|
399
|
+
readonly kind: 'acp-merge';
|
|
400
|
+
readonly mcpServers: readonly AcpMcpServerInput[];
|
|
401
|
+
}
|
|
402
|
+
/** `'opencode-env-content'` / `'mimo-env-content'` (opencode, mimo): one spawn-env variable carrying the serialised config. */
|
|
403
|
+
| {
|
|
404
|
+
readonly kind: 'env-content';
|
|
405
|
+
readonly envVarName: string;
|
|
406
|
+
readonly serverEntry: McpJsonServerEntry;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* `'codex-toml'` (codex): no path yet — unlike `'claude-mcp-json'`'s `mcpJsonPath`, the scratch
|
|
410
|
+
* `CODEX_HOME` directory is created with `fs.mkdtemp` (a real, non-deterministic filesystem
|
|
411
|
+
* effect — see `McpJsonInjectionOptions.mkdtemp`'s own doc for why), so it cannot be computed by
|
|
412
|
+
* this delivery's pure, synchronous dispatch. `prepareCodexHomeIfNeeded` stages it separately and
|
|
413
|
+
* reports the resulting path back into `childEnv.CODEX_HOME` directly, never through this type.
|
|
414
|
+
*/
|
|
415
|
+
| {
|
|
416
|
+
readonly kind: 'codex-toml';
|
|
417
|
+
readonly serverEntry: McpJsonServerEntry;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* `'env-passthrough'` (antigravity): the bridge entry's `env` triple (`JINI_RUN_ID`/
|
|
421
|
+
* `JINI_DAEMON_URL`/`JINI_DAEMON_TOKEN`) is set directly on the spawned CLI's own OS
|
|
422
|
+
* environment — no config document, no file, no CLI-specific schema. Correct only because this
|
|
423
|
+
* strategy's CLI already inherits its own env down to the stdio MCP child it launches for a
|
|
424
|
+
* server registered once, globally, out of band (see `types.ts`'s own doc on this strategy).
|
|
425
|
+
*/
|
|
426
|
+
| {
|
|
427
|
+
readonly kind: 'env-passthrough';
|
|
428
|
+
readonly serverEntry: McpJsonServerEntry;
|
|
429
|
+
};
|
|
430
|
+
/** A staged, run-scoped Codex `CODEX_HOME` — the directory-holding analogue of {@link PreparedPromptFile}/{@link PreparedAgentLogFile} from `@jini-ai/agent-runtime`. */
|
|
431
|
+
export type PreparedCodexHome = {
|
|
432
|
+
/** Absolute path to hand to the spawned child as its `CODEX_HOME` env var. */
|
|
433
|
+
readonly path: string;
|
|
434
|
+
/** Recursively removes the staged directory — the live `auth.json` copy it may hold makes this a confidentiality cleanup, not just tidiness. Safe to call more than once. */
|
|
435
|
+
readonly cleanup: () => Promise<void>;
|
|
436
|
+
};
|
|
437
|
+
/**
|
|
438
|
+
* Finding 1 of SEC-assistant-env-isolation-2026-09-07 — a staged, run-scoped `claude` CLI config
|
|
439
|
+
* directory, the `CLAUDE_CONFIG_DIR`-isolation analogue of {@link PreparedCodexHome} above.
|
|
440
|
+
*
|
|
441
|
+
* **The bug this closes:** `BASELINE_AGENT_ENV_KEYS` forwards `HOME` verbatim and neither this
|
|
442
|
+
* package nor `@jini-ai/agent-runtime` ever set `CLAUDE_CONFIG_DIR`, so a spawned `claude` child
|
|
443
|
+
* resolved its config (skills, plugins, agents, memory-path index, settings) from the OPERATOR's
|
|
444
|
+
* own real `$HOME/.claude` — a personal grant (including `Task`/`Edit`/`Write`/`Cron*`/worktree
|
|
445
|
+
* tools on this host) the host process never intended to hand the model.
|
|
446
|
+
*/
|
|
447
|
+
export type PreparedClaudeConfigDir = {
|
|
448
|
+
/** Absolute path to hand to the spawned child as its `CLAUDE_CONFIG_DIR` env var. */
|
|
449
|
+
readonly path: string;
|
|
450
|
+
/** Recursively removes the staged directory — it may hold a copied login credential (see {@link prepareClaudeConfigDirForRun}'s doc), the same confidentiality-cleanup duty as {@link PreparedCodexHome.cleanup}. Safe to call more than once. */
|
|
451
|
+
readonly cleanup: () => Promise<void>;
|
|
452
|
+
};
|
|
453
|
+
/** The Claude-config-dir mechanism's injectable filesystem seams, real by default — the directory-staging analogue of {@link CodexHomeSeams}, kept as its own small options bag (see {@link CreateAgentExecutorOptions.claudeConfigDirIsolation}) rather than folded into {@link McpJsonInjectionOptions}: isolating the operator's personal config is an env-hygiene concern independent of whether this host configured MCP federation at all, and must not be gated on that unrelated flag. */
|
|
454
|
+
export interface ClaudeConfigDirSeams {
|
|
455
|
+
readonly mkdtemp: ({ prefix }: {
|
|
456
|
+
readonly prefix: string;
|
|
457
|
+
}) => Promise<string>;
|
|
458
|
+
readonly readFile: ({ path }: {
|
|
459
|
+
readonly path: string;
|
|
460
|
+
}) => Promise<string>;
|
|
461
|
+
readonly writeFile: ({ path, content }: {
|
|
462
|
+
readonly path: string;
|
|
463
|
+
readonly content: string;
|
|
464
|
+
}) => Promise<void>;
|
|
465
|
+
readonly removeDir: ({ path }: {
|
|
466
|
+
readonly path: string;
|
|
467
|
+
}) => Promise<void>;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* `CreateAgentExecutorOptions.claudeConfigDirIsolation` — every field optional and real-filesystem
|
|
471
|
+
* by default, matching the executor's standing "no real disk I/O by default in tests" convention (see
|
|
472
|
+
* `preparePromptFileForAgent`/`prepareAgentLogFile`'s identical shape). Unlike
|
|
473
|
+
* {@link McpJsonInjectionOptions}, this bag is never itself a gate: {@link
|
|
474
|
+
* prepareClaudeConfigDirIfNeeded} stages a scratch directory for every `claude`-def run regardless
|
|
475
|
+
* of whether a host supplies overrides here — this options bag only lets a test observe/replace the
|
|
476
|
+
* filesystem calls, the same way `preparePromptFileForAgent`'s own default does.
|
|
477
|
+
*/
|
|
478
|
+
export interface ClaudeConfigDirIsolationOptions {
|
|
479
|
+
/** @default the real `fs.promises.mkdtemp(path.join(os.tmpdir(), prefix))` */
|
|
480
|
+
readonly mkdtemp?: ({ prefix }: {
|
|
481
|
+
readonly prefix: string;
|
|
482
|
+
}) => Promise<string>;
|
|
483
|
+
/** Reads the operator's real `.credentials.json`, if any — see {@link prepareClaudeConfigDirForRun}'s own doc. @default the real `fs.promises.readFile` (utf8) */
|
|
484
|
+
readonly readFile?: ({ path }: {
|
|
485
|
+
readonly path: string;
|
|
486
|
+
}) => Promise<string>;
|
|
487
|
+
/** Writes the copied `.credentials.json` into the scratch directory. @default the real `fs.promises.writeFile` (utf8) */
|
|
488
|
+
readonly writeFile?: ({ path, content }: {
|
|
489
|
+
readonly path: string;
|
|
490
|
+
readonly content: string;
|
|
491
|
+
}) => Promise<void>;
|
|
492
|
+
/** @default `fs.promises.rm(path, { recursive: true, force: true })` — already-gone is success, not an error. */
|
|
493
|
+
readonly removeDir?: ({ path }: {
|
|
494
|
+
readonly path: string;
|
|
495
|
+
}) => Promise<void>;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Gap 4 of the run/chat orchestration Final Recommendation: what
|
|
499
|
+
* `classifyFailure` (see `CreateAgentExecutorOptions.classifyFailure`) is
|
|
500
|
+
* given to decide whether a `'failed'` run is `resumable`. `code`/`signal`
|
|
501
|
+
* are the only *content-level* signals cheaply available at every one of the
|
|
502
|
+
* three lifecycle-wiring close handlers without new stderr/stdout buffering
|
|
503
|
+
* machinery (a host wanting output-pattern-based classification, the way
|
|
504
|
+
* OD's own ~20-vendor text-matching classifier worked, would need its own
|
|
505
|
+
* listener for that — an honest scope limit, not an oversight).
|
|
506
|
+
*
|
|
507
|
+
* `sideEffects` (2026-07-22) carries the two `RunRetrySideEffectState` fields
|
|
508
|
+
* every `wire*Lifecycle` driver already tracks live from the translated
|
|
509
|
+
* agent-event stream it's processing anyway — `userVisibleOutputSeen` (a
|
|
510
|
+
* non-empty `text_delta`/`thinking_delta`) and `toolCallSeen` (a `tool_use`)
|
|
511
|
+
* — so `decideSafeRunRetry`'s matching suppression guards are genuinely
|
|
512
|
+
* exercised, not permanently dead code. Two related fields are deliberately
|
|
513
|
+
* absent: `cancelRequested` is never included because it's structurally
|
|
514
|
+
* always `false` by the time a classifier runs at all (a cancelled run's
|
|
515
|
+
* status already routes to `'cancelled'` before `classifyFailure` is ever
|
|
516
|
+
* consulted — see each `wire*Lifecycle` close handler); `artifactWriteSeen`/
|
|
517
|
+
* `liveArtifactSeen` have no real signal to derive them from at all —
|
|
518
|
+
* `@jini-ai/protocol`'s `RunAgentEventPayload` union (`events.ts`) has no
|
|
519
|
+
* `'artifact'`/`'live_artifact'` event kind yet (Jini's own generalized
|
|
520
|
+
* GenUI/artifact surface isn't built — see this repo's "A2UI full protocol
|
|
521
|
+
* deferred" scope note), unlike OD, which `RunRetrySideEffectState`'s shape
|
|
522
|
+
* was carried over from.
|
|
523
|
+
*/
|
|
524
|
+
export interface FailureClassificationContext {
|
|
525
|
+
readonly runId: string;
|
|
526
|
+
readonly agentId: string;
|
|
527
|
+
readonly code: number | null;
|
|
528
|
+
readonly signal: string | null;
|
|
529
|
+
readonly sideEffects: Pick<RunRetrySideEffectState, 'userVisibleOutputSeen' | 'toolCallSeen'>;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Host-owned failure classifier — gap 4. Decides, for one specific
|
|
533
|
+
* `'failed'` run, whether `RunLifecycle.finish()`'s `resumable` flag should
|
|
534
|
+
* be `true`. Never consulted for `'succeeded'`/`'cancelled'` outcomes, and
|
|
535
|
+
* never consulted for a pre-spawn failure (`failBeforeSpawn`'s call sites) —
|
|
536
|
+
* those represent failures where no child process ever ran, so there is
|
|
537
|
+
* nothing a classifier could meaningfully examine.
|
|
538
|
+
*/
|
|
539
|
+
export type ClassifyFailure = (context: FailureClassificationContext) => boolean | Promise<boolean>;
|
|
540
|
+
export interface CreateAgentExecutorOptions {
|
|
541
|
+
readonly lifecycle: RunLifecycle;
|
|
542
|
+
/** @default the real `@jini-ai/agent-runtime` registry lookup */
|
|
543
|
+
readonly getAgentDef?: typeof getAgentDef;
|
|
544
|
+
/** @default the real `@jini-ai/agent-runtime` launch resolver */
|
|
545
|
+
readonly resolveAgentLaunch?: typeof resolveAgentLaunch;
|
|
546
|
+
/** @default the real `@jini-ai/agent-runtime` once-per-binary `--help` capability probe */
|
|
547
|
+
readonly ensureAgentCapabilities?: typeof ensureAgentCapabilities;
|
|
548
|
+
/** @default the real `@jini-ai/agent-runtime` PATH-env composer */
|
|
549
|
+
readonly applyAgentLaunchEnv?: typeof applyAgentLaunchEnv;
|
|
550
|
+
/** @default the real `@jini-ai/platform` cross-platform invocation builder */
|
|
551
|
+
readonly createCommandInvocation?: typeof createCommandInvocation;
|
|
552
|
+
/** @default `node:child_process`'s `spawn` */
|
|
553
|
+
readonly spawn?: typeof nodeSpawn;
|
|
554
|
+
/** @default the real `@jini-ai/agent-runtime` ACP session transport */
|
|
555
|
+
readonly attachAcpSession?: typeof attachAcpSession;
|
|
556
|
+
/**
|
|
557
|
+
* Host-owned policy for ACP agents' native tool calls. The ACP agent still
|
|
558
|
+
* executes its own selected option; Jini-registered tool execution belongs
|
|
559
|
+
* to `createDelegatedToolBridge`, not this permission callback.
|
|
560
|
+
*/
|
|
561
|
+
readonly acpPermissionHandler?: AcpPermissionHandler;
|
|
562
|
+
/** @default the real `@jini-ai/agent-runtime` pi-rpc session transport */
|
|
563
|
+
readonly attachPiRpcSession?: typeof attachPiRpcSession;
|
|
564
|
+
/**
|
|
565
|
+
* Stages a `promptViaFile` def's (grok-build) composed prompt to a temp
|
|
566
|
+
* file before `buildArgs` runs. Touches the real filesystem by default
|
|
567
|
+
* (`fs.mkdtemp`/`fs.writeFile`/`fs.rm`) — injectable so tests can drive
|
|
568
|
+
* it without real disk I/O, matching this factory's "no real subprocess,
|
|
569
|
+
* filesystem, or PATH lookup by default in tests" convention.
|
|
570
|
+
* @default the real `@jini-ai/agent-runtime` prompt-file stager
|
|
571
|
+
*/
|
|
572
|
+
readonly preparePromptFileForAgent?: typeof preparePromptFileForAgent;
|
|
573
|
+
/**
|
|
574
|
+
* Stages a `needsAgentLogFile` def's (antigravity) diagnostic-log path
|
|
575
|
+
* before `buildArgs` runs. Same real-filesystem/injectable-for-tests deal
|
|
576
|
+
* as `preparePromptFileForAgent` above, and a no-op for every def that
|
|
577
|
+
* did not opt in.
|
|
578
|
+
* @default the real `@jini-ai/agent-runtime` log-file stager
|
|
579
|
+
*/
|
|
580
|
+
readonly prepareAgentLogFile?: typeof prepareAgentLogFile;
|
|
581
|
+
/** @default the real `@jini-ai/platform` process-snapshot enumerator */
|
|
582
|
+
readonly listProcessSnapshots?: typeof listProcessSnapshots;
|
|
583
|
+
/** @default the real `@jini-ai/platform` descendant-PID collector */
|
|
584
|
+
readonly collectProcessTreePids?: CollectProcessTreePidsPort;
|
|
585
|
+
/** @default the real `@jini-ai/platform` SIGTERM→SIGKILL escalator */
|
|
586
|
+
readonly stopProcesses?: StopProcessesPort;
|
|
587
|
+
/** Host-owned sink for a process-tree cleanup failure (SEC-007) — e.g. EPERM stopping descendants. @default logs a redacted diagnostic via `console.error` */
|
|
588
|
+
readonly onCleanupFailure?: (context: AgentCleanupFailureContext) => void;
|
|
589
|
+
/**
|
|
590
|
+
* Gap 1's byte-journal (`packages/daemon/src/continuation/journal.ts`) — records every byte
|
|
591
|
+
* this driver sends to or receives from a child agent process, independent of and prior to any
|
|
592
|
+
* parsed/translated event. Covers `writePromptToStdin` (sent) and every `child.stdout`/
|
|
593
|
+
* `child.stderr` `'data'` handler this driver owns (received); does not cover ACP/pi-rpc's own
|
|
594
|
+
* prompt delivery, which happens inside their respective attach functions' own transport, out
|
|
595
|
+
* of this driver's direct view — see `WireAcpLifecycleContext.journal`'s doc.
|
|
596
|
+
* @default no journal — recording is entirely opt-in, unlike every other seam on this
|
|
597
|
+
* interface (which default to a real implementation): there is no generic "real" journal
|
|
598
|
+
* storage this package can default to without a caller-supplied `EventLog` instance.
|
|
599
|
+
*/
|
|
600
|
+
readonly journal?: RunByteJournal;
|
|
601
|
+
/**
|
|
602
|
+
* Gap 3's stdin-tool-result injection config — see `ContinuationOptions`'s own doc, especially
|
|
603
|
+
* on why `autonomousToolNames` (not stream-inferred intent) is this task's answer to the
|
|
604
|
+
* human-in-the-loop pause question.
|
|
605
|
+
* @default undefined — every `turn_end` closes stdin unconditionally, byte-identical to
|
|
606
|
+
* pre-gap-3 behavior. Opt-in only, like `journal`: there is no safe default allowlist of
|
|
607
|
+
* "tools okay to auto-continue without a human" this package can supply on a caller's behalf.
|
|
608
|
+
*/
|
|
609
|
+
readonly continuation?: ContinuationOptions;
|
|
610
|
+
/**
|
|
611
|
+
* Gap 4's failure classifier — see `ClassifyFailure`'s own doc.
|
|
612
|
+
* @default undefined — every `'failed'` run stays `resumable: false`, byte-identical to
|
|
613
|
+
* pre-gap-4 behavior. No default classifier exists: OD's own ~20-vendor-CLI text-matching
|
|
614
|
+
* failure classifier was deliberately never ported (see index.ts module doc), so there is no
|
|
615
|
+
* generic "real" classification logic this package could supply on a caller's behalf.
|
|
616
|
+
*/
|
|
617
|
+
readonly classifyFailure?: ClassifyFailure;
|
|
618
|
+
/**
|
|
619
|
+
* Gap 3, part 2's spawn-time `.mcp.json` injection — see {@link McpJsonInjectionOptions}'s own
|
|
620
|
+
* doc for the full design (why only `'claude-mcp-json'`-injection defs, why host-resolved).
|
|
621
|
+
* @default undefined — no `.mcp.json` is written and no filesystem access beyond what already
|
|
622
|
+
* happened (prompt-file staging) occurs on this path, byte-identical to pre-this-task behavior.
|
|
623
|
+
* Opt-in only, like `journal`/`continuation`/`classifyFailure`: there is no safe default
|
|
624
|
+
* `command`/`daemonUrl` this package could assume on a caller's behalf.
|
|
625
|
+
*/
|
|
626
|
+
readonly mcpJsonInjection?: McpJsonInjectionOptions;
|
|
627
|
+
/**
|
|
628
|
+
* Finding 1 of SEC-assistant-env-isolation-2026-09-07's injectable filesystem seams — see
|
|
629
|
+
* {@link ClaudeConfigDirIsolationOptions}'s own doc. This bag itself is still NOT a gate (supplying
|
|
630
|
+
* it only lets a test observe/replace the real filesystem calls); whether staging happens AT ALL is
|
|
631
|
+
* now {@link claudeConfigDirIsolationEnabled}'s job — see that field's doc for why the two were
|
|
632
|
+
* split apart instead of overloading this one's presence as the switch.
|
|
633
|
+
* @default the real `fs.promises.mkdtemp`/`readFile`/`rm` — no real disk I/O for every def other
|
|
634
|
+
* than `claude`, matching this factory's "no real filesystem by default in tests" convention.
|
|
635
|
+
*/
|
|
636
|
+
readonly claudeConfigDirIsolation?: ClaudeConfigDirIsolationOptions;
|
|
637
|
+
/**
|
|
638
|
+
* The actual on/off switch for Finding 1's `CLAUDE_CONFIG_DIR` isolation (see
|
|
639
|
+
* {@link prepareClaudeConfigDirIfNeeded}'s doc for the staging behavior this gates). Split out as
|
|
640
|
+
* its own boolean rather than reusing {@link claudeConfigDirIsolation}'s presence, because that bag
|
|
641
|
+
* is a test-seam-injection convention shared with every other `*IsolationOptions`/`*Seams` field in
|
|
642
|
+
* this file (see `mcpJsonInjection`'s own "NOT a gate" precedent) — overloading it here would mean a
|
|
643
|
+
* host that only wants to override `mkdtemp` for a test silently also flips production behavior.
|
|
644
|
+
*
|
|
645
|
+
* @default `false`. This DEFAULTS OFF, which reopens the leak Finding 1 closed (the spawned
|
|
646
|
+
* `claude` child again reads the operator's real `~/.claude` — skills, plugins, memory index, and
|
|
647
|
+
* whatever tool grant that directory carries) — **not a regression discovered later, a deliberate
|
|
648
|
+
* rollback landed the same day as the isolation fix itself.** Reason: on macOS the Keychain login
|
|
649
|
+
* `claude auth status` reports is keyed to `CLAUDE_CONFIG_DIR` (see {@link
|
|
650
|
+
* prepareClaudeConfigDirForRun}'s doc), and no caller of this factory was passing a credential of
|
|
651
|
+
* its own (`AgentExecutorRunInput.credentialEnv`) when Finding 1 landed unconditionally — so every
|
|
652
|
+
* isolated child ran unauthenticated and the assistant reported "Not logged in" on its default
|
|
653
|
+
* runtime. A host that provisions a real `ANTHROPIC_API_KEY`/`CLAUDE_CODE_OAUTH_TOKEN` via
|
|
654
|
+
* `credentialEnv` (both outrank Keychain login in Claude Code's own auth precedence, so an isolated,
|
|
655
|
+
* still-logged-in child needs no access to the operator's personal Keychain entry at all) should
|
|
656
|
+
* flip this back on — the mechanism itself is unchanged and was already proven live
|
|
657
|
+
* (2026-09-07); only the default changed.
|
|
658
|
+
*/
|
|
659
|
+
readonly claudeConfigDirIsolationEnabled?: boolean;
|
|
660
|
+
/**
|
|
661
|
+
* Ceiling on how many bytes of a `'until-close'` def's stdout this driver will hold in memory
|
|
662
|
+
* before it stops accumulating and reports the shortfall — see
|
|
663
|
+
* {@link DEFAULT_BUFFERED_STDOUT_MAX_BYTES} for the threat this closes and why 8 MiB.
|
|
664
|
+
* @default {@link DEFAULT_BUFFERED_STDOUT_MAX_BYTES}
|
|
665
|
+
*/
|
|
666
|
+
readonly bufferedStdoutMaxBytes?: number;
|
|
667
|
+
/**
|
|
668
|
+
* Host-owned system-prompt overlay — see `prompt-augmenter.ts`'s own doc for why this seam
|
|
669
|
+
* exists (product-specific discovery/behavior instructions that don't belong in the engine).
|
|
670
|
+
* When present, `systemOverlay()` is called once per `run()` and its result (if non-null) is
|
|
671
|
+
* threaded through to `buildArgs` as `RuntimeBuildOptions.systemPromptOverlay` — a def with no
|
|
672
|
+
* append-system-prompt mechanism ignores it.
|
|
673
|
+
* @default undefined — no overlay is computed and no def sees `systemPromptOverlay`,
|
|
674
|
+
* byte-identical to pre-this-option behavior.
|
|
675
|
+
*/
|
|
676
|
+
readonly promptAugmenter?: PromptAugmenter;
|
|
677
|
+
}
|
|
678
|
+
/** `createAgentExecutor`'s internal `failBeforeSpawn` — a nested closure over `lifecycle`, so every extracted `run()` phase below takes it as an explicit injected collaborator rather than reaching for a module-level one. */
|
|
679
|
+
export type FailBeforeSpawn = (args: {
|
|
680
|
+
readonly runId: string;
|
|
681
|
+
readonly code: AgentExecutorErrorCode;
|
|
682
|
+
readonly message: string;
|
|
683
|
+
}) => Promise<never>;
|
|
684
|
+
/**
|
|
685
|
+
* {@link resolveSystemPromptOverlayDelivery}'s decision, threaded out of that one resolver to every
|
|
686
|
+
* channel that can actually carry the overlay to the CLI.
|
|
687
|
+
*
|
|
688
|
+
* **Why `promptPrefix` exists alongside `prompt`.** `prompt` is the composed prompt with the
|
|
689
|
+
* fallback prefix already applied, and is what `buildArgs`, the staged prompt file, and a
|
|
690
|
+
* `promptViaStdin` def's stdin all receive. But the two RPC transports (`runAcpDispatch`,
|
|
691
|
+
* `runPiRpcDispatch`) deliberately send `run()`'s *raw* `input.prompt`, not the image-delivery
|
|
692
|
+
* rewrite this resolver was handed — those defs deliver images through their own native protocol
|
|
693
|
+
* and must never also get `'prompt-path'`'s appended paths (see `resolveImageDeliveryAndArgvBudget`'s
|
|
694
|
+
* call site in `run()` for that hazard). Handing them `prompt` would silently couple them to the
|
|
695
|
+
* image rewrite; handing them `promptPrefix + input.prompt` applies exactly this resolver's overlay
|
|
696
|
+
* decision to exactly the prompt text they were already sending.
|
|
697
|
+
*
|
|
698
|
+
* **The no-double-delivery invariant.** `promptPrefix` is a non-empty string on exactly one path —
|
|
699
|
+
* the universal fallback, the only strategy that carries the overlay *in the prompt text*. Every
|
|
700
|
+
* declared strategy (`'append-flag'`, `'env-var'`, `'config-instructions-file'`) and every
|
|
701
|
+
* suppressed case (no overlay, continuing a session) returns `''`, so a def that receives the
|
|
702
|
+
* overlay through argv, an env var, or a staged config file never also receives it inline. A
|
|
703
|
+
* transport therefore does not need to know which strategy applies: applying `promptPrefix`
|
|
704
|
+
* unconditionally is correct precisely because the resolver already zeroed it where it must not
|
|
705
|
+
* apply.
|
|
706
|
+
*/
|
|
707
|
+
export interface SystemPromptOverlayDelivery {
|
|
708
|
+
/**
|
|
709
|
+
* The text to prepend to whatever prompt a transport is about to send — `''` for every def whose
|
|
710
|
+
* overlay rides another channel, so it is always safe to apply unconditionally. See this
|
|
711
|
+
* interface's own doc for why callers with their own prompt text use this rather than `prompt`.
|
|
712
|
+
*/
|
|
713
|
+
readonly promptPrefix: string;
|
|
714
|
+
/** `promptPrefix` already applied to the prompt this resolver was handed. */
|
|
715
|
+
readonly prompt: string;
|
|
716
|
+
/** Extra argv to append to `buildArgs`' own result — non-empty only for `'append-flag'`. */
|
|
717
|
+
readonly extraArgs: readonly string[];
|
|
718
|
+
/** Env vars to merge into the spawn env — non-empty only for `'env-var'`. */
|
|
719
|
+
readonly envOverrides: Readonly<Record<string, string>>;
|
|
720
|
+
}
|
|
721
|
+
/** {@link prepareSystemPromptOverlayFileIfNeeded}'s result. */
|
|
722
|
+
export type PreparedSystemPromptOverlayFile = {
|
|
723
|
+
/** Absolute path to the staged file, ready to merge into a `config-instructions-file` def's `instructions` array. */
|
|
724
|
+
readonly path: string;
|
|
725
|
+
/** Recursively removes the staged directory. Safe to call more than once. */
|
|
726
|
+
readonly cleanup: () => Promise<void>;
|
|
727
|
+
};
|
|
728
|
+
/** {@link spawnAgentChildProcess}'s result — a discriminated union rather than a thrown/rejected outcome so the call can stay synchronous; see that function's doc for why. */
|
|
729
|
+
export type SpawnAgentChildProcessResult = {
|
|
730
|
+
readonly kind: 'ok';
|
|
731
|
+
readonly child: ChildProcess;
|
|
732
|
+
} | {
|
|
733
|
+
readonly kind: 'error';
|
|
734
|
+
readonly error: unknown;
|
|
735
|
+
};
|
|
736
|
+
//# sourceMappingURL=contracts.d.ts.map
|