@byok-sdk/client 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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';
2
+ import { createHash, randomUUID, sign, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual, createHmac } from 'crypto';
3
3
  import { promises, mkdirSync, existsSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, linkSync, fstatSync, lstatSync, unlinkSync, constants, readFileSync, realpathSync } from 'fs';
4
- import path16, { 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';
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 = `${path16.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 path16.resolve(value);
86
+ return path17.resolve(value);
87
87
  }
88
88
  function isContained(root, candidate) {
89
- const relative = path16.relative(root, candidate);
90
- return relative === "" || !relative.startsWith(`..${path16.sep}`) && !path16.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 = path16.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 = path16.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(path16.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 = path16.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;
@@ -673,7 +673,7 @@ var GitWorkspaceStore = class {
673
673
  };
674
674
  var PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
675
675
  function readPackageJson(dir) {
676
- const candidate = path16.join(dir, "package.json");
676
+ const candidate = path17.join(dir, "package.json");
677
677
  if (!existsSync(candidate)) return void 0;
678
678
  try {
679
679
  return JSON.parse(readFileSync(candidate, "utf8"));
@@ -688,17 +688,17 @@ function resolvePiBin() {
688
688
  }
689
689
  try {
690
690
  const mainEntryUrl = import.meta.resolve(PI_PACKAGE_NAME);
691
- let dir = path16.dirname(fileURLToPath(mainEntryUrl));
691
+ let dir = path17.dirname(fileURLToPath(mainEntryUrl));
692
692
  for (let depth = 0; depth < 6; depth++) {
693
693
  const pkg = readPackageJson(dir);
694
694
  if (pkg?.name === PI_PACKAGE_NAME) {
695
695
  const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi;
696
696
  if (binRel) {
697
- return { command: path16.join(dir, binRel), source: "package" };
697
+ return { command: path17.join(dir, binRel), source: "package" };
698
698
  }
699
699
  break;
700
700
  }
701
- const parent = path16.dirname(dir);
701
+ const parent = path17.dirname(dir);
702
702
  if (parent === dir) break;
703
703
  dir = parent;
704
704
  }
@@ -1088,13 +1088,8 @@ var PiRpcClient = class {
1088
1088
  }
1089
1089
  };
1090
1090
 
1091
- // src/adapters/pi/pi-adapter.ts
1092
- var execFileAsync = promisify(execFile);
1093
- var DETECT_TIMEOUT_MS = 5e3;
1094
- function errorMessage(err) {
1095
- return err instanceof Error ? err.message : String(err);
1096
- }
1097
- var KNOWN_PROVIDER_ENV_VARS = [
1091
+ // src/adapters/provider-credential-environment.ts
1092
+ var PROVIDER_CREDENTIAL_ENV_NAMES = [
1098
1093
  "ANTHROPIC_API_KEY",
1099
1094
  "ANTHROPIC_OAUTH_TOKEN",
1100
1095
  "OPENAI_API_KEY",
@@ -1105,24 +1100,66 @@ var KNOWN_PROVIDER_ENV_VARS = [
1105
1100
  "MISTRAL_API_KEY",
1106
1101
  "OPENROUTER_API_KEY",
1107
1102
  "XAI_API_KEY",
1108
- // Confirmed against the installed pi's own docs/providers.md ("ZAI |
1109
- // `ZAI_API_KEY` | `zai`") and exercised live against real GLM traffic
1110
- // during this task's acceptance run — omitting it made `authPresent`
1111
- // silently false for a perfectly valid, working z.ai/GLM setup.
1112
1103
  "ZAI_API_KEY"
1113
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
+ }
1114
1150
  var PiAdapter = class {
1115
1151
  constructor(options = {}) {
1116
1152
  this.options = options;
1117
1153
  }
1118
1154
  options;
1119
1155
  id = "pi";
1156
+ supportsDispatchSelection = true;
1120
1157
  async detect() {
1121
1158
  try {
1122
1159
  const bin = this.resolveBin();
1123
1160
  const { stdout, stderr } = await execFileAsync(bin.command, ["--version"], { timeout: DETECT_TIMEOUT_MS });
1124
1161
  const version = stdout.trim() || stderr.trim();
1125
- 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);
1126
1163
  return { present: true, version, authPresent };
1127
1164
  } catch {
1128
1165
  return { present: false };
@@ -1141,7 +1178,7 @@ var PiAdapter = class {
1141
1178
  * variable beyond the platform baseline (`daemon/environment.ts`).
1142
1179
  */
1143
1180
  environmentRequirements() {
1144
- return { credentialNames: KNOWN_PROVIDER_ENV_VARS };
1181
+ return { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES };
1145
1182
  }
1146
1183
  async start(task, ctx) {
1147
1184
  if (typeof task.instruction !== "string") {
@@ -1153,12 +1190,45 @@ var PiAdapter = class {
1153
1190
  }
1154
1191
  const bin = this.resolveBin();
1155
1192
  const resumeSessionId = task.sessionRef;
1156
- 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
+ }
1157
1227
  const rpc = new PiRpcClient({
1158
- command: bin.command,
1228
+ command,
1159
1229
  args,
1160
1230
  cwd: ctx.workspaceDir,
1161
- env: ctx.env,
1231
+ env: selection === void 0 ? ctx.env : withoutProviderCredentials(ctx.env),
1162
1232
  spawnFn: this.options.spawnFn
1163
1233
  });
1164
1234
  const response = await rpc.send({ type: "prompt", message: task.instruction });
@@ -1177,7 +1247,7 @@ var PiAdapter = class {
1177
1247
  throw err;
1178
1248
  }
1179
1249
  }
1180
- return new PiSession(sessionRef, rpc);
1250
+ return new PiSession(sessionRef, rpc, selection);
1181
1251
  }
1182
1252
  resolveBin() {
1183
1253
  return (this.options.resolveBin ?? resolvePiBin)();
@@ -1205,12 +1275,14 @@ async function resolveFreshSessionId(rpc) {
1205
1275
  );
1206
1276
  }
1207
1277
  var PiSession = class {
1208
- constructor(sessionRef, rpc) {
1278
+ constructor(sessionRef, rpc, selection) {
1209
1279
  this.sessionRef = sessionRef;
1210
1280
  this.rpc = rpc;
1281
+ this.selection = selection;
1211
1282
  }
1212
1283
  sessionRef;
1213
1284
  rpc;
1285
+ selection;
1214
1286
  get events() {
1215
1287
  const rpc = this.rpc;
1216
1288
  return {
@@ -1239,6 +1311,12 @@ var PiSession = class {
1239
1311
  if (typeof task.instruction !== "string") {
1240
1312
  throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
1241
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
+ }
1242
1320
  await this.rpc.send({ type: "prompt", message: task.instruction, streamingBehavior: "followUp" });
1243
1321
  }
1244
1322
  async interrupt() {
@@ -1265,7 +1343,7 @@ function resolveApprovalMcpBin() {
1265
1343
  if (override) {
1266
1344
  return { command: override, args: [], source: "env" };
1267
1345
  }
1268
- const distBin = path16.join(path16.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");
1269
1347
  return { command: process.execPath, args: [distBin], source: "dist" };
1270
1348
  }
1271
1349
 
@@ -1345,7 +1423,7 @@ var EXTENSION_CONTENT_TYPES = {
1345
1423
  ".yml": "application/yaml"
1346
1424
  };
1347
1425
  function guessContentType(filePath) {
1348
- const ext = path16.extname(filePath).toLowerCase();
1426
+ const ext = path17.extname(filePath).toLowerCase();
1349
1427
  return EXTENSION_CONTENT_TYPES[ext] ?? "application/octet-stream";
1350
1428
  }
1351
1429
  function mapAssistant(msg, correlation) {
@@ -1417,11 +1495,11 @@ function tryBuildArtifactEvent(msg, workspaceDir) {
1417
1495
  const filePath = toolUseResult && typeof toolUseResult.filePath === "string" ? toolUseResult.filePath : void 0;
1418
1496
  if (!filePath) return void 0;
1419
1497
  const realWorkspaceDir = tryRealpath(workspaceDir) ?? workspaceDir;
1420
- const fileDir = path16.dirname(filePath);
1498
+ const fileDir = path17.dirname(filePath);
1421
1499
  const realFileDir = tryRealpath(fileDir) ?? fileDir;
1422
- const realFilePath = path16.join(realFileDir, path16.basename(filePath));
1423
- const relative = path16.relative(realWorkspaceDir, realFilePath);
1424
- if (relative === "" || relative.startsWith("..") || path16.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)) {
1425
1503
  return void 0;
1426
1504
  }
1427
1505
  return { type: "artifact", name: relative, contentType: guessContentType(filePath) };
@@ -1663,7 +1741,7 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
1663
1741
  var APPROVAL_MCP_SERVER_NAME = "byokapproval";
1664
1742
  var execFileAsync2 = promisify(execFile);
1665
1743
  var DETECT_TIMEOUT_MS2 = 5e3;
1666
- async function cleanupApprovalMcpConfigDir(dir) {
1744
+ async function cleanupMcpConfigDir(dir) {
1667
1745
  if (!dir) return;
1668
1746
  await promises.rm(dir, { recursive: true, force: true }).catch(() => {
1669
1747
  });
@@ -1673,6 +1751,7 @@ var ClaudeAdapter = class {
1673
1751
  this.options = options;
1674
1752
  }
1675
1753
  options;
1754
+ supportsDispatchSelection = true;
1676
1755
  id = "claude";
1677
1756
  async detect() {
1678
1757
  const bin = this.resolveBin();
@@ -1686,7 +1765,13 @@ var ClaudeAdapter = class {
1686
1765
  }
1687
1766
  }
1688
1767
  capabilities() {
1689
- 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
+ };
1690
1775
  }
1691
1776
  /**
1692
1777
  * M5: deliberate product-boundary decision, not an oversight — byok's
@@ -1712,42 +1797,51 @@ var ClaudeAdapter = class {
1712
1797
  if (!mapping.ok) {
1713
1798
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
1714
1799
  }
1715
- 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;
1716
1804
  if (mapping.needsApprovalMcp) {
1717
1805
  if (!ctx.approvalChannel) {
1718
1806
  throw new PolicyUnsupportedError(
1719
1807
  'claude adapter requires policy.mode "confirm" to be started with an approval channel (TaskContext.approvalChannel) \u2014 none was provided'
1720
1808
  );
1721
1809
  }
1722
- const approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
1723
- approvalMcpConfigDir = await promises.mkdtemp(path16.join(os6.tmpdir(), "byok-approval-mcp-"));
1724
- 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(() => {
1725
1819
  });
1726
- const mcpConfigPath = path16.join(approvalMcpConfigDir, "mcp-config.json");
1727
- const mcpConfig = {
1728
- mcpServers: {
1729
- [APPROVAL_MCP_SERVER_NAME]: {
1730
- command: approvalMcpBin.command,
1731
- args: approvalMcpBin.args,
1732
- env: {
1733
- BYOK_STORE_DIR: ctx.approvalChannel.storeDir,
1734
- BYOK_PRODUCT_ID: ctx.approvalChannel.productId,
1735
- BYOK_TASK_ID: ctx.approvalChannel.taskId,
1736
- BYOK_APPROVAL_TIMEOUT_MS: String(ctx.approvalChannel.timeoutMs)
1737
- }
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)
1738
1834
  }
1739
- }
1740
- };
1741
- await promises.writeFile(mcpConfigPath, JSON.stringify(mcpConfig), { mode: 384 });
1835
+ };
1836
+ }
1837
+ await promises.writeFile(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
1742
1838
  mapping.args = [
1743
1839
  ...mapping.args,
1744
- "--permission-prompt-tool",
1745
- `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`,
1840
+ ...mapping.needsApprovalMcp ? ["--permission-prompt-tool", `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`] : [],
1746
1841
  "--mcp-config",
1747
1842
  mcpConfigPath,
1748
- // Never let this task's confirm-mode run pick up some OTHER MCP
1749
- // server from ambient project/user config the approval channel is
1750
- // 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.
1751
1845
  "--strict-mcp-config"
1752
1846
  ];
1753
1847
  }
@@ -1764,6 +1858,7 @@ var ClaudeAdapter = class {
1764
1858
  // "Error: When using --print, --output-format=stream-json requires
1765
1859
  // --verbose", before spawning any model call.
1766
1860
  "--verbose",
1861
+ ...modelId ? ["--model", modelId] : [],
1767
1862
  ...resumeSessionId ? ["--resume", resumeSessionId] : [],
1768
1863
  ...mapping.args
1769
1864
  ];
@@ -1771,7 +1866,7 @@ var ClaudeAdapter = class {
1771
1866
  command: bin.command,
1772
1867
  args,
1773
1868
  cwd: ctx.workspaceDir,
1774
- env: ctx.env,
1869
+ env: withoutProviderCredentials(ctx.env),
1775
1870
  spawnFn: this.options.spawnFn
1776
1871
  });
1777
1872
  client.writeUserMessage(task.instruction);
@@ -1780,17 +1875,24 @@ var ClaudeAdapter = class {
1780
1875
  sessionRef = await client.waitForInit();
1781
1876
  } catch (err) {
1782
1877
  client.kill();
1783
- await cleanupApprovalMcpConfigDir(approvalMcpConfigDir);
1878
+ await cleanupMcpConfigDir(mcpConfigDir);
1784
1879
  throw err;
1785
1880
  }
1786
1881
  if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
1787
1882
  client.kill();
1788
- await cleanupApprovalMcpConfigDir(approvalMcpConfigDir);
1883
+ await cleanupMcpConfigDir(mcpConfigDir);
1789
1884
  throw new Error(
1790
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)`
1791
1886
  );
1792
1887
  }
1793
- 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
+ );
1794
1896
  }
1795
1897
  /**
1796
1898
  * `claude auth status --json` is claude's OWN non-secret login-state
@@ -1824,19 +1926,31 @@ var ClaudeAdapter = class {
1824
1926
  return (this.options.resolveBin ?? resolveClaudeBin)();
1825
1927
  }
1826
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
+ }
1827
1939
  var ClaudeSession = class {
1828
- constructor(sessionRef, client, workspaceDir, approvalChannel, approvalMcpConfigDir) {
1940
+ constructor(sessionRef, client, workspaceDir, approvalChannel, mcpConfigDir, modelId) {
1829
1941
  this.sessionRef = sessionRef;
1830
1942
  this.client = client;
1831
1943
  this.workspaceDir = workspaceDir;
1832
1944
  this.approvalChannel = approvalChannel;
1833
- this.approvalMcpConfigDir = approvalMcpConfigDir;
1945
+ this.mcpConfigDir = mcpConfigDir;
1946
+ this.modelId = modelId;
1834
1947
  }
1835
1948
  sessionRef;
1836
1949
  client;
1837
1950
  workspaceDir;
1838
1951
  approvalChannel;
1839
- approvalMcpConfigDir;
1952
+ mcpConfigDir;
1953
+ modelId;
1840
1954
  correlation = createToolUseCorrelation();
1841
1955
  get events() {
1842
1956
  const client = this.client;
@@ -1887,6 +2001,12 @@ var ClaudeSession = class {
1887
2001
  if (typeof task.instruction !== "string") {
1888
2002
  throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
1889
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
+ }
1890
2010
  this.client.writeUserMessage(task.instruction);
1891
2011
  }
1892
2012
  /**
@@ -1906,7 +2026,7 @@ var ClaudeSession = class {
1906
2026
  }
1907
2027
  async close() {
1908
2028
  this.client.kill();
1909
- await cleanupApprovalMcpConfigDir(this.approvalMcpConfigDir);
2029
+ await cleanupMcpConfigDir(this.mcpConfigDir);
1910
2030
  }
1911
2031
  /**
1912
2032
  * M4 Phase 3: routes into the out-of-band approval channel `start()`
@@ -2070,8 +2190,8 @@ function extractArtifactEvents(changes, workspaceDir) {
2070
2190
  const absolutePath = typeof change.path === "string" ? change.path : void 0;
2071
2191
  const kind = typeof change.kind === "string" ? change.kind : void 0;
2072
2192
  if (!absolutePath || kind === "delete") continue;
2073
- const relative = path16.relative(workspaceDir, absolutePath);
2074
- if (relative.length === 0 || relative.startsWith("..") || path16.isAbsolute(relative)) continue;
2193
+ const relative = path17.relative(workspaceDir, absolutePath);
2194
+ if (relative.length === 0 || relative.startsWith("..") || path17.isAbsolute(relative)) continue;
2075
2195
  events.push({ type: "artifact", name: relative, contentType: guessContentType2(relative) });
2076
2196
  }
2077
2197
  return events;
@@ -2092,7 +2212,7 @@ var CONTENT_TYPE_BY_EXTENSION = {
2092
2212
  ".csv": "text/csv"
2093
2213
  };
2094
2214
  function guessContentType2(relativePath) {
2095
- return CONTENT_TYPE_BY_EXTENSION[path16.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
2215
+ return CONTENT_TYPE_BY_EXTENSION[path17.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
2096
2216
  }
2097
2217
  function extractErrorMessage(rawError) {
2098
2218
  if (typeof rawError === "string") return rawError;
@@ -2230,6 +2350,7 @@ var CodexAdapter = class {
2230
2350
  this.options = options;
2231
2351
  }
2232
2352
  options;
2353
+ supportsDispatchSelection = true;
2233
2354
  id = "codex";
2234
2355
  async detect() {
2235
2356
  const bin = this.resolveBin();
@@ -2302,17 +2423,20 @@ ${withStreams.stderr ?? ""}`);
2302
2423
  if (!mapping.ok) {
2303
2424
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
2304
2425
  }
2426
+ const modelId = subscriptionModel2(task);
2305
2427
  const bin = this.resolveBin();
2306
2428
  const queue = new AsyncQueue();
2307
2429
  const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
2308
2430
  const workspaceDir = await resolveRealWorkspaceDir(ctx.workspaceDir);
2431
+ const runtimeEnv = withoutProviderCredentials(ctx.env);
2309
2432
  const { sessionRef, runner } = await runCodexTurn({
2310
2433
  command: bin.command,
2311
2434
  resumeRef: task.sessionRef,
2312
2435
  instruction: task.instruction,
2436
+ modelId,
2313
2437
  policyArgs: mapping.args,
2314
2438
  cwd: ctx.workspaceDir,
2315
- env: ctx.env,
2439
+ env: runtimeEnv,
2316
2440
  spawnFn: this.options.spawnFn,
2317
2441
  workspaceDir,
2318
2442
  queue,
@@ -2329,7 +2453,8 @@ ${withStreams.stderr ?? ""}`);
2329
2453
  queue,
2330
2454
  recordUnmapped,
2331
2455
  initialRunner: runner,
2332
- preparedGit: ctx.gitWorkspace !== void 0
2456
+ preparedGit: ctx.gitWorkspace !== void 0,
2457
+ modelId
2333
2458
  });
2334
2459
  }
2335
2460
  resolveBin() {
@@ -2350,12 +2475,25 @@ function makeUnmappedFrameRecorder(counts) {
2350
2475
  }
2351
2476
  };
2352
2477
  }
2353
- function buildArgv(resumeRef, policyArgs, instruction, preparedGit = false) {
2478
+ function buildArgv(resumeRef, policyArgs, instruction, modelId, preparedGit = false) {
2354
2479
  const base = resumeRef !== void 0 ? ["exec", "resume", resumeRef] : ["exec"];
2355
- 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
+ ];
2356
2488
  }
2357
2489
  async function runCodexTurn(params) {
2358
- 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
+ );
2359
2497
  let firstLineSettled = false;
2360
2498
  let resolveFirstLine;
2361
2499
  let rejectFirstLine;
@@ -2456,6 +2594,7 @@ var CodexSession = class {
2456
2594
  queue;
2457
2595
  recordUnmapped;
2458
2596
  preparedGit;
2597
+ modelId;
2459
2598
  currentRunner;
2460
2599
  closed = false;
2461
2600
  constructor(options) {
@@ -2467,6 +2606,7 @@ var CodexSession = class {
2467
2606
  this.queue = options.queue;
2468
2607
  this.recordUnmapped = options.recordUnmapped;
2469
2608
  this.preparedGit = options.preparedGit;
2609
+ this.modelId = options.modelId;
2470
2610
  this.currentRunner = options.initialRunner;
2471
2611
  void this.forgetRunnerOnceClosed(options.initialRunner);
2472
2612
  }
@@ -2527,6 +2667,13 @@ var CodexSession = class {
2527
2667
  if (!mapping.ok) {
2528
2668
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
2529
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;
2530
2677
  const resumeRef = this.sessionRef;
2531
2678
  let sessionRef;
2532
2679
  let runner;
@@ -2535,9 +2682,10 @@ var CodexSession = class {
2535
2682
  command: this.command,
2536
2683
  resumeRef,
2537
2684
  instruction: task.instruction,
2685
+ modelId,
2538
2686
  policyArgs: mapping.args,
2539
2687
  cwd: this.workspaceDir,
2540
- env: this.env,
2688
+ env: withoutProviderCredentials(this.env),
2541
2689
  spawnFn: this.spawnFn,
2542
2690
  workspaceDir: this.workspaceDir,
2543
2691
  queue: this.queue,
@@ -2597,6 +2745,16 @@ var CodexSession = class {
2597
2745
  );
2598
2746
  }
2599
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
+ }
2600
2758
 
2601
2759
  // src/daemon/approvals.ts
2602
2760
  var ApprovalNotFoundError = class extends Error {
@@ -2699,12 +2857,12 @@ var DeviceStore = class _DeviceStore {
2699
2857
  */
2700
2858
  constructor(storeDir, secureDirOptions) {
2701
2859
  this.secureDirOptions = secureDirOptions;
2702
- this.filePath = path16.join(storeDir, "device.json");
2860
+ this.filePath = path17.join(storeDir, "device.json");
2703
2861
  }
2704
2862
  secureDirOptions;
2705
2863
  filePath;
2706
2864
  static defaultDir(productId) {
2707
- return path16.join(os6.homedir(), ".byok", productId);
2865
+ return path17.join(os5.homedir(), ".byok", productId);
2708
2866
  }
2709
2867
  /**
2710
2868
  * Resolve the one store pathname every daemon/CLI component must share.
@@ -2713,7 +2871,7 @@ var DeviceStore = class _DeviceStore {
2713
2871
  * cwd to pin a quarantine directory inode.
2714
2872
  */
2715
2873
  static resolveDir(productId, configured) {
2716
- return path16.resolve(configured ?? _DeviceStore.defaultDir(productId));
2874
+ return path17.resolve(configured ?? _DeviceStore.defaultDir(productId));
2717
2875
  }
2718
2876
  async load() {
2719
2877
  const opened = await this.openBounded();
@@ -2754,7 +2912,7 @@ var DeviceStore = class _DeviceStore {
2754
2912
  }
2755
2913
  }
2756
2914
  async save(record) {
2757
- const storeDir = path16.dirname(this.filePath);
2915
+ const storeDir = path17.dirname(this.filePath);
2758
2916
  await ensureSecureDir(storeDir, this.secureDirOptions);
2759
2917
  await atomicWriteFile(this.filePath, JSON.stringify(record, null, 2), { mode: 384 });
2760
2918
  }
@@ -2821,13 +2979,10 @@ function exportPrivateKeyPem(privateKey) {
2821
2979
  function importPrivateKeyPem(pem) {
2822
2980
  return createPrivateKey(pem);
2823
2981
  }
2824
- var NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
2825
2982
  function signNonce(privateKey, nonce) {
2826
- const signature = sign(null, Buffer.from(NONCE_SIGNING_DOMAIN + nonce, "utf8"), privateKey);
2983
+ const signature = sign(null, nonceSigningBytes(nonce), privateKey);
2827
2984
  return signature.toString("base64url");
2828
2985
  }
2829
-
2830
- // src/daemon/url.ts
2831
2986
  function toHttpBase(serverUrl) {
2832
2987
  const url = new URL(serverUrl);
2833
2988
  if (url.protocol === "ws:") url.protocol = "http:";
@@ -2837,7 +2992,7 @@ function toHttpBase(serverUrl) {
2837
2992
  return url.toString();
2838
2993
  }
2839
2994
  function toWsUrl(serverUrl) {
2840
- const url = new URL("/byok/ws", toHttpBase(serverUrl));
2995
+ const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
2841
2996
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
2842
2997
  return url.toString();
2843
2998
  }
@@ -2925,13 +3080,13 @@ var AuthManager = class {
2925
3080
  return await this.runCredentialMutation(async () => {
2926
3081
  const existing = this.record ?? await this.opts.store.load();
2927
3082
  const keyPair = existing ? { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey } : generateDeviceKeyPair();
2928
- const url = new URL("/byok/pair", toHttpBase(this.opts.serverUrl));
3083
+ const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
2929
3084
  const res = await fetch(url, {
2930
3085
  method: "POST",
2931
3086
  headers: { "content-type": "application/json" },
2932
3087
  body: JSON.stringify({
2933
3088
  pairingCode,
2934
- deviceName: this.opts.deviceName ?? os6.hostname(),
3089
+ deviceName: this.opts.deviceName ?? os5.hostname(),
2935
3090
  devicePublicKey: keyPair.publicKeyBase64Url
2936
3091
  })
2937
3092
  });
@@ -2959,6 +3114,7 @@ var AuthManager = class {
2959
3114
  async getValidAccessToken() {
2960
3115
  if (!this.record) throw new Error("device is not paired yet; call pair(pairingCode) first");
2961
3116
  if (this.revoked) throw new DeviceRevokedError();
3117
+ if (this.renewing) return this.renewing;
2962
3118
  if (msUntilExpiry(this.record.expiresAt) > RENEW_MARGIN_MS) return this.record.accessToken;
2963
3119
  return this.renew();
2964
3120
  }
@@ -2987,7 +3143,7 @@ var AuthManager = class {
2987
3143
  const record = this.record;
2988
3144
  const base = toHttpBase(this.opts.serverUrl);
2989
3145
  const privateKey = importPrivateKeyPem(record.devicePrivateKeyPem);
2990
- const challengeRes = await fetch(new URL("/byok/challenge", base), {
3146
+ const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
2991
3147
  method: "POST",
2992
3148
  headers: { "content-type": "application/json" },
2993
3149
  body: JSON.stringify({ deviceId: record.deviceId })
@@ -3000,7 +3156,7 @@ var AuthManager = class {
3000
3156
  }
3001
3157
  const { nonce } = await challengeRes.json();
3002
3158
  const signature = signNonce(privateKey, nonce);
3003
- const tokenRes = await fetch(new URL("/byok/token", base), {
3159
+ const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
3004
3160
  method: "POST",
3005
3161
  headers: { "content-type": "application/json" },
3006
3162
  body: JSON.stringify({ deviceId: record.deviceId, nonce, signature })
@@ -3098,7 +3254,7 @@ var BlobClient = class {
3098
3254
  async resolveInstruction(blobRef) {
3099
3255
  const base = toHttpBase(this.serverUrl);
3100
3256
  const urlRes = await authedFetch(
3101
- new URL(`/byok/blobs/${encodeURIComponent(blobRef.blobId)}/url`, base),
3257
+ new URL(byokBlobUrlPath(blobRef.blobId), base),
3102
3258
  { method: "GET" },
3103
3259
  this.auth
3104
3260
  );
@@ -3122,18 +3278,18 @@ var BlobClient = class {
3122
3278
  /** `POST /byok/blobs` (declares size/contentType/contentHash) -> PUT the bytes to the presigned upload URL -> a `BlobRef` for `task.artifact.blobRef`. */
3123
3279
  async uploadArtifact(content, contentType) {
3124
3280
  const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
3125
- const contentHash2 = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
3281
+ const contentHash3 = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
3126
3282
  const base = toHttpBase(this.serverUrl);
3127
3283
  const reservationId = `blob_${randomUUID()}`;
3128
3284
  const createRes = await authedFetch(
3129
- new URL("/byok/blobs", base),
3285
+ new URL(BYOK_BLOBS_PATH, base),
3130
3286
  {
3131
3287
  method: "POST",
3132
3288
  headers: {
3133
3289
  "content-type": "application/json",
3134
3290
  "idempotency-key": reservationId
3135
3291
  },
3136
- body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash2 })
3292
+ body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash3 })
3137
3293
  },
3138
3294
  this.auth
3139
3295
  );
@@ -3150,7 +3306,7 @@ var BlobClient = class {
3150
3306
  throw new Error(`failed to upload blob content: HTTP ${putRes.status}`);
3151
3307
  }
3152
3308
  await this.#finalize(base, blobId, reservationId);
3153
- return { blobId, contentHash: contentHash2, size: bytes.length, contentType };
3309
+ return { blobId, contentHash: contentHash3, size: bytes.length, contentType };
3154
3310
  }
3155
3311
  async #finalize(base, blobId, reservationId) {
3156
3312
  let lastFailure;
@@ -3158,7 +3314,7 @@ var BlobClient = class {
3158
3314
  let response;
3159
3315
  try {
3160
3316
  response = await authedFetch(
3161
- new URL(`/byok/blobs/${encodeURIComponent(blobId)}/finalize`, base),
3317
+ new URL(byokBlobFinalizePath(blobId), base),
3162
3318
  {
3163
3319
  method: "POST",
3164
3320
  headers: { "idempotency-key": reservationId }
@@ -3181,26 +3337,187 @@ var BlobClient = class {
3181
3337
  throw lastFailure;
3182
3338
  }
3183
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
+ }
3184
3500
  var CONTROL_PROTOCOL_VERSION = 1;
3185
3501
  var HANDSHAKE_TIMEOUT_MS = 3e3;
3186
3502
  var UNIX_SOCKET_PATH_SOFT_LIMIT = 100;
3503
+ var CONTROL_SOCKET_FALLBACK_ROOT = "/tmp";
3187
3504
  function shortHash(input) {
3188
3505
  return createHash("sha256").update(input, "utf8").digest("hex").slice(0, 16);
3189
3506
  }
3190
3507
  function controlSocketPath(storeDir) {
3191
- const candidate = path16.join(storeDir, "control.sock");
3508
+ const candidate = path17.join(storeDir, "control.sock");
3192
3509
  if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT) return candidate;
3193
- return path16.join(os6.tmpdir(), `byok-${shortHash(storeDir)}`, "sock");
3510
+ return path17.join(CONTROL_SOCKET_FALLBACK_ROOT, `byok-${shortHash(storeDir)}`, "sock");
3194
3511
  }
3195
3512
  function controlPipeName(productId, storeDir) {
3196
- const id = shortHash(`${productId}|${path16.resolve(storeDir)}`);
3513
+ const id = shortHash(`${productId}|${path17.resolve(storeDir)}`);
3197
3514
  return `\\\\.\\pipe\\byok-${id}`;
3198
3515
  }
3199
3516
  function controlEndpointPath(productId, storeDir, platform = process.platform) {
3200
3517
  return platform === "win32" ? controlPipeName(productId, storeDir) : controlSocketPath(storeDir);
3201
3518
  }
3202
3519
  function controlTokenPath(storeDir) {
3203
- return path16.join(storeDir, "control.token");
3520
+ return path17.join(storeDir, "control.token");
3204
3521
  }
3205
3522
  var SERVER_PROOF_LABEL = "byok-control-server|";
3206
3523
  var CLIENT_AUTH_LABEL = "byok-control-client|";
@@ -3230,11 +3547,23 @@ function parseClientHello(value) {
3230
3547
  if (value.v !== CONTROL_PROTOCOL_VERSION || value.hello !== "client" || typeof value.nonce !== "string") return void 0;
3231
3548
  return { v: CONTROL_PROTOCOL_VERSION, hello: "client", nonce: value.nonce };
3232
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
+ }
3233
3557
  function parseClientAuth(value) {
3234
3558
  if (!isRecord2(value)) return void 0;
3235
3559
  if (value.v !== CONTROL_PROTOCOL_VERSION || typeof value.auth !== "string") return void 0;
3236
3560
  return { v: CONTROL_PROTOCOL_VERSION, auth: value.auth };
3237
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
+ }
3238
3567
  function parseRawControlRequest(value) {
3239
3568
  if (!isRecord2(value)) return void 0;
3240
3569
  if (typeof value.id !== "string" || typeof value.method !== "string") return void 0;
@@ -3284,6 +3613,16 @@ function parseApprovalsRequestParams(value) {
3284
3613
  if (typeof value.summary !== "string") return void 0;
3285
3614
  return { taskId: value.taskId, summary: value.summary };
3286
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
+ }
3287
3626
  function parseShutdownParams(value) {
3288
3627
  if (!isRecord2(value)) return {};
3289
3628
  return value.reason === "unpair" || value.reason === "operator" ? { reason: value.reason } : {};
@@ -3345,7 +3684,7 @@ async function assertOwnedPrivateDir(dir) {
3345
3684
  }
3346
3685
  async function bindControlEndpoint(server, endpoint) {
3347
3686
  if (process.platform !== "win32") {
3348
- const endpointDir = path16.dirname(endpoint);
3687
+ const endpointDir = path17.dirname(endpoint);
3349
3688
  await promises.mkdir(endpointDir, { recursive: true, mode: 448 });
3350
3689
  await promises.chmod(endpointDir, 448).catch(() => {
3351
3690
  });
@@ -3616,7 +3955,7 @@ var LongPollClient = class {
3616
3955
  try {
3617
3956
  const base = toHttpBase(this.opts.serverUrl);
3618
3957
  const res = await authedFetch(
3619
- new URL("/byok/messages", base),
3958
+ new URL(BYOK_MESSAGES_PATH, base),
3620
3959
  {
3621
3960
  method: "POST",
3622
3961
  headers: { "content-type": "application/json" },
@@ -3640,7 +3979,7 @@ var LongPollClient = class {
3640
3979
  while (this.running) {
3641
3980
  try {
3642
3981
  const base = toHttpBase(this.opts.serverUrl);
3643
- const url = new URL("/byok/events", base);
3982
+ const url = new URL(BYOK_EVENTS_PATH, base);
3644
3983
  const cursor = this.opts.getCursor();
3645
3984
  if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
3646
3985
  const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
@@ -4626,7 +4965,7 @@ function sameFileState2(left, right) {
4626
4965
  return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
4627
4966
  }
4628
4967
  async function openOperationalHealthFile(storeDir) {
4629
- const filePath = path16.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
4968
+ const filePath = path17.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
4630
4969
  let namedBefore;
4631
4970
  try {
4632
4971
  namedBefore = await promises.lstat(filePath, { bigint: true });
@@ -4668,7 +5007,7 @@ var OperationalHealthTracker = class {
4668
5007
  #writeTail = Promise.resolve();
4669
5008
  #started = false;
4670
5009
  constructor(storeDir, options = {}) {
4671
- this.#filePath = path16.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
5010
+ this.#filePath = path17.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
4672
5011
  this.#windowMs = options.windowMs ?? 6e4;
4673
5012
  this.#failureThreshold = options.failureThreshold ?? 3;
4674
5013
  this.#maxFailures = options.maxFailures ?? 128;
@@ -4755,7 +5094,7 @@ var OperationalHealthTracker = class {
4755
5094
  async #load() {
4756
5095
  let opened;
4757
5096
  try {
4758
- opened = await openOperationalHealthFile(path16.dirname(this.#filePath));
5097
+ opened = await openOperationalHealthFile(path17.dirname(this.#filePath));
4759
5098
  } catch (err) {
4760
5099
  throw new Error("operational health state could not be read");
4761
5100
  }
@@ -4791,7 +5130,7 @@ var OperationalHealthTracker = class {
4791
5130
  if (!this.#state) return;
4792
5131
  const body = JSON.stringify(this.#state, null, 2);
4793
5132
  this.#writeTail = this.#writeTail.then(async () => {
4794
- await ensureSecureDir(path16.dirname(this.#filePath));
5133
+ await ensureSecureDir(path17.dirname(this.#filePath));
4795
5134
  await atomicWriteFile(this.#filePath, body, { mode: 384, fsync: true });
4796
5135
  });
4797
5136
  try {
@@ -4866,19 +5205,19 @@ var RECLAIM_FILENAME = `${DAEMON_OWNER_FILENAME}.reclaim`;
4866
5205
  var MAX_OWNER_BYTES = 4096;
4867
5206
  var RECLAIM_MALFORMED_GRACE_MS = 3e4;
4868
5207
  var SELF_PROCESS_STARTED_AT = new Date(Date.now() - process.uptime() * 1e3).toISOString();
4869
- var STORE_MUTEX_PORT_BASE = 1e4;
4870
- var STORE_MUTEX_PORT_COUNT = 2e4;
4871
- var STORE_MUTEX_PORT_CANDIDATES = 32;
4872
5208
  var STORE_MUTEX_ID_PREFIX = "byok-store-mutex-v1:";
4873
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";
4874
5213
  function storeMutexIdentity(canonicalStoreDir) {
4875
5214
  return createHash("sha256").update(canonicalStoreDir).digest("hex");
4876
5215
  }
4877
- function storeMutexPort(identity, attempt) {
4878
- const digest = Buffer.from(identity, "hex");
4879
- const start = digest.readUInt32BE(0) % STORE_MUTEX_PORT_COUNT;
4880
- const step = 1 + digest.readUInt32BE(4) % (STORE_MUTEX_PORT_COUNT - 1);
4881
- 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");
4882
5221
  }
4883
5222
  var DaemonOwnerActiveError = class extends Error {
4884
5223
  constructor(role) {
@@ -4999,70 +5338,90 @@ async function createLivenessListener() {
4999
5338
  })
5000
5339
  };
5001
5340
  }
5002
- async function probeStoreMutex(port) {
5341
+ async function probeStoreMutex(endpoint, identity) {
5003
5342
  return new Promise((resolve) => {
5004
- const socket = createConnection({ host: "127.0.0.1", port });
5343
+ const socket = createConnection(endpoint);
5005
5344
  let settled = false;
5006
5345
  let raw = "";
5007
5346
  const finish = (result) => {
5008
5347
  if (settled) return;
5009
5348
  settled = true;
5349
+ clearTimeout(timer);
5350
+ socket.removeAllListeners();
5010
5351
  socket.destroy();
5011
5352
  resolve(result);
5012
5353
  };
5354
+ const timer = setTimeout(() => finish({ kind: "occupied" }), STORE_MUTEX_PROBE_TIMEOUT_MS);
5013
5355
  socket.setEncoding("utf8");
5014
- socket.setTimeout(STORE_MUTEX_PROBE_TIMEOUT_MS, () => finish({ kind: "uncertain" }));
5015
5356
  socket.on("data", (chunk) => {
5016
5357
  raw += chunk;
5017
- if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "foreign-or-gone" });
5018
- });
5019
- socket.once("end", () => {
5020
- const line = raw.trimEnd();
5021
- const identity = line.startsWith(STORE_MUTEX_ID_PREFIX) ? line.slice(STORE_MUTEX_ID_PREFIX.length) : void 0;
5022
- finish(
5023
- identity && /^[a-f0-9]{64}$/.test(identity) ? { kind: "identity", identity } : { kind: "foreign-or-gone" }
5024
- );
5358
+ if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "occupied" });
5025
5359
  });
5026
- 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
+ );
5027
5365
  });
5028
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
+ }
5029
5387
  async function acquireStoreMutex(canonicalStoreDir) {
5030
5388
  const identity = storeMutexIdentity(canonicalStoreDir);
5031
- for (let attempt = 0; attempt < STORE_MUTEX_PORT_CANDIDATES; attempt += 1) {
5032
- 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}
5033
5400
  `));
5034
- const port = storeMutexPort(identity, attempt);
5035
- try {
5036
- await new Promise((resolve, reject) => {
5037
- server.once("error", reject);
5038
- server.listen({ host: "127.0.0.1", port, exclusive: true }, () => {
5039
- server.removeListener("error", reject);
5040
- resolve();
5041
- });
5401
+ try {
5402
+ await new Promise((resolve, reject) => {
5403
+ server.once("error", reject);
5404
+ server.listen(endpoint, () => {
5405
+ server.removeListener("error", reject);
5406
+ resolve();
5042
5407
  });
5043
- } catch (err) {
5044
- if (err.code !== "EADDRINUSE") throw err;
5045
- const probe = await probeStoreMutex(port);
5046
- if (probe.kind === "uncertain" || probe.kind === "identity" && probe.identity === identity) {
5047
- throw new DaemonOwnerActiveError("unknown");
5048
- }
5049
- continue;
5050
- }
5051
- server.unref();
5052
- let closed = false;
5053
- return {
5054
- port,
5055
- close: () => new Promise((resolve, reject) => {
5056
- if (closed) {
5057
- resolve();
5058
- return;
5059
- }
5060
- closed = true;
5061
- server.close((err) => err ? reject(err) : resolve());
5062
- })
5063
- };
5408
+ });
5409
+ } catch (err) {
5410
+ if (err.code === "EADDRINUSE") throw new DaemonOwnerActiveError("unknown");
5411
+ throw err;
5064
5412
  }
5065
- 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
+ };
5066
5425
  }
5067
5426
  async function reclaimExistsAndIsActive(reclaimPath) {
5068
5427
  let stat;
@@ -5110,8 +5469,8 @@ async function acquireDaemonOwner(storeDir, role, clock = () => /* @__PURE__ */
5110
5469
  await mutex.close().catch(() => void 0);
5111
5470
  throw err;
5112
5471
  }
5113
- const ownerPath = path16.join(storeDir, DAEMON_OWNER_FILENAME);
5114
- const reclaimPath = path16.join(storeDir, RECLAIM_FILENAME);
5472
+ const ownerPath = path17.join(storeDir, DAEMON_OWNER_FILENAME);
5473
+ const reclaimPath = path17.join(storeDir, RECLAIM_FILENAME);
5115
5474
  const record = {
5116
5475
  version: 2,
5117
5476
  pid: process.pid,
@@ -5178,6 +5537,7 @@ function toRuntimeInfoCapabilities(caps) {
5178
5537
  steer: caps.steer,
5179
5538
  resume: caps.resume,
5180
5539
  approvalInteractive: caps.approvalInteractive,
5540
+ ...caps.mcpToolsets === void 0 ? {} : { mcpToolsets: caps.mcpToolsets },
5181
5541
  permissionModes: caps.permissionModes
5182
5542
  };
5183
5543
  }
@@ -5188,7 +5548,7 @@ var CursorStore = class {
5188
5548
  storeDir;
5189
5549
  fileFor(serverUrl, deviceId) {
5190
5550
  const key = createHash("sha256").update(`${serverUrl}::${deviceId}`).digest("hex").slice(0, 32);
5191
- return path16.join(this.storeDir, `cursor-${key}.json`);
5551
+ return path17.join(this.storeDir, `cursor-${key}.json`);
5192
5552
  }
5193
5553
  async load(serverUrl, deviceId) {
5194
5554
  let raw;
@@ -5208,7 +5568,7 @@ var CursorStore = class {
5208
5568
  }
5209
5569
  async save(serverUrl, deviceId, cursor) {
5210
5570
  const file = this.fileFor(serverUrl, deviceId);
5211
- await promises.mkdir(path16.dirname(file), { recursive: true, mode: 448 });
5571
+ await promises.mkdir(path17.dirname(file), { recursive: true, mode: 448 });
5212
5572
  await atomicWriteFile(file, JSON.stringify({ cursor }));
5213
5573
  }
5214
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. */
@@ -5251,7 +5611,7 @@ var DaemonObserver = class {
5251
5611
  }
5252
5612
  /**
5253
5613
  * Feed a raw INBOUND (server -> daemon) envelope. Deliberately narrow: only
5254
- * `task.offer` produces a local event here — every other inbound type
5614
+ * either offer variant produces a local event here — every other inbound type
5255
5615
  * (`task.cancel`/`task.steer`/`task.approve`/`task.reject`) is a
5256
5616
  * best-effort notification whose OWN observable effect already surfaces
5257
5617
  * through the daemon's outbound envelopes (`task.cancelled`, `task.progress`
@@ -5259,7 +5619,7 @@ var DaemonObserver = class {
5259
5619
  * where those are actually reported from.
5260
5620
  */
5261
5621
  handleInboundEnvelope(envelope) {
5262
- if (envelope.type !== "task.offer") return;
5622
+ if (envelope.type !== "task.offer" && envelope.type !== "task.offer_with_toolsets") return;
5263
5623
  const taskId = envelope.task_id;
5264
5624
  if (this.taskInfo.has(taskId)) return;
5265
5625
  this.upsertTask(taskId, { state: "Offered", runtime: envelope.payload.runtime });
@@ -5380,6 +5740,37 @@ var DaemonObserver = class {
5380
5740
  noteShutdownComplete(reason, undeliveredOutboxCount) {
5381
5741
  this.emit({ kind: "shutdown-complete", ts: nowIso(), reason, undeliveredOutboxCount });
5382
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
+ }
5383
5774
  /** M4 Phase 3 hardening: see the `stale-approval-decision` `DaemonEvent` variant's own doc comment. */
5384
5775
  noteStaleApprovalDecision(taskId, decision, reason) {
5385
5776
  this.emit({ kind: "stale-approval-decision", ts: nowIso(), taskId, decision, reason });
@@ -5504,7 +5895,7 @@ var SessionWorkspaceStore = class {
5504
5895
  */
5505
5896
  queue = Promise.resolve();
5506
5897
  constructor(storeDir) {
5507
- this.filePath = path16.join(storeDir, "session-workspaces.json");
5898
+ this.filePath = path17.join(storeDir, "session-workspaces.json");
5508
5899
  }
5509
5900
  async get(sessionRef) {
5510
5901
  return this.enqueue(async () => {
@@ -5562,7 +5953,7 @@ var SessionWorkspaceStore = class {
5562
5953
  }
5563
5954
  }
5564
5955
  async save(all) {
5565
- const dir = path16.dirname(this.filePath);
5956
+ const dir = path17.dirname(this.filePath);
5566
5957
  await promises.mkdir(dir, { recursive: true, mode: 448 });
5567
5958
  const tmpPath = `${this.filePath}.${process.pid}-${tmpSeq2++}.tmp`;
5568
5959
  try {
@@ -5667,9 +6058,9 @@ function isSqliteAvailable() {
5667
6058
  }
5668
6059
  }
5669
6060
  var SECURE_FILE_MODE = 384;
5670
- function openJournalDatabase(path20, busyTimeoutMs, faults) {
6061
+ function openJournalDatabase(path21, busyTimeoutMs, faults) {
5671
6062
  const { DatabaseSync } = loadSqliteModule();
5672
- const db = new DatabaseSync(path20, { timeout: busyTimeoutMs });
6063
+ const db = new DatabaseSync(path21, { timeout: busyTimeoutMs });
5673
6064
  try {
5674
6065
  faults?.onStep?.("after-open");
5675
6066
  db.exec("PRAGMA auto_vacuum = INCREMENTAL;");
@@ -5812,9 +6203,9 @@ var RECEIVED_STATE = "received";
5812
6203
  function byteLength(value) {
5813
6204
  return Buffer.byteLength(value, "utf8");
5814
6205
  }
5815
- function fileBytes(path20) {
6206
+ function fileBytes(path21) {
5816
6207
  try {
5817
- return statSync(path20).size;
6208
+ return statSync(path21).size;
5818
6209
  } catch {
5819
6210
  return 0;
5820
6211
  }
@@ -7021,6 +7412,21 @@ var MAX_INLINE_ARTIFACT_BYTES = 64 * 1024;
7021
7412
  var MAX_TRACKED_TASK_IDS = 2e3;
7022
7413
  var MAX_DURATION_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxDurationMs";
7023
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
+ }
7024
7430
  var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
7025
7431
  function isKnownRuntimeId(id) {
7026
7432
  return RuntimeIdSchema.safeParse(id).success;
@@ -7033,6 +7439,14 @@ function orderByPreference(candidates, preference) {
7033
7439
  function adapterSupportsMode(adapter, mode) {
7034
7440
  return adapter.capabilities().permissionModes.includes(mode);
7035
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
+ }
7036
7450
  function errorMessage3(err) {
7037
7451
  return err instanceof Error ? err.message : String(err);
7038
7452
  }
@@ -7068,8 +7482,8 @@ function estimateEventBytes(event) {
7068
7482
  }
7069
7483
  async function openArtifact(workspaceDir, name) {
7070
7484
  const realWorkspaceDir = await promises.realpath(workspaceDir).catch(() => workspaceDir);
7071
- const candidate = path16.resolve(realWorkspaceDir, name);
7072
- const prefix = realWorkspaceDir.endsWith(path16.sep) ? realWorkspaceDir : realWorkspaceDir + path16.sep;
7485
+ const candidate = path17.resolve(realWorkspaceDir, name);
7486
+ const prefix = realWorkspaceDir.endsWith(path17.sep) ? realWorkspaceDir : realWorkspaceDir + path17.sep;
7073
7487
  if (candidate !== realWorkspaceDir && !candidate.startsWith(prefix)) {
7074
7488
  return { ok: false, reason: `artifact name "${name}" resolves outside the task workspace \u2014 rejected` };
7075
7489
  }
@@ -7388,6 +7802,9 @@ var TaskRunner = class {
7388
7802
  case "task.offer":
7389
7803
  await this.handleOffer(envelope.task_id, envelope.payload);
7390
7804
  return;
7805
+ case "task.offer_with_toolsets":
7806
+ await this.handleOffer(envelope.task_id, envelope.payload);
7807
+ return;
7391
7808
  case "task.cancel":
7392
7809
  await this.handleCancel(envelope.task_id, envelope.payload.reason);
7393
7810
  return;
@@ -7441,7 +7858,22 @@ var TaskRunner = class {
7441
7858
  );
7442
7859
  return;
7443
7860
  }
7444
- 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);
7445
7877
  if (!pick.ok) {
7446
7878
  this.decline(taskId, pick.reason, pick.retryable);
7447
7879
  return;
@@ -7467,7 +7899,7 @@ var TaskRunner = class {
7467
7899
  const sameProtocolTask = ledger?.taskId === taskId;
7468
7900
  const interruptedOldTask = ledger?.phase === "interrupted" && sameProtocolTask;
7469
7901
  const activeDifferentTask = ledger !== void 0 && ledger.taskId !== taskId && (ledger.phase === "preparing" || ledger.phase === "active");
7470
- if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !== payload.sessionRef || path16.resolve(ledger.workspaceDir) !== path16.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) {
7471
7903
  this.decline(taskId, "session is incompatible with Git workspace mode", true);
7472
7904
  return;
7473
7905
  }
@@ -7482,7 +7914,7 @@ var TaskRunner = class {
7482
7914
  return;
7483
7915
  }
7484
7916
  } else {
7485
- workspaceDir = path16.join(this.deps.workspaceRoot, taskId);
7917
+ workspaceDir = path17.join(this.deps.workspaceRoot, taskId);
7486
7918
  }
7487
7919
  try {
7488
7920
  gitLease = await gitManager.acquireLease(workspaceDir, payload.sessionRef);
@@ -7492,7 +7924,7 @@ var TaskRunner = class {
7492
7924
  }
7493
7925
  } else if (!this.deps.gitWorkspaceManager && !this.deps.gitWorkspaceStore) {
7494
7926
  known = payload.sessionRef ? await this.deps.sessionWorkspaces.get(payload.sessionRef) : void 0;
7495
- workspaceDir = known?.workspaceDir ?? path16.join(this.deps.workspaceRoot, taskId);
7927
+ workspaceDir = known?.workspaceDir ?? path17.join(this.deps.workspaceRoot, taskId);
7496
7928
  plainWorkspaceNeedsResolve = true;
7497
7929
  } else {
7498
7930
  this.decline(taskId, "workspace mode is unavailable", true);
@@ -7604,6 +8036,7 @@ var TaskRunner = class {
7604
8036
  const ctx = {
7605
8037
  workspaceDir,
7606
8038
  policy: decision.policy,
8039
+ ...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {},
7607
8040
  ...gitWorkspaceId ? { gitWorkspace: { workspaceId: gitWorkspaceId, baseline: gitBaseline } } : {},
7608
8041
  // M5: no longer `process.env` verbatim (see `environment.ts`'s own
7609
8042
  // module doc comment for the credential-leak gap that closed) —
@@ -7640,7 +8073,7 @@ var TaskRunner = class {
7640
8073
  }
7641
8074
  };
7642
8075
  const effectiveOffer = {
7643
- ...payload,
8076
+ ...withoutRequiredToolsets(payload),
7644
8077
  instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
7645
8078
  // Never forward a sessionRef this device has no recorded workspace
7646
8079
  // for (stale, from another device, or simply made up) — an adapter
@@ -7718,6 +8151,36 @@ var TaskRunner = class {
7718
8151
  if (typeof instruction === "string") return instruction;
7719
8152
  return this.deps.blobClient.resolveInstruction(instruction.blobRef);
7720
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
+ }
7721
8184
  async pump(active) {
7722
8185
  try {
7723
8186
  for await (const event of active.session.events) {
@@ -7751,11 +8214,38 @@ var TaskRunner = class {
7751
8214
  if (event.type === "turn_end") {
7752
8215
  active.batcher.push(event);
7753
8216
  active.batcher.flush();
8217
+ const finalOutput = active.summaryParts.join("");
8218
+ const outcome = await this.resolveResultDocument(active, finalOutput);
8219
+ if (!outcome.deliver) return;
7754
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
+ }
7755
8229
  this.deps.send(
7756
8230
  createEnvelope(
7757
8231
  "task.complete",
7758
- { 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
+ },
7759
8249
  { taskId: active.taskId, sessionRef: active.session.sessionRef }
7760
8250
  )
7761
8251
  );
@@ -8293,6 +8783,102 @@ var TaskRunner = class {
8293
8783
  this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId }));
8294
8784
  await this.finish(taskId);
8295
8785
  }
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;
8836
+ try {
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
+ }
8296
8882
  async observeGit(active, phase) {
8297
8883
  if (!active.gitWorkspaceId || !this.deps.gitWorkspaceManager || !this.deps.gitWorkspaceStore) return;
8298
8884
  try {
@@ -8364,7 +8950,7 @@ var TaskRunner = class {
8364
8950
  }
8365
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. */
8366
8952
  async resolveWorkspaceDir(taskId, reuseDir) {
8367
- const dir = reuseDir ?? path16.join(this.deps.workspaceRoot, taskId);
8953
+ const dir = reuseDir ?? path17.join(this.deps.workspaceRoot, taskId);
8368
8954
  await promises.mkdir(dir, { recursive: true });
8369
8955
  return dir;
8370
8956
  }
@@ -8395,7 +8981,7 @@ var TaskRunner = class {
8395
8981
  * is device-specific (which runtimes happen to be installed here), so a
8396
8982
  * different device's installed runtime set might satisfy it.
8397
8983
  */
8398
- async pickAdapter(requestedRuntime, policyMode) {
8984
+ async pickAdapter(requestedRuntime, policyMode, requiresMcpToolsets) {
8399
8985
  const allowlist = this.deps.runtimeAllowlist;
8400
8986
  if (requestedRuntime) {
8401
8987
  if (allowlist && !allowlist.includes(requestedRuntime)) {
@@ -8416,6 +9002,13 @@ var TaskRunner = class {
8416
9002
  retryable: false
8417
9003
  };
8418
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
+ }
8419
9012
  const detected = await adapter.detect();
8420
9013
  if (!detected.present) {
8421
9014
  return {
@@ -8430,12 +9023,13 @@ var TaskRunner = class {
8430
9023
  const candidates = orderByPreference(eligible, this.deps.runtimePreference ?? DEFAULT_RUNTIME_PREFERENCE);
8431
9024
  for (const adapter of candidates) {
8432
9025
  if (!adapterSupportsMode(adapter, policyMode)) continue;
9026
+ if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) continue;
8433
9027
  const detected = await adapter.detect();
8434
9028
  if (detected.present) return { ok: true, adapter };
8435
9029
  }
8436
9030
  return {
8437
9031
  ok: false,
8438
- 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}"`,
8439
9033
  retryable: true
8440
9034
  };
8441
9035
  }
@@ -8466,7 +9060,7 @@ function toJournalEnvelopeRecord(envelope, identity) {
8466
9060
  bytes,
8467
9061
  bytesHash: journalHash(bytes),
8468
9062
  receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
8469
- opensTask: envelope.type === "task.offer"
9063
+ opensTask: envelope.type === "task.offer" || envelope.type === "task.offer_with_toolsets"
8470
9064
  };
8471
9065
  }
8472
9066
  function isRuntimeId(id) {
@@ -8490,48 +9084,235 @@ function computeCapabilities(adapters) {
8490
9084
  if (adapters.some((adapter) => adapter.capabilities().steer)) flags.push("steer");
8491
9085
  flags.push("blob-upload");
8492
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
+ }
8493
9096
  return flags;
8494
9097
  }
8495
9098
  var ALL_RUNTIME_IDS = ["pi", "claude", "codex"];
8496
- function buildAdapter(id) {
9099
+ function buildAdapter(id, config) {
8497
9100
  switch (id) {
8498
9101
  case "pi":
8499
- return new PiAdapter();
9102
+ return new PiAdapter({ byokLauncher: config.piByokLauncher });
8500
9103
  case "claude":
8501
9104
  return new ClaudeAdapter();
8502
9105
  case "codex":
8503
9106
  return new CodexAdapter();
8504
9107
  }
8505
9108
  }
8506
- function buildDefaultAdapters(runtimeAllowlist) {
8507
- const ids = runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => runtimeAllowlist.includes(id)) : ALL_RUNTIME_IDS;
8508
- 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));
8509
9112
  }
8510
- function createDaemonWithAdapters(config, adapters, overrides = {}) {
8511
- if (config.maxTaskOutputBytes !== void 0 && !(config.maxTaskOutputBytes > 0)) {
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)) {
8512
9124
  throw new Error(
8513
- `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".`
9125
+ "DaemonConfig.piByokLauncher profileDbPath and sessionDir must be absolute paths"
8514
9126
  );
8515
9127
  }
8516
- const storeDir = DeviceStore.resolveDir(config.productId, config.storeDir);
8517
- const store = new DeviceStore(storeDir);
8518
- const operationalHealth = new OperationalHealthTracker(storeDir);
8519
- let fleetJitter;
8520
- let maintenanceSequence = 0;
8521
- const cursorStore = new CursorStore(storeDir);
8522
- const sessionWorkspaces = new SessionWorkspaceStore(storeDir);
8523
- const gitWorkspaceManager = config.gitWorkspace ? overrides.gitWorkspace?.manager ?? new GitWorkspaceManager(config.workspaceRoot, { ownerId: stableGitWorkspaceOwnerId(storeDir, config.productId) }) : void 0;
8524
- const gitWorkspaceStore = config.gitWorkspace ? overrides.gitWorkspace?.store ?? new GitWorkspaceStore(storeDir) : void 0;
8525
- let resolvedStoragePolicy;
8526
- if (config.hostedJournal) {
8527
- if (config.hostedJournal.mode !== "sqlite") {
8528
- throw new Error(`DaemonConfig.hostedJournal.mode must be "sqlite" \u2014 got ${JSON.stringify(config.hostedJournal.mode)}`);
8529
- }
8530
- if (typeof config.hostedJournal.tenantId !== "string" || config.hostedJournal.tenantId.trim() === "") {
8531
- throw new Error(
8532
- "DaemonConfig.hostedJournal.tenantId must be a non-empty tenant id \u2014 a hosted journal row with no tenant is durable evidence nobody can act on"
8533
- );
8534
- }
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;
9274
+ }
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
+ }
9283
+ if (config.maxTaskOutputBytes !== void 0 && !(config.maxTaskOutputBytes > 0)) {
9284
+ throw new Error(
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".`
9286
+ );
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;
9297
+ const storeDir = DeviceStore.resolveDir(config.productId, config.storeDir);
9298
+ const store = new DeviceStore(storeDir);
9299
+ const operationalHealth = new OperationalHealthTracker(storeDir);
9300
+ let fleetJitter;
9301
+ let maintenanceSequence = 0;
9302
+ const cursorStore = new CursorStore(storeDir);
9303
+ const sessionWorkspaces = new SessionWorkspaceStore(storeDir);
9304
+ const gitWorkspaceManager = config.gitWorkspace ? overrides.gitWorkspace?.manager ?? new GitWorkspaceManager(config.workspaceRoot, { ownerId: stableGitWorkspaceOwnerId(storeDir, config.productId) }) : void 0;
9305
+ const gitWorkspaceStore = config.gitWorkspace ? overrides.gitWorkspace?.store ?? new GitWorkspaceStore(storeDir) : void 0;
9306
+ let resolvedStoragePolicy;
9307
+ if (config.hostedJournal) {
9308
+ if (config.hostedJournal.mode !== "sqlite") {
9309
+ throw new Error(`DaemonConfig.hostedJournal.mode must be "sqlite" \u2014 got ${JSON.stringify(config.hostedJournal.mode)}`);
9310
+ }
9311
+ if (typeof config.hostedJournal.tenantId !== "string" || config.hostedJournal.tenantId.trim() === "") {
9312
+ throw new Error(
9313
+ "DaemonConfig.hostedJournal.tenantId must be a non-empty tenant id \u2014 a hosted journal row with no tenant is durable evidence nobody can act on"
9314
+ );
9315
+ }
8535
9316
  if (config.hostedJournal.storagePolicy) {
8536
9317
  resolvedStoragePolicy = resolveLocalStoragePolicy(config.hostedJournal.storagePolicy);
8537
9318
  }
@@ -8603,6 +9384,9 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8603
9384
  let runner;
8604
9385
  let controlServerHandle;
8605
9386
  let daemonOwnerLease;
9387
+ let presencePublisher;
9388
+ let presenceDiscovery;
9389
+ let presenceDiscoveryInFlight = false;
8606
9390
  let shutdownPromise;
8607
9391
  const pendingLateMutationBarriers = /* @__PURE__ */ new Set();
8608
9392
  let startedAt;
@@ -8775,6 +9559,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8775
9559
  deviceId: record.deviceId,
8776
9560
  // M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
8777
9561
  runtimeEnvironment: config.runtimeEnvironment,
9562
+ ...mcpToolsets ? { mcpToolsets } : {},
8778
9563
  // M3-2a: `send` is already this file's OWN closure (not something
8779
9564
  // `TaskRunner` builds) — every `task.claim`/`task.started`/
8780
9565
  // `task.progress`/`task.artifact`/`task.await_approval`/
@@ -8814,6 +9599,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8814
9599
  shutdownInterruptTimeoutMs: overrides.shutdown?.taskInterruptTimeoutMs,
8815
9600
  // M5 batch-3 (workstream 2): see DaemonConfig.maxTaskOutputBytes's own doc comment — already validated above.
8816
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 } : {},
8817
9607
  // M4 Phase 3 hardening: bridges TaskRunner's stale-approval-race
8818
9608
  // finding out to the SAME local observability seam every other
8819
9609
  // daemon-local event already uses (see observer.ts's own module doc
@@ -8889,8 +9679,10 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8889
9679
  return runner?.handleEnvelope(envelope) ?? Promise.resolve();
8890
9680
  },
8891
9681
  onStateChange: (state) => {
9682
+ const wasSettled = connectionState === "open" || connectionState === "degraded";
8892
9683
  connectionState = state;
8893
9684
  observer.noteConnectionState(state);
9685
+ if (!wasSettled && (state === "open" || state === "degraded")) runPresenceDiscovery();
8894
9686
  },
8895
9687
  backoff: overrides.backoff,
8896
9688
  liveness: overrides.liveness,
@@ -8908,6 +9700,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8908
9700
  });
8909
9701
  await connection.start();
8910
9702
  await connection.waitForAck();
9703
+ startPresenceProducer();
8911
9704
  } catch (err) {
8912
9705
  try {
8913
9706
  await runShutdownSequence("startup failed", { drainTimeoutMs: 0 });
@@ -8917,7 +9710,41 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8917
9710
  throw err;
8918
9711
  }
8919
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
+ }
8920
9746
  async function runShutdownSequence(reason, opts = {}) {
9747
+ shuttingDown = true;
8921
9748
  const errors = [];
8922
9749
  let mutationBarrierComplete = hostedStorageInitializationBarrierComplete;
8923
9750
  if (!hostedStorageInitializationBarrierComplete) {
@@ -8946,6 +9773,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
8946
9773
  errors.push(new Error("a prior active task teardown remains unsettled; ownership lease retained"));
8947
9774
  }
8948
9775
  }
9776
+ presenceDiscovery?.abort();
9777
+ presenceDiscovery = void 0;
9778
+ presenceDiscoveryInFlight = false;
9779
+ presencePublisher?.stop();
9780
+ presencePublisher = void 0;
8949
9781
  const stoppingOwnedPressureEngine = ownedPressureEngine;
8950
9782
  const stoppingOwnedJournal = ownedJournal;
8951
9783
  const maintenanceStopped = stoppingOwnedPressureEngine?.stop() ?? Promise.resolve();
@@ -9006,6 +9838,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
9006
9838
  }
9007
9839
  }
9008
9840
  async function stop(opts = {}) {
9841
+ shuttingDown = true;
9009
9842
  await requestShutdown(opts.reason ?? "operator", { drainTimeoutMs: opts.drainTimeoutMs });
9010
9843
  }
9011
9844
  function requestShutdown(reason, opts = {}) {
@@ -9018,6 +9851,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
9018
9851
  return current;
9019
9852
  }
9020
9853
  async function unpair() {
9854
+ shuttingDown = true;
9021
9855
  await runLifecycleMutation(unpairUnderLease);
9022
9856
  }
9023
9857
  async function unpairUnderLease() {
@@ -9096,6 +9930,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
9096
9930
  }
9097
9931
  async function performControlShutdown(reason) {
9098
9932
  const effectiveReason = reason ?? "operator";
9933
+ shuttingDown = true;
9099
9934
  observer.noteShutdownRequested(effectiveReason);
9100
9935
  try {
9101
9936
  await requestShutdown(`control socket shutdown (${effectiveReason})`);
@@ -9133,7 +9968,107 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
9133
9968
  if (!runner) throw new ControlError("not_found", "daemon is not started");
9134
9969
  return runner.requestApproval(parsed.taskId, parsed.summary);
9135
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
+ },
9136
10070
  shutdown: (params) => {
10071
+ shuttingDown = true;
9137
10072
  const { reason } = parseShutdownParams(params);
9138
10073
  setImmediate(() => {
9139
10074
  void performControlShutdown(reason).catch((err) => {
@@ -9178,7 +10113,275 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
9178
10113
  return { pair, start, stop, status, subscribe, tasks, unpair, approve, reject };
9179
10114
  }
9180
10115
  function createDaemon(config) {
9181
- 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
+ }
9182
10385
  }
9183
10386
  var StoredDeviceProofSigner = class {
9184
10387
  constructor(options) {
@@ -9237,9 +10440,359 @@ function requireNonEmpty(name, value) {
9237
10440
  function sha256(bytes) {
9238
10441
  return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
9239
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
+ }
9240
10794
  var EMPTY_BODY = new Uint8Array();
9241
10795
  var LARGE_SNAPSHOT_THRESHOLD_BYTES = 1024 * 1024;
9242
- var DEVICE_PROOF_HEADER = "x-byok-device-proof";
9243
10796
  var TruthMemoryClientError = class extends Error {
9244
10797
  constructor(code, message, status) {
9245
10798
  super(message);
@@ -9266,8 +10819,8 @@ var TruthMemoryClient = class {
9266
10819
  #requestId;
9267
10820
  #allowedObjectDownloadOrigins;
9268
10821
  async listManifest(query = {}) {
9269
- const path20 = manifestPath(query);
9270
- const response = await this.#proofFetch(path20, {
10822
+ const path21 = manifestPath(query);
10823
+ const response = await this.#proofFetch(path21, {
9271
10824
  method: "GET",
9272
10825
  operation: "truth.list",
9273
10826
  resource: "records",
@@ -9374,9 +10927,9 @@ var TruthMemoryClient = class {
9374
10927
  }
9375
10928
  async #write(kind, recordKey, requestId, payload, expectedPrimary, expectedSnapshots) {
9376
10929
  assertDistinctExpectedWrites([expectedPrimary, ...expectedSnapshots]);
9377
- const path20 = recordPath(kind, recordKey);
10930
+ const path21 = recordPath(kind, recordKey);
9378
10931
  const body = new TextEncoder().encode(JSON.stringify(payload));
9379
- const response = await this.#proofFetch(path20, {
10932
+ const response = await this.#proofFetch(path21, {
9380
10933
  method: "PUT",
9381
10934
  operation: "truth.write",
9382
10935
  resource: `${kind}/${recordKey}`,
@@ -9402,8 +10955,8 @@ var TruthMemoryClient = class {
9402
10955
  };
9403
10956
  }
9404
10957
  async #readVerified(listed) {
9405
- const path20 = recordPath(listed.kind, listed.recordKey);
9406
- const response = await this.#proofFetch(path20, {
10958
+ const path21 = recordPath(listed.kind, listed.recordKey);
10959
+ const response = await this.#proofFetch(path21, {
9407
10960
  method: "GET",
9408
10961
  operation: "truth.read",
9409
10962
  resource: `${listed.kind}/${listed.recordKey}`,
@@ -9464,16 +11017,16 @@ var TruthMemoryClient = class {
9464
11017
  }
9465
11018
  return { ...listed, bytes };
9466
11019
  }
9467
- async #proofFetch(path20, request) {
11020
+ async #proofFetch(path21, request) {
9468
11021
  const proof = await this.options.signer.sign({
9469
11022
  method: request.method,
9470
- path: path20,
11023
+ path: path21,
9471
11024
  operation: request.operation,
9472
11025
  resource: request.resource,
9473
11026
  requestId: request.requestId,
9474
11027
  body: request.body
9475
11028
  });
9476
- const response = await this.#fetch(new URL(path20, this.#base), {
11029
+ const response = await this.#fetch(new URL(path21, this.#base), {
9477
11030
  method: request.method,
9478
11031
  headers: {
9479
11032
  ...request.headers,
@@ -9484,7 +11037,7 @@ var TruthMemoryClient = class {
9484
11037
  if (!response.ok) {
9485
11038
  throw new TruthMemoryClientError(
9486
11039
  "truth_http_failed",
9487
- `truth request ${request.method} ${path20} failed with HTTP ${response.status}`,
11040
+ `truth request ${request.method} ${path21} failed with HTTP ${response.status}`,
9488
11041
  response.status
9489
11042
  );
9490
11043
  }
@@ -9497,10 +11050,10 @@ function manifestPath(query) {
9497
11050
  if (query.keyPrefix !== void 0) search.set("prefix", query.keyPrefix);
9498
11051
  if (query.limit !== void 0) search.set("limit", String(query.limit));
9499
11052
  const encoded = search.toString();
9500
- return encoded.length === 0 ? "/byok/records" : `/byok/records?${encoded}`;
11053
+ return encoded.length === 0 ? BYOK_RECORDS_PATH : `${BYOK_RECORDS_PATH}?${encoded}`;
9501
11054
  }
9502
11055
  function recordPath(kind, recordKey) {
9503
- return `/byok/records/${encodeURIComponent(kind)}/${encodeURIComponent(recordKey)}`;
11056
+ return byokRecordPath(kind, recordKey);
9504
11057
  }
9505
11058
  function prepareTransportBody(body) {
9506
11059
  if (body.kind === "inline") {
@@ -9742,9 +11295,9 @@ function plistString(value) {
9742
11295
  function generateLaunchdPlist(def) {
9743
11296
  const { label, program, logDir } = def;
9744
11297
  const args = [program.command, ...program.args];
9745
- const cwd = program.cwd ?? os6.homedir();
9746
- const outLog = path16.join(logDir, `${label}.out.log`);
9747
- const errLog = path16.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`);
9748
11301
  return `<?xml version="1.0" encoding="UTF-8"?>
9749
11302
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
9750
11303
  <plist version="1.0">
@@ -9776,8 +11329,8 @@ ${args.map((a) => ` ${plistString(a)}`).join("\n")}
9776
11329
  }
9777
11330
  function createLaunchdLifecycle(def, deps = {}) {
9778
11331
  const run = deps.run ?? defaultRunner;
9779
- const fs15 = deps.fs ?? promises;
9780
- const homedir = deps.homedir ?? (() => os6.homedir());
11332
+ const fs17 = deps.fs ?? promises;
11333
+ const homedir = deps.homedir ?? (() => os5.homedir());
9781
11334
  const getuid = deps.getuid ?? (() => {
9782
11335
  if (typeof process.getuid !== "function") {
9783
11336
  throw new Error("launchd lifecycle requires a POSIX uid (process.getuid unavailable) \u2014 this module only runs on macOS");
@@ -9785,12 +11338,12 @@ function createLaunchdLifecycle(def, deps = {}) {
9785
11338
  return process.getuid();
9786
11339
  });
9787
11340
  const label = sanitizeServiceName(def.name);
9788
- const plistPath = () => path16.join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
11341
+ const plistPath = () => path17.join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
9789
11342
  const domainTarget = () => `gui/${getuid()}`;
9790
11343
  const serviceTarget = () => `${domainTarget()}/${label}`;
9791
11344
  async function fileExists(p) {
9792
11345
  try {
9793
- await fs15.stat(p);
11346
+ await fs17.stat(p);
9794
11347
  return true;
9795
11348
  } catch {
9796
11349
  return false;
@@ -9798,9 +11351,9 @@ function createLaunchdLifecycle(def, deps = {}) {
9798
11351
  }
9799
11352
  async function writePlist(program) {
9800
11353
  const xml = generateLaunchdPlist({ label, program, logDir: def.logDir });
9801
- await fs15.mkdir(path16.dirname(plistPath()), { recursive: true });
9802
- await fs15.mkdir(def.logDir, { recursive: true });
9803
- 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");
9804
11357
  }
9805
11358
  async function install(opts = {}) {
9806
11359
  await writePlist(opts.program ?? def.program);
@@ -9811,7 +11364,7 @@ function createLaunchdLifecycle(def, deps = {}) {
9811
11364
  }
9812
11365
  async function uninstall() {
9813
11366
  await runIdempotent(run, "launchctl", ["bootout", serviceTarget()], "launchctl bootout", LAUNCHD_NOT_LOADED);
9814
- await fs15.rm(plistPath(), { force: true });
11367
+ await fs17.rm(plistPath(), { force: true });
9815
11368
  }
9816
11369
  async function start() {
9817
11370
  if (!await fileExists(plistPath())) {
@@ -9870,10 +11423,10 @@ function generateSystemdUnit(def) {
9870
11423
  const { name, displayName, program, logDir } = def;
9871
11424
  assertNoControlChars(name, "name");
9872
11425
  assertNoControlChars(displayName, "displayName");
9873
- const cwd = program.cwd ?? os6.homedir();
11426
+ const cwd = program.cwd ?? os5.homedir();
9874
11427
  assertNoControlChars(cwd, "program.cwd");
9875
- const outLog = path16.join(logDir, `${name}.out.log`);
9876
- const errLog = path16.join(logDir, `${name}.err.log`);
11428
+ const outLog = path17.join(logDir, `${name}.out.log`);
11429
+ const errLog = path17.join(logDir, `${name}.err.log`);
9877
11430
  assertNoControlChars(outLog, "logDir");
9878
11431
  assertNoControlChars(errLog, "logDir");
9879
11432
  const execStart = [program.command, ...program.args].map(quoteSystemdArg).join(" ");
@@ -9895,14 +11448,14 @@ WantedBy=default.target
9895
11448
  }
9896
11449
  function createSystemdLifecycle(def, deps = {}) {
9897
11450
  const run = deps.run ?? defaultRunner;
9898
- const fs15 = deps.fs ?? promises;
9899
- const homedir = deps.homedir ?? (() => os6.homedir());
11451
+ const fs17 = deps.fs ?? promises;
11452
+ const homedir = deps.homedir ?? (() => os5.homedir());
9900
11453
  const name = sanitizeServiceName(def.name);
9901
11454
  const unitName = `${name}.service`;
9902
- const unitPath = () => path16.join(homedir(), ".config", "systemd", "user", unitName);
11455
+ const unitPath = () => path17.join(homedir(), ".config", "systemd", "user", unitName);
9903
11456
  async function fileExists(p) {
9904
11457
  try {
9905
- await fs15.stat(p);
11458
+ await fs17.stat(p);
9906
11459
  return true;
9907
11460
  } catch {
9908
11461
  return false;
@@ -9910,9 +11463,9 @@ function createSystemdLifecycle(def, deps = {}) {
9910
11463
  }
9911
11464
  async function writeUnit(program) {
9912
11465
  const unit = generateSystemdUnit({ name, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
9913
- await fs15.mkdir(path16.dirname(unitPath()), { recursive: true });
9914
- await fs15.mkdir(def.logDir, { recursive: true });
9915
- 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");
9916
11469
  }
9917
11470
  async function install(opts = {}) {
9918
11471
  await writeUnit(opts.program ?? def.program);
@@ -9921,7 +11474,7 @@ function createSystemdLifecycle(def, deps = {}) {
9921
11474
  }
9922
11475
  async function uninstall() {
9923
11476
  await runIdempotent(run, "systemctl", ["--user", "disable", "--now", unitName], "systemctl disable --now", SYSTEMD_NOT_LOADED);
9924
- await fs15.rm(unitPath(), { force: true });
11477
+ await fs17.rm(unitPath(), { force: true });
9925
11478
  await run("systemctl", ["--user", "daemon-reload"]);
9926
11479
  }
9927
11480
  async function start() {
@@ -9984,7 +11537,7 @@ ${argXml}${cwdXml}
9984
11537
  }
9985
11538
  function createWinswLifecycle(def, deps = {}) {
9986
11539
  const run = deps.run ?? defaultRunner;
9987
- const fs15 = deps.fs ?? promises;
11540
+ const fs17 = deps.fs ?? promises;
9988
11541
  const windows = def.windows;
9989
11542
  if (!windows) {
9990
11543
  throw new Error("WinSW service lifecycle requires `ServiceDefinition.windows.winswBin` (the product-bundled WinSW executable path)");
@@ -9992,11 +11545,11 @@ function createWinswLifecycle(def, deps = {}) {
9992
11545
  const winswBin = windows.winswBin;
9993
11546
  const id = sanitizeServiceName(def.name);
9994
11547
  const installDir = windows.installDir ?? def.logDir;
9995
- const exePath = path16.join(installDir, `${id}.exe`);
9996
- const xmlPath = path16.join(installDir, `${id}.xml`);
11548
+ const exePath = path17.join(installDir, `${id}.exe`);
11549
+ const xmlPath = path17.join(installDir, `${id}.xml`);
9997
11550
  async function fileExists(p) {
9998
11551
  try {
9999
- await fs15.stat(p);
11552
+ await fs17.stat(p);
10000
11553
  return true;
10001
11554
  } catch {
10002
11555
  return false;
@@ -10004,10 +11557,10 @@ function createWinswLifecycle(def, deps = {}) {
10004
11557
  }
10005
11558
  async function writeFiles(program) {
10006
11559
  const xml = generateWinswXml({ id, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
10007
- await fs15.mkdir(installDir, { recursive: true });
10008
- await fs15.mkdir(def.logDir, { recursive: true });
10009
- await fs15.copyFile(winswBin, exePath);
10010
- 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");
10011
11564
  }
10012
11565
  async function install(opts = {}) {
10013
11566
  await writeFiles(opts.program ?? def.program);
@@ -10017,8 +11570,8 @@ function createWinswLifecycle(def, deps = {}) {
10017
11570
  async function uninstall() {
10018
11571
  await runIdempotent(run, exePath, ["stop"], "winsw stop", WINSW_NOT_INSTALLED);
10019
11572
  await runIdempotent(run, exePath, ["uninstall"], "winsw uninstall", WINSW_NOT_INSTALLED);
10020
- await fs15.rm(exePath, { force: true });
10021
- await fs15.rm(xmlPath, { force: true });
11573
+ await fs17.rm(exePath, { force: true });
11574
+ await fs17.rm(xmlPath, { force: true });
10022
11575
  }
10023
11576
  async function start() {
10024
11577
  if (!await fileExists(xmlPath)) {
@@ -10061,6 +11614,6 @@ function createServiceLifecycle(def, opts = {}) {
10061
11614
  }
10062
11615
  }
10063
11616
 
10064
- 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 };
10065
11618
  //# sourceMappingURL=index.js.map
10066
11619
  //# sourceMappingURL=index.js.map