@byok-sdk/server 0.16.0 → 0.18.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
@@ -26,3 +26,80 @@ 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
28
  MIT licensed. Node.js 22.22.0 or newer.
29
+
30
+ ## SQLite enrollment, receipts and schema adoption
31
+
32
+ `storage: { kind: 'sqlite', path }` persists device enrollment, authenticated
33
+ capabilities, request receipts and the six coordination interfaces. Pairing and
34
+ authentication share the device directory. Receipt keys are tenant scoped and
35
+ first-write-wins across connections and restart. Mailbox retention never deletes
36
+ receipts: immutable offer, delivered and canonical terminal facts remain for the
37
+ lifetime of the database. There is no receipt TTL or automatic pruning. Monitor
38
+ disk usage; exhaustion is an error, not permission to delete identity fences.
39
+
40
+ Unredeemed pairing codes, presence and other unmodified ports remain ephemeral.
41
+ Keep daemon OS credentials, server URL and token signer stable across restart.
42
+ This does not restore TaskHandle promises, subscriptions or provider processes.
43
+
44
+ This candidate writes schema v3. Stop **all** writers and take a consistent
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.
52
+
53
+ ```ts
54
+ const server = createByokServer({
55
+ productId,
56
+ tokenSigner,
57
+ storage: { kind: 'sqlite', path, migration: 'v2-to-v3' },
58
+ });
59
+ await server.close();
60
+ ```
61
+
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.
75
+
76
+
77
+ ## Persistent host request identity
78
+
79
+ Before enqueue, durably bind the authenticated scope and external execution key
80
+ (including cycle/pass where applicable) to one taskId, explicit deviceId and
81
+ immutable dispatch input. Pass that taskId to `dispatch` or
82
+ `dispatchFreshAgentEgress`. Omitting taskId retains fresh-task semantics.
83
+ A caller taskId requires an explicit device; the façade never reselects an
84
+ ambient device for that identity. Initial dispatch still requires a connected
85
+ device. Concurrent calls for one caller identity in one façade are rejected
86
+ while the first is in flight.
87
+
88
+ After restart use `tasks.offer(taskId)` to read the kernel's immutable executable
89
+ offer and delivered marker, and `tasks.get(taskId)` for its state and result.
90
+ These reads work offline. Duplicate dispatch can still throw a kernel conflict:
91
+ only treat it as recovery after matching the original scope/target/type/payload
92
+ and durable delivery. Missing or mismatched readback is not success. A false
93
+ delivered marker does not prove mailbox append never happened. `tasks.offer`
94
+ returns no invented transport sequence or reconstructed TaskHandle. Host-only
95
+ agentMessageContext is not part of this executable-offer projection; callers
96
+ must preserve its original binding and its separate consumer authority.
97
+
98
+ `await server.tasks.cancel(taskId, reason)` uses the same kernel cancellation
99
+ and relay notification as a live handle; it does not require a connected device.
100
+ Missing tasks reject. A host must separately persist cancellation intent if it
101
+ can crash before this call, and prevent dispatch of a cancelled prepared request.
102
+
103
+ The runnable public-package fixture is
104
+ `docs/researches/2026-09-09-g1c-facade/public-consumer-probe.mjs`. It demonstrates
105
+ pre-enqueue binding and recovery, not product authentication or consumer ACK.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { Hono } from 'hono';
2
- import { type PairingCodeInfo } from '@byok-sdk/cloud';
3
- import { type AgentRef } from '@byok-sdk/protocol';
2
+ import { type EnqueuedOffer, type PairingCodeInfo, type TaskAttempt } from '@byok-sdk/cloud';
3
+ import { type AgentRef, type AgentMessagePublishPayload, type AgentMessageDispositionPayload } from '@byok-sdk/protocol';
4
4
  import type { MailboxRetentionInput, MailboxRetentionResult } from '@byok-sdk/core';
5
5
  import type { ByokServerEvent, AgentContentReadRequest, AgentHomeProjectionRequest, AgentHomeProjectionStatusReadback, AgentEgressReceipt, CreateByokServerOptions, DispatchInput, FreshAgentEgressDispatchInput, HubStats, MachineInfo, TaskHandle, TaskSnapshot } from './types';
6
6
  export type { ByokServerEvent, AgentContentReadRequest, AgentHomeProjectionRequest, AgentHomeProjectionStatusReadback, AgentEgressReceipt, ByokServerStorage, CreateByokServerOptions, DispatchInput, FreshAgentEgressDispatchInput, HubStats, MachineInfo, ServerTaskEvent, TaskHandle, TaskResult, TaskSnapshot, } from './types';
7
+ export type { TaskOfferReadback } from '@byok-sdk/cloud';
7
8
  /**
8
9
  * M5 (approval targeting, docs/protocol.md §5.3): `TaskHandle.approve`/`reject`'s
9
10
  * `opts.approvalId` targeting throws this when the id names an approval the
@@ -81,6 +82,10 @@ export interface ByokServer {
81
82
  /** Mint a single-use pairing code for this server's product and tenant (docs/protocol.md §6.1). */
82
83
  createPairingCode(input: CreatePairingCodeInput): Promise<PairingCodeInfo>;
83
84
  };
85
+ /** Durable recurring submission; no TaskHandle is needed to recover after restart. */
86
+ recurring: {
87
+ submit(input: import('@byok-sdk/cloud').RecurringExecutionInput): Promise<EnqueuedOffer>;
88
+ };
84
89
  dispatch(input: DispatchInput): Promise<TaskHandle>;
85
90
  /** Dispatch a fresh Agent execution whose runtime will mint its session after start. */
86
91
  dispatchFreshAgentEgress(input: FreshAgentEgressDispatchInput): Promise<TaskHandle>;
@@ -91,7 +96,19 @@ export interface ByokServer {
91
96
  /** Durable desired-state and terminal-outcome readback for one exact device-and-Agent request. */
92
97
  readAgentHomeProjection(deviceId: string, agentRef: AgentRef, requestId: string): Promise<AgentHomeProjectionStatusReadback | undefined>;
93
98
  tasks: {
99
+ /** Canonical durable attempt, including cancellation intent; not a resource-release observation. */
100
+ attempt(taskId: string): Promise<TaskAttempt | undefined>;
94
101
  get(taskId: string): Promise<TaskSnapshot | undefined>;
102
+ /** Actual device terminal, independently of Host cancellation or home release. */
103
+ deviceTerminal(taskId: string): Promise<import('@byok-sdk/cloud').DeviceTerminal | undefined>;
104
+ /** Immutable kernel offer readback for verifying a persisted host binding. No transport seq is inferred. */
105
+ offer(taskId: string): Promise<import('@byok-sdk/cloud').TaskOfferReadback | undefined>;
106
+ /** Discover the unique message and its pending/final disposition through the frozen Agent binding. */
107
+ agentMessage(taskId: string, deviceId: string, agentRef: AgentRef): Promise<import('@byok-sdk/cloud').TaskAgentMessage | undefined>;
108
+ /** Exact SDK message decision; pending is undefined and corrupt persisted evidence throws. */
109
+ messageDisposition(taskId: string, deviceId: string, payload: AgentMessagePublishPayload): Promise<AgentMessageDispositionPayload | undefined>;
110
+ /** Request cancellation through the kernel without a process-owned TaskHandle. */
111
+ cancel(taskId: string, reason?: string): Promise<void>;
95
112
  /**
96
113
  * One bounded page, keyset-paged by task id. Paged rather than "all of
97
114
  * them" because the underlying store is: an unbounded `list()` would have
@@ -194,7 +211,7 @@ export interface ByokServer {
194
211
  * one, an in-process notification relay, and the observability an embedder used
195
212
  * to get from the hub.
196
213
  *
197
- * State is in-memory by default. Explicit SQLite mode persists the six
214
+ * State is in-memory by default. Explicit SQLite mode persists request receipts, the device directory and six
198
215
  * coordination interfaces whose contracts cross a process restart; all other
199
216
  * ports remain process-local.
200
217
  */
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Hono } from 'hono';
2
- import { createHmacTokenSigner, createByokCloud, fullCapabilityDeclaration, agentHomeProjectionRequestKey, pendingApproval, createWebCrypto, createInMemoryCloudStores, SteerRejectedError as SteerRejectedError$1, assertTaskAttemptListLimit } from '@byok-sdk/cloud';
2
+ import { createHmacTokenSigner, createByokCloud, fullCapabilityDeclaration, agentHomeProjectionRequestKey, pendingApproval, createWebCrypto, createInMemoryCloudStores, SteerRejectedError as SteerRejectedError$1, InMemoryPairingCodeStore, assertTaskAttemptListLimit } from '@byok-sdk/cloud';
3
3
  export { StaleApprovalError, createHmacTokenSigner } from '@byok-sdk/cloud';
4
- import { TASK_STATES, AgentHomeProjectionPayloadSchema, AgentContentReadPayloadSchema, AgentRefSchema, AgentEgressPolicySchema, AgentMessageEgressRequirementSchema, TerminalProjectionSelectionSchema, AgentMessageServerContextSchema, DispatchSelectionSchema, RequiredToolsetsSchema, STRICT_AGENT_ONLY_CAPABILITY, byokBlobContentPath } from '@byok-sdk/protocol';
5
- import { tenantId, createInMemoryCoreStores, InMemoryQuotaStore, ByokCoreError, assertCanonicalTimestamp, isLegalObjectTransition, CoreConflictError } from '@byok-sdk/core';
4
+ import { TASK_STATES, AgentHomeProjectionPayloadSchema, AgentContentReadPayloadSchema, AgentRefSchema, AgentEgressPolicySchema, AgentMessageEgressRequirementSchema, TerminalProjectionSelectionSchema, AgentMessageServerContextSchema, DispatchSelectionSchema, RequiredToolsetsSchema, STRICT_AGENT_ONLY_CAPABILITY, byokBlobContentPath, HarnessInventorySchema } from '@byok-sdk/protocol';
5
+ import { tenantId, createInMemoryCoreStores, InMemoryQuotaStore, ByokCoreError, assertCanonicalTimestamp, isLegalObjectTransition, CoreConflictError, PRESENCE_LEVELS } from '@byok-sdk/core';
6
6
  import { mkdirSync, existsSync, chmodSync } from 'fs';
7
7
  import { createRequire } from 'module';
8
8
  import { dirname } from 'path';
@@ -712,13 +712,183 @@ function secureSqliteFilePermissions(dbPath) {
712
712
  }
713
713
  }
714
714
  }
715
+ var DEVICE_SCHEMA = `
716
+ CREATE TABLE device_directory (
717
+ tenant_id TEXT NOT NULL,
718
+ device_id TEXT NOT NULL UNIQUE,
719
+ product_id TEXT NOT NULL,
720
+ machine_id TEXT,
721
+ device_name TEXT NOT NULL,
722
+ device_public_key TEXT NOT NULL,
723
+ proof_key_id TEXT NOT NULL,
724
+ proof_key_epoch INTEGER NOT NULL CHECK (proof_key_epoch >= 0),
725
+ capabilities_json TEXT,
726
+ harnesses_json TEXT,
727
+ PRIMARY KEY (tenant_id, device_id)
728
+ );
729
+ CREATE UNIQUE INDEX device_directory_machine
730
+ ON device_directory (tenant_id, product_id, machine_id) WHERE machine_id IS NOT NULL;
731
+ `;
732
+ function requiredString(row, field) {
733
+ const value = row[field];
734
+ if (typeof value !== "string" || value.length === 0) throw new Error(`Invalid SQLite device ${field}`);
735
+ return value;
736
+ }
737
+ function capabilities(value) {
738
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
739
+ throw new Error("Invalid SQLite device capabilities");
740
+ }
741
+ return Object.freeze([...value]);
742
+ }
743
+ function fromRow(row) {
744
+ const epoch = row.proof_key_epoch;
745
+ if (typeof epoch !== "number" || !Number.isSafeInteger(epoch) || epoch < 0) {
746
+ throw new Error("Invalid SQLite device proof_key_epoch");
747
+ }
748
+ return {
749
+ tenantId: requiredString(row, "tenant_id"),
750
+ deviceId: requiredString(row, "device_id"),
751
+ productId: requiredString(row, "product_id"),
752
+ deviceName: requiredString(row, "device_name"),
753
+ devicePublicKey: requiredString(row, "device_public_key"),
754
+ proofKeyId: requiredString(row, "proof_key_id"),
755
+ proofKeyEpoch: epoch,
756
+ revoked: false,
757
+ ...row.machine_id === null ? {} : { machineId: requiredString(row, "machine_id") },
758
+ ...row.capabilities_json === null ? {} : {
759
+ capabilities: capabilities(JSON.parse(requiredString(row, "capabilities_json")))
760
+ },
761
+ ...row.harnesses_json === null ? {} : {
762
+ harnesses: HarnessInventorySchema.parse(JSON.parse(requiredString(row, "harnesses_json")))
763
+ }
764
+ };
765
+ }
766
+ var SqliteDeviceDirectory = class {
767
+ constructor(coordinator) {
768
+ this.coordinator = coordinator;
769
+ }
770
+ coordinator;
771
+ register(tenant, input) {
772
+ return this.coordinator.transaction((db) => {
773
+ const owner = db.prepare("SELECT tenant_id FROM device_directory WHERE device_id = ?").get(input.deviceId);
774
+ if (owner !== void 0 && owner.tenant_id !== tenant) throw new Error("Device ID already belongs to another tenant");
775
+ if (input.machineId !== void 0) {
776
+ db.prepare("DELETE FROM device_directory WHERE tenant_id = ? AND product_id = ? AND machine_id = ? AND device_id <> ?").run(tenant, input.productId, input.machineId, input.deviceId);
777
+ }
778
+ db.prepare(`INSERT INTO device_directory
779
+ (tenant_id, device_id, product_id, machine_id, device_name, device_public_key, proof_key_id, proof_key_epoch)
780
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
781
+ ON CONFLICT (tenant_id, device_id) DO UPDATE SET
782
+ product_id = excluded.product_id, machine_id = excluded.machine_id,
783
+ device_name = excluded.device_name, device_public_key = excluded.device_public_key,
784
+ proof_key_id = excluded.proof_key_id, proof_key_epoch = excluded.proof_key_epoch,
785
+ capabilities_json = NULL, harnesses_json = NULL`).run(
786
+ tenant,
787
+ input.deviceId,
788
+ input.productId,
789
+ input.machineId ?? null,
790
+ input.deviceName,
791
+ input.devicePublicKey,
792
+ input.proofKeyId,
793
+ input.proofKeyEpoch
794
+ );
795
+ return fromRow(db.prepare("SELECT * FROM device_directory WHERE tenant_id = ? AND device_id = ?").get(tenant, input.deviceId));
796
+ });
797
+ }
798
+ get(tenant, deviceId) {
799
+ return this.coordinator.run((db) => {
800
+ const row = db.prepare("SELECT * FROM device_directory WHERE tenant_id = ? AND device_id = ?").get(tenant, deviceId);
801
+ return row === void 0 ? void 0 : fromRow(row);
802
+ });
803
+ }
804
+ revoke(tenant, deviceId) {
805
+ return this.coordinator.run((db) => {
806
+ db.prepare("DELETE FROM device_directory WHERE tenant_id = ? AND device_id = ?").run(tenant, deviceId);
807
+ });
808
+ }
809
+ list(tenant) {
810
+ return this.coordinator.run((db) => db.prepare("SELECT * FROM device_directory WHERE tenant_id = ? ORDER BY device_id").all(tenant).map(fromRow));
811
+ }
812
+ resolveByDeviceId(deviceId) {
813
+ return this.coordinator.run((db) => {
814
+ const row = db.prepare("SELECT * FROM device_directory WHERE device_id = ?").get(deviceId);
815
+ return row === void 0 ? void 0 : fromRow(row);
816
+ });
817
+ }
818
+ recordCapabilities(tenant, input) {
819
+ return this.coordinator.transaction((db) => {
820
+ const row = db.prepare("SELECT * FROM device_directory WHERE tenant_id = ? AND device_id = ?").get(tenant, input.deviceId);
821
+ if (row === void 0) return void 0;
822
+ fromRow(row);
823
+ const declaredCapabilities = capabilities(input.capabilities);
824
+ const harnesses = HarnessInventorySchema.parse(input.harnesses ?? []);
825
+ db.prepare("UPDATE device_directory SET capabilities_json = ?, harnesses_json = ? WHERE tenant_id = ? AND device_id = ?").run(JSON.stringify(declaredCapabilities), JSON.stringify(harnesses), tenant, input.deviceId);
826
+ return fromRow(db.prepare("SELECT * FROM device_directory WHERE tenant_id = ? AND device_id = ?").get(tenant, input.deviceId));
827
+ });
828
+ }
829
+ async readiness(tenant, presence) {
830
+ const devices = await this.list(tenant);
831
+ const livePresence = await presence.list(tenant);
832
+ const presenceByDevice = new Map(livePresence.map((hint) => [hint.deviceId, hint]));
833
+ const activeDeviceIds = new Set(
834
+ devices.filter((device) => !device.revoked).map((device) => device.deviceId)
835
+ );
836
+ const observedPresenceByLevel = Object.fromEntries(
837
+ PRESENCE_LEVELS.map((level) => [level, 0])
838
+ );
839
+ for (const hint of livePresence) {
840
+ if (!activeDeviceIds.has(hint.deviceId)) continue;
841
+ observedPresenceByLevel[hint.level] += 1;
842
+ }
843
+ return {
844
+ tenantId: tenant,
845
+ activePairedDeviceCount: devices.filter((device) => !device.revoked).length,
846
+ revokedDeviceCount: devices.filter((device) => device.revoked).length,
847
+ observedPresenceCount: Object.values(observedPresenceByLevel).reduce(
848
+ (total, count) => total + count,
849
+ 0
850
+ ),
851
+ observedPresenceByLevel,
852
+ devices: [...devices].sort((left, right) => left.deviceId.localeCompare(right.deviceId)).map((device) => {
853
+ const hint = device.revoked ? void 0 : presenceByDevice.get(device.deviceId);
854
+ return {
855
+ deviceId: device.deviceId,
856
+ productId: device.productId,
857
+ deviceName: device.deviceName,
858
+ revoked: device.revoked,
859
+ ...hint === void 0 ? {} : {
860
+ presence: {
861
+ level: hint.level,
862
+ ...hint.detail === void 0 ? {} : { detail: hint.detail },
863
+ ...hint.configuredToolsets === void 0 ? {} : { configuredToolsets: hint.configuredToolsets },
864
+ ...hint.clientVersion === void 0 ? {} : { clientVersion: hint.clientVersion },
865
+ ...hint.protocolVersions === void 0 ? {} : { protocolVersions: hint.protocolVersions },
866
+ ...hint.runtimes === void 0 ? {} : { runtimes: hint.runtimes },
867
+ observedAt: hint.observedAt,
868
+ expiresAt: hint.expiresAt
869
+ }
870
+ }
871
+ };
872
+ })
873
+ };
874
+ }
875
+ };
715
876
 
716
877
  // src/stores/sqlite/index.ts
717
878
  var DEFAULT_MAILBOX_READ_LIMIT = 50;
718
879
  var DEFAULT_OBJECT_LIST_LIMIT = 100;
719
880
  var DEFAULT_BLOB_URL_TTL_MS = 15 * 6e4;
720
881
  var SIGNING_SECRET_BYTES = 32;
721
- var SQLITE_SCHEMA_VERSION = "1";
882
+ var SQLITE_SCHEMA_VERSION = "3";
883
+ var RECEIPT_SCHEMA = `
884
+ CREATE TABLE request_receipt (
885
+ tenant_id TEXT NOT NULL,
886
+ key TEXT NOT NULL,
887
+ body TEXT NOT NULL,
888
+ recorded_at TEXT NOT NULL,
889
+ PRIMARY KEY (tenant_id, key)
890
+ );
891
+ `;
722
892
  var SCHEMA = `
723
893
  CREATE TABLE IF NOT EXISTS byok_sqlite_meta (
724
894
  key TEXT PRIMARY KEY,
@@ -807,21 +977,68 @@ var SqliteCoordinator = class {
807
977
  #tail = Promise.resolve();
808
978
  #closing = false;
809
979
  #closePromise;
810
- constructor(path) {
980
+ constructor(path, migration) {
811
981
  this.db = openSqliteDatabase(path);
812
982
  try {
813
- const hasMetadata = this.db.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'byok_sqlite_meta'").get();
814
- if (hasMetadata !== void 0) {
815
- const schemaVersion = this.db.prepare("SELECT value FROM byok_sqlite_meta WHERE key = 'schema_version'").get();
816
- if (schemaVersion?.value !== SQLITE_SCHEMA_VERSION) {
817
- throw new Error(
818
- `Unsupported BYOK SQLite schema version ${JSON.stringify(schemaVersion?.value)}; this build requires ${SQLITE_SCHEMA_VERSION}`
819
- );
983
+ this.db.exec("BEGIN IMMEDIATE");
984
+ try {
985
+ const hasMetadata = this.db.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'byok_sqlite_meta'").get();
986
+ if (hasMetadata !== void 0) {
987
+ 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.`);
990
+ }
991
+ for (const projection of [
992
+ "tenant_id, device_id, next_seq, delivered_seq, acked_seq, updated_at FROM mailbox_cursor",
993
+ "tenant_id, device_id, seq, message_id, body, body_hash, byte_size, state, appended_at FROM mailbox_message",
994
+ "tenant_id, task_id, device_id, agent_ref_json, owner_device_id, claimed_runtime, claimed_runtime_capabilities_json, status, terminal_cause, cancellation_requested_at, cancellation_reason, cancellation_message_id, updated_at FROM task_attempt",
995
+ "tenant_id, task_id, message_id, payload_body, terminal_body FROM agent_message_admission",
996
+ "tenant_id, hash, byte_size, content_type, state, created_at, updated_at, delete_pending_at FROM object_manifest",
997
+ "tenant_id, hash, ref_kind, ref_id, created_at FROM object_reference",
998
+ "blob_id, tenant_id, reservation_id, content_hash, byte_size, content_type, uploaded, data FROM blob"
999
+ ]) {
1000
+ this.db.prepare(`SELECT ${projection} LIMIT 0`);
1001
+ }
1002
+ if (version?.value !== SQLITE_SCHEMA_VERSION) {
1003
+ for (const table of ["task_attempt", "mailbox_message", "agent_message_admission"]) {
1004
+ 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");
1006
+ }
1007
+ }
1008
+ 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");
1010
+ }
1011
+ 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");
1013
+ }
1014
+ this.db.exec(RECEIPT_SCHEMA);
1015
+ }
1016
+ if (version?.value === "1") {
1017
+ 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");
1019
+ this.db.exec(DEVICE_SCHEMA);
1020
+ }
1021
+ if (version?.value !== SQLITE_SCHEMA_VERSION) {
1022
+ this.db.prepare("UPDATE byok_sqlite_meta SET value = ? WHERE key = 'schema_version'").run(SQLITE_SCHEMA_VERSION);
1023
+ }
1024
+ } else {
1025
+ 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");
1027
+ this.db.exec(SCHEMA);
1028
+ this.db.exec(RECEIPT_SCHEMA);
1029
+ this.db.exec(DEVICE_SCHEMA);
1030
+ this.db.prepare("INSERT INTO byok_sqlite_meta (key, value) VALUES ('schema_version', ?)").run(SQLITE_SCHEMA_VERSION);
820
1031
  }
821
- }
822
- this.db.exec(SCHEMA);
823
- if (hasMetadata === void 0) {
824
- this.db.prepare("INSERT INTO byok_sqlite_meta (key, value) VALUES ('schema_version', ?)").run(SQLITE_SCHEMA_VERSION);
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");
1033
+ const receiptColumns = this.db.prepare("PRAGMA table_info(request_receipt)").all();
1034
+ const expectedColumns = ["tenant_id", "key", "body", "recorded_at"];
1035
+ 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");
1037
+ }
1038
+ this.db.exec("COMMIT");
1039
+ } catch (error) {
1040
+ this.db.exec("ROLLBACK");
1041
+ throw error;
825
1042
  }
826
1043
  secureSqliteFilePermissions(path);
827
1044
  } catch (error) {
@@ -888,6 +1105,30 @@ function readTask(db, tenant, taskId) {
888
1105
  function sameAgentRef(left, right) {
889
1106
  return left?.agentId === right?.agentId && left?.profileRevision === right?.profileRevision;
890
1107
  }
1108
+ var SqliteRequestReceiptStore = class {
1109
+ constructor(coordinator, clock) {
1110
+ this.coordinator = coordinator;
1111
+ this.clock = clock;
1112
+ }
1113
+ coordinator;
1114
+ clock;
1115
+ record(tenant, input) {
1116
+ return this.coordinator.run((db) => {
1117
+ const result = db.prepare(
1118
+ `INSERT INTO request_receipt (tenant_id, key, body, recorded_at) VALUES (?, ?, ?, ?)
1119
+ ON CONFLICT (tenant_id, key) DO NOTHING`
1120
+ ).run(tenant, input.key, input.body, this.clock.now().toISOString());
1121
+ return { receipt: this.#read(db, tenant, input.key), created: Number(result.changes) === 1 };
1122
+ });
1123
+ }
1124
+ get(tenant, key) {
1125
+ return this.coordinator.run((db) => this.#read(db, tenant, key));
1126
+ }
1127
+ #read(db, tenant, key) {
1128
+ const row = db.prepare("SELECT body, recorded_at FROM request_receipt WHERE tenant_id = ? AND key = ?").get(tenant, key);
1129
+ return row === void 0 ? void 0 : { tenantId: tenant, key, body: row.body, recordedAt: row.recorded_at };
1130
+ }
1131
+ };
891
1132
  var SqliteTaskAttemptStore = class {
892
1133
  constructor(coordinator, clock) {
893
1134
  this.coordinator = coordinator;
@@ -933,6 +1174,20 @@ var SqliteTaskAttemptStore = class {
933
1174
  return "reserved";
934
1175
  });
935
1176
  }
1177
+ readTaskAgentMessage(tenant, input) {
1178
+ return this.coordinator.run((db) => {
1179
+ if (readTask(db, tenant, input.taskId)?.deviceId !== input.deviceId) return void 0;
1180
+ const row = db.prepare(
1181
+ `SELECT message_id, payload_body, terminal_body FROM agent_message_admission
1182
+ WHERE tenant_id = ? AND task_id = ?`
1183
+ ).get(tenant, input.taskId);
1184
+ return row === void 0 ? void 0 : {
1185
+ messageId: row.message_id,
1186
+ payloadBody: row.payload_body,
1187
+ ...row.terminal_body === null ? {} : { terminalBody: row.terminal_body }
1188
+ };
1189
+ });
1190
+ }
936
1191
  readAgentMessage(tenant, input) {
937
1192
  return this.coordinator.run((db) => {
938
1193
  const attempt = readTask(db, tenant, input.taskId);
@@ -1506,7 +1761,7 @@ var SqliteBlobContentProxy = class {
1506
1761
  }
1507
1762
  };
1508
1763
  function createSqliteEmbeddedStores(options, dependencies) {
1509
- const coordinator = new SqliteCoordinator(options.path);
1764
+ const coordinator = new SqliteCoordinator(options.path, options.migration);
1510
1765
  const objects = new SqliteObjectStore(coordinator, dependencies.clock);
1511
1766
  const mailbox = new SqliteMailboxStore(coordinator, dependencies.clock);
1512
1767
  const tasks = new SqliteTaskAttemptStore(coordinator, dependencies.clock);
@@ -1532,9 +1787,15 @@ function createSqliteEmbeddedStores(options, dependencies) {
1532
1787
  objects,
1533
1788
  mailbox
1534
1789
  );
1790
+ const devices = new SqliteDeviceDirectory(coordinator);
1791
+ const pairing = new InMemoryPairingCodeStore(dependencies.clock, devices);
1535
1792
  const cloud = {
1536
1793
  ...inMemoryCloud.stores,
1794
+ devices,
1795
+ pairingCodes: { issue: pairing.issue.bind(pairing) },
1796
+ pairing: { redeemAndRegister: pairing.redeemAndRegister.bind(pairing) },
1537
1797
  tasks,
1798
+ receipts: new SqliteRequestReceiptStore(coordinator, dependencies.clock),
1538
1799
  cancellations,
1539
1800
  blobs
1540
1801
  };
@@ -1555,6 +1816,7 @@ function composeFacadeStores(options) {
1555
1816
  const sqlite = options.storage?.kind === "sqlite" ? createSqliteEmbeddedStores(
1556
1817
  {
1557
1818
  path: options.storage.path,
1819
+ ...options.storage.migration === void 0 ? {} : { migration: options.storage.migration },
1558
1820
  ...options.storage.urlTtlMs === void 0 ? {} : { urlTtlMs: options.storage.urlTtlMs }
1559
1821
  },
1560
1822
  { clock, crypto }
@@ -1830,6 +2092,21 @@ function createByokServer(opts) {
1830
2092
  async function requireConnected(deviceId) {
1831
2093
  if (!connections.isConnected(deviceId)) throw new Error(`device ${deviceId} is not connected`);
1832
2094
  }
2095
+ const dispatching = /* @__PURE__ */ new Set();
2096
+ async function dispatchWithIdentity(input, fresh) {
2097
+ const submission = { ...input };
2098
+ const taskId = submission.taskId;
2099
+ if (taskId === void 0) return dispatchInternal(submission, fresh);
2100
+ if (typeof taskId !== "string" || taskId.length === 0) throw new Error("taskId must be a non-empty string");
2101
+ if (input.deviceId === void 0) throw new Error("caller taskId requires an explicit deviceId");
2102
+ if (dispatching.has(taskId)) throw new Error("dispatch for this taskId is already in progress; retain the original binding");
2103
+ dispatching.add(taskId);
2104
+ try {
2105
+ return await dispatchInternal(submission, fresh);
2106
+ } finally {
2107
+ dispatching.delete(taskId);
2108
+ }
2109
+ }
1833
2110
  async function dispatchInternal(input, freshAgentEgress) {
1834
2111
  const sessionRef = "sessionRef" in input ? input.sessionRef : void 0;
1835
2112
  const agentRef = input.agentRef === void 0 ? void 0 : AgentRefSchema.parse(input.agentRef);
@@ -1859,14 +2136,14 @@ function createByokServer(opts) {
1859
2136
  throw new Error("no connected device to dispatch to (M0 does not queue tasks until a device connects)");
1860
2137
  }
1861
2138
  await requireConnected(deviceId);
1862
- const capabilities = await deviceCapabilities(deviceId);
1863
- if (dispatchSelection !== void 0 && !(capabilities?.includes("dispatch-selection") ?? false)) {
2139
+ const capabilities2 = await deviceCapabilities(deviceId);
2140
+ if (dispatchSelection !== void 0 && !(capabilities2?.includes("dispatch-selection") ?? false)) {
1864
2141
  throw new Error(
1865
2142
  `device ${deviceId} did not advertise dispatch-selection capability; refusing authoritative provider/model dispatch`
1866
2143
  );
1867
2144
  }
1868
2145
  if (requiredToolsets !== void 0) {
1869
- if (!(capabilities?.includes("toolset-selection") ?? false)) {
2146
+ if (!(capabilities2?.includes("toolset-selection") ?? false)) {
1870
2147
  throw new Error(
1871
2148
  `device ${deviceId} did not advertise toolset-selection capability; refusing a task whose semantics require local MCP tools`
1872
2149
  );
@@ -1888,7 +2165,7 @@ function createByokServer(opts) {
1888
2165
  `dispatch runtime ${input.runtime} does not match dispatchSelection.runtimeId ${dispatchSelection.runtimeId}`
1889
2166
  );
1890
2167
  }
1891
- if (agentRef === void 0 && capabilities?.includes(STRICT_AGENT_ONLY_CAPABILITY) === true) {
2168
+ if (agentRef === void 0 && capabilities2?.includes(STRICT_AGENT_ONLY_CAPABILITY) === true) {
1892
2169
  throw new Error(
1893
2170
  `device ${deviceId} advertises strict-agent-only; legacy task dispatch is refused before enqueue`
1894
2171
  );
@@ -1902,7 +2179,7 @@ function createByokServer(opts) {
1902
2179
  ...runtime === void 0 ? {} : { runtime },
1903
2180
  ...dispatchSelection === void 0 ? {} : { dispatchSelection }
1904
2181
  };
1905
- const taskId = globalThis.crypto.randomUUID();
2182
+ const taskId = input.taskId ?? globalThis.crypto.randomUUID();
1906
2183
  relay.provision(taskId);
1907
2184
  let enqueued;
1908
2185
  try {
@@ -2006,7 +2283,7 @@ function createByokServer(opts) {
2006
2283
  }
2007
2284
  },
2008
2285
  dispatch(input) {
2009
- return dispatchInternal(input, false);
2286
+ return dispatchWithIdentity(input, false);
2010
2287
  },
2011
2288
  dispatchFreshAgentEgress(input) {
2012
2289
  if (Object.prototype.hasOwnProperty.call(input, "sessionRef")) {
@@ -2018,7 +2295,7 @@ function createByokServer(opts) {
2018
2295
  if (input.agentRef === void 0 || input.egressPolicy === void 0) {
2019
2296
  throw new Error("fresh Agent egress dispatch requires exact AgentRef and egress policy");
2020
2297
  }
2021
- return dispatchInternal(input, true);
2298
+ return dispatchWithIdentity(input, true);
2022
2299
  },
2023
2300
  async requestAgentContentRead(input) {
2024
2301
  const payload = AgentContentReadPayloadSchema.parse(input.payload);
@@ -2048,7 +2325,31 @@ function createByokServer(opts) {
2048
2325
  projectionHash: desired.projectionHash
2049
2326
  });
2050
2327
  },
2328
+ recurring: { submit(input) {
2329
+ return cloud.submitRecurringExecution(tenant, input);
2330
+ } },
2051
2331
  tasks: {
2332
+ attempt(taskId) {
2333
+ return cloud.readTaskAttempt(tenant, taskId);
2334
+ },
2335
+ deviceTerminal(taskId) {
2336
+ return cloud.readDeviceTerminal(tenant, taskId);
2337
+ },
2338
+ offer(taskId) {
2339
+ return cloud.readTaskOffer(tenant, taskId);
2340
+ },
2341
+ agentMessage(taskId, deviceId, agentRef) {
2342
+ return cloud.readTaskAgentMessage(tenant, deviceId, taskId, agentRef);
2343
+ },
2344
+ messageDisposition(taskId, deviceId, payload) {
2345
+ return cloud.readAgentMessageDisposition(tenant, deviceId, taskId, payload);
2346
+ },
2347
+ async cancel(taskId, reason) {
2348
+ const attempt = await cloud.cancelTask(tenant, taskId, reason);
2349
+ if (attempt.cancellation !== void 0) {
2350
+ relay.noteHostTransition(taskId, "Cancelled", attempt.cancellation.requestedAt);
2351
+ }
2352
+ },
2052
2353
  async get(taskId) {
2053
2354
  const attempt = await cloud.readTaskAttempt(tenant, taskId);
2054
2355
  return attempt === void 0 ? void 0 : projectTask(attempt);