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.
@@ -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 { AgentProfile, AgentProfileBase, ProvisioningHookSet, ProvisioningMcpServer, SpawnProvider } from "agent-relay-sdk";
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
- sharedListener?: { url: string };
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 provisionedMcp = profileProvisioning(config).mcpServers;
433
- const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
434
- const relayEndpoint = profileAllowsRelayFeature(config, "mcp");
590
+ const { declaredMcp, hostMcp, relayEndpoint, strictMcp, mcpServers, projectServers, hostMcpServers, sharedListenerUrl } =
591
+ resolveClaudeDeclaredMcp(config);
435
592
 
436
- // #667project `.mcp.json` projection (opt-in). The spawn cwd's committed servers are
437
- // injected through the SAME `--mcp-config` channel as relay-provisioned servers, so they
438
- // survive `--strict-mcp-config` without the ambient project-discovery modal. A relay-
439
- // provisioned server of the same name wins (the profile is the explicit, relay-owned
440
- // source); the relay endpoint name is reserved. `declaredMcp` is the union relay injects.
441
- const projectMcpRaw = resolveProjectMcpServers(config);
442
- const projectMcp = Object.fromEntries(
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
+ // #1292a 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 provisionedMcp = profileProvisioning(config).mcpServers;
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
- ...codexProvisionedMcpArgs(declaredMcp),
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
- if (managedHookSettings) writeClaudeSettingsJson(configHome, managedHookSettings);
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
  /**