@byok-sdk/client 0.1.1 → 0.3.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 (34) hide show
  1. package/README.md +46 -6
  2. package/dist/adapters/claude/claude-adapter.d.ts +3 -0
  3. package/dist/adapters/claude/resolve-bin.d.ts +2 -2
  4. package/dist/adapters/codex/codex-adapter.d.ts +5 -3
  5. package/dist/adapters/index.d.ts +1 -1
  6. package/dist/adapters/index.js +293 -100
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/adapters/pi/events.d.ts +10 -37
  9. package/dist/adapters/pi/permission-mapping.d.ts +4 -20
  10. package/dist/adapters/pi/pi-adapter.d.ts +22 -0
  11. package/dist/adapters/pi/resolve-bin.d.ts +18 -14
  12. package/dist/adapters/pi/rpc-client.d.ts +1 -4
  13. package/dist/adapters/provider-credential-environment.d.ts +18 -0
  14. package/dist/bin/byok-agent.js +1839 -838
  15. package/dist/bin/byok-agent.js.map +1 -1
  16. package/dist/bin/byok-approval-mcp.js +2 -2
  17. package/dist/bin/byok-approval-mcp.js.map +1 -1
  18. package/dist/daemon/assertion-client.d.ts +68 -0
  19. package/dist/daemon/capabilities-client.d.ts +48 -0
  20. package/dist/daemon/control-protocol.d.ts +81 -4
  21. package/dist/daemon/create-daemon.d.ts +169 -1
  22. package/dist/daemon/daemon-owner.d.ts +35 -0
  23. package/dist/daemon/device-assertion-signer.d.ts +41 -0
  24. package/dist/daemon/device-keys.d.ts +15 -13
  25. package/dist/daemon/observer.d.ts +68 -3
  26. package/dist/daemon/presence-publisher.d.ts +69 -0
  27. package/dist/daemon/skill-pack-installer.d.ts +116 -0
  28. package/dist/daemon/task-runner.d.ts +129 -3
  29. package/dist/index.d.ts +22 -3
  30. package/dist/index.js +1872 -284
  31. package/dist/index.js.map +1 -1
  32. package/dist/lifecycle/create-service-lifecycle.d.ts +2 -2
  33. package/dist/types.d.ts +29 -0
  34. package/package.json +6 -5
@@ -1,9 +1,9 @@
1
1
  import { execFile, spawn, spawnSync } from 'child_process';
2
2
  import { promisify } from 'util';
3
- import { promises, realpathSync } from 'fs';
4
- import os from 'os';
5
- import path2 from 'path';
3
+ import { promises, existsSync, readFileSync, realpathSync } from 'fs';
4
+ import path3 from 'path';
6
5
  import { fileURLToPath } from 'url';
6
+ import os from 'os';
7
7
  import 'readline';
8
8
 
9
9
  // src/adapters/pi/pi-adapter.ts
@@ -24,20 +24,50 @@ var SteerUnsupportedError = class extends Error {
24
24
  this.runtimeId = runtimeId;
25
25
  }
26
26
  };
27
-
28
- // src/adapters/pi/resolve-bin.ts
29
27
  var PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
28
+ function readPackageJson(dir) {
29
+ const candidate = path3.join(dir, "package.json");
30
+ if (!existsSync(candidate)) return void 0;
31
+ try {
32
+ return JSON.parse(readFileSync(candidate, "utf8"));
33
+ } catch {
34
+ return void 0;
35
+ }
36
+ }
30
37
  function resolvePiBin() {
31
38
  const override = process.env.BYOK_PI_BIN;
32
39
  if (override) {
33
- return { command: override, source: "path" };
40
+ return { command: override, source: "env" };
34
41
  }
35
- return { command: "pi", source: "path" };
42
+ try {
43
+ const mainEntryUrl = import.meta.resolve(PI_PACKAGE_NAME);
44
+ let dir = path3.dirname(fileURLToPath(mainEntryUrl));
45
+ for (let depth = 0; depth < 6; depth++) {
46
+ const pkg = readPackageJson(dir);
47
+ if (pkg?.name === PI_PACKAGE_NAME) {
48
+ const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi;
49
+ if (binRel) {
50
+ return { command: path3.join(dir, binRel), source: "package" };
51
+ }
52
+ break;
53
+ }
54
+ const parent = path3.dirname(dir);
55
+ if (parent === dir) break;
56
+ dir = parent;
57
+ }
58
+ } catch (cause) {
59
+ throw new Error(
60
+ `Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.19+ pi sidecar`,
61
+ { cause }
62
+ );
63
+ }
64
+ throw new Error(
65
+ `Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.19+ pi sidecar`
66
+ );
36
67
  }
37
68
 
38
69
  // src/adapters/pi/permission-mapping.ts
39
70
  var READONLY_TOOLS = ["read", "grep", "find", "ls"];
40
- var DEFAULT_ACTIVE_TOOLS = ["read", "bash", "edit", "write"];
41
71
  function mapPermissionPolicyToPiArgs(policy) {
42
72
  if (policy.network === false) {
43
73
  return {
@@ -56,22 +86,18 @@ function mapPermissionPolicyToPiArgs(policy) {
56
86
  const denyTools = policy.denyTools ?? [];
57
87
  if (policy.mode === "readonly") {
58
88
  const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS.includes(tool)) : [...READONLY_TOOLS];
59
- const effective = subtractDenied(base, denyTools);
60
- return { ok: true, args: effective.length === 0 ? ["--no-tools"] : ["--tools", effective.join(",")] };
61
- }
62
- if (denyTools.length > 0) {
63
- const base = policy.allowTools && policy.allowTools.length > 0 ? policy.allowTools : [...DEFAULT_ACTIVE_TOOLS];
64
- const effective = subtractDenied(base, denyTools);
65
- return { ok: true, args: effective.length === 0 ? ["--no-tools"] : ["--tools", effective.join(",")] };
89
+ if (base.length === 0) return { ok: true, args: ["--no-tools"] };
90
+ return {
91
+ ok: true,
92
+ args: ["--tools", base.join(","), ...denyTools.length > 0 ? ["--exclude-tools", denyTools.join(",")] : []]
93
+ };
66
94
  }
95
+ const args = [];
67
96
  if (policy.allowTools && policy.allowTools.length > 0) {
68
- return { ok: true, args: ["--tools", policy.allowTools.join(",")] };
97
+ args.push("--tools", policy.allowTools.join(","));
69
98
  }
70
- return { ok: true, args: [] };
71
- }
72
- function subtractDenied(tools, denyTools) {
73
- const denied = new Set(denyTools);
74
- return tools.filter((tool) => !denied.has(tool));
99
+ if (denyTools.length > 0) args.push("--exclude-tools", denyTools.join(","));
100
+ return { ok: true, args };
75
101
  }
76
102
 
77
103
  // src/adapters/pi/events.ts
@@ -96,7 +122,7 @@ function mapPiMessageToAgentEvent(msg) {
96
122
  output: { result: msg.result, isError: msg.isError === true }
97
123
  };
98
124
  }
99
- case "agent_end":
125
+ case "agent_settled":
100
126
  return { type: "turn_end" };
101
127
  /**
102
128
  * `artifact` is NOT a real pi RPC message — pi's own `write` tool only
@@ -132,16 +158,22 @@ function mapPiMessageToAgentEvent(msg) {
132
158
  // `recordUnmappedFrame`) can tell "known, expected, silently ignored"
133
159
  // apart from "genuinely never seen before" (falls to `default` below).
134
160
  case "agent_start":
161
+ case "agent_end":
162
+ // one low-level run; `agent_settled` is BYOK completion
135
163
  case "turn_start":
136
164
  case "turn_end":
137
- // pi's own per-LLM-turn boundary, not ours — see `agent_end` above
165
+ // pi's own per-LLM-turn boundary, not ours
138
166
  case "message_start":
139
167
  case "message_end":
168
+ case "bash_execution_update":
140
169
  case "tool_execution_update":
141
170
  case "queue_update":
142
171
  case "compaction_start":
143
172
  case "compaction_end":
144
173
  case "auto_retry_start":
174
+ case "summarization_retry_scheduled":
175
+ case "summarization_retry_attempt_start":
176
+ case "summarization_retry_finished":
145
177
  case "session_info_changed":
146
178
  case "thinking_level_changed":
147
179
  return void 0;
@@ -151,15 +183,20 @@ function mapPiMessageToAgentEvent(msg) {
151
183
  }
152
184
  var ROUTINE_PI_EVENT_TYPES = /* @__PURE__ */ new Set([
153
185
  "agent_start",
186
+ "agent_end",
154
187
  "turn_start",
155
188
  "turn_end",
156
189
  "message_start",
157
190
  "message_end",
191
+ "bash_execution_update",
158
192
  "tool_execution_update",
159
193
  "queue_update",
160
194
  "compaction_start",
161
195
  "compaction_end",
162
196
  "auto_retry_start",
197
+ "summarization_retry_scheduled",
198
+ "summarization_retry_attempt_start",
199
+ "summarization_retry_finished",
163
200
  "session_info_changed",
164
201
  "thinking_level_changed"
165
202
  ]);
@@ -293,10 +330,7 @@ var PiRpcClient = class {
293
330
  * traffic. Logs once per distinct type (not per occurrence, so a
294
331
  * repeating unmapped type can't spam stdout); the running tally is also
295
332
  * folded into this client's exit-time error message (`buildExitError`) so
296
- * a post-mortem on a failed/hung task has it without needing separate log
297
- * scraping. This is the exact mechanism that would have turned this
298
- * task's root-cause hang (`agent_end` arriving with no mapping) into a
299
- * one-line, immediate warning instead of a silent stall.
333
+ * a post-mortem on a failed/hung task has it without separate log scraping.
300
334
  */
301
335
  recordUnmappedFrame(type) {
302
336
  const next = (this.unmappedFrameCounts.get(type) ?? 0) + 1;
@@ -407,13 +441,8 @@ var PiRpcClient = class {
407
441
  }
408
442
  };
409
443
 
410
- // src/adapters/pi/pi-adapter.ts
411
- var execFileAsync = promisify(execFile);
412
- var DETECT_TIMEOUT_MS = 5e3;
413
- function errorMessage(err) {
414
- return err instanceof Error ? err.message : String(err);
415
- }
416
- var KNOWN_PROVIDER_ENV_VARS = [
444
+ // src/adapters/provider-credential-environment.ts
445
+ var PROVIDER_CREDENTIAL_ENV_NAMES = [
417
446
  "ANTHROPIC_API_KEY",
418
447
  "ANTHROPIC_OAUTH_TOKEN",
419
448
  "OPENAI_API_KEY",
@@ -424,24 +453,66 @@ var KNOWN_PROVIDER_ENV_VARS = [
424
453
  "MISTRAL_API_KEY",
425
454
  "OPENROUTER_API_KEY",
426
455
  "XAI_API_KEY",
427
- // Confirmed against the installed pi's own docs/providers.md ("ZAI |
428
- // `ZAI_API_KEY` | `zai`") and exercised live against real GLM traffic
429
- // during this task's acceptance run — omitting it made `authPresent`
430
- // silently false for a perfectly valid, working z.ai/GLM setup.
431
456
  "ZAI_API_KEY"
432
457
  ];
458
+ var PROVIDER_CREDENTIAL_ENV_DENY_NAMES = [
459
+ ...PROVIDER_CREDENTIAL_ENV_NAMES,
460
+ "ANT_LING_API_KEY",
461
+ "NVIDIA_API_KEY",
462
+ "CEREBRAS_API_KEY",
463
+ "CLOUDFLARE_API_KEY",
464
+ "AI_GATEWAY_API_KEY",
465
+ "ZAI_CODING_CN_API_KEY",
466
+ "OPENCODE_API_KEY",
467
+ "RADIUS_API_KEY",
468
+ "FIREWORKS_API_KEY",
469
+ "TOGETHER_API_KEY",
470
+ "BASETEN_API_KEY",
471
+ "KIMI_API_KEY",
472
+ "MINIMAX_API_KEY",
473
+ "MINIMAX_CN_API_KEY",
474
+ "QWEN_TOKEN_PLAN_API_KEY",
475
+ "QWEN_TOKEN_PLAN_CN_API_KEY",
476
+ "XIAOMI_API_KEY",
477
+ "XIAOMI_TOKEN_PLAN_CN_API_KEY",
478
+ "XIAOMI_TOKEN_PLAN_AMS_API_KEY",
479
+ "XIAOMI_TOKEN_PLAN_SGP_API_KEY",
480
+ "AWS_ACCESS_KEY_ID",
481
+ "AWS_SECRET_ACCESS_KEY",
482
+ "AWS_SESSION_TOKEN",
483
+ "GOOGLE_APPLICATION_CREDENTIALS",
484
+ // Reserved by the keys-owned Pi projection. It must never be inherited
485
+ // from the daemon; the launcher deletes any ambient copy and injects only
486
+ // the exact credential it just resolved from OS custody.
487
+ "PI_PROVIDER_API_KEY"
488
+ ];
489
+ function withoutProviderCredentials(env) {
490
+ const sanitized = { ...env };
491
+ for (const name of PROVIDER_CREDENTIAL_ENV_DENY_NAMES) {
492
+ delete sanitized[name];
493
+ }
494
+ return sanitized;
495
+ }
496
+
497
+ // src/adapters/pi/pi-adapter.ts
498
+ var execFileAsync = promisify(execFile);
499
+ var DETECT_TIMEOUT_MS = 5e3;
500
+ function errorMessage(err) {
501
+ return err instanceof Error ? err.message : String(err);
502
+ }
433
503
  var PiAdapter = class {
434
504
  constructor(options = {}) {
435
505
  this.options = options;
436
506
  }
437
507
  options;
438
508
  id = "pi";
509
+ supportsDispatchSelection = true;
439
510
  async detect() {
440
- const bin = this.resolveBin();
441
511
  try {
512
+ const bin = this.resolveBin();
442
513
  const { stdout, stderr } = await execFileAsync(bin.command, ["--version"], { timeout: DETECT_TIMEOUT_MS });
443
514
  const version = stdout.trim() || stderr.trim();
444
- const authPresent = KNOWN_PROVIDER_ENV_VARS.some((name) => process.env[name] !== void 0);
515
+ const authPresent = PROVIDER_CREDENTIAL_ENV_NAMES.some((name) => process.env[name] !== void 0);
445
516
  return { present: true, version, authPresent };
446
517
  } catch {
447
518
  return { present: false };
@@ -460,7 +531,7 @@ var PiAdapter = class {
460
531
  * variable beyond the platform baseline (`daemon/environment.ts`).
461
532
  */
462
533
  environmentRequirements() {
463
- return { credentialNames: KNOWN_PROVIDER_ENV_VARS };
534
+ return { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES };
464
535
  }
465
536
  async start(task, ctx) {
466
537
  if (typeof task.instruction !== "string") {
@@ -472,12 +543,45 @@ var PiAdapter = class {
472
543
  }
473
544
  const bin = this.resolveBin();
474
545
  const resumeSessionId = task.sessionRef;
475
- const args = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
546
+ const piArgs = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
547
+ const selection = task.dispatchSelection;
548
+ let command = bin.command;
549
+ let args = piArgs;
550
+ if (selection !== void 0) {
551
+ if (selection.lane !== "byok" || selection.runtimeId !== "pi") {
552
+ throw new PolicyUnsupportedError(
553
+ `pi adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
554
+ );
555
+ }
556
+ const launcher = this.options.byokLauncher;
557
+ if (launcher === void 0) {
558
+ throw new PolicyUnsupportedError(
559
+ "pi BYOK selection requires a configured credential-custody launcher"
560
+ );
561
+ }
562
+ command = launcher.command;
563
+ args = [
564
+ ...launcher.args ?? [],
565
+ "--pi-bin",
566
+ bin.command,
567
+ "--profile-db",
568
+ launcher.profileDbPath,
569
+ "--session-dir",
570
+ launcher.sessionDir,
571
+ ...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
572
+ "--provider",
573
+ selection.providerId,
574
+ "--model",
575
+ selection.modelId,
576
+ "--",
577
+ ...piArgs
578
+ ];
579
+ }
476
580
  const rpc = new PiRpcClient({
477
- command: bin.command,
581
+ command,
478
582
  args,
479
583
  cwd: ctx.workspaceDir,
480
- env: ctx.env,
584
+ env: selection === void 0 ? ctx.env : withoutProviderCredentials(ctx.env),
481
585
  spawnFn: this.options.spawnFn
482
586
  });
483
587
  const response = await rpc.send({ type: "prompt", message: task.instruction });
@@ -496,7 +600,7 @@ var PiAdapter = class {
496
600
  throw err;
497
601
  }
498
602
  }
499
- return new PiSession(sessionRef, rpc);
603
+ return new PiSession(sessionRef, rpc, selection);
500
604
  }
501
605
  resolveBin() {
502
606
  return (this.options.resolveBin ?? resolvePiBin)();
@@ -524,12 +628,14 @@ async function resolveFreshSessionId(rpc) {
524
628
  );
525
629
  }
526
630
  var PiSession = class {
527
- constructor(sessionRef, rpc) {
631
+ constructor(sessionRef, rpc, selection) {
528
632
  this.sessionRef = sessionRef;
529
633
  this.rpc = rpc;
634
+ this.selection = selection;
530
635
  }
531
636
  sessionRef;
532
637
  rpc;
638
+ selection;
533
639
  get events() {
534
640
  const rpc = this.rpc;
535
641
  return {
@@ -558,6 +664,12 @@ var PiSession = class {
558
664
  if (typeof task.instruction !== "string") {
559
665
  throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
560
666
  }
667
+ const requestedSelection = task.dispatchSelection;
668
+ if (requestedSelection !== void 0 && (this.selection?.lane !== "byok" || requestedSelection.lane !== "byok" || requestedSelection.runtimeId !== "pi" || requestedSelection.providerId !== this.selection.providerId || requestedSelection.modelId !== this.selection.modelId)) {
669
+ throw new PolicyUnsupportedError(
670
+ "pi persistent session cannot change its authoritative BYOK provider/model selection"
671
+ );
672
+ }
561
673
  await this.rpc.send({ type: "prompt", message: task.instruction, streamingBehavior: "followUp" });
562
674
  }
563
675
  async interrupt() {
@@ -584,7 +696,7 @@ function resolveApprovalMcpBin() {
584
696
  if (override) {
585
697
  return { command: override, args: [], source: "env" };
586
698
  }
587
- const distBin = path2.join(path2.dirname(fileURLToPath(import.meta.url)), "bin", "byok-approval-mcp.js");
699
+ const distBin = path3.join(path3.dirname(fileURLToPath(import.meta.url)), "bin", "byok-approval-mcp.js");
588
700
  return { command: process.execPath, args: [distBin], source: "dist" };
589
701
  }
590
702
 
@@ -615,7 +727,7 @@ function mapPermissionPolicyToClaudeArgs(policy) {
615
727
  }
616
728
  if (policy.mode === "readonly") {
617
729
  const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS2.includes(tool)) : [...READONLY_TOOLS2];
618
- const effective = subtractDenied2(base, denyTools);
730
+ const effective = subtractDenied(base, denyTools);
619
731
  return { ok: true, args: ["--permission-mode", "default", "--tools", effective.join(",")] };
620
732
  }
621
733
  if (denyTools.length > 0) {
@@ -632,7 +744,7 @@ function mapPermissionPolicyToClaudeArgs(policy) {
632
744
  }
633
745
  return { ok: true, args };
634
746
  }
635
- function subtractDenied2(tools, denyTools) {
747
+ function subtractDenied(tools, denyTools) {
636
748
  const denied = new Set(denyTools);
637
749
  return tools.filter((tool) => !denied.has(tool));
638
750
  }
@@ -664,7 +776,7 @@ var EXTENSION_CONTENT_TYPES = {
664
776
  ".yml": "application/yaml"
665
777
  };
666
778
  function guessContentType(filePath) {
667
- const ext = path2.extname(filePath).toLowerCase();
779
+ const ext = path3.extname(filePath).toLowerCase();
668
780
  return EXTENSION_CONTENT_TYPES[ext] ?? "application/octet-stream";
669
781
  }
670
782
  function mapAssistant(msg, correlation) {
@@ -736,11 +848,11 @@ function tryBuildArtifactEvent(msg, workspaceDir) {
736
848
  const filePath = toolUseResult && typeof toolUseResult.filePath === "string" ? toolUseResult.filePath : void 0;
737
849
  if (!filePath) return void 0;
738
850
  const realWorkspaceDir = tryRealpath(workspaceDir) ?? workspaceDir;
739
- const fileDir = path2.dirname(filePath);
851
+ const fileDir = path3.dirname(filePath);
740
852
  const realFileDir = tryRealpath(fileDir) ?? fileDir;
741
- const realFilePath = path2.join(realFileDir, path2.basename(filePath));
742
- const relative = path2.relative(realWorkspaceDir, realFilePath);
743
- if (relative === "" || relative.startsWith("..") || path2.isAbsolute(relative)) {
853
+ const realFilePath = path3.join(realFileDir, path3.basename(filePath));
854
+ const relative = path3.relative(realWorkspaceDir, realFilePath);
855
+ if (relative === "" || relative.startsWith("..") || path3.isAbsolute(relative)) {
744
856
  return void 0;
745
857
  }
746
858
  return { type: "artifact", name: relative, contentType: guessContentType(filePath) };
@@ -982,7 +1094,7 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
982
1094
  var APPROVAL_MCP_SERVER_NAME = "byokapproval";
983
1095
  var execFileAsync2 = promisify(execFile);
984
1096
  var DETECT_TIMEOUT_MS2 = 5e3;
985
- async function cleanupApprovalMcpConfigDir(dir) {
1097
+ async function cleanupMcpConfigDir(dir) {
986
1098
  if (!dir) return;
987
1099
  await promises.rm(dir, { recursive: true, force: true }).catch(() => {
988
1100
  });
@@ -992,6 +1104,7 @@ var ClaudeAdapter = class {
992
1104
  this.options = options;
993
1105
  }
994
1106
  options;
1107
+ supportsDispatchSelection = true;
995
1108
  id = "claude";
996
1109
  async detect() {
997
1110
  const bin = this.resolveBin();
@@ -1005,7 +1118,13 @@ var ClaudeAdapter = class {
1005
1118
  }
1006
1119
  }
1007
1120
  capabilities() {
1008
- return { steer: false, resume: true, approvalInteractive: true, permissionModes: ["auto", "readonly", "plan", "confirm"] };
1121
+ return {
1122
+ steer: false,
1123
+ resume: true,
1124
+ approvalInteractive: true,
1125
+ mcpToolsets: true,
1126
+ permissionModes: ["auto", "readonly", "plan", "confirm"]
1127
+ };
1009
1128
  }
1010
1129
  /**
1011
1130
  * M5: deliberate product-boundary decision, not an oversight — byok's
@@ -1031,42 +1150,51 @@ var ClaudeAdapter = class {
1031
1150
  if (!mapping.ok) {
1032
1151
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
1033
1152
  }
1034
- let approvalMcpConfigDir;
1153
+ const modelId = subscriptionModel(task, "claude");
1154
+ let mcpConfigDir;
1155
+ const taskMcpServers = ctx.mcpServers ?? {};
1156
+ const needsMcpConfig = mapping.needsApprovalMcp || Object.keys(taskMcpServers).length > 0;
1035
1157
  if (mapping.needsApprovalMcp) {
1036
1158
  if (!ctx.approvalChannel) {
1037
1159
  throw new PolicyUnsupportedError(
1038
1160
  'claude adapter requires policy.mode "confirm" to be started with an approval channel (TaskContext.approvalChannel) \u2014 none was provided'
1039
1161
  );
1040
1162
  }
1041
- const approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
1042
- approvalMcpConfigDir = await promises.mkdtemp(path2.join(os.tmpdir(), "byok-approval-mcp-"));
1043
- await promises.chmod(approvalMcpConfigDir, 448).catch(() => {
1163
+ if (Object.prototype.hasOwnProperty.call(taskMcpServers, APPROVAL_MCP_SERVER_NAME)) {
1164
+ throw new PolicyUnsupportedError(
1165
+ `MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`
1166
+ );
1167
+ }
1168
+ }
1169
+ if (needsMcpConfig) {
1170
+ mcpConfigDir = await promises.mkdtemp(path3.join(os.tmpdir(), "byok-mcp-"));
1171
+ await promises.chmod(mcpConfigDir, 448).catch(() => {
1044
1172
  });
1045
- const mcpConfigPath = path2.join(approvalMcpConfigDir, "mcp-config.json");
1046
- const mcpConfig = {
1047
- mcpServers: {
1048
- [APPROVAL_MCP_SERVER_NAME]: {
1049
- command: approvalMcpBin.command,
1050
- args: approvalMcpBin.args,
1051
- env: {
1052
- BYOK_STORE_DIR: ctx.approvalChannel.storeDir,
1053
- BYOK_PRODUCT_ID: ctx.approvalChannel.productId,
1054
- BYOK_TASK_ID: ctx.approvalChannel.taskId,
1055
- BYOK_APPROVAL_TIMEOUT_MS: String(ctx.approvalChannel.timeoutMs)
1056
- }
1173
+ const mcpConfigPath = path3.join(mcpConfigDir, "mcp-config.json");
1174
+ const mcpServers = { ...taskMcpServers };
1175
+ if (mapping.needsApprovalMcp) {
1176
+ const approvalChannel = ctx.approvalChannel;
1177
+ if (!approvalChannel) throw new Error("unreachable: approval channel checked above");
1178
+ const approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
1179
+ mcpServers[APPROVAL_MCP_SERVER_NAME] = {
1180
+ command: approvalMcpBin.command,
1181
+ args: approvalMcpBin.args,
1182
+ env: {
1183
+ BYOK_STORE_DIR: approvalChannel.storeDir,
1184
+ BYOK_PRODUCT_ID: approvalChannel.productId,
1185
+ BYOK_TASK_ID: approvalChannel.taskId,
1186
+ BYOK_APPROVAL_TIMEOUT_MS: String(approvalChannel.timeoutMs)
1057
1187
  }
1058
- }
1059
- };
1060
- await promises.writeFile(mcpConfigPath, JSON.stringify(mcpConfig), { mode: 384 });
1188
+ };
1189
+ }
1190
+ await promises.writeFile(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
1061
1191
  mapping.args = [
1062
1192
  ...mapping.args,
1063
- "--permission-prompt-tool",
1064
- `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`,
1193
+ ...mapping.needsApprovalMcp ? ["--permission-prompt-tool", `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`] : [],
1065
1194
  "--mcp-config",
1066
1195
  mcpConfigPath,
1067
- // Never let this task's confirm-mode run pick up some OTHER MCP
1068
- // server from ambient project/user config the approval channel is
1069
- // the only MCP server this invocation should ever see.
1196
+ // The generated file is the complete task-scoped MCP authority.
1197
+ // Never merge ambient user/project MCP configuration into it.
1070
1198
  "--strict-mcp-config"
1071
1199
  ];
1072
1200
  }
@@ -1083,6 +1211,7 @@ var ClaudeAdapter = class {
1083
1211
  // "Error: When using --print, --output-format=stream-json requires
1084
1212
  // --verbose", before spawning any model call.
1085
1213
  "--verbose",
1214
+ ...modelId ? ["--model", modelId] : [],
1086
1215
  ...resumeSessionId ? ["--resume", resumeSessionId] : [],
1087
1216
  ...mapping.args
1088
1217
  ];
@@ -1090,7 +1219,7 @@ var ClaudeAdapter = class {
1090
1219
  command: bin.command,
1091
1220
  args,
1092
1221
  cwd: ctx.workspaceDir,
1093
- env: ctx.env,
1222
+ env: withoutProviderCredentials(ctx.env),
1094
1223
  spawnFn: this.options.spawnFn
1095
1224
  });
1096
1225
  client.writeUserMessage(task.instruction);
@@ -1099,17 +1228,24 @@ var ClaudeAdapter = class {
1099
1228
  sessionRef = await client.waitForInit();
1100
1229
  } catch (err) {
1101
1230
  client.kill();
1102
- await cleanupApprovalMcpConfigDir(approvalMcpConfigDir);
1231
+ await cleanupMcpConfigDir(mcpConfigDir);
1103
1232
  throw err;
1104
1233
  }
1105
1234
  if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
1106
1235
  client.kill();
1107
- await cleanupApprovalMcpConfigDir(approvalMcpConfigDir);
1236
+ await cleanupMcpConfigDir(mcpConfigDir);
1108
1237
  throw new Error(
1109
1238
  `claude --resume echoed a different session id than requested (requested ${resumeSessionId}, got ${sessionRef}) \u2014 refusing to continue in a possibly-wrong session (fail-closed)`
1110
1239
  );
1111
1240
  }
1112
- return new ClaudeSession(sessionRef, client, ctx.workspaceDir, ctx.approvalChannel, approvalMcpConfigDir);
1241
+ return new ClaudeSession(
1242
+ sessionRef,
1243
+ client,
1244
+ ctx.workspaceDir,
1245
+ ctx.approvalChannel,
1246
+ mcpConfigDir,
1247
+ modelId
1248
+ );
1113
1249
  }
1114
1250
  /**
1115
1251
  * `claude auth status --json` is claude's OWN non-secret login-state
@@ -1143,19 +1279,31 @@ var ClaudeAdapter = class {
1143
1279
  return (this.options.resolveBin ?? resolveClaudeBin)();
1144
1280
  }
1145
1281
  };
1282
+ function subscriptionModel(task, runtimeId) {
1283
+ const selection = task.dispatchSelection;
1284
+ if (selection === void 0) return void 0;
1285
+ if (selection.lane !== "subscription" || selection.runtimeId !== runtimeId) {
1286
+ throw new PolicyUnsupportedError(
1287
+ `claude adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
1288
+ );
1289
+ }
1290
+ return selection.modelId;
1291
+ }
1146
1292
  var ClaudeSession = class {
1147
- constructor(sessionRef, client, workspaceDir, approvalChannel, approvalMcpConfigDir) {
1293
+ constructor(sessionRef, client, workspaceDir, approvalChannel, mcpConfigDir, modelId) {
1148
1294
  this.sessionRef = sessionRef;
1149
1295
  this.client = client;
1150
1296
  this.workspaceDir = workspaceDir;
1151
1297
  this.approvalChannel = approvalChannel;
1152
- this.approvalMcpConfigDir = approvalMcpConfigDir;
1298
+ this.mcpConfigDir = mcpConfigDir;
1299
+ this.modelId = modelId;
1153
1300
  }
1154
1301
  sessionRef;
1155
1302
  client;
1156
1303
  workspaceDir;
1157
1304
  approvalChannel;
1158
- approvalMcpConfigDir;
1305
+ mcpConfigDir;
1306
+ modelId;
1159
1307
  correlation = createToolUseCorrelation();
1160
1308
  get events() {
1161
1309
  const client = this.client;
@@ -1206,6 +1354,12 @@ var ClaudeSession = class {
1206
1354
  if (typeof task.instruction !== "string") {
1207
1355
  throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
1208
1356
  }
1357
+ const requestedModel = subscriptionModel(task, "claude");
1358
+ if (requestedModel !== void 0 && requestedModel !== this.modelId) {
1359
+ throw new PolicyUnsupportedError(
1360
+ `claude persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
1361
+ );
1362
+ }
1209
1363
  this.client.writeUserMessage(task.instruction);
1210
1364
  }
1211
1365
  /**
@@ -1225,7 +1379,7 @@ var ClaudeSession = class {
1225
1379
  }
1226
1380
  async close() {
1227
1381
  this.client.kill();
1228
- await cleanupApprovalMcpConfigDir(this.approvalMcpConfigDir);
1382
+ await cleanupMcpConfigDir(this.mcpConfigDir);
1229
1383
  }
1230
1384
  /**
1231
1385
  * M4 Phase 3: routes into the out-of-band approval channel `start()`
@@ -1389,8 +1543,8 @@ function extractArtifactEvents(changes, workspaceDir) {
1389
1543
  const absolutePath = typeof change.path === "string" ? change.path : void 0;
1390
1544
  const kind = typeof change.kind === "string" ? change.kind : void 0;
1391
1545
  if (!absolutePath || kind === "delete") continue;
1392
- const relative = path2.relative(workspaceDir, absolutePath);
1393
- if (relative.length === 0 || relative.startsWith("..") || path2.isAbsolute(relative)) continue;
1546
+ const relative = path3.relative(workspaceDir, absolutePath);
1547
+ if (relative.length === 0 || relative.startsWith("..") || path3.isAbsolute(relative)) continue;
1394
1548
  events.push({ type: "artifact", name: relative, contentType: guessContentType2(relative) });
1395
1549
  }
1396
1550
  return events;
@@ -1411,7 +1565,7 @@ var CONTENT_TYPE_BY_EXTENSION = {
1411
1565
  ".csv": "text/csv"
1412
1566
  };
1413
1567
  function guessContentType2(relativePath) {
1414
- return CONTENT_TYPE_BY_EXTENSION[path2.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
1568
+ return CONTENT_TYPE_BY_EXTENSION[path3.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
1415
1569
  }
1416
1570
  function extractErrorMessage(rawError) {
1417
1571
  if (typeof rawError === "string") return rawError;
@@ -1549,6 +1703,7 @@ var CodexAdapter = class {
1549
1703
  this.options = options;
1550
1704
  }
1551
1705
  options;
1706
+ supportsDispatchSelection = true;
1552
1707
  id = "codex";
1553
1708
  async detect() {
1554
1709
  const bin = this.resolveBin();
@@ -1572,9 +1727,10 @@ var CodexAdapter = class {
1572
1727
  * Two independently-verified channel gotchas apply here, the "pi lesson"
1573
1728
  * yet again:
1574
1729
  * - `codex login status`'s human-readable "Logged in using ChatGPT"
1575
- * message prints on STDERR, not stdout (the opposite-channel
1576
- * counterpart of pi's own `--version`-goes-to-stderr surprise) both
1577
- * streams are checked here for exactly that reason.
1730
+ * message prints on STDERR, not stdout both streams are checked
1731
+ * here for exactly that reason. pi's `--version` is the same class of
1732
+ * hazard from the other direction: its channel has moved between pi
1733
+ * releases (see ../pi/pi-adapter.ts), so neither stream is assumed.
1578
1734
  * - The NOT-logged-in message/exit-code shape was deliberately never
1579
1735
  * empirically tested: this machine has a real, live ChatGPT login, and
1580
1736
  * running `codex logout` to observe the negative case would have
@@ -1620,17 +1776,20 @@ ${withStreams.stderr ?? ""}`);
1620
1776
  if (!mapping.ok) {
1621
1777
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
1622
1778
  }
1779
+ const modelId = subscriptionModel2(task);
1623
1780
  const bin = this.resolveBin();
1624
1781
  const queue = new AsyncQueue();
1625
1782
  const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
1626
1783
  const workspaceDir = await resolveRealWorkspaceDir(ctx.workspaceDir);
1784
+ const runtimeEnv = withoutProviderCredentials(ctx.env);
1627
1785
  const { sessionRef, runner } = await runCodexTurn({
1628
1786
  command: bin.command,
1629
1787
  resumeRef: task.sessionRef,
1630
1788
  instruction: task.instruction,
1789
+ modelId,
1631
1790
  policyArgs: mapping.args,
1632
1791
  cwd: ctx.workspaceDir,
1633
- env: ctx.env,
1792
+ env: runtimeEnv,
1634
1793
  spawnFn: this.options.spawnFn,
1635
1794
  workspaceDir,
1636
1795
  queue,
@@ -1647,7 +1806,8 @@ ${withStreams.stderr ?? ""}`);
1647
1806
  queue,
1648
1807
  recordUnmapped,
1649
1808
  initialRunner: runner,
1650
- preparedGit: ctx.gitWorkspace !== void 0
1809
+ preparedGit: ctx.gitWorkspace !== void 0,
1810
+ modelId
1651
1811
  });
1652
1812
  }
1653
1813
  resolveBin() {
@@ -1668,12 +1828,25 @@ function makeUnmappedFrameRecorder(counts) {
1668
1828
  }
1669
1829
  };
1670
1830
  }
1671
- function buildArgv(resumeRef, policyArgs, instruction, preparedGit = false) {
1831
+ function buildArgv(resumeRef, policyArgs, instruction, modelId, preparedGit = false) {
1672
1832
  const base = resumeRef !== void 0 ? ["exec", "resume", resumeRef] : ["exec"];
1673
- return [...base, "--json", ...preparedGit ? [] : ["--skip-git-repo-check"], ...policyArgs, instruction];
1833
+ return [
1834
+ ...base,
1835
+ "--json",
1836
+ ...modelId ? ["--model", modelId] : [],
1837
+ ...preparedGit ? [] : ["--skip-git-repo-check"],
1838
+ ...policyArgs,
1839
+ instruction
1840
+ ];
1674
1841
  }
1675
1842
  async function runCodexTurn(params) {
1676
- const argv = buildArgv(params.resumeRef, params.policyArgs, params.instruction, params.preparedGit);
1843
+ const argv = buildArgv(
1844
+ params.resumeRef,
1845
+ params.policyArgs,
1846
+ params.instruction,
1847
+ params.modelId,
1848
+ params.preparedGit
1849
+ );
1677
1850
  let firstLineSettled = false;
1678
1851
  let resolveFirstLine;
1679
1852
  let rejectFirstLine;
@@ -1774,6 +1947,7 @@ var CodexSession = class {
1774
1947
  queue;
1775
1948
  recordUnmapped;
1776
1949
  preparedGit;
1950
+ modelId;
1777
1951
  currentRunner;
1778
1952
  closed = false;
1779
1953
  constructor(options) {
@@ -1785,6 +1959,7 @@ var CodexSession = class {
1785
1959
  this.queue = options.queue;
1786
1960
  this.recordUnmapped = options.recordUnmapped;
1787
1961
  this.preparedGit = options.preparedGit;
1962
+ this.modelId = options.modelId;
1788
1963
  this.currentRunner = options.initialRunner;
1789
1964
  void this.forgetRunnerOnceClosed(options.initialRunner);
1790
1965
  }
@@ -1845,6 +2020,13 @@ var CodexSession = class {
1845
2020
  if (!mapping.ok) {
1846
2021
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
1847
2022
  }
2023
+ const requestedModel = subscriptionModel2(task);
2024
+ if (requestedModel !== void 0 && requestedModel !== this.modelId) {
2025
+ throw new PolicyUnsupportedError(
2026
+ `codex persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
2027
+ );
2028
+ }
2029
+ const modelId = this.modelId;
1848
2030
  const resumeRef = this.sessionRef;
1849
2031
  let sessionRef;
1850
2032
  let runner;
@@ -1853,9 +2035,10 @@ var CodexSession = class {
1853
2035
  command: this.command,
1854
2036
  resumeRef,
1855
2037
  instruction: task.instruction,
2038
+ modelId,
1856
2039
  policyArgs: mapping.args,
1857
2040
  cwd: this.workspaceDir,
1858
- env: this.env,
2041
+ env: withoutProviderCredentials(this.env),
1859
2042
  spawnFn: this.spawnFn,
1860
2043
  workspaceDir: this.workspaceDir,
1861
2044
  queue: this.queue,
@@ -1915,6 +2098,16 @@ var CodexSession = class {
1915
2098
  );
1916
2099
  }
1917
2100
  };
2101
+ function subscriptionModel2(task) {
2102
+ const selection = task.dispatchSelection;
2103
+ if (selection === void 0) return void 0;
2104
+ if (selection.lane !== "subscription" || selection.runtimeId !== "codex") {
2105
+ throw new PolicyUnsupportedError(
2106
+ `codex adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
2107
+ );
2108
+ }
2109
+ return selection.modelId;
2110
+ }
1918
2111
 
1919
2112
  export { ClaudeAdapter, CodexAdapter, PI_PACKAGE_NAME, PiAdapter };
1920
2113
  //# sourceMappingURL=index.js.map