@byok-sdk/cloud 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cloud.d.ts +53 -8
- package/dist/errors.d.ts +12 -0
- package/dist/handlers/messages.d.ts +11 -0
- package/dist/inbound.d.ts +4 -3
- package/dist/index.d.ts +5 -5
- package/dist/index.js +779 -89
- package/dist/index.js.map +1 -1
- package/dist/stores/in-memory/agent-egress.d.ts +12 -0
- package/dist/stores/in-memory/blobs.d.ts +8 -2
- package/dist/stores/in-memory/device-directory.d.ts +6 -1
- package/dist/stores/in-memory/index.d.ts +3 -1
- package/dist/stores/in-memory/task-attempts.d.ts +27 -6
- package/dist/stores/in-memory/task-cancellations.d.ts +9 -0
- package/dist/stores/ports.d.ts +116 -5
- package/dist/tenant-stores.d.ts +31 -2
- package/dist/terminal-result.d.ts +11 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BOARD_STATUSES,
|
|
1
|
+
import { BOARD_STATUSES, CapabilityDeclarationSchema, NONCE_SIGNING_DOMAIN, hasCapability, isTenantId, tenantId, principalTenant, parseDeviceProofEnvelope, deviceProofSigningInput, contentHash, parseCapabilityDeclaration, ByokCoreError, tenantKey, PRESENCE_LEVELS, createInMemoryCoreStores, authenticateDeviceAssertion, assertCapability, isCoreConflictError, isCoreError, DEVICE_PROOF_HEADER, TRUTH_RECORD_KINDS, STORAGE_ERROR_CODES, STORAGE_ERROR_HTTP_STATUS } from '@byok-sdk/core';
|
|
2
2
|
export { DEVICE_PROOF_HEADER, NONCE_SIGNING_DOMAIN, isTenantId, tenantId } from '@byok-sdk/core';
|
|
3
|
-
import { AgentEventOrUnknownSchema,
|
|
3
|
+
import { AgentEventOrUnknownSchema, PROTOCOL_VERSION, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, encodeEnvelope, decodeEnvelope, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, BYOK_CAPABILITIES_PATH, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, BYOK_BOARD_PATH, BYOK_BOARD_CLAIM_ROUTE, BYOK_BOARD_UNCLAIM_ROUTE, BYOK_BOARD_STATUS_ROUTE, BYOK_BOARD_STREAM_PATH, BYOK_PRESENCE_PATH, BYOK_ACTIVITY_PATH, BYOK_RECORDS_PATH, BYOK_RECORD_ROUTE, BYOK_SKILL_PACKS_PATH, BYOK_SKILL_PACK_FILE_ROUTE, BYOK_BLOBS_PATH, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, createEnvelope, AgentContentReadPayloadSchema, AGENT_EGRESS_POLICY_CAPABILITY, TaskOfferForAgentWithEgressPayloadSchema, TaskOfferForAgentPayloadSchema, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, PairRequestSchema, ChallengeRequestSchema, TokenRequestSchema, MessagesSendRequestSchema, PresencePublishRequestSchema, CreateBlobRequestSchema, byokBlobContentPath, AgentEgressAckPayloadSchema, AgentContentReceiptPayloadSchema } from '@byok-sdk/protocol';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { Hono } from 'hono';
|
|
6
6
|
|
|
@@ -160,8 +160,8 @@ function createHmacTokenSigner(secret, clock) {
|
|
|
160
160
|
}
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
|
-
function verifyNonceSignature(
|
|
164
|
-
return
|
|
163
|
+
function verifyNonceSignature(crypto2, devicePublicKey, nonce, signature) {
|
|
164
|
+
return crypto2.verifyEd25519(devicePublicKey, NONCE_SIGNING_DOMAIN + nonce, signature);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
// src/auth/plane.ts
|
|
@@ -170,13 +170,13 @@ var PAIRING_CODE_LENGTH = 8;
|
|
|
170
170
|
var DEVICE_IDENTITY_PROOF_KEY_ID = "identity";
|
|
171
171
|
var DEVICE_IDENTITY_PROOF_KEY_EPOCH = 0;
|
|
172
172
|
function createAuthPlane(deps) {
|
|
173
|
-
const { stores, crypto, clock, tokenSigner } = deps;
|
|
173
|
+
const { stores, crypto: crypto2, clock, tokenSigner } = deps;
|
|
174
174
|
const ttlSeconds = deps.accessTokenTtlSeconds ?? ACCESS_TOKEN_TTL_SECONDS;
|
|
175
175
|
return {
|
|
176
176
|
async createPairingCode(tenant, input) {
|
|
177
177
|
const expiresAt = new Date(clock.now().getTime() + (input.ttlMs ?? PAIRING_CODE_TTL_MS)).toISOString();
|
|
178
178
|
return stores.pairingCodes.issue(tenant, {
|
|
179
|
-
code:
|
|
179
|
+
code: crypto2.randomPairingCode(PAIRING_CODE_LENGTH),
|
|
180
180
|
productId: input.productId,
|
|
181
181
|
expiresAt
|
|
182
182
|
});
|
|
@@ -186,7 +186,7 @@ function createAuthPlane(deps) {
|
|
|
186
186
|
if (claims === void 0) return void 0;
|
|
187
187
|
return stores.devices.register(claims.tenantId, {
|
|
188
188
|
productId: claims.productId,
|
|
189
|
-
deviceId: `dev_${
|
|
189
|
+
deviceId: `dev_${crypto2.randomUuid()}`,
|
|
190
190
|
deviceName: input.deviceName,
|
|
191
191
|
devicePublicKey: input.devicePublicKey,
|
|
192
192
|
proofKeyId: DEVICE_IDENTITY_PROOF_KEY_ID,
|
|
@@ -208,7 +208,7 @@ function createAuthPlane(deps) {
|
|
|
208
208
|
return stores.nonces.markUsed(device.tenantId, nonce);
|
|
209
209
|
},
|
|
210
210
|
verifySignature(device, nonce, signature) {
|
|
211
|
-
return verifyNonceSignature(
|
|
211
|
+
return verifyNonceSignature(crypto2, device.devicePublicKey, nonce, signature);
|
|
212
212
|
},
|
|
213
213
|
async mintAccessToken(device) {
|
|
214
214
|
const accessToken = await tokenSigner.sign(
|
|
@@ -317,7 +317,19 @@ var CLOUD_ERROR_CODES = {
|
|
|
317
317
|
*/
|
|
318
318
|
terminal_receipt_unreadable: "terminal_receipt_unreadable",
|
|
319
319
|
/** A progress/activity batch exceeded the configured event or byte ceiling. */
|
|
320
|
-
activity_batch_too_large: "activity_batch_too_large"
|
|
320
|
+
activity_batch_too_large: "activity_batch_too_large",
|
|
321
|
+
/** Host control-plane task lookup is tenant-closed and found no task. */
|
|
322
|
+
task_not_found: "task_not_found",
|
|
323
|
+
/** Durable target-device capability is absent, revoked, or unavailable. */
|
|
324
|
+
agent_capability_missing: "agent_capability_missing",
|
|
325
|
+
/** Inbound Agent identity did not exactly match the offered identity. */
|
|
326
|
+
agent_ref_mismatch: "agent_ref_mismatch",
|
|
327
|
+
/** A strict Agent task id already names a durable attempt and cannot be re-enqueued. */
|
|
328
|
+
agent_task_already_exists: "agent_task_already_exists",
|
|
329
|
+
/** A first-write-wins Agent control record was replayed with a different body. */
|
|
330
|
+
agent_content_request_mismatch: "agent_content_request_mismatch",
|
|
331
|
+
/** A durable mailbox receipt id resolved to an envelope other than its exact acknowledgement. */
|
|
332
|
+
mailbox_receipt_mismatch: "mailbox_receipt_mismatch"
|
|
321
333
|
};
|
|
322
334
|
var ByokCloudError = class extends Error {
|
|
323
335
|
code;
|
|
@@ -524,14 +536,24 @@ function tenantStoresFor(principal, root) {
|
|
|
524
536
|
devices: {
|
|
525
537
|
get: (deviceId) => cloud.devices.get(tenant, deviceId),
|
|
526
538
|
list: () => cloud.devices.list(tenant),
|
|
527
|
-
revoke: (deviceId) => cloud.devices.revoke(tenant, deviceId)
|
|
539
|
+
revoke: (deviceId) => cloud.devices.revoke(tenant, deviceId),
|
|
540
|
+
recordCapabilities: (input) => principal.kind === "device" ? cloud.devices.recordCapabilities(tenant, {
|
|
541
|
+
deviceId: principal.deviceId,
|
|
542
|
+
capabilities: input.capabilities
|
|
543
|
+
}) : Promise.resolve(void 0),
|
|
544
|
+
readiness: () => cloud.devices.readiness(tenant, core.presence)
|
|
528
545
|
},
|
|
529
546
|
tasks: {
|
|
530
547
|
open: (input) => cloud.tasks.open(tenant, input),
|
|
548
|
+
reserveAgentOffer: (input) => cloud.tasks.reserveAgentOffer(tenant, input),
|
|
531
549
|
get: (taskId) => cloud.tasks.get(tenant, taskId),
|
|
550
|
+
getMany: (taskIds) => cloud.tasks.getMany(tenant, taskIds),
|
|
532
551
|
claim: (input) => cloud.tasks.claim(tenant, input),
|
|
533
552
|
recordStatus: (input) => cloud.tasks.recordStatus(tenant, input)
|
|
534
553
|
},
|
|
554
|
+
cancellations: {
|
|
555
|
+
request: (input) => cloud.cancellations.request(tenant, input)
|
|
556
|
+
},
|
|
535
557
|
dedup: {
|
|
536
558
|
checkAndRecord: (deviceId, envelopeId) => cloud.dedup.checkAndRecord(tenant, deviceId, envelopeId)
|
|
537
559
|
},
|
|
@@ -539,6 +561,10 @@ function tenantStoresFor(principal, root) {
|
|
|
539
561
|
record: (input) => cloud.receipts.record(tenant, input),
|
|
540
562
|
get: (key) => cloud.receipts.get(tenant, key)
|
|
541
563
|
},
|
|
564
|
+
egress: {
|
|
565
|
+
record: (input) => cloud.egress.record(tenant, input),
|
|
566
|
+
get: (deviceId, eventId) => cloud.egress.get(tenant, deviceId, eventId)
|
|
567
|
+
},
|
|
542
568
|
blobs: {
|
|
543
569
|
createUpload: (reservation) => cloud.blobs.createUpload(tenant, reservation),
|
|
544
570
|
observeUpload: (blobId, reservation) => cloud.blobs.observeUpload(tenant, blobId, reservation),
|
|
@@ -678,11 +704,16 @@ function blobDownloadContentHandler(deps) {
|
|
|
678
704
|
if (!query.success || !await deps.contentProxy.verifySignedUrl(blobId, "get", query.data.sig, query.data.exp)) {
|
|
679
705
|
return c.json({ error: "invalid or expired signature" }, 401);
|
|
680
706
|
}
|
|
681
|
-
const
|
|
682
|
-
if (
|
|
683
|
-
|
|
707
|
+
const result = await deps.contentProxy.readContent(blobId);
|
|
708
|
+
if (result === void 0) return c.json({ error: "blob not found" }, 404);
|
|
709
|
+
if (!result.ok) return c.json({ error: result.code }, BLOB_READ_ERROR_HTTP_STATUS[result.code]);
|
|
710
|
+
return c.body(new Uint8Array(result.content.data), 200, { "content-type": result.content.contentType });
|
|
684
711
|
};
|
|
685
712
|
}
|
|
713
|
+
var BLOB_READ_ERROR_HTTP_STATUS = {
|
|
714
|
+
blob_upstream_unavailable: 502,
|
|
715
|
+
blob_upstream_stream_interrupted: 502
|
|
716
|
+
};
|
|
686
717
|
function idempotencyKey(c) {
|
|
687
718
|
const value = c.req.header("Idempotency-Key");
|
|
688
719
|
if (value === void 0 || value.length === 0 || value.length > 200) return void 0;
|
|
@@ -752,7 +783,15 @@ function tokenHandler(deps) {
|
|
|
752
783
|
return c.json(response, 200);
|
|
753
784
|
};
|
|
754
785
|
}
|
|
755
|
-
var CLOUD_PROTOCOL_CAPABILITIES = [
|
|
786
|
+
var CLOUD_PROTOCOL_CAPABILITIES = [
|
|
787
|
+
"result-document",
|
|
788
|
+
"agent-home-contract",
|
|
789
|
+
"agent-egress-policy",
|
|
790
|
+
"agent-egress-reliable-ack",
|
|
791
|
+
"agent-content-workspace-read",
|
|
792
|
+
"agent-content-transcript-read",
|
|
793
|
+
"agent-content-artifact-read"
|
|
794
|
+
];
|
|
756
795
|
function sleep(ms) {
|
|
757
796
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
758
797
|
}
|
|
@@ -774,13 +813,32 @@ function eventsHandler(deps) {
|
|
|
774
813
|
}
|
|
775
814
|
const attempts = Math.max(1, Math.ceil(deps.longPollHoldMs / deps.longPollIntervalMs));
|
|
776
815
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
816
|
+
let scanCursor = cursor;
|
|
817
|
+
while (true) {
|
|
818
|
+
const page = await stores.mailbox.readAfter({
|
|
819
|
+
deviceId: device.deviceId,
|
|
820
|
+
afterSeq: scanCursor,
|
|
821
|
+
limit: deps.pageLimit
|
|
822
|
+
});
|
|
823
|
+
if (page.messages.length === 0) break;
|
|
824
|
+
const decoded = page.messages.map((message) => decodeEnvelope(message.body));
|
|
825
|
+
const offeredTaskIds = decoded.flatMap(
|
|
826
|
+
(event) => (event.type === "task.offer" || event.type === "task.offer_with_toolsets" || event.type === "task.offer_for_agent" || event.type === "task.offer_for_agent_with_egress") && event.task_id !== void 0 ? [event.task_id] : []
|
|
827
|
+
);
|
|
828
|
+
const attemptsByTaskId = Object.fromEntries(
|
|
829
|
+
(await stores.tasks.getMany(offeredTaskIds)).map((attempt2) => [attempt2.taskId, attempt2])
|
|
830
|
+
);
|
|
831
|
+
const events = decoded.filter((event) => {
|
|
832
|
+
if (event.type !== "task.offer" && event.type !== "task.offer_with_toolsets" && event.type !== "task.offer_for_agent" && event.type !== "task.offer_for_agent_with_egress") {
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
return event.task_id === void 0 || attemptsByTaskId[event.task_id]?.cancellation === void 0;
|
|
836
|
+
});
|
|
837
|
+
if (events.length === 0 && page.hasMore) {
|
|
838
|
+
scanCursor = page.nextSeq;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (events.length === 0) break;
|
|
784
842
|
const response2 = {
|
|
785
843
|
events,
|
|
786
844
|
cursor: page.nextSeq,
|
|
@@ -886,13 +944,103 @@ async function projectTerminalToReview(board, taskId) {
|
|
|
886
944
|
function terminalReceiptKey(taskId) {
|
|
887
945
|
return `task:${taskId}:terminal`;
|
|
888
946
|
}
|
|
947
|
+
function sameAgentRef(expected, actual) {
|
|
948
|
+
return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
|
|
949
|
+
}
|
|
950
|
+
function sameReliableEgress(expected, actual) {
|
|
951
|
+
return sameAgentRef(expected.agentRef, actual.agentRef) && expected.sessionRef === actual.sessionRef && expected.policyRevision === actual.policyRevision && expected.eventId === actual.eventId && expected.cursor === actual.cursor && expected.contentHash === actual.contentHash && expected.byteCount === actual.byteCount && JSON.stringify(expected.payload) === JSON.stringify(actual.payload);
|
|
952
|
+
}
|
|
953
|
+
function contentReadCapability(surface) {
|
|
954
|
+
switch (surface) {
|
|
955
|
+
case "workspace":
|
|
956
|
+
return AGENT_CONTENT_WORKSPACE_READ_CAPABILITY;
|
|
957
|
+
case "transcript":
|
|
958
|
+
return AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY;
|
|
959
|
+
case "artifact":
|
|
960
|
+
return AGENT_CONTENT_ARTIFACT_READ_CAPABILITY;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function hasCapabilities(capabilities, required) {
|
|
964
|
+
return capabilities !== void 0 && required.every((capability) => capabilities.includes(capability));
|
|
965
|
+
}
|
|
966
|
+
function contentRequestReceiptKey(deviceId, requestId) {
|
|
967
|
+
return `agent-content-request:${deviceId}:${requestId}`;
|
|
968
|
+
}
|
|
969
|
+
function contentReceiptKey(deviceId, requestId) {
|
|
970
|
+
return `agent-content:${deviceId}:${requestId}`;
|
|
971
|
+
}
|
|
972
|
+
function matchesContentReadReceipt(request, receipt) {
|
|
973
|
+
return request.requestId === receipt.requestId && request.surface === receipt.surface && request.actor.kind === receipt.actor.kind && request.actor.id === receipt.actor.id && sameAgentRef(request.agentRef, receipt.agentRef) && request.sessionRef === receipt.sessionRef && request.runtime === receipt.runtime && request.cwd === receipt.cwd && request.policyRevision === receipt.policyRevision && request.target === receipt.target && request.mimeType === receipt.mimeType && request.decodeAs === receipt.decodeAs;
|
|
974
|
+
}
|
|
975
|
+
async function readContentRequest(stores, deviceId, requestId) {
|
|
976
|
+
const stored = await stores.receipts.get(contentRequestReceiptKey(deviceId, requestId));
|
|
977
|
+
if (stored === void 0) return void 0;
|
|
978
|
+
try {
|
|
979
|
+
return AgentContentReadPayloadSchema.parse(JSON.parse(stored.body));
|
|
980
|
+
} catch {
|
|
981
|
+
return void 0;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
function inboundAgentRef(envelope) {
|
|
985
|
+
switch (envelope.type) {
|
|
986
|
+
case "task.claim":
|
|
987
|
+
case "task.decline":
|
|
988
|
+
case "task.complete":
|
|
989
|
+
case "task.fail":
|
|
990
|
+
case "task.cancelled":
|
|
991
|
+
return envelope.payload.agentRef;
|
|
992
|
+
default:
|
|
993
|
+
return void 0;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
function agentRefEchoRequired(envelope) {
|
|
997
|
+
return envelope.type === "task.claim" || envelope.type === "task.decline" || envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled";
|
|
998
|
+
}
|
|
889
999
|
async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS) {
|
|
890
1000
|
if (!await stores.rateLimiter.consume(deviceId)) return "rate_limited";
|
|
1001
|
+
if (envelope.type === "conn.hello") {
|
|
1002
|
+
if (envelope.payload.deviceId !== deviceId) return "rejected";
|
|
1003
|
+
if (!envelope.payload.protocolVersions.includes(PROTOCOL_VERSION)) return "rejected";
|
|
1004
|
+
const device = await stores.devices.get(deviceId);
|
|
1005
|
+
if (device === void 0 || device.revoked || device.productId !== envelope.payload.productId) {
|
|
1006
|
+
return "rejected";
|
|
1007
|
+
}
|
|
1008
|
+
if (await stores.dedup.checkAndRecord(deviceId, envelope.id)) return "duplicate";
|
|
1009
|
+
return await stores.devices.recordCapabilities({ capabilities: envelope.payload.capabilities }) === void 0 ? "rejected" : "accepted";
|
|
1010
|
+
}
|
|
891
1011
|
if (!DAEMON_TO_SERVER_TYPES.includes(envelope.type)) return "rejected";
|
|
1012
|
+
if (envelope.type === "agent.egress.reliable") {
|
|
1013
|
+
const device = await stores.devices.get(deviceId);
|
|
1014
|
+
if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
|
|
1015
|
+
return "rejected";
|
|
1016
|
+
}
|
|
1017
|
+
const outcome = await stores.egress.record({
|
|
1018
|
+
deviceId,
|
|
1019
|
+
payload: envelope.payload,
|
|
1020
|
+
receiptId: crypto.randomUUID()
|
|
1021
|
+
});
|
|
1022
|
+
return outcome.created || sameReliableEgress(outcome.record.payload, envelope.payload) ? outcome.created ? "accepted" : "duplicate" : "rejected";
|
|
1023
|
+
}
|
|
1024
|
+
if (envelope.type === "agent.content.receipt") {
|
|
1025
|
+
const device = await stores.devices.get(deviceId);
|
|
1026
|
+
if (device === void 0 || device.revoked || envelope.payload.eventId !== envelope.payload.requestId || !hasCapabilities(device.capabilities, [contentReadCapability(envelope.payload.surface), AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
|
|
1027
|
+
return "rejected";
|
|
1028
|
+
}
|
|
1029
|
+
const request = await readContentRequest(stores, deviceId, envelope.payload.requestId);
|
|
1030
|
+
if (request === void 0 || !matchesContentReadReceipt(request, envelope.payload)) return "rejected";
|
|
1031
|
+
const key = contentReceiptKey(deviceId, envelope.payload.requestId);
|
|
1032
|
+
const body = JSON.stringify(envelope.payload);
|
|
1033
|
+
const receipt = await stores.receipts.record({ key, body });
|
|
1034
|
+
return receipt.created ? "accepted" : receipt.receipt.body === body ? "duplicate" : "rejected";
|
|
1035
|
+
}
|
|
892
1036
|
const taskId = envelope.task_id;
|
|
893
1037
|
if (taskId === void 0) return "rejected";
|
|
894
1038
|
const attempt = await stores.tasks.get(taskId);
|
|
1039
|
+
if (attempt?.agentRef !== void 0 && attempt.deviceId !== deviceId) return "rejected";
|
|
895
1040
|
if (attempt?.ownerDeviceId !== void 0 && attempt.ownerDeviceId !== deviceId) return "rejected";
|
|
1041
|
+
if (attempt?.agentRef !== void 0 && agentRefEchoRequired(envelope) && !sameAgentRef(attempt.agentRef, inboundAgentRef(envelope))) {
|
|
1042
|
+
return "rejected";
|
|
1043
|
+
}
|
|
896
1044
|
if (envelope.type === "task.progress" && envelope.payload.events.length > 0) {
|
|
897
1045
|
try {
|
|
898
1046
|
validateActivityBatch(
|
|
@@ -922,19 +1070,27 @@ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds
|
|
|
922
1070
|
}
|
|
923
1071
|
}
|
|
924
1072
|
if (await stores.dedup.checkAndRecord(deviceId, envelope.id)) return "duplicate";
|
|
925
|
-
await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds);
|
|
1073
|
+
await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, attempt?.agentRef);
|
|
926
1074
|
return "accepted";
|
|
927
1075
|
}
|
|
928
|
-
async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds) {
|
|
1076
|
+
async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, persistedAgentRef) {
|
|
929
1077
|
switch (envelope.type) {
|
|
930
1078
|
case "task.claim":
|
|
931
1079
|
await stores.tasks.claim({ taskId, deviceId });
|
|
932
1080
|
return;
|
|
933
1081
|
case "task.started":
|
|
934
|
-
await stores.tasks.recordStatus({
|
|
1082
|
+
await stores.tasks.recordStatus({
|
|
1083
|
+
taskId,
|
|
1084
|
+
status: "running",
|
|
1085
|
+
...persistedAgentRef === void 0 ? {} : { agentRef: persistedAgentRef }
|
|
1086
|
+
});
|
|
935
1087
|
return;
|
|
936
1088
|
case "task.decline":
|
|
937
|
-
await stores.tasks.recordStatus({
|
|
1089
|
+
await stores.tasks.recordStatus({
|
|
1090
|
+
taskId,
|
|
1091
|
+
status: "failed",
|
|
1092
|
+
...persistedAgentRef === void 0 ? {} : { agentRef: persistedAgentRef }
|
|
1093
|
+
});
|
|
938
1094
|
return;
|
|
939
1095
|
case "task.progress":
|
|
940
1096
|
if (envelope.payload.events.length > 0) {
|
|
@@ -999,12 +1155,30 @@ function approvalTimelineAppendInput(taskId, envelope) {
|
|
|
999
1155
|
}
|
|
1000
1156
|
}
|
|
1001
1157
|
async function recordTerminal(stores, taskId, envelope, status) {
|
|
1158
|
+
const attempt = await stores.tasks.get(taskId);
|
|
1002
1159
|
const { created } = await stores.receipts.record({
|
|
1003
1160
|
key: terminalReceiptKey(taskId),
|
|
1004
1161
|
body: encodeEnvelope(envelope)
|
|
1005
1162
|
});
|
|
1163
|
+
if (attempt?.cancellation !== void 0) {
|
|
1164
|
+
if (status === "cancelled") {
|
|
1165
|
+
await stores.tasks.recordStatus({
|
|
1166
|
+
taskId,
|
|
1167
|
+
status: "cancelled",
|
|
1168
|
+
...attempt.agentRef === void 0 ? {} : { agentRef: attempt.agentRef }
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1006
1173
|
if (!created) return;
|
|
1007
|
-
|
|
1174
|
+
const cause = envelope.type === "task.fail" || envelope.type === "task.cancelled" ? envelope.payload.reason : void 0;
|
|
1175
|
+
const recordedAttempt = await stores.tasks.recordStatus({
|
|
1176
|
+
taskId,
|
|
1177
|
+
status,
|
|
1178
|
+
...attempt?.agentRef === void 0 ? {} : { agentRef: attempt.agentRef },
|
|
1179
|
+
...cause === void 0 ? {} : { terminalCause: cause }
|
|
1180
|
+
});
|
|
1181
|
+
if (recordedAttempt?.status !== status || recordedAttempt.cancellation !== void 0) return;
|
|
1008
1182
|
await projectTerminalToReview(stores.board, taskId);
|
|
1009
1183
|
}
|
|
1010
1184
|
|
|
@@ -1026,6 +1200,16 @@ function messagesHandler(deps) {
|
|
|
1026
1200
|
deps.activityBounds
|
|
1027
1201
|
);
|
|
1028
1202
|
if (outcome === "rate_limited") return c.json({ error: "rate limit exceeded" }, 429);
|
|
1203
|
+
if (envelope.type === "agent.egress.reliable" && (outcome === "accepted" || outcome === "duplicate")) {
|
|
1204
|
+
const record = await stores.egress.get(device.deviceId, envelope.payload.eventId);
|
|
1205
|
+
if (record === void 0) {
|
|
1206
|
+
throw new Error(`Accepted reliable egress ${envelope.payload.eventId} has no durable receipt record.`);
|
|
1207
|
+
}
|
|
1208
|
+
await deps.appendReliableEgressAck(stores, record);
|
|
1209
|
+
}
|
|
1210
|
+
if (envelope.type === "agent.content.receipt" && (outcome === "accepted" || outcome === "duplicate")) {
|
|
1211
|
+
await deps.appendContentReceiptAck(stores, device.deviceId, envelope.payload);
|
|
1212
|
+
}
|
|
1029
1213
|
if (outcome === "rejected") rejected += 1;
|
|
1030
1214
|
else accepted += 1;
|
|
1031
1215
|
}
|
|
@@ -1271,11 +1455,7 @@ function boardFailure(c, caught) {
|
|
|
1271
1455
|
if (isCoreError(caught, "board_not_held")) return c.json({ error: caught.code }, 409);
|
|
1272
1456
|
throw caught;
|
|
1273
1457
|
}
|
|
1274
|
-
var PresenceBodySchema =
|
|
1275
|
-
level: z.enum(PRESENCE_LEVELS),
|
|
1276
|
-
detail: z.string().optional(),
|
|
1277
|
-
configuredToolsets: ConfiguredToolsetsSchema.optional()
|
|
1278
|
-
});
|
|
1458
|
+
var PresenceBodySchema = PresencePublishRequestSchema;
|
|
1279
1459
|
function presencePublishHandler(deps) {
|
|
1280
1460
|
return async (c) => {
|
|
1281
1461
|
const authenticated = await authenticateDevice(c, deps);
|
|
@@ -1292,6 +1472,15 @@ function presencePublishHandler(deps) {
|
|
|
1292
1472
|
level: parsed.data.level,
|
|
1293
1473
|
...parsed.data.detail === void 0 ? {} : { detail: parsed.data.detail },
|
|
1294
1474
|
...parsed.data.configuredToolsets === void 0 ? {} : { configuredToolsets: parsed.data.configuredToolsets },
|
|
1475
|
+
...parsed.data.clientVersion === void 0 ? {} : { clientVersion: parsed.data.clientVersion },
|
|
1476
|
+
...parsed.data.protocolVersions === void 0 ? {} : { protocolVersions: parsed.data.protocolVersions },
|
|
1477
|
+
...parsed.data.runtimes === void 0 ? {} : {
|
|
1478
|
+
runtimes: parsed.data.runtimes.map(({ id, version, authPresent }) => ({
|
|
1479
|
+
id,
|
|
1480
|
+
...version === void 0 ? {} : { version },
|
|
1481
|
+
...authPresent === void 0 ? {} : { authPresent }
|
|
1482
|
+
}))
|
|
1483
|
+
},
|
|
1295
1484
|
ttlMs: deps.ttlMs,
|
|
1296
1485
|
minimumIntervalMs: deps.minimumIntervalMs
|
|
1297
1486
|
}),
|
|
@@ -1788,25 +1977,33 @@ function projectTerminalResult(taskId, receipt) {
|
|
|
1788
1977
|
return {
|
|
1789
1978
|
taskId,
|
|
1790
1979
|
state: "complete",
|
|
1980
|
+
...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
|
|
1791
1981
|
summary: envelope.payload.summary,
|
|
1792
1982
|
sessionRef: envelope.payload.sessionRef,
|
|
1793
1983
|
...envelope.payload.artifactRefs !== void 0 ? { artifactRefs: envelope.payload.artifactRefs } : {},
|
|
1794
1984
|
...envelope.payload.document !== void 0 ? { document: envelope.payload.document } : {},
|
|
1985
|
+
...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
|
|
1795
1986
|
recordedAt: receipt.recordedAt
|
|
1796
1987
|
};
|
|
1797
1988
|
case "task.fail":
|
|
1798
1989
|
return {
|
|
1799
1990
|
taskId,
|
|
1800
1991
|
state: "failed",
|
|
1992
|
+
...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
|
|
1801
1993
|
reason: envelope.payload.reason,
|
|
1994
|
+
terminalCause: envelope.payload.reason,
|
|
1802
1995
|
...envelope.payload.retryable !== void 0 ? { retryable: envelope.payload.retryable } : {},
|
|
1996
|
+
...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
|
|
1803
1997
|
recordedAt: receipt.recordedAt
|
|
1804
1998
|
};
|
|
1805
1999
|
case "task.cancelled":
|
|
1806
2000
|
return {
|
|
1807
2001
|
taskId,
|
|
1808
2002
|
state: "cancelled",
|
|
2003
|
+
...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
|
|
1809
2004
|
...envelope.payload.reason !== void 0 ? { reason: envelope.payload.reason } : {},
|
|
2005
|
+
...envelope.payload.reason === void 0 ? {} : { terminalCause: envelope.payload.reason },
|
|
2006
|
+
...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
|
|
1810
2007
|
recordedAt: receipt.recordedAt
|
|
1811
2008
|
};
|
|
1812
2009
|
default:
|
|
@@ -1822,6 +2019,10 @@ var DEFAULT_MAX_BLOB_SIZE_BYTES = 100 * 1024 * 1024;
|
|
|
1822
2019
|
var DEFAULT_LONG_POLL_HOLD_MS = 5e4;
|
|
1823
2020
|
var DEFAULT_LONG_POLL_INTERVAL_MS = 250;
|
|
1824
2021
|
var DEFAULT_EVENTS_PAGE_LIMIT = 50;
|
|
2022
|
+
var AGENT_HOME_CONTRACT_CAPABILITY = "agent-home-contract";
|
|
2023
|
+
function sameAgentRef2(expected, actual) {
|
|
2024
|
+
return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
|
|
2025
|
+
}
|
|
1825
2026
|
function createByokCloud(options) {
|
|
1826
2027
|
const declaration = parseDeclaration(options.capabilities);
|
|
1827
2028
|
assertNoOverDeclaration(
|
|
@@ -1871,7 +2072,12 @@ function createByokCloud(options) {
|
|
|
1871
2072
|
if (declares(declaration, CLOUD_CAPABILITIES.messagesBatch)) {
|
|
1872
2073
|
registry.register(
|
|
1873
2074
|
{ method: "POST", path: BYOK_MESSAGES_PATH, class: "device" },
|
|
1874
|
-
messagesHandler({
|
|
2075
|
+
messagesHandler({
|
|
2076
|
+
...deviceRouteDeps,
|
|
2077
|
+
activityBounds,
|
|
2078
|
+
appendReliableEgressAck: enqueueReliableEgressAck,
|
|
2079
|
+
appendContentReceiptAck: enqueueContentReceiptAck
|
|
2080
|
+
})
|
|
1875
2081
|
);
|
|
1876
2082
|
}
|
|
1877
2083
|
if (declares(declaration, CLOUD_CAPABILITIES.boardCoordination)) {
|
|
@@ -1989,16 +2195,98 @@ function createByokCloud(options) {
|
|
|
1989
2195
|
function controlPlane(tenant) {
|
|
1990
2196
|
return { kind: "control-plane", tenantId: tenant, operatorId };
|
|
1991
2197
|
}
|
|
1992
|
-
async function enqueueTaskEnvelope(tenant, deviceId, requestedTaskId, buildEnvelope) {
|
|
2198
|
+
async function enqueueTaskEnvelope(tenant, deviceId, requestedTaskId, agentRef, buildEnvelope) {
|
|
1993
2199
|
const stores = tenantStoresFor(controlPlane(tenant), root);
|
|
1994
2200
|
const taskId = requestedTaskId ?? `task_${options.crypto.randomUuid()}`;
|
|
1995
2201
|
const messageId = options.crypto.randomUuid();
|
|
2202
|
+
const reservation = agentRef === void 0 ? void 0 : await stores.tasks.reserveAgentOffer({ taskId, deviceId, agentRef });
|
|
2203
|
+
const openedBeforeAppend = reservation?.attempt;
|
|
2204
|
+
if (agentRef !== void 0 && (openedBeforeAppend === void 0 || openedBeforeAppend.deviceId !== deviceId || openedBeforeAppend.agentRef === void 0 || !sameAgentRef2(agentRef, openedBeforeAppend.agentRef))) {
|
|
2205
|
+
throw new ByokCloudError(
|
|
2206
|
+
"agent_ref_mismatch",
|
|
2207
|
+
`Task ${taskId} already has a different durable Agent identity or target device.`
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
2210
|
+
if (reservation !== void 0 && !reservation.created) {
|
|
2211
|
+
throw new ByokCloudError(
|
|
2212
|
+
"agent_task_already_exists",
|
|
2213
|
+
`Task ${taskId} already has a durable Agent attempt and cannot be enqueued again.`
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
let message;
|
|
2217
|
+
try {
|
|
2218
|
+
message = await stores.mailbox.append({
|
|
2219
|
+
deviceId,
|
|
2220
|
+
messageId,
|
|
2221
|
+
materialize: async (seq) => {
|
|
2222
|
+
const envelope2 = buildEnvelope(taskId, seq, messageId);
|
|
2223
|
+
const body = encodeEnvelope(envelope2);
|
|
2224
|
+
const bytes = new TextEncoder().encode(body);
|
|
2225
|
+
return {
|
|
2226
|
+
body,
|
|
2227
|
+
bodyHash: contentHash(await options.crypto.sha256(bytes)),
|
|
2228
|
+
byteSize: BigInt(bytes.length)
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
} catch (error) {
|
|
2233
|
+
if (openedBeforeAppend !== void 0) {
|
|
2234
|
+
try {
|
|
2235
|
+
await stores.tasks.recordStatus({
|
|
2236
|
+
taskId,
|
|
2237
|
+
status: "failed",
|
|
2238
|
+
agentRef,
|
|
2239
|
+
terminalCause: "mailbox append failed before Agent offer delivery"
|
|
2240
|
+
});
|
|
2241
|
+
} catch (cleanupError) {
|
|
2242
|
+
throw new AggregateError(
|
|
2243
|
+
[error, cleanupError],
|
|
2244
|
+
`Agent task ${taskId} could not be delivered or durably closed`
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
throw error;
|
|
2249
|
+
}
|
|
2250
|
+
const envelope = decodeEnvelope(message.body);
|
|
2251
|
+
if (envelope.seq !== message.seq) {
|
|
2252
|
+
throw new ByokCloudError(
|
|
2253
|
+
"mailbox_seq_mismatch",
|
|
2254
|
+
`Mailbox stored offer seq ${String(envelope.seq)} at row ${message.seq}; the daemon's redelivery cursor would be wrong.`
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
const attempt = openedBeforeAppend ?? await stores.tasks.open({
|
|
2258
|
+
taskId,
|
|
2259
|
+
deviceId
|
|
2260
|
+
});
|
|
2261
|
+
return { taskId, seq: message.seq, envelope, attempt };
|
|
2262
|
+
}
|
|
2263
|
+
async function assertAgentCapabilities(tenant, deviceId, required) {
|
|
2264
|
+
const device = await options.cloud.devices.get(tenant, deviceId);
|
|
2265
|
+
if (device === void 0 || device.revoked || !device.capabilities || !required.every((capability) => device.capabilities.includes(capability))) {
|
|
2266
|
+
const missing = required.filter((capability) => !device?.capabilities?.includes(capability));
|
|
2267
|
+
throw new ByokCloudError(
|
|
2268
|
+
"agent_capability_missing",
|
|
2269
|
+
`Device ${deviceId} lacks durable Agent capabilities (${missing.join(", ")}); refusing before mailbox append.`
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
function contentReadCapability2(surface) {
|
|
2274
|
+
switch (surface) {
|
|
2275
|
+
case "workspace":
|
|
2276
|
+
return AGENT_CONTENT_WORKSPACE_READ_CAPABILITY;
|
|
2277
|
+
case "transcript":
|
|
2278
|
+
return AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY;
|
|
2279
|
+
case "artifact":
|
|
2280
|
+
return AGENT_CONTENT_ARTIFACT_READ_CAPABILITY;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
async function enqueueAgentControlEnvelope(tenant, deviceId, messageId, buildEnvelope) {
|
|
2284
|
+
const stores = tenantStoresFor(controlPlane(tenant), root);
|
|
1996
2285
|
const message = await stores.mailbox.append({
|
|
1997
2286
|
deviceId,
|
|
1998
2287
|
messageId,
|
|
1999
2288
|
materialize: async (seq) => {
|
|
2000
|
-
const
|
|
2001
|
-
const body = encodeEnvelope(envelope2);
|
|
2289
|
+
const body = encodeEnvelope(buildEnvelope(seq));
|
|
2002
2290
|
const bytes = new TextEncoder().encode(body);
|
|
2003
2291
|
return {
|
|
2004
2292
|
body,
|
|
@@ -2011,11 +2299,77 @@ function createByokCloud(options) {
|
|
|
2011
2299
|
if (envelope.seq !== message.seq) {
|
|
2012
2300
|
throw new ByokCloudError(
|
|
2013
2301
|
"mailbox_seq_mismatch",
|
|
2014
|
-
`Mailbox stored
|
|
2302
|
+
`Mailbox stored Agent control seq ${String(envelope.seq)} at row ${message.seq}.`
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
return { seq: message.seq, envelope };
|
|
2306
|
+
}
|
|
2307
|
+
async function enqueueReliableEgressAck(stores, record) {
|
|
2308
|
+
const payload = AgentEgressAckPayloadSchema.parse({
|
|
2309
|
+
agentRef: record.payload.agentRef,
|
|
2310
|
+
sessionRef: record.payload.sessionRef,
|
|
2311
|
+
policyRevision: record.payload.policyRevision,
|
|
2312
|
+
eventId: record.payload.eventId,
|
|
2313
|
+
cursor: record.payload.cursor,
|
|
2314
|
+
receiptId: record.receiptId
|
|
2315
|
+
});
|
|
2316
|
+
const message = await stores.mailbox.append({
|
|
2317
|
+
deviceId: record.deviceId,
|
|
2318
|
+
messageId: record.receiptId,
|
|
2319
|
+
materialize: async (seq) => {
|
|
2320
|
+
const body = encodeEnvelope(createEnvelope("agent.egress.ack", payload, { id: record.receiptId, seq }));
|
|
2321
|
+
const bytes = new TextEncoder().encode(body);
|
|
2322
|
+
return {
|
|
2323
|
+
body,
|
|
2324
|
+
bodyHash: contentHash(await options.crypto.sha256(bytes)),
|
|
2325
|
+
byteSize: BigInt(bytes.length)
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
});
|
|
2329
|
+
const decoded = decodeEnvelope(message.body);
|
|
2330
|
+
if (decoded.type !== "agent.egress.ack" || decoded.seq !== message.seq || JSON.stringify(decoded.payload) !== JSON.stringify(payload)) {
|
|
2331
|
+
throw new ByokCloudError(
|
|
2332
|
+
"mailbox_receipt_mismatch",
|
|
2333
|
+
`Mailbox receipt ${record.receiptId} does not exactly acknowledge Agent egress ${record.payload.eventId}.`
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
async function enqueueContentReceiptAck(stores, deviceId, receipt) {
|
|
2338
|
+
const exactReceipt = AgentContentReceiptPayloadSchema.parse(receipt);
|
|
2339
|
+
if (exactReceipt.eventId !== exactReceipt.requestId) {
|
|
2340
|
+
throw new ByokCloudError("mailbox_receipt_mismatch", "Content receipt eventId must equal requestId.");
|
|
2341
|
+
}
|
|
2342
|
+
const payload = AgentEgressAckPayloadSchema.parse({
|
|
2343
|
+
agentRef: exactReceipt.agentRef,
|
|
2344
|
+
sessionRef: exactReceipt.sessionRef,
|
|
2345
|
+
policyRevision: exactReceipt.policyRevision,
|
|
2346
|
+
eventId: exactReceipt.eventId,
|
|
2347
|
+
cursor: exactReceipt.cursor,
|
|
2348
|
+
receiptId: exactReceipt.requestId
|
|
2349
|
+
});
|
|
2350
|
+
const message = await stores.mailbox.append({
|
|
2351
|
+
deviceId,
|
|
2352
|
+
// The request itself already occupies its bare request id in this
|
|
2353
|
+
// mailbox. A namespaced producer key keeps the ack idempotent without
|
|
2354
|
+
// returning that control envelope on duplicate receipt delivery.
|
|
2355
|
+
messageId: `agent-content-ack:${exactReceipt.requestId}`,
|
|
2356
|
+
materialize: async (seq) => {
|
|
2357
|
+
const body = encodeEnvelope(createEnvelope("agent.egress.ack", payload, { id: exactReceipt.requestId, seq }));
|
|
2358
|
+
const bytes = new TextEncoder().encode(body);
|
|
2359
|
+
return {
|
|
2360
|
+
body,
|
|
2361
|
+
bodyHash: contentHash(await options.crypto.sha256(bytes)),
|
|
2362
|
+
byteSize: BigInt(bytes.length)
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
});
|
|
2366
|
+
const decoded = decodeEnvelope(message.body);
|
|
2367
|
+
if (decoded.type !== "agent.egress.ack" || decoded.seq !== message.seq || JSON.stringify(decoded.payload) !== JSON.stringify(payload)) {
|
|
2368
|
+
throw new ByokCloudError(
|
|
2369
|
+
"mailbox_receipt_mismatch",
|
|
2370
|
+
`Mailbox content receipt ${exactReceipt.requestId} does not exactly acknowledge its durable content fact.`
|
|
2015
2371
|
);
|
|
2016
2372
|
}
|
|
2017
|
-
const attempt = await stores.tasks.open({ taskId, deviceId });
|
|
2018
|
-
return { taskId, seq: message.seq, envelope, attempt };
|
|
2019
2373
|
}
|
|
2020
2374
|
return {
|
|
2021
2375
|
fetch: registry.fetch,
|
|
@@ -2030,6 +2384,7 @@ function createByokCloud(options) {
|
|
|
2030
2384
|
tenant,
|
|
2031
2385
|
deviceId,
|
|
2032
2386
|
input.taskId,
|
|
2387
|
+
void 0,
|
|
2033
2388
|
(taskId, seq, messageId) => createEnvelope("task.offer", input.payload, { id: messageId, taskId, seq })
|
|
2034
2389
|
);
|
|
2035
2390
|
},
|
|
@@ -2038,17 +2393,128 @@ function createByokCloud(options) {
|
|
|
2038
2393
|
tenant,
|
|
2039
2394
|
deviceId,
|
|
2040
2395
|
input.taskId,
|
|
2396
|
+
void 0,
|
|
2041
2397
|
(taskId, seq, messageId) => createEnvelope("task.offer_with_toolsets", input.payload, { id: messageId, taskId, seq })
|
|
2042
2398
|
);
|
|
2043
2399
|
},
|
|
2400
|
+
async enqueueAgentOffer(tenant, deviceId, input) {
|
|
2401
|
+
await assertAgentCapabilities(tenant, deviceId, [AGENT_HOME_CONTRACT_CAPABILITY]);
|
|
2402
|
+
const payload = TaskOfferForAgentPayloadSchema.parse(input.payload);
|
|
2403
|
+
return enqueueTaskEnvelope(
|
|
2404
|
+
tenant,
|
|
2405
|
+
deviceId,
|
|
2406
|
+
input.taskId,
|
|
2407
|
+
payload.agentRef,
|
|
2408
|
+
(taskId, seq, messageId) => createEnvelope("task.offer_for_agent", payload, { id: messageId, taskId, seq })
|
|
2409
|
+
);
|
|
2410
|
+
},
|
|
2411
|
+
async enqueueAgentEgressOffer(tenant, deviceId, input) {
|
|
2412
|
+
await assertAgentCapabilities(tenant, deviceId, [
|
|
2413
|
+
AGENT_HOME_CONTRACT_CAPABILITY,
|
|
2414
|
+
AGENT_EGRESS_POLICY_CAPABILITY,
|
|
2415
|
+
AGENT_EGRESS_RELIABLE_ACK_CAPABILITY
|
|
2416
|
+
]);
|
|
2417
|
+
const payload = TaskOfferForAgentWithEgressPayloadSchema.parse(input.payload);
|
|
2418
|
+
return enqueueTaskEnvelope(
|
|
2419
|
+
tenant,
|
|
2420
|
+
deviceId,
|
|
2421
|
+
input.taskId,
|
|
2422
|
+
payload.agentRef,
|
|
2423
|
+
(taskId, seq, messageId) => createEnvelope("task.offer_for_agent_with_egress", payload, { id: messageId, taskId, seq })
|
|
2424
|
+
);
|
|
2425
|
+
},
|
|
2426
|
+
async enqueueAgentContentRead(tenant, deviceId, input) {
|
|
2427
|
+
const payload = AgentContentReadPayloadSchema.parse(input.payload);
|
|
2428
|
+
await assertAgentCapabilities(tenant, deviceId, [
|
|
2429
|
+
AGENT_HOME_CONTRACT_CAPABILITY,
|
|
2430
|
+
AGENT_EGRESS_POLICY_CAPABILITY,
|
|
2431
|
+
AGENT_EGRESS_RELIABLE_ACK_CAPABILITY,
|
|
2432
|
+
contentReadCapability2(payload.surface)
|
|
2433
|
+
]);
|
|
2434
|
+
const stores = tenantStoresFor(controlPlane(tenant), root);
|
|
2435
|
+
const persisted = await stores.receipts.record({
|
|
2436
|
+
key: `agent-content-request:${deviceId}:${payload.requestId}`,
|
|
2437
|
+
body: JSON.stringify(payload)
|
|
2438
|
+
});
|
|
2439
|
+
if (!persisted.created && persisted.receipt.body !== JSON.stringify(payload)) {
|
|
2440
|
+
throw new ByokCloudError(
|
|
2441
|
+
"agent_content_request_mismatch",
|
|
2442
|
+
`Request ${payload.requestId} already exists with a different immutable content-read body.`
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
const control = await enqueueAgentControlEnvelope(
|
|
2446
|
+
tenant,
|
|
2447
|
+
deviceId,
|
|
2448
|
+
payload.requestId,
|
|
2449
|
+
(seq) => createEnvelope("agent.content.read", payload, { id: payload.requestId, seq })
|
|
2450
|
+
);
|
|
2451
|
+
if (control.envelope.type !== "agent.content.read" || JSON.stringify(control.envelope.payload) !== JSON.stringify(payload)) {
|
|
2452
|
+
throw new ByokCloudError(
|
|
2453
|
+
"agent_content_request_mismatch",
|
|
2454
|
+
`Request ${payload.requestId} already exists with a different immutable content-read body.`
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
return control;
|
|
2458
|
+
},
|
|
2459
|
+
async cancelTask(tenant, taskId, reason) {
|
|
2460
|
+
const stores = tenantStoresFor(controlPlane(tenant), root);
|
|
2461
|
+
const proposedMessageId = options.crypto.randomUuid();
|
|
2462
|
+
const mutation = await stores.cancellations.request({
|
|
2463
|
+
taskId,
|
|
2464
|
+
proposedMessageId,
|
|
2465
|
+
...reason === void 0 ? {} : { reason },
|
|
2466
|
+
materialize: async (seq, messageId) => {
|
|
2467
|
+
const envelope = createEnvelope("task.cancel", reason === void 0 ? {} : { reason }, {
|
|
2468
|
+
id: messageId,
|
|
2469
|
+
taskId,
|
|
2470
|
+
seq
|
|
2471
|
+
});
|
|
2472
|
+
const body = encodeEnvelope(envelope);
|
|
2473
|
+
const bytes = new TextEncoder().encode(body);
|
|
2474
|
+
return {
|
|
2475
|
+
body,
|
|
2476
|
+
bodyHash: contentHash(await options.crypto.sha256(bytes)),
|
|
2477
|
+
byteSize: BigInt(bytes.length)
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
});
|
|
2481
|
+
if (mutation === void 0) {
|
|
2482
|
+
throw new ByokCloudError("task_not_found", `Task ${taskId} was not found for this tenant.`);
|
|
2483
|
+
}
|
|
2484
|
+
if (mutation.message !== void 0) {
|
|
2485
|
+
const envelope = decodeEnvelope(mutation.message.body);
|
|
2486
|
+
if (envelope.seq !== mutation.message.seq || envelope.type !== "task.cancel") {
|
|
2487
|
+
throw new ByokCloudError(
|
|
2488
|
+
"mailbox_seq_mismatch",
|
|
2489
|
+
`Mailbox stored cancellation seq ${String(envelope.seq)} at row ${mutation.message.seq}.`
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
return mutation.attempt;
|
|
2494
|
+
},
|
|
2044
2495
|
readTaskAttempt(tenant, taskId) {
|
|
2045
2496
|
return tenantStoresFor(controlPlane(tenant), root).tasks.get(taskId);
|
|
2046
2497
|
},
|
|
2047
2498
|
readTerminalReceipt(tenant, taskId) {
|
|
2048
2499
|
return tenantStoresFor(controlPlane(tenant), root).receipts.get(terminalReceiptKey(taskId));
|
|
2049
2500
|
},
|
|
2501
|
+
readAgentEgress(tenant, deviceId, eventId) {
|
|
2502
|
+
return tenantStoresFor(controlPlane(tenant), root).egress.get(deviceId, eventId);
|
|
2503
|
+
},
|
|
2050
2504
|
async readTaskResult(tenant, taskId) {
|
|
2051
|
-
const
|
|
2505
|
+
const stores = tenantStoresFor(controlPlane(tenant), root);
|
|
2506
|
+
const attempt = await stores.tasks.get(taskId);
|
|
2507
|
+
if (attempt?.cancellation !== void 0) {
|
|
2508
|
+
return {
|
|
2509
|
+
taskId,
|
|
2510
|
+
state: "cancelled",
|
|
2511
|
+
...attempt.agentRef === void 0 ? {} : { agentRef: attempt.agentRef },
|
|
2512
|
+
...attempt.cancellation.reason === void 0 ? {} : { reason: attempt.cancellation.reason },
|
|
2513
|
+
...attempt.cancellation.reason === void 0 ? {} : { terminalCause: attempt.cancellation.reason },
|
|
2514
|
+
recordedAt: attempt.cancellation.requestedAt
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
const receipt = await stores.receipts.get(
|
|
2052
2518
|
terminalReceiptKey(taskId)
|
|
2053
2519
|
);
|
|
2054
2520
|
return receipt === void 0 ? void 0 : projectTerminalResult(taskId, receipt);
|
|
@@ -2079,6 +2545,9 @@ function createByokCloud(options) {
|
|
|
2079
2545
|
listPresence(tenant) {
|
|
2080
2546
|
return tenantStoresFor(controlPlane(tenant), root).presence.list();
|
|
2081
2547
|
},
|
|
2548
|
+
readTenantReadiness(tenant) {
|
|
2549
|
+
return tenantStoresFor(controlPlane(tenant), root).devices.readiness();
|
|
2550
|
+
},
|
|
2082
2551
|
readActivity(tenant, taskId) {
|
|
2083
2552
|
return tenantStoresFor(controlPlane(tenant), root).activity.read(taskId);
|
|
2084
2553
|
},
|
|
@@ -2146,9 +2615,9 @@ var InMemoryBlobRegistry = class {
|
|
|
2146
2615
|
crypto;
|
|
2147
2616
|
secret;
|
|
2148
2617
|
urlTtlMs;
|
|
2149
|
-
constructor(clock,
|
|
2618
|
+
constructor(clock, crypto2, options) {
|
|
2150
2619
|
this.clock = clock;
|
|
2151
|
-
this.crypto =
|
|
2620
|
+
this.crypto = crypto2;
|
|
2152
2621
|
this.secret = globalThis.crypto.getRandomValues(new Uint8Array(SIGNING_SECRET_BYTES));
|
|
2153
2622
|
this.urlTtlMs = options.urlTtlMs ?? BLOB_URL_TTL_MS;
|
|
2154
2623
|
}
|
|
@@ -2253,14 +2722,20 @@ var InMemoryBlobContentProxy = class {
|
|
|
2253
2722
|
record.uploaded = true;
|
|
2254
2723
|
return { ok: true };
|
|
2255
2724
|
}
|
|
2725
|
+
/**
|
|
2726
|
+
* Never returns `{ok:false}`: this composition holds the bytes in the same
|
|
2727
|
+
* process, so there is no upstream to be unreachable and no stream to be
|
|
2728
|
+
* interrupted. Both `BlobReadErrorCode`s are structurally unreachable here
|
|
2729
|
+
* — a proxy that fetches from object storage is where they become live.
|
|
2730
|
+
*/
|
|
2256
2731
|
async readContent(blobId) {
|
|
2257
2732
|
const record = this.#registry.blobs.get(blobId);
|
|
2258
2733
|
if (record === void 0 || !record.uploaded || record.data === void 0) return void 0;
|
|
2259
|
-
return { data: record.data, contentType: record.contentType };
|
|
2734
|
+
return { ok: true, content: { data: record.data, contentType: record.contentType } };
|
|
2260
2735
|
}
|
|
2261
2736
|
};
|
|
2262
|
-
function createInMemoryBlobs(clock,
|
|
2263
|
-
const registry = new InMemoryBlobRegistry(clock,
|
|
2737
|
+
function createInMemoryBlobs(clock, crypto2, objects, options = {}) {
|
|
2738
|
+
const registry = new InMemoryBlobRegistry(clock, crypto2, options);
|
|
2264
2739
|
return {
|
|
2265
2740
|
blobs: new InMemoryCloudBlobStore(registry, objects),
|
|
2266
2741
|
contentProxy: new InMemoryBlobContentProxy(registry)
|
|
@@ -2303,10 +2778,67 @@ var InMemoryDeviceDirectory = class {
|
|
|
2303
2778
|
if (record === void 0) return;
|
|
2304
2779
|
this.#byTenant.set(key, { ...record, revoked: true });
|
|
2305
2780
|
}
|
|
2781
|
+
async recordCapabilities(tenant, input) {
|
|
2782
|
+
const key = tenantKey(tenant, input.deviceId);
|
|
2783
|
+
const record = this.#byTenant.get(key);
|
|
2784
|
+
if (record === void 0 || record.revoked) return void 0;
|
|
2785
|
+
const updated = {
|
|
2786
|
+
...record,
|
|
2787
|
+
capabilities: Object.freeze([...input.capabilities])
|
|
2788
|
+
};
|
|
2789
|
+
this.#byTenant.set(key, updated);
|
|
2790
|
+
return updated;
|
|
2791
|
+
}
|
|
2306
2792
|
async list(tenant) {
|
|
2307
2793
|
const prefix = tenantKey(tenant, "");
|
|
2308
2794
|
return [...this.#byTenant.entries()].filter(([key]) => key.startsWith(prefix)).map(([, record]) => record);
|
|
2309
2795
|
}
|
|
2796
|
+
async readiness(tenant, presence) {
|
|
2797
|
+
const devices = await this.list(tenant);
|
|
2798
|
+
const livePresence = await presence.list(tenant);
|
|
2799
|
+
const presenceByDevice = new Map(livePresence.map((hint) => [hint.deviceId, hint]));
|
|
2800
|
+
const activeDeviceIds = new Set(
|
|
2801
|
+
devices.filter((device) => !device.revoked).map((device) => device.deviceId)
|
|
2802
|
+
);
|
|
2803
|
+
const observedPresenceByLevel = Object.fromEntries(
|
|
2804
|
+
PRESENCE_LEVELS.map((level) => [level, 0])
|
|
2805
|
+
);
|
|
2806
|
+
for (const hint of livePresence) {
|
|
2807
|
+
if (!activeDeviceIds.has(hint.deviceId)) continue;
|
|
2808
|
+
observedPresenceByLevel[hint.level] += 1;
|
|
2809
|
+
}
|
|
2810
|
+
return {
|
|
2811
|
+
tenantId: tenant,
|
|
2812
|
+
activePairedDeviceCount: devices.filter((device) => !device.revoked).length,
|
|
2813
|
+
revokedDeviceCount: devices.filter((device) => device.revoked).length,
|
|
2814
|
+
observedPresenceCount: Object.values(observedPresenceByLevel).reduce(
|
|
2815
|
+
(total, count) => total + count,
|
|
2816
|
+
0
|
|
2817
|
+
),
|
|
2818
|
+
observedPresenceByLevel,
|
|
2819
|
+
devices: [...devices].sort((left, right) => left.deviceId.localeCompare(right.deviceId)).map((device) => {
|
|
2820
|
+
const hint = device.revoked ? void 0 : presenceByDevice.get(device.deviceId);
|
|
2821
|
+
return {
|
|
2822
|
+
deviceId: device.deviceId,
|
|
2823
|
+
productId: device.productId,
|
|
2824
|
+
deviceName: device.deviceName,
|
|
2825
|
+
revoked: device.revoked,
|
|
2826
|
+
...hint === void 0 ? {} : {
|
|
2827
|
+
presence: {
|
|
2828
|
+
level: hint.level,
|
|
2829
|
+
...hint.detail === void 0 ? {} : { detail: hint.detail },
|
|
2830
|
+
...hint.configuredToolsets === void 0 ? {} : { configuredToolsets: hint.configuredToolsets },
|
|
2831
|
+
...hint.clientVersion === void 0 ? {} : { clientVersion: hint.clientVersion },
|
|
2832
|
+
...hint.protocolVersions === void 0 ? {} : { protocolVersions: hint.protocolVersions },
|
|
2833
|
+
...hint.runtimes === void 0 ? {} : { runtimes: hint.runtimes },
|
|
2834
|
+
observedAt: hint.observedAt,
|
|
2835
|
+
expiresAt: hint.expiresAt
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
};
|
|
2839
|
+
})
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2310
2842
|
async resolveByDeviceId(deviceId) {
|
|
2311
2843
|
const key = this.#byDeviceId.get(deviceId);
|
|
2312
2844
|
return key === void 0 ? void 0 : this.#byTenant.get(key);
|
|
@@ -2342,9 +2874,9 @@ var InMemoryNonceStore = class {
|
|
|
2342
2874
|
#clock;
|
|
2343
2875
|
#crypto;
|
|
2344
2876
|
#ttlMs;
|
|
2345
|
-
constructor(clock,
|
|
2877
|
+
constructor(clock, crypto2, ttlMs = NONCE_TTL_MS) {
|
|
2346
2878
|
this.#clock = clock;
|
|
2347
|
-
this.#crypto =
|
|
2879
|
+
this.#crypto = crypto2;
|
|
2348
2880
|
this.#ttlMs = ttlMs;
|
|
2349
2881
|
}
|
|
2350
2882
|
/** Number of records currently held (post-sweep). Test-facing only. */
|
|
@@ -2448,54 +2980,171 @@ var InMemoryProofRequestReceiptStore = class {
|
|
|
2448
2980
|
return this.#receipts.get(tenantKey(tenant, deviceId, requestId));
|
|
2449
2981
|
}
|
|
2450
2982
|
};
|
|
2983
|
+
function sameAgentRef3(left, right) {
|
|
2984
|
+
return left?.agentId === right?.agentId && left?.profileRevision === right?.profileRevision;
|
|
2985
|
+
}
|
|
2451
2986
|
var InMemoryTaskAttemptStore = class {
|
|
2452
|
-
#
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
this.#clock = clock;
|
|
2987
|
+
#state;
|
|
2988
|
+
constructor(clock, state = new InMemoryTaskAttemptState(clock)) {
|
|
2989
|
+
this.#state = state;
|
|
2456
2990
|
}
|
|
2457
2991
|
async open(tenant, input) {
|
|
2458
2992
|
const key = tenantKey(tenant, input.taskId);
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2993
|
+
return this.#state.mutate(key, () => {
|
|
2994
|
+
const existing = this.#state.attempts.get(key);
|
|
2995
|
+
if (existing !== void 0) return existing;
|
|
2996
|
+
const attempt = {
|
|
2997
|
+
tenantId: tenant,
|
|
2998
|
+
taskId: input.taskId,
|
|
2999
|
+
deviceId: input.deviceId,
|
|
3000
|
+
...input.agentRef === void 0 ? {} : { agentRef: { ...input.agentRef } },
|
|
3001
|
+
status: "offered",
|
|
3002
|
+
updatedAt: this.#now()
|
|
3003
|
+
};
|
|
3004
|
+
this.#state.attempts.set(key, attempt);
|
|
3005
|
+
return attempt;
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
async reserveAgentOffer(tenant, input) {
|
|
3009
|
+
const key = tenantKey(tenant, input.taskId);
|
|
3010
|
+
return this.#state.mutate(key, () => {
|
|
3011
|
+
const existing = this.#state.attempts.get(key);
|
|
3012
|
+
if (existing !== void 0) return { attempt: existing, created: false };
|
|
3013
|
+
const attempt = {
|
|
3014
|
+
tenantId: tenant,
|
|
3015
|
+
taskId: input.taskId,
|
|
3016
|
+
deviceId: input.deviceId,
|
|
3017
|
+
agentRef: { ...input.agentRef },
|
|
3018
|
+
status: "offered",
|
|
3019
|
+
updatedAt: this.#now()
|
|
3020
|
+
};
|
|
3021
|
+
this.#state.attempts.set(key, attempt);
|
|
3022
|
+
return { attempt, created: true };
|
|
3023
|
+
});
|
|
2470
3024
|
}
|
|
2471
3025
|
async get(tenant, taskId) {
|
|
2472
|
-
return this.#attempts.get(tenantKey(tenant, taskId));
|
|
3026
|
+
return this.#state.attempts.get(tenantKey(tenant, taskId));
|
|
3027
|
+
}
|
|
3028
|
+
async getMany(tenant, taskIds) {
|
|
3029
|
+
return taskIds.flatMap((taskId) => {
|
|
3030
|
+
const attempt = this.#state.attempts.get(tenantKey(tenant, taskId));
|
|
3031
|
+
return attempt === void 0 ? [] : [attempt];
|
|
3032
|
+
});
|
|
2473
3033
|
}
|
|
2474
3034
|
async claim(tenant, input) {
|
|
2475
3035
|
const key = tenantKey(tenant, input.taskId);
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
3036
|
+
return this.#state.mutate(key, () => {
|
|
3037
|
+
const existing = this.#state.attempts.get(key);
|
|
3038
|
+
if (existing === void 0) return void 0;
|
|
3039
|
+
if (existing.ownerDeviceId !== void 0 || existing.cancellation !== void 0 || existing.status !== "offered") return existing;
|
|
3040
|
+
const claimed = {
|
|
3041
|
+
...existing,
|
|
3042
|
+
ownerDeviceId: input.deviceId,
|
|
3043
|
+
status: "claimed",
|
|
3044
|
+
updatedAt: this.#now()
|
|
3045
|
+
};
|
|
3046
|
+
this.#state.attempts.set(key, claimed);
|
|
3047
|
+
return claimed;
|
|
3048
|
+
});
|
|
2487
3049
|
}
|
|
2488
3050
|
async recordStatus(tenant, input) {
|
|
2489
3051
|
const key = tenantKey(tenant, input.taskId);
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
3052
|
+
return this.#state.mutate(key, () => {
|
|
3053
|
+
const existing = this.#state.attempts.get(key);
|
|
3054
|
+
if (existing === void 0) return void 0;
|
|
3055
|
+
if (existing.agentRef === void 0 !== (input.agentRef === void 0) || existing.agentRef !== void 0 && !sameAgentRef3(existing.agentRef, input.agentRef)) return existing;
|
|
3056
|
+
if (existing.cancellation !== void 0) {
|
|
3057
|
+
if (input.status !== "cancelled" || existing.status === "cancelled") return existing;
|
|
3058
|
+
} else if (existing.status === "complete" || existing.status === "failed" || existing.status === "cancelled") {
|
|
3059
|
+
return existing;
|
|
3060
|
+
}
|
|
3061
|
+
const updated = {
|
|
3062
|
+
...existing,
|
|
3063
|
+
status: input.status,
|
|
3064
|
+
...input.terminalCause === void 0 ? {} : { terminalCause: input.terminalCause },
|
|
3065
|
+
updatedAt: this.#now()
|
|
3066
|
+
};
|
|
3067
|
+
this.#state.attempts.set(key, updated);
|
|
3068
|
+
return updated;
|
|
3069
|
+
});
|
|
2495
3070
|
}
|
|
2496
3071
|
#now() {
|
|
3072
|
+
return this.#state.now();
|
|
3073
|
+
}
|
|
3074
|
+
};
|
|
3075
|
+
var InMemoryTaskAttemptState = class {
|
|
3076
|
+
attempts = /* @__PURE__ */ new Map();
|
|
3077
|
+
#clock;
|
|
3078
|
+
#mutationTails = /* @__PURE__ */ new Map();
|
|
3079
|
+
constructor(clock) {
|
|
3080
|
+
this.#clock = clock;
|
|
3081
|
+
}
|
|
3082
|
+
now() {
|
|
2497
3083
|
return this.#clock.now().toISOString();
|
|
2498
3084
|
}
|
|
3085
|
+
/** Serialize every state-changing operation for one tenant/task key. */
|
|
3086
|
+
async mutate(key, operation) {
|
|
3087
|
+
const previous = this.#mutationTails.get(key) ?? Promise.resolve();
|
|
3088
|
+
const result = previous.then(operation);
|
|
3089
|
+
const tail = result.then(
|
|
3090
|
+
() => void 0,
|
|
3091
|
+
() => void 0
|
|
3092
|
+
);
|
|
3093
|
+
this.#mutationTails.set(key, tail);
|
|
3094
|
+
try {
|
|
3095
|
+
return await result;
|
|
3096
|
+
} finally {
|
|
3097
|
+
if (this.#mutationTails.get(key) === tail) this.#mutationTails.delete(key);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
};
|
|
3101
|
+
var InMemoryTaskCancellationStore = class {
|
|
3102
|
+
#state;
|
|
3103
|
+
#mailbox;
|
|
3104
|
+
#messageIds = /* @__PURE__ */ new Map();
|
|
3105
|
+
#inFlight = /* @__PURE__ */ new Map();
|
|
3106
|
+
constructor(state, mailbox) {
|
|
3107
|
+
this.#state = state;
|
|
3108
|
+
this.#mailbox = mailbox;
|
|
3109
|
+
}
|
|
3110
|
+
request(tenant, input) {
|
|
3111
|
+
const key = tenantKey(tenant, input.taskId);
|
|
3112
|
+
const running = this.#inFlight.get(key);
|
|
3113
|
+
if (running !== void 0) return running;
|
|
3114
|
+
const operation = this.#request(tenant, key, input).finally(() => {
|
|
3115
|
+
this.#inFlight.delete(key);
|
|
3116
|
+
});
|
|
3117
|
+
this.#inFlight.set(key, operation);
|
|
3118
|
+
return operation;
|
|
3119
|
+
}
|
|
3120
|
+
async #request(tenant, key, input) {
|
|
3121
|
+
return this.#state.mutate(key, async () => {
|
|
3122
|
+
const existing = this.#state.attempts.get(key);
|
|
3123
|
+
if (existing === void 0) return void 0;
|
|
3124
|
+
if (existing.cancellation === void 0 && (existing.status === "complete" || existing.status === "failed" || existing.status === "cancelled")) {
|
|
3125
|
+
return { attempt: existing };
|
|
3126
|
+
}
|
|
3127
|
+
const messageId = this.#messageIds.get(key) ?? input.proposedMessageId;
|
|
3128
|
+
const message = await this.#mailbox.append(tenant, {
|
|
3129
|
+
deviceId: existing.deviceId,
|
|
3130
|
+
messageId,
|
|
3131
|
+
materialize: (seq) => input.materialize(seq, messageId)
|
|
3132
|
+
});
|
|
3133
|
+
const requestedAt = this.#state.now();
|
|
3134
|
+
const attempt = existing.cancellation === void 0 ? {
|
|
3135
|
+
...existing,
|
|
3136
|
+
status: existing.ownerDeviceId === void 0 ? "cancelled" : "cancel_requested",
|
|
3137
|
+
cancellation: {
|
|
3138
|
+
requestedAt,
|
|
3139
|
+
...input.reason === void 0 ? {} : { reason: input.reason }
|
|
3140
|
+
},
|
|
3141
|
+
updatedAt: requestedAt
|
|
3142
|
+
} : existing;
|
|
3143
|
+
this.#state.attempts.set(key, attempt);
|
|
3144
|
+
this.#messageIds.set(key, messageId);
|
|
3145
|
+
return { attempt, message };
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
2499
3148
|
};
|
|
2500
3149
|
|
|
2501
3150
|
// src/stores/in-memory/activity.ts
|
|
@@ -2614,20 +3263,48 @@ var InMemoryApprovalTimelineStore = class {
|
|
|
2614
3263
|
return tail;
|
|
2615
3264
|
}
|
|
2616
3265
|
};
|
|
3266
|
+
var InMemoryAgentEgressStore = class {
|
|
3267
|
+
#records = /* @__PURE__ */ new Map();
|
|
3268
|
+
#clock;
|
|
3269
|
+
constructor(clock) {
|
|
3270
|
+
this.#clock = clock;
|
|
3271
|
+
}
|
|
3272
|
+
async record(tenant, input) {
|
|
3273
|
+
const key = tenantKey(tenant, `${input.deviceId}\0${input.payload.eventId}`);
|
|
3274
|
+
const existing = this.#records.get(key);
|
|
3275
|
+
if (existing !== void 0) return { record: existing, created: false };
|
|
3276
|
+
const record = {
|
|
3277
|
+
tenantId: tenant,
|
|
3278
|
+
deviceId: input.deviceId,
|
|
3279
|
+
payload: input.payload,
|
|
3280
|
+
receiptId: input.receiptId,
|
|
3281
|
+
recordedAt: this.#clock.now().toISOString()
|
|
3282
|
+
};
|
|
3283
|
+
this.#records.set(key, record);
|
|
3284
|
+
return { record, created: true };
|
|
3285
|
+
}
|
|
3286
|
+
async get(tenant, deviceId, eventId) {
|
|
3287
|
+
return this.#records.get(tenantKey(tenant, `${deviceId}\0${eventId}`));
|
|
3288
|
+
}
|
|
3289
|
+
};
|
|
2617
3290
|
|
|
2618
3291
|
// src/stores/in-memory/index.ts
|
|
2619
|
-
function createInMemoryCloudStores(clock,
|
|
2620
|
-
const blobs = createInMemoryBlobs(clock,
|
|
3292
|
+
function createInMemoryCloudStores(clock, crypto2, objects, mailbox) {
|
|
3293
|
+
const blobs = createInMemoryBlobs(clock, crypto2, objects);
|
|
3294
|
+
const taskState = new InMemoryTaskAttemptState(clock);
|
|
3295
|
+
const tasks = new InMemoryTaskAttemptStore(clock, taskState);
|
|
2621
3296
|
return {
|
|
2622
3297
|
stores: {
|
|
2623
3298
|
activity: new InMemoryActivityStore(clock),
|
|
2624
3299
|
approvals: new InMemoryApprovalTimelineStore(clock),
|
|
2625
3300
|
devices: new InMemoryDeviceDirectory(),
|
|
2626
3301
|
pairingCodes: new InMemoryPairingCodeStore(clock),
|
|
2627
|
-
nonces: new InMemoryNonceStore(clock,
|
|
3302
|
+
nonces: new InMemoryNonceStore(clock, crypto2),
|
|
2628
3303
|
dedup: new InMemoryInboundDedupStore(),
|
|
2629
|
-
tasks
|
|
3304
|
+
tasks,
|
|
3305
|
+
cancellations: new InMemoryTaskCancellationStore(taskState, mailbox),
|
|
2630
3306
|
receipts: new InMemoryRequestReceiptStore(clock),
|
|
3307
|
+
egress: new InMemoryAgentEgressStore(clock),
|
|
2631
3308
|
proofReceipts: new InMemoryProofRequestReceiptStore(clock),
|
|
2632
3309
|
blobs: blobs.blobs,
|
|
2633
3310
|
rateLimiter: new AllowAllRateLimiter()
|
|
@@ -2643,9 +3320,14 @@ function systemClock() {
|
|
|
2643
3320
|
}
|
|
2644
3321
|
function createInMemoryByokCloud(options = {}) {
|
|
2645
3322
|
const clock = options.clock ?? systemClock();
|
|
2646
|
-
const
|
|
3323
|
+
const crypto2 = options.crypto ?? createWebCrypto();
|
|
2647
3324
|
const core = createInMemoryCoreStores({ clock }).stores;
|
|
2648
|
-
const { stores, blobContentProxy } = createInMemoryCloudStores(
|
|
3325
|
+
const { stores, blobContentProxy } = createInMemoryCloudStores(
|
|
3326
|
+
clock,
|
|
3327
|
+
crypto2,
|
|
3328
|
+
core.objects,
|
|
3329
|
+
core.mailbox
|
|
3330
|
+
);
|
|
2649
3331
|
const tokenSigner = options.tokenSigner ?? createHmacTokenSigner(globalThis.crypto.getRandomValues(new Uint8Array(TOKEN_SECRET_BYTES)), clock);
|
|
2650
3332
|
const cloud = createByokCloud({
|
|
2651
3333
|
core,
|
|
@@ -2655,7 +3337,7 @@ function createInMemoryByokCloud(options = {}) {
|
|
|
2655
3337
|
// pairing is what keeps hosted-in-memory behavior identical to what it was
|
|
2656
3338
|
// before the port narrowed.
|
|
2657
3339
|
blobContentProxy,
|
|
2658
|
-
crypto,
|
|
3340
|
+
crypto: crypto2,
|
|
2659
3341
|
tokenSigner,
|
|
2660
3342
|
clock,
|
|
2661
3343
|
capabilities: options.capabilities ?? fullCapabilityDeclaration(),
|
|
@@ -2684,7 +3366,7 @@ function createInMemoryByokCloud(options = {}) {
|
|
|
2684
3366
|
...options.skillPacks === void 0 ? {} : { skillPacks: options.skillPacks },
|
|
2685
3367
|
...options.skillPackPageLimit === void 0 ? {} : { skillPackPageLimit: options.skillPackPageLimit }
|
|
2686
3368
|
});
|
|
2687
|
-
return { cloud, core, stores, blobContentProxy, clock, crypto };
|
|
3369
|
+
return { cloud, core, stores, blobContentProxy, clock, crypto: crypto2 };
|
|
2688
3370
|
}
|
|
2689
3371
|
function authenticateHostedDeviceAssertion(input, deps) {
|
|
2690
3372
|
return authenticateDeviceAssertion(input, {
|
|
@@ -2847,10 +3529,12 @@ var TASK_ATTEMPT_STATUSES = [
|
|
|
2847
3529
|
"offered",
|
|
2848
3530
|
"claimed",
|
|
2849
3531
|
"running",
|
|
3532
|
+
"cancel_requested",
|
|
2850
3533
|
"complete",
|
|
2851
3534
|
"failed",
|
|
2852
3535
|
"cancelled"
|
|
2853
3536
|
];
|
|
3537
|
+
var BLOB_READ_ERROR_CODES = ["blob_upstream_unavailable", "blob_upstream_stream_interrupted"];
|
|
2854
3538
|
var CLOUD_STORE_NAMES = [
|
|
2855
3539
|
"activity",
|
|
2856
3540
|
"approvals",
|
|
@@ -2859,7 +3543,9 @@ var CLOUD_STORE_NAMES = [
|
|
|
2859
3543
|
"nonces",
|
|
2860
3544
|
"dedup",
|
|
2861
3545
|
"tasks",
|
|
3546
|
+
"cancellations",
|
|
2862
3547
|
"receipts",
|
|
3548
|
+
"egress",
|
|
2863
3549
|
"proofReceipts",
|
|
2864
3550
|
"blobs",
|
|
2865
3551
|
"rateLimiter"
|
|
@@ -2869,12 +3555,14 @@ var CLOUD_STORE_NAMES = [
|
|
|
2869
3555
|
var CLOUD_PORT_METHODS = {
|
|
2870
3556
|
activity: ["append", "read"],
|
|
2871
3557
|
approvals: ["append", "read"],
|
|
2872
|
-
devices: ["register", "get", "revoke", "list", "resolveByDeviceId"],
|
|
3558
|
+
devices: ["register", "get", "revoke", "list", "readiness", "recordCapabilities", "resolveByDeviceId"],
|
|
2873
3559
|
pairingCodes: ["issue", "redeem"],
|
|
2874
3560
|
nonces: ["issue", "validate", "markUsed"],
|
|
2875
3561
|
dedup: ["checkAndRecord"],
|
|
2876
|
-
tasks: ["open", "get", "claim", "recordStatus"],
|
|
3562
|
+
tasks: ["open", "reserveAgentOffer", "get", "getMany", "claim", "recordStatus"],
|
|
3563
|
+
cancellations: ["request"],
|
|
2877
3564
|
receipts: ["record", "get"],
|
|
3565
|
+
egress: ["record", "get"],
|
|
2878
3566
|
proofReceipts: ["record", "get"],
|
|
2879
3567
|
// Three methods, not six: the byte-proxy trio moved to `BlobContentProxy`,
|
|
2880
3568
|
// which is a composition input rather than a port and therefore has no row
|
|
@@ -2890,12 +3578,14 @@ var CLOUD_PORT_INTERFACES = {
|
|
|
2890
3578
|
nonces: "NonceStore",
|
|
2891
3579
|
dedup: "InboundDedupStore",
|
|
2892
3580
|
tasks: "TaskAttemptStore",
|
|
3581
|
+
cancellations: "TaskCancellationStore",
|
|
2893
3582
|
receipts: "RequestReceiptStore",
|
|
3583
|
+
egress: "AgentEgressStore",
|
|
2894
3584
|
proofReceipts: "ProofRequestReceiptStore",
|
|
2895
3585
|
blobs: "CloudBlobStore",
|
|
2896
3586
|
rateLimiter: "InboundRateLimiter"
|
|
2897
3587
|
};
|
|
2898
3588
|
|
|
2899
|
-
export { ACCESS_TOKEN_TTL_SECONDS, APPROVAL_SUMMARY_MAX_BYTES, ActivityAppendRequestSchema, AllowAllRateLimiter, ApprovalObservationSchema, ApprovalTimelineEventSchema, BLOB_URL_TTL_MS, BoardFeedClient, BoardFeedRetryableError, BoardFeedStoppedError, ByokCloudError, CLOUD_CAPABILITIES, CLOUD_ERROR_CODES, CLOUD_PORT_INTERFACES, CLOUD_PORT_METHODS, CLOUD_STORE_NAMES, CapabilitiesResponseSchema, CloudRouteRegistry, DEDUP_RING_CAPACITY, DEFAULT_ACTIVITY_BOUNDS, DEFAULT_ACTIVITY_CAPACITY, DEFAULT_ACTIVITY_MAX_BYTES, DEFAULT_ACTIVITY_MAX_EVENTS, DEFAULT_ACTIVITY_TTL_MS, DEFAULT_APPROVAL_TIMELINE_CAPACITY, DEFAULT_APPROVAL_TIMELINE_TTL_MS, DEFAULT_BOARD_CHANNEL_MAX_BYTES, DEFAULT_BOARD_PAGE_LIMIT, DEFAULT_BOARD_STREAM_HEARTBEAT_INTERVAL_MS, DEFAULT_BOARD_STREAM_QUERY_INTERVAL_MS, DEFAULT_BOARD_STREAM_RECONCILIATION_INTERVAL_MS, DEFAULT_BOARD_TITLE_MAX_BYTES, DEFAULT_DEVICE_PROOF_CLOCK_SKEW_MS, DEFAULT_DEVICE_PROOF_MAX_LIFETIME_MS, DEFAULT_EVENTS_PAGE_LIMIT, DEFAULT_LONG_POLL_HOLD_MS, DEFAULT_LONG_POLL_INTERVAL_MS, DEFAULT_MAX_BLOB_SIZE_BYTES, DEFAULT_MAX_TRUTH_REQUEST_BYTES, DEFAULT_PRESENCE_DETAIL_MAX_BYTES, DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS, DEFAULT_PRESENCE_TTL_MS, DEFAULT_SKILL_PACK_PAGE_LIMIT, DEVICE_IDENTITY_PROOF_KEY_EPOCH, DEVICE_IDENTITY_PROOF_KEY_ID, InMemoryActivityStore, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryDeviceDirectory, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryProofRequestReceiptStore, InMemoryRequestReceiptStore, InMemoryTaskAttemptStore, MAX_DEVICE_PROOF_CLOCK_SKEW_MS, MAX_DEVICE_PROOF_HEADER_BYTES, MAX_DEVICE_PROOF_MAX_LIFETIME_MS, NONCE_TTL_MS, PAIRING_CODE_TTL_MS, ROUTE_CLASSES, ROUTE_METHODS, TASK_ATTEMPT_STATUSES, TRUTH_BATCH_MAX_RECORDS, TRUTH_INLINE_CONTENT_TYPE, TRUTH_LABEL_MAX_LENGTH, TRUTH_MANIFEST_MAX_LIMIT, TRUTH_RECORD_CAPABILITY, TRUTH_RECORD_KEY_MAX_LENGTH, TRUTH_REQUEST_ID_MAX_LENGTH, TimelineEventSchema, TruthBodyInputSchema, TruthCommitError, TruthCommitResponseSchema, TruthRecordKeySchema, TruthRecordMetadataSchema, TruthWriteRequestSchema, activityCursor, approvalTimelineCursor, authenticateBearer, authenticateDeviceProof, authenticateHostedDeviceAssertion, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, projectTerminalResult, projectTimelineEvents, routeKey, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, verifyNonceSignature };
|
|
3589
|
+
export { ACCESS_TOKEN_TTL_SECONDS, AGENT_HOME_CONTRACT_CAPABILITY, APPROVAL_SUMMARY_MAX_BYTES, ActivityAppendRequestSchema, AllowAllRateLimiter, ApprovalObservationSchema, ApprovalTimelineEventSchema, BLOB_READ_ERROR_CODES, BLOB_URL_TTL_MS, BoardFeedClient, BoardFeedRetryableError, BoardFeedStoppedError, ByokCloudError, CLOUD_CAPABILITIES, CLOUD_ERROR_CODES, CLOUD_PORT_INTERFACES, CLOUD_PORT_METHODS, CLOUD_STORE_NAMES, CapabilitiesResponseSchema, CloudRouteRegistry, DEDUP_RING_CAPACITY, DEFAULT_ACTIVITY_BOUNDS, DEFAULT_ACTIVITY_CAPACITY, DEFAULT_ACTIVITY_MAX_BYTES, DEFAULT_ACTIVITY_MAX_EVENTS, DEFAULT_ACTIVITY_TTL_MS, DEFAULT_APPROVAL_TIMELINE_CAPACITY, DEFAULT_APPROVAL_TIMELINE_TTL_MS, DEFAULT_BOARD_CHANNEL_MAX_BYTES, DEFAULT_BOARD_PAGE_LIMIT, DEFAULT_BOARD_STREAM_HEARTBEAT_INTERVAL_MS, DEFAULT_BOARD_STREAM_QUERY_INTERVAL_MS, DEFAULT_BOARD_STREAM_RECONCILIATION_INTERVAL_MS, DEFAULT_BOARD_TITLE_MAX_BYTES, DEFAULT_DEVICE_PROOF_CLOCK_SKEW_MS, DEFAULT_DEVICE_PROOF_MAX_LIFETIME_MS, DEFAULT_EVENTS_PAGE_LIMIT, DEFAULT_LONG_POLL_HOLD_MS, DEFAULT_LONG_POLL_INTERVAL_MS, DEFAULT_MAX_BLOB_SIZE_BYTES, DEFAULT_MAX_TRUTH_REQUEST_BYTES, DEFAULT_PRESENCE_DETAIL_MAX_BYTES, DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS, DEFAULT_PRESENCE_TTL_MS, DEFAULT_SKILL_PACK_PAGE_LIMIT, DEVICE_IDENTITY_PROOF_KEY_EPOCH, DEVICE_IDENTITY_PROOF_KEY_ID, InMemoryActivityStore, InMemoryAgentEgressStore, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryDeviceDirectory, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryProofRequestReceiptStore, InMemoryRequestReceiptStore, InMemoryTaskAttemptStore, InMemoryTaskCancellationStore, MAX_DEVICE_PROOF_CLOCK_SKEW_MS, MAX_DEVICE_PROOF_HEADER_BYTES, MAX_DEVICE_PROOF_MAX_LIFETIME_MS, NONCE_TTL_MS, PAIRING_CODE_TTL_MS, ROUTE_CLASSES, ROUTE_METHODS, TASK_ATTEMPT_STATUSES, TRUTH_BATCH_MAX_RECORDS, TRUTH_INLINE_CONTENT_TYPE, TRUTH_LABEL_MAX_LENGTH, TRUTH_MANIFEST_MAX_LIMIT, TRUTH_RECORD_CAPABILITY, TRUTH_RECORD_KEY_MAX_LENGTH, TRUTH_REQUEST_ID_MAX_LENGTH, TimelineEventSchema, TruthBodyInputSchema, TruthCommitError, TruthCommitResponseSchema, TruthRecordKeySchema, TruthRecordMetadataSchema, TruthWriteRequestSchema, activityCursor, approvalTimelineCursor, authenticateBearer, authenticateDeviceProof, authenticateHostedDeviceAssertion, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, projectTerminalResult, projectTimelineEvents, routeKey, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, verifyNonceSignature };
|
|
2900
3590
|
//# sourceMappingURL=index.js.map
|
|
2901
3591
|
//# sourceMappingURL=index.js.map
|