@byok-sdk/server 0.15.0 → 0.17.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 +77 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +296 -24
- package/dist/index.js.map +1 -1
- package/dist/stores/sqlite/device-directory.d.ts +26 -0
- package/dist/stores/sqlite/index.d.ts +3 -2
- package/dist/types.d.ts +4 -0
- package/package.json +4 -4
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
|
@@ -4,6 +4,7 @@ import { type AgentRef } 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
|
|
@@ -92,6 +93,10 @@ export interface ByokServer {
|
|
|
92
93
|
readAgentHomeProjection(deviceId: string, agentRef: AgentRef, requestId: string): Promise<AgentHomeProjectionStatusReadback | undefined>;
|
|
93
94
|
tasks: {
|
|
94
95
|
get(taskId: string): Promise<TaskSnapshot | undefined>;
|
|
96
|
+
/** Immutable kernel offer readback for verifying a persisted host binding. No transport seq is inferred. */
|
|
97
|
+
offer(taskId: string): Promise<import('@byok-sdk/cloud').TaskOfferReadback | undefined>;
|
|
98
|
+
/** Request cancellation through the kernel without a process-owned TaskHandle. */
|
|
99
|
+
cancel(taskId: string, reason?: string): Promise<void>;
|
|
95
100
|
/**
|
|
96
101
|
* One bounded page, keyset-paged by task id. Paged rather than "all of
|
|
97
102
|
* them" because the underlying store is: an unbounded `list()` would have
|
|
@@ -194,7 +199,7 @@ export interface ByokServer {
|
|
|
194
199
|
* one, an in-process notification relay, and the observability an embedder used
|
|
195
200
|
* to get from the hub.
|
|
196
201
|
*
|
|
197
|
-
* State is in-memory by default. Explicit SQLite mode persists the six
|
|
202
|
+
* State is in-memory by default. Explicit SQLite mode persists request receipts, the device directory and six
|
|
198
203
|
* coordination interfaces whose contracts cross a process restart; all other
|
|
199
204
|
* ports remain process-local.
|
|
200
205
|
*/
|
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 = "
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
const
|
|
816
|
-
if (
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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;
|
|
@@ -1506,7 +1747,7 @@ var SqliteBlobContentProxy = class {
|
|
|
1506
1747
|
}
|
|
1507
1748
|
};
|
|
1508
1749
|
function createSqliteEmbeddedStores(options, dependencies) {
|
|
1509
|
-
const coordinator = new SqliteCoordinator(options.path);
|
|
1750
|
+
const coordinator = new SqliteCoordinator(options.path, options.migration);
|
|
1510
1751
|
const objects = new SqliteObjectStore(coordinator, dependencies.clock);
|
|
1511
1752
|
const mailbox = new SqliteMailboxStore(coordinator, dependencies.clock);
|
|
1512
1753
|
const tasks = new SqliteTaskAttemptStore(coordinator, dependencies.clock);
|
|
@@ -1532,9 +1773,15 @@ function createSqliteEmbeddedStores(options, dependencies) {
|
|
|
1532
1773
|
objects,
|
|
1533
1774
|
mailbox
|
|
1534
1775
|
);
|
|
1776
|
+
const devices = new SqliteDeviceDirectory(coordinator);
|
|
1777
|
+
const pairing = new InMemoryPairingCodeStore(dependencies.clock, devices);
|
|
1535
1778
|
const cloud = {
|
|
1536
1779
|
...inMemoryCloud.stores,
|
|
1780
|
+
devices,
|
|
1781
|
+
pairingCodes: { issue: pairing.issue.bind(pairing) },
|
|
1782
|
+
pairing: { redeemAndRegister: pairing.redeemAndRegister.bind(pairing) },
|
|
1537
1783
|
tasks,
|
|
1784
|
+
receipts: new SqliteRequestReceiptStore(coordinator, dependencies.clock),
|
|
1538
1785
|
cancellations,
|
|
1539
1786
|
blobs
|
|
1540
1787
|
};
|
|
@@ -1555,6 +1802,7 @@ function composeFacadeStores(options) {
|
|
|
1555
1802
|
const sqlite = options.storage?.kind === "sqlite" ? createSqliteEmbeddedStores(
|
|
1556
1803
|
{
|
|
1557
1804
|
path: options.storage.path,
|
|
1805
|
+
...options.storage.migration === void 0 ? {} : { migration: options.storage.migration },
|
|
1558
1806
|
...options.storage.urlTtlMs === void 0 ? {} : { urlTtlMs: options.storage.urlTtlMs }
|
|
1559
1807
|
},
|
|
1560
1808
|
{ clock, crypto }
|
|
@@ -1830,6 +2078,21 @@ function createByokServer(opts) {
|
|
|
1830
2078
|
async function requireConnected(deviceId) {
|
|
1831
2079
|
if (!connections.isConnected(deviceId)) throw new Error(`device ${deviceId} is not connected`);
|
|
1832
2080
|
}
|
|
2081
|
+
const dispatching = /* @__PURE__ */ new Set();
|
|
2082
|
+
async function dispatchWithIdentity(input, fresh) {
|
|
2083
|
+
const submission = { ...input };
|
|
2084
|
+
const taskId = submission.taskId;
|
|
2085
|
+
if (taskId === void 0) return dispatchInternal(submission, fresh);
|
|
2086
|
+
if (typeof taskId !== "string" || taskId.length === 0) throw new Error("taskId must be a non-empty string");
|
|
2087
|
+
if (input.deviceId === void 0) throw new Error("caller taskId requires an explicit deviceId");
|
|
2088
|
+
if (dispatching.has(taskId)) throw new Error("dispatch for this taskId is already in progress; retain the original binding");
|
|
2089
|
+
dispatching.add(taskId);
|
|
2090
|
+
try {
|
|
2091
|
+
return await dispatchInternal(submission, fresh);
|
|
2092
|
+
} finally {
|
|
2093
|
+
dispatching.delete(taskId);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
1833
2096
|
async function dispatchInternal(input, freshAgentEgress) {
|
|
1834
2097
|
const sessionRef = "sessionRef" in input ? input.sessionRef : void 0;
|
|
1835
2098
|
const agentRef = input.agentRef === void 0 ? void 0 : AgentRefSchema.parse(input.agentRef);
|
|
@@ -1859,14 +2122,14 @@ function createByokServer(opts) {
|
|
|
1859
2122
|
throw new Error("no connected device to dispatch to (M0 does not queue tasks until a device connects)");
|
|
1860
2123
|
}
|
|
1861
2124
|
await requireConnected(deviceId);
|
|
1862
|
-
const
|
|
1863
|
-
if (dispatchSelection !== void 0 && !(
|
|
2125
|
+
const capabilities2 = await deviceCapabilities(deviceId);
|
|
2126
|
+
if (dispatchSelection !== void 0 && !(capabilities2?.includes("dispatch-selection") ?? false)) {
|
|
1864
2127
|
throw new Error(
|
|
1865
2128
|
`device ${deviceId} did not advertise dispatch-selection capability; refusing authoritative provider/model dispatch`
|
|
1866
2129
|
);
|
|
1867
2130
|
}
|
|
1868
2131
|
if (requiredToolsets !== void 0) {
|
|
1869
|
-
if (!(
|
|
2132
|
+
if (!(capabilities2?.includes("toolset-selection") ?? false)) {
|
|
1870
2133
|
throw new Error(
|
|
1871
2134
|
`device ${deviceId} did not advertise toolset-selection capability; refusing a task whose semantics require local MCP tools`
|
|
1872
2135
|
);
|
|
@@ -1888,7 +2151,7 @@ function createByokServer(opts) {
|
|
|
1888
2151
|
`dispatch runtime ${input.runtime} does not match dispatchSelection.runtimeId ${dispatchSelection.runtimeId}`
|
|
1889
2152
|
);
|
|
1890
2153
|
}
|
|
1891
|
-
if (agentRef === void 0 &&
|
|
2154
|
+
if (agentRef === void 0 && capabilities2?.includes(STRICT_AGENT_ONLY_CAPABILITY) === true) {
|
|
1892
2155
|
throw new Error(
|
|
1893
2156
|
`device ${deviceId} advertises strict-agent-only; legacy task dispatch is refused before enqueue`
|
|
1894
2157
|
);
|
|
@@ -1902,7 +2165,7 @@ function createByokServer(opts) {
|
|
|
1902
2165
|
...runtime === void 0 ? {} : { runtime },
|
|
1903
2166
|
...dispatchSelection === void 0 ? {} : { dispatchSelection }
|
|
1904
2167
|
};
|
|
1905
|
-
const taskId = globalThis.crypto.randomUUID();
|
|
2168
|
+
const taskId = input.taskId ?? globalThis.crypto.randomUUID();
|
|
1906
2169
|
relay.provision(taskId);
|
|
1907
2170
|
let enqueued;
|
|
1908
2171
|
try {
|
|
@@ -2006,7 +2269,7 @@ function createByokServer(opts) {
|
|
|
2006
2269
|
}
|
|
2007
2270
|
},
|
|
2008
2271
|
dispatch(input) {
|
|
2009
|
-
return
|
|
2272
|
+
return dispatchWithIdentity(input, false);
|
|
2010
2273
|
},
|
|
2011
2274
|
dispatchFreshAgentEgress(input) {
|
|
2012
2275
|
if (Object.prototype.hasOwnProperty.call(input, "sessionRef")) {
|
|
@@ -2018,7 +2281,7 @@ function createByokServer(opts) {
|
|
|
2018
2281
|
if (input.agentRef === void 0 || input.egressPolicy === void 0) {
|
|
2019
2282
|
throw new Error("fresh Agent egress dispatch requires exact AgentRef and egress policy");
|
|
2020
2283
|
}
|
|
2021
|
-
return
|
|
2284
|
+
return dispatchWithIdentity(input, true);
|
|
2022
2285
|
},
|
|
2023
2286
|
async requestAgentContentRead(input) {
|
|
2024
2287
|
const payload = AgentContentReadPayloadSchema.parse(input.payload);
|
|
@@ -2049,6 +2312,15 @@ function createByokServer(opts) {
|
|
|
2049
2312
|
});
|
|
2050
2313
|
},
|
|
2051
2314
|
tasks: {
|
|
2315
|
+
offer(taskId) {
|
|
2316
|
+
return cloud.readTaskOffer(tenant, taskId);
|
|
2317
|
+
},
|
|
2318
|
+
async cancel(taskId, reason) {
|
|
2319
|
+
const attempt = await cloud.cancelTask(tenant, taskId, reason);
|
|
2320
|
+
if (attempt.cancellation !== void 0) {
|
|
2321
|
+
relay.noteHostTransition(taskId, "Cancelled", attempt.cancellation.requestedAt);
|
|
2322
|
+
}
|
|
2323
|
+
},
|
|
2052
2324
|
async get(taskId) {
|
|
2053
2325
|
const attempt = await cloud.readTaskAttempt(tenant, taskId);
|
|
2054
2326
|
return attempt === void 0 ? void 0 : projectTask(attempt);
|