@byok-sdk/server 0.23.0 → 0.25.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,6 @@ const task = await server.dispatch({
15
15
  deviceId,
16
16
  instruction: 'Find five qualified prospects and draft follow-ups.',
17
17
  runtime: 'claude',
18
- policy: { mode: 'auto' },
19
18
  requiredToolsets: ['salesko.prospecting'],
20
19
  });
21
20
  ```
@@ -25,7 +24,7 @@ live device advertises `toolset-selection` and its `configuredToolsets`
25
24
  inventory contains every required ID. `machines.list()` projects that same
26
25
  logical-ID-only inventory; MCP commands and credentials remain device-local.
27
26
 
28
- MIT licensed. Node.js 22.22.0 or newer.
27
+ MIT licensed. Node.js 24.15.0 or newer.
29
28
 
30
29
  ## SQLite enrollment, receipts and schema adoption
31
30
 
@@ -41,37 +40,57 @@ Unredeemed pairing codes, presence and other unmodified ports remain ephemeral.
41
40
  Keep daemon OS credentials, server URL and token signer stable across restart.
42
41
  This does not restore TaskHandle promises, subscriptions or provider processes.
43
42
 
44
- This candidate writes schema v3. Stop **all** writers and take a consistent
43
+ This candidate writes schema v4, including the write-once `claimedHarnessId`
44
+ stored atomically with task ownership. Stop **all** writers and take a consistent
45
45
  SQLite backup (including WAL state or using SQLite's backup facility) before
46
- explicit adoption. An already-open old writer is not stopped by the version
47
- fence. Only legacy databases with no task, mailbox-message, agent-admission or
48
- advanced cursor history can migrate: old in-memory receipts cannot be recovered
49
- from task status or fabricated from a retry payload. A pristine poll cursor is
50
- allowed. Preserve rejected databases for separate reconciliation; do not delete
51
- history, reset cursors or edit version markers to make adoption pass.
46
+ explicit adoption. An already-open old writer is not stopped by the version fence.
47
+
48
+ - `v1-to-v4` and `v2-to-v4` require no task, mailbox-message, agent-admission or
49
+ advanced cursor history: old in-memory receipts cannot be recovered from task
50
+ status or fabricated from a retry payload. A pristine poll cursor is allowed.
51
+ - `v3-to-v4` preserves receipts, unclaimed offers and claims with an identified
52
+ built-in runtime. It rejects any owned task with no `claimed_runtime`, including
53
+ terminal tasks: a lost custom-harness identity cannot be distinguished from an
54
+ identity-free legacy built-in claim. Neither the requested offer nor current
55
+ device inventory proves the actual claiming adapter. Existing rows receive a
56
+ null harness identity; migration never invents one.
57
+
58
+ Preserve rejected databases for separate reconciliation; do not delete history,
59
+ reset cursors or edit version markers to make adoption pass.
60
+
61
+ Every refusal at open is a `SqliteSchemaError` with `foundVersion` and
62
+ `requiredVersion`, so a host can check the file at startup and branch on `code`:
63
+
64
+ - `SQLITE_SCHEMA_UNSUPPORTED`: the file needs an explicit `migration` selector
65
+ (older version) or a newer build (newer version).
66
+ - `SQLITE_MIGRATION_REFUSED`: the selected migration would lose history;
67
+ preserve the file.
68
+ - `SQLITE_SCHEMA_INVALID`: the file contradicts its declared schema or is not a
69
+ BYOK database.
52
70
 
53
71
  ```ts
54
72
  const server = createByokServer({
55
73
  productId,
56
74
  tokenSigner,
57
- storage: { kind: 'sqlite', path, migration: 'v2-to-v3' },
75
+ storage: { kind: 'sqlite', path, migration: 'v3-to-v4' },
58
76
  });
59
77
  await server.close();
60
78
  ```
61
79
 
62
- Use `v1-to-v3` for a v1 database meeting the same eligibility rules. Remove
63
- `migration` from normal startup. Adoption adds receipts, adds an empty device
64
- directory for v1, and updates the marker atomically; any failure rolls back.
65
- Existing v2 devices and artifacts remain. V1 enrollment was in-memory and needs
66
- explicit pairing. Unknown versions, missing authorities and malformed receipt
67
- columns/primary key fail closed. Broader validation of legacy table constraints
68
- is a separate tracked concern.
69
-
70
- **Breaking storage boundary:** published 0.16 (schema v1) and earlier schema-v2
71
- candidate writers refuse v3. The previous unpublished `v1-to-v2` selector is
72
- replaced, without a compatibility alias. Restore of a backup requires a separate
73
- recovery procedure because it can lose new state or restore old grants. This
74
- belongs to the next MINOR release; these local changes are not a published upgrade.
80
+ Remove `migration` from normal startup. Adoption adds the nullable harness column,
81
+ adds receipts for v1/v2 and an empty device directory for v1, and updates the marker
82
+ atomically; any failure rolls back. Existing v2/v3 devices and artifacts remain.
83
+ V1 enrollment was in-memory and needs explicit pairing. Unknown versions, missing
84
+ authorities and malformed receipt columns/primary key fail closed. Broader
85
+ validation of legacy table constraints is a separate tracked concern.
86
+
87
+ **Breaking storage boundary:** schema-v1/v2/v3 writers refuse v4. The public
88
+ `v1-to-v3` / `v2-to-v3` selectors are replaced by the corresponding target-v4
89
+ selectors, with no alias or silent reinterpretation: callers must update their
90
+ one-shot migration configuration after checking eligibility and backing up.
91
+ Restore of a backup requires a separate recovery procedure because it can lose
92
+ new state or restore old grants. This belongs to the next MINOR release; these
93
+ local changes are not a published upgrade.
75
94
 
76
95
 
77
96
  ## Persistent host request identity
package/dist/index.d.ts CHANGED
@@ -35,7 +35,8 @@ export type { AccessTokenClaims, DeviceRecord, PairingCodeInfo, TenantId, TokenS
35
35
  export { createHmacTokenSigner } from '@byok-sdk/cloud';
36
36
  /** Cutoffs and result of {@link ByokServer.mailbox.collectRetired}, owned by `@byok-sdk/core`. */
37
37
  export type { MailboxRetentionInput, MailboxRetentionResult } from '@byok-sdk/core';
38
- export { SqliteUnavailableError } from './sqlite-support';
38
+ export { SqliteSchemaError, SqliteUnavailableError } from './sqlite-support';
39
+ export type { SqliteSchemaErrorCode } from './sqlite-support';
39
40
  export type { RateLimiterOptions } from './rate-limiter';
40
41
  export { DEFAULT_TASK_EVENT_BUFFER_LIMIT, DEFAULT_TASK_EVENT_RETENTION_MS } from './relay';
41
42
  /** Page size `tasks.list()` uses when the caller names none. */
package/dist/index.js CHANGED
@@ -231,7 +231,9 @@ var TaskEventRelay = class {
231
231
  }
232
232
  /** The per-task feed backing `TaskHandle.events()`, replayed from the start of what is retained. */
233
233
  events(taskId) {
234
- return this.#open(taskId).queue.subscribe();
234
+ const state = this.#tasks.get(taskId);
235
+ return state === void 0 ? { async *[Symbol.asyncIterator]() {
236
+ } } : state.queue.subscribe();
235
237
  }
236
238
  /** Settles when this task first reaches a terminal — the barrier `TaskHandle.result()` awaits. */
237
239
  terminal(taskId) {
@@ -652,6 +654,20 @@ var SqliteUnavailableError = class extends Error {
652
654
  this.cause = cause;
653
655
  }
654
656
  };
657
+ var SqliteSchemaError = class extends Error {
658
+ code;
659
+ /** The stored `schema_version`; `undefined` when the file has none. */
660
+ foundVersion;
661
+ /** The schema version this build writes. */
662
+ requiredVersion;
663
+ constructor(code, message, foundVersion, requiredVersion, options) {
664
+ super(message, options);
665
+ this.name = "SqliteSchemaError";
666
+ this.code = code;
667
+ this.foundVersion = foundVersion;
668
+ this.requiredVersion = requiredVersion;
669
+ }
670
+ };
655
671
  var MIN_NODE_MAJOR = 22;
656
672
  var MIN_NODE_MINOR = 5;
657
673
  function isSqliteCapableNodeVersion(nodeVersion) {
@@ -879,7 +895,7 @@ var DEFAULT_MAILBOX_READ_LIMIT = 50;
879
895
  var DEFAULT_OBJECT_LIST_LIMIT = 100;
880
896
  var DEFAULT_BLOB_URL_TTL_MS = 15 * 6e4;
881
897
  var SIGNING_SECRET_BYTES = 32;
882
- var SQLITE_SCHEMA_VERSION = "3";
898
+ var SQLITE_SCHEMA_VERSION = "4";
883
899
  var RECEIPT_SCHEMA = `
884
900
  CREATE TABLE request_receipt (
885
901
  tenant_id TEXT NOT NULL,
@@ -923,6 +939,7 @@ CREATE TABLE IF NOT EXISTS task_attempt (
923
939
  agent_ref_json TEXT,
924
940
  owner_device_id TEXT,
925
941
  claimed_runtime TEXT,
942
+ claimed_harness_id TEXT,
926
943
  claimed_runtime_capabilities_json TEXT,
927
944
  status TEXT NOT NULL,
928
945
  terminal_cause TEXT,
@@ -979,14 +996,24 @@ var SqliteCoordinator = class {
979
996
  #closePromise;
980
997
  constructor(path, migration) {
981
998
  this.db = openSqliteDatabase(path);
999
+ let foundVersion;
1000
+ const schemaError = (code, message, options) => new SqliteSchemaError(code, message, foundVersion, SQLITE_SCHEMA_VERSION, options);
1001
+ const assertProjection = (projection) => {
1002
+ try {
1003
+ this.db.prepare(`SELECT ${projection} LIMIT 0`);
1004
+ } catch (error) {
1005
+ throw schemaError("SQLITE_SCHEMA_INVALID", `Missing BYOK SQLite durable authority: SELECT ${projection}`, { cause: error });
1006
+ }
1007
+ };
982
1008
  try {
983
1009
  this.db.exec("BEGIN IMMEDIATE");
984
1010
  try {
985
1011
  const hasMetadata = this.db.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'byok_sqlite_meta'").get();
986
1012
  if (hasMetadata !== void 0) {
987
1013
  const version = this.db.prepare("SELECT value FROM byok_sqlite_meta WHERE key = 'schema_version'").get();
988
- if (version?.value !== SQLITE_SCHEMA_VERSION && !(version?.value === "1" && migration === "v1-to-v3" || version?.value === "2" && migration === "v2-to-v3")) {
989
- throw new Error(`Unsupported BYOK SQLite schema version ${JSON.stringify(version?.value)}; this build requires ${SQLITE_SCHEMA_VERSION}. Stop all writers, back up the database and explicitly select migration: 'v1-to-v3' or 'v2-to-v3' for a receipt-free legacy database without task history.`);
1014
+ foundVersion = version?.value;
1015
+ if (version?.value !== SQLITE_SCHEMA_VERSION && !(version?.value === "1" && migration === "v1-to-v4" || version?.value === "2" && migration === "v2-to-v4" || version?.value === "3" && migration === "v3-to-v4")) {
1016
+ throw schemaError("SQLITE_SCHEMA_UNSUPPORTED", `Unsupported BYOK SQLite schema version ${JSON.stringify(version?.value)}; this build requires ${SQLITE_SCHEMA_VERSION}. Stop all writers, back up the database and explicitly select migration: 'v1-to-v4' or 'v2-to-v4' for a receipt-free legacy database without task history, or 'v3-to-v4' for a database without ambiguous claimed identity. Target-v3 migration selectors are no longer supported.`);
990
1017
  }
991
1018
  for (const projection of [
992
1019
  "tenant_id, device_id, next_seq, delivered_seq, acked_seq, updated_at FROM mailbox_cursor",
@@ -997,43 +1024,48 @@ var SqliteCoordinator = class {
997
1024
  "tenant_id, hash, ref_kind, ref_id, created_at FROM object_reference",
998
1025
  "blob_id, tenant_id, reservation_id, content_hash, byte_size, content_type, uploaded, data FROM blob"
999
1026
  ]) {
1000
- this.db.prepare(`SELECT ${projection} LIMIT 0`);
1027
+ assertProjection(projection);
1001
1028
  }
1002
- if (version?.value !== SQLITE_SCHEMA_VERSION) {
1029
+ if (version?.value === "1" || version?.value === "2") {
1003
1030
  for (const table of ["task_attempt", "mailbox_message", "agent_message_admission"]) {
1004
1031
  if (this.db.prepare(`SELECT 1 FROM ${table} LIMIT 1`).get() !== void 0) {
1005
- throw new Error("Cannot migrate BYOK SQLite database with task history: historical receipts are unavailable; preserve this database for reconciliation");
1032
+ throw schemaError("SQLITE_MIGRATION_REFUSED", "Cannot migrate BYOK SQLite database with task history: historical receipts are unavailable; preserve this database for reconciliation");
1006
1033
  }
1007
1034
  }
1008
1035
  if (this.db.prepare("SELECT 1 FROM mailbox_cursor WHERE next_seq <> 1 OR delivered_seq <> 0 OR acked_seq <> 0 LIMIT 1").get() !== void 0) {
1009
- throw new Error("Cannot migrate BYOK SQLite database with delivery history: historical receipts are unavailable; preserve this database for reconciliation");
1036
+ throw schemaError("SQLITE_MIGRATION_REFUSED", "Cannot migrate BYOK SQLite database with delivery history: historical receipts are unavailable; preserve this database for reconciliation");
1010
1037
  }
1011
1038
  if (this.db.prepare("SELECT 1 FROM sqlite_master WHERE name = 'request_receipt'").get() !== void 0) {
1012
- throw new Error("Unexpected request receipt authority in legacy BYOK SQLite schema");
1039
+ throw schemaError("SQLITE_SCHEMA_INVALID", "Unexpected request receipt authority in legacy BYOK SQLite schema");
1013
1040
  }
1014
1041
  this.db.exec(RECEIPT_SCHEMA);
1015
1042
  }
1016
1043
  if (version?.value === "1") {
1017
1044
  const unexpected = this.db.prepare("SELECT 1 FROM sqlite_master WHERE name = 'device_directory'").get();
1018
- if (unexpected !== void 0) throw new Error("Unexpected device directory in BYOK SQLite schema v1");
1045
+ if (unexpected !== void 0) throw schemaError("SQLITE_SCHEMA_INVALID", "Unexpected device directory in BYOK SQLite schema v1");
1019
1046
  this.db.exec(DEVICE_SCHEMA);
1020
1047
  }
1021
1048
  if (version?.value !== SQLITE_SCHEMA_VERSION) {
1049
+ if (this.db.prepare("SELECT 1 FROM task_attempt WHERE owner_device_id IS NOT NULL AND claimed_runtime IS NULL LIMIT 1").get() !== void 0) {
1050
+ throw schemaError("SQLITE_MIGRATION_REFUSED", "Cannot migrate BYOK SQLite database with ambiguous claimed identity: historical harness identities are unavailable; preserve this database for reconciliation");
1051
+ }
1052
+ this.db.exec("ALTER TABLE task_attempt ADD COLUMN claimed_harness_id TEXT");
1022
1053
  this.db.prepare("UPDATE byok_sqlite_meta SET value = ? WHERE key = 'schema_version'").run(SQLITE_SCHEMA_VERSION);
1023
1054
  }
1024
1055
  } else {
1025
1056
  const existing = this.db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").get();
1026
- if (existing !== void 0) throw new Error("Existing SQLite database has no BYOK schema metadata");
1057
+ if (existing !== void 0) throw schemaError("SQLITE_SCHEMA_INVALID", "Existing SQLite database has no BYOK schema metadata");
1027
1058
  this.db.exec(SCHEMA);
1028
1059
  this.db.exec(RECEIPT_SCHEMA);
1029
1060
  this.db.exec(DEVICE_SCHEMA);
1030
1061
  this.db.prepare("INSERT INTO byok_sqlite_meta (key, value) VALUES ('schema_version', ?)").run(SQLITE_SCHEMA_VERSION);
1031
1062
  }
1032
- this.db.prepare("SELECT tenant_id, device_id, product_id, machine_id, device_name, device_public_key, proof_key_id, proof_key_epoch, capabilities_json, harnesses_json FROM device_directory LIMIT 0");
1063
+ assertProjection("claimed_harness_id FROM task_attempt");
1064
+ assertProjection("tenant_id, device_id, product_id, machine_id, device_name, device_public_key, proof_key_id, proof_key_epoch, capabilities_json, harnesses_json FROM device_directory");
1033
1065
  const receiptColumns = this.db.prepare("PRAGMA table_info(request_receipt)").all();
1034
1066
  const expectedColumns = ["tenant_id", "key", "body", "recorded_at"];
1035
1067
  if (receiptColumns.length !== expectedColumns.length || receiptColumns.some((column, index) => column.name !== expectedColumns[index] || column.type !== "TEXT" || column.notnull !== 1 || column.pk !== (index < 2 ? index + 1 : 0))) {
1036
- throw new Error("Invalid BYOK SQLite request receipt schema");
1068
+ throw schemaError("SQLITE_SCHEMA_INVALID", "Invalid BYOK SQLite request receipt schema");
1037
1069
  }
1038
1070
  this.db.exec("COMMIT");
1039
1071
  } catch (error) {
@@ -1086,6 +1118,7 @@ function taskRow(row) {
1086
1118
  ...row.agent_ref_json === null ? {} : { agentRef: JSON.parse(row.agent_ref_json) },
1087
1119
  ...row.owner_device_id === null ? {} : { ownerDeviceId: row.owner_device_id },
1088
1120
  ...row.claimed_runtime === null ? {} : { claimedRuntime: row.claimed_runtime },
1121
+ ...row.claimed_harness_id === null ? {} : { claimedHarnessId: row.claimed_harness_id },
1089
1122
  ...row.claimed_runtime_capabilities_json === null ? {} : { claimedRuntimeCapabilities: JSON.parse(row.claimed_runtime_capabilities_json) },
1090
1123
  status: row.status,
1091
1124
  ...row.terminal_cause === null ? {} : { terminalCause: row.terminal_cause },
@@ -1247,13 +1280,14 @@ var SqliteTaskAttemptStore = class {
1247
1280
  claim(tenant, input) {
1248
1281
  return this.coordinator.run((db) => {
1249
1282
  db.prepare(
1250
- `UPDATE task_attempt SET owner_device_id = ?, claimed_runtime = ?, claimed_runtime_capabilities_json = ?,
1283
+ `UPDATE task_attempt SET owner_device_id = ?, claimed_runtime = ?, claimed_harness_id = ?, claimed_runtime_capabilities_json = ?,
1251
1284
  status = 'claimed', updated_at = ?
1252
1285
  WHERE tenant_id = ? AND task_id = ? AND device_id = ? AND owner_device_id IS NULL
1253
1286
  AND cancellation_requested_at IS NULL AND status = 'offered'`
1254
1287
  ).run(
1255
1288
  input.deviceId,
1256
1289
  input.runtime ?? null,
1290
+ input.harnessId ?? null,
1257
1291
  input.capabilities === void 0 ? null : JSON.stringify(input.capabilities),
1258
1292
  this.#now(),
1259
1293
  tenant,
@@ -2170,12 +2204,10 @@ function createByokServer(opts) {
2170
2204
  `device ${deviceId} advertises strict-agent-only; legacy task dispatch is refused before enqueue`
2171
2205
  );
2172
2206
  }
2173
- const policy = input.policy ?? { mode: "confirm" };
2174
2207
  const runtime = dispatchSelection?.runtimeId ?? input.runtime;
2175
2208
  const common = {
2176
2209
  ...input.harnessId === void 0 ? {} : { harnessId: input.harnessId },
2177
2210
  instruction: input.instruction,
2178
- policy,
2179
2211
  ...runtime === void 0 ? {} : { runtime },
2180
2212
  ...dispatchSelection === void 0 ? {} : { dispatchSelection }
2181
2213
  };
@@ -2443,6 +2475,6 @@ function longPollInterval(holdMs) {
2443
2475
  return Math.max(MIN_LONG_POLL_INTERVAL_MS, Math.min(MAX_LONG_POLL_INTERVAL_MS, derived));
2444
2476
  }
2445
2477
 
2446
- export { DEFAULT_TASK_EVENT_BUFFER_LIMIT, DEFAULT_TASK_EVENT_RETENTION_MS, DEFAULT_TASK_PAGE_LIMIT, SqliteUnavailableError, SteerRejectedError, createByokServer };
2478
+ export { DEFAULT_TASK_EVENT_BUFFER_LIMIT, DEFAULT_TASK_EVENT_RETENTION_MS, DEFAULT_TASK_PAGE_LIMIT, SqliteSchemaError, SqliteUnavailableError, SteerRejectedError, createByokServer };
2447
2479
  //# sourceMappingURL=index.js.map
2448
2480
  //# sourceMappingURL=index.js.map