@byok-sdk/client 0.2.0 → 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.
package/README.md CHANGED
@@ -5,16 +5,56 @@ WebSocket or long poll, dispatches to local Claude Code, Codex, or pi adapters,
5
5
  and exposes authenticated local diagnostics/control commands.
6
6
 
7
7
  The package installs `byok-agent` and `byok-approval-mcp` binaries. Provider
8
- credentials are not read by the dispatch plane; `@byok-sdk/keys` is separate.
8
+ credentials are not read by the dispatch plane; `@byok-sdk/keys` is a separate
9
+ install and keeps a zero dependency edge to this package.
9
10
 
10
- Pi is a required exact npm dependency and runs as an external Node subprocess;
11
- authenticate it with your own provider credentials. Claude Code and Codex
12
- remain user-installed runtimes. The SDK never reads or packages provider
13
- credentials. Hosts that only need runtime detection/composition can import the
11
+ Pi is a required exact npm dependency and runs as an external Node subprocess.
12
+ For an authoritative BYOK `dispatchSelection`, configure `piByokLauncher` with
13
+ the separately installed `byok-pi-provider-launcher`, the local non-secret
14
+ profile database path, and a stable Pi session directory. The client passes
15
+ only those paths plus provider/model ids; the launcher alone reads the OS
16
+ credential when required and spawns Pi. Both custody paths must be absolute;
17
+ missing launcher configuration fails closed.
18
+
19
+ Claude Code and Codex remain user-installed runtimes and use their own login
20
+ state. Hosts that only need runtime detection/composition can import the
14
21
  transport-free adapter surface:
15
22
 
16
23
  ```ts
17
24
  import { PiAdapter, ClaudeAdapter, CodexAdapter } from '@byok-sdk/client/adapters';
18
25
  ```
19
26
 
27
+ Claude tasks can select operator-owned local stdio MCP servers by logical id.
28
+ The toolset selector carries no MCP command or connector credential:
29
+
30
+ ```ts
31
+ import { createDaemon } from '@byok-sdk/client';
32
+
33
+ createDaemon({
34
+ // ...normal device and transport configuration
35
+ mcpToolsets: {
36
+ 'salesko.prospecting': {
37
+ mcpServers: {
38
+ 'salesko-connectors': {
39
+ command: '/opt/salesko/bin/connector-mcp',
40
+ args: ['--profile', 'default'],
41
+ },
42
+ },
43
+ },
44
+ },
45
+ });
46
+ ```
47
+
48
+ The map accepts only `command` and `args`; put OAuth tokens, cookies, and other
49
+ secrets behind the local MCP process's own credential broker. Toolset offers
50
+ for Pi or Codex are declined because those adapters do not yet expose a strict
51
+ task-scoped MCP configuration boundary.
52
+
53
+ For a concrete private host composition, see the
54
+ [`examples/salesko-connector-broker`](../../examples/salesko-connector-broker)
55
+ reference. It keeps `@byok-sdk/client` credential-blind while combining
56
+ OS-backed refresh-token custody, a PKCE desktop Google OAuth flow, exact domain
57
+ policy, a real read-only Gmail metadata adapter, and a closed metadata-only MCP
58
+ result.
59
+
20
60
  MIT licensed. Node.js 22.19.0 or newer.
@@ -3,6 +3,8 @@ import { type RuntimeAdapter, type RuntimeCapabilities, type RuntimeDetectResult
3
3
  import { type ResolvedBin } from './resolve-bin';
4
4
  import { type ResolvedApprovalMcpBin } from './resolve-approval-mcp-bin';
5
5
  import { type SpawnFn } from './process-client';
6
+ /** The MCP server NAME this adapter registers `byok-approval-mcp` under in the generated `--mcp-config` (arbitrary, local to this file) — combined with {@link APPROVAL_TOOL_NAME} (imported, single-sourced from `bin/approval-mcp-server.ts` so the two can never independently drift) to form the `mcp__<server>__<tool>` identifier `--permission-prompt-tool` expects. */
7
+ export declare const APPROVAL_MCP_SERVER_NAME = "byokapproval";
6
8
  export interface ClaudeAdapterOptions {
7
9
  /** Override bin resolution — tests substitute the fake-claude fixture script. */
8
10
  resolveBin?: () => ResolvedBin;
@@ -112,6 +114,7 @@ export interface ClaudeAdapterOptions {
112
114
  */
113
115
  export declare class ClaudeAdapter implements RuntimeAdapter {
114
116
  private readonly options;
117
+ readonly supportsDispatchSelection = true;
115
118
  readonly id = "claude";
116
119
  constructor(options?: ClaudeAdapterOptions);
117
120
  detect(): Promise<RuntimeDetectResult>;
@@ -52,6 +52,7 @@ export interface CodexAdapterOptions {
52
52
  */
53
53
  export declare class CodexAdapter implements RuntimeAdapter {
54
54
  private readonly options;
55
+ readonly supportsDispatchSelection = true;
55
56
  readonly id = "codex";
56
57
  constructor(options?: CodexAdapterOptions);
57
58
  detect(): Promise<RuntimeDetectResult>;
@@ -1,7 +1,7 @@
1
1
  export type { RuntimeAdapter, RuntimeCapabilities, RuntimeDetectResult, } from '../types';
2
2
  export type { RuntimeEnvironmentRequirements } from '../daemon/environment';
3
3
  export { PiAdapter } from './pi/pi-adapter';
4
- export type { PiAdapterOptions } from './pi/pi-adapter';
4
+ export type { PiAdapterOptions, PiByokLauncherConfig } from './pi/pi-adapter';
5
5
  export { PI_PACKAGE_NAME } from './pi/resolve-bin';
6
6
  export { ClaudeAdapter } from './claude/claude-adapter';
7
7
  export type { ClaudeAdapterOptions } from './claude/claude-adapter';
@@ -441,13 +441,8 @@ var PiRpcClient = class {
441
441
  }
442
442
  };
443
443
 
444
- // src/adapters/pi/pi-adapter.ts
445
- var execFileAsync = promisify(execFile);
446
- var DETECT_TIMEOUT_MS = 5e3;
447
- function errorMessage(err) {
448
- return err instanceof Error ? err.message : String(err);
449
- }
450
- var KNOWN_PROVIDER_ENV_VARS = [
444
+ // src/adapters/provider-credential-environment.ts
445
+ var PROVIDER_CREDENTIAL_ENV_NAMES = [
451
446
  "ANTHROPIC_API_KEY",
452
447
  "ANTHROPIC_OAUTH_TOKEN",
453
448
  "OPENAI_API_KEY",
@@ -458,24 +453,66 @@ var KNOWN_PROVIDER_ENV_VARS = [
458
453
  "MISTRAL_API_KEY",
459
454
  "OPENROUTER_API_KEY",
460
455
  "XAI_API_KEY",
461
- // Confirmed against the installed pi's own docs/providers.md ("ZAI |
462
- // `ZAI_API_KEY` | `zai`") and exercised live against real GLM traffic
463
- // during this task's acceptance run — omitting it made `authPresent`
464
- // silently false for a perfectly valid, working z.ai/GLM setup.
465
456
  "ZAI_API_KEY"
466
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
+ }
467
503
  var PiAdapter = class {
468
504
  constructor(options = {}) {
469
505
  this.options = options;
470
506
  }
471
507
  options;
472
508
  id = "pi";
509
+ supportsDispatchSelection = true;
473
510
  async detect() {
474
511
  try {
475
512
  const bin = this.resolveBin();
476
513
  const { stdout, stderr } = await execFileAsync(bin.command, ["--version"], { timeout: DETECT_TIMEOUT_MS });
477
514
  const version = stdout.trim() || stderr.trim();
478
- 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);
479
516
  return { present: true, version, authPresent };
480
517
  } catch {
481
518
  return { present: false };
@@ -494,7 +531,7 @@ var PiAdapter = class {
494
531
  * variable beyond the platform baseline (`daemon/environment.ts`).
495
532
  */
496
533
  environmentRequirements() {
497
- return { credentialNames: KNOWN_PROVIDER_ENV_VARS };
534
+ return { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES };
498
535
  }
499
536
  async start(task, ctx) {
500
537
  if (typeof task.instruction !== "string") {
@@ -506,12 +543,45 @@ var PiAdapter = class {
506
543
  }
507
544
  const bin = this.resolveBin();
508
545
  const resumeSessionId = task.sessionRef;
509
- 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
+ }
510
580
  const rpc = new PiRpcClient({
511
- command: bin.command,
581
+ command,
512
582
  args,
513
583
  cwd: ctx.workspaceDir,
514
- env: ctx.env,
584
+ env: selection === void 0 ? ctx.env : withoutProviderCredentials(ctx.env),
515
585
  spawnFn: this.options.spawnFn
516
586
  });
517
587
  const response = await rpc.send({ type: "prompt", message: task.instruction });
@@ -530,7 +600,7 @@ var PiAdapter = class {
530
600
  throw err;
531
601
  }
532
602
  }
533
- return new PiSession(sessionRef, rpc);
603
+ return new PiSession(sessionRef, rpc, selection);
534
604
  }
535
605
  resolveBin() {
536
606
  return (this.options.resolveBin ?? resolvePiBin)();
@@ -558,12 +628,14 @@ async function resolveFreshSessionId(rpc) {
558
628
  );
559
629
  }
560
630
  var PiSession = class {
561
- constructor(sessionRef, rpc) {
631
+ constructor(sessionRef, rpc, selection) {
562
632
  this.sessionRef = sessionRef;
563
633
  this.rpc = rpc;
634
+ this.selection = selection;
564
635
  }
565
636
  sessionRef;
566
637
  rpc;
638
+ selection;
567
639
  get events() {
568
640
  const rpc = this.rpc;
569
641
  return {
@@ -592,6 +664,12 @@ var PiSession = class {
592
664
  if (typeof task.instruction !== "string") {
593
665
  throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
594
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
+ }
595
673
  await this.rpc.send({ type: "prompt", message: task.instruction, streamingBehavior: "followUp" });
596
674
  }
597
675
  async interrupt() {
@@ -1016,7 +1094,7 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
1016
1094
  var APPROVAL_MCP_SERVER_NAME = "byokapproval";
1017
1095
  var execFileAsync2 = promisify(execFile);
1018
1096
  var DETECT_TIMEOUT_MS2 = 5e3;
1019
- async function cleanupApprovalMcpConfigDir(dir) {
1097
+ async function cleanupMcpConfigDir(dir) {
1020
1098
  if (!dir) return;
1021
1099
  await promises.rm(dir, { recursive: true, force: true }).catch(() => {
1022
1100
  });
@@ -1026,6 +1104,7 @@ var ClaudeAdapter = class {
1026
1104
  this.options = options;
1027
1105
  }
1028
1106
  options;
1107
+ supportsDispatchSelection = true;
1029
1108
  id = "claude";
1030
1109
  async detect() {
1031
1110
  const bin = this.resolveBin();
@@ -1039,7 +1118,13 @@ var ClaudeAdapter = class {
1039
1118
  }
1040
1119
  }
1041
1120
  capabilities() {
1042
- 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
+ };
1043
1128
  }
1044
1129
  /**
1045
1130
  * M5: deliberate product-boundary decision, not an oversight — byok's
@@ -1065,42 +1150,51 @@ var ClaudeAdapter = class {
1065
1150
  if (!mapping.ok) {
1066
1151
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
1067
1152
  }
1068
- 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;
1069
1157
  if (mapping.needsApprovalMcp) {
1070
1158
  if (!ctx.approvalChannel) {
1071
1159
  throw new PolicyUnsupportedError(
1072
1160
  'claude adapter requires policy.mode "confirm" to be started with an approval channel (TaskContext.approvalChannel) \u2014 none was provided'
1073
1161
  );
1074
1162
  }
1075
- const approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
1076
- approvalMcpConfigDir = await promises.mkdtemp(path3.join(os.tmpdir(), "byok-approval-mcp-"));
1077
- 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(() => {
1078
1172
  });
1079
- const mcpConfigPath = path3.join(approvalMcpConfigDir, "mcp-config.json");
1080
- const mcpConfig = {
1081
- mcpServers: {
1082
- [APPROVAL_MCP_SERVER_NAME]: {
1083
- command: approvalMcpBin.command,
1084
- args: approvalMcpBin.args,
1085
- env: {
1086
- BYOK_STORE_DIR: ctx.approvalChannel.storeDir,
1087
- BYOK_PRODUCT_ID: ctx.approvalChannel.productId,
1088
- BYOK_TASK_ID: ctx.approvalChannel.taskId,
1089
- BYOK_APPROVAL_TIMEOUT_MS: String(ctx.approvalChannel.timeoutMs)
1090
- }
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)
1091
1187
  }
1092
- }
1093
- };
1094
- await promises.writeFile(mcpConfigPath, JSON.stringify(mcpConfig), { mode: 384 });
1188
+ };
1189
+ }
1190
+ await promises.writeFile(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
1095
1191
  mapping.args = [
1096
1192
  ...mapping.args,
1097
- "--permission-prompt-tool",
1098
- `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`,
1193
+ ...mapping.needsApprovalMcp ? ["--permission-prompt-tool", `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`] : [],
1099
1194
  "--mcp-config",
1100
1195
  mcpConfigPath,
1101
- // Never let this task's confirm-mode run pick up some OTHER MCP
1102
- // server from ambient project/user config the approval channel is
1103
- // 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.
1104
1198
  "--strict-mcp-config"
1105
1199
  ];
1106
1200
  }
@@ -1117,6 +1211,7 @@ var ClaudeAdapter = class {
1117
1211
  // "Error: When using --print, --output-format=stream-json requires
1118
1212
  // --verbose", before spawning any model call.
1119
1213
  "--verbose",
1214
+ ...modelId ? ["--model", modelId] : [],
1120
1215
  ...resumeSessionId ? ["--resume", resumeSessionId] : [],
1121
1216
  ...mapping.args
1122
1217
  ];
@@ -1124,7 +1219,7 @@ var ClaudeAdapter = class {
1124
1219
  command: bin.command,
1125
1220
  args,
1126
1221
  cwd: ctx.workspaceDir,
1127
- env: ctx.env,
1222
+ env: withoutProviderCredentials(ctx.env),
1128
1223
  spawnFn: this.options.spawnFn
1129
1224
  });
1130
1225
  client.writeUserMessage(task.instruction);
@@ -1133,17 +1228,24 @@ var ClaudeAdapter = class {
1133
1228
  sessionRef = await client.waitForInit();
1134
1229
  } catch (err) {
1135
1230
  client.kill();
1136
- await cleanupApprovalMcpConfigDir(approvalMcpConfigDir);
1231
+ await cleanupMcpConfigDir(mcpConfigDir);
1137
1232
  throw err;
1138
1233
  }
1139
1234
  if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
1140
1235
  client.kill();
1141
- await cleanupApprovalMcpConfigDir(approvalMcpConfigDir);
1236
+ await cleanupMcpConfigDir(mcpConfigDir);
1142
1237
  throw new Error(
1143
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)`
1144
1239
  );
1145
1240
  }
1146
- 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
+ );
1147
1249
  }
1148
1250
  /**
1149
1251
  * `claude auth status --json` is claude's OWN non-secret login-state
@@ -1177,19 +1279,31 @@ var ClaudeAdapter = class {
1177
1279
  return (this.options.resolveBin ?? resolveClaudeBin)();
1178
1280
  }
1179
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
+ }
1180
1292
  var ClaudeSession = class {
1181
- constructor(sessionRef, client, workspaceDir, approvalChannel, approvalMcpConfigDir) {
1293
+ constructor(sessionRef, client, workspaceDir, approvalChannel, mcpConfigDir, modelId) {
1182
1294
  this.sessionRef = sessionRef;
1183
1295
  this.client = client;
1184
1296
  this.workspaceDir = workspaceDir;
1185
1297
  this.approvalChannel = approvalChannel;
1186
- this.approvalMcpConfigDir = approvalMcpConfigDir;
1298
+ this.mcpConfigDir = mcpConfigDir;
1299
+ this.modelId = modelId;
1187
1300
  }
1188
1301
  sessionRef;
1189
1302
  client;
1190
1303
  workspaceDir;
1191
1304
  approvalChannel;
1192
- approvalMcpConfigDir;
1305
+ mcpConfigDir;
1306
+ modelId;
1193
1307
  correlation = createToolUseCorrelation();
1194
1308
  get events() {
1195
1309
  const client = this.client;
@@ -1240,6 +1354,12 @@ var ClaudeSession = class {
1240
1354
  if (typeof task.instruction !== "string") {
1241
1355
  throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
1242
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
+ }
1243
1363
  this.client.writeUserMessage(task.instruction);
1244
1364
  }
1245
1365
  /**
@@ -1259,7 +1379,7 @@ var ClaudeSession = class {
1259
1379
  }
1260
1380
  async close() {
1261
1381
  this.client.kill();
1262
- await cleanupApprovalMcpConfigDir(this.approvalMcpConfigDir);
1382
+ await cleanupMcpConfigDir(this.mcpConfigDir);
1263
1383
  }
1264
1384
  /**
1265
1385
  * M4 Phase 3: routes into the out-of-band approval channel `start()`
@@ -1583,6 +1703,7 @@ var CodexAdapter = class {
1583
1703
  this.options = options;
1584
1704
  }
1585
1705
  options;
1706
+ supportsDispatchSelection = true;
1586
1707
  id = "codex";
1587
1708
  async detect() {
1588
1709
  const bin = this.resolveBin();
@@ -1655,17 +1776,20 @@ ${withStreams.stderr ?? ""}`);
1655
1776
  if (!mapping.ok) {
1656
1777
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
1657
1778
  }
1779
+ const modelId = subscriptionModel2(task);
1658
1780
  const bin = this.resolveBin();
1659
1781
  const queue = new AsyncQueue();
1660
1782
  const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
1661
1783
  const workspaceDir = await resolveRealWorkspaceDir(ctx.workspaceDir);
1784
+ const runtimeEnv = withoutProviderCredentials(ctx.env);
1662
1785
  const { sessionRef, runner } = await runCodexTurn({
1663
1786
  command: bin.command,
1664
1787
  resumeRef: task.sessionRef,
1665
1788
  instruction: task.instruction,
1789
+ modelId,
1666
1790
  policyArgs: mapping.args,
1667
1791
  cwd: ctx.workspaceDir,
1668
- env: ctx.env,
1792
+ env: runtimeEnv,
1669
1793
  spawnFn: this.options.spawnFn,
1670
1794
  workspaceDir,
1671
1795
  queue,
@@ -1682,7 +1806,8 @@ ${withStreams.stderr ?? ""}`);
1682
1806
  queue,
1683
1807
  recordUnmapped,
1684
1808
  initialRunner: runner,
1685
- preparedGit: ctx.gitWorkspace !== void 0
1809
+ preparedGit: ctx.gitWorkspace !== void 0,
1810
+ modelId
1686
1811
  });
1687
1812
  }
1688
1813
  resolveBin() {
@@ -1703,12 +1828,25 @@ function makeUnmappedFrameRecorder(counts) {
1703
1828
  }
1704
1829
  };
1705
1830
  }
1706
- function buildArgv(resumeRef, policyArgs, instruction, preparedGit = false) {
1831
+ function buildArgv(resumeRef, policyArgs, instruction, modelId, preparedGit = false) {
1707
1832
  const base = resumeRef !== void 0 ? ["exec", "resume", resumeRef] : ["exec"];
1708
- 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
+ ];
1709
1841
  }
1710
1842
  async function runCodexTurn(params) {
1711
- 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
+ );
1712
1850
  let firstLineSettled = false;
1713
1851
  let resolveFirstLine;
1714
1852
  let rejectFirstLine;
@@ -1809,6 +1947,7 @@ var CodexSession = class {
1809
1947
  queue;
1810
1948
  recordUnmapped;
1811
1949
  preparedGit;
1950
+ modelId;
1812
1951
  currentRunner;
1813
1952
  closed = false;
1814
1953
  constructor(options) {
@@ -1820,6 +1959,7 @@ var CodexSession = class {
1820
1959
  this.queue = options.queue;
1821
1960
  this.recordUnmapped = options.recordUnmapped;
1822
1961
  this.preparedGit = options.preparedGit;
1962
+ this.modelId = options.modelId;
1823
1963
  this.currentRunner = options.initialRunner;
1824
1964
  void this.forgetRunnerOnceClosed(options.initialRunner);
1825
1965
  }
@@ -1880,6 +2020,13 @@ var CodexSession = class {
1880
2020
  if (!mapping.ok) {
1881
2021
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
1882
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;
1883
2030
  const resumeRef = this.sessionRef;
1884
2031
  let sessionRef;
1885
2032
  let runner;
@@ -1888,9 +2035,10 @@ var CodexSession = class {
1888
2035
  command: this.command,
1889
2036
  resumeRef,
1890
2037
  instruction: task.instruction,
2038
+ modelId,
1891
2039
  policyArgs: mapping.args,
1892
2040
  cwd: this.workspaceDir,
1893
- env: this.env,
2041
+ env: withoutProviderCredentials(this.env),
1894
2042
  spawnFn: this.spawnFn,
1895
2043
  workspaceDir: this.workspaceDir,
1896
2044
  queue: this.queue,
@@ -1950,6 +2098,16 @@ var CodexSession = class {
1950
2098
  );
1951
2099
  }
1952
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
+ }
1953
2111
 
1954
2112
  export { ClaudeAdapter, CodexAdapter, PI_PACKAGE_NAME, PiAdapter };
1955
2113
  //# sourceMappingURL=index.js.map