@byok-sdk/server 0.2.0 → 0.4.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,18 +1,18 @@
1
1
  import { randomBytes, randomUUID, timingSafeEqual, createHmac, createHash, createPublicKey, verify } from 'crypto';
2
+ import { nonceSigningBytes } from '@byok-sdk/core';
2
3
  import { jwtVerify, SignJWT } from 'jose';
3
4
  import { mkdir, writeFile, readFile } from 'fs/promises';
4
5
  import { mkdtempSync, mkdirSync, existsSync, chmodSync } from 'fs';
5
6
  import { tmpdir } from 'os';
6
7
  import path, { dirname } from 'path';
8
+ import { CAPABILITY_FLAGS, byokBlobContentPath, canTransition, PROTOCOL_VERSION, encodeEnvelope, DAEMON_TO_SERVER_TYPES, DispatchSelectionSchema, RequiredToolsetsSchema, createEnvelope, TASK_STATES, BYOK_PAIR_PATH, PairRequestSchema, BYOK_CHALLENGE_PATH, ChallengeRequestSchema, BYOK_TOKEN_PATH, TokenRequestSchema, BYOK_BLOBS_PATH, CreateBlobRequestSchema, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendRequestSchema, decodeEnvelope, BYOK_WS_PATH } from '@byok-sdk/protocol';
7
9
  import { Hono } from 'hono';
8
- import { CAPABILITY_FLAGS, canTransition, PROTOCOL_VERSION, encodeEnvelope, DAEMON_TO_SERVER_TYPES, createEnvelope, TASK_STATES, PairRequestSchema, ChallengeRequestSchema, TokenRequestSchema, CreateBlobRequestSchema, MessagesSendRequestSchema, decodeEnvelope } from '@byok-sdk/protocol';
9
10
  import { WebSocketServer } from 'ws';
10
11
  import { createRequire } from 'module';
11
12
 
12
13
  // src/auth.ts
13
14
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
14
15
  var NONCE_TTL_MS = 5 * 60 * 1e3;
15
- var NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
16
16
  function createHmacTokenSigner(secret = randomBytes(32)) {
17
17
  return {
18
18
  async sign(claims, expiresInSeconds) {
@@ -143,13 +143,13 @@ function verifyEd25519Signature(devicePublicKey, message, signature) {
143
143
  key: { kty: "OKP", crv: "Ed25519", x: devicePublicKey },
144
144
  format: "jwk"
145
145
  });
146
- return verify(null, Buffer.from(message, "utf8"), keyObject, Buffer.from(signature, "base64url"));
146
+ return verify(null, message, keyObject, Buffer.from(signature, "base64url"));
147
147
  } catch {
148
148
  return false;
149
149
  }
150
150
  }
151
151
  function verifyNonceSignature(devicePublicKey, nonce, signature) {
152
- return verifyEd25519Signature(devicePublicKey, NONCE_SIGNING_DOMAIN + nonce, signature);
152
+ return verifyEd25519Signature(devicePublicKey, nonceSigningBytes(nonce), signature);
153
153
  }
154
154
  function extractBearerToken(header) {
155
155
  if (!header) return void 0;
@@ -164,6 +164,7 @@ async function authenticateBearer(header, deps) {
164
164
  const device = deps.devices.get(claims.tenantId, claims.deviceId);
165
165
  if (!device || device.revoked) return void 0;
166
166
  if (device.productId !== claims.productId) return void 0;
167
+ if (device.productId !== deps.productId) return void 0;
167
168
  return { deviceId: device.deviceId, tenantId: device.tenantId, productId: device.productId };
168
169
  }
169
170
  var BlobDeclarationConflictError = class extends Error {
@@ -247,7 +248,7 @@ var LocalDiskBlobStore = class {
247
248
  signUrl(blobId, action) {
248
249
  const exp = Date.now() + this.urlTtlMs;
249
250
  const sig = this.computeSig(blobId, action, exp);
250
- return `/byok/blobs/${blobId}/content?sig=${sig}&exp=${exp}`;
251
+ return `${byokBlobContentPath(blobId)}?sig=${sig}&exp=${exp}`;
251
252
  }
252
253
  };
253
254
  var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
@@ -342,7 +343,7 @@ function buildHonoApp(deps) {
342
343
  if (deps.healthzRoute) {
343
344
  app.get("/healthz", (c) => c.json({ ok: true, uptimeMs: Date.now() - serverStartedAtMs }, 200));
344
345
  }
345
- app.post("/byok/pair", async (c) => {
346
+ app.post(BYOK_PAIR_PATH, async (c) => {
346
347
  const parsed = PairRequestSchema.safeParse(await readJsonBody(c));
347
348
  if (!parsed.success) {
348
349
  return c.json({ error: "pairingCode, deviceName, and devicePublicKey are required strings" }, 400);
@@ -373,7 +374,7 @@ function buildHonoApp(deps) {
373
374
  const response = { deviceId, accessToken, refreshHint: expiresAt };
374
375
  return c.json(response, 200);
375
376
  });
376
- app.post("/byok/challenge", async (c) => {
377
+ app.post(BYOK_CHALLENGE_PATH, async (c) => {
377
378
  const parsed = ChallengeRequestSchema.safeParse(await readJsonBody(c));
378
379
  if (!parsed.success) return c.json({ error: "deviceId is required" }, 400);
379
380
  const { deviceId } = parsed.data;
@@ -385,7 +386,7 @@ function buildHonoApp(deps) {
385
386
  const response = { nonce };
386
387
  return c.json(response, 200);
387
388
  });
388
- app.post("/byok/token", async (c) => {
389
+ app.post(BYOK_TOKEN_PATH, async (c) => {
389
390
  const parsed = TokenRequestSchema.safeParse(await readJsonBody(c));
390
391
  if (!parsed.success) return c.json({ error: "deviceId, nonce, and signature are required" }, 400);
391
392
  const { deviceId, nonce, signature } = parsed.data;
@@ -408,7 +409,7 @@ function buildHonoApp(deps) {
408
409
  const response = { accessToken, expiresAt };
409
410
  return c.json(response, 200);
410
411
  });
411
- app.post("/byok/blobs", async (c) => {
412
+ app.post(BYOK_BLOBS_PATH, async (c) => {
412
413
  const principal = await authenticateBearer(c.req.header("authorization"), deps);
413
414
  if (!principal) return c.json({ error: "unauthorized" }, 401);
414
415
  const parsed = CreateBlobRequestSchema.safeParse(await readJsonBody(c));
@@ -432,7 +433,7 @@ function buildHonoApp(deps) {
432
433
  throw error;
433
434
  }
434
435
  });
435
- app.post("/byok/blobs/:id/finalize", async (c) => {
436
+ app.post(BYOK_BLOB_FINALIZE_ROUTE, async (c) => {
436
437
  const principal = await authenticateBearer(c.req.header("authorization"), deps);
437
438
  if (!principal) return c.json({ error: "unauthorized" }, 401);
438
439
  const reservationId = c.req.header("idempotency-key");
@@ -448,7 +449,7 @@ function buildHonoApp(deps) {
448
449
  }
449
450
  return c.body(null, 204);
450
451
  });
451
- app.get("/byok/blobs/:id/url", async (c) => {
452
+ app.get(BYOK_BLOB_URL_ROUTE, async (c) => {
452
453
  const principal = await authenticateBearer(c.req.header("authorization"), deps);
453
454
  if (!principal) return c.json({ error: "unauthorized" }, 401);
454
455
  const downloadUrl = await deps.blobStore.getDownloadUrl(c.req.param("id"));
@@ -456,7 +457,7 @@ function buildHonoApp(deps) {
456
457
  const response = { downloadUrl };
457
458
  return c.json(response, 200);
458
459
  });
459
- app.put("/byok/blobs/:id/content", async (c) => {
460
+ app.put(BYOK_BLOB_CONTENT_ROUTE, async (c) => {
460
461
  const blobId = c.req.param("id");
461
462
  const { sig, exp } = signedUrlParams(c.req.query("sig"), c.req.query("exp"));
462
463
  if (!sig || exp === void 0 || !deps.blobStore.verifySignedUrl(blobId, "put", sig, exp)) {
@@ -467,7 +468,7 @@ function buildHonoApp(deps) {
467
468
  if (!result.ok) return c.json({ error: result.reason }, 422);
468
469
  return c.body(null, 204);
469
470
  });
470
- app.get("/byok/blobs/:id/content", async (c) => {
471
+ app.get(BYOK_BLOB_CONTENT_ROUTE, async (c) => {
471
472
  const blobId = c.req.param("id");
472
473
  const { sig, exp } = signedUrlParams(c.req.query("sig"), c.req.query("exp"));
473
474
  if (!sig || exp === void 0 || !deps.blobStore.verifySignedUrl(blobId, "get", sig, exp)) {
@@ -477,7 +478,7 @@ function buildHonoApp(deps) {
477
478
  if (!content) return c.json({ error: "blob not found" }, 404);
478
479
  return c.body(new Uint8Array(content.data), 200, { "content-type": content.contentType });
479
480
  });
480
- app.get("/byok/events", async (c) => {
481
+ app.get(BYOK_EVENTS_PATH, async (c) => {
481
482
  const principal = await authenticateBearer(c.req.header("authorization"), deps);
482
483
  if (!principal) return c.json({ error: "unauthorized" }, 401);
483
484
  const cursorRaw = c.req.query("cursor");
@@ -491,7 +492,7 @@ function buildHonoApp(deps) {
491
492
  const response = result;
492
493
  return c.json(response, 200);
493
494
  });
494
- app.post("/byok/messages", async (c) => {
495
+ app.post(BYOK_MESSAGES_PATH, async (c) => {
495
496
  const principal = await authenticateBearer(c.req.header("authorization"), deps);
496
497
  if (!principal) return c.json({ error: "unauthorized" }, 401);
497
498
  const parsed = MessagesSendRequestSchema.safeParse(await readJsonBody(c));
@@ -843,9 +844,16 @@ var ConnectionHub = class {
843
844
  * connection this hub never learns capabilities for simply reads back
844
845
  * `undefined` from {@link getDeviceCapabilities}.
845
846
  */
846
- registerConnection(deviceId, ws, runtimes, capabilities) {
847
+ registerConnection(deviceId, ws, runtimes, capabilities, configuredToolsets) {
847
848
  const at = (/* @__PURE__ */ new Date()).toISOString();
848
- this.connections.set(deviceId, { ws, connected: true, lastSeen: at, runtimes, capabilities });
849
+ this.connections.set(deviceId, {
850
+ ws,
851
+ connected: true,
852
+ lastSeen: at,
853
+ runtimes,
854
+ capabilities,
855
+ configuredToolsets
856
+ });
849
857
  this.serverEvents.push({ kind: "device.connected", deviceId, at });
850
858
  this.settleLongPollWaiter(deviceId);
851
859
  }
@@ -940,7 +948,13 @@ var ConnectionHub = class {
940
948
  const wasFreshlyConnected = !conn || conn.ws !== void 0 || !conn.connected;
941
949
  if (conn?.ws) {
942
950
  const ws = conn.ws;
943
- this.connections.set(deviceId, { connected: true, lastSeen: at, runtimes: conn.runtimes, capabilities: conn.capabilities });
951
+ this.connections.set(deviceId, {
952
+ connected: true,
953
+ lastSeen: at,
954
+ runtimes: conn.runtimes,
955
+ capabilities: conn.capabilities,
956
+ configuredToolsets: conn.configuredToolsets
957
+ });
944
958
  ws.close(1e3, "superseded by long-poll connection");
945
959
  } else if (!conn) {
946
960
  this.connections.set(deviceId, { connected: true, lastSeen: at });
@@ -1274,7 +1288,17 @@ var ConnectionHub = class {
1274
1288
  state: "Complete",
1275
1289
  summary: payload.summary,
1276
1290
  sessionRef: payload.sessionRef,
1277
- artifactRefs: payload.artifactRefs
1291
+ artifactRefs: payload.artifactRefs,
1292
+ // additive-minor (`task.complete.document`): projected verbatim, the
1293
+ // same way `summary`/`artifactRefs` are. Nothing to validate or
1294
+ // measure here — the payload only got this far because
1295
+ // `TaskCompletePayloadSchema`'s own refinement already enforced
1296
+ // JSON-serializability and `RESULT_DOCUMENT_MAX_BYTES` at the inbound
1297
+ // boundary, and re-checking would make this a second authority for a
1298
+ // rule the wire already owns. Stays `undefined` for the two cases that
1299
+ // never carry one: a daemon with no extractor configured, and a
1300
+ // pre-`result-document` daemon build.
1301
+ document: payload.document
1278
1302
  };
1279
1303
  this.applyOrFail(taskId, "Complete", { result, sessionRef: payload.sessionRef });
1280
1304
  }
@@ -1658,19 +1682,53 @@ var ConnectionHub = class {
1658
1682
  // dispatch() and the TaskHandle it returns
1659
1683
  // ---------------------------------------------------------------------
1660
1684
  async dispatch(input) {
1661
- const deviceId = input.deviceId ?? this.pickFirstConnectedDevice();
1685
+ const dispatchSelection = input.dispatchSelection === void 0 ? void 0 : DispatchSelectionSchema.parse(input.dispatchSelection);
1686
+ const requiredToolsets = input.requiredToolsets === void 0 ? void 0 : RequiredToolsetsSchema.parse(input.requiredToolsets);
1687
+ const deviceId = input.deviceId ?? this.pickFirstConnectedDevice(requiredToolsets);
1662
1688
  if (!deviceId || !this.connections.get(deviceId)?.connected) {
1663
1689
  throw new Error(
1664
1690
  deviceId ? `device ${deviceId} is not connected` : "no connected device to dispatch to (M0 does not queue tasks until a device connects)"
1665
1691
  );
1666
1692
  }
1693
+ if (dispatchSelection !== void 0 && !(this.getDeviceCapabilities(deviceId)?.includes("dispatch-selection") ?? false)) {
1694
+ throw new Error(
1695
+ `device ${deviceId} did not advertise dispatch-selection capability; refusing authoritative provider/model dispatch`
1696
+ );
1697
+ }
1698
+ if (requiredToolsets !== void 0 && !(this.getDeviceCapabilities(deviceId)?.includes("toolset-selection") ?? false)) {
1699
+ throw new Error(
1700
+ `device ${deviceId} did not advertise toolset-selection capability; refusing a task whose semantics require local MCP tools`
1701
+ );
1702
+ }
1703
+ if (requiredToolsets !== void 0) {
1704
+ const configuredToolsets = this.connections.get(deviceId)?.configuredToolsets;
1705
+ if (configuredToolsets === void 0) {
1706
+ throw new Error(
1707
+ `device ${deviceId} did not advertise its configured toolset inventory; refusing to guess from runtime capability`
1708
+ );
1709
+ }
1710
+ const configured = new Set(configuredToolsets);
1711
+ const missing = requiredToolsets.filter((toolsetId) => !configured.has(toolsetId));
1712
+ if (missing.length > 0) {
1713
+ throw new Error(
1714
+ `device ${deviceId} is missing required MCP toolset(s): ${missing.join(", ")}`
1715
+ );
1716
+ }
1717
+ }
1718
+ if (dispatchSelection !== void 0 && input.runtime !== void 0 && input.runtime !== dispatchSelection.runtimeId) {
1719
+ throw new Error(
1720
+ `dispatch runtime ${input.runtime} does not match dispatchSelection.runtimeId ${dispatchSelection.runtimeId}`
1721
+ );
1722
+ }
1667
1723
  const taskId = generateTaskId();
1668
1724
  const policy = input.policy ?? DEFAULT_POLICY;
1725
+ const runtime = dispatchSelection?.runtimeId ?? input.runtime;
1669
1726
  const record = this.taskStore.create({
1670
1727
  taskId,
1671
1728
  instruction: input.instruction,
1672
- runtime: input.runtime,
1729
+ runtime,
1673
1730
  policy,
1731
+ requiredToolsets,
1674
1732
  deviceId,
1675
1733
  sessionRef: input.sessionRef
1676
1734
  });
@@ -1682,17 +1740,23 @@ var ConnectionHub = class {
1682
1740
  this.runtimes.set(taskId, { queue, resolveResult, result });
1683
1741
  queue.push({ kind: "state", state: record.state, at: record.createdAt });
1684
1742
  this.serverEvents.push({ kind: "task.created", taskId, at: record.createdAt });
1685
- this.sendToDevice(
1686
- deviceId,
1687
- "task.offer",
1688
- {
1689
- instruction: input.instruction,
1690
- policy,
1691
- runtime: input.runtime,
1692
- sessionRef: input.sessionRef
1693
- },
1694
- { taskId, sessionRef: input.sessionRef }
1695
- );
1743
+ const commonOffer = {
1744
+ instruction: input.instruction,
1745
+ policy,
1746
+ runtime,
1747
+ dispatchSelection,
1748
+ sessionRef: input.sessionRef
1749
+ };
1750
+ if (requiredToolsets === void 0) {
1751
+ this.sendToDevice(deviceId, "task.offer", commonOffer, { taskId, sessionRef: input.sessionRef });
1752
+ } else {
1753
+ this.sendToDevice(
1754
+ deviceId,
1755
+ "task.offer_with_toolsets",
1756
+ { ...commonOffer, requiredToolsets },
1757
+ { taskId, sessionRef: input.sessionRef }
1758
+ );
1759
+ }
1696
1760
  return this.buildTaskHandle(taskId);
1697
1761
  }
1698
1762
  buildTaskHandle(taskId) {
@@ -1843,9 +1907,14 @@ var ConnectionHub = class {
1843
1907
  }
1844
1908
  this.sendToDevice(record.deviceId, "task.steer", { text }, { taskId });
1845
1909
  }
1846
- pickFirstConnectedDevice() {
1910
+ pickFirstConnectedDevice(requiredToolsets) {
1847
1911
  for (const [deviceId, conn] of this.connections) {
1848
- if (conn.connected) return deviceId;
1912
+ if (!conn.connected) continue;
1913
+ if (requiredToolsets === void 0) return deviceId;
1914
+ if (!conn.capabilities?.includes("toolset-selection")) continue;
1915
+ if (conn.configuredToolsets === void 0) continue;
1916
+ const configured = new Set(conn.configuredToolsets);
1917
+ if (requiredToolsets.every((toolsetId) => configured.has(toolsetId))) return deviceId;
1849
1918
  }
1850
1919
  return void 0;
1851
1920
  }
@@ -1928,7 +1997,8 @@ var ConnectionHub = class {
1928
1997
  deviceName,
1929
1998
  connected: conn?.connected ?? false,
1930
1999
  lastSeen: conn?.lastSeen,
1931
- runtimes: conn?.runtimes
2000
+ runtimes: conn?.runtimes,
2001
+ configuredToolsets: conn?.configuredToolsets ? [...conn.configuredToolsets] : void 0
1932
2002
  };
1933
2003
  });
1934
2004
  }
@@ -2008,6 +2078,7 @@ var InMemoryTaskStore = class {
2008
2078
  instruction: input.instruction,
2009
2079
  runtime: input.runtime,
2010
2080
  policy: input.policy,
2081
+ requiredToolsets: input.requiredToolsets,
2011
2082
  deviceId: input.deviceId,
2012
2083
  sessionRef: input.sessionRef,
2013
2084
  createdAt: now,
@@ -2101,7 +2172,7 @@ function startHeartbeat(ws, opts = {}) {
2101
2172
  }
2102
2173
 
2103
2174
  // src/ws-server.ts
2104
- var WS_PATH = "/byok/ws";
2175
+ var WS_PATH = BYOK_WS_PATH;
2105
2176
  var SUPPORTED_CAPABILITIES = [...CAPABILITY_FLAGS];
2106
2177
  function matchesWsPath(url) {
2107
2178
  return url.split("?")[0] === WS_PATH;
@@ -2175,7 +2246,13 @@ function handleConnection(ws, principal, deps) {
2175
2246
  return;
2176
2247
  }
2177
2248
  helloReceived = true;
2178
- deps.hub.registerConnection(deviceId, ws, payload.runtimes, payload.capabilities);
2249
+ deps.hub.registerConnection(
2250
+ deviceId,
2251
+ ws,
2252
+ payload.runtimes,
2253
+ payload.capabilities,
2254
+ payload.configuredToolsets
2255
+ );
2179
2256
  deps.hub.sendConnAck(deviceId, SUPPORTED_CAPABILITIES);
2180
2257
  if (payload.cursor !== void 0) {
2181
2258
  deps.hub.redeliverAfterReconnect(deviceId, payload.cursor);
@@ -2199,6 +2276,14 @@ function handleConnection(ws, principal, deps) {
2199
2276
  }
2200
2277
  });
2201
2278
  }
2279
+ function closeSqliteDatabaseAfterInitializationFailure(db, initializationError, message, close = (database) => database.close()) {
2280
+ try {
2281
+ close(db);
2282
+ } catch (closeError) {
2283
+ throw new AggregateError([initializationError, closeError], message);
2284
+ }
2285
+ throw initializationError;
2286
+ }
2202
2287
  var SqliteUnavailableError = class extends Error {
2203
2288
  constructor(cause) {
2204
2289
  super(
@@ -2238,16 +2323,27 @@ function loadSqliteModule() {
2238
2323
  var DEFAULT_BUSY_TIMEOUT_MS = 5e3;
2239
2324
  var SECURE_FILE_MODE = 384;
2240
2325
  var SECURE_DIR_MODE = 448;
2241
- function openSqliteDatabase(path2, options) {
2326
+ function openSqliteDatabase(path2, options, faults) {
2242
2327
  const { DatabaseSync } = loadSqliteModule();
2243
2328
  if (path2 !== ":memory:") {
2244
2329
  mkdirSync(dirname(path2), { recursive: true, mode: SECURE_DIR_MODE });
2245
2330
  }
2246
2331
  const db = new DatabaseSync(path2, { timeout: DEFAULT_BUSY_TIMEOUT_MS, ...options });
2247
- if (path2 !== ":memory:") {
2248
- db.exec("PRAGMA journal_mode = WAL;");
2332
+ try {
2333
+ faults?.onStep?.("after-open");
2334
+ if (path2 !== ":memory:") {
2335
+ db.exec("PRAGMA journal_mode = WAL;");
2336
+ faults?.onStep?.("after-wal");
2337
+ }
2338
+ return db;
2339
+ } catch (error) {
2340
+ closeSqliteDatabaseAfterInitializationFailure(
2341
+ db,
2342
+ error,
2343
+ "SQLite open initialization failed and its native handle could not be closed",
2344
+ faults?.close
2345
+ );
2249
2346
  }
2250
- return db;
2251
2347
  }
2252
2348
  function secureSqliteFilePermissions(dbPath) {
2253
2349
  if (dbPath === ":memory:") return;
@@ -2265,6 +2361,7 @@ CREATE TABLE IF NOT EXISTS tasks (
2265
2361
  state TEXT NOT NULL,
2266
2362
  instruction TEXT NOT NULL,
2267
2363
  runtime TEXT,
2364
+ required_toolsets_json TEXT,
2268
2365
  policy_json TEXT NOT NULL,
2269
2366
  device_id TEXT,
2270
2367
  session_ref TEXT,
@@ -2279,6 +2376,7 @@ CREATE TABLE IF NOT EXISTS tasks (
2279
2376
  var ADDITIVE_COLUMNS = [
2280
2377
  { name: "pending_approval_id", ddl: "ALTER TABLE tasks ADD COLUMN pending_approval_id TEXT" },
2281
2378
  { name: "claimed_runtime", ddl: "ALTER TABLE tasks ADD COLUMN claimed_runtime TEXT" },
2379
+ { name: "required_toolsets_json", ddl: "ALTER TABLE tasks ADD COLUMN required_toolsets_json TEXT" },
2282
2380
  // S0 (GAP-002): the claim-time `RuntimeCapabilities` snapshot, stored as
2283
2381
  // the JSON text of that closed, protocol-validated object (same idiom as
2284
2382
  // `policy_json`/`result_json` above — no column-per-flag, so a future
@@ -2311,12 +2409,14 @@ function ensureAdditiveColumns(db) {
2311
2409
  }
2312
2410
  function rowToRecord(row) {
2313
2411
  const resultJson = row.result_json;
2412
+ const requiredToolsetsJson = row.required_toolsets_json;
2314
2413
  const claimedRuntimeCapabilitiesJson = row.claimed_runtime_capabilities_json;
2315
2414
  return {
2316
2415
  taskId: row.task_id,
2317
2416
  state: row.state,
2318
2417
  instruction: row.instruction,
2319
2418
  runtime: row.runtime ?? void 0,
2419
+ requiredToolsets: requiredToolsetsJson ? JSON.parse(requiredToolsetsJson) : void 0,
2320
2420
  policy: JSON.parse(row.policy_json),
2321
2421
  deviceId: row.device_id ?? void 0,
2322
2422
  sessionRef: row.session_ref ?? void 0,
@@ -2335,28 +2435,37 @@ var SqliteTaskStore = class {
2335
2435
  selectStmt;
2336
2436
  selectAllStmt;
2337
2437
  updatePendingApprovalIdStmt;
2438
+ closed = false;
2338
2439
  constructor(opts) {
2339
2440
  this.db = openSqliteDatabase(opts.path);
2340
- this.db.exec(SCHEMA);
2341
- ensureAdditiveColumns(this.db);
2342
- secureSqliteFilePermissions(opts.path);
2343
- this.insertStmt = this.db.prepare(
2344
- `INSERT INTO tasks
2345
- (task_id, state, instruction, runtime, policy_json, device_id, session_ref, created_at, updated_at, result_json)
2346
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2347
- );
2348
- this.updateStmt = this.db.prepare(
2349
- `UPDATE tasks SET
2350
- state = ?, instruction = ?, runtime = ?, policy_json = ?, device_id = ?,
2351
- session_ref = ?, created_at = ?, updated_at = ?, result_json = ?, pending_approval_id = ?,
2352
- claimed_runtime = ?, claimed_runtime_capabilities_json = ?
2353
- WHERE task_id = ? AND state = ?`
2354
- );
2355
- this.selectStmt = this.db.prepare("SELECT * FROM tasks WHERE task_id = ?");
2356
- this.selectAllStmt = this.db.prepare("SELECT * FROM tasks ORDER BY rowid ASC");
2357
- this.updatePendingApprovalIdStmt = this.db.prepare(
2358
- "UPDATE tasks SET pending_approval_id = ?, updated_at = ? WHERE task_id = ? AND state = 'AwaitApproval'"
2359
- );
2441
+ try {
2442
+ this.db.exec(SCHEMA);
2443
+ ensureAdditiveColumns(this.db);
2444
+ secureSqliteFilePermissions(opts.path);
2445
+ this.insertStmt = this.db.prepare(
2446
+ `INSERT INTO tasks
2447
+ (task_id, state, instruction, runtime, required_toolsets_json, policy_json, device_id, session_ref, created_at, updated_at, result_json)
2448
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2449
+ );
2450
+ this.updateStmt = this.db.prepare(
2451
+ `UPDATE tasks SET
2452
+ state = ?, instruction = ?, runtime = ?, required_toolsets_json = ?, policy_json = ?, device_id = ?,
2453
+ session_ref = ?, created_at = ?, updated_at = ?, result_json = ?, pending_approval_id = ?,
2454
+ claimed_runtime = ?, claimed_runtime_capabilities_json = ?
2455
+ WHERE task_id = ? AND state = ?`
2456
+ );
2457
+ this.selectStmt = this.db.prepare("SELECT * FROM tasks WHERE task_id = ?");
2458
+ this.selectAllStmt = this.db.prepare("SELECT * FROM tasks ORDER BY rowid ASC");
2459
+ this.updatePendingApprovalIdStmt = this.db.prepare(
2460
+ "UPDATE tasks SET pending_approval_id = ?, updated_at = ? WHERE task_id = ? AND state = 'AwaitApproval'"
2461
+ );
2462
+ } catch (error) {
2463
+ closeSqliteDatabaseAfterInitializationFailure(
2464
+ this.db,
2465
+ error,
2466
+ "SqliteTaskStore initialization failed and its native handle could not be closed"
2467
+ );
2468
+ }
2360
2469
  }
2361
2470
  create(input) {
2362
2471
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -2365,6 +2474,7 @@ var SqliteTaskStore = class {
2365
2474
  state: "Offered",
2366
2475
  instruction: input.instruction,
2367
2476
  runtime: input.runtime,
2477
+ requiredToolsets: input.requiredToolsets,
2368
2478
  policy: input.policy,
2369
2479
  deviceId: input.deviceId,
2370
2480
  sessionRef: input.sessionRef,
@@ -2376,6 +2486,7 @@ var SqliteTaskStore = class {
2376
2486
  record.state,
2377
2487
  record.instruction,
2378
2488
  record.runtime ?? null,
2489
+ record.requiredToolsets ? JSON.stringify(record.requiredToolsets) : null,
2379
2490
  JSON.stringify(record.policy),
2380
2491
  record.deviceId ?? null,
2381
2492
  record.sessionRef ?? null,
@@ -2433,6 +2544,7 @@ var SqliteTaskStore = class {
2433
2544
  updated.state,
2434
2545
  updated.instruction,
2435
2546
  updated.runtime ?? null,
2547
+ updated.requiredToolsets ? JSON.stringify(updated.requiredToolsets) : null,
2436
2548
  JSON.stringify(updated.policy),
2437
2549
  updated.deviceId ?? null,
2438
2550
  updated.sessionRef ?? null,
@@ -2481,7 +2593,9 @@ var SqliteTaskStore = class {
2481
2593
  * instance against the same file, or on process shutdown.
2482
2594
  */
2483
2595
  close() {
2596
+ if (this.closed) return;
2484
2597
  this.db.close();
2598
+ this.closed = true;
2485
2599
  }
2486
2600
  };
2487
2601
  var SCHEMA2 = `
@@ -2530,21 +2644,30 @@ var SqliteBlobStore = class {
2530
2644
  selectBlobStmt;
2531
2645
  selectUploadedStmt;
2532
2646
  writeContentStmt;
2647
+ closed = false;
2533
2648
  constructor(opts) {
2534
2649
  this.db = openSqliteDatabase(opts.path);
2535
- this.db.exec(SCHEMA2);
2536
- secureSqliteFilePermissions(opts.path);
2537
- this.urlTtlMs = opts.urlTtlMs ?? DEFAULT_URL_TTL_MS2;
2538
- this.secret = opts.signingKey ?? loadOrCreateSigningSecret(this.db);
2539
- this.insertBlobStmt = this.db.prepare(
2540
- `INSERT INTO blobs (blob_id, size, content_type, content_hash, uploaded, data)
2541
- VALUES (?, ?, ?, ?, 0, NULL)`
2542
- );
2543
- this.selectBlobStmt = this.db.prepare(
2544
- "SELECT size, content_type, content_hash, uploaded, data FROM blobs WHERE blob_id = ?"
2545
- );
2546
- this.selectUploadedStmt = this.db.prepare("SELECT uploaded FROM blobs WHERE blob_id = ?");
2547
- this.writeContentStmt = this.db.prepare("UPDATE blobs SET data = ?, uploaded = 1 WHERE blob_id = ?");
2650
+ try {
2651
+ this.db.exec(SCHEMA2);
2652
+ secureSqliteFilePermissions(opts.path);
2653
+ this.urlTtlMs = opts.urlTtlMs ?? DEFAULT_URL_TTL_MS2;
2654
+ this.secret = opts.signingKey ?? loadOrCreateSigningSecret(this.db);
2655
+ this.insertBlobStmt = this.db.prepare(
2656
+ `INSERT INTO blobs (blob_id, size, content_type, content_hash, uploaded, data)
2657
+ VALUES (?, ?, ?, ?, 0, NULL)`
2658
+ );
2659
+ this.selectBlobStmt = this.db.prepare(
2660
+ "SELECT size, content_type, content_hash, uploaded, data FROM blobs WHERE blob_id = ?"
2661
+ );
2662
+ this.selectUploadedStmt = this.db.prepare("SELECT uploaded FROM blobs WHERE blob_id = ?");
2663
+ this.writeContentStmt = this.db.prepare("UPDATE blobs SET data = ?, uploaded = 1 WHERE blob_id = ?");
2664
+ } catch (error) {
2665
+ closeSqliteDatabaseAfterInitializationFailure(
2666
+ this.db,
2667
+ error,
2668
+ "SqliteBlobStore initialization failed and its native handle could not be closed"
2669
+ );
2670
+ }
2548
2671
  }
2549
2672
  async createUpload(input, requestedBlobId) {
2550
2673
  const blobId = requestedBlobId ?? `blob_${randomUUID()}`;
@@ -2596,7 +2719,9 @@ var SqliteBlobStore = class {
2596
2719
  }
2597
2720
  /** Close the underlying database connection — see `SqliteTaskStore.close`'s doc comment; same rationale. */
2598
2721
  close() {
2722
+ if (this.closed) return;
2599
2723
  this.db.close();
2724
+ this.closed = true;
2600
2725
  }
2601
2726
  computeSig(blobId, action, exp) {
2602
2727
  return createHmac("sha256", this.secret).update(`${blobId}:${action}:${exp}`).digest("base64url");
@@ -2604,7 +2729,7 @@ var SqliteBlobStore = class {
2604
2729
  signUrl(blobId, action) {
2605
2730
  const exp = Date.now() + this.urlTtlMs;
2606
2731
  const sig = this.computeSig(blobId, action, exp);
2607
- return `/byok/blobs/${blobId}/content?sig=${sig}&exp=${exp}`;
2732
+ return `${byokBlobContentPath(blobId)}?sig=${sig}&exp=${exp}`;
2608
2733
  }
2609
2734
  };
2610
2735
 
@@ -2629,6 +2754,11 @@ function createByokServer(opts) {
2629
2754
  devices,
2630
2755
  nonces,
2631
2756
  tokenSigner,
2757
+ // S1: the product this instance serves is part of `authenticateBearer`'s
2758
+ // decision (`auth.ts`), not just of the WS hello gate — so every
2759
+ // bearer-authed route gets the same instance-equality guarantee the WS
2760
+ // upgrade already had transitively.
2761
+ productId: opts.productId,
2632
2762
  blobStore,
2633
2763
  maxBlobSizeBytes,
2634
2764
  longPollHoldMs,