@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 +42 -23
- package/dist/index.d.ts +2 -1
- package/dist/index.js +49 -17
- package/dist/index.js.map +1 -1
- package/dist/sqlite-support.d.ts +20 -0
- package/dist/stores/sqlite/index.d.ts +3 -2
- package/dist/types.d.ts +14 -8
- package/package.json +5 -5
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
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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: '
|
|
75
|
+
storage: { kind: 'sqlite', path, migration: 'v3-to-v4' },
|
|
58
76
|
});
|
|
59
77
|
await server.close();
|
|
60
78
|
```
|
|
61
79
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
belongs to the next MINOR release; these
|
|
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
|
-
|
|
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 = "
|
|
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
|
-
|
|
989
|
-
|
|
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
|
-
|
|
1027
|
+
assertProjection(projection);
|
|
1001
1028
|
}
|
|
1002
|
-
if (version?.value
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|