@jini-ai/daemon 0.6.12 → 0.6.15-rc.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 +34 -0
- package/dist/agent-executor/codex/home-config.d.ts +50 -0
- package/dist/agent-executor/codex/home-config.js +209 -0
- package/dist/agent-executor/codex/home-staging.d.ts +60 -0
- package/dist/agent-executor/codex/home-staging.js +195 -0
- package/dist/agent-executor/codex/isolation.d.ts +27 -0
- package/dist/agent-executor/codex/isolation.js +67 -0
- package/dist/agent-executor/contracts.d.ts +4 -1
- package/dist/agent-executor/event-translation.d.ts +7 -7
- package/dist/agent-executor/event-translation.js +17 -17
- package/dist/agent-executor/events.js +1 -1
- package/dist/agent-executor/index.d.ts +3 -1
- package/dist/agent-executor/index.js +3 -1
- package/dist/agent-executor/launch.d.ts +3 -2
- package/dist/agent-executor/launch.js +68 -17
- package/dist/agent-executor/mcp-bridge-probe.d.ts +2 -1
- package/dist/agent-executor/mcp-bridge-probe.js +1 -1
- package/dist/agent-executor/mcp-bridge.d.ts +9 -86
- package/dist/agent-executor/mcp-bridge.js +4 -355
- package/dist/ai-trace-lifecycle.d.ts +48 -0
- package/dist/ai-trace-lifecycle.js +116 -0
- package/dist/core/read-only-tools.js +3 -1
- package/dist/delegated-tool-bridge.js +19 -1
- package/dist/http/attachment-lookup-routes.d.ts +44 -0
- package/dist/http/attachment-lookup-routes.js +71 -0
- package/dist/http/attachments.d.ts +112 -7
- package/dist/http/attachments.js +58 -17
- package/dist/http/connectors.js +23 -18
- package/dist/http/index.d.ts +6 -0
- package/dist/http/index.js +3 -0
- package/dist/http/tool-execution-routes.d.ts +69 -0
- package/dist/http/tool-execution-routes.js +181 -0
- package/dist/http/xai.js +19 -9
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/remote-attachment-lookup.d.ts +29 -0
- package/dist/remote-attachment-lookup.js +71 -0
- package/dist/remote-tool-executor.d.ts +85 -0
- package/dist/remote-tool-executor.js +250 -0
- package/dist/routing-tool-executor.d.ts +29 -0
- package/dist/routing-tool-executor.js +16 -0
- package/dist/run-lifecycle.d.ts +4 -1
- package/dist/run-lifecycle.js +35 -5
- package/dist/store/event-log/sqlite-kernel.d.ts +18 -0
- package/dist/store/event-log/sqlite-kernel.js +135 -0
- package/dist/store/event-log/sqlite-policy.d.ts +93 -0
- package/dist/store/event-log/sqlite-policy.js +74 -0
- package/dist/store/event-log/sqlite.d.ts +7 -31
- package/dist/store/event-log/sqlite.js +32 -95
- package/dist/tool-recovery/analytics.d.ts +27 -0
- package/dist/tool-recovery/analytics.js +38 -0
- package/dist/tool-recovery/index.d.ts +2 -0
- package/dist/tool-recovery/index.js +2 -0
- package/dist/tool-recovery/observability.d.ts +82 -0
- package/dist/tool-recovery/observability.js +88 -0
- 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
|