agent-relay-runner 0.129.11 → 0.129.13
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/package.json +3 -3
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +8 -8
- package/src/adapter.ts +113 -11
- package/src/adapters/claude-delivery.ts +1 -7
- package/src/adapters/claude-exit.ts +289 -0
- package/src/adapters/claude-prompt-gates.ts +59 -79
- package/src/adapters/claude-session-capture.ts +13 -2
- package/src/adapters/claude-session-probe.ts +98 -7
- package/src/adapters/claude-shutdown.ts +149 -0
- package/src/adapters/claude-status-detectors.ts +201 -69
- package/src/adapters/claude-tmux.ts +94 -5
- package/src/adapters/claude-transcript-tail.ts +22 -3
- package/src/adapters/claude-transcript.ts +95 -1
- package/src/adapters/claude.ts +179 -76
- package/src/adapters/codex.ts +46 -8
- package/src/busy-reconciler.ts +67 -29
- package/src/config.ts +16 -0
- package/src/launch-assembly.ts +251 -82
- package/src/native-memory-lever.ts +308 -0
- package/src/outbox.ts +22 -4
- package/src/profile-projection.ts +45 -1
- package/src/provider-input-queue.ts +100 -0
- package/src/rate-limit.ts +106 -36
- package/src/relay-injection-events.ts +24 -0
- package/src/relay-instructions.ts +9 -0
- package/src/relay-mcp-proxy.ts +79 -8
- package/src/relay-mcp.ts +217 -8
- package/src/runner-core.ts +323 -47
- package/src/runner-helpers.ts +10 -0
- package/src/transcript-context.ts +83 -0
- package/src/turn-reconcile-event.ts +48 -0
package/src/launch-assembly.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve, join } from "node:path";
|
|
3
|
-
import type
|
|
3
|
+
import { isCodexLeanWorkerProfile, type AgentProfile, type AgentProfileBase, type ProvisioningHookSet, type ProvisioningMcpServer, type SpawnProvider } from "agent-relay-sdk";
|
|
4
4
|
import { shellQuote } from "agent-relay-sdk/shell-utils";
|
|
5
5
|
import {
|
|
6
6
|
profileAllowsRelayFeature,
|
|
@@ -35,15 +35,39 @@ import {
|
|
|
35
35
|
writeClaudeSettingsJson,
|
|
36
36
|
} from "./provisioning";
|
|
37
37
|
import {
|
|
38
|
+
claudeMcpConfigPayload,
|
|
38
39
|
claudeMcpLaunchArgs,
|
|
39
40
|
codexProvisionedMcpArgs,
|
|
41
|
+
codexProvisionedMcpTimeoutArgs,
|
|
40
42
|
relayMcpCodexConfigArgs,
|
|
41
43
|
resolveSharedMcpUrl,
|
|
42
44
|
sharedMcpBridgeServer,
|
|
45
|
+
sharedMcpMetaOnlyServer,
|
|
46
|
+
writeSharedMcpMetaOnlyConfig,
|
|
47
|
+
SHARED_MCP_METAONLY_CONFIG_FILENAME,
|
|
48
|
+
type MetaOnlyRelayDownstream,
|
|
43
49
|
tomlString,
|
|
50
|
+
upsertTomlTable,
|
|
51
|
+
writeCodexMcpServersToml,
|
|
44
52
|
RELAY_MCP_SERVER_NAME,
|
|
45
53
|
SHARED_MCP_SERVER_NAME,
|
|
46
54
|
} from "./relay-mcp";
|
|
55
|
+
import {
|
|
56
|
+
applyNativeMemoryBelt,
|
|
57
|
+
claudeManagedNativeMemorySettings,
|
|
58
|
+
resolveClaudeNativeMemoryLever,
|
|
59
|
+
resolveNativeMemoryLever,
|
|
60
|
+
writeCodexFeaturesToml,
|
|
61
|
+
type NativeMemoryLever,
|
|
62
|
+
} from "./native-memory-lever";
|
|
63
|
+
|
|
64
|
+
// #1565 P5.7 — relay's inline `--settings` actually reaches this launch. Same gate
|
|
65
|
+
// `claudeSettingsArgs` applies below, passed to the lever so its applied/suppressed report cannot
|
|
66
|
+
// claim a settings key the merge then drops.
|
|
67
|
+
const callerSettingsMergeable = (argLists: string[][]): boolean => {
|
|
68
|
+
const caller = extractCallerSettings(argLists);
|
|
69
|
+
return !caller || Boolean(caller.settings);
|
|
70
|
+
};
|
|
47
71
|
|
|
48
72
|
// #557 — THE single launch-assembly contract. Given a resolved AgentProfile (+ its
|
|
49
73
|
// spawn-time ResolvedProvisioning), `assembleLaunch` produces ONE provider-neutral
|
|
@@ -111,8 +135,10 @@ export interface AssembledMcp {
|
|
|
111
135
|
* the same `--mcp-config` / `-c` channel so it survives strict. `url` is the listener the
|
|
112
136
|
* bridge connects to. Undefined when projection is OFF (the default). The bridge is opaque
|
|
113
137
|
* at assembly time — its server SET is owned by the listener config, not enumerated here.
|
|
114
|
-
* The identity-bearing relay endpoint is never on the shared listener (#215).
|
|
115
|
-
|
|
138
|
+
* The identity-bearing relay endpoint is never on the shared listener (#215). #1450 —
|
|
139
|
+
* `relayFronted` marks a metaOnly callmux that ALSO fronts the relay endpoint on-demand as a
|
|
140
|
+
* second downstream (via the runner proxy, so identity/#215 hold); the relay stays per-agent. */
|
|
141
|
+
sharedListener?: { url: string; metaOnly?: boolean; relayFronted?: boolean };
|
|
116
142
|
}
|
|
117
143
|
|
|
118
144
|
export interface AssembledLaunch {
|
|
@@ -148,6 +174,12 @@ export interface AssembledLaunch {
|
|
|
148
174
|
* consume hooks.json. Drives a LOUD projection warning so a baseline guard is never
|
|
149
175
|
* SILENTLY absent while the report says "applied". */
|
|
150
176
|
hooksSuppressed?: { reason: string; names: string[] };
|
|
177
|
+
/** #1565 P5.7 — the provider-NATIVE-memory lever this launch carries. `policy:"default"` means
|
|
178
|
+
* the profile declared nothing: no env/arg/settings key was emitted and the launch is
|
|
179
|
+
* byte-identical to a pre-P5.7 one. Any other policy renders EXPLICITLY in both directions.
|
|
180
|
+
* `suppressed` names a lever that was resolved but could not be delivered on this base — the
|
|
181
|
+
* `hooksSuppressed` anti-lie precedent (#1096), so a lever is never silently absent. */
|
|
182
|
+
nativeMemory: NativeMemoryLever;
|
|
151
183
|
/** #1096 review — Claude only: true when the caller's own `--settings` was folded into the
|
|
152
184
|
* relay-emitted `--settings` (relay hooks/projectSettings/statusLine merged in). The adapter
|
|
153
185
|
* must then STRIP the caller's `--settings` from the provider args so exactly one reaches the
|
|
@@ -235,12 +267,16 @@ function claudeSettingsArgs(
|
|
|
235
267
|
provisionedHooks: ProvisioningHookSet,
|
|
236
268
|
hookNames: string[],
|
|
237
269
|
includeStatusLine: boolean,
|
|
270
|
+
nativeMemorySettings: Record<string, unknown>,
|
|
238
271
|
...argLists: string[][]
|
|
239
272
|
): ClaudeSettingsResolution {
|
|
240
273
|
const relaySettings: Record<string, unknown> = {
|
|
241
274
|
...projectSettings,
|
|
242
275
|
...(Object.keys(provisionedHooks).length ? { hooks: provisionedHooks } : {}),
|
|
243
276
|
...(includeStatusLine ? relayStatusLineSettings() : {}),
|
|
277
|
+
// #1565 P5.7 — LAST, so a project `.claude/settings.json` cannot quietly re-enable a
|
|
278
|
+
// profile's native memory. (Claude's own env predicate outranks all of this either way.)
|
|
279
|
+
...nativeMemorySettings,
|
|
244
280
|
};
|
|
245
281
|
const hasProvisionedHooks = Object.keys(provisionedHooks).length > 0;
|
|
246
282
|
const caller = extractCallerSettings(argLists);
|
|
@@ -374,6 +410,123 @@ function assembleInstructions(config: RunnerSpawnConfig): AssembledInstructions
|
|
|
374
410
|
};
|
|
375
411
|
}
|
|
376
412
|
|
|
413
|
+
// ── Per-provider MCP resolution (shared by assemble + materialize) ─────────────────
|
|
414
|
+
|
|
415
|
+
interface ClaudeMcpResolution {
|
|
416
|
+
declaredMcp: Record<string, ProvisioningMcpServer>;
|
|
417
|
+
hostMcp: Record<string, ProvisioningMcpServer>;
|
|
418
|
+
relayEndpoint: boolean;
|
|
419
|
+
strictMcp: boolean;
|
|
420
|
+
mcpServers: string[];
|
|
421
|
+
projectServers: string[];
|
|
422
|
+
hostMcpServers: string[];
|
|
423
|
+
sharedListenerUrl?: string;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// #557/#1292 — a PURE function of `config`, called independently from both `assembleClaude`
|
|
427
|
+
// (decides args/paths) and `materializeClaudeAssets` (writes the mcp-config.json the args
|
|
428
|
+
// reference). Both calls MUST derive the identical `declaredMcp`/`hostMcp` set — a
|
|
429
|
+
// mismatch would mean the launched `--mcp-config <path>` points at a file that doesn't
|
|
430
|
+
// carry what was assembled/reported.
|
|
431
|
+
function resolveClaudeDeclaredMcp(config: RunnerSpawnConfig): ClaudeMcpResolution {
|
|
432
|
+
const provisionedMcp = profileProvisioning(config).mcpServers;
|
|
433
|
+
const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
|
|
434
|
+
const relayEndpoint = profileAllowsRelayFeature(config, "mcp");
|
|
435
|
+
|
|
436
|
+
const projectMcpRaw = resolveProjectMcpServers(config);
|
|
437
|
+
const projectMcp = Object.fromEntries(
|
|
438
|
+
Object.entries(projectMcpRaw).filter(
|
|
439
|
+
([name]) => name !== RELAY_MCP_SERVER_NAME && !(name in provisionedMcp),
|
|
440
|
+
),
|
|
441
|
+
);
|
|
442
|
+
const projectServers = Object.keys(projectMcp);
|
|
443
|
+
|
|
444
|
+
const sharedListenerOn = Boolean(config.agentProfile?.sharedMcp);
|
|
445
|
+
const sharedListenerUrl = sharedListenerOn ? resolveSharedMcpUrl(config.sharedMcpUrl) : undefined;
|
|
446
|
+
const sharedMcp: Record<string, ProvisioningMcpServer> = sharedListenerUrl
|
|
447
|
+
? { [SHARED_MCP_SERVER_NAME]: sharedMcpBridgeServer(sharedListenerUrl, config.cwd) }
|
|
448
|
+
: {};
|
|
449
|
+
const declaredMcp = { ...projectMcp, ...provisionedMcp, ...sharedMcp };
|
|
450
|
+
|
|
451
|
+
const strictMcp = claudeLaunchPromptGateSettings(config).strictMcpConfig;
|
|
452
|
+
const usesHostMcp = profileUsesProviderHostGlobals(config)
|
|
453
|
+
&& (!config.agentProfile || config.agentProfile.mcp.mode === "host");
|
|
454
|
+
const hostMcp = usesHostMcp ? hostUserScopeMcpServers() : {};
|
|
455
|
+
const hostMcpServers = Object.keys(hostMcp).filter(
|
|
456
|
+
(name) => name !== RELAY_MCP_SERVER_NAME && !(name in declaredMcp),
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
return { declaredMcp, hostMcp, relayEndpoint, strictMcp, mcpServers, projectServers, hostMcpServers, sharedListenerUrl };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
interface CodexMcpResolution {
|
|
463
|
+
declaredMcp: Record<string, ProvisioningMcpServer>;
|
|
464
|
+
mcpServers: string[];
|
|
465
|
+
projectServers: string[];
|
|
466
|
+
sharedListenerUrl?: string;
|
|
467
|
+
/** #1575 — set when the shared-listener route is served through a per-agent metaOnly callmux
|
|
468
|
+
* (Codex worker) rather than the plain flat bridge. Carries the exact config path/url/cwd so
|
|
469
|
+
* materializeCodexAssets writes the SAME file assembleCodex's descriptor points at. #1450 —
|
|
470
|
+
* `relay` adds the identity-bearing relay endpoint as a SECOND downstream (via the runner proxy)
|
|
471
|
+
* when the proxy is active, so the on-demand relay set loads through callmux_call. */
|
|
472
|
+
metaOnlyCallmux?: { configPath: string; url: string; cwd: string; relay?: MetaOnlyRelayDownstream };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// #1575 — a Codex worker swaps the flat shared-listener bridge for a per-agent metaOnly callmux so
|
|
476
|
+
// the ~40 downstream schemas load on demand. Gated exactly like phase C's relay allowlist (#1570):
|
|
477
|
+
// an explicitly worker-classified profile AND a provider whose manifest eager-loads every tool
|
|
478
|
+
// schema (no client-native deferral). Data-driven from the manifest, never a provider-identity
|
|
479
|
+
// branch — Claude (client-defers) never takes this path, coordinators/reviewers keep the flat
|
|
480
|
+
// bridge. Requires a relay-owned provisioning home to materialize the per-agent config into (never
|
|
481
|
+
// the host ~/.codex); worker profiles are isolated, so this holds.
|
|
482
|
+
function codexMetaOnlyCallmuxConfigPath(config: RunnerSpawnConfig): string | undefined {
|
|
483
|
+
if (!isCodexLeanWorkerProfile(config.agentProfile?.name)) return undefined;
|
|
484
|
+
if (getManifest("codex")?.mcpToolManifest?.clientNativeToolDeferral !== false) return undefined;
|
|
485
|
+
const home = providerProvisioningHomePathFor("codex", config);
|
|
486
|
+
return home ? join(home, SHARED_MCP_METAONLY_CONFIG_FILENAME) : undefined;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Codex counterpart to resolveClaudeDeclaredMcp — same purity/consistency contract, shared
|
|
490
|
+
// by assembleCodex and materializeCodexAssets.
|
|
491
|
+
function resolveCodexDeclaredMcp(config: RunnerSpawnConfig): CodexMcpResolution {
|
|
492
|
+
const provisionedMcp = profileProvisioning(config).mcpServers;
|
|
493
|
+
const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
|
|
494
|
+
|
|
495
|
+
const projectMcpRaw = resolveProjectMcpServers(config);
|
|
496
|
+
const projectMcp = Object.fromEntries(
|
|
497
|
+
Object.entries(projectMcpRaw).filter(
|
|
498
|
+
([name]) => name !== RELAY_MCP_SERVER_NAME && !(name in provisionedMcp),
|
|
499
|
+
),
|
|
500
|
+
);
|
|
501
|
+
const projectServers = Object.keys(projectMcp);
|
|
502
|
+
|
|
503
|
+
const sharedListenerOn = Boolean(config.agentProfile?.sharedMcp);
|
|
504
|
+
const sharedListenerUrl = sharedListenerOn ? resolveSharedMcpUrl(config.sharedMcpUrl) : undefined;
|
|
505
|
+
const metaOnlyConfigPath = sharedListenerUrl ? codexMetaOnlyCallmuxConfigPath(config) : undefined;
|
|
506
|
+
// #1450 — front the relay endpoint on-demand through the SAME metaOnly callmux, but ONLY when the
|
|
507
|
+
// runner proxy is active (config.relayMcpEndpoint = the proxy URL, config.mcpProxySecret = its
|
|
508
|
+
// secret) AND the profile has the relay endpoint on. Routing through the proxy keeps the live
|
|
509
|
+
// token off disk (#215). This gate matches runner-core's proxy on-demand-hiding gate exactly, so
|
|
510
|
+
// the direct client's on-demand tools are hidden ONLY when this front simultaneously exists.
|
|
511
|
+
const metaOnlyRelay: MetaOnlyRelayDownstream | undefined =
|
|
512
|
+
metaOnlyConfigPath && profileAllowsRelayFeature(config, "mcp") && config.relayMcpEndpoint && config.mcpProxySecret
|
|
513
|
+
? { endpoint: config.relayMcpEndpoint, authSecret: config.mcpProxySecret }
|
|
514
|
+
: undefined;
|
|
515
|
+
const metaOnlyCallmux = sharedListenerUrl && metaOnlyConfigPath
|
|
516
|
+
? { configPath: metaOnlyConfigPath, url: sharedListenerUrl, cwd: config.cwd, ...(metaOnlyRelay ? { relay: metaOnlyRelay } : {}) }
|
|
517
|
+
: undefined;
|
|
518
|
+
const sharedMcp: Record<string, ProvisioningMcpServer> = sharedListenerUrl
|
|
519
|
+
? {
|
|
520
|
+
[SHARED_MCP_SERVER_NAME]: metaOnlyCallmux
|
|
521
|
+
? sharedMcpMetaOnlyServer(metaOnlyCallmux.configPath)
|
|
522
|
+
: sharedMcpBridgeServer(sharedListenerUrl, config.cwd),
|
|
523
|
+
}
|
|
524
|
+
: {};
|
|
525
|
+
const declaredMcp = { ...projectMcp, ...provisionedMcp, ...sharedMcp };
|
|
526
|
+
|
|
527
|
+
return { declaredMcp, mcpServers, projectServers, sharedListenerUrl, ...(metaOnlyCallmux ? { metaOnlyCallmux } : {}) };
|
|
528
|
+
}
|
|
529
|
+
|
|
377
530
|
// ── Per-provider arg assembly ──────────────────────────────────────────────────────
|
|
378
531
|
|
|
379
532
|
function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfig, hostDefaultArgs: string[]): AssembledLaunch {
|
|
@@ -403,11 +556,16 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
403
556
|
// and caller-`--settings` launches keep inline delivery (see claudeManagedHookSettings).
|
|
404
557
|
const managedHookSettings = claudeManagedHookSettings(config, configHome);
|
|
405
558
|
const inlineHookSet = managedHookSettings ? {} : provisionedHookSet;
|
|
559
|
+
// #1565 P5.7 — the native-memory lever. Resolved BEFORE the settings args so its keys can ride
|
|
560
|
+
// relay's inline `--settings` (the only settings channel a host-base launch has); the env var
|
|
561
|
+
// below is the one that must always be present, and outranks both settings channels.
|
|
562
|
+
const nativeMemory = resolveClaudeNativeMemoryLever(config, configHome, hostDefaultArgs, callerSettingsMergeable);
|
|
406
563
|
const settings = claudeSettingsArgs(
|
|
407
564
|
projectSettings.settings,
|
|
408
565
|
inlineHookSet,
|
|
409
566
|
provisionedHooks.map((h) => h.name),
|
|
410
567
|
statusLine,
|
|
568
|
+
nativeMemory.settings,
|
|
411
569
|
hostDefaultArgs,
|
|
412
570
|
config.providerArgs,
|
|
413
571
|
);
|
|
@@ -429,56 +587,16 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
429
587
|
const hostPluginDirs = profileAllowsHostAssets(config, "plugins") ? providerConfig.pluginDirs : [];
|
|
430
588
|
const pluginDirs = [...new Set([...relayPluginDirs, ...provisionedPluginDirs, ...hostPluginDirs])];
|
|
431
589
|
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
const relayEndpoint = profileAllowsRelayFeature(config, "mcp");
|
|
590
|
+
const { declaredMcp, hostMcp, relayEndpoint, strictMcp, mcpServers, projectServers, hostMcpServers, sharedListenerUrl } =
|
|
591
|
+
resolveClaudeDeclaredMcp(config);
|
|
435
592
|
|
|
436
|
-
// #
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
Object.entries(projectMcpRaw).filter(
|
|
444
|
-
([name]) => name !== RELAY_MCP_SERVER_NAME && !(name in provisionedMcp),
|
|
445
|
-
),
|
|
446
|
-
);
|
|
447
|
-
const projectServers = Object.keys(projectMcp);
|
|
448
|
-
|
|
449
|
-
// #672 — shared host-listener MCP (opt-in, default OFF). When on, route the agent's
|
|
450
|
-
// NON-IDENTITY MCP (tokenlean, github) through ONE orchestrator-owned shared `callmux`
|
|
451
|
-
// listener via a per-agent `callmux bridge --url <listener> --cwd "$WORKTREE"` stdio entry:
|
|
452
|
-
// the MCP session survives a listener/downstream restart, and --cwd gives each isolated
|
|
453
|
-
// worktree session-cwd isolation. It's a plain stdio server descriptor, so it rides the same
|
|
454
|
-
// controlled --mcp-config channel as provisioned servers (survives --strict-mcp-config) — no
|
|
455
|
-
// ambient discovery, no modal. Relay-owned infra → authoritative on its name (spread last);
|
|
456
|
-
// the relay endpoint stays per-agent (#215), NEVER on the shared listener.
|
|
457
|
-
const sharedListenerOn = Boolean(config.agentProfile?.sharedMcp);
|
|
458
|
-
const sharedListenerUrl = sharedListenerOn ? resolveSharedMcpUrl(config.sharedMcpUrl) : undefined;
|
|
459
|
-
const sharedMcp: Record<string, ProvisioningMcpServer> = sharedListenerUrl
|
|
460
|
-
? { [SHARED_MCP_SERVER_NAME]: sharedMcpBridgeServer(sharedListenerUrl, config.cwd) }
|
|
461
|
-
: {};
|
|
462
|
-
const declaredMcp = { ...projectMcp, ...provisionedMcp, ...sharedMcp };
|
|
463
|
-
|
|
464
|
-
// #662/#663 — strict controlled MCP is a Claude prompt-gate prevention entry, not a
|
|
465
|
-
// launch-assembly special case. It prevents the blocking "N new MCP servers found in
|
|
466
|
-
// this project" modal while preserving every relay-declared server in --mcp-config.
|
|
467
|
-
const strictMcp = claudeLaunchPromptGateSettings(config).strictMcpConfig;
|
|
468
|
-
|
|
469
|
-
// #662 follow-up — strict drops ALL on-disk config, including the host user-scope
|
|
470
|
-
// `~/.claude.json` MCP that a host-base agent loaded ambiently before strict. That
|
|
471
|
-
// over-reached: it stopped honoring `mcp.mode:"host"`, so default-relay workers/reviewers
|
|
472
|
-
// lost callmux/qmd/gh. Re-inject the host USER-scope servers (NOT the project `.mcp.json`,
|
|
473
|
-
// still excluded) when the launch both uses the host config home AND its mcp mode is host.
|
|
474
|
-
// An isolated/minimal base has its own config home and never had the real host's user MCP,
|
|
475
|
-
// so it stays relay+provisioned only even if a profile sets mode:"host".
|
|
476
|
-
const usesHostMcp = profileUsesProviderHostGlobals(config)
|
|
477
|
-
&& (!config.agentProfile || config.agentProfile.mcp.mode === "host");
|
|
478
|
-
const hostMcp = usesHostMcp ? hostUserScopeMcpServers() : {};
|
|
479
|
-
const hostMcpServers = Object.keys(hostMcp).filter(
|
|
480
|
-
(name) => name !== RELAY_MCP_SERVER_NAME && !(name in declaredMcp),
|
|
481
|
-
);
|
|
593
|
+
// #1292 — a provisioned/host server's env/headers can carry a literal secret; inline JSON
|
|
594
|
+
// in --mcp-config lands verbatim in argv (world-readable via ps/proc). Whenever there's a
|
|
595
|
+
// relay-owned scratch dir to write into (any launch with an agentProfile — see
|
|
596
|
+
// providerProvisioningHomePathFor, which gives even host-base profiles a relay-owned dir
|
|
597
|
+
// distinct from the real ~/.claude), route through a 0600 file instead. materializeClaudeAssets
|
|
598
|
+
// writes the file this path points at, from the SAME resolveClaudeDeclaredMcp(config) inputs.
|
|
599
|
+
const mcpConfigFilePath = assetHome ? join(assetHome, "mcp-config.json") : undefined;
|
|
482
600
|
|
|
483
601
|
const instructions = assembleInstructions(config);
|
|
484
602
|
|
|
@@ -494,6 +612,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
494
612
|
servers: declaredMcp,
|
|
495
613
|
hostServers: hostMcp,
|
|
496
614
|
strict: strictMcp,
|
|
615
|
+
...(mcpConfigFilePath ? { configFilePath: mcpConfigFilePath } : {}),
|
|
497
616
|
}),
|
|
498
617
|
...settings.args,
|
|
499
618
|
...(config.systemPromptAppend ? ["--append-system-prompt", config.systemPromptAppend] : []),
|
|
@@ -520,6 +639,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
520
639
|
},
|
|
521
640
|
hooks: deliveredHooks.map((h) => ({ name: h.name, dir: h.dir })),
|
|
522
641
|
...(hooksSuppressed ? { hooksSuppressed } : {}),
|
|
642
|
+
nativeMemory,
|
|
523
643
|
callerSettingsConsumed: settings.callerSettingsConsumed,
|
|
524
644
|
plugins: provisionedPlugins.map((p) => ({ name: p.name, dir: p.dir })),
|
|
525
645
|
hostPluginDirs,
|
|
@@ -529,7 +649,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
529
649
|
},
|
|
530
650
|
instructions,
|
|
531
651
|
args,
|
|
532
|
-
env: configHomeEnv("claude", configHome),
|
|
652
|
+
env: { ...configHomeEnv("claude", configHome), ...nativeMemory.env },
|
|
533
653
|
};
|
|
534
654
|
}
|
|
535
655
|
|
|
@@ -560,41 +680,33 @@ function assembleCodex(config: RunnerSpawnConfig, providerConfig: ProviderConfig
|
|
|
560
680
|
const bundledSkillDirs = !config.agentProfile && relaySkills ? bundledCodexSkillDirs() : [];
|
|
561
681
|
const skillDirs = [...bundledSkillDirs, ...provisionedSkills.map((s) => s.dir)];
|
|
562
682
|
|
|
563
|
-
const
|
|
564
|
-
const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
|
|
565
|
-
|
|
566
|
-
// #667 — project `.mcp.json` projection (opt-in), same union rule as Claude: a relay-
|
|
567
|
-
// provisioned server of the same name wins; the relay endpoint name is reserved. Codex has
|
|
568
|
-
// no `.mcp.json` discovery of its own, so this is purely additive (no strict/modal concern).
|
|
569
|
-
const projectMcpRaw = resolveProjectMcpServers(config);
|
|
570
|
-
const projectMcp = Object.fromEntries(
|
|
571
|
-
Object.entries(projectMcpRaw).filter(
|
|
572
|
-
([name]) => name !== RELAY_MCP_SERVER_NAME && !(name in provisionedMcp),
|
|
573
|
-
),
|
|
574
|
-
);
|
|
575
|
-
const projectServers = Object.keys(projectMcp);
|
|
576
|
-
|
|
577
|
-
// #672 — shared host-listener MCP (opt-in, default OFF), same as Claude: a per-agent
|
|
578
|
-
// `callmux bridge --url <listener> --cwd "$WORKTREE"` stdio entry routes the agent's
|
|
579
|
-
// NON-IDENTITY MCP (tokenlean, github) through the shared listener (session survives a
|
|
580
|
-
// restart; --cwd gives session-cwd isolation). Emitted via the same `-c mcp_servers.*`
|
|
581
|
-
// overrides as provisioned servers. The relay endpoint stays per-agent (#215).
|
|
582
|
-
const sharedListenerOn = Boolean(config.agentProfile?.sharedMcp);
|
|
583
|
-
const sharedListenerUrl = sharedListenerOn ? resolveSharedMcpUrl(config.sharedMcpUrl) : undefined;
|
|
584
|
-
const sharedMcp: Record<string, ProvisioningMcpServer> = sharedListenerUrl
|
|
585
|
-
? { [SHARED_MCP_SERVER_NAME]: sharedMcpBridgeServer(sharedListenerUrl, config.cwd) }
|
|
586
|
-
: {};
|
|
587
|
-
const declaredMcp = { ...projectMcp, ...provisionedMcp, ...sharedMcp };
|
|
683
|
+
const { declaredMcp, mcpServers, projectServers, sharedListenerUrl, metaOnlyCallmux } = resolveCodexDeclaredMcp(config);
|
|
588
684
|
|
|
589
685
|
const instructions = assembleInstructions(config);
|
|
590
686
|
|
|
687
|
+
// #1292 — a provisioned/project server's env can carry a literal secret; `-c
|
|
688
|
+
// mcp_servers.<name>.env.<K>=<V>` lands verbatim in argv (world-readable via ps/proc).
|
|
689
|
+
// An ISOLATED CODEX_HOME is a relay-owned per-instance dir Codex actually reads as its
|
|
690
|
+
// config home, so route command/args/url/env through CODEX_HOME/config.toml instead
|
|
691
|
+
// (materializeCodexAssets writes it) and keep only the non-secret timeout overrides in
|
|
692
|
+
// argv. Host-base Codex has no isolated CODEX_HOME to write into (it reuses the real
|
|
693
|
+
// ~/.codex, which relay must not persist provisioned secrets into) — same accepted gap as
|
|
694
|
+
// the hooksSuppressed carve-out above, unchanged from prior behavior.
|
|
695
|
+
const mcpArgs = configHome ? codexProvisionedMcpTimeoutArgs(declaredMcp) : codexProvisionedMcpArgs(declaredMcp);
|
|
696
|
+
|
|
591
697
|
// The skills/MCP/project-doc block; core launch config (model/approval/listen) stays
|
|
592
698
|
// in the adapter. -c overrides are order-independent, so the assembled block is contiguous.
|
|
699
|
+
// #1565 P5.7 — Codex's native-memory lever rides the same `-c` argv precedent as the
|
|
700
|
+
// project-doc suppressor above it (`-c project_doc_max_bytes=0`). Argv outranks config.toml, so
|
|
701
|
+
// this is the portable/authoritative half; the isolated CODEX_HOME config.toml is the belt.
|
|
702
|
+
const nativeMemory = resolveNativeMemoryLever({ provider: "codex", config, isolatedHome: Boolean(configHome) });
|
|
703
|
+
|
|
593
704
|
const args = [
|
|
594
705
|
...bundledSkillConfigArgs(skillDirs),
|
|
595
706
|
...(relayEndpoint ? relayMcpCodexConfigArgs(config.relayUrl, config.relayMcpEndpoint) : []),
|
|
596
|
-
...
|
|
707
|
+
...mcpArgs,
|
|
597
708
|
...codexProjectDocSuppressionArgs(config),
|
|
709
|
+
...nativeMemory.args,
|
|
598
710
|
];
|
|
599
711
|
|
|
600
712
|
return {
|
|
@@ -613,12 +725,13 @@ function assembleCodex(config: RunnerSpawnConfig, providerConfig: ProviderConfig
|
|
|
613
725
|
projectSettings: { applied: false, keys: [], ignoredKeys: [] },
|
|
614
726
|
hooks: provisionedHooks.map((h) => ({ name: h.name, dir: h.dir })),
|
|
615
727
|
...(hooksSuppressed ? { hooksSuppressed } : {}),
|
|
728
|
+
nativeMemory,
|
|
616
729
|
callerSettingsConsumed: false,
|
|
617
730
|
plugins: [],
|
|
618
731
|
hostPluginDirs: [],
|
|
619
732
|
mcp: {
|
|
620
733
|
relayEndpoint, strict: false, servers: mcpServers, projectServers, hostServers: [],
|
|
621
|
-
...(sharedListenerUrl ? { sharedListener: { url: sharedListenerUrl } } : {}),
|
|
734
|
+
...(sharedListenerUrl ? { sharedListener: { url: sharedListenerUrl, ...(metaOnlyCallmux ? { metaOnly: true } : {}), ...(metaOnlyCallmux?.relay ? { relayFronted: true } : {}) } } : {}),
|
|
622
735
|
},
|
|
623
736
|
instructions,
|
|
624
737
|
args,
|
|
@@ -660,11 +773,44 @@ function materializeClaudeAssets(config: RunnerSpawnConfig): void {
|
|
|
660
773
|
materializeResolvedAssets(resolveProvisionedHooks(configHome, config));
|
|
661
774
|
// #1096 — land the provisioned hooks in the managed home's settings.json (the documented
|
|
662
775
|
// hook source), matching the assembler's decision to keep them out of the inline --settings.
|
|
776
|
+
// #1565 P5.7 — the native-memory keys ride the SAME file (the isolated-home belt behind the
|
|
777
|
+
// env lever). Composed here rather than written separately so one write owns the file.
|
|
663
778
|
const managedHookSettings = claudeManagedHookSettings(config, configHome);
|
|
664
|
-
|
|
779
|
+
const managedSettings: Record<string, unknown> = {
|
|
780
|
+
...(managedHookSettings ?? {}),
|
|
781
|
+
...claudeManagedNativeMemorySettings(config, configHome, callerSettingsMergeable),
|
|
782
|
+
};
|
|
783
|
+
if (Object.keys(managedSettings).length) {
|
|
784
|
+
// The hook half of this file is NOT best-effort (a missing baseline guard is a real
|
|
785
|
+
// regression, #1096), so only the native-memory contribution is guarded: on a write
|
|
786
|
+
// failure, retry with the hooks alone so P5.7 can never cost a launch its safety hooks.
|
|
787
|
+
if (applyNativeMemoryBelt("the Claude managed settings.json", () => writeClaudeSettingsJson(configHome, managedSettings)) && managedHookSettings) {
|
|
788
|
+
writeClaudeSettingsJson(configHome, managedHookSettings);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
665
791
|
}
|
|
666
792
|
if (assetHome) materializeResolvedAssets(resolveClaudeProvisionedPlugins(assetHome, config));
|
|
667
793
|
if (!configHome && assetHome) materializeResolvedAssets(resolveProvisionedHooks(assetHome, config));
|
|
794
|
+
// #1292 — write the file assembleClaude's --mcp-config <path> points at, from the SAME
|
|
795
|
+
// resolveClaudeDeclaredMcp(config) inputs (see the purity/consistency note there). Keeps
|
|
796
|
+
// provisioned/host-server env/headers secrets out of argv.
|
|
797
|
+
if (assetHome) {
|
|
798
|
+
const { declaredMcp, hostMcp, relayEndpoint, strictMcp } = resolveClaudeDeclaredMcp(config);
|
|
799
|
+
const mcpServers = claudeMcpConfigPayload({
|
|
800
|
+
relayUrl: config.relayUrl,
|
|
801
|
+
...(config.relayMcpEndpoint ? { endpoint: config.relayMcpEndpoint } : {}),
|
|
802
|
+
includeRelay: relayEndpoint,
|
|
803
|
+
servers: declaredMcp,
|
|
804
|
+
hostServers: hostMcp,
|
|
805
|
+
});
|
|
806
|
+
const mcpConfigPath = join(assetHome, "mcp-config.json");
|
|
807
|
+
if (Object.keys(mcpServers).length || strictMcp) {
|
|
808
|
+
mkdirSync(assetHome, { recursive: true });
|
|
809
|
+
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), { mode: 0o600 });
|
|
810
|
+
} else {
|
|
811
|
+
rmSync(mcpConfigPath, { force: true });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
668
814
|
}
|
|
669
815
|
|
|
670
816
|
function materializeCodexAssets(config: RunnerSpawnConfig): void {
|
|
@@ -678,6 +824,29 @@ function materializeCodexAssets(config: RunnerSpawnConfig): void {
|
|
|
678
824
|
materializeResolvedAssets(hooks);
|
|
679
825
|
writeCodexHooksJson(configHome, renderProvisionedHookSet(hooks));
|
|
680
826
|
}
|
|
827
|
+
// #1292 — write the CODEX_HOME/config.toml sections assembleCodex's non-secret -c
|
|
828
|
+
// timeout-only overrides expect to find command/args/url/env in (see the mcpArgs branch
|
|
829
|
+
// there). Same resolveCodexDeclaredMcp(config) inputs, so disk == launch. No-op for
|
|
830
|
+
// host-base (no isolated CODEX_HOME) — assembleCodex falls back to the unchanged
|
|
831
|
+
// codexProvisionedMcpArgs(...) argv path for that case instead.
|
|
832
|
+
const { declaredMcp, metaOnlyCallmux } = resolveCodexDeclaredMcp(config);
|
|
833
|
+
if (configHome) {
|
|
834
|
+
writeCodexMcpServersToml(configHome, declaredMcp);
|
|
835
|
+
// #1565 P5.7 — the isolated-home belt behind the `-c features.memories=…` argv lever. Same
|
|
836
|
+
// resolveNativeMemoryLever inputs assembleCodex used, so disk == launch. No-op for host base
|
|
837
|
+
// (no isolated CODEX_HOME; relay must not write the real ~/.codex) and for an undeclared
|
|
838
|
+
// profile (`configToml` is empty, so nothing is written). Guarded: never fails the spawn.
|
|
839
|
+
applyNativeMemoryBelt("the Codex CODEX_HOME/config.toml [features] table", () => {
|
|
840
|
+
writeCodexFeaturesToml(configHome, resolveNativeMemoryLever({ provider: "codex", config, isolatedHome: true }).configToml);
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
// #1575/#1450 — write the per-agent metaOnly config the shared-listener descriptor points at
|
|
844
|
+
// (`callmux --config <path>`), from the SAME resolveCodexDeclaredMcp inputs so disk == launch.
|
|
845
|
+
// The optional relay downstream (#1450) rides here too, so the on-demand relay front and the
|
|
846
|
+
// agent's stdio callmux descriptor come from one resolution.
|
|
847
|
+
if (metaOnlyCallmux) {
|
|
848
|
+
writeSharedMcpMetaOnlyConfig(metaOnlyCallmux.configPath, metaOnlyCallmux.url, metaOnlyCallmux.cwd, metaOnlyCallmux.relay);
|
|
849
|
+
}
|
|
681
850
|
}
|
|
682
851
|
|
|
683
852
|
/**
|