@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
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import { execFile, spawn, spawnSync } from 'child_process';
2
- import { createHash, randomUUID, sign, createPrivateKey, generateKeyPairSync, randomBytes, createHmac, timingSafeEqual } from 'crypto';
3
- import { promises, mkdirSync, existsSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, linkSync, fstatSync, lstatSync, unlinkSync, constants, realpathSync } from 'fs';
4
- import path15, { join, isAbsolute } from 'path';
5
- import os6 from 'os';
6
- import { partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, createEnvelope, MAX_MESSAGES_PER_BATCH, RuntimeIdSchema, PROTOCOL_VERSION, decodeEnvelope, MessagesSendResponseSchema, parseMessage, UnknownMessageTypeError } from '@byok-sdk/protocol';
2
+ import { createHash, randomUUID, sign, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual, createHmac } from 'crypto';
3
+ import { promises, mkdirSync, existsSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, linkSync, fstatSync, lstatSync, unlinkSync, constants, readFileSync, realpathSync } from 'fs';
4
+ import path17, { join, isAbsolute } from 'path';
5
+ import os5 from 'os';
6
+ import { parseDeviceAssertionEnvelope, tenantId, DeviceProofProtectedClaimsSchema, deviceProofSigningInput, DEVICE_PROOF_SCHEMA_ID, SKILL_PACK_MAX_BYTES, hasCapability, parseSkillPackManifest, checkSkillPackManifest, skillPackContentHashInput, checkSkillPackFileContent, SKILL_PACK_ENTRY_PATH, checkSkillPackEntry, isSkillPackPathSafe, DEVICE_PROOF_HEADER, contentHash, TRUTH_RECORD_KINDS, nonceSigningBytes, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, CONTENT_HASH_PATTERN, CapabilityDeclarationSchema, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
7
+ import { BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, ToolsetIdSchema, encodeEnvelope, createEnvelope, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, RESULT_DOCUMENT_MAX_BYTES, PROTOCOL_VERSION, decodeEnvelope, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, BYOK_EVENTS_PATH, parseMessage, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
7
8
  import { promisify } from 'util';
8
9
  import { fileURLToPath } from 'url';
9
10
  import 'readline';
10
11
  import net, { createServer, createConnection } from 'net';
11
12
  import { WebSocket } from 'ws';
12
13
  import { createRequire } from 'module';
13
- import { tenantId, DeviceProofProtectedClaimsSchema, deviceProofSigningInput, DEVICE_PROOF_SCHEMA_ID, contentHash, TRUTH_RECORD_KINDS } from '@byok-sdk/core';
14
14
 
15
15
  // src/types.ts
16
16
  var PolicyUnsupportedError = class extends Error {
@@ -71,7 +71,7 @@ function gitEnvironment(readOnly) {
71
71
  return env;
72
72
  }
73
73
  function stableGitWorkspaceOwnerId(storeDir, productId) {
74
- const identity = `${path15.resolve(storeDir)}\\0${productId}`;
74
+ const identity = `${path17.resolve(storeDir)}\\0${productId}`;
75
75
  return `store-product:${createHash("sha256").update(identity).digest("hex")}`;
76
76
  }
77
77
  var GUIDANCE = [
@@ -83,11 +83,11 @@ var GUIDANCE = [
83
83
  "Leave incomplete work visible for recovery."
84
84
  ].join("\n");
85
85
  function canonical(value) {
86
- return path15.resolve(value);
86
+ return path17.resolve(value);
87
87
  }
88
88
  function isContained(root, candidate) {
89
- const relative = path15.relative(root, candidate);
90
- return relative === "" || !relative.startsWith(`..${path15.sep}`) && !path15.isAbsolute(relative);
89
+ const relative = path17.relative(root, candidate);
90
+ return relative === "" || !relative.startsWith(`..${path17.sep}`) && !path17.isAbsolute(relative);
91
91
  }
92
92
  function bounded(value, max) {
93
93
  return Buffer.byteLength(value, "utf8") <= max ? value : value.slice(0, max);
@@ -192,7 +192,7 @@ var GitWorkspaceManager = class {
192
192
  await this.ensureOwnerMarker();
193
193
  }
194
194
  async ensureOwnerMarker() {
195
- const markerPath = path15.join(this.workspaceRoot, OWNER_MARKER);
195
+ const markerPath = path17.join(this.workspaceRoot, OWNER_MARKER);
196
196
  let existing;
197
197
  try {
198
198
  existing = JSON.parse(await promises.readFile(markerPath, "utf8"));
@@ -351,7 +351,7 @@ ${instruction}`;
351
351
  if (error instanceof GitWorkspaceError || code !== "ENOENT" && code !== "ENOTDIR") {
352
352
  throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
353
353
  }
354
- const parent = path15.dirname(current);
354
+ const parent = path17.dirname(current);
355
355
  if (parent === current) {
356
356
  throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
357
357
  }
@@ -416,7 +416,7 @@ async function atomicWriteFile(filePath, data, options = {}) {
416
416
  await target.close();
417
417
  }
418
418
  if (process.platform !== "win32") {
419
- const directory = await promises.open(path15.dirname(filePath), "r");
419
+ const directory = await promises.open(path17.dirname(filePath), "r");
420
420
  try {
421
421
  await directory.sync();
422
422
  } finally {
@@ -508,7 +508,7 @@ async function ensureSecureDir(dir, opts = {}) {
508
508
  await promises.chmod(dir, 448).catch(() => {
509
509
  });
510
510
  if (platform !== "win32") return;
511
- const { username } = os6.userInfo();
511
+ const { username } = os5.userInfo();
512
512
  let result;
513
513
  try {
514
514
  result = await run("icacls", buildIcaclsArgs(dir, username));
@@ -541,7 +541,7 @@ function isProtected(record) {
541
541
  var GitWorkspaceStore = class {
542
542
  constructor(storeDir, options = {}) {
543
543
  this.storeDir = storeDir;
544
- this.filePath = path15.join(storeDir, FILE_NAME);
544
+ this.filePath = path17.join(storeDir, FILE_NAME);
545
545
  this.maxRecords = Math.max(1, Math.floor(options.maxRecords ?? MAX_RECORDS));
546
546
  }
547
547
  storeDir;
@@ -671,20 +671,50 @@ var GitWorkspaceStore = class {
671
671
  await atomicWriteFile(this.filePath, JSON.stringify(ledger, null, 2), { mode: 384 });
672
672
  }
673
673
  };
674
-
675
- // src/adapters/pi/resolve-bin.ts
676
674
  var PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
675
+ function readPackageJson(dir) {
676
+ const candidate = path17.join(dir, "package.json");
677
+ if (!existsSync(candidate)) return void 0;
678
+ try {
679
+ return JSON.parse(readFileSync(candidate, "utf8"));
680
+ } catch {
681
+ return void 0;
682
+ }
683
+ }
677
684
  function resolvePiBin() {
678
685
  const override = process.env.BYOK_PI_BIN;
679
686
  if (override) {
680
- return { command: override, source: "path" };
687
+ return { command: override, source: "env" };
688
+ }
689
+ try {
690
+ const mainEntryUrl = import.meta.resolve(PI_PACKAGE_NAME);
691
+ let dir = path17.dirname(fileURLToPath(mainEntryUrl));
692
+ for (let depth = 0; depth < 6; depth++) {
693
+ const pkg = readPackageJson(dir);
694
+ if (pkg?.name === PI_PACKAGE_NAME) {
695
+ const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi;
696
+ if (binRel) {
697
+ return { command: path17.join(dir, binRel), source: "package" };
698
+ }
699
+ break;
700
+ }
701
+ const parent = path17.dirname(dir);
702
+ if (parent === dir) break;
703
+ dir = parent;
704
+ }
705
+ } catch (cause) {
706
+ throw new Error(
707
+ `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`,
708
+ { cause }
709
+ );
681
710
  }
682
- return { command: "pi", source: "path" };
711
+ throw new Error(
712
+ `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`
713
+ );
683
714
  }
684
715
 
685
716
  // src/adapters/pi/permission-mapping.ts
686
717
  var READONLY_TOOLS = ["read", "grep", "find", "ls"];
687
- var DEFAULT_ACTIVE_TOOLS = ["read", "bash", "edit", "write"];
688
718
  function mapPermissionPolicyToPiArgs(policy) {
689
719
  if (policy.network === false) {
690
720
  return {
@@ -703,22 +733,18 @@ function mapPermissionPolicyToPiArgs(policy) {
703
733
  const denyTools = policy.denyTools ?? [];
704
734
  if (policy.mode === "readonly") {
705
735
  const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS.includes(tool)) : [...READONLY_TOOLS];
706
- const effective = subtractDenied(base, denyTools);
707
- return { ok: true, args: effective.length === 0 ? ["--no-tools"] : ["--tools", effective.join(",")] };
708
- }
709
- if (denyTools.length > 0) {
710
- const base = policy.allowTools && policy.allowTools.length > 0 ? policy.allowTools : [...DEFAULT_ACTIVE_TOOLS];
711
- const effective = subtractDenied(base, denyTools);
712
- return { ok: true, args: effective.length === 0 ? ["--no-tools"] : ["--tools", effective.join(",")] };
736
+ if (base.length === 0) return { ok: true, args: ["--no-tools"] };
737
+ return {
738
+ ok: true,
739
+ args: ["--tools", base.join(","), ...denyTools.length > 0 ? ["--exclude-tools", denyTools.join(",")] : []]
740
+ };
713
741
  }
742
+ const args = [];
714
743
  if (policy.allowTools && policy.allowTools.length > 0) {
715
- return { ok: true, args: ["--tools", policy.allowTools.join(",")] };
744
+ args.push("--tools", policy.allowTools.join(","));
716
745
  }
717
- return { ok: true, args: [] };
718
- }
719
- function subtractDenied(tools, denyTools) {
720
- const denied = new Set(denyTools);
721
- return tools.filter((tool) => !denied.has(tool));
746
+ if (denyTools.length > 0) args.push("--exclude-tools", denyTools.join(","));
747
+ return { ok: true, args };
722
748
  }
723
749
 
724
750
  // src/adapters/pi/events.ts
@@ -743,7 +769,7 @@ function mapPiMessageToAgentEvent(msg) {
743
769
  output: { result: msg.result, isError: msg.isError === true }
744
770
  };
745
771
  }
746
- case "agent_end":
772
+ case "agent_settled":
747
773
  return { type: "turn_end" };
748
774
  /**
749
775
  * `artifact` is NOT a real pi RPC message — pi's own `write` tool only
@@ -779,16 +805,22 @@ function mapPiMessageToAgentEvent(msg) {
779
805
  // `recordUnmappedFrame`) can tell "known, expected, silently ignored"
780
806
  // apart from "genuinely never seen before" (falls to `default` below).
781
807
  case "agent_start":
808
+ case "agent_end":
809
+ // one low-level run; `agent_settled` is BYOK completion
782
810
  case "turn_start":
783
811
  case "turn_end":
784
- // pi's own per-LLM-turn boundary, not ours — see `agent_end` above
812
+ // pi's own per-LLM-turn boundary, not ours
785
813
  case "message_start":
786
814
  case "message_end":
815
+ case "bash_execution_update":
787
816
  case "tool_execution_update":
788
817
  case "queue_update":
789
818
  case "compaction_start":
790
819
  case "compaction_end":
791
820
  case "auto_retry_start":
821
+ case "summarization_retry_scheduled":
822
+ case "summarization_retry_attempt_start":
823
+ case "summarization_retry_finished":
792
824
  case "session_info_changed":
793
825
  case "thinking_level_changed":
794
826
  return void 0;
@@ -798,15 +830,20 @@ function mapPiMessageToAgentEvent(msg) {
798
830
  }
799
831
  var ROUTINE_PI_EVENT_TYPES = /* @__PURE__ */ new Set([
800
832
  "agent_start",
833
+ "agent_end",
801
834
  "turn_start",
802
835
  "turn_end",
803
836
  "message_start",
804
837
  "message_end",
838
+ "bash_execution_update",
805
839
  "tool_execution_update",
806
840
  "queue_update",
807
841
  "compaction_start",
808
842
  "compaction_end",
809
843
  "auto_retry_start",
844
+ "summarization_retry_scheduled",
845
+ "summarization_retry_attempt_start",
846
+ "summarization_retry_finished",
810
847
  "session_info_changed",
811
848
  "thinking_level_changed"
812
849
  ]);
@@ -940,10 +977,7 @@ var PiRpcClient = class {
940
977
  * traffic. Logs once per distinct type (not per occurrence, so a
941
978
  * repeating unmapped type can't spam stdout); the running tally is also
942
979
  * folded into this client's exit-time error message (`buildExitError`) so
943
- * a post-mortem on a failed/hung task has it without needing separate log
944
- * scraping. This is the exact mechanism that would have turned this
945
- * task's root-cause hang (`agent_end` arriving with no mapping) into a
946
- * one-line, immediate warning instead of a silent stall.
980
+ * a post-mortem on a failed/hung task has it without separate log scraping.
947
981
  */
948
982
  recordUnmappedFrame(type) {
949
983
  const next = (this.unmappedFrameCounts.get(type) ?? 0) + 1;
@@ -1054,13 +1088,8 @@ var PiRpcClient = class {
1054
1088
  }
1055
1089
  };
1056
1090
 
1057
- // src/adapters/pi/pi-adapter.ts
1058
- var execFileAsync = promisify(execFile);
1059
- var DETECT_TIMEOUT_MS = 5e3;
1060
- function errorMessage(err) {
1061
- return err instanceof Error ? err.message : String(err);
1062
- }
1063
- var KNOWN_PROVIDER_ENV_VARS = [
1091
+ // src/adapters/provider-credential-environment.ts
1092
+ var PROVIDER_CREDENTIAL_ENV_NAMES = [
1064
1093
  "ANTHROPIC_API_KEY",
1065
1094
  "ANTHROPIC_OAUTH_TOKEN",
1066
1095
  "OPENAI_API_KEY",
@@ -1071,24 +1100,66 @@ var KNOWN_PROVIDER_ENV_VARS = [
1071
1100
  "MISTRAL_API_KEY",
1072
1101
  "OPENROUTER_API_KEY",
1073
1102
  "XAI_API_KEY",
1074
- // Confirmed against the installed pi's own docs/providers.md ("ZAI |
1075
- // `ZAI_API_KEY` | `zai`") and exercised live against real GLM traffic
1076
- // during this task's acceptance run — omitting it made `authPresent`
1077
- // silently false for a perfectly valid, working z.ai/GLM setup.
1078
1103
  "ZAI_API_KEY"
1079
1104
  ];
1105
+ var PROVIDER_CREDENTIAL_ENV_DENY_NAMES = [
1106
+ ...PROVIDER_CREDENTIAL_ENV_NAMES,
1107
+ "ANT_LING_API_KEY",
1108
+ "NVIDIA_API_KEY",
1109
+ "CEREBRAS_API_KEY",
1110
+ "CLOUDFLARE_API_KEY",
1111
+ "AI_GATEWAY_API_KEY",
1112
+ "ZAI_CODING_CN_API_KEY",
1113
+ "OPENCODE_API_KEY",
1114
+ "RADIUS_API_KEY",
1115
+ "FIREWORKS_API_KEY",
1116
+ "TOGETHER_API_KEY",
1117
+ "BASETEN_API_KEY",
1118
+ "KIMI_API_KEY",
1119
+ "MINIMAX_API_KEY",
1120
+ "MINIMAX_CN_API_KEY",
1121
+ "QWEN_TOKEN_PLAN_API_KEY",
1122
+ "QWEN_TOKEN_PLAN_CN_API_KEY",
1123
+ "XIAOMI_API_KEY",
1124
+ "XIAOMI_TOKEN_PLAN_CN_API_KEY",
1125
+ "XIAOMI_TOKEN_PLAN_AMS_API_KEY",
1126
+ "XIAOMI_TOKEN_PLAN_SGP_API_KEY",
1127
+ "AWS_ACCESS_KEY_ID",
1128
+ "AWS_SECRET_ACCESS_KEY",
1129
+ "AWS_SESSION_TOKEN",
1130
+ "GOOGLE_APPLICATION_CREDENTIALS",
1131
+ // Reserved by the keys-owned Pi projection. It must never be inherited
1132
+ // from the daemon; the launcher deletes any ambient copy and injects only
1133
+ // the exact credential it just resolved from OS custody.
1134
+ "PI_PROVIDER_API_KEY"
1135
+ ];
1136
+ function withoutProviderCredentials(env) {
1137
+ const sanitized = { ...env };
1138
+ for (const name of PROVIDER_CREDENTIAL_ENV_DENY_NAMES) {
1139
+ delete sanitized[name];
1140
+ }
1141
+ return sanitized;
1142
+ }
1143
+
1144
+ // src/adapters/pi/pi-adapter.ts
1145
+ var execFileAsync = promisify(execFile);
1146
+ var DETECT_TIMEOUT_MS = 5e3;
1147
+ function errorMessage(err) {
1148
+ return err instanceof Error ? err.message : String(err);
1149
+ }
1080
1150
  var PiAdapter = class {
1081
1151
  constructor(options = {}) {
1082
1152
  this.options = options;
1083
1153
  }
1084
1154
  options;
1085
1155
  id = "pi";
1156
+ supportsDispatchSelection = true;
1086
1157
  async detect() {
1087
- const bin = this.resolveBin();
1088
1158
  try {
1159
+ const bin = this.resolveBin();
1089
1160
  const { stdout, stderr } = await execFileAsync(bin.command, ["--version"], { timeout: DETECT_TIMEOUT_MS });
1090
1161
  const version = stdout.trim() || stderr.trim();
1091
- const authPresent = KNOWN_PROVIDER_ENV_VARS.some((name) => process.env[name] !== void 0);
1162
+ const authPresent = PROVIDER_CREDENTIAL_ENV_NAMES.some((name) => process.env[name] !== void 0);
1092
1163
  return { present: true, version, authPresent };
1093
1164
  } catch {
1094
1165
  return { present: false };
@@ -1107,7 +1178,7 @@ var PiAdapter = class {
1107
1178
  * variable beyond the platform baseline (`daemon/environment.ts`).
1108
1179
  */
1109
1180
  environmentRequirements() {
1110
- return { credentialNames: KNOWN_PROVIDER_ENV_VARS };
1181
+ return { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES };
1111
1182
  }
1112
1183
  async start(task, ctx) {
1113
1184
  if (typeof task.instruction !== "string") {
@@ -1119,12 +1190,45 @@ var PiAdapter = class {
1119
1190
  }
1120
1191
  const bin = this.resolveBin();
1121
1192
  const resumeSessionId = task.sessionRef;
1122
- const args = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
1193
+ const piArgs = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
1194
+ const selection = task.dispatchSelection;
1195
+ let command = bin.command;
1196
+ let args = piArgs;
1197
+ if (selection !== void 0) {
1198
+ if (selection.lane !== "byok" || selection.runtimeId !== "pi") {
1199
+ throw new PolicyUnsupportedError(
1200
+ `pi adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
1201
+ );
1202
+ }
1203
+ const launcher = this.options.byokLauncher;
1204
+ if (launcher === void 0) {
1205
+ throw new PolicyUnsupportedError(
1206
+ "pi BYOK selection requires a configured credential-custody launcher"
1207
+ );
1208
+ }
1209
+ command = launcher.command;
1210
+ args = [
1211
+ ...launcher.args ?? [],
1212
+ "--pi-bin",
1213
+ bin.command,
1214
+ "--profile-db",
1215
+ launcher.profileDbPath,
1216
+ "--session-dir",
1217
+ launcher.sessionDir,
1218
+ ...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
1219
+ "--provider",
1220
+ selection.providerId,
1221
+ "--model",
1222
+ selection.modelId,
1223
+ "--",
1224
+ ...piArgs
1225
+ ];
1226
+ }
1123
1227
  const rpc = new PiRpcClient({
1124
- command: bin.command,
1228
+ command,
1125
1229
  args,
1126
1230
  cwd: ctx.workspaceDir,
1127
- env: ctx.env,
1231
+ env: selection === void 0 ? ctx.env : withoutProviderCredentials(ctx.env),
1128
1232
  spawnFn: this.options.spawnFn
1129
1233
  });
1130
1234
  const response = await rpc.send({ type: "prompt", message: task.instruction });
@@ -1143,7 +1247,7 @@ var PiAdapter = class {
1143
1247
  throw err;
1144
1248
  }
1145
1249
  }
1146
- return new PiSession(sessionRef, rpc);
1250
+ return new PiSession(sessionRef, rpc, selection);
1147
1251
  }
1148
1252
  resolveBin() {
1149
1253
  return (this.options.resolveBin ?? resolvePiBin)();
@@ -1171,12 +1275,14 @@ async function resolveFreshSessionId(rpc) {
1171
1275
  );
1172
1276
  }
1173
1277
  var PiSession = class {
1174
- constructor(sessionRef, rpc) {
1278
+ constructor(sessionRef, rpc, selection) {
1175
1279
  this.sessionRef = sessionRef;
1176
1280
  this.rpc = rpc;
1281
+ this.selection = selection;
1177
1282
  }
1178
1283
  sessionRef;
1179
1284
  rpc;
1285
+ selection;
1180
1286
  get events() {
1181
1287
  const rpc = this.rpc;
1182
1288
  return {
@@ -1205,6 +1311,12 @@ var PiSession = class {
1205
1311
  if (typeof task.instruction !== "string") {
1206
1312
  throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
1207
1313
  }
1314
+ const requestedSelection = task.dispatchSelection;
1315
+ if (requestedSelection !== void 0 && (this.selection?.lane !== "byok" || requestedSelection.lane !== "byok" || requestedSelection.runtimeId !== "pi" || requestedSelection.providerId !== this.selection.providerId || requestedSelection.modelId !== this.selection.modelId)) {
1316
+ throw new PolicyUnsupportedError(
1317
+ "pi persistent session cannot change its authoritative BYOK provider/model selection"
1318
+ );
1319
+ }
1208
1320
  await this.rpc.send({ type: "prompt", message: task.instruction, streamingBehavior: "followUp" });
1209
1321
  }
1210
1322
  async interrupt() {
@@ -1231,7 +1343,7 @@ function resolveApprovalMcpBin() {
1231
1343
  if (override) {
1232
1344
  return { command: override, args: [], source: "env" };
1233
1345
  }
1234
- const distBin = path15.join(path15.dirname(fileURLToPath(import.meta.url)), "bin", "byok-approval-mcp.js");
1346
+ const distBin = path17.join(path17.dirname(fileURLToPath(import.meta.url)), "bin", "byok-approval-mcp.js");
1235
1347
  return { command: process.execPath, args: [distBin], source: "dist" };
1236
1348
  }
1237
1349
 
@@ -1262,7 +1374,7 @@ function mapPermissionPolicyToClaudeArgs(policy) {
1262
1374
  }
1263
1375
  if (policy.mode === "readonly") {
1264
1376
  const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS2.includes(tool)) : [...READONLY_TOOLS2];
1265
- const effective = subtractDenied2(base, denyTools);
1377
+ const effective = subtractDenied(base, denyTools);
1266
1378
  return { ok: true, args: ["--permission-mode", "default", "--tools", effective.join(",")] };
1267
1379
  }
1268
1380
  if (denyTools.length > 0) {
@@ -1279,7 +1391,7 @@ function mapPermissionPolicyToClaudeArgs(policy) {
1279
1391
  }
1280
1392
  return { ok: true, args };
1281
1393
  }
1282
- function subtractDenied2(tools, denyTools) {
1394
+ function subtractDenied(tools, denyTools) {
1283
1395
  const denied = new Set(denyTools);
1284
1396
  return tools.filter((tool) => !denied.has(tool));
1285
1397
  }
@@ -1311,7 +1423,7 @@ var EXTENSION_CONTENT_TYPES = {
1311
1423
  ".yml": "application/yaml"
1312
1424
  };
1313
1425
  function guessContentType(filePath) {
1314
- const ext = path15.extname(filePath).toLowerCase();
1426
+ const ext = path17.extname(filePath).toLowerCase();
1315
1427
  return EXTENSION_CONTENT_TYPES[ext] ?? "application/octet-stream";
1316
1428
  }
1317
1429
  function mapAssistant(msg, correlation) {
@@ -1383,11 +1495,11 @@ function tryBuildArtifactEvent(msg, workspaceDir) {
1383
1495
  const filePath = toolUseResult && typeof toolUseResult.filePath === "string" ? toolUseResult.filePath : void 0;
1384
1496
  if (!filePath) return void 0;
1385
1497
  const realWorkspaceDir = tryRealpath(workspaceDir) ?? workspaceDir;
1386
- const fileDir = path15.dirname(filePath);
1498
+ const fileDir = path17.dirname(filePath);
1387
1499
  const realFileDir = tryRealpath(fileDir) ?? fileDir;
1388
- const realFilePath = path15.join(realFileDir, path15.basename(filePath));
1389
- const relative = path15.relative(realWorkspaceDir, realFilePath);
1390
- if (relative === "" || relative.startsWith("..") || path15.isAbsolute(relative)) {
1500
+ const realFilePath = path17.join(realFileDir, path17.basename(filePath));
1501
+ const relative = path17.relative(realWorkspaceDir, realFilePath);
1502
+ if (relative === "" || relative.startsWith("..") || path17.isAbsolute(relative)) {
1391
1503
  return void 0;
1392
1504
  }
1393
1505
  return { type: "artifact", name: relative, contentType: guessContentType(filePath) };
@@ -1629,7 +1741,7 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
1629
1741
  var APPROVAL_MCP_SERVER_NAME = "byokapproval";
1630
1742
  var execFileAsync2 = promisify(execFile);
1631
1743
  var DETECT_TIMEOUT_MS2 = 5e3;
1632
- async function cleanupApprovalMcpConfigDir(dir) {
1744
+ async function cleanupMcpConfigDir(dir) {
1633
1745
  if (!dir) return;
1634
1746
  await promises.rm(dir, { recursive: true, force: true }).catch(() => {
1635
1747
  });
@@ -1639,6 +1751,7 @@ var ClaudeAdapter = class {
1639
1751
  this.options = options;
1640
1752
  }
1641
1753
  options;
1754
+ supportsDispatchSelection = true;
1642
1755
  id = "claude";
1643
1756
  async detect() {
1644
1757
  const bin = this.resolveBin();
@@ -1652,7 +1765,13 @@ var ClaudeAdapter = class {
1652
1765
  }
1653
1766
  }
1654
1767
  capabilities() {
1655
- return { steer: false, resume: true, approvalInteractive: true, permissionModes: ["auto", "readonly", "plan", "confirm"] };
1768
+ return {
1769
+ steer: false,
1770
+ resume: true,
1771
+ approvalInteractive: true,
1772
+ mcpToolsets: true,
1773
+ permissionModes: ["auto", "readonly", "plan", "confirm"]
1774
+ };
1656
1775
  }
1657
1776
  /**
1658
1777
  * M5: deliberate product-boundary decision, not an oversight — byok's
@@ -1678,42 +1797,51 @@ var ClaudeAdapter = class {
1678
1797
  if (!mapping.ok) {
1679
1798
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
1680
1799
  }
1681
- let approvalMcpConfigDir;
1800
+ const modelId = subscriptionModel(task, "claude");
1801
+ let mcpConfigDir;
1802
+ const taskMcpServers = ctx.mcpServers ?? {};
1803
+ const needsMcpConfig = mapping.needsApprovalMcp || Object.keys(taskMcpServers).length > 0;
1682
1804
  if (mapping.needsApprovalMcp) {
1683
1805
  if (!ctx.approvalChannel) {
1684
1806
  throw new PolicyUnsupportedError(
1685
1807
  'claude adapter requires policy.mode "confirm" to be started with an approval channel (TaskContext.approvalChannel) \u2014 none was provided'
1686
1808
  );
1687
1809
  }
1688
- const approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
1689
- approvalMcpConfigDir = await promises.mkdtemp(path15.join(os6.tmpdir(), "byok-approval-mcp-"));
1690
- await promises.chmod(approvalMcpConfigDir, 448).catch(() => {
1810
+ if (Object.prototype.hasOwnProperty.call(taskMcpServers, APPROVAL_MCP_SERVER_NAME)) {
1811
+ throw new PolicyUnsupportedError(
1812
+ `MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`
1813
+ );
1814
+ }
1815
+ }
1816
+ if (needsMcpConfig) {
1817
+ mcpConfigDir = await promises.mkdtemp(path17.join(os5.tmpdir(), "byok-mcp-"));
1818
+ await promises.chmod(mcpConfigDir, 448).catch(() => {
1691
1819
  });
1692
- const mcpConfigPath = path15.join(approvalMcpConfigDir, "mcp-config.json");
1693
- const mcpConfig = {
1694
- mcpServers: {
1695
- [APPROVAL_MCP_SERVER_NAME]: {
1696
- command: approvalMcpBin.command,
1697
- args: approvalMcpBin.args,
1698
- env: {
1699
- BYOK_STORE_DIR: ctx.approvalChannel.storeDir,
1700
- BYOK_PRODUCT_ID: ctx.approvalChannel.productId,
1701
- BYOK_TASK_ID: ctx.approvalChannel.taskId,
1702
- BYOK_APPROVAL_TIMEOUT_MS: String(ctx.approvalChannel.timeoutMs)
1703
- }
1820
+ const mcpConfigPath = path17.join(mcpConfigDir, "mcp-config.json");
1821
+ const mcpServers = { ...taskMcpServers };
1822
+ if (mapping.needsApprovalMcp) {
1823
+ const approvalChannel = ctx.approvalChannel;
1824
+ if (!approvalChannel) throw new Error("unreachable: approval channel checked above");
1825
+ const approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
1826
+ mcpServers[APPROVAL_MCP_SERVER_NAME] = {
1827
+ command: approvalMcpBin.command,
1828
+ args: approvalMcpBin.args,
1829
+ env: {
1830
+ BYOK_STORE_DIR: approvalChannel.storeDir,
1831
+ BYOK_PRODUCT_ID: approvalChannel.productId,
1832
+ BYOK_TASK_ID: approvalChannel.taskId,
1833
+ BYOK_APPROVAL_TIMEOUT_MS: String(approvalChannel.timeoutMs)
1704
1834
  }
1705
- }
1706
- };
1707
- await promises.writeFile(mcpConfigPath, JSON.stringify(mcpConfig), { mode: 384 });
1835
+ };
1836
+ }
1837
+ await promises.writeFile(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
1708
1838
  mapping.args = [
1709
1839
  ...mapping.args,
1710
- "--permission-prompt-tool",
1711
- `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`,
1840
+ ...mapping.needsApprovalMcp ? ["--permission-prompt-tool", `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`] : [],
1712
1841
  "--mcp-config",
1713
1842
  mcpConfigPath,
1714
- // Never let this task's confirm-mode run pick up some OTHER MCP
1715
- // server from ambient project/user config the approval channel is
1716
- // the only MCP server this invocation should ever see.
1843
+ // The generated file is the complete task-scoped MCP authority.
1844
+ // Never merge ambient user/project MCP configuration into it.
1717
1845
  "--strict-mcp-config"
1718
1846
  ];
1719
1847
  }
@@ -1730,6 +1858,7 @@ var ClaudeAdapter = class {
1730
1858
  // "Error: When using --print, --output-format=stream-json requires
1731
1859
  // --verbose", before spawning any model call.
1732
1860
  "--verbose",
1861
+ ...modelId ? ["--model", modelId] : [],
1733
1862
  ...resumeSessionId ? ["--resume", resumeSessionId] : [],
1734
1863
  ...mapping.args
1735
1864
  ];
@@ -1737,7 +1866,7 @@ var ClaudeAdapter = class {
1737
1866
  command: bin.command,
1738
1867
  args,
1739
1868
  cwd: ctx.workspaceDir,
1740
- env: ctx.env,
1869
+ env: withoutProviderCredentials(ctx.env),
1741
1870
  spawnFn: this.options.spawnFn
1742
1871
  });
1743
1872
  client.writeUserMessage(task.instruction);
@@ -1746,17 +1875,24 @@ var ClaudeAdapter = class {
1746
1875
  sessionRef = await client.waitForInit();
1747
1876
  } catch (err) {
1748
1877
  client.kill();
1749
- await cleanupApprovalMcpConfigDir(approvalMcpConfigDir);
1878
+ await cleanupMcpConfigDir(mcpConfigDir);
1750
1879
  throw err;
1751
1880
  }
1752
1881
  if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
1753
1882
  client.kill();
1754
- await cleanupApprovalMcpConfigDir(approvalMcpConfigDir);
1883
+ await cleanupMcpConfigDir(mcpConfigDir);
1755
1884
  throw new Error(
1756
1885
  `claude --resume echoed a different session id than requested (requested ${resumeSessionId}, got ${sessionRef}) \u2014 refusing to continue in a possibly-wrong session (fail-closed)`
1757
1886
  );
1758
1887
  }
1759
- return new ClaudeSession(sessionRef, client, ctx.workspaceDir, ctx.approvalChannel, approvalMcpConfigDir);
1888
+ return new ClaudeSession(
1889
+ sessionRef,
1890
+ client,
1891
+ ctx.workspaceDir,
1892
+ ctx.approvalChannel,
1893
+ mcpConfigDir,
1894
+ modelId
1895
+ );
1760
1896
  }
1761
1897
  /**
1762
1898
  * `claude auth status --json` is claude's OWN non-secret login-state
@@ -1790,19 +1926,31 @@ var ClaudeAdapter = class {
1790
1926
  return (this.options.resolveBin ?? resolveClaudeBin)();
1791
1927
  }
1792
1928
  };
1929
+ function subscriptionModel(task, runtimeId) {
1930
+ const selection = task.dispatchSelection;
1931
+ if (selection === void 0) return void 0;
1932
+ if (selection.lane !== "subscription" || selection.runtimeId !== runtimeId) {
1933
+ throw new PolicyUnsupportedError(
1934
+ `claude adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
1935
+ );
1936
+ }
1937
+ return selection.modelId;
1938
+ }
1793
1939
  var ClaudeSession = class {
1794
- constructor(sessionRef, client, workspaceDir, approvalChannel, approvalMcpConfigDir) {
1940
+ constructor(sessionRef, client, workspaceDir, approvalChannel, mcpConfigDir, modelId) {
1795
1941
  this.sessionRef = sessionRef;
1796
1942
  this.client = client;
1797
1943
  this.workspaceDir = workspaceDir;
1798
1944
  this.approvalChannel = approvalChannel;
1799
- this.approvalMcpConfigDir = approvalMcpConfigDir;
1945
+ this.mcpConfigDir = mcpConfigDir;
1946
+ this.modelId = modelId;
1800
1947
  }
1801
1948
  sessionRef;
1802
1949
  client;
1803
1950
  workspaceDir;
1804
1951
  approvalChannel;
1805
- approvalMcpConfigDir;
1952
+ mcpConfigDir;
1953
+ modelId;
1806
1954
  correlation = createToolUseCorrelation();
1807
1955
  get events() {
1808
1956
  const client = this.client;
@@ -1853,6 +2001,12 @@ var ClaudeSession = class {
1853
2001
  if (typeof task.instruction !== "string") {
1854
2002
  throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
1855
2003
  }
2004
+ const requestedModel = subscriptionModel(task, "claude");
2005
+ if (requestedModel !== void 0 && requestedModel !== this.modelId) {
2006
+ throw new PolicyUnsupportedError(
2007
+ `claude persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
2008
+ );
2009
+ }
1856
2010
  this.client.writeUserMessage(task.instruction);
1857
2011
  }
1858
2012
  /**
@@ -1872,7 +2026,7 @@ var ClaudeSession = class {
1872
2026
  }
1873
2027
  async close() {
1874
2028
  this.client.kill();
1875
- await cleanupApprovalMcpConfigDir(this.approvalMcpConfigDir);
2029
+ await cleanupMcpConfigDir(this.mcpConfigDir);
1876
2030
  }
1877
2031
  /**
1878
2032
  * M4 Phase 3: routes into the out-of-band approval channel `start()`
@@ -2036,8 +2190,8 @@ function extractArtifactEvents(changes, workspaceDir) {
2036
2190
  const absolutePath = typeof change.path === "string" ? change.path : void 0;
2037
2191
  const kind = typeof change.kind === "string" ? change.kind : void 0;
2038
2192
  if (!absolutePath || kind === "delete") continue;
2039
- const relative = path15.relative(workspaceDir, absolutePath);
2040
- if (relative.length === 0 || relative.startsWith("..") || path15.isAbsolute(relative)) continue;
2193
+ const relative = path17.relative(workspaceDir, absolutePath);
2194
+ if (relative.length === 0 || relative.startsWith("..") || path17.isAbsolute(relative)) continue;
2041
2195
  events.push({ type: "artifact", name: relative, contentType: guessContentType2(relative) });
2042
2196
  }
2043
2197
  return events;
@@ -2058,7 +2212,7 @@ var CONTENT_TYPE_BY_EXTENSION = {
2058
2212
  ".csv": "text/csv"
2059
2213
  };
2060
2214
  function guessContentType2(relativePath) {
2061
- return CONTENT_TYPE_BY_EXTENSION[path15.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
2215
+ return CONTENT_TYPE_BY_EXTENSION[path17.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
2062
2216
  }
2063
2217
  function extractErrorMessage(rawError) {
2064
2218
  if (typeof rawError === "string") return rawError;
@@ -2196,6 +2350,7 @@ var CodexAdapter = class {
2196
2350
  this.options = options;
2197
2351
  }
2198
2352
  options;
2353
+ supportsDispatchSelection = true;
2199
2354
  id = "codex";
2200
2355
  async detect() {
2201
2356
  const bin = this.resolveBin();
@@ -2219,9 +2374,10 @@ var CodexAdapter = class {
2219
2374
  * Two independently-verified channel gotchas apply here, the "pi lesson"
2220
2375
  * yet again:
2221
2376
  * - `codex login status`'s human-readable "Logged in using ChatGPT"
2222
- * message prints on STDERR, not stdout (the opposite-channel
2223
- * counterpart of pi's own `--version`-goes-to-stderr surprise) both
2224
- * streams are checked here for exactly that reason.
2377
+ * message prints on STDERR, not stdout both streams are checked
2378
+ * here for exactly that reason. pi's `--version` is the same class of
2379
+ * hazard from the other direction: its channel has moved between pi
2380
+ * releases (see ../pi/pi-adapter.ts), so neither stream is assumed.
2225
2381
  * - The NOT-logged-in message/exit-code shape was deliberately never
2226
2382
  * empirically tested: this machine has a real, live ChatGPT login, and
2227
2383
  * running `codex logout` to observe the negative case would have
@@ -2267,17 +2423,20 @@ ${withStreams.stderr ?? ""}`);
2267
2423
  if (!mapping.ok) {
2268
2424
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
2269
2425
  }
2426
+ const modelId = subscriptionModel2(task);
2270
2427
  const bin = this.resolveBin();
2271
2428
  const queue = new AsyncQueue();
2272
2429
  const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
2273
2430
  const workspaceDir = await resolveRealWorkspaceDir(ctx.workspaceDir);
2431
+ const runtimeEnv = withoutProviderCredentials(ctx.env);
2274
2432
  const { sessionRef, runner } = await runCodexTurn({
2275
2433
  command: bin.command,
2276
2434
  resumeRef: task.sessionRef,
2277
2435
  instruction: task.instruction,
2436
+ modelId,
2278
2437
  policyArgs: mapping.args,
2279
2438
  cwd: ctx.workspaceDir,
2280
- env: ctx.env,
2439
+ env: runtimeEnv,
2281
2440
  spawnFn: this.options.spawnFn,
2282
2441
  workspaceDir,
2283
2442
  queue,
@@ -2294,7 +2453,8 @@ ${withStreams.stderr ?? ""}`);
2294
2453
  queue,
2295
2454
  recordUnmapped,
2296
2455
  initialRunner: runner,
2297
- preparedGit: ctx.gitWorkspace !== void 0
2456
+ preparedGit: ctx.gitWorkspace !== void 0,
2457
+ modelId
2298
2458
  });
2299
2459
  }
2300
2460
  resolveBin() {
@@ -2315,12 +2475,25 @@ function makeUnmappedFrameRecorder(counts) {
2315
2475
  }
2316
2476
  };
2317
2477
  }
2318
- function buildArgv(resumeRef, policyArgs, instruction, preparedGit = false) {
2478
+ function buildArgv(resumeRef, policyArgs, instruction, modelId, preparedGit = false) {
2319
2479
  const base = resumeRef !== void 0 ? ["exec", "resume", resumeRef] : ["exec"];
2320
- return [...base, "--json", ...preparedGit ? [] : ["--skip-git-repo-check"], ...policyArgs, instruction];
2480
+ return [
2481
+ ...base,
2482
+ "--json",
2483
+ ...modelId ? ["--model", modelId] : [],
2484
+ ...preparedGit ? [] : ["--skip-git-repo-check"],
2485
+ ...policyArgs,
2486
+ instruction
2487
+ ];
2321
2488
  }
2322
2489
  async function runCodexTurn(params) {
2323
- const argv = buildArgv(params.resumeRef, params.policyArgs, params.instruction, params.preparedGit);
2490
+ const argv = buildArgv(
2491
+ params.resumeRef,
2492
+ params.policyArgs,
2493
+ params.instruction,
2494
+ params.modelId,
2495
+ params.preparedGit
2496
+ );
2324
2497
  let firstLineSettled = false;
2325
2498
  let resolveFirstLine;
2326
2499
  let rejectFirstLine;
@@ -2421,6 +2594,7 @@ var CodexSession = class {
2421
2594
  queue;
2422
2595
  recordUnmapped;
2423
2596
  preparedGit;
2597
+ modelId;
2424
2598
  currentRunner;
2425
2599
  closed = false;
2426
2600
  constructor(options) {
@@ -2432,6 +2606,7 @@ var CodexSession = class {
2432
2606
  this.queue = options.queue;
2433
2607
  this.recordUnmapped = options.recordUnmapped;
2434
2608
  this.preparedGit = options.preparedGit;
2609
+ this.modelId = options.modelId;
2435
2610
  this.currentRunner = options.initialRunner;
2436
2611
  void this.forgetRunnerOnceClosed(options.initialRunner);
2437
2612
  }
@@ -2492,6 +2667,13 @@ var CodexSession = class {
2492
2667
  if (!mapping.ok) {
2493
2668
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
2494
2669
  }
2670
+ const requestedModel = subscriptionModel2(task);
2671
+ if (requestedModel !== void 0 && requestedModel !== this.modelId) {
2672
+ throw new PolicyUnsupportedError(
2673
+ `codex persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
2674
+ );
2675
+ }
2676
+ const modelId = this.modelId;
2495
2677
  const resumeRef = this.sessionRef;
2496
2678
  let sessionRef;
2497
2679
  let runner;
@@ -2500,9 +2682,10 @@ var CodexSession = class {
2500
2682
  command: this.command,
2501
2683
  resumeRef,
2502
2684
  instruction: task.instruction,
2685
+ modelId,
2503
2686
  policyArgs: mapping.args,
2504
2687
  cwd: this.workspaceDir,
2505
- env: this.env,
2688
+ env: withoutProviderCredentials(this.env),
2506
2689
  spawnFn: this.spawnFn,
2507
2690
  workspaceDir: this.workspaceDir,
2508
2691
  queue: this.queue,
@@ -2562,6 +2745,16 @@ var CodexSession = class {
2562
2745
  );
2563
2746
  }
2564
2747
  };
2748
+ function subscriptionModel2(task) {
2749
+ const selection = task.dispatchSelection;
2750
+ if (selection === void 0) return void 0;
2751
+ if (selection.lane !== "subscription" || selection.runtimeId !== "codex") {
2752
+ throw new PolicyUnsupportedError(
2753
+ `codex adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
2754
+ );
2755
+ }
2756
+ return selection.modelId;
2757
+ }
2565
2758
 
2566
2759
  // src/daemon/approvals.ts
2567
2760
  var ApprovalNotFoundError = class extends Error {
@@ -2664,12 +2857,12 @@ var DeviceStore = class _DeviceStore {
2664
2857
  */
2665
2858
  constructor(storeDir, secureDirOptions) {
2666
2859
  this.secureDirOptions = secureDirOptions;
2667
- this.filePath = path15.join(storeDir, "device.json");
2860
+ this.filePath = path17.join(storeDir, "device.json");
2668
2861
  }
2669
2862
  secureDirOptions;
2670
2863
  filePath;
2671
2864
  static defaultDir(productId) {
2672
- return path15.join(os6.homedir(), ".byok", productId);
2865
+ return path17.join(os5.homedir(), ".byok", productId);
2673
2866
  }
2674
2867
  /**
2675
2868
  * Resolve the one store pathname every daemon/CLI component must share.
@@ -2678,7 +2871,7 @@ var DeviceStore = class _DeviceStore {
2678
2871
  * cwd to pin a quarantine directory inode.
2679
2872
  */
2680
2873
  static resolveDir(productId, configured) {
2681
- return path15.resolve(configured ?? _DeviceStore.defaultDir(productId));
2874
+ return path17.resolve(configured ?? _DeviceStore.defaultDir(productId));
2682
2875
  }
2683
2876
  async load() {
2684
2877
  const opened = await this.openBounded();
@@ -2719,7 +2912,7 @@ var DeviceStore = class _DeviceStore {
2719
2912
  }
2720
2913
  }
2721
2914
  async save(record) {
2722
- const storeDir = path15.dirname(this.filePath);
2915
+ const storeDir = path17.dirname(this.filePath);
2723
2916
  await ensureSecureDir(storeDir, this.secureDirOptions);
2724
2917
  await atomicWriteFile(this.filePath, JSON.stringify(record, null, 2), { mode: 384 });
2725
2918
  }
@@ -2786,13 +2979,10 @@ function exportPrivateKeyPem(privateKey) {
2786
2979
  function importPrivateKeyPem(pem) {
2787
2980
  return createPrivateKey(pem);
2788
2981
  }
2789
- var NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
2790
2982
  function signNonce(privateKey, nonce) {
2791
- const signature = sign(null, Buffer.from(NONCE_SIGNING_DOMAIN + nonce, "utf8"), privateKey);
2983
+ const signature = sign(null, nonceSigningBytes(nonce), privateKey);
2792
2984
  return signature.toString("base64url");
2793
2985
  }
2794
-
2795
- // src/daemon/url.ts
2796
2986
  function toHttpBase(serverUrl) {
2797
2987
  const url = new URL(serverUrl);
2798
2988
  if (url.protocol === "ws:") url.protocol = "http:";
@@ -2802,7 +2992,7 @@ function toHttpBase(serverUrl) {
2802
2992
  return url.toString();
2803
2993
  }
2804
2994
  function toWsUrl(serverUrl) {
2805
- const url = new URL("/byok/ws", toHttpBase(serverUrl));
2995
+ const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
2806
2996
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
2807
2997
  return url.toString();
2808
2998
  }
@@ -2890,13 +3080,13 @@ var AuthManager = class {
2890
3080
  return await this.runCredentialMutation(async () => {
2891
3081
  const existing = this.record ?? await this.opts.store.load();
2892
3082
  const keyPair = existing ? { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey } : generateDeviceKeyPair();
2893
- const url = new URL("/byok/pair", toHttpBase(this.opts.serverUrl));
3083
+ const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
2894
3084
  const res = await fetch(url, {
2895
3085
  method: "POST",
2896
3086
  headers: { "content-type": "application/json" },
2897
3087
  body: JSON.stringify({
2898
3088
  pairingCode,
2899
- deviceName: this.opts.deviceName ?? os6.hostname(),
3089
+ deviceName: this.opts.deviceName ?? os5.hostname(),
2900
3090
  devicePublicKey: keyPair.publicKeyBase64Url
2901
3091
  })
2902
3092
  });
@@ -2924,6 +3114,7 @@ var AuthManager = class {
2924
3114
  async getValidAccessToken() {
2925
3115
  if (!this.record) throw new Error("device is not paired yet; call pair(pairingCode) first");
2926
3116
  if (this.revoked) throw new DeviceRevokedError();
3117
+ if (this.renewing) return this.renewing;
2927
3118
  if (msUntilExpiry(this.record.expiresAt) > RENEW_MARGIN_MS) return this.record.accessToken;
2928
3119
  return this.renew();
2929
3120
  }
@@ -2952,7 +3143,7 @@ var AuthManager = class {
2952
3143
  const record = this.record;
2953
3144
  const base = toHttpBase(this.opts.serverUrl);
2954
3145
  const privateKey = importPrivateKeyPem(record.devicePrivateKeyPem);
2955
- const challengeRes = await fetch(new URL("/byok/challenge", base), {
3146
+ const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
2956
3147
  method: "POST",
2957
3148
  headers: { "content-type": "application/json" },
2958
3149
  body: JSON.stringify({ deviceId: record.deviceId })
@@ -2965,7 +3156,7 @@ var AuthManager = class {
2965
3156
  }
2966
3157
  const { nonce } = await challengeRes.json();
2967
3158
  const signature = signNonce(privateKey, nonce);
2968
- const tokenRes = await fetch(new URL("/byok/token", base), {
3159
+ const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
2969
3160
  method: "POST",
2970
3161
  headers: { "content-type": "application/json" },
2971
3162
  body: JSON.stringify({ deviceId: record.deviceId, nonce, signature })
@@ -3063,7 +3254,7 @@ var BlobClient = class {
3063
3254
  async resolveInstruction(blobRef) {
3064
3255
  const base = toHttpBase(this.serverUrl);
3065
3256
  const urlRes = await authedFetch(
3066
- new URL(`/byok/blobs/${encodeURIComponent(blobRef.blobId)}/url`, base),
3257
+ new URL(byokBlobUrlPath(blobRef.blobId), base),
3067
3258
  { method: "GET" },
3068
3259
  this.auth
3069
3260
  );
@@ -3087,18 +3278,18 @@ var BlobClient = class {
3087
3278
  /** `POST /byok/blobs` (declares size/contentType/contentHash) -> PUT the bytes to the presigned upload URL -> a `BlobRef` for `task.artifact.blobRef`. */
3088
3279
  async uploadArtifact(content, contentType) {
3089
3280
  const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
3090
- const contentHash2 = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
3281
+ const contentHash3 = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
3091
3282
  const base = toHttpBase(this.serverUrl);
3092
3283
  const reservationId = `blob_${randomUUID()}`;
3093
3284
  const createRes = await authedFetch(
3094
- new URL("/byok/blobs", base),
3285
+ new URL(BYOK_BLOBS_PATH, base),
3095
3286
  {
3096
3287
  method: "POST",
3097
3288
  headers: {
3098
3289
  "content-type": "application/json",
3099
3290
  "idempotency-key": reservationId
3100
3291
  },
3101
- body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash2 })
3292
+ body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash3 })
3102
3293
  },
3103
3294
  this.auth
3104
3295
  );
@@ -3115,7 +3306,7 @@ var BlobClient = class {
3115
3306
  throw new Error(`failed to upload blob content: HTTP ${putRes.status}`);
3116
3307
  }
3117
3308
  await this.#finalize(base, blobId, reservationId);
3118
- return { blobId, contentHash: contentHash2, size: bytes.length, contentType };
3309
+ return { blobId, contentHash: contentHash3, size: bytes.length, contentType };
3119
3310
  }
3120
3311
  async #finalize(base, blobId, reservationId) {
3121
3312
  let lastFailure;
@@ -3123,7 +3314,7 @@ var BlobClient = class {
3123
3314
  let response;
3124
3315
  try {
3125
3316
  response = await authedFetch(
3126
- new URL(`/byok/blobs/${encodeURIComponent(blobId)}/finalize`, base),
3317
+ new URL(byokBlobFinalizePath(blobId), base),
3127
3318
  {
3128
3319
  method: "POST",
3129
3320
  headers: { "idempotency-key": reservationId }
@@ -3146,26 +3337,187 @@ var BlobClient = class {
3146
3337
  throw lastFailure;
3147
3338
  }
3148
3339
  };
3340
+ var PRESENCE_HINTS_CAPABILITY = "presence.hints";
3341
+ var CapabilityDiscoveryError = class extends Error {
3342
+ constructor(message, options) {
3343
+ super(message, options);
3344
+ this.name = "CapabilityDiscoveryError";
3345
+ }
3346
+ };
3347
+ async function fetchCapabilityDeclaration(serverUrl, options = {}) {
3348
+ const url = new URL(BYOK_CAPABILITIES_PATH, toHttpBase(serverUrl));
3349
+ let response;
3350
+ try {
3351
+ response = await fetch(url, {
3352
+ method: "GET",
3353
+ ...options.signal === void 0 ? {} : { signal: options.signal }
3354
+ });
3355
+ } catch (err) {
3356
+ throw new CapabilityDiscoveryError(
3357
+ `failed to read the capability declaration from ${url.toString()}: ${err instanceof Error ? err.message : String(err)}`,
3358
+ { cause: err }
3359
+ );
3360
+ }
3361
+ if (!response.ok) {
3362
+ throw new CapabilityDiscoveryError(
3363
+ `failed to read the capability declaration from ${url.toString()}: HTTP ${response.status}`
3364
+ );
3365
+ }
3366
+ let body;
3367
+ try {
3368
+ body = await response.json();
3369
+ } catch (err) {
3370
+ throw new CapabilityDiscoveryError(
3371
+ `the capability declaration at ${url.toString()} is not JSON: ${err instanceof Error ? err.message : String(err)}`,
3372
+ { cause: err }
3373
+ );
3374
+ }
3375
+ const parsed = CapabilityDeclarationSchema.safeParse(body);
3376
+ if (!parsed.success) {
3377
+ throw new CapabilityDiscoveryError(
3378
+ `the capability declaration at ${url.toString()} is not a valid ADR-010 declaration: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")}`,
3379
+ { cause: parsed.error }
3380
+ );
3381
+ }
3382
+ return parsed.data;
3383
+ }
3384
+ function declares(declaration, capability) {
3385
+ return hasCapability(declaration, capability);
3386
+ }
3387
+ var DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS = 3e4;
3388
+ var DEFAULT_PRESENCE_TTL_MS = 9e4;
3389
+ var DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS = 5e3;
3390
+ function assertPresenceHeartbeatCadence(cadence) {
3391
+ const { intervalMs, ttlMs, minimumIntervalMs } = cadence;
3392
+ if (!(minimumIntervalMs < intervalMs && intervalMs < ttlMs)) {
3393
+ throw new Error(
3394
+ `presence heartbeat interval must satisfy minimumIntervalMs < intervalMs < ttlMs \u2014 got ${minimumIntervalMs} < ${intervalMs} < ${ttlMs}`
3395
+ );
3396
+ }
3397
+ }
3398
+ var PresencePublisher = class {
3399
+ constructor(opts) {
3400
+ this.opts = opts;
3401
+ const intervalMs = opts.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS;
3402
+ const ttlMs = opts.ttlMs ?? DEFAULT_PRESENCE_TTL_MS;
3403
+ const minimumIntervalMs = opts.minimumIntervalMs ?? DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS;
3404
+ assertPresenceHeartbeatCadence({ intervalMs, ttlMs, minimumIntervalMs });
3405
+ this.intervalMs = intervalMs;
3406
+ this.url = new URL(BYOK_PRESENCE_PATH, toHttpBase(opts.serverUrl));
3407
+ }
3408
+ opts;
3409
+ url;
3410
+ intervalMs;
3411
+ timer;
3412
+ running = false;
3413
+ /** Set once a revoked device is observed. Terminal: `start()` will not restart this instance. */
3414
+ stoppedPermanently = false;
3415
+ /** Publishes immediately, then every `intervalMs`. Idempotent; a no-op after a permanent stop. */
3416
+ start() {
3417
+ if (this.running || this.stoppedPermanently) return;
3418
+ this.running = true;
3419
+ void this.beat();
3420
+ }
3421
+ /** Stops the cadence. Idempotent, and the only "offline" signal this producer emits — the hint's TTL does the rest. */
3422
+ stop() {
3423
+ this.running = false;
3424
+ if (this.timer !== void 0) {
3425
+ clearTimeout(this.timer);
3426
+ this.timer = void 0;
3427
+ }
3428
+ }
3429
+ schedule() {
3430
+ if (!this.running) return;
3431
+ this.timer = setTimeout(() => {
3432
+ this.timer = void 0;
3433
+ void this.beat();
3434
+ }, this.intervalMs);
3435
+ this.timer.unref?.();
3436
+ }
3437
+ async beat() {
3438
+ if (!this.running) return;
3439
+ try {
3440
+ const response = await authedFetch(
3441
+ this.url,
3442
+ {
3443
+ method: "PUT",
3444
+ headers: { "content-type": "application/json" },
3445
+ body: JSON.stringify({ level: "online" })
3446
+ },
3447
+ this.opts.auth
3448
+ );
3449
+ if (!response.ok) {
3450
+ if (response.status === 401) {
3451
+ this.stopPermanently(`presence heartbeat unauthorized after token renewal (HTTP 401)`);
3452
+ return;
3453
+ }
3454
+ this.opts.onDegraded?.(`presence heartbeat failed: HTTP ${response.status}`);
3455
+ }
3456
+ } catch (err) {
3457
+ if (err instanceof DeviceRevokedError) {
3458
+ this.stopPermanently("presence heartbeat stopped: device has been revoked; re-pair required");
3459
+ return;
3460
+ }
3461
+ this.opts.onDegraded?.(`presence heartbeat failed: ${err instanceof Error ? err.message : String(err)}`);
3462
+ }
3463
+ this.schedule();
3464
+ }
3465
+ stopPermanently(reason) {
3466
+ this.stoppedPermanently = true;
3467
+ this.stop();
3468
+ this.opts.onDegraded?.(reason);
3469
+ }
3470
+ };
3471
+ function freshJti() {
3472
+ return randomBytes(16).toString("base64url");
3473
+ }
3474
+ function mintDeviceAssertion(input) {
3475
+ const issuedAtMs = input.now.getTime();
3476
+ const expiresAt = new Date(issuedAtMs + input.ttlMs).toISOString();
3477
+ const claims = DeviceAssertionClaimsSchema.parse({
3478
+ version: 1,
3479
+ issuer: input.issuer,
3480
+ productId: input.productId,
3481
+ deviceId: input.record.deviceId,
3482
+ audience: input.audience,
3483
+ jti: freshJti(),
3484
+ issuedAt: new Date(issuedAtMs).toISOString(),
3485
+ expiresAt
3486
+ });
3487
+ const privateKey = importPrivateKeyPem(input.record.devicePrivateKeyPem);
3488
+ const signature = sign(null, deviceAssertionSigningInput(claims), privateKey).toString("base64url");
3489
+ return {
3490
+ envelope: {
3491
+ schema: DEVICE_ASSERTION_SCHEMA_ID,
3492
+ algorithm: "ed25519",
3493
+ protected: claims,
3494
+ signature
3495
+ },
3496
+ claims,
3497
+ expiresAt
3498
+ };
3499
+ }
3149
3500
  var CONTROL_PROTOCOL_VERSION = 1;
3150
3501
  var HANDSHAKE_TIMEOUT_MS = 3e3;
3151
3502
  var UNIX_SOCKET_PATH_SOFT_LIMIT = 100;
3503
+ var CONTROL_SOCKET_FALLBACK_ROOT = "/tmp";
3152
3504
  function shortHash(input) {
3153
3505
  return createHash("sha256").update(input, "utf8").digest("hex").slice(0, 16);
3154
3506
  }
3155
3507
  function controlSocketPath(storeDir) {
3156
- const candidate = path15.join(storeDir, "control.sock");
3508
+ const candidate = path17.join(storeDir, "control.sock");
3157
3509
  if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT) return candidate;
3158
- return path15.join(os6.tmpdir(), `byok-${shortHash(storeDir)}`, "sock");
3510
+ return path17.join(CONTROL_SOCKET_FALLBACK_ROOT, `byok-${shortHash(storeDir)}`, "sock");
3159
3511
  }
3160
3512
  function controlPipeName(productId, storeDir) {
3161
- const id = shortHash(`${productId}|${path15.resolve(storeDir)}`);
3513
+ const id = shortHash(`${productId}|${path17.resolve(storeDir)}`);
3162
3514
  return `\\\\.\\pipe\\byok-${id}`;
3163
3515
  }
3164
3516
  function controlEndpointPath(productId, storeDir, platform = process.platform) {
3165
3517
  return platform === "win32" ? controlPipeName(productId, storeDir) : controlSocketPath(storeDir);
3166
3518
  }
3167
3519
  function controlTokenPath(storeDir) {
3168
- return path15.join(storeDir, "control.token");
3520
+ return path17.join(storeDir, "control.token");
3169
3521
  }
3170
3522
  var SERVER_PROOF_LABEL = "byok-control-server|";
3171
3523
  var CLIENT_AUTH_LABEL = "byok-control-client|";
@@ -3195,11 +3547,23 @@ function parseClientHello(value) {
3195
3547
  if (value.v !== CONTROL_PROTOCOL_VERSION || value.hello !== "client" || typeof value.nonce !== "string") return void 0;
3196
3548
  return { v: CONTROL_PROTOCOL_VERSION, hello: "client", nonce: value.nonce };
3197
3549
  }
3550
+ function parseServerHello(value) {
3551
+ if (!isRecord2(value)) return void 0;
3552
+ if (value.v !== CONTROL_PROTOCOL_VERSION || value.hello !== "server" || typeof value.proof !== "string" || typeof value.nonce !== "string") {
3553
+ return void 0;
3554
+ }
3555
+ return { v: CONTROL_PROTOCOL_VERSION, hello: "server", proof: value.proof, nonce: value.nonce };
3556
+ }
3198
3557
  function parseClientAuth(value) {
3199
3558
  if (!isRecord2(value)) return void 0;
3200
3559
  if (value.v !== CONTROL_PROTOCOL_VERSION || typeof value.auth !== "string") return void 0;
3201
3560
  return { v: CONTROL_PROTOCOL_VERSION, auth: value.auth };
3202
3561
  }
3562
+ function parseServerReady(value) {
3563
+ if (!isRecord2(value)) return void 0;
3564
+ if (value.v !== CONTROL_PROTOCOL_VERSION || value.ready !== true) return void 0;
3565
+ return { v: CONTROL_PROTOCOL_VERSION, ready: true };
3566
+ }
3203
3567
  function parseRawControlRequest(value) {
3204
3568
  if (!isRecord2(value)) return void 0;
3205
3569
  if (typeof value.id !== "string" || typeof value.method !== "string") return void 0;
@@ -3249,6 +3613,16 @@ function parseApprovalsRequestParams(value) {
3249
3613
  if (typeof value.summary !== "string") return void 0;
3250
3614
  return { taskId: value.taskId, summary: value.summary };
3251
3615
  }
3616
+ var ASSERTION_AUDIENCE_MAX_BYTES = 256;
3617
+ function parseAssertionIssueParams(value) {
3618
+ if (!isRecord2(value)) return void 0;
3619
+ const keys = Object.keys(value);
3620
+ if (keys.length !== 1 || keys[0] !== "audience") return void 0;
3621
+ const { audience } = value;
3622
+ if (typeof audience !== "string" || audience.length === 0) return void 0;
3623
+ if (Buffer.byteLength(audience, "utf8") > ASSERTION_AUDIENCE_MAX_BYTES) return void 0;
3624
+ return { audience };
3625
+ }
3252
3626
  function parseShutdownParams(value) {
3253
3627
  if (!isRecord2(value)) return {};
3254
3628
  return value.reason === "unpair" || value.reason === "operator" ? { reason: value.reason } : {};
@@ -3310,7 +3684,7 @@ async function assertOwnedPrivateDir(dir) {
3310
3684
  }
3311
3685
  async function bindControlEndpoint(server, endpoint) {
3312
3686
  if (process.platform !== "win32") {
3313
- const endpointDir = path15.dirname(endpoint);
3687
+ const endpointDir = path17.dirname(endpoint);
3314
3688
  await promises.mkdir(endpointDir, { recursive: true, mode: 448 });
3315
3689
  await promises.chmod(endpointDir, 448).catch(() => {
3316
3690
  });
@@ -3581,7 +3955,7 @@ var LongPollClient = class {
3581
3955
  try {
3582
3956
  const base = toHttpBase(this.opts.serverUrl);
3583
3957
  const res = await authedFetch(
3584
- new URL("/byok/messages", base),
3958
+ new URL(BYOK_MESSAGES_PATH, base),
3585
3959
  {
3586
3960
  method: "POST",
3587
3961
  headers: { "content-type": "application/json" },
@@ -3605,7 +3979,7 @@ var LongPollClient = class {
3605
3979
  while (this.running) {
3606
3980
  try {
3607
3981
  const base = toHttpBase(this.opts.serverUrl);
3608
- const url = new URL("/byok/events", base);
3982
+ const url = new URL(BYOK_EVENTS_PATH, base);
3609
3983
  const cursor = this.opts.getCursor();
3610
3984
  if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
3611
3985
  const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
@@ -4591,7 +4965,7 @@ function sameFileState2(left, right) {
4591
4965
  return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
4592
4966
  }
4593
4967
  async function openOperationalHealthFile(storeDir) {
4594
- const filePath = path15.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
4968
+ const filePath = path17.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
4595
4969
  let namedBefore;
4596
4970
  try {
4597
4971
  namedBefore = await promises.lstat(filePath, { bigint: true });
@@ -4633,7 +5007,7 @@ var OperationalHealthTracker = class {
4633
5007
  #writeTail = Promise.resolve();
4634
5008
  #started = false;
4635
5009
  constructor(storeDir, options = {}) {
4636
- this.#filePath = path15.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
5010
+ this.#filePath = path17.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
4637
5011
  this.#windowMs = options.windowMs ?? 6e4;
4638
5012
  this.#failureThreshold = options.failureThreshold ?? 3;
4639
5013
  this.#maxFailures = options.maxFailures ?? 128;
@@ -4720,7 +5094,7 @@ var OperationalHealthTracker = class {
4720
5094
  async #load() {
4721
5095
  let opened;
4722
5096
  try {
4723
- opened = await openOperationalHealthFile(path15.dirname(this.#filePath));
5097
+ opened = await openOperationalHealthFile(path17.dirname(this.#filePath));
4724
5098
  } catch (err) {
4725
5099
  throw new Error("operational health state could not be read");
4726
5100
  }
@@ -4756,7 +5130,7 @@ var OperationalHealthTracker = class {
4756
5130
  if (!this.#state) return;
4757
5131
  const body = JSON.stringify(this.#state, null, 2);
4758
5132
  this.#writeTail = this.#writeTail.then(async () => {
4759
- await ensureSecureDir(path15.dirname(this.#filePath));
5133
+ await ensureSecureDir(path17.dirname(this.#filePath));
4760
5134
  await atomicWriteFile(this.#filePath, body, { mode: 384, fsync: true });
4761
5135
  });
4762
5136
  try {
@@ -4831,19 +5205,19 @@ var RECLAIM_FILENAME = `${DAEMON_OWNER_FILENAME}.reclaim`;
4831
5205
  var MAX_OWNER_BYTES = 4096;
4832
5206
  var RECLAIM_MALFORMED_GRACE_MS = 3e4;
4833
5207
  var SELF_PROCESS_STARTED_AT = new Date(Date.now() - process.uptime() * 1e3).toISOString();
4834
- var STORE_MUTEX_PORT_BASE = 1e4;
4835
- var STORE_MUTEX_PORT_COUNT = 2e4;
4836
- var STORE_MUTEX_PORT_CANDIDATES = 32;
4837
5208
  var STORE_MUTEX_ID_PREFIX = "byok-store-mutex-v1:";
4838
5209
  var STORE_MUTEX_PROBE_TIMEOUT_MS = 1e3;
5210
+ var STORE_MUTEX_SOCKET_FILENAME = "mutex.sock";
5211
+ var UNIX_SOCKET_PATH_SOFT_LIMIT2 = 100;
5212
+ var STORE_MUTEX_FALLBACK_ROOT = "/tmp";
4839
5213
  function storeMutexIdentity(canonicalStoreDir) {
4840
5214
  return createHash("sha256").update(canonicalStoreDir).digest("hex");
4841
5215
  }
4842
- function storeMutexPort(identity, attempt) {
4843
- const digest = Buffer.from(identity, "hex");
4844
- const start = digest.readUInt32BE(0) % STORE_MUTEX_PORT_COUNT;
4845
- const step = 1 + digest.readUInt32BE(4) % (STORE_MUTEX_PORT_COUNT - 1);
4846
- return STORE_MUTEX_PORT_BASE + (start + attempt * step) % STORE_MUTEX_PORT_COUNT;
5216
+ function storeMutexEndpoint(canonicalStoreDir, identity, platform = process.platform) {
5217
+ if (platform === "win32") return `\\\\.\\pipe\\byok-store-mutex-${identity.slice(0, 16)}`;
5218
+ const candidate = path17.join(canonicalStoreDir, STORE_MUTEX_SOCKET_FILENAME);
5219
+ if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT2) return candidate;
5220
+ return path17.join(STORE_MUTEX_FALLBACK_ROOT, `byok-store-mutex-${identity.slice(0, 16)}`, "sock");
4847
5221
  }
4848
5222
  var DaemonOwnerActiveError = class extends Error {
4849
5223
  constructor(role) {
@@ -4964,70 +5338,90 @@ async function createLivenessListener() {
4964
5338
  })
4965
5339
  };
4966
5340
  }
4967
- async function probeStoreMutex(port) {
5341
+ async function probeStoreMutex(endpoint, identity) {
4968
5342
  return new Promise((resolve) => {
4969
- const socket = createConnection({ host: "127.0.0.1", port });
5343
+ const socket = createConnection(endpoint);
4970
5344
  let settled = false;
4971
5345
  let raw = "";
4972
5346
  const finish = (result) => {
4973
5347
  if (settled) return;
4974
5348
  settled = true;
5349
+ clearTimeout(timer);
5350
+ socket.removeAllListeners();
4975
5351
  socket.destroy();
4976
5352
  resolve(result);
4977
5353
  };
5354
+ const timer = setTimeout(() => finish({ kind: "occupied" }), STORE_MUTEX_PROBE_TIMEOUT_MS);
4978
5355
  socket.setEncoding("utf8");
4979
- socket.setTimeout(STORE_MUTEX_PROBE_TIMEOUT_MS, () => finish({ kind: "uncertain" }));
4980
5356
  socket.on("data", (chunk) => {
4981
5357
  raw += chunk;
4982
- if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "foreign-or-gone" });
5358
+ if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "occupied" });
4983
5359
  });
4984
- socket.once("end", () => {
4985
- const line = raw.trimEnd();
4986
- const identity = line.startsWith(STORE_MUTEX_ID_PREFIX) ? line.slice(STORE_MUTEX_ID_PREFIX.length) : void 0;
4987
- finish(
4988
- identity && /^[a-f0-9]{64}$/.test(identity) ? { kind: "identity", identity } : { kind: "foreign-or-gone" }
4989
- );
4990
- });
4991
- socket.once("error", () => finish({ kind: "foreign-or-gone" }));
5360
+ socket.once("end", () => finish(raw.trimEnd() === `${STORE_MUTEX_ID_PREFIX}${identity}` ? { kind: "holder" } : { kind: "occupied" }));
5361
+ socket.once(
5362
+ "error",
5363
+ (err) => finish(err.code === "ECONNREFUSED" || err.code === "ENOENT" ? { kind: "unbound" } : { kind: "occupied" })
5364
+ );
4992
5365
  });
4993
5366
  }
5367
+ async function clearStaleStoreMutexSocket(endpoint, identity) {
5368
+ let stat;
5369
+ try {
5370
+ stat = await promises.lstat(endpoint);
5371
+ } catch (err) {
5372
+ if (err.code === "ENOENT") return;
5373
+ throw err;
5374
+ }
5375
+ if (!stat.isSocket()) throw new Error("store mutation lock path exists but is not a socket");
5376
+ if ((await probeStoreMutex(endpoint, identity)).kind !== "unbound") throw new DaemonOwnerActiveError("unknown");
5377
+ await promises.rm(endpoint, { force: true });
5378
+ }
5379
+ async function assertOwnedPrivateDir2(dir) {
5380
+ const uid = process.getuid?.();
5381
+ if (uid === void 0) return;
5382
+ const stat = await promises.lstat(dir);
5383
+ if (stat.isSymbolicLink() || stat.uid !== uid) {
5384
+ throw new Error(`refusing to bind the store mutation lock under "${dir}": not a real directory owned by this process's own uid`);
5385
+ }
5386
+ }
4994
5387
  async function acquireStoreMutex(canonicalStoreDir) {
4995
5388
  const identity = storeMutexIdentity(canonicalStoreDir);
4996
- for (let attempt = 0; attempt < STORE_MUTEX_PORT_CANDIDATES; attempt += 1) {
4997
- const server = createServer((socket) => socket.end(`${STORE_MUTEX_ID_PREFIX}${identity}
5389
+ const endpoint = storeMutexEndpoint(canonicalStoreDir, identity);
5390
+ const isPipe = process.platform === "win32";
5391
+ if (!isPipe) {
5392
+ const endpointDir = path17.dirname(endpoint);
5393
+ if (endpointDir !== canonicalStoreDir) {
5394
+ await ensureSecureDir(endpointDir);
5395
+ await assertOwnedPrivateDir2(endpointDir);
5396
+ }
5397
+ await clearStaleStoreMutexSocket(endpoint, identity);
5398
+ }
5399
+ const server = createServer((socket) => socket.end(`${STORE_MUTEX_ID_PREFIX}${identity}
4998
5400
  `));
4999
- const port = storeMutexPort(identity, attempt);
5000
- try {
5001
- await new Promise((resolve, reject) => {
5002
- server.once("error", reject);
5003
- server.listen({ host: "127.0.0.1", port, exclusive: true }, () => {
5004
- server.removeListener("error", reject);
5005
- resolve();
5006
- });
5401
+ try {
5402
+ await new Promise((resolve, reject) => {
5403
+ server.once("error", reject);
5404
+ server.listen(endpoint, () => {
5405
+ server.removeListener("error", reject);
5406
+ resolve();
5007
5407
  });
5008
- } catch (err) {
5009
- if (err.code !== "EADDRINUSE") throw err;
5010
- const probe = await probeStoreMutex(port);
5011
- if (probe.kind === "uncertain" || probe.kind === "identity" && probe.identity === identity) {
5012
- throw new DaemonOwnerActiveError("unknown");
5013
- }
5014
- continue;
5015
- }
5016
- server.unref();
5017
- let closed = false;
5018
- return {
5019
- port,
5020
- close: () => new Promise((resolve, reject) => {
5021
- if (closed) {
5022
- resolve();
5023
- return;
5024
- }
5025
- closed = true;
5026
- server.close((err) => err ? reject(err) : resolve());
5027
- })
5028
- };
5408
+ });
5409
+ } catch (err) {
5410
+ if (err.code === "EADDRINUSE") throw new DaemonOwnerActiveError("unknown");
5411
+ throw err;
5029
5412
  }
5030
- throw new DaemonOwnerActiveError("unknown");
5413
+ if (!isPipe) await promises.chmod(endpoint, 384).catch(() => void 0);
5414
+ server.unref();
5415
+ let closed = false;
5416
+ return {
5417
+ endpoint,
5418
+ close: async () => {
5419
+ if (closed) return;
5420
+ closed = true;
5421
+ await new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
5422
+ if (!isPipe) await promises.rm(endpoint, { force: true }).catch(() => void 0);
5423
+ }
5424
+ };
5031
5425
  }
5032
5426
  async function reclaimExistsAndIsActive(reclaimPath) {
5033
5427
  let stat;
@@ -5075,8 +5469,8 @@ async function acquireDaemonOwner(storeDir, role, clock = () => /* @__PURE__ */
5075
5469
  await mutex.close().catch(() => void 0);
5076
5470
  throw err;
5077
5471
  }
5078
- const ownerPath = path15.join(storeDir, DAEMON_OWNER_FILENAME);
5079
- const reclaimPath = path15.join(storeDir, RECLAIM_FILENAME);
5472
+ const ownerPath = path17.join(storeDir, DAEMON_OWNER_FILENAME);
5473
+ const reclaimPath = path17.join(storeDir, RECLAIM_FILENAME);
5080
5474
  const record = {
5081
5475
  version: 2,
5082
5476
  pid: process.pid,
@@ -5143,6 +5537,7 @@ function toRuntimeInfoCapabilities(caps) {
5143
5537
  steer: caps.steer,
5144
5538
  resume: caps.resume,
5145
5539
  approvalInteractive: caps.approvalInteractive,
5540
+ ...caps.mcpToolsets === void 0 ? {} : { mcpToolsets: caps.mcpToolsets },
5146
5541
  permissionModes: caps.permissionModes
5147
5542
  };
5148
5543
  }
@@ -5153,7 +5548,7 @@ var CursorStore = class {
5153
5548
  storeDir;
5154
5549
  fileFor(serverUrl, deviceId) {
5155
5550
  const key = createHash("sha256").update(`${serverUrl}::${deviceId}`).digest("hex").slice(0, 32);
5156
- return path15.join(this.storeDir, `cursor-${key}.json`);
5551
+ return path17.join(this.storeDir, `cursor-${key}.json`);
5157
5552
  }
5158
5553
  async load(serverUrl, deviceId) {
5159
5554
  let raw;
@@ -5173,7 +5568,7 @@ var CursorStore = class {
5173
5568
  }
5174
5569
  async save(serverUrl, deviceId, cursor) {
5175
5570
  const file = this.fileFor(serverUrl, deviceId);
5176
- await promises.mkdir(path15.dirname(file), { recursive: true, mode: 448 });
5571
+ await promises.mkdir(path17.dirname(file), { recursive: true, mode: 448 });
5177
5572
  await atomicWriteFile(file, JSON.stringify({ cursor }));
5178
5573
  }
5179
5574
  /** Remove any persisted cursor for (serverUrl, deviceId) — a no-op if none exists. Called from `pair()` (finding F5) so a device that's about to be replaced never leaves a cursor a future, unrelated device could somehow inherit. */
@@ -5216,7 +5611,7 @@ var DaemonObserver = class {
5216
5611
  }
5217
5612
  /**
5218
5613
  * Feed a raw INBOUND (server -> daemon) envelope. Deliberately narrow: only
5219
- * `task.offer` produces a local event here — every other inbound type
5614
+ * either offer variant produces a local event here — every other inbound type
5220
5615
  * (`task.cancel`/`task.steer`/`task.approve`/`task.reject`) is a
5221
5616
  * best-effort notification whose OWN observable effect already surfaces
5222
5617
  * through the daemon's outbound envelopes (`task.cancelled`, `task.progress`
@@ -5224,7 +5619,7 @@ var DaemonObserver = class {
5224
5619
  * where those are actually reported from.
5225
5620
  */
5226
5621
  handleInboundEnvelope(envelope) {
5227
- if (envelope.type !== "task.offer") return;
5622
+ if (envelope.type !== "task.offer" && envelope.type !== "task.offer_with_toolsets") return;
5228
5623
  const taskId = envelope.task_id;
5229
5624
  if (this.taskInfo.has(taskId)) return;
5230
5625
  this.upsertTask(taskId, { state: "Offered", runtime: envelope.payload.runtime });
@@ -5345,6 +5740,37 @@ var DaemonObserver = class {
5345
5740
  noteShutdownComplete(reason, undeliveredOutboxCount) {
5346
5741
  this.emit({ kind: "shutdown-complete", ts: nowIso(), reason, undeliveredOutboxCount });
5347
5742
  }
5743
+ /**
5744
+ * Plan `device-assertion-broker`: see the `device-assertion` `DaemonEvent`
5745
+ * variant's own doc comment. The parameter type is what keeps the signature
5746
+ * out — there is no field to pass one through.
5747
+ *
5748
+ * codex round-2 F4: the DENIED caller can pass its raw `audience` here, but
5749
+ * it is converted to a byte SIZE the instant the event is constructed and the
5750
+ * raw string is dropped — it is never placed on the emitted `DaemonEvent`, so
5751
+ * it cannot reach a subscriber, `format.ts`, stdout, or the audit file. The
5752
+ * ISSUED `audience` came from the allowlist and is kept verbatim.
5753
+ */
5754
+ noteDeviceAssertion(event) {
5755
+ if (event.result === "issued") {
5756
+ this.emit({
5757
+ kind: "device-assertion",
5758
+ ts: nowIso(),
5759
+ result: "issued",
5760
+ audience: event.audience,
5761
+ jti: event.jti,
5762
+ expiresAt: event.expiresAt
5763
+ });
5764
+ return;
5765
+ }
5766
+ this.emit({
5767
+ kind: "device-assertion",
5768
+ ts: nowIso(),
5769
+ result: "denied",
5770
+ reason: event.reason,
5771
+ audienceSize: event.audience === void 0 ? void 0 : Buffer.byteLength(event.audience, "utf8")
5772
+ });
5773
+ }
5348
5774
  /** M4 Phase 3 hardening: see the `stale-approval-decision` `DaemonEvent` variant's own doc comment. */
5349
5775
  noteStaleApprovalDecision(taskId, decision, reason) {
5350
5776
  this.emit({ kind: "stale-approval-decision", ts: nowIso(), taskId, decision, reason });
@@ -5469,7 +5895,7 @@ var SessionWorkspaceStore = class {
5469
5895
  */
5470
5896
  queue = Promise.resolve();
5471
5897
  constructor(storeDir) {
5472
- this.filePath = path15.join(storeDir, "session-workspaces.json");
5898
+ this.filePath = path17.join(storeDir, "session-workspaces.json");
5473
5899
  }
5474
5900
  async get(sessionRef) {
5475
5901
  return this.enqueue(async () => {
@@ -5527,7 +5953,7 @@ var SessionWorkspaceStore = class {
5527
5953
  }
5528
5954
  }
5529
5955
  async save(all) {
5530
- const dir = path15.dirname(this.filePath);
5956
+ const dir = path17.dirname(this.filePath);
5531
5957
  await promises.mkdir(dir, { recursive: true, mode: 448 });
5532
5958
  const tmpPath = `${this.filePath}.${process.pid}-${tmpSeq2++}.tmp`;
5533
5959
  try {
@@ -5632,9 +6058,9 @@ function isSqliteAvailable() {
5632
6058
  }
5633
6059
  }
5634
6060
  var SECURE_FILE_MODE = 384;
5635
- function openJournalDatabase(path19, busyTimeoutMs, faults) {
6061
+ function openJournalDatabase(path21, busyTimeoutMs, faults) {
5636
6062
  const { DatabaseSync } = loadSqliteModule();
5637
- const db = new DatabaseSync(path19, { timeout: busyTimeoutMs });
6063
+ const db = new DatabaseSync(path21, { timeout: busyTimeoutMs });
5638
6064
  try {
5639
6065
  faults?.onStep?.("after-open");
5640
6066
  db.exec("PRAGMA auto_vacuum = INCREMENTAL;");
@@ -5777,9 +6203,9 @@ var RECEIVED_STATE = "received";
5777
6203
  function byteLength(value) {
5778
6204
  return Buffer.byteLength(value, "utf8");
5779
6205
  }
5780
- function fileBytes(path19) {
6206
+ function fileBytes(path21) {
5781
6207
  try {
5782
- return statSync(path19).size;
6208
+ return statSync(path21).size;
5783
6209
  } catch {
5784
6210
  return 0;
5785
6211
  }
@@ -6986,6 +7412,21 @@ var MAX_INLINE_ARTIFACT_BYTES = 64 * 1024;
6986
7412
  var MAX_TRACKED_TASK_IDS = 2e3;
6987
7413
  var MAX_DURATION_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxDurationMs";
6988
7414
  var MAX_OUTPUT_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxTaskOutputBytes";
7415
+ var RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX = "result document undeliverable";
7416
+ function resultDocumentRejectionDetail(check) {
7417
+ switch (check.reason) {
7418
+ case "over-cap":
7419
+ return `${check.bytes} bytes as canonical JSON, over the ${RESULT_DOCUMENT_MAX_BYTES}-byte limit (it is never truncated \u2014 a truncated JSON document is not valid JSON; use artifactRefs for a result this size)`;
7420
+ case "not-serializable":
7421
+ return "not JSON-serializable (JSON.stringify threw, or produced no output at all)";
7422
+ case "not-plain-json":
7423
+ return "not plain JSON data: it does not equal its own JSON round trip, so serializing it would silently change it (an undefined-valued key, NaN, a function or symbol value, a Date, a toJSON that rewrites the value, or a getter that answers differently on a second read)";
7424
+ default: {
7425
+ const exhaustive = check;
7426
+ throw new Error(`unhandled result document rejection: ${JSON.stringify(exhaustive)}`);
7427
+ }
7428
+ }
7429
+ }
6989
7430
  var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
6990
7431
  function isKnownRuntimeId(id) {
6991
7432
  return RuntimeIdSchema.safeParse(id).success;
@@ -6998,6 +7439,14 @@ function orderByPreference(candidates, preference) {
6998
7439
  function adapterSupportsMode(adapter, mode) {
6999
7440
  return adapter.capabilities().permissionModes.includes(mode);
7000
7441
  }
7442
+ function adapterSupportsMcpToolsets(adapter) {
7443
+ return adapter.capabilities().mcpToolsets === true;
7444
+ }
7445
+ function withoutRequiredToolsets(payload) {
7446
+ if (!("requiredToolsets" in payload)) return payload;
7447
+ const { requiredToolsets, ...offer } = payload;
7448
+ return offer;
7449
+ }
7001
7450
  function errorMessage3(err) {
7002
7451
  return err instanceof Error ? err.message : String(err);
7003
7452
  }
@@ -7033,8 +7482,8 @@ function estimateEventBytes(event) {
7033
7482
  }
7034
7483
  async function openArtifact(workspaceDir, name) {
7035
7484
  const realWorkspaceDir = await promises.realpath(workspaceDir).catch(() => workspaceDir);
7036
- const candidate = path15.resolve(realWorkspaceDir, name);
7037
- const prefix = realWorkspaceDir.endsWith(path15.sep) ? realWorkspaceDir : realWorkspaceDir + path15.sep;
7485
+ const candidate = path17.resolve(realWorkspaceDir, name);
7486
+ const prefix = realWorkspaceDir.endsWith(path17.sep) ? realWorkspaceDir : realWorkspaceDir + path17.sep;
7038
7487
  if (candidate !== realWorkspaceDir && !candidate.startsWith(prefix)) {
7039
7488
  return { ok: false, reason: `artifact name "${name}" resolves outside the task workspace \u2014 rejected` };
7040
7489
  }
@@ -7353,6 +7802,9 @@ var TaskRunner = class {
7353
7802
  case "task.offer":
7354
7803
  await this.handleOffer(envelope.task_id, envelope.payload);
7355
7804
  return;
7805
+ case "task.offer_with_toolsets":
7806
+ await this.handleOffer(envelope.task_id, envelope.payload);
7807
+ return;
7356
7808
  case "task.cancel":
7357
7809
  await this.handleCancel(envelope.task_id, envelope.payload.reason);
7358
7810
  return;
@@ -7406,7 +7858,22 @@ var TaskRunner = class {
7406
7858
  );
7407
7859
  return;
7408
7860
  }
7409
- const pick = await this.pickAdapter(payload.runtime, payload.policy.mode);
7861
+ if (payload.dispatchSelection !== void 0 && payload.runtime !== void 0 && payload.runtime !== payload.dispatchSelection.runtimeId) {
7862
+ this.decline(
7863
+ taskId,
7864
+ `offer runtime ${payload.runtime} does not match dispatchSelection.runtimeId ${payload.dispatchSelection.runtimeId}`,
7865
+ false
7866
+ );
7867
+ return;
7868
+ }
7869
+ const requiredToolsets = "requiredToolsets" in payload ? payload.requiredToolsets : void 0;
7870
+ const resolvedMcp = requiredToolsets ? this.resolveMcpServers(requiredToolsets) : void 0;
7871
+ if (resolvedMcp && !resolvedMcp.ok) {
7872
+ this.decline(taskId, resolvedMcp.reason, true);
7873
+ return;
7874
+ }
7875
+ const requestedRuntime = payload.dispatchSelection?.runtimeId ?? payload.runtime;
7876
+ const pick = await this.pickAdapter(requestedRuntime, payload.policy.mode, requiredToolsets !== void 0);
7410
7877
  if (!pick.ok) {
7411
7878
  this.decline(taskId, pick.reason, pick.retryable);
7412
7879
  return;
@@ -7432,7 +7899,7 @@ var TaskRunner = class {
7432
7899
  const sameProtocolTask = ledger?.taskId === taskId;
7433
7900
  const interruptedOldTask = ledger?.phase === "interrupted" && sameProtocolTask;
7434
7901
  const activeDifferentTask = ledger !== void 0 && ledger.taskId !== taskId && (ledger.phase === "preparing" || ledger.phase === "active");
7435
- if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !== payload.sessionRef || path15.resolve(ledger.workspaceDir) !== path15.resolve(known.workspaceDir) || interruptedOldTask || activeDifferentTask) {
7902
+ if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !== payload.sessionRef || path17.resolve(ledger.workspaceDir) !== path17.resolve(known.workspaceDir) || interruptedOldTask || activeDifferentTask) {
7436
7903
  this.decline(taskId, "session is incompatible with Git workspace mode", true);
7437
7904
  return;
7438
7905
  }
@@ -7447,7 +7914,7 @@ var TaskRunner = class {
7447
7914
  return;
7448
7915
  }
7449
7916
  } else {
7450
- workspaceDir = path15.join(this.deps.workspaceRoot, taskId);
7917
+ workspaceDir = path17.join(this.deps.workspaceRoot, taskId);
7451
7918
  }
7452
7919
  try {
7453
7920
  gitLease = await gitManager.acquireLease(workspaceDir, payload.sessionRef);
@@ -7457,7 +7924,7 @@ var TaskRunner = class {
7457
7924
  }
7458
7925
  } else if (!this.deps.gitWorkspaceManager && !this.deps.gitWorkspaceStore) {
7459
7926
  known = payload.sessionRef ? await this.deps.sessionWorkspaces.get(payload.sessionRef) : void 0;
7460
- workspaceDir = known?.workspaceDir ?? path15.join(this.deps.workspaceRoot, taskId);
7927
+ workspaceDir = known?.workspaceDir ?? path17.join(this.deps.workspaceRoot, taskId);
7461
7928
  plainWorkspaceNeedsResolve = true;
7462
7929
  } else {
7463
7930
  this.decline(taskId, "workspace mode is unavailable", true);
@@ -7569,6 +8036,7 @@ var TaskRunner = class {
7569
8036
  const ctx = {
7570
8037
  workspaceDir,
7571
8038
  policy: decision.policy,
8039
+ ...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {},
7572
8040
  ...gitWorkspaceId ? { gitWorkspace: { workspaceId: gitWorkspaceId, baseline: gitBaseline } } : {},
7573
8041
  // M5: no longer `process.env` verbatim (see `environment.ts`'s own
7574
8042
  // module doc comment for the credential-leak gap that closed) —
@@ -7605,7 +8073,7 @@ var TaskRunner = class {
7605
8073
  }
7606
8074
  };
7607
8075
  const effectiveOffer = {
7608
- ...payload,
8076
+ ...withoutRequiredToolsets(payload),
7609
8077
  instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
7610
8078
  // Never forward a sessionRef this device has no recorded workspace
7611
8079
  // for (stale, from another device, or simply made up) — an adapter
@@ -7683,6 +8151,36 @@ var TaskRunner = class {
7683
8151
  if (typeof instruction === "string") return instruction;
7684
8152
  return this.deps.blobClient.resolveInstruction(instruction.blobRef);
7685
8153
  }
8154
+ /** Resolve every requested logical id locally and reject missing/colliding server authority before claim. */
8155
+ resolveMcpServers(requiredToolsets) {
8156
+ const registry = this.deps.mcpToolsets;
8157
+ if (!registry) {
8158
+ return { ok: false, reason: "offer requires MCP toolsets, but this device has no local mcpToolsets registry" };
8159
+ }
8160
+ const servers = /* @__PURE__ */ Object.create(null);
8161
+ for (const toolsetId of requiredToolsets) {
8162
+ const toolset = registry.get(toolsetId);
8163
+ if (!toolset) {
8164
+ return { ok: false, reason: `required MCP toolset "${toolsetId}" is not configured on this device` };
8165
+ }
8166
+ for (const [serverName, server] of Object.entries(toolset.mcpServers)) {
8167
+ if (Object.prototype.hasOwnProperty.call(servers, serverName)) {
8168
+ return {
8169
+ ok: false,
8170
+ reason: `required MCP toolsets collide on server name "${serverName}"; refusing ambiguous projection`
8171
+ };
8172
+ }
8173
+ servers[serverName] = Object.freeze({
8174
+ command: server.command,
8175
+ ...server.args ? { args: Object.freeze([...server.args]) } : {}
8176
+ });
8177
+ }
8178
+ }
8179
+ if (Object.keys(servers).length === 0) {
8180
+ return { ok: false, reason: "required MCP toolsets resolved to no servers; refusing to run without tools" };
8181
+ }
8182
+ return { ok: true, servers: Object.freeze(servers) };
8183
+ }
7686
8184
  async pump(active) {
7687
8185
  try {
7688
8186
  for await (const event of active.session.events) {
@@ -7716,11 +8214,38 @@ var TaskRunner = class {
7716
8214
  if (event.type === "turn_end") {
7717
8215
  active.batcher.push(event);
7718
8216
  active.batcher.flush();
8217
+ const finalOutput = active.summaryParts.join("");
8218
+ const outcome = await this.resolveResultDocument(active, finalOutput);
8219
+ if (!outcome.deliver) return;
7719
8220
  await this.observeGit(active, "completed");
8221
+ if (outcome.document !== void 0 && !this.hasResultDocumentCapability()) {
8222
+ await this.fail(
8223
+ active.taskId,
8224
+ `${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the connected server stopped advertising the result-document capability before this completion could be sent (a reconnect to an older server), so it would silently discard this document`,
8225
+ false
8226
+ );
8227
+ return;
8228
+ }
7720
8229
  this.deps.send(
7721
8230
  createEnvelope(
7722
8231
  "task.complete",
7723
- { summary: active.summaryParts.join(""), sessionRef: active.session.sessionRef },
8232
+ {
8233
+ summary: finalOutput,
8234
+ sessionRef: active.session.sessionRef,
8235
+ // Spread rather than `document: outcome.document`, so a
8236
+ // completion with no document is the exact same payload it
8237
+ // was before this field existed — not one carrying an
8238
+ // explicit `document: undefined` key.
8239
+ //
8240
+ // `outcome.document` is the protocol's CANONICAL SNAPSHOT
8241
+ // (`checkResultDocument`), never the object the extractor
8242
+ // returned: pure data serializes identically at the root
8243
+ // (where it was measured) and nested inside this payload
8244
+ // (where the codec actually serializes it), so a contextual
8245
+ // `toJSON(key)` or an unstable getter cannot make the wire
8246
+ // bytes differ from what the cap gate approved.
8247
+ ...outcome.document !== void 0 ? { document: outcome.document } : {}
8248
+ },
7724
8249
  { taskId: active.taskId, sessionRef: active.session.sessionRef }
7725
8250
  )
7726
8251
  );
@@ -8258,21 +8783,117 @@ var TaskRunner = class {
8258
8783
  this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId }));
8259
8784
  await this.finish(taskId);
8260
8785
  }
8261
- async observeGit(active, phase) {
8262
- if (!active.gitWorkspaceId || !this.deps.gitWorkspaceManager || !this.deps.gitWorkspaceStore) return;
8786
+ /**
8787
+ * additive-minor (`task.complete.document`): the whole daemon-side gate
8788
+ * between a configured {@link ResultDocumentExtractor} and the wire —
8789
+ * called once, from the `turn_end` completion path, immediately before
8790
+ * `task.complete` is built.
8791
+ *
8792
+ * `{deliver: true}` means "go on and send `task.complete`", carrying the
8793
+ * document when there is one. `{deliver: false}` means this method has
8794
+ * ALREADY reported `task.fail` and finished the task; the caller must
8795
+ * return without sending anything further.
8796
+ *
8797
+ * Four fail-closed branches, all `retryable: false` (see
8798
+ * {@link RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX} for why none of them
8799
+ * can succeed on a retry):
8800
+ *
8801
+ * 1. The extractor threw — its error is surfaced, never swallowed.
8802
+ * 2. The extractor returned a thenable, violating the synchronous
8803
+ * contract in the one way that would otherwise ship a wrong answer.
8804
+ * 3. The document is over the cap, not JSON-serializable, or not plain
8805
+ * JSON data, per `checkResultDocument` — the protocol's OWN check,
8806
+ * imported rather than reimplemented, so this gate and the server's
8807
+ * schema validation can never disagree about what is legal.
8808
+ * 4. The connected server never advertised `result-document`. Its
8809
+ * tolerant `z.object()` would silently strip the field on arrival
8810
+ * (`version.ts`'s own flag doc comment), so "send anyway" is not a
8811
+ * degraded-but-working path — it is the task's primary structured
8812
+ * result being deleted in transit with nothing reported anywhere.
8813
+ *
8814
+ * The capability is checked LAST, deliberately: a document that is itself
8815
+ * invalid is the host's own bug and is worth reporting as such even when
8816
+ * the connected server could not have accepted any document at all. It is
8817
+ * then re-checked once more by the caller after its own last await, since
8818
+ * a reconnect can invalidate this answer in between (F3).
8819
+ *
8820
+ * **Residual window (bounded, deliberately not hacked around).** Even the
8821
+ * caller's re-check happens before `ConnectionManager.send` hands the
8822
+ * envelope to a transport, and a queued envelope can outlive the
8823
+ * connection it was queued for: a reconnect between `send()` and the
8824
+ * outbox actually draining could still deliver this `task.complete` to a
8825
+ * rolled-back N-1 server that strips the document. Closing that would
8826
+ * mean teaching the transport outbox to inspect payload semantics and
8827
+ * mint a substitute `task.fail` for a task this runner already finished —
8828
+ * a second authority over terminal outcomes living in the queue, which is
8829
+ * worse than the window it closes. Documented instead, here and in
8830
+ * docs/protocol.md §7.2.
8831
+ */
8832
+ async resolveResultDocument(active, finalOutput) {
8833
+ const extract = this.deps.resultDocument?.extract;
8834
+ if (!extract) return { deliver: true };
8835
+ let document;
8263
8836
  try {
8264
- const observation = await new Promise((resolve, reject) => {
8265
- const timer = setTimeout(() => reject(new Error("Git workspace observation timed out")), GIT_OBSERVATION_TIMEOUT_MS);
8266
- timer.unref?.();
8267
- void this.deps.gitWorkspaceManager.observe(active.workspaceDir, active.gitBaseline).then(
8268
- (value) => {
8269
- clearTimeout(timer);
8270
- resolve(value);
8271
- },
8272
- (error) => {
8273
- clearTimeout(timer);
8274
- reject(error);
8275
- }
8837
+ document = extract(finalOutput, { taskId: active.taskId, sessionRef: active.session.sessionRef });
8838
+ } catch (err) {
8839
+ await this.fail(
8840
+ active.taskId,
8841
+ `${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract threw: ${errorMessage3(err)}`,
8842
+ false
8843
+ );
8844
+ return { deliver: false };
8845
+ }
8846
+ if (typeof document?.then === "function") {
8847
+ await this.fail(
8848
+ active.taskId,
8849
+ `${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract returned a promise; the contract is synchronous (an awaited value is never read, and a promise encodes to an empty document)`,
8850
+ false
8851
+ );
8852
+ return { deliver: false };
8853
+ }
8854
+ if (document === void 0) return { deliver: true };
8855
+ const check = checkResultDocument(document);
8856
+ if (!check.ok) {
8857
+ const detail = resultDocumentRejectionDetail(check);
8858
+ await this.fail(active.taskId, `${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: ${detail}`, false);
8859
+ return { deliver: false };
8860
+ }
8861
+ if (!this.hasResultDocumentCapability()) {
8862
+ await this.fail(
8863
+ active.taskId,
8864
+ `${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the connected server did not advertise the result-document capability, so it would silently discard this ${check.bytes}-byte document`,
8865
+ false
8866
+ );
8867
+ return { deliver: false };
8868
+ }
8869
+ return { deliver: true, document: check.canonical };
8870
+ }
8871
+ /**
8872
+ * Whether the CURRENTLY connected server advertised `result-document` —
8873
+ * read fresh on every call, never captured, because the answer changes
8874
+ * across a reconnect (`ConnectionManager.getServerCapabilities` returns
8875
+ * `[]` from the moment an acked connection closes until a fresh
8876
+ * `conn.ack` repopulates it). An absent `getServerCapabilities` seam is
8877
+ * "no capabilities", the fail-closed reading.
8878
+ */
8879
+ hasResultDocumentCapability() {
8880
+ return (this.deps.getServerCapabilities?.() ?? []).includes("result-document");
8881
+ }
8882
+ async observeGit(active, phase) {
8883
+ if (!active.gitWorkspaceId || !this.deps.gitWorkspaceManager || !this.deps.gitWorkspaceStore) return;
8884
+ try {
8885
+ const observation = await new Promise((resolve, reject) => {
8886
+ const timer = setTimeout(() => reject(new Error("Git workspace observation timed out")), GIT_OBSERVATION_TIMEOUT_MS);
8887
+ timer.unref?.();
8888
+ void this.deps.gitWorkspaceManager.observe(active.workspaceDir, active.gitBaseline).then(
8889
+ (value) => {
8890
+ clearTimeout(timer);
8891
+ resolve(value);
8892
+ },
8893
+ (error) => {
8894
+ clearTimeout(timer);
8895
+ reject(error);
8896
+ }
8276
8897
  );
8277
8898
  });
8278
8899
  await this.deps.gitWorkspaceStore.updateObservation(active.gitWorkspaceId, observation, phase);
@@ -8329,7 +8950,7 @@ var TaskRunner = class {
8329
8950
  }
8330
8951
  /** `reuseDir`, when set (a known sessionRef's recorded workspace), is used verbatim instead of a fresh `workspaceRoot/<taskId>` directory — `mkdir recursive` is idempotent either way, so ensuring-exists is safe to do unconditionally. */
8331
8952
  async resolveWorkspaceDir(taskId, reuseDir) {
8332
- const dir = reuseDir ?? path15.join(this.deps.workspaceRoot, taskId);
8953
+ const dir = reuseDir ?? path17.join(this.deps.workspaceRoot, taskId);
8333
8954
  await promises.mkdir(dir, { recursive: true });
8334
8955
  return dir;
8335
8956
  }
@@ -8360,7 +8981,7 @@ var TaskRunner = class {
8360
8981
  * is device-specific (which runtimes happen to be installed here), so a
8361
8982
  * different device's installed runtime set might satisfy it.
8362
8983
  */
8363
- async pickAdapter(requestedRuntime, policyMode) {
8984
+ async pickAdapter(requestedRuntime, policyMode, requiresMcpToolsets) {
8364
8985
  const allowlist = this.deps.runtimeAllowlist;
8365
8986
  if (requestedRuntime) {
8366
8987
  if (allowlist && !allowlist.includes(requestedRuntime)) {
@@ -8381,6 +9002,13 @@ var TaskRunner = class {
8381
9002
  retryable: false
8382
9003
  };
8383
9004
  }
9005
+ if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) {
9006
+ return {
9007
+ ok: false,
9008
+ reason: `runtime "${requestedRuntime}" cannot project required MCP toolsets`,
9009
+ retryable: false
9010
+ };
9011
+ }
8384
9012
  const detected = await adapter.detect();
8385
9013
  if (!detected.present) {
8386
9014
  return {
@@ -8395,12 +9023,13 @@ var TaskRunner = class {
8395
9023
  const candidates = orderByPreference(eligible, this.deps.runtimePreference ?? DEFAULT_RUNTIME_PREFERENCE);
8396
9024
  for (const adapter of candidates) {
8397
9025
  if (!adapterSupportsMode(adapter, policyMode)) continue;
9026
+ if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) continue;
8398
9027
  const detected = await adapter.detect();
8399
9028
  if (detected.present) return { ok: true, adapter };
8400
9029
  }
8401
9030
  return {
8402
9031
  ok: false,
8403
- reason: `no available runtime on this device can express permission mode "${policyMode}"`,
9032
+ reason: requiresMcpToolsets ? `no available runtime on this device can express permission mode "${policyMode}" with required MCP toolsets` : `no available runtime on this device can express permission mode "${policyMode}"`,
8404
9033
  retryable: true
8405
9034
  };
8406
9035
  }
@@ -8431,7 +9060,7 @@ function toJournalEnvelopeRecord(envelope, identity) {
8431
9060
  bytes,
8432
9061
  bytesHash: journalHash(bytes),
8433
9062
  receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
8434
- opensTask: envelope.type === "task.offer"
9063
+ opensTask: envelope.type === "task.offer" || envelope.type === "task.offer_with_toolsets"
8435
9064
  };
8436
9065
  }
8437
9066
  function isRuntimeId(id) {
@@ -8455,29 +9084,216 @@ function computeCapabilities(adapters) {
8455
9084
  if (adapters.some((adapter) => adapter.capabilities().steer)) flags.push("steer");
8456
9085
  flags.push("blob-upload");
8457
9086
  flags.push("approval-targeting");
9087
+ const selectionAdapters = adapters.filter(
9088
+ (adapter) => ALL_RUNTIME_IDS.includes(adapter.id)
9089
+ );
9090
+ if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.supportsDispatchSelection === true)) {
9091
+ flags.push("dispatch-selection");
9092
+ }
9093
+ if (adapters.some((adapter) => adapter.capabilities().mcpToolsets === true)) {
9094
+ flags.push("toolset-selection");
9095
+ }
8458
9096
  return flags;
8459
9097
  }
8460
9098
  var ALL_RUNTIME_IDS = ["pi", "claude", "codex"];
8461
- function buildAdapter(id) {
9099
+ function buildAdapter(id, config) {
8462
9100
  switch (id) {
8463
9101
  case "pi":
8464
- return new PiAdapter();
9102
+ return new PiAdapter({ byokLauncher: config.piByokLauncher });
8465
9103
  case "claude":
8466
9104
  return new ClaudeAdapter();
8467
9105
  case "codex":
8468
9106
  return new CodexAdapter();
8469
9107
  }
8470
9108
  }
8471
- function buildDefaultAdapters(runtimeAllowlist) {
8472
- const ids = runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => runtimeAllowlist.includes(id)) : ALL_RUNTIME_IDS;
8473
- return ids.map(buildAdapter);
9109
+ function buildDefaultAdapters(config) {
9110
+ const ids = config.runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => config.runtimeAllowlist?.includes(id)) : ALL_RUNTIME_IDS;
9111
+ return ids.map((id) => buildAdapter(id, config));
9112
+ }
9113
+ function validatePiByokLauncherConfig(launcher) {
9114
+ for (const [field, value] of [
9115
+ ["command", launcher.command],
9116
+ ["profileDbPath", launcher.profileDbPath],
9117
+ ["sessionDir", launcher.sessionDir]
9118
+ ]) {
9119
+ if (value.trim().length === 0 || /[\u0000\r\n]/u.test(value)) {
9120
+ throw new Error(`DaemonConfig.piByokLauncher.${field} must be a non-empty single-line string`);
9121
+ }
9122
+ }
9123
+ if (!isAbsolute(launcher.profileDbPath) || !isAbsolute(launcher.sessionDir)) {
9124
+ throw new Error(
9125
+ "DaemonConfig.piByokLauncher profileDbPath and sessionDir must be absolute paths"
9126
+ );
9127
+ }
9128
+ if (launcher.secretServicePrefix !== void 0 && (launcher.secretServicePrefix.trim().length === 0 || /[\u0000\r\n]/u.test(launcher.secretServicePrefix))) {
9129
+ throw new Error(
9130
+ "DaemonConfig.piByokLauncher.secretServicePrefix must be a non-empty single-line string"
9131
+ );
9132
+ }
9133
+ const reserved = /* @__PURE__ */ new Set([
9134
+ "--",
9135
+ "--pi-bin",
9136
+ "--profile-db",
9137
+ "--session-dir",
9138
+ "--secret-service-prefix",
9139
+ "--provider",
9140
+ "--model"
9141
+ ]);
9142
+ const conflicting = launcher.args?.find((arg) => reserved.has(arg));
9143
+ if (conflicting !== void 0) {
9144
+ throw new Error(
9145
+ `DaemonConfig.piByokLauncher.args must not override reserved launcher argument ${conflicting}`
9146
+ );
9147
+ }
9148
+ const invalidArg = launcher.args?.find((arg) => arg.length === 0 || /[\u0000\r\n]/u.test(arg));
9149
+ if (invalidArg !== void 0) {
9150
+ throw new Error("DaemonConfig.piByokLauncher.args must contain only non-empty single-line strings");
9151
+ }
9152
+ }
9153
+ var MAX_LOCAL_MCP_TOOLSETS = 64;
9154
+ var MAX_LOCAL_MCP_SERVERS_PER_TOOLSET = 16;
9155
+ var MAX_LOCAL_MCP_ARGS = 64;
9156
+ var MAX_LOCAL_MCP_TOKEN_CHARS = 4096;
9157
+ function isNonEmptySingleLine(value) {
9158
+ return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_LOCAL_MCP_TOKEN_CHARS && !/[\u0000\r\n]/u.test(value);
9159
+ }
9160
+ function resolveMcpToolsets(configured) {
9161
+ if (configured === void 0) return void 0;
9162
+ if (configured === null || typeof configured !== "object" || Array.isArray(configured)) {
9163
+ throw new Error("DaemonConfig.mcpToolsets must be an object keyed by logical toolset id");
9164
+ }
9165
+ const toolsetEntries = Object.entries(configured);
9166
+ if (toolsetEntries.length > MAX_LOCAL_MCP_TOOLSETS) {
9167
+ throw new Error(`DaemonConfig.mcpToolsets may contain at most ${MAX_LOCAL_MCP_TOOLSETS} toolsets`);
9168
+ }
9169
+ const resolved = /* @__PURE__ */ new Map();
9170
+ for (const [toolsetId, rawToolset] of toolsetEntries) {
9171
+ const parsedId = ToolsetIdSchema.safeParse(toolsetId);
9172
+ if (!parsedId.success) {
9173
+ throw new Error(`DaemonConfig.mcpToolsets contains invalid toolset id ${JSON.stringify(toolsetId)}`);
9174
+ }
9175
+ if (rawToolset === null || typeof rawToolset !== "object" || Array.isArray(rawToolset)) {
9176
+ throw new Error(`DaemonConfig.mcpToolsets.${toolsetId} must be an object`);
9177
+ }
9178
+ const toolsetKeys = Object.keys(rawToolset);
9179
+ if (toolsetKeys.some((key) => key !== "mcpServers")) {
9180
+ throw new Error(`DaemonConfig.mcpToolsets.${toolsetId} accepts only the mcpServers field`);
9181
+ }
9182
+ const rawServers = rawToolset.mcpServers;
9183
+ if (rawServers === null || typeof rawServers !== "object" || Array.isArray(rawServers)) {
9184
+ throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers must be an object`);
9185
+ }
9186
+ const serverEntries = Object.entries(rawServers);
9187
+ if (serverEntries.length === 0 || serverEntries.length > MAX_LOCAL_MCP_SERVERS_PER_TOOLSET) {
9188
+ throw new Error(
9189
+ `DaemonConfig.mcpToolsets.${toolsetId}.mcpServers must contain 1-${MAX_LOCAL_MCP_SERVERS_PER_TOOLSET} servers`
9190
+ );
9191
+ }
9192
+ const servers = {};
9193
+ for (const [serverName, rawServer] of serverEntries) {
9194
+ if (!ToolsetIdSchema.safeParse(serverName).success) {
9195
+ throw new Error(
9196
+ `DaemonConfig.mcpToolsets.${toolsetId}.mcpServers contains invalid server name ${JSON.stringify(serverName)}`
9197
+ );
9198
+ }
9199
+ if (serverName === APPROVAL_MCP_SERVER_NAME) {
9200
+ throw new Error(
9201
+ `DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} uses a server name reserved by the daemon`
9202
+ );
9203
+ }
9204
+ if (rawServer === null || typeof rawServer !== "object" || Array.isArray(rawServer)) {
9205
+ throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} must be an object`);
9206
+ }
9207
+ const serverKeys = Object.keys(rawServer);
9208
+ if (serverKeys.some((key) => key !== "command" && key !== "args")) {
9209
+ throw new Error(
9210
+ `DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} accepts only command and args; env, headers, and remote task data are not supported`
9211
+ );
9212
+ }
9213
+ const server = rawServer;
9214
+ if (!isNonEmptySingleLine(server.command)) {
9215
+ throw new Error(
9216
+ `DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.command must be a non-empty single-line string no longer than ${MAX_LOCAL_MCP_TOKEN_CHARS} characters`
9217
+ );
9218
+ }
9219
+ if (server.args !== void 0 && !Array.isArray(server.args)) {
9220
+ throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.args must be an array`);
9221
+ }
9222
+ const args = server.args ?? [];
9223
+ if (args.length > MAX_LOCAL_MCP_ARGS || args.some((arg) => !isNonEmptySingleLine(arg))) {
9224
+ throw new Error(
9225
+ `DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.args must contain at most ${MAX_LOCAL_MCP_ARGS} non-empty single-line strings`
9226
+ );
9227
+ }
9228
+ servers[serverName] = Object.freeze({
9229
+ command: server.command,
9230
+ ...args.length > 0 ? { args: Object.freeze([...args]) } : {}
9231
+ });
9232
+ }
9233
+ resolved.set(toolsetId, Object.freeze({ mcpServers: Object.freeze(servers) }));
9234
+ }
9235
+ return resolved;
9236
+ }
9237
+ function resolveDeviceAssertionAudiences(config) {
9238
+ if (config === void 0) return void 0;
9239
+ if (!Array.isArray(config.audiences)) {
9240
+ throw new Error(
9241
+ `DaemonConfig.deviceAssertion.audiences must be an array of exact audience strings \u2014 got ${JSON.stringify(config.audiences)}. Omit the deviceAssertion section (or pass an empty array) to leave the assertion broker disabled.`
9242
+ );
9243
+ }
9244
+ if (config.audiences.length === 0) return void 0;
9245
+ const audiences = /* @__PURE__ */ new Set();
9246
+ for (const audience of config.audiences) {
9247
+ if (typeof audience !== "string" || audience.length === 0) {
9248
+ throw new Error(
9249
+ `DaemonConfig.deviceAssertion.audiences entries must be non-empty strings \u2014 got ${JSON.stringify(audience)}`
9250
+ );
9251
+ }
9252
+ if (Buffer.byteLength(audience, "utf8") > DEVICE_ASSERTION_AUDIENCE_MAX_BYTES) {
9253
+ throw new Error(
9254
+ `DaemonConfig.deviceAssertion.audiences entry ${JSON.stringify(audience)} exceeds ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
9255
+ );
9256
+ }
9257
+ if (audiences.has(audience)) {
9258
+ throw new Error(
9259
+ `DaemonConfig.deviceAssertion.audiences contains ${JSON.stringify(audience)} twice \u2014 rejected rather than de-duplicated, because a duplicate is usually a copy-paste that hid a typo in the entry that was meant to be different`
9260
+ );
9261
+ }
9262
+ audiences.add(audience);
9263
+ }
9264
+ return audiences;
9265
+ }
9266
+ function resolveDeviceAssertionTtlMs(config) {
9267
+ const ttlMs = config?.ttlMs ?? DEVICE_ASSERTION_DEFAULT_TTL_MS;
9268
+ if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > DEVICE_ASSERTION_MAX_TTL_MS) {
9269
+ throw new Error(
9270
+ `DaemonConfig.deviceAssertion.ttlMs must be a positive integer no greater than ${DEVICE_ASSERTION_MAX_TTL_MS} ms \u2014 got ${JSON.stringify(config?.ttlMs)}`
9271
+ );
9272
+ }
9273
+ return ttlMs;
8474
9274
  }
8475
9275
  function createDaemonWithAdapters(config, adapters, overrides = {}) {
9276
+ return buildDaemonWithAdapters(config, adapters, overrides);
9277
+ }
9278
+ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProbe) {
9279
+ const mcpToolsets = resolveMcpToolsets(config.mcpToolsets);
9280
+ if (config.piByokLauncher !== void 0) {
9281
+ validatePiByokLauncherConfig(config.piByokLauncher);
9282
+ }
8476
9283
  if (config.maxTaskOutputBytes !== void 0 && !(config.maxTaskOutputBytes > 0)) {
8477
9284
  throw new Error(
8478
9285
  `DaemonConfig.maxTaskOutputBytes must be a positive number (or omitted to use the ${DEFAULT_MAX_TASK_OUTPUT_BYTES}-byte default) \u2014 got ${config.maxTaskOutputBytes}. Pass Number.POSITIVE_INFINITY to explicitly disable the cap; 0 or a negative number is rejected rather than silently treated as "disabled".`
8479
9286
  );
8480
9287
  }
9288
+ const presenceCadence = {
9289
+ intervalMs: config.presence?.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS,
9290
+ ttlMs: config.presence?.ttlMs ?? DEFAULT_PRESENCE_TTL_MS,
9291
+ minimumIntervalMs: config.presence?.minimumIntervalMs ?? DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS
9292
+ };
9293
+ assertPresenceHeartbeatCadence(presenceCadence);
9294
+ const deviceAssertionAudiences = resolveDeviceAssertionAudiences(config.deviceAssertion);
9295
+ const deviceAssertionTtlMs = resolveDeviceAssertionTtlMs(config.deviceAssertion);
9296
+ let shuttingDown = false;
8481
9297
  const storeDir = DeviceStore.resolveDir(config.productId, config.storeDir);
8482
9298
  const store = new DeviceStore(storeDir);
8483
9299
  const operationalHealth = new OperationalHealthTracker(storeDir);
@@ -8568,6 +9384,9 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8568
9384
  let runner;
8569
9385
  let controlServerHandle;
8570
9386
  let daemonOwnerLease;
9387
+ let presencePublisher;
9388
+ let presenceDiscovery;
9389
+ let presenceDiscoveryInFlight = false;
8571
9390
  let shutdownPromise;
8572
9391
  const pendingLateMutationBarriers = /* @__PURE__ */ new Set();
8573
9392
  let startedAt;
@@ -8740,6 +9559,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8740
9559
  deviceId: record.deviceId,
8741
9560
  // M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
8742
9561
  runtimeEnvironment: config.runtimeEnvironment,
9562
+ ...mcpToolsets ? { mcpToolsets } : {},
8743
9563
  // M3-2a: `send` is already this file's OWN closure (not something
8744
9564
  // `TaskRunner` builds) — every `task.claim`/`task.started`/
8745
9565
  // `task.progress`/`task.artifact`/`task.await_approval`/
@@ -8779,6 +9599,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8779
9599
  shutdownInterruptTimeoutMs: overrides.shutdown?.taskInterruptTimeoutMs,
8780
9600
  // M5 batch-3 (workstream 2): see DaemonConfig.maxTaskOutputBytes's own doc comment — already validated above.
8781
9601
  maxTaskOutputBytes: config.maxTaskOutputBytes,
9602
+ // additive-minor (`task.complete.document`): passed through verbatim,
9603
+ // absent when unconfigured — see `DaemonConfig.resultDocument`'s own
9604
+ // doc comment. Spread rather than assigned so an unconfigured daemon
9605
+ // builds the exact `deps` object it did before this seam existed.
9606
+ ...config.resultDocument ? { resultDocument: config.resultDocument } : {},
8782
9607
  // M4 Phase 3 hardening: bridges TaskRunner's stale-approval-race
8783
9608
  // finding out to the SAME local observability seam every other
8784
9609
  // daemon-local event already uses (see observer.ts's own module doc
@@ -8854,8 +9679,10 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8854
9679
  return runner?.handleEnvelope(envelope) ?? Promise.resolve();
8855
9680
  },
8856
9681
  onStateChange: (state) => {
9682
+ const wasSettled = connectionState === "open" || connectionState === "degraded";
8857
9683
  connectionState = state;
8858
9684
  observer.noteConnectionState(state);
9685
+ if (!wasSettled && (state === "open" || state === "degraded")) runPresenceDiscovery();
8859
9686
  },
8860
9687
  backoff: overrides.backoff,
8861
9688
  liveness: overrides.liveness,
@@ -8873,6 +9700,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8873
9700
  });
8874
9701
  await connection.start();
8875
9702
  await connection.waitForAck();
9703
+ startPresenceProducer();
8876
9704
  } catch (err) {
8877
9705
  try {
8878
9706
  await runShutdownSequence("startup failed", { drainTimeoutMs: 0 });
@@ -8882,7 +9710,41 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8882
9710
  throw err;
8883
9711
  }
8884
9712
  }
9713
+ function startPresenceProducer() {
9714
+ presenceDiscovery = new AbortController();
9715
+ runPresenceDiscovery();
9716
+ }
9717
+ function runPresenceDiscovery() {
9718
+ const discovery = presenceDiscovery;
9719
+ if (!discovery || presenceDiscoveryInFlight) return;
9720
+ presenceDiscoveryInFlight = true;
9721
+ void (async () => {
9722
+ try {
9723
+ const declaration = await fetchCapabilityDeclaration(config.serverUrl, { signal: discovery.signal });
9724
+ if (discovery.signal.aborted) return;
9725
+ if (declares(declaration, PRESENCE_HINTS_CAPABILITY)) {
9726
+ presencePublisher ??= new PresencePublisher({
9727
+ serverUrl: config.serverUrl,
9728
+ auth,
9729
+ ...presenceCadence,
9730
+ onDegraded: (reason) => console.warn(`[byok/client] ${reason}`)
9731
+ });
9732
+ presencePublisher.start();
9733
+ } else {
9734
+ presencePublisher?.stop();
9735
+ }
9736
+ } catch (err) {
9737
+ if (discovery.signal.aborted) return;
9738
+ console.warn(
9739
+ `[byok/client] capability discovery failed; presence publishing stays off until the next reconnect: ${err instanceof Error ? err.message : String(err)}`
9740
+ );
9741
+ } finally {
9742
+ presenceDiscoveryInFlight = false;
9743
+ }
9744
+ })();
9745
+ }
8885
9746
  async function runShutdownSequence(reason, opts = {}) {
9747
+ shuttingDown = true;
8886
9748
  const errors = [];
8887
9749
  let mutationBarrierComplete = hostedStorageInitializationBarrierComplete;
8888
9750
  if (!hostedStorageInitializationBarrierComplete) {
@@ -8911,6 +9773,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8911
9773
  errors.push(new Error("a prior active task teardown remains unsettled; ownership lease retained"));
8912
9774
  }
8913
9775
  }
9776
+ presenceDiscovery?.abort();
9777
+ presenceDiscovery = void 0;
9778
+ presenceDiscoveryInFlight = false;
9779
+ presencePublisher?.stop();
9780
+ presencePublisher = void 0;
8914
9781
  const stoppingOwnedPressureEngine = ownedPressureEngine;
8915
9782
  const stoppingOwnedJournal = ownedJournal;
8916
9783
  const maintenanceStopped = stoppingOwnedPressureEngine?.stop() ?? Promise.resolve();
@@ -8971,6 +9838,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8971
9838
  }
8972
9839
  }
8973
9840
  async function stop(opts = {}) {
9841
+ shuttingDown = true;
8974
9842
  await requestShutdown(opts.reason ?? "operator", { drainTimeoutMs: opts.drainTimeoutMs });
8975
9843
  }
8976
9844
  function requestShutdown(reason, opts = {}) {
@@ -8983,6 +9851,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8983
9851
  return current;
8984
9852
  }
8985
9853
  async function unpair() {
9854
+ shuttingDown = true;
8986
9855
  await runLifecycleMutation(unpairUnderLease);
8987
9856
  }
8988
9857
  async function unpairUnderLease() {
@@ -9061,6 +9930,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
9061
9930
  }
9062
9931
  async function performControlShutdown(reason) {
9063
9932
  const effectiveReason = reason ?? "operator";
9933
+ shuttingDown = true;
9064
9934
  observer.noteShutdownRequested(effectiveReason);
9065
9935
  try {
9066
9936
  await requestShutdown(`control socket shutdown (${effectiveReason})`);
@@ -9098,7 +9968,107 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
9098
9968
  if (!runner) throw new ControlError("not_found", "daemon is not started");
9099
9969
  return runner.requestApproval(parsed.taskId, parsed.summary);
9100
9970
  },
9971
+ /**
9972
+ * Plan `device-assertion-broker`: mint one short-lived, audience-scoped
9973
+ * device assertion for a sibling local process.
9974
+ *
9975
+ * SIX fail-closed gates, in this exact order, none of which signs
9976
+ * anything on the way out. The order is part of the contract, not an
9977
+ * implementation detail:
9978
+ *
9979
+ * 1. `assertion_disabled` — before anything else, because a daemon that
9980
+ * was never configured for this must not reveal, by answering
9981
+ * differently for different inputs, that it even validates params.
9982
+ * 2. `bad_request` — shape/length, checked before the allowlist so a
9983
+ * malformed request cannot be used to probe membership.
9984
+ * 3. `audience_denied` — EXACT `Set.has`, never a prefix/suffix rule
9985
+ * (`salesko-api.evil.com` and `salesko-ap` both fail against an entry
9986
+ * of `salesko-api`). The message deliberately does not echo the
9987
+ * allowlist: a refusal must not be an enumeration oracle.
9988
+ * 4. `shutting_down` — see `performControlShutdown`'s own comment for
9989
+ * the minting window this closes.
9990
+ * 5. `revoked` — the server-side revocation this daemon already knows
9991
+ * about.
9992
+ * 6. `not_paired` — the on-disk record, re-read on EVERY call (never
9993
+ * cached), so clearing `device.json` removes local signing authority
9994
+ * immediately.
9995
+ *
9996
+ * Only after all six does the private key get imported, used once, and
9997
+ * dropped (`device-assertion-signer.ts`).
9998
+ *
9999
+ * Honest limit, and it must stay in the docs as well as here: gates 4-6
10000
+ * are only HALF of revocation. They make this daemon stop minting
10001
+ * promptly, but an assertion already in a caller's hands is not recalled
10002
+ * by any of them. The other half is the host's own recheck at exchange
10003
+ * time, which is why core's `verifyDeviceAssertion` makes the device
10004
+ * row's `revoked` state a REQUIRED parameter. Nothing here entitles
10005
+ * anyone to claim this daemon delivers synchronous invalidation on its
10006
+ * own.
10007
+ */
10008
+ "assertion.issue": async (params) => {
10009
+ if (deviceAssertionAudiences === void 0) {
10010
+ observer.noteDeviceAssertion({ result: "denied", reason: "assertion_disabled" });
10011
+ throw new ControlError(
10012
+ "assertion_disabled",
10013
+ "this daemon is not configured to issue device assertions (DaemonConfig.deviceAssertion.audiences is absent or empty)"
10014
+ );
10015
+ }
10016
+ const parsed = parseAssertionIssueParams(params);
10017
+ if (!parsed) {
10018
+ const rawAudience = typeof params === "object" && params !== null && typeof params.audience === "string" ? params.audience : void 0;
10019
+ observer.noteDeviceAssertion({ result: "denied", reason: "bad_request", audience: rawAudience });
10020
+ throw new ControlError(
10021
+ "bad_request",
10022
+ `assertion.issue requires exactly {audience} where audience is a non-empty string of at most ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
10023
+ );
10024
+ }
10025
+ if (!deviceAssertionAudiences.has(parsed.audience)) {
10026
+ observer.noteDeviceAssertion({ result: "denied", reason: "audience_denied", audience: parsed.audience });
10027
+ throw new ControlError("audience_denied", "the requested audience is not allowed by this daemon");
10028
+ }
10029
+ if (shuttingDown) {
10030
+ observer.noteDeviceAssertion({ result: "denied", reason: "shutting_down", audience: parsed.audience });
10031
+ throw new ControlError("shutting_down", "this daemon is shutting down and will not issue new assertions");
10032
+ }
10033
+ if (auth.isRevoked()) {
10034
+ observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
10035
+ throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
10036
+ }
10037
+ const record = await store.load();
10038
+ if (record === void 0) {
10039
+ observer.noteDeviceAssertion({ result: "denied", reason: "not_paired", audience: parsed.audience });
10040
+ throw new ControlError("not_paired", "this device is not paired; nothing can be asserted about it");
10041
+ }
10042
+ if (shuttingDown) {
10043
+ observer.noteDeviceAssertion({ result: "denied", reason: "shutting_down", audience: parsed.audience });
10044
+ throw new ControlError("shutting_down", "this daemon is shutting down and will not issue new assertions");
10045
+ }
10046
+ if (auth.isRevoked()) {
10047
+ observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
10048
+ throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
10049
+ }
10050
+ const minted = mintDeviceAssertion({
10051
+ record,
10052
+ // `toHttpBase` is the one place a configured serverUrl is normalized
10053
+ // (ws:->http:, wss:->https:, path stripped), so an operator who
10054
+ // configured the websocket spelling and one who configured the HTTP
10055
+ // spelling of the same deployment produce the same issuer.
10056
+ issuer: new URL(toHttpBase(config.serverUrl)).origin,
10057
+ productId: config.productId,
10058
+ audience: parsed.audience,
10059
+ ttlMs: deviceAssertionTtlMs,
10060
+ now: /* @__PURE__ */ new Date()
10061
+ });
10062
+ observer.noteDeviceAssertion({
10063
+ result: "issued",
10064
+ audience: minted.claims.audience,
10065
+ jti: minted.claims.jti,
10066
+ expiresAt: minted.expiresAt
10067
+ });
10068
+ return { assertion: minted.envelope, expiresAt: minted.expiresAt };
10069
+ },
9101
10070
  shutdown: (params) => {
10071
+ shuttingDown = true;
9102
10072
  const { reason } = parseShutdownParams(params);
9103
10073
  setImmediate(() => {
9104
10074
  void performControlShutdown(reason).catch((err) => {
@@ -9143,7 +10113,275 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
9143
10113
  return { pair, start, stop, status, subscribe, tasks, unpair, approve, reject };
9144
10114
  }
9145
10115
  function createDaemon(config) {
9146
- return createDaemonWithAdapters(config, buildDefaultAdapters(config.runtimeAllowlist));
10116
+ return createDaemonWithAdapters(config, buildDefaultAdapters(config));
10117
+ }
10118
+ var MAX_CONTROL_TOKEN_BYTES = 256;
10119
+ function errorMessage4(err) {
10120
+ return err instanceof Error ? err.message : String(err);
10121
+ }
10122
+ function sameFileState3(left, right) {
10123
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
10124
+ }
10125
+ async function readControlToken(tokenPath) {
10126
+ let namedBefore;
10127
+ try {
10128
+ namedBefore = await promises.lstat(tokenPath, { bigint: true });
10129
+ } catch (err) {
10130
+ if (err.code === "ENOENT") return void 0;
10131
+ throw err;
10132
+ }
10133
+ if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {
10134
+ throw new Error("control token is not a real regular file");
10135
+ }
10136
+ const handle = await promises.open(
10137
+ tokenPath,
10138
+ constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0)
10139
+ );
10140
+ try {
10141
+ const opened = await handle.stat({ bigint: true });
10142
+ const namedAfterOpen = await promises.lstat(tokenPath, { bigint: true });
10143
+ if (!opened.isFile() || !namedAfterOpen.isFile() || namedAfterOpen.isSymbolicLink() || !sameFileState3(namedBefore, opened) || !sameFileState3(opened, namedAfterOpen)) {
10144
+ throw new Error("control token pathname changed before safe open");
10145
+ }
10146
+ if (opened.size < 0 || opened.size > BigInt(MAX_CONTROL_TOKEN_BYTES)) {
10147
+ throw new Error("control token exceeds the bounded read limit");
10148
+ }
10149
+ const size = Number(opened.size);
10150
+ const bytes = Buffer.alloc(size);
10151
+ const { bytesRead } = await handle.read(bytes, 0, size, 0);
10152
+ const afterRead = await handle.stat({ bigint: true });
10153
+ const namedAfterRead = await promises.lstat(tokenPath, { bigint: true });
10154
+ if (bytesRead !== size || namedAfterRead.isSymbolicLink() || !sameFileState3(opened, afterRead) || !sameFileState3(afterRead, namedAfterRead)) {
10155
+ throw new Error("control token changed during bounded read");
10156
+ }
10157
+ return bytes.toString("utf8").trim();
10158
+ } finally {
10159
+ await handle.close();
10160
+ }
10161
+ }
10162
+ async function connectControlClient(opts) {
10163
+ const tokenPath = controlTokenPath(opts.storeDir);
10164
+ let token;
10165
+ try {
10166
+ const read = await readControlToken(tokenPath);
10167
+ if (read === void 0) {
10168
+ return { ok: false, reason: "daemon is not running (no control.token found)" };
10169
+ }
10170
+ token = read;
10171
+ } catch (err) {
10172
+ return { ok: false, reason: `could not read the control token: ${errorMessage4(err)}` };
10173
+ }
10174
+ if (!token) {
10175
+ return { ok: false, reason: "control token file is empty" };
10176
+ }
10177
+ const endpoint = controlEndpointPath(opts.productId, opts.storeDir);
10178
+ try {
10179
+ const client = await connectAndHandshake(endpoint, token, opts);
10180
+ return { ok: true, client };
10181
+ } catch (err) {
10182
+ return { ok: false, reason: `daemon control socket not reachable: ${errorMessage4(err)}` };
10183
+ }
10184
+ }
10185
+ function connectAndHandshake(endpoint, token, opts) {
10186
+ return new Promise((resolve, reject) => {
10187
+ const socket = net.createConnection(endpoint);
10188
+ const reader = new NdjsonLineReader();
10189
+ let phase = "server-hello";
10190
+ let settled = false;
10191
+ const clientNonce = randomNonceHex();
10192
+ const timer = setTimeout(() => {
10193
+ fail(new Error("handshake timed out"));
10194
+ }, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);
10195
+ timer.unref?.();
10196
+ function fail(err) {
10197
+ if (settled) return;
10198
+ settled = true;
10199
+ clearTimeout(timer);
10200
+ socket.removeAllListeners();
10201
+ socket.destroy();
10202
+ reject(err instanceof Error ? err : new Error(String(err)));
10203
+ }
10204
+ function succeed() {
10205
+ settled = true;
10206
+ clearTimeout(timer);
10207
+ socket.removeListener("error", onError);
10208
+ socket.removeListener("data", onData);
10209
+ resolve(createControlClient(socket, reader, opts));
10210
+ }
10211
+ function onData(chunk) {
10212
+ let lines;
10213
+ try {
10214
+ lines = reader.push(chunk);
10215
+ } catch (err) {
10216
+ fail(err);
10217
+ return;
10218
+ }
10219
+ for (const line of lines) {
10220
+ let parsed;
10221
+ try {
10222
+ parsed = JSON.parse(line);
10223
+ } catch {
10224
+ fail(new Error("malformed handshake frame"));
10225
+ return;
10226
+ }
10227
+ if (phase === "server-hello") {
10228
+ const hello = parseServerHello(parsed);
10229
+ if (!hello) {
10230
+ fail(new Error("malformed or unexpected server hello"));
10231
+ return;
10232
+ }
10233
+ if (!timingSafeEqualHex(hello.proof, computeServerProof(token, clientNonce))) {
10234
+ fail(new Error("server failed to prove it holds the control token"));
10235
+ return;
10236
+ }
10237
+ socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, auth: computeClientAuth(token, hello.nonce) }));
10238
+ phase = "ready";
10239
+ continue;
10240
+ }
10241
+ if (!parseServerReady(parsed)) {
10242
+ fail(new Error("server did not confirm readiness"));
10243
+ return;
10244
+ }
10245
+ succeed();
10246
+ return;
10247
+ }
10248
+ }
10249
+ function onError(err) {
10250
+ fail(err);
10251
+ }
10252
+ socket.once("error", onError);
10253
+ socket.once("connect", () => {
10254
+ socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, hello: "client", nonce: clientNonce }));
10255
+ socket.on("data", onData);
10256
+ });
10257
+ });
10258
+ }
10259
+ function withTimeout(promise, ms, message) {
10260
+ return new Promise((resolve, reject) => {
10261
+ const timer = setTimeout(() => reject(new Error(message)), ms);
10262
+ timer.unref?.();
10263
+ promise.then(
10264
+ (value) => {
10265
+ clearTimeout(timer);
10266
+ resolve(value);
10267
+ },
10268
+ (err) => {
10269
+ clearTimeout(timer);
10270
+ reject(err);
10271
+ }
10272
+ );
10273
+ });
10274
+ }
10275
+ function createControlClient(socket, reader, opts) {
10276
+ const pending = /* @__PURE__ */ new Map();
10277
+ let idSeq = 0;
10278
+ let closed = false;
10279
+ function handleFrame(parsed) {
10280
+ if (!isRecord2(parsed) || typeof parsed.id !== "string") return;
10281
+ const entry = pending.get(parsed.id);
10282
+ if (!entry) return;
10283
+ if ("event" in parsed) {
10284
+ entry.onEvent?.(parsed.event);
10285
+ return;
10286
+ }
10287
+ if (parsed.ok === true) {
10288
+ pending.delete(parsed.id);
10289
+ entry.resolve(parsed.done === true ? void 0 : parsed.result);
10290
+ return;
10291
+ }
10292
+ pending.delete(parsed.id);
10293
+ const shape = parsed.error;
10294
+ entry.reject(
10295
+ new ControlError(
10296
+ typeof shape?.code === "string" ? shape.code : "internal_error",
10297
+ typeof shape?.message === "string" ? shape.message : "unknown control error"
10298
+ )
10299
+ );
10300
+ }
10301
+ socket.on("data", (chunk) => {
10302
+ let lines;
10303
+ try {
10304
+ lines = reader.push(chunk);
10305
+ } catch {
10306
+ socket.destroy();
10307
+ return;
10308
+ }
10309
+ for (const line of lines) {
10310
+ let parsed;
10311
+ try {
10312
+ parsed = JSON.parse(line);
10313
+ } catch {
10314
+ continue;
10315
+ }
10316
+ handleFrame(parsed);
10317
+ }
10318
+ });
10319
+ socket.on("close", () => {
10320
+ closed = true;
10321
+ for (const entry of pending.values()) entry.reject(new Error("control connection closed"));
10322
+ pending.clear();
10323
+ });
10324
+ socket.on("error", () => {
10325
+ });
10326
+ function send(method, params, onEvent) {
10327
+ const id = `c${++idSeq}`;
10328
+ const promise = new Promise((resolve, reject) => {
10329
+ pending.set(id, { resolve, reject, onEvent });
10330
+ });
10331
+ socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, id, method, params }));
10332
+ return { id, promise };
10333
+ }
10334
+ return {
10335
+ async request(method, params) {
10336
+ if (closed) throw new Error("control connection is closed");
10337
+ const { promise } = send(method, params);
10338
+ const result = await withTimeout(promise, opts.requestTimeoutMs ?? 1e4, `control request "${method}" timed out`);
10339
+ return result;
10340
+ },
10341
+ subscribe(method, params, onEvent) {
10342
+ const { id, promise } = send(method, params, onEvent);
10343
+ promise.catch(() => {
10344
+ });
10345
+ return {
10346
+ close: () => {
10347
+ pending.delete(id);
10348
+ socket.destroy();
10349
+ }
10350
+ };
10351
+ },
10352
+ close() {
10353
+ socket.destroy();
10354
+ }
10355
+ };
10356
+ }
10357
+
10358
+ // src/daemon/assertion-client.ts
10359
+ function errorMessage5(err) {
10360
+ return err instanceof Error ? err.message : String(err);
10361
+ }
10362
+ async function requestDeviceAssertion(options) {
10363
+ const storeDir = DeviceStore.resolveDir(options.productId, options.storeDir);
10364
+ const connected = await connectControlClient({
10365
+ storeDir,
10366
+ productId: options.productId,
10367
+ ...options.timeoutMs === void 0 ? {} : { requestTimeoutMs: options.timeoutMs }
10368
+ });
10369
+ if (!connected.ok) return { ok: false, code: "unavailable", reason: connected.reason };
10370
+ try {
10371
+ const result = await connected.client.request("assertion.issue", {
10372
+ audience: options.audience
10373
+ });
10374
+ if (result === null || typeof result !== "object" || typeof result.expiresAt !== "string") {
10375
+ return { ok: false, code: "bad_response", reason: "daemon returned a malformed assertion.issue result" };
10376
+ }
10377
+ const assertion = parseDeviceAssertionEnvelope(result.assertion);
10378
+ return { ok: true, assertion, expiresAt: result.expiresAt };
10379
+ } catch (err) {
10380
+ if (err instanceof ControlError) return { ok: false, code: err.code, reason: err.message };
10381
+ return { ok: false, code: "bad_response", reason: errorMessage5(err) };
10382
+ } finally {
10383
+ connected.client.close();
10384
+ }
9147
10385
  }
9148
10386
  var StoredDeviceProofSigner = class {
9149
10387
  constructor(options) {
@@ -9202,9 +10440,359 @@ function requireNonEmpty(name, value) {
9202
10440
  function sha256(bytes) {
9203
10441
  return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
9204
10442
  }
10443
+ var SKILL_PACKS_CAPABILITY = "skills.pack";
10444
+ var SKILL_PACKS_DIRNAME = "skill-packs";
10445
+ var SKILL_PACK_LOCK_FILENAME = "lock.json";
10446
+ var SKILL_PACK_AUDIT_FILENAME = "audit.jsonl";
10447
+ var SKILL_PACK_LOCK_SCHEMA = "byok-skill-pack-lock-v1";
10448
+ var DIR_MODE = 448;
10449
+ var FILE_MODE = 384;
10450
+ var SKILL_PACK_RESPONSE_MAX_BYTES = SKILL_PACK_MAX_BYTES * 2;
10451
+ var SKILL_PACK_INSTALL_ERROR_CODES = [
10452
+ "capability_unavailable",
10453
+ "transport_failed",
10454
+ "response_invalid",
10455
+ "response_too_large",
10456
+ "manifest_invalid",
10457
+ "content_rejected",
10458
+ "store_unsafe"
10459
+ ];
10460
+ var SkillPackInstallError = class extends Error {
10461
+ code;
10462
+ packName;
10463
+ constructor(code, message, options = {}) {
10464
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
10465
+ this.name = "SkillPackInstallError";
10466
+ this.code = code;
10467
+ this.packName = options.packName;
10468
+ }
10469
+ };
10470
+ function hashBytes(bytes) {
10471
+ return contentHash(`sha256:${createHash("sha256").update(bytes).digest("hex")}`);
10472
+ }
10473
+ function originOf(serverUrl) {
10474
+ return new URL(toHttpBase(serverUrl)).origin;
10475
+ }
10476
+ async function readBoundedJson(response, what) {
10477
+ const declaredLength = response.headers.get("content-length");
10478
+ if (declaredLength !== null) {
10479
+ const parsed = Number(declaredLength);
10480
+ if (Number.isSafeInteger(parsed) && parsed > SKILL_PACK_RESPONSE_MAX_BYTES) {
10481
+ throw new SkillPackInstallError(
10482
+ "response_too_large",
10483
+ `${what} declared ${parsed} bytes, over the ${SKILL_PACK_RESPONSE_MAX_BYTES} byte response limit.`
10484
+ );
10485
+ }
10486
+ }
10487
+ const bytes = new Uint8Array(await response.arrayBuffer());
10488
+ if (bytes.byteLength > SKILL_PACK_RESPONSE_MAX_BYTES) {
10489
+ throw new SkillPackInstallError(
10490
+ "response_too_large",
10491
+ `${what} delivered ${bytes.byteLength} bytes, over the ${SKILL_PACK_RESPONSE_MAX_BYTES} byte response limit.`
10492
+ );
10493
+ }
10494
+ try {
10495
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
10496
+ } catch (cause) {
10497
+ throw new SkillPackInstallError("response_invalid", `${what} is not valid JSON.`, { cause });
10498
+ }
10499
+ }
10500
+ async function getJson(url, auth, signal, what) {
10501
+ let response;
10502
+ try {
10503
+ response = await authedFetch(url, { method: "GET", ...signal === void 0 ? {} : { signal } }, auth);
10504
+ } catch (cause) {
10505
+ throw new SkillPackInstallError("transport_failed", `${what} could not be fetched from ${url.toString()}.`, {
10506
+ cause
10507
+ });
10508
+ }
10509
+ if (!response.ok) {
10510
+ throw new SkillPackInstallError("transport_failed", `${what} answered HTTP ${response.status}.`);
10511
+ }
10512
+ return readBoundedJson(response, what);
10513
+ }
10514
+ function skillPacksRoot(dataDir) {
10515
+ return path17.join(dataDir, SKILL_PACKS_DIRNAME);
10516
+ }
10517
+ function resolveInside(baseDir, relative) {
10518
+ if (!isSkillPackPathSafe(relative)) {
10519
+ throw new SkillPackInstallError("store_unsafe", `${JSON.stringify(relative)} is not a safe pack-relative path.`);
10520
+ }
10521
+ const resolved = path17.resolve(baseDir, relative);
10522
+ const prefix = path17.resolve(baseDir) + path17.sep;
10523
+ if (!resolved.startsWith(prefix)) {
10524
+ throw new SkillPackInstallError(
10525
+ "store_unsafe",
10526
+ `${JSON.stringify(relative)} resolves outside ${baseDir}.`
10527
+ );
10528
+ }
10529
+ return resolved;
10530
+ }
10531
+ async function assertNotSymlink(target) {
10532
+ let stats;
10533
+ try {
10534
+ stats = await promises.lstat(target);
10535
+ } catch (err) {
10536
+ if (err.code === "ENOENT") return;
10537
+ throw err;
10538
+ }
10539
+ if (stats.isSymbolicLink()) {
10540
+ throw new SkillPackInstallError("store_unsafe", `${target} is a symbolic link; refusing to follow it.`);
10541
+ }
10542
+ }
10543
+ async function appendAuditLine(dataDir, record) {
10544
+ const root = skillPacksRoot(dataDir);
10545
+ await promises.mkdir(root, { recursive: true, mode: DIR_MODE });
10546
+ await promises.chmod(root, DIR_MODE).catch(() => {
10547
+ });
10548
+ const filePath = path17.join(root, SKILL_PACK_AUDIT_FILENAME);
10549
+ const handle = await promises.open(filePath, "a", FILE_MODE);
10550
+ try {
10551
+ await handle.chmod(FILE_MODE);
10552
+ await handle.appendFile(`${JSON.stringify(record)}
10553
+ `, "utf8");
10554
+ } finally {
10555
+ await handle.close();
10556
+ }
10557
+ }
10558
+ async function installSkillPacks(options) {
10559
+ if (!hasCapability(options.declaration, SKILL_PACKS_CAPABILITY)) {
10560
+ throw new SkillPackInstallError(
10561
+ "capability_unavailable",
10562
+ `This deployment does not declare ${SKILL_PACKS_CAPABILITY}; refusing to fetch skill packs.`
10563
+ );
10564
+ }
10565
+ const base = toHttpBase(options.serverUrl);
10566
+ const source = originOf(options.serverUrl);
10567
+ const body = await getJson(
10568
+ new URL(BYOK_SKILL_PACKS_PATH, base),
10569
+ options.auth,
10570
+ options.signal,
10571
+ "the skill pack manifest list"
10572
+ );
10573
+ const rawPacks = body?.packs;
10574
+ if (!Array.isArray(rawPacks)) {
10575
+ throw new SkillPackInstallError("response_invalid", "the skill pack manifest list has no `packs` array.");
10576
+ }
10577
+ const installed = [];
10578
+ const unchanged = [];
10579
+ for (const raw of rawPacks) {
10580
+ const manifest = parseManifest(raw);
10581
+ const existing = await readLock(options.dataDir, manifest.name);
10582
+ if (existing?.lock.content_hash === manifest.contentHash) {
10583
+ unchanged.push(manifest.name);
10584
+ continue;
10585
+ }
10586
+ installed.push(await installOne(options, manifest, source, base));
10587
+ }
10588
+ return { installed, unchanged };
10589
+ }
10590
+ function parseManifest(raw) {
10591
+ let manifest;
10592
+ try {
10593
+ manifest = parseSkillPackManifest(raw);
10594
+ } catch (cause) {
10595
+ throw new SkillPackInstallError("manifest_invalid", "a published skill pack manifest is not valid.", { cause });
10596
+ }
10597
+ const structural = checkSkillPackManifest(manifest);
10598
+ if (!structural.ok) {
10599
+ throw new SkillPackInstallError(
10600
+ "manifest_invalid",
10601
+ `skill pack ${JSON.stringify(manifest.name)} was refused: ${structural.reason} \u2014 ${structural.detail}`,
10602
+ { packName: manifest.name }
10603
+ );
10604
+ }
10605
+ return manifest;
10606
+ }
10607
+ async function installOne(options, manifest, source, base) {
10608
+ const refuse = async (code, message) => {
10609
+ await appendAuditLine(options.dataDir, {
10610
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10611
+ event: "skill-pack-rejected",
10612
+ name: manifest.name,
10613
+ contentHash: manifest.contentHash,
10614
+ source,
10615
+ code,
10616
+ reason: message
10617
+ });
10618
+ throw new SkillPackInstallError(code, message, { packName: manifest.name });
10619
+ };
10620
+ const expectedPackHash = hashBytes(new TextEncoder().encode(skillPackContentHashInput(manifest)));
10621
+ if (expectedPackHash !== manifest.contentHash) {
10622
+ await refuse(
10623
+ "manifest_invalid",
10624
+ `skill pack ${JSON.stringify(manifest.name)} declares ${manifest.contentHash} but its file rows address ${expectedPackHash}.`
10625
+ );
10626
+ }
10627
+ const bodies = /* @__PURE__ */ new Map();
10628
+ let totalBytes = 0;
10629
+ for (const declared of manifest.files) {
10630
+ const url = new URL(byokSkillPackFilePath(manifest.name, declared.path), base);
10631
+ const raw = await getJson(url, options.auth, options.signal, `skill pack file ${JSON.stringify(declared.path)}`);
10632
+ const content = raw?.content;
10633
+ if (typeof content !== "string") {
10634
+ await refuse("response_invalid", `skill pack file ${JSON.stringify(declared.path)} carried no string content.`);
10635
+ }
10636
+ const bytes = new TextEncoder().encode(content);
10637
+ const check = checkSkillPackFileContent(declared, {
10638
+ byteSize: bytes.byteLength,
10639
+ contentHash: hashBytes(bytes)
10640
+ });
10641
+ if (!check.ok) {
10642
+ await refuse("content_rejected", `skill pack ${JSON.stringify(manifest.name)}: ${check.reason} \u2014 ${check.detail}`);
10643
+ }
10644
+ totalBytes += bytes.byteLength;
10645
+ bodies.set(declared.path, content);
10646
+ }
10647
+ const entryText = bodies.get(SKILL_PACK_ENTRY_PATH);
10648
+ if (entryText === void 0) {
10649
+ await refuse("content_rejected", `skill pack ${JSON.stringify(manifest.name)}: entry-missing \u2014 ${SKILL_PACK_ENTRY_PATH} was not delivered.`);
10650
+ }
10651
+ let entryCheck;
10652
+ try {
10653
+ entryCheck = checkSkillPackEntry(manifest, entryText);
10654
+ } catch (cause) {
10655
+ await refuse(
10656
+ "content_rejected",
10657
+ `skill pack ${JSON.stringify(manifest.name)}: ${SKILL_PACK_ENTRY_PATH} frontmatter was refused (${cause instanceof Error ? cause.message : String(cause)}).`
10658
+ );
10659
+ }
10660
+ if (!entryCheck.ok) {
10661
+ await refuse("content_rejected", `skill pack ${JSON.stringify(manifest.name)}: ${entryCheck.reason} \u2014 ${entryCheck.detail}`);
10662
+ }
10663
+ const packRoot = path17.join(skillPacksRoot(options.dataDir), manifest.name);
10664
+ const revisionDir = path17.join(packRoot, manifest.contentHash.slice("sha256:".length));
10665
+ await promises.mkdir(revisionDir, { recursive: true, mode: DIR_MODE });
10666
+ for (const [relative, content] of bodies) {
10667
+ const target = resolveInside(revisionDir, relative);
10668
+ await promises.mkdir(path17.dirname(target), { recursive: true, mode: DIR_MODE });
10669
+ await assertNotSymlink(target);
10670
+ await atomicWriteFile(target, content, { mode: FILE_MODE });
10671
+ }
10672
+ const lock = {
10673
+ schema: SKILL_PACK_LOCK_SCHEMA,
10674
+ name: manifest.name,
10675
+ version: manifest.version,
10676
+ description: manifest.description,
10677
+ content_hash: manifest.contentHash,
10678
+ source,
10679
+ installed_at: (/* @__PURE__ */ new Date()).toISOString(),
10680
+ files: manifest.files.map((file) => ({
10681
+ path: file.path,
10682
+ sha256: file.contentHash,
10683
+ bytes: file.byteSize
10684
+ }))
10685
+ };
10686
+ await atomicWriteFile(path17.join(packRoot, SKILL_PACK_LOCK_FILENAME), `${JSON.stringify(lock, null, 2)}
10687
+ `, {
10688
+ mode: FILE_MODE
10689
+ });
10690
+ await appendAuditLine(options.dataDir, {
10691
+ ts: lock.installed_at,
10692
+ event: "skill-pack-installed",
10693
+ name: lock.name,
10694
+ version: lock.version,
10695
+ contentHash: lock.content_hash,
10696
+ source: lock.source,
10697
+ files: lock.files.length,
10698
+ bytes: totalBytes
10699
+ });
10700
+ return { name: manifest.name, lock, directory: revisionDir };
10701
+ }
10702
+ function isLockShaped(value) {
10703
+ const lock = value;
10704
+ return lock !== null && typeof lock === "object" && lock.schema === SKILL_PACK_LOCK_SCHEMA && typeof lock.name === "string" && typeof lock.content_hash === "string" && CONTENT_HASH_PATTERN.test(lock.content_hash) && Array.isArray(lock.files);
10705
+ }
10706
+ async function readLock(dataDir, name) {
10707
+ if (!isSkillPackPathSafe(name)) return void 0;
10708
+ const packRoot = path17.join(skillPacksRoot(dataDir), name);
10709
+ let raw;
10710
+ try {
10711
+ raw = await promises.readFile(path17.join(packRoot, SKILL_PACK_LOCK_FILENAME), "utf8");
10712
+ } catch (err) {
10713
+ if (err.code === "ENOENT") return void 0;
10714
+ throw err;
10715
+ }
10716
+ let parsed;
10717
+ try {
10718
+ parsed = JSON.parse(raw);
10719
+ } catch {
10720
+ return void 0;
10721
+ }
10722
+ if (!isLockShaped(parsed) || parsed.name !== name) return void 0;
10723
+ return {
10724
+ name,
10725
+ lock: parsed,
10726
+ directory: path17.join(packRoot, parsed.content_hash.slice("sha256:".length))
10727
+ };
10728
+ }
10729
+ async function listInstalledSkillPacks(dataDir) {
10730
+ let entries;
10731
+ try {
10732
+ entries = await promises.readdir(skillPacksRoot(dataDir), { withFileTypes: true });
10733
+ } catch (err) {
10734
+ if (err.code === "ENOENT") return [];
10735
+ throw err;
10736
+ }
10737
+ const packs = [];
10738
+ for (const entry of entries) {
10739
+ if (!entry.isDirectory()) continue;
10740
+ const installed = await readLock(dataDir, entry.name);
10741
+ if (installed !== void 0) packs.push(installed);
10742
+ }
10743
+ packs.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
10744
+ return packs;
10745
+ }
10746
+ async function projectSkillPack(dataDir, name, targetDir) {
10747
+ const installed = await readLock(dataDir, name);
10748
+ if (installed === void 0) {
10749
+ throw new SkillPackInstallError("store_unsafe", `no installed skill pack named ${JSON.stringify(name)}.`, {
10750
+ packName: name
10751
+ });
10752
+ }
10753
+ const copied = [];
10754
+ for (const file of installed.lock.files) {
10755
+ const sourcePath = resolveInside(installed.directory, file.path);
10756
+ await assertNotSymlink(sourcePath);
10757
+ let bytes;
10758
+ try {
10759
+ bytes = await promises.readFile(sourcePath);
10760
+ } catch (cause) {
10761
+ throw new SkillPackInstallError(
10762
+ "store_unsafe",
10763
+ `installed skill pack ${JSON.stringify(name)} is missing ${JSON.stringify(file.path)}.`,
10764
+ { cause, packName: name }
10765
+ );
10766
+ }
10767
+ if (hashBytes(bytes) !== file.sha256 || bytes.byteLength !== file.bytes) {
10768
+ throw new SkillPackInstallError(
10769
+ "store_unsafe",
10770
+ `installed skill pack ${JSON.stringify(name)} no longer matches its lock at ${JSON.stringify(file.path)}.`,
10771
+ { packName: name }
10772
+ );
10773
+ }
10774
+ const destination = resolveInside(targetDir, file.path);
10775
+ await promises.mkdir(path17.dirname(destination), { recursive: true, mode: DIR_MODE });
10776
+ await assertNotSymlink(destination);
10777
+ await atomicWriteFile(destination, bytes, { mode: FILE_MODE });
10778
+ copied.push(file.path);
10779
+ }
10780
+ await appendAuditLine(dataDir, {
10781
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10782
+ event: "skill-pack-projected",
10783
+ name: installed.name,
10784
+ contentHash: installed.lock.content_hash,
10785
+ files: copied.length
10786
+ });
10787
+ return {
10788
+ name: installed.name,
10789
+ contentHash: installed.lock.content_hash,
10790
+ targetDir: path17.resolve(targetDir),
10791
+ files: copied
10792
+ };
10793
+ }
9205
10794
  var EMPTY_BODY = new Uint8Array();
9206
10795
  var LARGE_SNAPSHOT_THRESHOLD_BYTES = 1024 * 1024;
9207
- var DEVICE_PROOF_HEADER = "x-byok-device-proof";
9208
10796
  var TruthMemoryClientError = class extends Error {
9209
10797
  constructor(code, message, status) {
9210
10798
  super(message);
@@ -9231,8 +10819,8 @@ var TruthMemoryClient = class {
9231
10819
  #requestId;
9232
10820
  #allowedObjectDownloadOrigins;
9233
10821
  async listManifest(query = {}) {
9234
- const path19 = manifestPath(query);
9235
- const response = await this.#proofFetch(path19, {
10822
+ const path21 = manifestPath(query);
10823
+ const response = await this.#proofFetch(path21, {
9236
10824
  method: "GET",
9237
10825
  operation: "truth.list",
9238
10826
  resource: "records",
@@ -9339,9 +10927,9 @@ var TruthMemoryClient = class {
9339
10927
  }
9340
10928
  async #write(kind, recordKey, requestId, payload, expectedPrimary, expectedSnapshots) {
9341
10929
  assertDistinctExpectedWrites([expectedPrimary, ...expectedSnapshots]);
9342
- const path19 = recordPath(kind, recordKey);
10930
+ const path21 = recordPath(kind, recordKey);
9343
10931
  const body = new TextEncoder().encode(JSON.stringify(payload));
9344
- const response = await this.#proofFetch(path19, {
10932
+ const response = await this.#proofFetch(path21, {
9345
10933
  method: "PUT",
9346
10934
  operation: "truth.write",
9347
10935
  resource: `${kind}/${recordKey}`,
@@ -9367,8 +10955,8 @@ var TruthMemoryClient = class {
9367
10955
  };
9368
10956
  }
9369
10957
  async #readVerified(listed) {
9370
- const path19 = recordPath(listed.kind, listed.recordKey);
9371
- const response = await this.#proofFetch(path19, {
10958
+ const path21 = recordPath(listed.kind, listed.recordKey);
10959
+ const response = await this.#proofFetch(path21, {
9372
10960
  method: "GET",
9373
10961
  operation: "truth.read",
9374
10962
  resource: `${listed.kind}/${listed.recordKey}`,
@@ -9429,16 +11017,16 @@ var TruthMemoryClient = class {
9429
11017
  }
9430
11018
  return { ...listed, bytes };
9431
11019
  }
9432
- async #proofFetch(path19, request) {
11020
+ async #proofFetch(path21, request) {
9433
11021
  const proof = await this.options.signer.sign({
9434
11022
  method: request.method,
9435
- path: path19,
11023
+ path: path21,
9436
11024
  operation: request.operation,
9437
11025
  resource: request.resource,
9438
11026
  requestId: request.requestId,
9439
11027
  body: request.body
9440
11028
  });
9441
- const response = await this.#fetch(new URL(path19, this.#base), {
11029
+ const response = await this.#fetch(new URL(path21, this.#base), {
9442
11030
  method: request.method,
9443
11031
  headers: {
9444
11032
  ...request.headers,
@@ -9449,7 +11037,7 @@ var TruthMemoryClient = class {
9449
11037
  if (!response.ok) {
9450
11038
  throw new TruthMemoryClientError(
9451
11039
  "truth_http_failed",
9452
- `truth request ${request.method} ${path19} failed with HTTP ${response.status}`,
11040
+ `truth request ${request.method} ${path21} failed with HTTP ${response.status}`,
9453
11041
  response.status
9454
11042
  );
9455
11043
  }
@@ -9462,10 +11050,10 @@ function manifestPath(query) {
9462
11050
  if (query.keyPrefix !== void 0) search.set("prefix", query.keyPrefix);
9463
11051
  if (query.limit !== void 0) search.set("limit", String(query.limit));
9464
11052
  const encoded = search.toString();
9465
- return encoded.length === 0 ? "/byok/records" : `/byok/records?${encoded}`;
11053
+ return encoded.length === 0 ? BYOK_RECORDS_PATH : `${BYOK_RECORDS_PATH}?${encoded}`;
9466
11054
  }
9467
11055
  function recordPath(kind, recordKey) {
9468
- return `/byok/records/${encodeURIComponent(kind)}/${encodeURIComponent(recordKey)}`;
11056
+ return byokRecordPath(kind, recordKey);
9469
11057
  }
9470
11058
  function prepareTransportBody(body) {
9471
11059
  if (body.kind === "inline") {
@@ -9707,9 +11295,9 @@ function plistString(value) {
9707
11295
  function generateLaunchdPlist(def) {
9708
11296
  const { label, program, logDir } = def;
9709
11297
  const args = [program.command, ...program.args];
9710
- const cwd = program.cwd ?? os6.homedir();
9711
- const outLog = path15.join(logDir, `${label}.out.log`);
9712
- const errLog = path15.join(logDir, `${label}.err.log`);
11298
+ const cwd = program.cwd ?? os5.homedir();
11299
+ const outLog = path17.join(logDir, `${label}.out.log`);
11300
+ const errLog = path17.join(logDir, `${label}.err.log`);
9713
11301
  return `<?xml version="1.0" encoding="UTF-8"?>
9714
11302
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
9715
11303
  <plist version="1.0">
@@ -9741,8 +11329,8 @@ ${args.map((a) => ` ${plistString(a)}`).join("\n")}
9741
11329
  }
9742
11330
  function createLaunchdLifecycle(def, deps = {}) {
9743
11331
  const run = deps.run ?? defaultRunner;
9744
- const fs15 = deps.fs ?? promises;
9745
- const homedir = deps.homedir ?? (() => os6.homedir());
11332
+ const fs17 = deps.fs ?? promises;
11333
+ const homedir = deps.homedir ?? (() => os5.homedir());
9746
11334
  const getuid = deps.getuid ?? (() => {
9747
11335
  if (typeof process.getuid !== "function") {
9748
11336
  throw new Error("launchd lifecycle requires a POSIX uid (process.getuid unavailable) \u2014 this module only runs on macOS");
@@ -9750,12 +11338,12 @@ function createLaunchdLifecycle(def, deps = {}) {
9750
11338
  return process.getuid();
9751
11339
  });
9752
11340
  const label = sanitizeServiceName(def.name);
9753
- const plistPath = () => path15.join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
11341
+ const plistPath = () => path17.join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
9754
11342
  const domainTarget = () => `gui/${getuid()}`;
9755
11343
  const serviceTarget = () => `${domainTarget()}/${label}`;
9756
11344
  async function fileExists(p) {
9757
11345
  try {
9758
- await fs15.stat(p);
11346
+ await fs17.stat(p);
9759
11347
  return true;
9760
11348
  } catch {
9761
11349
  return false;
@@ -9763,9 +11351,9 @@ function createLaunchdLifecycle(def, deps = {}) {
9763
11351
  }
9764
11352
  async function writePlist(program) {
9765
11353
  const xml = generateLaunchdPlist({ label, program, logDir: def.logDir });
9766
- await fs15.mkdir(path15.dirname(plistPath()), { recursive: true });
9767
- await fs15.mkdir(def.logDir, { recursive: true });
9768
- await fs15.writeFile(plistPath(), xml, "utf8");
11354
+ await fs17.mkdir(path17.dirname(plistPath()), { recursive: true });
11355
+ await fs17.mkdir(def.logDir, { recursive: true });
11356
+ await fs17.writeFile(plistPath(), xml, "utf8");
9769
11357
  }
9770
11358
  async function install(opts = {}) {
9771
11359
  await writePlist(opts.program ?? def.program);
@@ -9776,7 +11364,7 @@ function createLaunchdLifecycle(def, deps = {}) {
9776
11364
  }
9777
11365
  async function uninstall() {
9778
11366
  await runIdempotent(run, "launchctl", ["bootout", serviceTarget()], "launchctl bootout", LAUNCHD_NOT_LOADED);
9779
- await fs15.rm(plistPath(), { force: true });
11367
+ await fs17.rm(plistPath(), { force: true });
9780
11368
  }
9781
11369
  async function start() {
9782
11370
  if (!await fileExists(plistPath())) {
@@ -9835,10 +11423,10 @@ function generateSystemdUnit(def) {
9835
11423
  const { name, displayName, program, logDir } = def;
9836
11424
  assertNoControlChars(name, "name");
9837
11425
  assertNoControlChars(displayName, "displayName");
9838
- const cwd = program.cwd ?? os6.homedir();
11426
+ const cwd = program.cwd ?? os5.homedir();
9839
11427
  assertNoControlChars(cwd, "program.cwd");
9840
- const outLog = path15.join(logDir, `${name}.out.log`);
9841
- const errLog = path15.join(logDir, `${name}.err.log`);
11428
+ const outLog = path17.join(logDir, `${name}.out.log`);
11429
+ const errLog = path17.join(logDir, `${name}.err.log`);
9842
11430
  assertNoControlChars(outLog, "logDir");
9843
11431
  assertNoControlChars(errLog, "logDir");
9844
11432
  const execStart = [program.command, ...program.args].map(quoteSystemdArg).join(" ");
@@ -9860,14 +11448,14 @@ WantedBy=default.target
9860
11448
  }
9861
11449
  function createSystemdLifecycle(def, deps = {}) {
9862
11450
  const run = deps.run ?? defaultRunner;
9863
- const fs15 = deps.fs ?? promises;
9864
- const homedir = deps.homedir ?? (() => os6.homedir());
11451
+ const fs17 = deps.fs ?? promises;
11452
+ const homedir = deps.homedir ?? (() => os5.homedir());
9865
11453
  const name = sanitizeServiceName(def.name);
9866
11454
  const unitName = `${name}.service`;
9867
- const unitPath = () => path15.join(homedir(), ".config", "systemd", "user", unitName);
11455
+ const unitPath = () => path17.join(homedir(), ".config", "systemd", "user", unitName);
9868
11456
  async function fileExists(p) {
9869
11457
  try {
9870
- await fs15.stat(p);
11458
+ await fs17.stat(p);
9871
11459
  return true;
9872
11460
  } catch {
9873
11461
  return false;
@@ -9875,9 +11463,9 @@ function createSystemdLifecycle(def, deps = {}) {
9875
11463
  }
9876
11464
  async function writeUnit(program) {
9877
11465
  const unit = generateSystemdUnit({ name, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
9878
- await fs15.mkdir(path15.dirname(unitPath()), { recursive: true });
9879
- await fs15.mkdir(def.logDir, { recursive: true });
9880
- await fs15.writeFile(unitPath(), unit, "utf8");
11466
+ await fs17.mkdir(path17.dirname(unitPath()), { recursive: true });
11467
+ await fs17.mkdir(def.logDir, { recursive: true });
11468
+ await fs17.writeFile(unitPath(), unit, "utf8");
9881
11469
  }
9882
11470
  async function install(opts = {}) {
9883
11471
  await writeUnit(opts.program ?? def.program);
@@ -9886,7 +11474,7 @@ function createSystemdLifecycle(def, deps = {}) {
9886
11474
  }
9887
11475
  async function uninstall() {
9888
11476
  await runIdempotent(run, "systemctl", ["--user", "disable", "--now", unitName], "systemctl disable --now", SYSTEMD_NOT_LOADED);
9889
- await fs15.rm(unitPath(), { force: true });
11477
+ await fs17.rm(unitPath(), { force: true });
9890
11478
  await run("systemctl", ["--user", "daemon-reload"]);
9891
11479
  }
9892
11480
  async function start() {
@@ -9949,7 +11537,7 @@ ${argXml}${cwdXml}
9949
11537
  }
9950
11538
  function createWinswLifecycle(def, deps = {}) {
9951
11539
  const run = deps.run ?? defaultRunner;
9952
- const fs15 = deps.fs ?? promises;
11540
+ const fs17 = deps.fs ?? promises;
9953
11541
  const windows = def.windows;
9954
11542
  if (!windows) {
9955
11543
  throw new Error("WinSW service lifecycle requires `ServiceDefinition.windows.winswBin` (the product-bundled WinSW executable path)");
@@ -9957,11 +11545,11 @@ function createWinswLifecycle(def, deps = {}) {
9957
11545
  const winswBin = windows.winswBin;
9958
11546
  const id = sanitizeServiceName(def.name);
9959
11547
  const installDir = windows.installDir ?? def.logDir;
9960
- const exePath = path15.join(installDir, `${id}.exe`);
9961
- const xmlPath = path15.join(installDir, `${id}.xml`);
11548
+ const exePath = path17.join(installDir, `${id}.exe`);
11549
+ const xmlPath = path17.join(installDir, `${id}.xml`);
9962
11550
  async function fileExists(p) {
9963
11551
  try {
9964
- await fs15.stat(p);
11552
+ await fs17.stat(p);
9965
11553
  return true;
9966
11554
  } catch {
9967
11555
  return false;
@@ -9969,10 +11557,10 @@ function createWinswLifecycle(def, deps = {}) {
9969
11557
  }
9970
11558
  async function writeFiles(program) {
9971
11559
  const xml = generateWinswXml({ id, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
9972
- await fs15.mkdir(installDir, { recursive: true });
9973
- await fs15.mkdir(def.logDir, { recursive: true });
9974
- await fs15.copyFile(winswBin, exePath);
9975
- await fs15.writeFile(xmlPath, xml, "utf8");
11560
+ await fs17.mkdir(installDir, { recursive: true });
11561
+ await fs17.mkdir(def.logDir, { recursive: true });
11562
+ await fs17.copyFile(winswBin, exePath);
11563
+ await fs17.writeFile(xmlPath, xml, "utf8");
9976
11564
  }
9977
11565
  async function install(opts = {}) {
9978
11566
  await writeFiles(opts.program ?? def.program);
@@ -9982,8 +11570,8 @@ function createWinswLifecycle(def, deps = {}) {
9982
11570
  async function uninstall() {
9983
11571
  await runIdempotent(run, exePath, ["stop"], "winsw stop", WINSW_NOT_INSTALLED);
9984
11572
  await runIdempotent(run, exePath, ["uninstall"], "winsw uninstall", WINSW_NOT_INSTALLED);
9985
- await fs15.rm(exePath, { force: true });
9986
- await fs15.rm(xmlPath, { force: true });
11573
+ await fs17.rm(exePath, { force: true });
11574
+ await fs17.rm(xmlPath, { force: true });
9987
11575
  }
9988
11576
  async function start() {
9989
11577
  if (!await fileExists(xmlPath)) {
@@ -10026,6 +11614,6 @@ function createServiceLifecycle(def, opts = {}) {
10026
11614
  }
10027
11615
  }
10028
11616
 
10029
- export { AuthManager, BlobClient, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, DeviceRevokedError, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, SecureDirHardeningError, SqliteLocalTaskJournal, SteerUnsupportedError, StoredDeviceProofSigner, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, ensureSecureDir, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, isGitWorkspaceConfig, journalHash, nodeAgentProgram, prependGitWorkspaceGuidance, resolveLocalStoragePolicy, sanitizeServiceName };
11617
+ export { AuthManager, BlobClient, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, DeviceRevokedError, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, SKILL_PACKS_CAPABILITY, SKILL_PACKS_DIRNAME, SKILL_PACK_AUDIT_FILENAME, SKILL_PACK_INSTALL_ERROR_CODES, SKILL_PACK_LOCK_FILENAME, SKILL_PACK_LOCK_SCHEMA, SKILL_PACK_RESPONSE_MAX_BYTES, SecureDirHardeningError, SkillPackInstallError, SqliteLocalTaskJournal, SteerUnsupportedError, StoredDeviceProofSigner, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, ensureSecureDir, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, installSkillPacks, isGitWorkspaceConfig, journalHash, listInstalledSkillPacks, nodeAgentProgram, prependGitWorkspaceGuidance, projectSkillPack, requestDeviceAssertion, resolveLocalStoragePolicy, sanitizeServiceName, skillPacksRoot };
10030
11618
  //# sourceMappingURL=index.js.map
10031
11619
  //# sourceMappingURL=index.js.map