@jini-ai/daemon 0.6.12 → 0.6.15-rc.2

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/agent-executor/codex/home-config.d.ts +50 -0
  3. package/dist/agent-executor/codex/home-config.js +209 -0
  4. package/dist/agent-executor/codex/home-staging.d.ts +60 -0
  5. package/dist/agent-executor/codex/home-staging.js +195 -0
  6. package/dist/agent-executor/codex/isolation.d.ts +27 -0
  7. package/dist/agent-executor/codex/isolation.js +67 -0
  8. package/dist/agent-executor/contracts.d.ts +4 -1
  9. package/dist/agent-executor/event-translation.d.ts +7 -7
  10. package/dist/agent-executor/event-translation.js +17 -17
  11. package/dist/agent-executor/events.js +5 -2
  12. package/dist/agent-executor/index.d.ts +3 -1
  13. package/dist/agent-executor/index.js +3 -1
  14. package/dist/agent-executor/launch.d.ts +3 -2
  15. package/dist/agent-executor/launch.js +77 -21
  16. package/dist/agent-executor/live-session-state.js +4 -1
  17. package/dist/agent-executor/mcp-bridge-probe.d.ts +2 -1
  18. package/dist/agent-executor/mcp-bridge-probe.js +1 -1
  19. package/dist/agent-executor/mcp-bridge.d.ts +9 -86
  20. package/dist/agent-executor/mcp-bridge.js +12 -360
  21. package/dist/ai-trace-lifecycle.d.ts +48 -0
  22. package/dist/ai-trace-lifecycle.js +116 -0
  23. package/dist/core/read-only-tools.js +3 -1
  24. package/dist/delegated-tool-bridge.js +19 -1
  25. package/dist/http/attachment-lookup-routes.d.ts +44 -0
  26. package/dist/http/attachment-lookup-routes.js +71 -0
  27. package/dist/http/attachments.d.ts +112 -7
  28. package/dist/http/attachments.js +58 -17
  29. package/dist/http/connectors.js +23 -18
  30. package/dist/http/index.d.ts +6 -0
  31. package/dist/http/index.js +3 -0
  32. package/dist/http/message-relay.js +4 -1
  33. package/dist/http/tool-execution-routes.d.ts +69 -0
  34. package/dist/http/tool-execution-routes.js +181 -0
  35. package/dist/http/xai.js +19 -9
  36. package/dist/index.d.ts +3 -0
  37. package/dist/index.js +3 -0
  38. package/dist/remote-attachment-lookup.d.ts +29 -0
  39. package/dist/remote-attachment-lookup.js +71 -0
  40. package/dist/remote-tool-executor.d.ts +85 -0
  41. package/dist/remote-tool-executor.js +250 -0
  42. package/dist/routing-tool-executor.d.ts +29 -0
  43. package/dist/routing-tool-executor.js +16 -0
  44. package/dist/run-lifecycle.d.ts +4 -1
  45. package/dist/run-lifecycle.js +35 -5
  46. package/dist/store/event-log/sqlite-kernel.d.ts +18 -0
  47. package/dist/store/event-log/sqlite-kernel.js +135 -0
  48. package/dist/store/event-log/sqlite-policy.d.ts +93 -0
  49. package/dist/store/event-log/sqlite-policy.js +74 -0
  50. package/dist/store/event-log/sqlite.d.ts +7 -31
  51. package/dist/store/event-log/sqlite.js +32 -95
  52. package/dist/tool-recovery/analytics.d.ts +27 -0
  53. package/dist/tool-recovery/analytics.js +38 -0
  54. package/dist/tool-recovery/index.d.ts +2 -0
  55. package/dist/tool-recovery/index.js +2 -0
  56. package/dist/tool-recovery/observability.d.ts +82 -0
  57. package/dist/tool-recovery/observability.js +88 -0
  58. package/package.json +15 -14
package/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
1
+ ## 0.6.13
2
+
3
+ ### Patch Changes
4
+
5
+ - aef0152: Folder-limited Claude runs get a private per-run CLAUDE_CODE_TMPDIR (a 0700 /tmp/jini-\* directory, removed on exit, crash, cancellation or startup failure). The sandbox grants only that run's scratch subtree; the shared per-user /tmp/claude-<uid> grant is gone, and overlong bases are refused instead of falling back to shared scratch.
6
+ - e9f64d4: PROTOTYPE (cloud/observability-1025, not for release): `withToolCallObservability` (`@jini-ai/daemon/tool-recovery`) makes each tool call one `tool.call` operation on a structural observability slice; execution errors are reported, validation errors are counted only.
7
+ - a0ce70f: Add opt-in host-managed SQLite event-log schema setup, a borrowed-kernel factory that joins host transactions (its replay reads one statement snapshot, so it also works on read-only kernels and inside a host run()), and append receipts for conflicting retries or retention-cap refusals. Existing append defaults and FIFO retention remain unchanged.
8
+ - 0728914: Engine guard clean-up (`pnpm guard` 106 -> 0): layer, subpath-isolation and module-state findings fixed by moving
9
+ shared contracts to their owners instead of reaching across domains. Additive public API: `@jini-ai/core/filesystem`
10
+ (guarded-reader contracts), `useFetchQueryScope` from `@jini-ai/ui` fetch-query (per-provider query scope),
11
+ `@jini-ai/chat/react` re-exports of the accepted-input, run-state and agent-plugin chat-state helpers. The Claude
12
+ filesystem probe cache in `@jini-ai/agent-runtime` is instance-owned. No behaviour change intended; host-facing
13
+ breaks are listed in `owner-store-to-server` and `guard-host-ports`.
14
+ - cbce255: Apply AI-trace capture policy to errors, contain tracing and sink failures, preserve canonical
15
+ usage and stop reasons, accumulate request usage snapshots, scope tool identities per request,
16
+ and finalize traces from committed terminal events including inactivity watchdog completion.
17
+ Provider/session trace mappers now receive object arguments.
18
+ A trace whose stream subscription is refused or rejects after `run_started` is now finished from
19
+ the run's real terminal state (`waitForTerminal`, in the background) instead of being dropped.
20
+ `RunLifecycle.waitForTerminal` takes an optional `{ signal }`: aborting it rejects with the reason and
21
+ unregisters the waiter. The traced lifecycle gains `dispose()`, which releases open traces without
22
+ finishing them and cancels those background waits; the server kernel's `close()` calls it after the
23
+ agent runtime stops, so a retained kernel keeps no recorder or waiter.
24
+ - 803a949: Observability vendor adapters as provider subpaths: `@jini-ai/diagnostics/observability/opentelemetry` (moved from Tovu core) and `@jini-ai/diagnostics/observability/sentry`, each returning `{ port, dispose }` with optional peer SDKs. Review fixes: attribute strings and `Error.name` redacted and capped; fan-out isolates each adapter's tracker creation and end; Sentry root operations start fresh traces and never call `NodeClient#init()` or channel injection. `withToolCallObservability` exports a fixed label for unregistered tool ids and adds `user.id` (pseudonymous principal).
25
+ - b1fcb8f: AI tracing no longer reopens a trace when `dispose()` runs while the underlying start is pending; `waitForTerminal()` observes its abort signal while a durable start append is still pending; the fenced-outbox conformance overlap case reports a synchronously throwing `awaitOverlap` hook as a barrier failure (contender settled, nothing unhandled) and `awaitOverlap` is documented.
26
+ - Updated dependencies [7479159]
27
+ - Updated dependencies [c31264d]
28
+ - Updated dependencies [0728914]
29
+ - Updated dependencies [cbce255]
30
+ - Updated dependencies [6ecbdb2]
31
+ - Updated dependencies [63bacc5]
32
+ - @jini-ai/core@0.6.13
33
+ - @jini-ai/protocol@0.6.13
34
+
1
35
  ## 0.6.12
2
36
 
3
37
  ### Patch Changes
@@ -0,0 +1,50 @@
1
+ import type { McpJsonServerEntry } from '../contracts.js';
2
+ /** A TOML key: bare when TOML allows it, quoted otherwise — host-supplied bridge env names are not guaranteed bare-safe. */
3
+ export declare function tomlKey(key: string): string;
4
+ /**
5
+ * Mechanism 5 of 5 — `'codex-toml'`'s serialization step. Builds the `[mcp_servers.jini]` TOML
6
+ * table (plus, when the entry carries any env vars, a separate `[mcp_servers.jini.env]` table)
7
+ * Codex's own config schema expects.
8
+ *
9
+ * Confirmed against a real installed Codex CLI (0.151.0), not assumed from docs: round-tripping
10
+ * `codex mcp add <name> --env K=V -- <cmd> <args>` against a scratch `CODEX_HOME` and reading back
11
+ * `config.toml` produced exactly this shape (`command`/`args` as TOML strings/array in the main
12
+ * table, env vars in a nested `.env` table) — see `archived provenance ledger` for the transcript.
13
+ * @param entry - The shared bridge entry from {@link buildMcpJsonServerEntry}.
14
+ * @returns A TOML fragment with no leading/trailing blank-line padding — {@link buildCodexHomeConfigToml} owns spacing when combining it with existing content.
15
+ * @complexity O(n) in the number of argv/env entries.
16
+ * @overallScore 100/100
17
+ */
18
+ export declare function buildCodexMcpServerToml({ entry }: {
19
+ readonly entry: McpJsonServerEntry;
20
+ }): string;
21
+ /**
22
+ * Builds the full `config.toml` a run's scratch `CODEX_HOME` gets: the real Codex home's own
23
+ * config, with any pre-existing `[mcp_servers.jini]` table removed (see
24
+ * {@link stripExistingJiniMcpServerTable}), then this run's own table appended.
25
+ *
26
+ * **Append-only by design, not a parse-and-merge, for everything but the jini table itself.**
27
+ * `mergeMcpJsonContent`/`mergeEnvContentMcpConfig` in `mcp-bridge.ts` can safely parse-merge-reserialize because
28
+ * their formats have a JS-native parser (`JSON.parse`); re-serializing parsed TOML would drop
29
+ * comments and layout (see `buildCodexMcpServerToml`'s doc), and every other setting a real Codex
30
+ * install carries — model choice, sandbox policy, the trusted-project list, the operator's own
31
+ * other MCP servers — must survive a spawn byte-for-byte. Only the one table this driver itself
32
+ * owns (`jini` is this integration's own reserved server name — see {@link JINI_MCP_SERVER_KEY}) is
33
+ * removed by default, via the narrow line-oriented scan above, never a full TOML parse. Explicit
34
+ * per-run OFF choices additionally omit personal MCP tables or retain only those tables when
35
+ * user settings are off. Inline root MCP declarations fail closed when isolation is requested,
36
+ * and `isolation.ts` parses the result to refuse any server or trust entry this line scan could not see.
37
+ * @param existingRaw - The real Codex home's `config.toml` content, or `undefined` when it does not
38
+ * exist (a fresh Codex install — degrades to "start from just this run's block", matching
39
+ * {@link mergeMcpJsonContent}'s own "missing file" handling).
40
+ * @param entry - The shared bridge entry.
41
+ * @returns The full text to write to the scratch `CODEX_HOME`'s `config.toml`.
42
+ * @complexity O(n) in the existing config's length.
43
+ */
44
+ export declare function buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp, inheritUserSettings }: {
45
+ readonly existingRaw: string | undefined;
46
+ readonly entry: McpJsonServerEntry;
47
+ readonly inheritUserMcp?: boolean;
48
+ readonly inheritUserSettings?: boolean;
49
+ }, _optional?: Record<string, never>): string;
50
+ //# sourceMappingURL=home-config.d.ts.map
@@ -0,0 +1,209 @@
1
+ import { JINI_MCP_SERVER_KEY } from '../mcp-bridge.js';
2
+ /**
3
+ * TOML basic-string escaping for the narrow value shapes {@link buildCodexMcpServerToml} emits (a
4
+ * command name, an argv token, an env var value — never multi-line or control-character-heavy
5
+ * text). Escapes exactly what TOML's basic-string grammar requires: backslash first (so it is not
6
+ * re-escaped by a later replacement), then the quote delimiter, then the three whitespace control
7
+ * characters a real command/argv/env value could plausibly contain.
8
+ *
9
+ * A hand-rolled minimal escaper rather than a TOML serializer — this mechanism never needs to
10
+ * *re-serialize* TOML (the real install's existing `config.toml` is appended after, never
11
+ * rewritten — see {@link buildCodexHomeConfigToml}), so a full TOML writer for one serialization
12
+ * shape would be substantially more surface than the problem needs. The isolation check
13
+ * (`isolation.ts`) parses layers with smol-toml, but only reads them.
14
+ * @param value - The raw string to embed inside TOML `"..."` delimiters.
15
+ * @returns The escaped text, WITHOUT the surrounding quotes — {@link tomlString} adds those.
16
+ * @complexity O(n) in the string's length.
17
+ */
18
+ function escapeTomlBasicString(value) {
19
+ return value
20
+ .replace(/\\/g, '\\\\')
21
+ .replace(/"/g, '\\"')
22
+ .replace(/\n/g, '\\n')
23
+ .replace(/\r/g, '\\r')
24
+ .replace(/\t/g, '\\t');
25
+ }
26
+ /** Wraps {@link escapeTomlBasicString}'s output in the TOML basic-string delimiters. */
27
+ function tomlString(value) {
28
+ return `"${escapeTomlBasicString(value)}"`;
29
+ }
30
+ /** A TOML key: bare when TOML allows it, quoted otherwise — host-supplied bridge env names are not guaranteed bare-safe. */
31
+ export function tomlKey(key) {
32
+ return /^[A-Za-z0-9_-]+$/.test(key) ? key : tomlString(key);
33
+ }
34
+ /**
35
+ * Codex's per-tool deadline for the Jini bridge, in seconds: `@jini-ai/mcp`'s
36
+ * `DEFAULT_DELEGATED_TOOL_TIMEOUT_MS` (6 min, mirrored because this package does not depend on
37
+ * that one) plus 40 s. Codex's own default (60 s) is shorter than a delegated call parked on a
38
+ * human dialog, and when Codex abandons a call the dialog stayed answerable, so a late click ran
39
+ * an action the model had been told failed. Outlasting the bridge's deadline means the bridge
40
+ * times out first, drops its daemon request, and the daemon expires the dialog. Codex does not
41
+ * pass the parent env through to MCP servers, so the bridge under Codex always runs at its default.
42
+ */
43
+ const CODEX_TOOL_TIMEOUT_SEC = 6 * 60 + 40;
44
+ /**
45
+ * Mechanism 5 of 5 — `'codex-toml'`'s serialization step. Builds the `[mcp_servers.jini]` TOML
46
+ * table (plus, when the entry carries any env vars, a separate `[mcp_servers.jini.env]` table)
47
+ * Codex's own config schema expects.
48
+ *
49
+ * Confirmed against a real installed Codex CLI (0.151.0), not assumed from docs: round-tripping
50
+ * `codex mcp add <name> --env K=V -- <cmd> <args>` against a scratch `CODEX_HOME` and reading back
51
+ * `config.toml` produced exactly this shape (`command`/`args` as TOML strings/array in the main
52
+ * table, env vars in a nested `.env` table) — see `archived provenance ledger` for the transcript.
53
+ * @param entry - The shared bridge entry from {@link buildMcpJsonServerEntry}.
54
+ * @returns A TOML fragment with no leading/trailing blank-line padding — {@link buildCodexHomeConfigToml} owns spacing when combining it with existing content.
55
+ * @complexity O(n) in the number of argv/env entries.
56
+ * @overallScore 100/100
57
+ */
58
+ export function buildCodexMcpServerToml({ entry }) {
59
+ const argsLiteral = entry.args.map(tomlString).join(', ');
60
+ const serverTable = `[mcp_servers.${JINI_MCP_SERVER_KEY}]\ncommand = ${tomlString(entry.command)}\nargs = [${argsLiteral}]\n` +
61
+ `tool_timeout_sec = ${CODEX_TOOL_TIMEOUT_SEC}\n`;
62
+ const envLines = Object.entries(entry.env)
63
+ .filter((pair) => typeof pair[1] === 'string')
64
+ .map(([key, value]) => `${tomlKey(key)} = ${tomlString(value)}`);
65
+ if (envLines.length === 0)
66
+ return serverTable;
67
+ return `${serverTable}\n[mcp_servers.${JINI_MCP_SERVER_KEY}.env]\n${envLines.join('\n')}\n`;
68
+ }
69
+ /**
70
+ * Splits a TOML table header's dotted key into its individual segments, honoring quoted parts
71
+ * (`"basic"` or `'literal'`) that may themselves contain a literal `.` — a bare `.` only
72
+ * separates segments outside of quotes. Each segment is trimmed and, if quoted, unwrapped.
73
+ *
74
+ * Not a full TOML parser: it does not resolve escape sequences (`\"`, `\u...`) inside basic
75
+ * strings. That is deliberately out of scope — see {@link stripExistingJiniMcpServerTable}'s doc
76
+ * for why it is safe to skip for this specific comparison.
77
+ * @param rawKey - The raw text between a table header's `[` and `]`.
78
+ * @returns The dotted key's segments, dequoted and trimmed.
79
+ * @complexity O(n) in the length of `rawKey`.
80
+ */
81
+ function splitTomlDottedKey(rawKey) {
82
+ const scan = { segments: [], current: '', quote: null };
83
+ for (const ch of rawKey) {
84
+ if (scan.quote === null)
85
+ scanBareTomlKeyChar(scan, ch);
86
+ else if (ch === scan.quote)
87
+ scan.quote = null;
88
+ else
89
+ scan.current += ch;
90
+ }
91
+ scan.segments.push(scan.current.trim());
92
+ return scan.segments;
93
+ }
94
+ /** One key character outside quotes: opens a quote, ends the open segment at a `.`, or extends it. */
95
+ function scanBareTomlKeyChar(scan, ch) {
96
+ if (ch === '"' || ch === "'")
97
+ scan.quote = ch;
98
+ else if (ch === '.') {
99
+ scan.segments.push(scan.current.trim());
100
+ scan.current = '';
101
+ }
102
+ else
103
+ scan.current += ch;
104
+ }
105
+ // An inline or dotted root `mcp_servers` declaration, which the table scanner below cannot rewrite.
106
+ const INLINE_MCP_SERVERS = /^\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*(?:\.|=)/;
107
+ // `[<key>]` optionally followed only by a `# comment`; `[[...]]` never matches (see below).
108
+ const TOML_TABLE_HEADER = /^\s*\[([^[\]]+)\]\s*(#.*)?$/;
109
+ /**
110
+ * Whether {@link stripExistingJiniMcpServerTable} drops the table named `key`: always the jini
111
+ * table and its subtables; with `allServers` every MCP table and every `projects` (trust) table,
112
+ * so Codex loads no project layer that could add a server; with `mcpOnly` every non-MCP table.
113
+ */
114
+ function dropsTomlTable(key, optional) {
115
+ const jiniTableKey = `mcp_servers.${JINI_MCP_SERVER_KEY}`;
116
+ if (key === jiniTableKey || key.startsWith(`${jiniTableKey}.`))
117
+ return true;
118
+ const isMcp = key === 'mcp_servers' || key.startsWith('mcp_servers.');
119
+ if (optional.allServers === true && (isMcp || key === 'projects' || key.startsWith('projects.')))
120
+ return true;
121
+ return optional.mcpOnly === true && !isMcp;
122
+ }
123
+ /**
124
+ * Removes any pre-existing `[mcp_servers.{@link JINI_MCP_SERVER_KEY}]` table — and its
125
+ * `[mcp_servers.{@link JINI_MCP_SERVER_KEY}.*]` subtables (e.g. `.env`) — from a real Codex
126
+ * `config.toml`'s raw text, so {@link buildCodexHomeConfigToml} can append this run's own table
127
+ * without producing the duplicate TOML key Codex's parser rejects at startup.
128
+ *
129
+ * Line-oriented, not a real TOML parser (matching {@link buildCodexMcpServerToml}'s own
130
+ * no-TOML-dependency constraint), but wide enough to survive a hand-edited config, which is the
131
+ * scenario this whole function exists for. A table header line is recognized when, after
132
+ * trimming, it is `[<key>]` optionally followed only by a `# comment` (TOML's own grammar allows
133
+ * nothing else there) — so it tolerates leading indentation, whitespace inside the brackets, a
134
+ * trailing line comment, and a dotted key written with quoted segments (`["mcp_servers"."jini"]`),
135
+ * via {@link splitTomlDottedKey}. Once such a header's key matches the jini table or one of its
136
+ * subtables, every following line is dropped until the next table header (of any name) or EOF.
137
+ * Every other table, key, comment-only, and blank line is passed through untouched.
138
+ *
139
+ * Deliberately unhandled, and why it is safe to leave that way:
140
+ * - **Escape sequences inside quoted key segments** (e.g. a segment containing `\"`) — neither
141
+ * `mcp_servers` nor {@link JINI_MCP_SERVER_KEY} ever needs escaping, and this driver's own
142
+ * writer ({@link buildCodexMcpServerToml}) never emits a quoted form at all, so no real
143
+ * `config.toml` this driver produced can exercise this gap; a hand-edit that goes out of its
144
+ * way to escape a character inside a key that is supposed to spell "jini" would fail to match
145
+ * and fall back to today's pre-fix behavior (append a duplicate) rather than silently doing
146
+ * something worse.
147
+ * - **`[[array-of-tables]]` syntax** — Codex's schema has no array-of-tables shape for
148
+ * `mcp_servers`, and the header regex's `[^[\]]+` capture cannot match a line starting with a
149
+ * second `[`, so `[[mcp_servers.jini]]` is not recognized as a header before or after this fix
150
+ * — unchanged, not a new gap.
151
+ * - **A single quoted segment that happens to spell the same characters with the dot included**
152
+ * (e.g. `["mcp_servers.jini"]`, which in real TOML names one key literally containing a `.`,
153
+ * not two nested tables) — this collapses to the same joined string as the real nested-table
154
+ * spelling and is therefore treated as a match. This is a false positive in the conservative
155
+ * direction the reported bug calls for: a missed detection duplicates a key and crashes Codex,
156
+ * so on this axis over-matching a spelling nobody would plausibly hand-write for an MCP server
157
+ * table is preferable to under-matching the real one.
158
+ * @param existingRaw - The real Codex home's `config.toml` content, already known to be defined
159
+ * (callers pass `''` for a missing file).
160
+ * @returns `existingRaw` with any jini table/subtable removed.
161
+ * @complexity O(n) in the number of lines.
162
+ */
163
+ function stripExistingJiniMcpServerTable(existingRaw, optional = {}) {
164
+ const kept = [];
165
+ let skipping = optional.mcpOnly === true;
166
+ for (const line of existingRaw.split('\n')) {
167
+ // The existing table scanner cannot safely rewrite inline/dotted root server values. Refuse
168
+ // that uncommon shape when isolating servers rather than accidentally preserving a grant.
169
+ if (optional.allServers && INLINE_MCP_SERVERS.test(line)) {
170
+ 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.');
171
+ }
172
+ const header = TOML_TABLE_HEADER.exec(line);
173
+ // The header's key capture group is not optional, so `header[1]` is always set on a match.
174
+ if (header)
175
+ skipping = dropsTomlTable(splitTomlDottedKey(header[1]).join('.'), optional);
176
+ if (!skipping)
177
+ kept.push(line);
178
+ }
179
+ return kept.join('\n');
180
+ }
181
+ /**
182
+ * Builds the full `config.toml` a run's scratch `CODEX_HOME` gets: the real Codex home's own
183
+ * config, with any pre-existing `[mcp_servers.jini]` table removed (see
184
+ * {@link stripExistingJiniMcpServerTable}), then this run's own table appended.
185
+ *
186
+ * **Append-only by design, not a parse-and-merge, for everything but the jini table itself.**
187
+ * `mergeMcpJsonContent`/`mergeEnvContentMcpConfig` in `mcp-bridge.ts` can safely parse-merge-reserialize because
188
+ * their formats have a JS-native parser (`JSON.parse`); re-serializing parsed TOML would drop
189
+ * comments and layout (see `buildCodexMcpServerToml`'s doc), and every other setting a real Codex
190
+ * install carries — model choice, sandbox policy, the trusted-project list, the operator's own
191
+ * other MCP servers — must survive a spawn byte-for-byte. Only the one table this driver itself
192
+ * owns (`jini` is this integration's own reserved server name — see {@link JINI_MCP_SERVER_KEY}) is
193
+ * removed by default, via the narrow line-oriented scan above, never a full TOML parse. Explicit
194
+ * per-run OFF choices additionally omit personal MCP tables or retain only those tables when
195
+ * user settings are off. Inline root MCP declarations fail closed when isolation is requested,
196
+ * and `isolation.ts` parses the result to refuse any server or trust entry this line scan could not see.
197
+ * @param existingRaw - The real Codex home's `config.toml` content, or `undefined` when it does not
198
+ * exist (a fresh Codex install — degrades to "start from just this run's block", matching
199
+ * {@link mergeMcpJsonContent}'s own "missing file" handling).
200
+ * @param entry - The shared bridge entry.
201
+ * @returns The full text to write to the scratch `CODEX_HOME`'s `config.toml`.
202
+ * @complexity O(n) in the existing config's length.
203
+ */
204
+ export function buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp = true, inheritUserSettings = true }, _optional = {}) {
205
+ const base = stripExistingJiniMcpServerTable(existingRaw ?? '', { allServers: !inheritUserMcp, mcpOnly: !inheritUserSettings });
206
+ const separator = base.length === 0 ? '' : base.endsWith('\n') ? '\n' : '\n\n';
207
+ return `${base}${separator}${buildCodexMcpServerToml({ entry: entry })}`;
208
+ }
209
+ //# sourceMappingURL=home-config.js.map
@@ -0,0 +1,60 @@
1
+ import type { RuntimeAgentDef } from '@jini-ai/agent-runtime';
2
+ import type { McpJsonInjectionOptions, McpBridgeDelivery, PreparedCodexHome, FailBeforeSpawn } from '../contracts.js';
3
+ /**
4
+ * Where `'codex-toml'` reads the operator's REAL Codex config from, to seed a run's scratch copy —
5
+ * never where it writes. Resolved against the daemon HOST process's own environment (`hostEnv`,
6
+ * `process.env` at the real call site), not a run's sandboxed spawn env: `CODEX_HOME` is not in
7
+ * `BASELINE_AGENT_ENV_KEYS`, so a spawned child never inherits it anyway, and the whole point here
8
+ * is finding wherever the *operator's actual* Codex install lives, which is a host-machine fact.
9
+ * @param hostEnv - The daemon process's own environment.
10
+ * @returns `hostEnv.CODEX_HOME` when set to a non-blank value (matching Codex's own resolution
11
+ * order), else the CLI's documented default, `~/.codex`.
12
+ * @complexity O(1).
13
+ * @overallScore 100/100
14
+ */
15
+ export declare function resolveSourceCodexHomeDir({ hostEnv }: {
16
+ readonly hostEnv: NodeJS.ProcessEnv;
17
+ }): string;
18
+ /**
19
+ * The child env pointed at a staged `CODEX_HOME`. Drops `CODEX_INTERNAL_ORIGINATOR_OVERRIDE`: a
20
+ * first-party originator lets a mentioned skill install and start MCP servers (codex-rs
21
+ * `mcp_skill_dependencies.rs`), and `codex exec` is `codex_exec` without it.
22
+ */
23
+ export declare function withStagedCodexHome({ env, codexHomeDir }: {
24
+ readonly env: NodeJS.ProcessEnv;
25
+ readonly codexHomeDir: string;
26
+ }, _options?: {}): NodeJS.ProcessEnv;
27
+ /** Which personal Codex config a staged home keeps: none when native tools are locked, else what the run's capabilities allow. */
28
+ export declare function codexInheritance(input: {
29
+ readonly nativeToolAccess?: boolean | undefined;
30
+ readonly nativeCapabilities?: Partial<import('@jini-ai/agent-runtime').NativeCapabilities>;
31
+ }): {
32
+ settings: boolean;
33
+ mcp: boolean;
34
+ };
35
+ /**
36
+ * Phase 10b: mechanism 5 of 5's one effect — stages this run's scratch `CODEX_HOME` directory,
37
+ * returning the prepared handle `cleanupStagedFiles` should later release (`null` for every other
38
+ * mechanism, or for an unconfigured host — matching {@link writeMcpJsonIfNeeded}'s identical gate).
39
+ * @param input.def - Only used for its `id`, in the failure message.
40
+ * @param input.mcpBridge - This run's resolved bridge delivery — a no-op unless its `kind` is `'codex-toml'`.
41
+ * @param deps.hostEnv - The daemon's own environment, threaded through to {@link resolveSourceCodexHomeDir} rather than read from a module-level `process.env` so this phase stays testable with an injected env.
42
+ * @complexity O(1) plus {@link prepareCodexHomeForRun}'s own cost.
43
+ * @overallScore 100/100
44
+ */
45
+ export declare function prepareCodexHomeIfNeeded({ input, deps }: {
46
+ readonly input: {
47
+ readonly runId: string;
48
+ readonly nativeToolAccess?: boolean | undefined;
49
+ readonly nativeCapabilities?: Partial<import('@jini-ai/agent-runtime').NativeCapabilities>;
50
+ readonly def: RuntimeAgentDef;
51
+ readonly mcpBridge: McpBridgeDelivery | null;
52
+ };
53
+ readonly deps: {
54
+ readonly mcpJsonInjection: McpJsonInjectionOptions | undefined;
55
+ readonly hostEnv: NodeJS.ProcessEnv;
56
+ readonly releaseStagedResources: () => Promise<void>;
57
+ readonly failBeforeSpawn: FailBeforeSpawn;
58
+ };
59
+ }): Promise<PreparedCodexHome | null>;
60
+ //# sourceMappingURL=home-staging.d.ts.map
@@ -0,0 +1,195 @@
1
+ import { promises as fsPromises } from 'node:fs';
2
+ import { homedir, tmpdir, } from 'node:os';
3
+ import { join, resolve, } from 'node:path';
4
+ import { errorMessage } from '../values.js';
5
+ import { defaultReadMcpJsonFile, defaultWriteMcpJsonFile } from '../mcp-bridge.js';
6
+ import { buildCodexHomeConfigToml } from './home-config.js';
7
+ import { assertCodexHomeConfigParses, assertStagedHomeIsolated, CodexMcpIsolationError, codexIsolationRefusalMessage } from './isolation.js';
8
+ /**
9
+ * Where `'codex-toml'` reads the operator's REAL Codex config from, to seed a run's scratch copy —
10
+ * never where it writes. Resolved against the daemon HOST process's own environment (`hostEnv`,
11
+ * `process.env` at the real call site), not a run's sandboxed spawn env: `CODEX_HOME` is not in
12
+ * `BASELINE_AGENT_ENV_KEYS`, so a spawned child never inherits it anyway, and the whole point here
13
+ * is finding wherever the *operator's actual* Codex install lives, which is a host-machine fact.
14
+ * @param hostEnv - The daemon process's own environment.
15
+ * @returns `hostEnv.CODEX_HOME` when set to a non-blank value (matching Codex's own resolution
16
+ * order), else the CLI's documented default, `~/.codex`.
17
+ * @complexity O(1).
18
+ * @overallScore 100/100
19
+ */
20
+ export function resolveSourceCodexHomeDir({ hostEnv }) {
21
+ const override = hostEnv.CODEX_HOME;
22
+ return override !== undefined && override.trim().length > 0 ? override : join(homedir(), '.codex');
23
+ }
24
+ /**
25
+ * The child env pointed at a staged `CODEX_HOME`. Drops `CODEX_INTERNAL_ORIGINATOR_OVERRIDE`: a
26
+ * first-party originator lets a mentioned skill install and start MCP servers (codex-rs
27
+ * `mcp_skill_dependencies.rs`), and `codex exec` is `codex_exec` without it.
28
+ */
29
+ export function withStagedCodexHome({ env, codexHomeDir }, _options = {}) {
30
+ const { CODEX_INTERNAL_ORIGINATOR_OVERRIDE: _originator, ...rest } = env;
31
+ return { ...rest, CODEX_HOME: codexHomeDir };
32
+ }
33
+ function defaultMkdtempCodexHome({ prefix }) {
34
+ return fsPromises.mkdtemp(join(tmpdir(), prefix));
35
+ }
36
+ function defaultRemoveCodexHomeDir({ path }) {
37
+ return fsPromises.rm(path, { recursive: true, force: true });
38
+ }
39
+ /** Config credentials stay run-scoped, but rollouts must survive both completion and cancellation.
40
+ * Native links keep Codex as the sole writer of sessions and refreshed authentication.
41
+ * Parity also links skills/rules/plugins and other home entries; config.toml alone stays staged
42
+ * so run-scoped bearer tokens never reach the real config. Cleanup removes links, not targets.
43
+ * O(h) I/O for h top-level home entries; locked runs link only sessions and authentication. */
44
+ async function defaultLinkCodexSessionStore({ runHome, sourceHome }, optional = {}) {
45
+ for (const name of ['sessions', 'archived_sessions']) {
46
+ const target = resolve(sourceHome, name);
47
+ await fsPromises.mkdir(target, { recursive: true, mode: 0o700 });
48
+ await fsPromises.symlink(target, join(runHome, name), 'junction');
49
+ }
50
+ await fsPromises.symlink(join(sourceHome, 'auth.json'), join(runHome, 'auth.json'), 'file');
51
+ if (optional.inheritUserConfig !== false) {
52
+ for (const entry of await fsPromises.readdir(sourceHome, { withFileTypes: true })) {
53
+ if (['config.toml', 'auth.json', 'sessions', 'archived_sessions'].includes(entry.name))
54
+ continue;
55
+ await fsPromises.symlink(join(sourceHome, entry.name), join(runHome, entry.name), entry.isDirectory() ? 'junction' : 'file');
56
+ }
57
+ }
58
+ }
59
+ function resolveCodexHomeSeams(options) {
60
+ return {
61
+ mkdtemp: options.mkdtemp ?? defaultMkdtempCodexHome,
62
+ readFile: options.readFile ?? defaultReadMcpJsonFile,
63
+ writeFile: options.writeFile ?? defaultWriteMcpJsonFile,
64
+ removeDir: options.removeDir ?? defaultRemoveCodexHomeDir,
65
+ linkSessionStore: options.linkSessionStore ?? defaultLinkCodexSessionStore,
66
+ };
67
+ }
68
+ /**
69
+ * The staged `config.toml`. With personal MCP off the real home config must parse before its line
70
+ * rewrite is trusted, and the staged home must declare only the bridge and trust no project (so
71
+ * Codex's trust gate keeps project `.codex/config.toml` layers out unless another layer trusts
72
+ * them). Plugin, app and skill-installed servers are turned off on the CLI layer (`defs/codex.ts`).
73
+ * Layers outside the home (system, MDM, cloud bundle) are the machine owner's policy, which Off
74
+ * doesn't override (design note 2026-10-27, option N). The spawn-time refusal in
75
+ * {@link prepareCodexHomeIfNeeded} stays the backstop for every blocked case.
76
+ * @throws {@link CodexMcpIsolationError} when the run can't be isolated.
77
+ */
78
+ function buildStagedCodexConfig({ existingRaw, entry, inheritUserConfig, inheritUserMcp, sourceHome }) {
79
+ const build = () => buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp, inheritUserSettings: inheritUserConfig });
80
+ if (inheritUserMcp)
81
+ return build();
82
+ const path = join(sourceHome, 'config.toml');
83
+ assertCodexHomeConfigParses({ raw: existingRaw, path });
84
+ const homeConfig = build();
85
+ assertStagedHomeIsolated({ homeConfig, path });
86
+ return homeConfig;
87
+ }
88
+ /**
89
+ * Mechanism 5 of 5 — `'codex-toml'`'s one effect. Stages a fresh, randomly-named `CODEX_HOME`
90
+ * directory (see {@link McpJsonInjectionOptions.mkdtemp}'s doc for why non-deterministic naming is
91
+ * load-bearing here, not cosmetic) carrying:
92
+ * - `config.toml`: the real Codex home's own config (read best-effort — see
93
+ * {@link buildCodexHomeConfigToml}'s "missing file" handling) with this run's
94
+ * `[mcp_servers.jini]` table appended.
95
+ * - `auth.json`: a link to the real Codex home's stored login, so the spawned CLI is
96
+ * still authenticated. Best-effort is safe here, not merely convenient: a real headless spawn
97
+ * against a `CODEX_HOME` with no `auth.json` at all was confirmed (against installed Codex CLI
98
+ * 0.151.0) to fail fast with a structured `401 Unauthorized` stream event, never an interactive
99
+ * login prompt or a hang — see `defs/codex.ts`'s module doc for the full transcript summary.
100
+ *
101
+ * Never changes the real config. Authentication is linked so a CLI token refresh survives cleanup;
102
+ * copying rotating credentials lost refreshes and could log the operator out. Deleting scratch
103
+ * state must not erase linked native state. The run-bound MCP credential stays in scratch config.
104
+ *
105
+ * A failure after the directory is created (a rejecting `writeFile`, most plausibly) does not leak
106
+ * it: the directory may already hold a partial `config.toml` or a copied credential, so the
107
+ * `catch` below best-effort-removes it before rethrowing, exactly the "partial-failure state leak"
108
+ * class of bug this package's own adversarial-test-design guidance calls out.
109
+ * @param runId - Embedded in the temp-dir prefix for traceability, sanitized the same way
110
+ * `@jini-ai/agent-runtime`'s `prepareAgentLogFile`'s `label` is.
111
+ * @param entry - The shared bridge entry.
112
+ * @param sourceCodexHomeDir - Where to read the real install's `config.toml`/`auth.json` from — see {@link resolveSourceCodexHomeDir}.
113
+ * @param seams - Injectable mkdtemp/readFile/writeFile/removeDir/linkSessionStore, real filesystem by default.
114
+ * @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.
115
+ * @complexity O(h) filesystem operations for h top-level user-home entries; one staged config write.
116
+ * @overallScore 100/100
117
+ */
118
+ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams, inheritUserConfig = true, inheritUserMcp = true) {
119
+ // Stricter than `@jini-ai/agent-runtime`'s `prepareAgentLogFile`/`preparePromptFileForAgent`
120
+ // labels (which keep dots): this prefix stages a directory that ends up holding a copied Codex
121
+ // login credential, so it gets `mcpJsonPathForRun`'s tighter discipline instead — dots stripped
122
+ // too, not just path separators, so a run id like `../../etc/evil` cannot leave even a cosmetic
123
+ // `..` substring in the mkdtemp prefix.
124
+ const safeRunId = runId.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 80) || 'run';
125
+ const dir = await seams.mkdtemp({ prefix: `jini-codex-home-${safeRunId}-` });
126
+ try {
127
+ let existingConfigRaw;
128
+ try {
129
+ existingConfigRaw = inheritUserConfig || inheritUserMcp ? await seams.readFile({ path: join(sourceCodexHomeDir, 'config.toml') }) : undefined;
130
+ }
131
+ catch {
132
+ // No config yet (fresh Codex install) or unreadable — start from just this run's block,
133
+ // matching writeMcpJsonForRun's identical "missing file" handling.
134
+ existingConfigRaw = undefined;
135
+ }
136
+ const content = buildStagedCodexConfig({ existingRaw: existingConfigRaw, entry, inheritUserConfig, inheritUserMcp, sourceHome: sourceCodexHomeDir });
137
+ await seams.writeFile({ path: join(dir, 'config.toml'), content });
138
+ await seams.linkSessionStore({ runHome: dir, sourceHome: sourceCodexHomeDir }, { inheritUserConfig });
139
+ }
140
+ catch (err) {
141
+ await seams.removeDir({ path: dir }).catch(() => {
142
+ // Best-effort only — the original error below is what the caller must see either way.
143
+ });
144
+ throw err;
145
+ }
146
+ return {
147
+ path: dir,
148
+ cleanup: async () => {
149
+ await seams.removeDir({ path: dir });
150
+ },
151
+ };
152
+ }
153
+ /** True when a Codex run turns off the operator's personal MCP servers or settings, which only a staged home can do. */
154
+ function codexTurnsOffPersonalConfig(input) {
155
+ const capabilities = input.nativeCapabilities;
156
+ return input.def.id === 'codex' && (capabilities?.userMcp === false || capabilities?.userSettings === false);
157
+ }
158
+ /** Which personal Codex config a staged home keeps: none when native tools are locked, else what the run's capabilities allow. */
159
+ export function codexInheritance(input) {
160
+ const nativeAllowed = input.nativeToolAccess !== false;
161
+ return { settings: nativeAllowed && input.nativeCapabilities?.userSettings !== false, mcp: nativeAllowed && input.nativeCapabilities?.userMcp !== false };
162
+ }
163
+ /**
164
+ * Phase 10b: mechanism 5 of 5's one effect — stages this run's scratch `CODEX_HOME` directory,
165
+ * returning the prepared handle `cleanupStagedFiles` should later release (`null` for every other
166
+ * mechanism, or for an unconfigured host — matching {@link writeMcpJsonIfNeeded}'s identical gate).
167
+ * @param input.def - Only used for its `id`, in the failure message.
168
+ * @param input.mcpBridge - This run's resolved bridge delivery — a no-op unless its `kind` is `'codex-toml'`.
169
+ * @param deps.hostEnv - The daemon's own environment, threaded through to {@link resolveSourceCodexHomeDir} rather than read from a module-level `process.env` so this phase stays testable with an injected env.
170
+ * @complexity O(1) plus {@link prepareCodexHomeForRun}'s own cost.
171
+ * @overallScore 100/100
172
+ */
173
+ export async function prepareCodexHomeIfNeeded({ input, deps }) {
174
+ if (input.mcpBridge?.kind !== 'codex-toml' || deps.mcpJsonInjection === undefined) {
175
+ if (codexTurnsOffPersonalConfig(input)) {
176
+ await deps.releaseStagedResources();
177
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'NATIVE_TOOL_POLICY_UNSUPPORTED',
178
+ message: 'AgentExecutor: disabling personal Codex configuration requires run-scoped home staging.' });
179
+ }
180
+ return null;
181
+ }
182
+ const inherit = codexInheritance(input);
183
+ try {
184
+ return await prepareCodexHomeForRun(input.runId, input.mcpBridge.serverEntry, resolveSourceCodexHomeDir({ hostEnv: deps.hostEnv }), resolveCodexHomeSeams(deps.mcpJsonInjection), inherit.settings, inherit.mcp);
185
+ }
186
+ catch (err) {
187
+ await deps.releaseStagedResources();
188
+ if (err instanceof CodexMcpIsolationError) {
189
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'NATIVE_TOOL_POLICY_UNSUPPORTED',
190
+ message: codexIsolationRefusalMessage(err.message) });
191
+ }
192
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not stage a CODEX_HOME for agent "${input.def.id}": ${errorMessage(err)}` });
193
+ }
194
+ }
195
+ //# sourceMappingURL=home-staging.js.map
@@ -0,0 +1,27 @@
1
+ /** A run with personal MCP off that Codex cannot keep to the bridge alone: refused, never run with a leak. */
2
+ export declare class CodexMcpIsolationError extends Error {
3
+ }
4
+ /** The user-facing refusal for a {@link CodexMcpIsolationError} reason. */
5
+ export declare function codexIsolationRefusalMessage(reason: string): string;
6
+ /**
7
+ * The real home config must parse before its line-based rewrite ({@link buildCodexHomeConfigToml})
8
+ * is trusted: a malformed block the rewrite drops would otherwise hide that the line scan misread
9
+ * the file. A missing file (`undefined`) is fine.
10
+ * @throws {@link CodexMcpIsolationError} when it isn't valid TOML.
11
+ */
12
+ export declare function assertCodexHomeConfigParses({ raw, path }: {
13
+ readonly raw: string | undefined;
14
+ readonly path: string;
15
+ }): void;
16
+ /**
17
+ * The staged home config, parsed: it must declare no MCP server but the bridge and trust no
18
+ * project. The home rewrite ({@link buildCodexHomeConfigToml}) is line-based so every other byte
19
+ * survives, so a server or trust entry it cannot see (an escaped header, a dotted root key) or a
20
+ * multi-line value a fake header split is refused here instead of staged. O(n) in the config length.
21
+ * @throws {@link CodexMcpIsolationError} when the staged config would not isolate the run.
22
+ */
23
+ export declare function assertStagedHomeIsolated({ homeConfig, path }: {
24
+ readonly homeConfig: string;
25
+ readonly path: string;
26
+ }): void;
27
+ //# sourceMappingURL=isolation.d.ts.map
@@ -0,0 +1,67 @@
1
+ import { parse } from 'smol-toml';
2
+ import { JINI_MCP_SERVER_KEY } from '../mcp-bridge.js';
3
+ import { isRecord } from '../values.js';
4
+ /** A run with personal MCP off that Codex cannot keep to the bridge alone: refused, never run with a leak. */
5
+ export class CodexMcpIsolationError extends Error {
6
+ }
7
+ /** The user-facing refusal for a {@link CodexMcpIsolationError} reason. */
8
+ export function codexIsolationRefusalMessage(reason) {
9
+ return `Codex can't run with 'Your MCP servers' off: ${reason} Turn it on in the assistant settings, or choose Claude.`;
10
+ }
11
+ /**
12
+ * Parses one Codex config layer with a real TOML parser, so no spelling (quoted or escaped keys,
13
+ * dotted keys, inline tables, or a `[header]`-shaped line inside a multi-line string or array)
14
+ * hides a server from the check. A file that does not parse is refused: what it declares can't be
15
+ * known, and Codex could not load it either.
16
+ * @complexity O(n) in the file length.
17
+ */
18
+ function parseCodexLayer(raw, path) {
19
+ try {
20
+ return parse(raw);
21
+ }
22
+ catch {
23
+ throw new CodexMcpIsolationError(`${path} isn't valid TOML, so its MCP servers can't be checked.`);
24
+ }
25
+ }
26
+ /**
27
+ * Refuses a layer that trusts a project. Codex loads a project's `.codex/config.toml` only when the
28
+ * config merged below it trusts that project (codex-rs `config/src/loader/mod.rs` L:1098-1125), so
29
+ * with no `projects` table anywhere no project layer, worktree root or root-marker range can add a
30
+ * server. Parsed, so a dotted, inline or escaped spelling is caught too.
31
+ */
32
+ function assertTrustsNoProject(config, path) {
33
+ if ('projects' in config)
34
+ throw new CodexMcpIsolationError(`${path} trusts projects in a way that can't be removed safely.`);
35
+ }
36
+ /**
37
+ * The real home config must parse before its line-based rewrite ({@link buildCodexHomeConfigToml})
38
+ * is trusted: a malformed block the rewrite drops would otherwise hide that the line scan misread
39
+ * the file. A missing file (`undefined`) is fine.
40
+ * @throws {@link CodexMcpIsolationError} when it isn't valid TOML.
41
+ */
42
+ export function assertCodexHomeConfigParses({ raw, path }) {
43
+ if (raw !== undefined)
44
+ parseCodexLayer(raw, path);
45
+ }
46
+ /**
47
+ * The staged home config, parsed: it must declare no MCP server but the bridge and trust no
48
+ * project. The home rewrite ({@link buildCodexHomeConfigToml}) is line-based so every other byte
49
+ * survives, so a server or trust entry it cannot see (an escaped header, a dotted root key) or a
50
+ * multi-line value a fake header split is refused here instead of staged. O(n) in the config length.
51
+ * @throws {@link CodexMcpIsolationError} when the staged config would not isolate the run.
52
+ */
53
+ export function assertStagedHomeIsolated({ homeConfig, path }) {
54
+ const refusal = new CodexMcpIsolationError(`${path} declares MCP servers that can't be removed safely.`);
55
+ let config;
56
+ try {
57
+ config = parse(homeConfig);
58
+ }
59
+ catch {
60
+ throw refusal;
61
+ }
62
+ const ids = isRecord(config.mcp_servers) ? Object.keys(config.mcp_servers) : [];
63
+ if (ids.some(id => id !== JINI_MCP_SERVER_KEY))
64
+ throw refusal;
65
+ assertTrustsNoProject(config, path);
66
+ }
67
+ //# sourceMappingURL=isolation.js.map