@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 +77 -0
- package/dist/index.d.ts +20 -3
- package/dist/index.js +325 -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 +7 -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
|
@@ -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 = "
|
|
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;
|
|
@@ -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
|
|
1863
|
-
if (dispatchSelection !== void 0 && !(
|
|
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 (!(
|
|
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 &&
|
|
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
|
|
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
|
|
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);
|