@byok-sdk/client 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +71 -3
  2. package/dist/adapters/claude/claude-adapter.d.ts +3 -2
  3. package/dist/adapters/claude/permission-mapping.d.ts +48 -1
  4. package/dist/adapters/claude/resolve-approval-mcp-bin.d.ts +3 -2
  5. package/dist/adapters/codex/permission-mapping.d.ts +16 -0
  6. package/dist/adapters/index.js +292 -23
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/adapters/mcp-tool-grants.d.ts +36 -0
  9. package/dist/adapters/pi/resolve-extensions.d.ts +4 -1
  10. package/dist/adapters/pi/subagents-policy-config.d.ts +7 -0
  11. package/dist/adapters/pi/subagents-policy-extension.js +137 -0
  12. package/dist/adapters/pi/subagents-policy-extension.js.map +1 -0
  13. package/dist/agent-memory/index.d.ts +1 -1
  14. package/dist/agent-memory/index.js +4 -4
  15. package/dist/agent-memory/index.js.map +1 -1
  16. package/dist/bin/agent-memory-mcp-server.d.ts +2 -2
  17. package/dist/bin/agent-message-mcp-server.d.ts +0 -1
  18. package/dist/bin/byok-agent-memory-mcp.js +340 -7
  19. package/dist/bin/byok-agent-memory-mcp.js.map +1 -1
  20. package/dist/bin/byok-agent-message-mcp.js +331 -7
  21. package/dist/bin/byok-agent-message-mcp.js.map +1 -1
  22. package/dist/bin/byok-agent-team-mcp.d.ts +2 -0
  23. package/dist/bin/byok-agent-team-mcp.js +765 -0
  24. package/dist/bin/byok-agent-team-mcp.js.map +1 -0
  25. package/dist/bin/byok-agent.js +15889 -14610
  26. package/dist/bin/byok-agent.js.map +1 -1
  27. package/dist/bin/byok-approval-mcp.js +312 -45
  28. package/dist/bin/byok-approval-mcp.js.map +1 -1
  29. package/dist/bin/commands/team.d.ts +15 -0
  30. package/dist/bin/sdk-reserved-helper-runners.d.ts +2 -0
  31. package/dist/bin/team-mcp-server.d.ts +27 -0
  32. package/dist/bin/team-tmux-view.d.ts +19 -0
  33. package/dist/daemon/agent-message-mcp-preflight.d.ts +17 -0
  34. package/dist/daemon/control-protocol.d.ts +38 -0
  35. package/dist/daemon/create-daemon.d.ts +7 -0
  36. package/dist/daemon/mcp-tools-probe.d.ts +105 -0
  37. package/dist/daemon/resolve-agent-memory-mcp-bin.d.ts +2 -1
  38. package/dist/daemon/resolve-agent-message-mcp-bin.d.ts +2 -1
  39. package/dist/daemon/task-runner.d.ts +18 -1
  40. package/dist/daemon/team-workspace.d.ts +202 -0
  41. package/dist/daemon/toolset-registry.d.ts +0 -2
  42. package/dist/index.d.ts +4 -0
  43. package/dist/index.js +2682 -1090
  44. package/dist/index.js.map +1 -1
  45. package/dist/sdk-reserved-helper-host.d.ts +25 -0
  46. package/dist/sdk-reserved-mcp.d.ts +23 -0
  47. package/dist/types.d.ts +36 -0
  48. package/package.json +10 -5
@@ -67,6 +67,7 @@ function freezeRuntimeAdapterDescriptor(descriptor) {
67
67
  return Object.freeze({
68
68
  id: descriptor.id,
69
69
  supportsDispatchSelection: descriptor.supportsDispatchSelection === true,
70
+ requiresMcpToolsetToolObservation: descriptor.requiresMcpToolsetToolObservation === true,
70
71
  capabilities: Object.freeze({
71
72
  steer: descriptor.capabilities.steer === true,
72
73
  resume: descriptor.capabilities.resume === true,
@@ -143,12 +144,21 @@ function resolvePiExtensions() {
143
144
  const clientManifest = fileURLToPath(import.meta.resolve("@byok-sdk/client/package.json"));
144
145
  return {
145
146
  webAccess: fileURLToPath(import.meta.resolve("pi-web-access/index.ts")),
146
- mcpAdapter: path5.join(path5.dirname(clientManifest), "dist", "adapters", "pi", "mcp-extension.js")
147
+ mcpAdapter: path5.join(path5.dirname(clientManifest), "dist", "adapters", "pi", "mcp-extension.js"),
148
+ subagentsPolicy: path5.join(path5.dirname(clientManifest), "dist", "adapters", "pi", "subagents-policy-extension.js"),
149
+ subagents: fileURLToPath(import.meta.resolve("pi-subagents")),
150
+ // rpiv-todo publishes TypeScript source without a JavaScript `main`, so
151
+ // select the single authoritative Pi entry declared by its package metadata.
152
+ todo: fileURLToPath(import.meta.resolve("@juicesharp/rpiv-todo/index.ts"))
147
153
  };
148
154
  }
149
155
 
156
+ // src/adapters/pi/subagents-policy-config.ts
157
+ var BYOK_PI_PERMISSION_MODE = "BYOK_PI_PERMISSION_MODE";
158
+ var BYOK_PI_READONLY_PARENT_TOOLS = ["subagent", "todo"];
159
+
150
160
  // src/adapters/pi/permission-mapping.ts
151
- var READONLY_TOOLS = ["read", "grep", "find", "ls"];
161
+ var READONLY_TOOLS = ["read", "grep", "find", "ls", ...BYOK_PI_READONLY_PARENT_TOOLS];
152
162
  function mapPermissionPolicyToPiArgs(policy) {
153
163
  if (policy.network === false) {
154
164
  return {
@@ -970,6 +980,11 @@ var PiAdapter = class {
970
980
  descriptor = freezeRuntimeAdapterDescriptor({
971
981
  id: "pi",
972
982
  supportsDispatchSelection: true,
983
+ // `requiresMcpToolsetToolObservation` is deliberately absent: pi projects
984
+ // MCP servers through its own adapter extension and grants their tools
985
+ // itself, so it never reads `mcpToolsetTools`. Declaring the requirement
986
+ // would make every pi-routed toolset offer wait on a `tools/list` probe of
987
+ // every projected server for an observation nothing consumes.
973
988
  capabilities: {
974
989
  steer: true,
975
990
  resume: true,
@@ -1067,21 +1082,40 @@ var PiAdapter = class {
1067
1082
  let mcpConfigDir;
1068
1083
  let runtimeEnv = manifestSelection === void 0 ? startInput.env : withoutProviderCredentials(startInput.env);
1069
1084
  const taskMcpServers = startInput.mcpServers ?? {};
1070
- const hasMcpServers = Object.keys(taskMcpServers).length > 0;
1071
- if (hasMcpServers) {
1085
+ let mcpConfigPath;
1086
+ try {
1072
1087
  mcpConfigDir = await promises.mkdtemp(path5.join(os.tmpdir(), "byok-pi-mcp-"));
1073
1088
  await promises.chmod(mcpConfigDir, 448).catch(() => {
1074
1089
  });
1075
- const mcpConfigPath = path5.join(mcpConfigDir, "mcp-config.json");
1090
+ mcpConfigPath = path5.join(mcpConfigDir, "mcp-config.json");
1076
1091
  await promises.writeFile(mcpConfigPath, JSON.stringify({ mcpServers: taskMcpServers }), { mode: 384 });
1077
- runtimeEnv = { ...runtimeEnv, [BYOK_PI_MCP_CONFIG_PATH]: mcpConfigPath };
1092
+ } catch (cause) {
1093
+ await cleanupMcpConfigDir(mcpConfigDir);
1094
+ throw new RuntimeExecutionFailure({
1095
+ phase: "start",
1096
+ category: "infrastructure",
1097
+ retry: "retryable",
1098
+ reason: "pi task-scoped MCP configuration could not be created"
1099
+ }, { cause });
1078
1100
  }
1101
+ runtimeEnv = {
1102
+ ...runtimeEnv,
1103
+ [BYOK_PI_MCP_CONFIG_PATH]: mcpConfigPath,
1104
+ [BYOK_PI_PERMISSION_MODE]: input.policy.mode
1105
+ };
1079
1106
  const piArgs = [
1080
1107
  "--mode",
1081
1108
  "rpc",
1082
1109
  "--extension",
1083
1110
  extensions.webAccess,
1084
- ...hasMcpServers ? ["--extension", extensions.mcpAdapter] : [],
1111
+ "--extension",
1112
+ extensions.mcpAdapter,
1113
+ "--extension",
1114
+ extensions.subagentsPolicy,
1115
+ "--extension",
1116
+ extensions.subagents,
1117
+ "--extension",
1118
+ extensions.todo,
1085
1119
  ...resumeSessionId ? ["--session", resumeSessionId] : [],
1086
1120
  ...mapping.args
1087
1121
  ];
@@ -1298,18 +1332,79 @@ function resolveClaudeBin() {
1298
1332
  }
1299
1333
  return { command: "claude", source: "path" };
1300
1334
  }
1301
- function resolveApprovalMcpBin() {
1335
+
1336
+ // src/sdk-reserved-mcp.ts
1337
+ var AGENT_MESSAGE_MCP_SERVER_NAME = "byokagentmessage";
1338
+ var AGENT_MESSAGE_TOOL_NAME = "send_agent_message";
1339
+ var AGENT_MEMORY_MCP_SERVER_NAME = "byokagentmemory";
1340
+ var AGENT_TEAM_MCP_SERVER_NAME = "byokagentteam";
1341
+ var APPROVAL_MCP_SERVER_NAME = "byokapproval";
1342
+ var RESERVED_MCP_SERVER_NAMES = Object.freeze([
1343
+ AGENT_MESSAGE_MCP_SERVER_NAME,
1344
+ AGENT_MEMORY_MCP_SERVER_NAME,
1345
+ APPROVAL_MCP_SERVER_NAME,
1346
+ AGENT_TEAM_MCP_SERVER_NAME
1347
+ ]);
1348
+ function isReservedMcpServerName(name) {
1349
+ return RESERVED_MCP_SERVER_NAMES.includes(name);
1350
+ }
1351
+ var AGENT_MEMORY_RECALL_TOOL_NAME = "memory_recall";
1352
+ var AGENT_MEMORY_SAVE_TOOL_NAME = "memory_save";
1353
+ var APPROVAL_TOOL_NAME = "approval_prompt";
1354
+
1355
+ // src/sdk-reserved-helper-host.ts
1356
+ var BYOK_SDK_HELPER_SUBCOMMAND = "__byok_sdk_helper";
1357
+ var DIST_SCRIPT_BY_KIND = Object.freeze({
1358
+ "agent-message-mcp": "byok-agent-message-mcp.js",
1359
+ "agent-memory-mcp": "byok-agent-memory-mcp.js",
1360
+ "approval-mcp": "byok-approval-mcp.js",
1361
+ "agent-team-mcp": "byok-agent-team-mcp.js"
1362
+ });
1363
+ function assertExecutable(executable) {
1364
+ if (!path5.isAbsolute(executable) || /[\u0000\r\n]/u.test(executable)) {
1365
+ throw new Error("SdkHelperHostConfig.executable must be an absolute executable path");
1366
+ }
1367
+ }
1368
+ function resolveSdkReservedHelperBin(kind, host) {
1369
+ if (host !== void 0) {
1370
+ if (host.mode !== "self-executable") throw new Error(`unsupported SDK helper host mode: ${String(host.mode)}`);
1371
+ const executable = host.executable ?? process.execPath;
1372
+ assertExecutable(executable);
1373
+ return Object.freeze({
1374
+ command: executable,
1375
+ args: Object.freeze([BYOK_SDK_HELPER_SUBCOMMAND, kind]),
1376
+ source: "self-executable"
1377
+ });
1378
+ }
1379
+ const script = path5.join(
1380
+ path5.dirname(fileURLToPath(import.meta.url)),
1381
+ "bin",
1382
+ DIST_SCRIPT_BY_KIND[kind]
1383
+ );
1384
+ return Object.freeze({
1385
+ command: process.execPath,
1386
+ args: Object.freeze([script]),
1387
+ source: "dist-script"
1388
+ });
1389
+ }
1390
+
1391
+ // src/adapters/claude/resolve-approval-mcp-bin.ts
1392
+ function resolveApprovalMcpBin(host) {
1302
1393
  const override = process.env.BYOK_APPROVAL_MCP_BIN;
1303
1394
  if (override) {
1304
1395
  return { command: override, args: [], source: "env" };
1305
1396
  }
1306
- const distBin = path5.join(path5.dirname(fileURLToPath(import.meta.url)), "bin", "byok-approval-mcp.js");
1307
- return { command: process.execPath, args: [distBin], source: "dist" };
1397
+ const resolved = resolveSdkReservedHelperBin("approval-mcp", host);
1398
+ return {
1399
+ command: resolved.command,
1400
+ args: [...resolved.args],
1401
+ source: resolved.source === "self-executable" ? "host" : "dist"
1402
+ };
1308
1403
  }
1309
1404
 
1310
1405
  // src/adapters/claude/permission-mapping.ts
1311
1406
  var READONLY_TOOLS2 = ["Read", "Glob", "Grep"];
1312
- function mapPermissionPolicyToClaudeArgs(policy) {
1407
+ function mapPermissionPolicyToClaudeArgs(policy, toolsetGrants = []) {
1313
1408
  if (policy.network === false) {
1314
1409
  return {
1315
1410
  ok: false,
@@ -1335,7 +1430,10 @@ function mapPermissionPolicyToClaudeArgs(policy) {
1335
1430
  if (policy.mode === "readonly") {
1336
1431
  const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS2.includes(tool)) : [...READONLY_TOOLS2];
1337
1432
  const effective = subtractDenied(base, denyTools);
1338
- return { ok: true, args: ["--permission-mode", "default", "--tools", effective.join(",")] };
1433
+ return {
1434
+ ok: true,
1435
+ args: ["--permission-mode", "default", "--tools", effective.join(","), ...allowedToolsArgs(toolsetGrants)]
1436
+ };
1339
1437
  }
1340
1438
  if (denyTools.length > 0) {
1341
1439
  return {
@@ -1349,12 +1447,17 @@ function mapPermissionPolicyToClaudeArgs(policy) {
1349
1447
  if (policy.allowTools && policy.allowTools.length > 0) {
1350
1448
  args.push("--tools", policy.allowTools.join(","));
1351
1449
  }
1450
+ if (policy.mode !== "plan") args.push(...allowedToolsArgs(toolsetGrants));
1352
1451
  return { ok: true, args };
1353
1452
  }
1354
1453
  function subtractDenied(tools, denyTools) {
1355
1454
  const denied = new Set(denyTools);
1356
1455
  return tools.filter((tool) => !denied.has(tool));
1357
1456
  }
1457
+ function allowedToolsArgs(toolsetGrants) {
1458
+ const identifiers = toolsetGrants.flatMap((grant) => grant.tools.map((tool) => `mcp__${grant.server}__${tool}`)).sort();
1459
+ return identifiers.length === 0 ? [] : ["--allowedTools", identifiers.join(",")];
1460
+ }
1358
1461
  function createToolUseCorrelation() {
1359
1462
  return { toolNameByUseId: /* @__PURE__ */ new Map() };
1360
1463
  }
@@ -1759,10 +1862,62 @@ var ClaudeProcessClient = class {
1759
1862
  this.eventQueue.end();
1760
1863
  }
1761
1864
  };
1762
- var APPROVAL_TOOL_NAME = "approval_prompt";
1865
+ var GRANTABLE_TOOL_NAME = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$/u;
1866
+ var GRANTABLE_MCP_SERVER_NAME = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$/u;
1867
+
1868
+ // src/adapters/mcp-tool-grants.ts
1869
+ var PREGRANTED_RESERVED_MCP_TOOLS = Object.freeze({
1870
+ [AGENT_MESSAGE_MCP_SERVER_NAME]: Object.freeze([AGENT_MESSAGE_TOOL_NAME]),
1871
+ [AGENT_MEMORY_MCP_SERVER_NAME]: Object.freeze([
1872
+ AGENT_MEMORY_RECALL_TOOL_NAME,
1873
+ AGENT_MEMORY_SAVE_TOOL_NAME
1874
+ ])
1875
+ });
1876
+ function resolveReservedMcpToolGrants(servers) {
1877
+ return Object.entries(PREGRANTED_RESERVED_MCP_TOOLS).filter(([server]) => Object.prototype.hasOwnProperty.call(servers ?? {}, server)).map(([server, tools]) => Object.freeze({ server, tools }));
1878
+ }
1879
+ function resolveMcpToolsetGrants(servers, observation) {
1880
+ const projected = Object.keys(servers ?? {}).filter((name) => !isReservedMcpServerName(name)).sort();
1881
+ const ungrantableServers = projected.filter((name) => !GRANTABLE_MCP_SERVER_NAME.test(name));
1882
+ if (ungrantableServers.length > 0) {
1883
+ return {
1884
+ ok: false,
1885
+ reason: `projected MCP toolset server name(s) [${ungrantableServers.join(", ")}] cannot be expressed as a runtime tool grant \u2014 refusing to start a task whose grants would be ambiguous`
1886
+ };
1887
+ }
1888
+ const observed = observation ?? {};
1889
+ const unexpected = Object.keys(observed).filter((name) => !projected.includes(name)).sort();
1890
+ if (unexpected.length > 0) {
1891
+ return {
1892
+ ok: false,
1893
+ reason: `observed MCP tool names for server(s) [${unexpected.join(", ")}] that are not projected for this task \u2014 refusing to grant tools for a server the task was never given`
1894
+ };
1895
+ }
1896
+ const grants = [];
1897
+ for (const server of projected) {
1898
+ const tools = observed[server];
1899
+ if (tools === void 0 || tools.length === 0) {
1900
+ return {
1901
+ ok: false,
1902
+ reason: `no tools/list observation for projected MCP toolset server "${server}" \u2014 refusing to start a task whose toolset tools cannot be granted`
1903
+ };
1904
+ }
1905
+ const ungrantableTools = tools.filter((tool) => typeof tool !== "string" || !GRANTABLE_TOOL_NAME.test(tool));
1906
+ if (ungrantableTools.length > 0) {
1907
+ return {
1908
+ ok: false,
1909
+ reason: `MCP toolset server "${server}" reported tool name(s) [${ungrantableTools.map((tool) => JSON.stringify(tool)).join(", ")}] that cannot be expressed as a runtime tool grant`
1910
+ };
1911
+ }
1912
+ grants.push({ server, tools: Object.freeze([...tools].sort()) });
1913
+ }
1914
+ return { ok: true, grants: Object.freeze(grants) };
1915
+ }
1916
+ function grantFingerprint(grants) {
1917
+ return grants.flatMap((grant) => grant.tools.map((tool) => `${grant.server} ${tool}`)).sort().join("\n");
1918
+ }
1763
1919
 
1764
1920
  // src/adapters/claude/claude-adapter.ts
1765
- var APPROVAL_MCP_SERVER_NAME = "byokapproval";
1766
1921
  var execFileAsync2 = promisify(execFile);
1767
1922
  var DETECT_TIMEOUT_MS2 = 5e3;
1768
1923
  function errorMessage2(err) {
@@ -1787,6 +1942,10 @@ var ClaudeAdapter = class {
1787
1942
  descriptor = freezeRuntimeAdapterDescriptor({
1788
1943
  id: "claude",
1789
1944
  supportsDispatchSelection: true,
1945
+ // `--allowedTools mcp__<server>__<tool>` names each projected toolset
1946
+ // tool explicitly, so this adapter cannot admit a projected server
1947
+ // without the daemon's own `tools/list` observation of it.
1948
+ requiresMcpToolsetToolObservation: true,
1790
1949
  capabilities: {
1791
1950
  steer: false,
1792
1951
  resume: true,
@@ -1808,7 +1967,13 @@ var ClaudeAdapter = class {
1808
1967
  }
1809
1968
  }
1810
1969
  async prepare(input) {
1811
- const mapping = mapPermissionPolicyToClaudeArgs(input.policy);
1970
+ const toolsetGrants = resolveMcpToolsetGrants(input.mcpServers, input.mcpToolsetTools);
1971
+ if (!toolsetGrants.ok) return { kind: "reject", reason: `claude adapter cannot grant projected MCP toolset tools: ${toolsetGrants.reason}`, retryable: false };
1972
+ const reservedMemoryGrants = resolveReservedMcpToolGrants(input.mcpServers).filter((grant) => grant.server === AGENT_MEMORY_MCP_SERVER_NAME);
1973
+ const mapping = mapPermissionPolicyToClaudeArgs(input.policy, [
1974
+ ...reservedMemoryGrants,
1975
+ ...toolsetGrants.grants
1976
+ ]);
1812
1977
  if (!mapping.ok) return { kind: "reject", reason: mapping.reason ?? "policy rejected by claude adapter", retryable: false };
1813
1978
  let modelId;
1814
1979
  try {
@@ -1836,11 +2001,11 @@ var ClaudeAdapter = class {
1836
2001
  return {
1837
2002
  kind: "prepared",
1838
2003
  operation: {
1839
- start: (startInput) => this.startPrepared(startInput, mapping, modelId, bin, approvalMcpBin)
2004
+ start: (startInput) => this.startPrepared(startInput, mapping, modelId, bin, approvalMcpBin, toolsetGrants.grants)
1840
2005
  }
1841
2006
  };
1842
2007
  }
1843
- async startPrepared(startInput, initialMapping, modelId, bin, approvalMcpBin) {
2008
+ async startPrepared(startInput, initialMapping, modelId, bin, approvalMcpBin, preparedGrants) {
1844
2009
  if (!initialMapping.ok) throw new RuntimeExecutionFailure({
1845
2010
  phase: "start",
1846
2011
  category: "authority",
@@ -1855,6 +2020,15 @@ var ClaudeAdapter = class {
1855
2020
  reason: "prepared claude operation requires a resolved string instruction"
1856
2021
  });
1857
2022
  }
2023
+ const startGrants = resolveMcpToolsetGrants(startInput.mcpServers, startInput.mcpToolsetTools);
2024
+ if (!startGrants.ok || grantFingerprint(startGrants.grants) !== grantFingerprint(preparedGrants)) {
2025
+ throw new RuntimeExecutionFailure({
2026
+ phase: "start",
2027
+ category: "authority",
2028
+ retry: "non-retryable",
2029
+ reason: "prepared claude operation received different MCP toolset tool authority than it was admitted with"
2030
+ });
2031
+ }
1858
2032
  const mapping = { ...initialMapping, args: [...initialMapping.args] };
1859
2033
  let mcpConfigDir;
1860
2034
  const taskMcpServers = startInput.mcpServers ?? {};
@@ -2539,6 +2713,8 @@ var CodexProcessRunner = class {
2539
2713
  // src/adapters/codex/codex-adapter.ts
2540
2714
  var execFileAsync3 = promisify(execFile);
2541
2715
  var DETECT_TIMEOUT_MS3 = 5e3;
2716
+ var RESERVED_MCP_POLICY_PROBE_TIMEOUT_MS = 5e3;
2717
+ var MIN_CODEX_RESERVED_MCP_APPROVAL_VERSION = [0, 149, 0];
2542
2718
  var CodexAdapter = class {
2543
2719
  constructor(options = {}) {
2544
2720
  this.options = options;
@@ -2547,6 +2723,11 @@ var CodexAdapter = class {
2547
2723
  descriptor = freezeRuntimeAdapterDescriptor({
2548
2724
  id: "codex",
2549
2725
  supportsDispatchSelection: true,
2726
+ // `mcp_servers.<name>.enabled_tools` plus a per-tool `approval_mode`
2727
+ // names each projected toolset tool explicitly, so this adapter cannot
2728
+ // admit a projected server without the daemon's own `tools/list`
2729
+ // observation of it.
2730
+ requiresMcpToolsetToolObservation: true,
2550
2731
  capabilities: {
2551
2732
  steer: false,
2552
2733
  resume: true,
@@ -2619,14 +2800,43 @@ ${withStreams.stderr ?? ""}`);
2619
2800
  } catch (error) {
2620
2801
  return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
2621
2802
  }
2803
+ const toolsetGrants = resolveMcpToolsetGrants(input.mcpServers, input.mcpToolsetTools);
2804
+ if (!toolsetGrants.ok) {
2805
+ return { kind: "reject", reason: `codex adapter cannot grant projected MCP toolset tools: ${toolsetGrants.reason}`, retryable: false };
2806
+ }
2807
+ const reservedGrants = resolveReservedMcpToolGrants(input.mcpServers);
2808
+ const allGrants = Object.freeze([...reservedGrants, ...toolsetGrants.grants]);
2809
+ if (allGrants.length > 0) {
2810
+ try {
2811
+ await requireCodexPerToolApprovalSupport(command);
2812
+ for (const grant of allGrants) {
2813
+ await probeCodexMcpToolApproval(command, grant.server, input.mcpServers[grant.server], grant.tools);
2814
+ }
2815
+ } catch (error) {
2816
+ return {
2817
+ kind: "reject",
2818
+ reason: `codex MCP tool approval preflight failed: ${error instanceof Error ? error.message : String(error)}`,
2819
+ retryable: false
2820
+ };
2821
+ }
2822
+ }
2622
2823
  return {
2623
2824
  kind: "prepared",
2624
2825
  operation: {
2625
- start: (startInput) => this.startPrepared(startInput, mapping.args, modelId, command)
2826
+ start: (startInput) => this.startPrepared(startInput, mapping.args, modelId, command, toolsetGrants.grants, allGrants)
2626
2827
  }
2627
2828
  };
2628
2829
  }
2629
- async startPrepared(startInput, policyArgs, modelId, command) {
2830
+ async startPrepared(startInput, policyArgs, modelId, command, preparedToolsetGrants, preparedMcpGrants) {
2831
+ const startGrants = resolveMcpToolsetGrants(startInput.mcpServers, startInput.mcpToolsetTools);
2832
+ if (!startGrants.ok || grantFingerprint(startGrants.grants) !== grantFingerprint(preparedToolsetGrants)) {
2833
+ throw new RuntimeExecutionFailure({
2834
+ phase: "start",
2835
+ category: "authority",
2836
+ retry: "non-retryable",
2837
+ reason: "prepared codex operation received different MCP toolset tool authority than it was admitted with"
2838
+ });
2839
+ }
2630
2840
  if (typeof startInput.instruction !== "string") {
2631
2841
  throw new RuntimeExecutionFailure({
2632
2842
  phase: "start",
@@ -2678,12 +2888,13 @@ ${withStreams.stderr ?? ""}`);
2678
2888
  reason: "prepared codex operation received a manifest with different runtime selection"
2679
2889
  });
2680
2890
  }
2891
+ const mcpConfigArgs = codexMcpConfigArgs(startInput.mcpServers, preparedMcpGrants);
2681
2892
  const { sessionRef, runner } = await runCodexTurn({
2682
2893
  command,
2683
2894
  resumeRef: startInput.manifest.sessionRef,
2684
2895
  instruction: startInput.instruction,
2685
2896
  modelId: manifestModelId,
2686
- policyArgs: [...policyArgs, ...codexMcpConfigArgs(startInput.mcpServers)],
2897
+ policyArgs: [...policyArgs, ...mcpConfigArgs],
2687
2898
  cwd: manifestCwd,
2688
2899
  env: runtimeEnv,
2689
2900
  spawnFn: this.options.spawnFn,
@@ -2706,15 +2917,55 @@ ${withStreams.stderr ?? ""}`);
2706
2917
  initialRunner: runner,
2707
2918
  preparedGit: startInput.manifest.workspace.workspaceId !== void 0,
2708
2919
  modelId: manifestModelId,
2709
- terminal
2920
+ terminal,
2921
+ mcpConfigArgs
2710
2922
  });
2711
2923
  }
2712
2924
  resolveBin() {
2713
2925
  return (this.options.resolveBin ?? resolveCodexBin)();
2714
2926
  }
2715
2927
  };
2716
- function codexMcpConfigArgs(servers) {
2928
+ async function requireCodexPerToolApprovalSupport(command) {
2929
+ const versionResult = await execFileAsync3(command, ["--version"], { timeout: RESERVED_MCP_POLICY_PROBE_TIMEOUT_MS });
2930
+ const versionText = `${versionResult.stdout}
2931
+ ${versionResult.stderr}`.trim();
2932
+ const versionMatch = /codex-cli\s+(\d+)\.(\d+)\.(\d+)/u.exec(versionText);
2933
+ if (versionMatch === null) throw new Error(`unrecognized Codex version: ${versionText || "(empty)"}`);
2934
+ const version = versionMatch.slice(1, 4).map(Number);
2935
+ for (let index = 0; index < MIN_CODEX_RESERVED_MCP_APPROVAL_VERSION.length; index += 1) {
2936
+ if (version[index] > MIN_CODEX_RESERVED_MCP_APPROVAL_VERSION[index]) break;
2937
+ if (version[index] < MIN_CODEX_RESERVED_MCP_APPROVAL_VERSION[index]) {
2938
+ throw new Error(`Codex ${version.slice(0, 3).join(".")} lacks the required per-MCP-tool approval contract`);
2939
+ }
2940
+ }
2941
+ }
2942
+ async function probeCodexMcpToolApproval(command, name, server, tools) {
2943
+ const probeArgs = [
2944
+ "mcp",
2945
+ "get",
2946
+ name,
2947
+ "--json",
2948
+ "-c",
2949
+ `mcp_servers.${name}.command=${JSON.stringify(server.command)}`,
2950
+ ...codexMcpToolApprovalArgs(name, tools)
2951
+ ];
2952
+ const result = await execFileAsync3(command, probeArgs, { timeout: RESERVED_MCP_POLICY_PROBE_TIMEOUT_MS });
2953
+ const parsed = JSON.parse(result.stdout);
2954
+ const readBack = Array.isArray(parsed.enabled_tools) ? parsed.enabled_tools : void 0;
2955
+ if (parsed.name !== name || parsed.enabled !== true || readBack === void 0 || readBack.length !== tools.length || readBack.some((tool, index) => tool !== tools[index])) {
2956
+ throw new Error(`Codex did not read back the exact tool allowlist for MCP server "${name}"`);
2957
+ }
2958
+ }
2959
+ function codexMcpToolApprovalArgs(name, tools) {
2960
+ const args = ["-c", `mcp_servers.${name}.enabled_tools=${JSON.stringify([...tools])}`];
2961
+ for (const tool of tools) {
2962
+ args.push("-c", `mcp_servers.${name}.tools.${tool}.approval_mode="approve"`);
2963
+ }
2964
+ return args;
2965
+ }
2966
+ function codexMcpConfigArgs(servers, grants = []) {
2717
2967
  if (servers === void 0 || Object.keys(servers).length === 0) return [];
2968
+ const grantedTools = new Map(grants.map((grant) => [grant.server, grant.tools]));
2718
2969
  const args = ["--ignore-user-config"];
2719
2970
  for (const [name, server] of Object.entries(servers).sort(([left], [right]) => left.localeCompare(right))) {
2720
2971
  args.push("-c", `mcp_servers.${name}.command=${JSON.stringify(server.command)}`);
@@ -2722,6 +2973,8 @@ function codexMcpConfigArgs(servers) {
2722
2973
  for (const [key, value] of Object.entries(server.env ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
2723
2974
  args.push("-c", `mcp_servers.${name}.env.${key}=${JSON.stringify(value)}`);
2724
2975
  }
2976
+ const granted = grantedTools.get(name);
2977
+ if (granted !== void 0) args.push(...codexMcpToolApprovalArgs(name, granted));
2725
2978
  }
2726
2979
  return args;
2727
2980
  }
@@ -2908,6 +3161,19 @@ var CodexSession = class {
2908
3161
  recordUnmapped;
2909
3162
  preparedGit;
2910
3163
  modelId;
3164
+ /**
3165
+ * A resume spawns a BRAND NEW codex process, which inherits none of the
3166
+ * first turn's `-c` overrides — exactly the same reason `followUp()`
3167
+ * re-maps the permission policy on every call. Without replaying these
3168
+ * bytes, `--ignore-user-config` and every `mcp_servers.*` key (command,
3169
+ * args, env, `enabled_tools`, per-tool `approval_mode`) would silently
3170
+ * vanish after turn one: the reserved message server and any projected
3171
+ * toolset would stop existing, and any surviving tool call would be
3172
+ * refused outright under `approval_policy=never`. Frozen at start; never
3173
+ * recomputed from anything a later turn supplies, so a follow-up can never
3174
+ * widen this session's MCP authority.
3175
+ */
3176
+ mcpConfigArgs;
2911
3177
  terminal;
2912
3178
  currentRunner;
2913
3179
  ownedRunners = /* @__PURE__ */ new Set();
@@ -2925,6 +3191,7 @@ var CodexSession = class {
2925
3191
  this.preparedGit = options.preparedGit;
2926
3192
  this.modelId = options.modelId;
2927
3193
  this.terminal = options.terminal;
3194
+ this.mcpConfigArgs = options.mcpConfigArgs;
2928
3195
  this.currentRunner = options.initialRunner;
2929
3196
  this.ownedRunners.add(options.initialRunner);
2930
3197
  void this.forgetRunnerOnceClosed(options.initialRunner);
@@ -3033,7 +3300,9 @@ var CodexSession = class {
3033
3300
  resumeRef,
3034
3301
  instruction: task.instruction,
3035
3302
  modelId,
3036
- policyArgs: mapping.args,
3303
+ // Freshly re-mapped policy for THIS turn, plus the start turn's
3304
+ // frozen MCP config replayed verbatim — see `mcpConfigArgs`.
3305
+ policyArgs: [...mapping.args, ...this.mcpConfigArgs],
3037
3306
  cwd: this.workspaceDir,
3038
3307
  env: withoutProviderCredentials(this.env),
3039
3308
  spawnFn: this.spawnFn,