@byok-sdk/server 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,7 @@ live device advertises `toolset-selection` and its `configuredToolsets`
25
25
  inventory contains every required ID. `machines.list()` projects that same
26
26
  logical-ID-only inventory; MCP commands and credentials remain device-local.
27
27
 
28
- MIT licensed. Node.js 22.22.0 or newer.
28
+ MIT licensed. Node.js 24.15.0 or newer.
29
29
 
30
30
  ## SQLite enrollment, receipts and schema adoption
31
31
 
@@ -41,37 +41,47 @@ Unredeemed pairing codes, presence and other unmodified ports remain ephemeral.
41
41
  Keep daemon OS credentials, server URL and token signer stable across restart.
42
42
  This does not restore TaskHandle promises, subscriptions or provider processes.
43
43
 
44
- This candidate writes schema v3. Stop **all** writers and take a consistent
44
+ This candidate writes schema v4, including the write-once `claimedHarnessId`
45
+ stored atomically with task ownership. Stop **all** writers and take a consistent
45
46
  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.
47
+ explicit adoption. An already-open old writer is not stopped by the version fence.
48
+
49
+ - `v1-to-v4` and `v2-to-v4` require no task, mailbox-message, agent-admission or
50
+ advanced cursor history: old in-memory receipts cannot be recovered from task
51
+ status or fabricated from a retry payload. A pristine poll cursor is allowed.
52
+ - `v3-to-v4` preserves receipts, unclaimed offers and claims with an identified
53
+ built-in runtime. It rejects any owned task with no `claimed_runtime`, including
54
+ terminal tasks: a lost custom-harness identity cannot be distinguished from an
55
+ identity-free legacy built-in claim. Neither the requested offer nor current
56
+ device inventory proves the actual claiming adapter. Existing rows receive a
57
+ null harness identity; migration never invents one.
58
+
59
+ Preserve rejected databases for separate reconciliation; do not delete history,
60
+ reset cursors or edit version markers to make adoption pass.
52
61
 
53
62
  ```ts
54
63
  const server = createByokServer({
55
64
  productId,
56
65
  tokenSigner,
57
- storage: { kind: 'sqlite', path, migration: 'v2-to-v3' },
66
+ storage: { kind: 'sqlite', path, migration: 'v3-to-v4' },
58
67
  });
59
68
  await server.close();
60
69
  ```
61
70
 
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.
71
+ Remove `migration` from normal startup. Adoption adds the nullable harness column,
72
+ adds receipts for v1/v2 and an empty device directory for v1, and updates the marker
73
+ atomically; any failure rolls back. Existing v2/v3 devices and artifacts remain.
74
+ V1 enrollment was in-memory and needs explicit pairing. Unknown versions, missing
75
+ authorities and malformed receipt columns/primary key fail closed. Broader
76
+ validation of legacy table constraints is a separate tracked concern.
77
+
78
+ **Breaking storage boundary:** schema-v1/v2/v3 writers refuse v4. The public
79
+ `v1-to-v3` / `v2-to-v3` selectors are replaced by the corresponding target-v4
80
+ selectors, with no alias or silent reinterpretation: callers must update their
81
+ one-shot migration configuration after checking eligibility and backing up.
82
+ Restore of a backup requires a separate recovery procedure because it can lose
83
+ new state or restore old grants. This belongs to the next MINOR release; these
84
+ local changes are not a published upgrade.
75
85
 
76
86
 
77
87
  ## Persistent host request identity
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) {
@@ -879,7 +881,7 @@ var DEFAULT_MAILBOX_READ_LIMIT = 50;
879
881
  var DEFAULT_OBJECT_LIST_LIMIT = 100;
880
882
  var DEFAULT_BLOB_URL_TTL_MS = 15 * 6e4;
881
883
  var SIGNING_SECRET_BYTES = 32;
882
- var SQLITE_SCHEMA_VERSION = "3";
884
+ var SQLITE_SCHEMA_VERSION = "4";
883
885
  var RECEIPT_SCHEMA = `
884
886
  CREATE TABLE request_receipt (
885
887
  tenant_id TEXT NOT NULL,
@@ -923,6 +925,7 @@ CREATE TABLE IF NOT EXISTS task_attempt (
923
925
  agent_ref_json TEXT,
924
926
  owner_device_id TEXT,
925
927
  claimed_runtime TEXT,
928
+ claimed_harness_id TEXT,
926
929
  claimed_runtime_capabilities_json TEXT,
927
930
  status TEXT NOT NULL,
928
931
  terminal_cause TEXT,
@@ -985,8 +988,8 @@ var SqliteCoordinator = class {
985
988
  const hasMetadata = this.db.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'byok_sqlite_meta'").get();
986
989
  if (hasMetadata !== void 0) {
987
990
  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.`);
991
+ 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")) {
992
+ 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-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
993
  }
991
994
  for (const projection of [
992
995
  "tenant_id, device_id, next_seq, delivered_seq, acked_seq, updated_at FROM mailbox_cursor",
@@ -999,7 +1002,7 @@ var SqliteCoordinator = class {
999
1002
  ]) {
1000
1003
  this.db.prepare(`SELECT ${projection} LIMIT 0`);
1001
1004
  }
1002
- if (version?.value !== SQLITE_SCHEMA_VERSION) {
1005
+ if (version?.value === "1" || version?.value === "2") {
1003
1006
  for (const table of ["task_attempt", "mailbox_message", "agent_message_admission"]) {
1004
1007
  if (this.db.prepare(`SELECT 1 FROM ${table} LIMIT 1`).get() !== void 0) {
1005
1008
  throw new Error("Cannot migrate BYOK SQLite database with task history: historical receipts are unavailable; preserve this database for reconciliation");
@@ -1019,6 +1022,10 @@ var SqliteCoordinator = class {
1019
1022
  this.db.exec(DEVICE_SCHEMA);
1020
1023
  }
1021
1024
  if (version?.value !== SQLITE_SCHEMA_VERSION) {
1025
+ 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) {
1026
+ throw new Error("Cannot migrate BYOK SQLite database with ambiguous claimed identity: historical harness identities are unavailable; preserve this database for reconciliation");
1027
+ }
1028
+ this.db.exec("ALTER TABLE task_attempt ADD COLUMN claimed_harness_id TEXT");
1022
1029
  this.db.prepare("UPDATE byok_sqlite_meta SET value = ? WHERE key = 'schema_version'").run(SQLITE_SCHEMA_VERSION);
1023
1030
  }
1024
1031
  } else {
@@ -1029,6 +1036,7 @@ var SqliteCoordinator = class {
1029
1036
  this.db.exec(DEVICE_SCHEMA);
1030
1037
  this.db.prepare("INSERT INTO byok_sqlite_meta (key, value) VALUES ('schema_version', ?)").run(SQLITE_SCHEMA_VERSION);
1031
1038
  }
1039
+ this.db.prepare("SELECT claimed_harness_id FROM task_attempt LIMIT 0");
1032
1040
  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");
1033
1041
  const receiptColumns = this.db.prepare("PRAGMA table_info(request_receipt)").all();
1034
1042
  const expectedColumns = ["tenant_id", "key", "body", "recorded_at"];
@@ -1086,6 +1094,7 @@ function taskRow(row) {
1086
1094
  ...row.agent_ref_json === null ? {} : { agentRef: JSON.parse(row.agent_ref_json) },
1087
1095
  ...row.owner_device_id === null ? {} : { ownerDeviceId: row.owner_device_id },
1088
1096
  ...row.claimed_runtime === null ? {} : { claimedRuntime: row.claimed_runtime },
1097
+ ...row.claimed_harness_id === null ? {} : { claimedHarnessId: row.claimed_harness_id },
1089
1098
  ...row.claimed_runtime_capabilities_json === null ? {} : { claimedRuntimeCapabilities: JSON.parse(row.claimed_runtime_capabilities_json) },
1090
1099
  status: row.status,
1091
1100
  ...row.terminal_cause === null ? {} : { terminalCause: row.terminal_cause },
@@ -1247,13 +1256,14 @@ var SqliteTaskAttemptStore = class {
1247
1256
  claim(tenant, input) {
1248
1257
  return this.coordinator.run((db) => {
1249
1258
  db.prepare(
1250
- `UPDATE task_attempt SET owner_device_id = ?, claimed_runtime = ?, claimed_runtime_capabilities_json = ?,
1259
+ `UPDATE task_attempt SET owner_device_id = ?, claimed_runtime = ?, claimed_harness_id = ?, claimed_runtime_capabilities_json = ?,
1251
1260
  status = 'claimed', updated_at = ?
1252
1261
  WHERE tenant_id = ? AND task_id = ? AND device_id = ? AND owner_device_id IS NULL
1253
1262
  AND cancellation_requested_at IS NULL AND status = 'offered'`
1254
1263
  ).run(
1255
1264
  input.deviceId,
1256
1265
  input.runtime ?? null,
1266
+ input.harnessId ?? null,
1257
1267
  input.capabilities === void 0 ? null : JSON.stringify(input.capabilities),
1258
1268
  this.#now(),
1259
1269
  tenant,