@byok-sdk/cloud 0.11.0 → 0.13.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/dist/activity.d.ts +13 -0
- package/dist/agent-home-projections.d.ts +2 -2
- package/dist/agent-reliability.d.ts +9 -0
- package/dist/approval-control.d.ts +75 -0
- package/dist/approval-timeline.d.ts +60 -14
- package/dist/auth/bearer.d.ts +17 -0
- package/dist/auth/plane.d.ts +3 -4
- package/dist/cloud.d.ts +116 -5
- package/dist/composition/in-memory.d.ts +4 -0
- package/dist/coordination.d.ts +8 -0
- package/dist/errors.d.ts +9 -0
- package/dist/handlers/blobs.d.ts +2 -0
- package/dist/handlers/events.d.ts +10 -4
- package/dist/handlers/messages.d.ts +7 -0
- package/dist/inbound.d.ts +45 -5
- package/dist/index.d.ts +12 -8
- package/dist/index.js +1029 -223
- package/dist/index.js.map +1 -1
- package/dist/steer-control.d.ts +81 -0
- package/dist/stores/in-memory/agent-egress.d.ts +1 -1
- package/dist/stores/in-memory/approval-timeline.d.ts +2 -1
- package/dist/stores/in-memory/blobs.d.ts +1 -0
- package/dist/stores/in-memory/dedup.d.ts +8 -5
- package/dist/stores/in-memory/nonces.d.ts +1 -2
- package/dist/stores/in-memory/pairing-codes.d.ts +9 -5
- package/dist/stores/in-memory/task-attempts.d.ts +29 -1
- package/dist/stores/ports.d.ts +177 -29
- package/dist/tenant-stores.d.ts +29 -2
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,73 @@
|
|
|
1
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, PROTOCOL_VERSION, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, encodeEnvelope,
|
|
3
|
+
import { AgentEventOrUnknownSchema, PROTOCOL_VERSION, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, encodeEnvelope, decodeEnvelope, AgentHomeProjectionCompletionRequestSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, BYOK_CAPABILITIES_PATH, BYOK_AGENT_MEMORY_PROJECTIONS_PATH, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, BYOK_AGENT_HOME_PROJECTION_COMPLETION_ROUTE, 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, AgentHomeProjectionPayloadSchema, AgentContentReadPayloadSchema, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, TaskOfferForAgentWithEgressFreshPayloadSchema, AgentMessageServerContextSchema, TaskOfferForAgentWithEgressPayloadSchema, TaskOfferForAgentPayloadSchema, AgentMemoryProjectionMutationSchema, AgentMemoryProjectionReceiptSchema, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionEraseResultSchema, STRICT_AGENT_ONLY_CAPABILITY, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AgentRefSchema, PairRequestSchema, PairResponseSchema, ChallengeRequestSchema, TokenRequestSchema, AgentMemoryProjectionCommitRequestSchema, agentMemoryProjectionBase64UrlByteLength, MessagesSendRequestSchema, PresencePublishRequestSchema, CreateBlobRequestSchema, byokBlobContentPath, AgentEgressAckPayloadSchema, AgentContentReceiptPayloadSchema } from '@byok-sdk/protocol';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { Hono } from 'hono';
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
8
|
|
|
9
|
+
// src/approval-control.ts
|
|
10
|
+
var StaleApprovalError = class extends Error {
|
|
11
|
+
constructor(taskId, requestedApprovalId, currentApprovalId) {
|
|
12
|
+
super(
|
|
13
|
+
`cannot resolve approval ${requestedApprovalId} for task ${taskId}: the currently pending approval is ${currentApprovalId ?? "(none recorded)"}`
|
|
14
|
+
);
|
|
15
|
+
this.taskId = taskId;
|
|
16
|
+
this.requestedApprovalId = requestedApprovalId;
|
|
17
|
+
this.currentApprovalId = currentApprovalId;
|
|
18
|
+
this.name = "StaleApprovalError";
|
|
19
|
+
}
|
|
20
|
+
taskId;
|
|
21
|
+
requestedApprovalId;
|
|
22
|
+
currentApprovalId;
|
|
23
|
+
};
|
|
24
|
+
function pendingApproval(tail) {
|
|
25
|
+
if (tail === void 0) return void 0;
|
|
26
|
+
let pending;
|
|
27
|
+
for (const entry of tail.entries) {
|
|
28
|
+
const event = entry.event;
|
|
29
|
+
if (event.type === "approval_requested") {
|
|
30
|
+
pending = {
|
|
31
|
+
sourceEnvelopeId: entry.sourceEnvelopeId,
|
|
32
|
+
revision: entry.revision,
|
|
33
|
+
...event.approvalId === void 0 ? {} : { approvalId: event.approvalId }
|
|
34
|
+
};
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (pending === void 0) continue;
|
|
38
|
+
if (pending.approvalId === void 0 || pending.approvalId === event.approvalId) {
|
|
39
|
+
pending = void 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return pending;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/steer-control.ts
|
|
46
|
+
function steerRejectionMessage(taskId, code, status, runtime) {
|
|
47
|
+
switch (code) {
|
|
48
|
+
case "task_terminal":
|
|
49
|
+
return `cannot steer task ${taskId}: task is already terminal (status ${status})`;
|
|
50
|
+
case "task_not_running":
|
|
51
|
+
return `cannot steer task ${taskId}: not running (status ${status})`;
|
|
52
|
+
case "steer_unsupported_runtime":
|
|
53
|
+
return `cannot steer task ${taskId}: claimed runtime ${runtime ?? "(unknown)"} does not support steering`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
var SteerRejectedError = class extends Error {
|
|
57
|
+
constructor(taskId, code, status, runtime) {
|
|
58
|
+
super(steerRejectionMessage(taskId, code, status, runtime));
|
|
59
|
+
this.taskId = taskId;
|
|
60
|
+
this.code = code;
|
|
61
|
+
this.status = status;
|
|
62
|
+
this.runtime = runtime;
|
|
63
|
+
this.name = "SteerRejectedError";
|
|
64
|
+
}
|
|
65
|
+
taskId;
|
|
66
|
+
code;
|
|
67
|
+
status;
|
|
68
|
+
runtime;
|
|
69
|
+
};
|
|
70
|
+
|
|
9
71
|
// src/crypto/web-crypto.ts
|
|
10
72
|
var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
11
73
|
var HEX_DIGITS = "0123456789abcdef";
|
|
@@ -182,10 +244,8 @@ function createAuthPlane(deps) {
|
|
|
182
244
|
});
|
|
183
245
|
},
|
|
184
246
|
async redeemAndRegister(input) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return stores.devices.register(claims.tenantId, {
|
|
188
|
-
productId: claims.productId,
|
|
247
|
+
return stores.pairing.redeemAndRegister({
|
|
248
|
+
pairingCode: input.pairingCode,
|
|
189
249
|
deviceId: `dev_${crypto2.randomUuid()}`,
|
|
190
250
|
deviceName: input.deviceName,
|
|
191
251
|
devicePublicKey: input.devicePublicKey,
|
|
@@ -202,11 +262,8 @@ function createAuthPlane(deps) {
|
|
|
202
262
|
issueNonce(device) {
|
|
203
263
|
return stores.nonces.issue(device.tenantId, device.deviceId);
|
|
204
264
|
},
|
|
205
|
-
|
|
206
|
-
return stores.nonces.
|
|
207
|
-
},
|
|
208
|
-
consumeNonce(device, nonce) {
|
|
209
|
-
return stores.nonces.markUsed(device.tenantId, nonce);
|
|
265
|
+
consumeNonceIfValid(device, nonce) {
|
|
266
|
+
return stores.nonces.consumeIfValid(device.tenantId, device.deviceId, nonce);
|
|
210
267
|
},
|
|
211
268
|
verifySignature(device, nonce, signature) {
|
|
212
269
|
return verifyNonceSignature(crypto2, device.devicePublicKey, nonce, signature);
|
|
@@ -326,6 +383,15 @@ var CLOUD_ERROR_CODES = {
|
|
|
326
383
|
activity_batch_too_large: "activity_batch_too_large",
|
|
327
384
|
/** Host control-plane task lookup is tenant-closed and found no task. */
|
|
328
385
|
task_not_found: "task_not_found",
|
|
386
|
+
/**
|
|
387
|
+
* A host `approveTask`/`rejectTask` names a task with no approval to
|
|
388
|
+
* resolve: the durable timeline holds no unresolved `approval_requested`
|
|
389
|
+
* (including a tail that has expired or was never written), the attempt has
|
|
390
|
+
* already reached a terminal status, or no device ever claimed it so there
|
|
391
|
+
* is nothing to notify. Fail-closed by construction — cloud never invents a
|
|
392
|
+
* pending approval to make the call succeed.
|
|
393
|
+
*/
|
|
394
|
+
task_not_awaiting_approval: "task_not_awaiting_approval",
|
|
329
395
|
/** Durable target-device capability is absent, revoked, or unavailable. */
|
|
330
396
|
agent_capability_missing: "agent_capability_missing",
|
|
331
397
|
/** Inbound Agent identity did not exactly match the offered identity. */
|
|
@@ -392,6 +458,12 @@ var TimelineEventSchema = z.object({
|
|
|
392
458
|
sourceEnvelopeId: SourceEnvelopeIdSchema,
|
|
393
459
|
batchSeq: z.number().int().nonnegative(),
|
|
394
460
|
eventIndex: z.number().int().nonnegative(),
|
|
461
|
+
/**
|
|
462
|
+
* The producer-declared loss attached to this source batch. It travels with
|
|
463
|
+
* every retained event so a replay includes it in the canonical batch
|
|
464
|
+
* binding rather than treating the aggregate as an incidental counter.
|
|
465
|
+
*/
|
|
466
|
+
sourceDropped: z.number().int().nonnegative().optional(),
|
|
395
467
|
receivedAt: z.iso.datetime(),
|
|
396
468
|
event: AgentEventOrUnknownSchema
|
|
397
469
|
});
|
|
@@ -425,11 +497,24 @@ function projectTimelineEvents(input, receivedAt) {
|
|
|
425
497
|
sourceEnvelopeId: input.sourceEnvelopeId,
|
|
426
498
|
batchSeq: input.batchSeq,
|
|
427
499
|
eventIndex,
|
|
500
|
+
sourceDropped: input.dropped,
|
|
428
501
|
receivedAt,
|
|
429
502
|
event
|
|
430
503
|
})
|
|
431
504
|
);
|
|
432
505
|
}
|
|
506
|
+
function activitySourceBatchState(entries, input) {
|
|
507
|
+
const existing = entries.filter((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId).sort((left, right) => left.eventIndex - right.eventIndex);
|
|
508
|
+
if (existing.length === 0) return "absent";
|
|
509
|
+
if (existing.length !== input.events.length) return "conflict";
|
|
510
|
+
for (const [eventIndex, event] of input.events.entries()) {
|
|
511
|
+
const stored = existing[eventIndex];
|
|
512
|
+
if (stored === void 0 || stored.taskId !== input.taskId || stored.batchSeq !== input.batchSeq || stored.eventIndex !== eventIndex || stored.sourceDropped !== input.dropped || JSON.stringify(stored.event) !== JSON.stringify(event)) {
|
|
513
|
+
return "conflict";
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return "same";
|
|
517
|
+
}
|
|
433
518
|
function parseTimelineEvents(value) {
|
|
434
519
|
return z.array(TimelineEventSchema).parse(value);
|
|
435
520
|
}
|
|
@@ -474,6 +559,14 @@ function assertBoardLabels(channel, title, limits) {
|
|
|
474
559
|
);
|
|
475
560
|
}
|
|
476
561
|
}
|
|
562
|
+
function assertTaskAttemptListLimit(limit) {
|
|
563
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
564
|
+
throw new ByokCloudError(
|
|
565
|
+
"coordination_input_invalid",
|
|
566
|
+
`Task attempt list limit must be a positive integer; received ${String(limit)}.`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
477
570
|
function validateActivityEvents(events, dropped, bounds) {
|
|
478
571
|
if (!Number.isSafeInteger(dropped) || dropped < 0) {
|
|
479
572
|
throw new ByokCloudError(
|
|
@@ -525,6 +618,9 @@ async function authenticateBearer(header, deps) {
|
|
|
525
618
|
const device = await deps.devices.get(tenantId(claims.tenantId), claims.deviceId);
|
|
526
619
|
if (device === void 0 || device.revoked) return void 0;
|
|
527
620
|
if (device.productId !== claims.productId) return void 0;
|
|
621
|
+
if (deps.instanceProductId !== void 0 && claims.productId !== deps.instanceProductId) {
|
|
622
|
+
return void 0;
|
|
623
|
+
}
|
|
528
624
|
return {
|
|
529
625
|
kind: "device",
|
|
530
626
|
tenantId: device.tenantId,
|
|
@@ -541,6 +637,7 @@ function tenantStoresFor(principal, root) {
|
|
|
541
637
|
mailbox: {
|
|
542
638
|
append: (input) => core.mailbox.append(tenant, input),
|
|
543
639
|
readAfter: (query) => core.mailbox.readAfter(tenant, query),
|
|
640
|
+
recordDelivery: (input) => core.mailbox.recordDelivery(tenant, input),
|
|
544
641
|
advanceCursor: (input) => core.mailbox.advanceCursor(tenant, input),
|
|
545
642
|
readCursor: (deviceId) => core.mailbox.readCursor(tenant, deviceId)
|
|
546
643
|
},
|
|
@@ -578,8 +675,12 @@ function tenantStoresFor(principal, root) {
|
|
|
578
675
|
tasks: {
|
|
579
676
|
open: (input) => cloud.tasks.open(tenant, input),
|
|
580
677
|
reserveAgentOffer: (input) => cloud.tasks.reserveAgentOffer(tenant, input),
|
|
678
|
+
reserveAgentMessage: (input) => cloud.tasks.reserveAgentMessage(tenant, input),
|
|
679
|
+
readAgentMessage: (input) => cloud.tasks.readAgentMessage(tenant, input),
|
|
680
|
+
finalizeAgentMessage: (input) => cloud.tasks.finalizeAgentMessage(tenant, input),
|
|
581
681
|
get: (taskId) => cloud.tasks.get(tenant, taskId),
|
|
582
682
|
getMany: (taskIds) => cloud.tasks.getMany(tenant, taskIds),
|
|
683
|
+
list: (query) => cloud.tasks.list(tenant, query),
|
|
583
684
|
claim: (input) => cloud.tasks.claim(tenant, input),
|
|
584
685
|
recordStatus: (input) => cloud.tasks.recordStatus(tenant, input)
|
|
585
686
|
},
|
|
@@ -587,7 +688,8 @@ function tenantStoresFor(principal, root) {
|
|
|
587
688
|
request: (input) => cloud.cancellations.request(tenant, input)
|
|
588
689
|
},
|
|
589
690
|
dedup: {
|
|
590
|
-
checkAndRecord: (deviceId, envelopeId) => cloud.dedup.checkAndRecord(tenant, deviceId, envelopeId)
|
|
691
|
+
checkAndRecord: (deviceId, envelopeId) => cloud.dedup.checkAndRecord(tenant, deviceId, envelopeId),
|
|
692
|
+
checkAndRecordAgent: (deviceId, agentRef, envelopeId) => cloud.dedup.checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId)
|
|
591
693
|
},
|
|
592
694
|
receipts: {
|
|
593
695
|
record: (input) => cloud.receipts.record(tenant, input),
|
|
@@ -595,7 +697,7 @@ function tenantStoresFor(principal, root) {
|
|
|
595
697
|
},
|
|
596
698
|
egress: {
|
|
597
699
|
record: (input) => cloud.egress.record(tenant, input),
|
|
598
|
-
get: (deviceId, eventId) => cloud.egress.get(tenant, deviceId, eventId)
|
|
700
|
+
get: (deviceId, agentRef, eventId) => cloud.egress.get(tenant, deviceId, agentRef, eventId)
|
|
599
701
|
},
|
|
600
702
|
blobs: {
|
|
601
703
|
createUpload: (reservation) => cloud.blobs.createUpload(tenant, reservation),
|
|
@@ -627,6 +729,7 @@ async function readBoundedJsonBody(c, maximum) {
|
|
|
627
729
|
if (declaredLength !== void 0) {
|
|
628
730
|
const parsedLength = parseContentLength(declaredLength);
|
|
629
731
|
if (parsedLength !== void 0 && parsedLength > BigInt(maximum)) {
|
|
732
|
+
cancelBody(c.req.raw.body);
|
|
630
733
|
return { body: void 0, tooLarge: true };
|
|
631
734
|
}
|
|
632
735
|
}
|
|
@@ -640,10 +743,7 @@ async function readBoundedJsonBody(c, maximum) {
|
|
|
640
743
|
const { done, value } = await reader.read();
|
|
641
744
|
if (done) break;
|
|
642
745
|
if (value.byteLength > maximum - total) {
|
|
643
|
-
|
|
644
|
-
await reader.cancel();
|
|
645
|
-
} catch {
|
|
646
|
-
}
|
|
746
|
+
cancelBody(reader);
|
|
647
747
|
return { body: void 0, tooLarge: true };
|
|
648
748
|
}
|
|
649
749
|
if (value.byteLength === 0) continue;
|
|
@@ -663,6 +763,14 @@ async function readBoundedJsonBody(c, maximum) {
|
|
|
663
763
|
}
|
|
664
764
|
return { body: parseJsonBytes(bytes), tooLarge: false };
|
|
665
765
|
}
|
|
766
|
+
function cancelBody(body) {
|
|
767
|
+
if (body === null) return;
|
|
768
|
+
try {
|
|
769
|
+
void Promise.resolve(body.cancel()).catch(() => {
|
|
770
|
+
});
|
|
771
|
+
} catch {
|
|
772
|
+
}
|
|
773
|
+
}
|
|
666
774
|
function parseContentLength(value) {
|
|
667
775
|
const normalized = value.trim();
|
|
668
776
|
if (!/^\d+$/u.test(normalized)) return void 0;
|
|
@@ -781,11 +889,43 @@ function blobUploadContentHandler(deps) {
|
|
|
781
889
|
if (!query.success || !await deps.contentProxy.verifySignedUrl(blobId, "put", query.data.sig, query.data.exp)) {
|
|
782
890
|
return c.json({ error: "invalid or expired signature" }, 401);
|
|
783
891
|
}
|
|
784
|
-
const
|
|
892
|
+
const expectedBytes = await deps.contentProxy.expectedUploadBytes(blobId);
|
|
893
|
+
if (expectedBytes === void 0) return c.json({ error: "blob not found" }, 404);
|
|
894
|
+
const absoluteCeiling = BigInt(deps.maxUploadBytes);
|
|
895
|
+
if (expectedBytes > absoluteCeiling) return c.json({ error: "upload exceeds size limit" }, 413);
|
|
896
|
+
const contentLength = c.req.header("content-length");
|
|
897
|
+
if (contentLength !== void 0 && /^\d+$/u.test(contentLength) && BigInt(contentLength) > expectedBytes) {
|
|
898
|
+
return c.json({ error: "upload exceeds declared size" }, 413);
|
|
899
|
+
}
|
|
900
|
+
const body = await readBoundedBody(c.req.raw.body, Number(expectedBytes));
|
|
901
|
+
if (body.tooLarge) return c.json({ error: "upload exceeds declared size" }, 413);
|
|
902
|
+
const result = await deps.contentProxy.writeContent(blobId, body.data);
|
|
785
903
|
if (!result.ok) return c.json({ error: result.reason }, 422);
|
|
786
904
|
return c.body(null, 204);
|
|
787
905
|
};
|
|
788
906
|
}
|
|
907
|
+
async function readBoundedBody(body, ceiling) {
|
|
908
|
+
if (body === null) return { tooLarge: false, data: new Uint8Array() };
|
|
909
|
+
const reader = body.getReader();
|
|
910
|
+
const data = new Uint8Array(ceiling);
|
|
911
|
+
let offset = 0;
|
|
912
|
+
try {
|
|
913
|
+
for (; ; ) {
|
|
914
|
+
const next = await reader.read();
|
|
915
|
+
if (next.done) break;
|
|
916
|
+
const chunk = next.value;
|
|
917
|
+
if (chunk.byteLength > ceiling - offset) {
|
|
918
|
+
void reader.cancel().catch(() => void 0);
|
|
919
|
+
return { tooLarge: true };
|
|
920
|
+
}
|
|
921
|
+
data.set(chunk, offset);
|
|
922
|
+
offset += chunk.byteLength;
|
|
923
|
+
}
|
|
924
|
+
} finally {
|
|
925
|
+
reader.releaseLock();
|
|
926
|
+
}
|
|
927
|
+
return { tooLarge: false, data: data.subarray(0, offset) };
|
|
928
|
+
}
|
|
789
929
|
function blobDownloadContentHandler(deps) {
|
|
790
930
|
return async (c) => {
|
|
791
931
|
const blobId = c.req.param("id") ?? "";
|
|
@@ -830,9 +970,12 @@ function renderStorageError(c, error) {
|
|
|
830
970
|
function capabilitiesHandler(deps) {
|
|
831
971
|
return async (c) => c.json(deps.declaration, 200);
|
|
832
972
|
}
|
|
973
|
+
var AUTH_JSON_BODY_MAX_BYTES = 16 * 1024;
|
|
833
974
|
function pairHandler(deps) {
|
|
834
975
|
return async (c) => {
|
|
835
|
-
const
|
|
976
|
+
const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
|
|
977
|
+
if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
|
|
978
|
+
const parsed = PairRequestSchema.safeParse(body.body);
|
|
836
979
|
if (!parsed.success) {
|
|
837
980
|
return c.json(
|
|
838
981
|
{
|
|
@@ -855,7 +998,9 @@ function pairHandler(deps) {
|
|
|
855
998
|
}
|
|
856
999
|
function challengeHandler(deps) {
|
|
857
1000
|
return async (c) => {
|
|
858
|
-
const
|
|
1001
|
+
const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
|
|
1002
|
+
if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
|
|
1003
|
+
const parsed = ChallengeRequestSchema.safeParse(body.body);
|
|
859
1004
|
if (!parsed.success) return c.json({ error: "deviceId is required" }, 400);
|
|
860
1005
|
const device = await deps.auth.resolveDevice(parsed.data.deviceId);
|
|
861
1006
|
if (device === void 0) return c.json({ error: "unknown or revoked device" }, 401);
|
|
@@ -865,18 +1010,19 @@ function challengeHandler(deps) {
|
|
|
865
1010
|
}
|
|
866
1011
|
function tokenHandler(deps) {
|
|
867
1012
|
return async (c) => {
|
|
868
|
-
const
|
|
1013
|
+
const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
|
|
1014
|
+
if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
|
|
1015
|
+
const parsed = TokenRequestSchema.safeParse(body.body);
|
|
869
1016
|
if (!parsed.success) return c.json({ error: "deviceId, nonce, and signature are required" }, 400);
|
|
870
1017
|
const { deviceId, nonce, signature } = parsed.data;
|
|
871
1018
|
const device = await deps.auth.resolveDevice(deviceId);
|
|
872
1019
|
if (device === void 0) return c.json({ error: "unknown or revoked device" }, 401);
|
|
873
|
-
if (!await deps.auth.validateNonce(device, nonce)) {
|
|
874
|
-
return c.json({ error: "invalid, expired, or already-used nonce" }, 401);
|
|
875
|
-
}
|
|
876
1020
|
if (!await deps.auth.verifySignature(device, nonce, signature)) {
|
|
877
1021
|
return c.json({ error: "invalid signature" }, 401);
|
|
878
1022
|
}
|
|
879
|
-
await deps.auth.
|
|
1023
|
+
if (!await deps.auth.consumeNonceIfValid(device, nonce)) {
|
|
1024
|
+
return c.json({ error: "invalid, expired, or already-used nonce" }, 401);
|
|
1025
|
+
}
|
|
880
1026
|
const { accessToken, expiresAt } = await deps.auth.mintAccessToken(device);
|
|
881
1027
|
const response = { accessToken, expiresAt };
|
|
882
1028
|
return c.json(response, 200);
|
|
@@ -884,11 +1030,20 @@ function tokenHandler(deps) {
|
|
|
884
1030
|
}
|
|
885
1031
|
var CLOUD_PROTOCOL_CAPABILITIES = [
|
|
886
1032
|
"result-document",
|
|
1033
|
+
// The kernel's inbound handles `task.approval_resolved` on both the plain
|
|
1034
|
+
// and the approval-timeline paths (`packages/cloud/src/inbound.ts:576,610`).
|
|
1035
|
+
// Without this flag `TaskRunner.sendApprovalResolved` returns early and a
|
|
1036
|
+
// locally-resolved approval is never reported on the wire at all.
|
|
1037
|
+
"approval_resolved",
|
|
887
1038
|
"agent-home-contract",
|
|
888
1039
|
"agent-home-projection",
|
|
889
1040
|
"agent-egress-policy",
|
|
890
1041
|
"agent-egress-reliable-ack",
|
|
891
1042
|
"agent-egress-fresh-session",
|
|
1043
|
+
// `handleAgentMessagePublish` accepts `agent.message_egress` gated on exactly
|
|
1044
|
+
// this capability (`packages/cloud/src/inbound.ts:124`) — the same inbound
|
|
1045
|
+
// family as the three egress flags above, omitted by the same drift.
|
|
1046
|
+
"agent-message-egress",
|
|
892
1047
|
"agent-content-workspace-read",
|
|
893
1048
|
"agent-content-transcript-read",
|
|
894
1049
|
"agent-content-artifact-read"
|
|
@@ -905,12 +1060,21 @@ function eventsHandler(deps) {
|
|
|
905
1060
|
let cursor = 0;
|
|
906
1061
|
if (cursorRaw !== void 0) {
|
|
907
1062
|
const parsedCursor = Number(cursorRaw);
|
|
908
|
-
if (!Number.
|
|
1063
|
+
if (!Number.isSafeInteger(parsedCursor) || parsedCursor < 0) {
|
|
1064
|
+
return c.json({ error: "invalid cursor" }, 400);
|
|
1065
|
+
}
|
|
909
1066
|
cursor = parsedCursor;
|
|
910
1067
|
}
|
|
911
1068
|
const acked = await stores.mailbox.readCursor(device.deviceId);
|
|
912
1069
|
if (cursor > acked.ackedSeq) {
|
|
913
|
-
|
|
1070
|
+
try {
|
|
1071
|
+
await stores.mailbox.advanceCursor({ deviceId: device.deviceId, ackedSeq: cursor });
|
|
1072
|
+
} catch (caught) {
|
|
1073
|
+
if (isCoreConflictError(caught, "mailbox_cursor_ahead_of_delivery")) {
|
|
1074
|
+
return c.json({ error: "cursor exceeds delivered watermark" }, 409);
|
|
1075
|
+
}
|
|
1076
|
+
throw caught;
|
|
1077
|
+
}
|
|
914
1078
|
}
|
|
915
1079
|
const attempts = Math.max(1, Math.ceil(deps.longPollHoldMs / deps.longPollIntervalMs));
|
|
916
1080
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
@@ -921,6 +1085,9 @@ function eventsHandler(deps) {
|
|
|
921
1085
|
afterSeq: scanCursor,
|
|
922
1086
|
limit: deps.pageLimit
|
|
923
1087
|
});
|
|
1088
|
+
if (cursor < page.recoverableFrom - 1) {
|
|
1089
|
+
return c.json({ error: "cursor_too_old", recoverableFrom: page.recoverableFrom }, 409);
|
|
1090
|
+
}
|
|
924
1091
|
if (page.messages.length === 0) break;
|
|
925
1092
|
const decoded = page.messages.map((message) => decodeEnvelope(message.body));
|
|
926
1093
|
const offeredTaskIds = decoded.flatMap(
|
|
@@ -945,6 +1112,10 @@ function eventsHandler(deps) {
|
|
|
945
1112
|
cursor: page.nextSeq,
|
|
946
1113
|
capabilities: CLOUD_PROTOCOL_CAPABILITIES
|
|
947
1114
|
};
|
|
1115
|
+
await stores.mailbox.recordDelivery({
|
|
1116
|
+
deviceId: device.deviceId,
|
|
1117
|
+
deliveredSeq: response2.cursor
|
|
1118
|
+
});
|
|
948
1119
|
return c.json(response2, 200);
|
|
949
1120
|
}
|
|
950
1121
|
if (attempt < attempts - 1) await sleep(deps.longPollIntervalMs);
|
|
@@ -962,19 +1133,45 @@ var DEFAULT_APPROVAL_TIMELINE_TTL_MS = 10 * 60 * 1e3;
|
|
|
962
1133
|
var APPROVAL_SUMMARY_MAX_BYTES = 16 * 1024;
|
|
963
1134
|
var NonBlankIdSchema = z.string().max(200).regex(/\S/, "value must not be blank");
|
|
964
1135
|
var TaskIdSchema = z.string().min(1).max(200);
|
|
965
|
-
var
|
|
966
|
-
z.
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
z.
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1136
|
+
var ApprovalRequestedEventSchema = z.object({
|
|
1137
|
+
type: z.literal("approval_requested"),
|
|
1138
|
+
summary: z.string(),
|
|
1139
|
+
approvalId: NonBlankIdSchema.optional()
|
|
1140
|
+
}).strict();
|
|
1141
|
+
var LocalApprovalResolutionEventSchema = z.object({
|
|
1142
|
+
type: z.literal("approval_resolved"),
|
|
1143
|
+
// Absent only when a daemon resolved the single outstanding request without
|
|
1144
|
+
// native approval identity. No id is synthesized.
|
|
1145
|
+
approvalId: NonBlankIdSchema.optional(),
|
|
1146
|
+
decision: z.enum(["approve", "reject"]),
|
|
1147
|
+
// A daemon's `task.approval_resolved` is pure wire observation; it cannot
|
|
1148
|
+
// declare a host command payload.
|
|
1149
|
+
resolvedBy: z.literal("local"),
|
|
1150
|
+
at: z.iso.datetime({ offset: true })
|
|
1151
|
+
}).strict();
|
|
1152
|
+
var HostApprovalResolutionEventSchema = z.object({
|
|
1153
|
+
type: z.literal("approval_resolved"),
|
|
1154
|
+
approvalId: NonBlankIdSchema.optional(),
|
|
1155
|
+
decision: z.literal("approve"),
|
|
1156
|
+
resolvedBy: z.literal("host"),
|
|
1157
|
+
at: z.iso.datetime({ offset: true })
|
|
1158
|
+
}).strict();
|
|
1159
|
+
var HostRejectResolutionEventSchema = z.object({
|
|
1160
|
+
type: z.literal("approval_resolved"),
|
|
1161
|
+
approvalId: NonBlankIdSchema.optional(),
|
|
1162
|
+
decision: z.literal("reject"),
|
|
1163
|
+
resolvedBy: z.literal("host"),
|
|
1164
|
+
// Required even when the host deliberately omitted a wire reason. `null`
|
|
1165
|
+
// records that complete payload, so a historical host reject with no field
|
|
1166
|
+
// cannot be mistaken for a recoverable executable command.
|
|
1167
|
+
reason: z.string().nullable(),
|
|
1168
|
+
at: z.iso.datetime({ offset: true })
|
|
1169
|
+
}).strict();
|
|
1170
|
+
var ApprovalTimelineEventSchema = z.union([
|
|
1171
|
+
ApprovalRequestedEventSchema,
|
|
1172
|
+
LocalApprovalResolutionEventSchema,
|
|
1173
|
+
HostApprovalResolutionEventSchema,
|
|
1174
|
+
HostRejectResolutionEventSchema
|
|
978
1175
|
]);
|
|
979
1176
|
var ApprovalObservationSchema = z.object({
|
|
980
1177
|
taskId: TaskIdSchema,
|
|
@@ -1017,6 +1214,20 @@ function validateApprovalTimelineAppend(input) {
|
|
|
1017
1214
|
}
|
|
1018
1215
|
return { capacity, ttlMs, event: parsed.data.event };
|
|
1019
1216
|
}
|
|
1217
|
+
function validateApprovalTimelineResolve(input) {
|
|
1218
|
+
const validated = validateApprovalTimelineAppend(input);
|
|
1219
|
+
const expected = z.object({
|
|
1220
|
+
expectedSourceEnvelopeId: NonBlankIdSchema,
|
|
1221
|
+
expectedRevision: z.number().int().positive()
|
|
1222
|
+
}).safeParse(input);
|
|
1223
|
+
if (!expected.success || validated.event.type !== "approval_resolved" || validated.event.resolvedBy !== "host") {
|
|
1224
|
+
throw new ByokCloudError(
|
|
1225
|
+
"coordination_input_invalid",
|
|
1226
|
+
"Host approval resolution requires an exact pending source/revision and a host resolution event."
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
return validated;
|
|
1230
|
+
}
|
|
1020
1231
|
function approvalTimelineKey(tenant, taskId) {
|
|
1021
1232
|
return tenantKey(tenant, taskId);
|
|
1022
1233
|
}
|
|
@@ -1040,13 +1251,17 @@ async function projectTerminalToReview(board, taskId) {
|
|
|
1040
1251
|
throw caught;
|
|
1041
1252
|
}
|
|
1042
1253
|
}
|
|
1254
|
+
function agentReliabilityKey(domain, deviceId, agentRef, id) {
|
|
1255
|
+
const exact = AgentRefSchema.parse(agentRef);
|
|
1256
|
+
return `${domain}:${JSON.stringify([deviceId, exact.agentId, exact.profileRevision, id])}`;
|
|
1257
|
+
}
|
|
1043
1258
|
|
|
1044
1259
|
// src/inbound.ts
|
|
1045
|
-
async function handleAgentMessagePublish(stores, deviceId, taskId, payload, consume) {
|
|
1260
|
+
async function handleAgentMessagePublish(stores, deviceId, taskId, payload, consume, alreadyDeduplicated) {
|
|
1046
1261
|
const device = await stores.devices.get(deviceId);
|
|
1047
1262
|
const attempt = await stores.tasks.get(taskId);
|
|
1048
1263
|
const binding = await stores.receipts.get(`agent-message-offer:${deviceId}:${taskId}`);
|
|
1049
|
-
if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_MESSAGE_EGRESS_CAPABILITY]) || attempt === void 0 || attempt.deviceId !== deviceId || binding === void 0 || !sameAgentRef(attempt.agentRef, payload.agentRef)) return { outcome: "rejected" };
|
|
1264
|
+
if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_MESSAGE_EGRESS_CAPABILITY]) || attempt === void 0 || attempt.deviceId !== deviceId || binding === void 0 || attempt.agentRef === void 0 || !sameAgentRef(attempt.agentRef, payload.agentRef)) return { outcome: "rejected" };
|
|
1050
1265
|
let frozen;
|
|
1051
1266
|
try {
|
|
1052
1267
|
frozen = JSON.parse(binding.body);
|
|
@@ -1054,19 +1269,38 @@ async function handleAgentMessagePublish(stores, deviceId, taskId, payload, cons
|
|
|
1054
1269
|
return { outcome: "rejected" };
|
|
1055
1270
|
}
|
|
1056
1271
|
if (!sameAgentRef(frozen.agentRef, payload.agentRef) || frozen.context === void 0 || frozen.requirement?.contract !== payload.contract || frozen.sessionRef !== void 0 && frozen.sessionRef !== payload.sessionRef || frozen.requirement.contentType !== payload.contentType || typeof frozen.requirement.maxBytes !== "number" || payload.byteCount > frozen.requirement.maxBytes) return { outcome: "rejected" };
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1272
|
+
const priorDisposition = await readAgentMessageDisposition(stores, deviceId, taskId, payload);
|
|
1273
|
+
if (priorDisposition !== void 0) return { outcome: "duplicate", disposition: priorDisposition };
|
|
1274
|
+
const payloadBody = JSON.stringify(payload);
|
|
1275
|
+
const reservation = await stores.tasks.reserveAgentMessage({
|
|
1276
|
+
taskId,
|
|
1277
|
+
deviceId,
|
|
1278
|
+
messageId: payload.messageId,
|
|
1279
|
+
payloadBody
|
|
1280
|
+
});
|
|
1281
|
+
if (reservation !== "reserved" && reservation !== "pending") return { outcome: "rejected" };
|
|
1282
|
+
let decision;
|
|
1283
|
+
if (consume === void 0) {
|
|
1284
|
+
if (reservation === "pending") {
|
|
1285
|
+
throw new Error("Agent message consumer is unavailable while reconciling an exact pending admission.");
|
|
1286
|
+
}
|
|
1287
|
+
decision = { outcome: "held", reasonCode: "consumer_unavailable" };
|
|
1288
|
+
} else {
|
|
1289
|
+
decision = await consume({ tenant: stores.tenant, deviceId, taskId, context: frozen.context, payload });
|
|
1290
|
+
}
|
|
1291
|
+
const disposition = agentMessageDisposition(payload, decision);
|
|
1292
|
+
const admission = await stores.tasks.finalizeAgentMessage({
|
|
1293
|
+
taskId,
|
|
1294
|
+
deviceId,
|
|
1295
|
+
messageId: payload.messageId,
|
|
1296
|
+
payloadBody,
|
|
1297
|
+
terminalBody: JSON.stringify({ payload, disposition })
|
|
1298
|
+
});
|
|
1299
|
+
const terminal = admission?.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
|
|
1300
|
+
return terminal === void 0 ? { outcome: "rejected" } : { outcome: "accepted", disposition: terminal };
|
|
1301
|
+
}
|
|
1302
|
+
function agentMessageDisposition(payload, decision) {
|
|
1303
|
+
return {
|
|
1070
1304
|
agentRef: payload.agentRef,
|
|
1071
1305
|
sessionRef: payload.sessionRef,
|
|
1072
1306
|
contract: payload.contract,
|
|
@@ -1077,9 +1311,23 @@ async function handleAgentMessagePublish(stores, deviceId, taskId, payload, cons
|
|
|
1077
1311
|
receiptId: crypto.randomUUID(),
|
|
1078
1312
|
...decision.reasonCode === void 0 ? {} : { reasonCode: decision.reasonCode }
|
|
1079
1313
|
};
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1314
|
+
}
|
|
1315
|
+
async function readAgentMessageDisposition(stores, deviceId, taskId, payload) {
|
|
1316
|
+
const admission = await stores.tasks.readAgentMessage({
|
|
1317
|
+
taskId,
|
|
1318
|
+
deviceId,
|
|
1319
|
+
messageId: payload.messageId,
|
|
1320
|
+
payloadBody: JSON.stringify(payload)
|
|
1321
|
+
});
|
|
1322
|
+
return admission?.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
|
|
1323
|
+
}
|
|
1324
|
+
function parseAgentMessageDisposition(payload, terminalBody) {
|
|
1325
|
+
try {
|
|
1326
|
+
const decoded = JSON.parse(terminalBody);
|
|
1327
|
+
return decoded.disposition !== void 0 && JSON.stringify(decoded.payload) === JSON.stringify(payload) ? decoded.disposition : void 0;
|
|
1328
|
+
} catch {
|
|
1329
|
+
return void 0;
|
|
1330
|
+
}
|
|
1083
1331
|
}
|
|
1084
1332
|
function terminalReceiptKey(taskId) {
|
|
1085
1333
|
return `task:${taskId}:terminal`;
|
|
@@ -1103,17 +1351,17 @@ function contentReadCapability(surface) {
|
|
|
1103
1351
|
function hasCapabilities(capabilities, required) {
|
|
1104
1352
|
return capabilities !== void 0 && required.every((capability) => capabilities.includes(capability));
|
|
1105
1353
|
}
|
|
1106
|
-
function contentRequestReceiptKey(deviceId, requestId) {
|
|
1107
|
-
return
|
|
1354
|
+
function contentRequestReceiptKey(deviceId, agentRef, requestId) {
|
|
1355
|
+
return agentReliabilityKey("agent-content-request", deviceId, agentRef, requestId);
|
|
1108
1356
|
}
|
|
1109
|
-
function contentReceiptKey(deviceId, requestId) {
|
|
1110
|
-
return
|
|
1357
|
+
function contentReceiptKey(deviceId, agentRef, requestId) {
|
|
1358
|
+
return agentReliabilityKey("agent-content-receipt", deviceId, agentRef, requestId);
|
|
1111
1359
|
}
|
|
1112
1360
|
function matchesContentReadReceipt(request, receipt) {
|
|
1113
1361
|
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;
|
|
1114
1362
|
}
|
|
1115
|
-
async function readContentRequest(stores, deviceId, requestId) {
|
|
1116
|
-
const stored = await stores.receipts.get(contentRequestReceiptKey(deviceId, requestId));
|
|
1363
|
+
async function readContentRequest(stores, deviceId, agentRef, requestId) {
|
|
1364
|
+
const stored = await stores.receipts.get(contentRequestReceiptKey(deviceId, agentRef, requestId));
|
|
1117
1365
|
if (stored === void 0) return void 0;
|
|
1118
1366
|
try {
|
|
1119
1367
|
return AgentContentReadPayloadSchema.parse(JSON.parse(stored.body));
|
|
@@ -1136,7 +1384,23 @@ function inboundAgentRef(envelope) {
|
|
|
1136
1384
|
function agentRefEchoRequired(envelope) {
|
|
1137
1385
|
return envelope.type === "task.claim" || envelope.type === "task.decline" || envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled";
|
|
1138
1386
|
}
|
|
1139
|
-
async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS) {
|
|
1387
|
+
async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS, agentMessageConsume, observer) {
|
|
1388
|
+
const outcome = await applyInboundGate(
|
|
1389
|
+
stores,
|
|
1390
|
+
deviceId,
|
|
1391
|
+
envelope,
|
|
1392
|
+
activityBounds,
|
|
1393
|
+
agentMessageConsume
|
|
1394
|
+
);
|
|
1395
|
+
if (outcome === "accepted" && observer !== void 0) {
|
|
1396
|
+
try {
|
|
1397
|
+
observer.onInboundCommitted({ tenantId: stores.tenant, deviceId, envelope, outcome });
|
|
1398
|
+
} catch {
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return outcome;
|
|
1402
|
+
}
|
|
1403
|
+
async function applyInboundGate(stores, deviceId, envelope, activityBounds, agentMessageConsume) {
|
|
1140
1404
|
if (!await stores.rateLimiter.consume(deviceId)) return "rate_limited";
|
|
1141
1405
|
if (envelope.type === "conn.hello") {
|
|
1142
1406
|
if (envelope.payload.deviceId !== deviceId) return "rejected";
|
|
@@ -1145,10 +1409,21 @@ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds
|
|
|
1145
1409
|
if (device === void 0 || device.revoked || device.productId !== envelope.payload.productId) {
|
|
1146
1410
|
return "rejected";
|
|
1147
1411
|
}
|
|
1148
|
-
if (await stores.
|
|
1149
|
-
|
|
1412
|
+
if (await stores.devices.recordCapabilities({ capabilities: envelope.payload.capabilities }) === void 0) {
|
|
1413
|
+
return "rejected";
|
|
1414
|
+
}
|
|
1415
|
+
return completeInboundEnvelope(stores, deviceId, envelope.id, void 0);
|
|
1150
1416
|
}
|
|
1151
1417
|
if (!DAEMON_TO_SERVER_TYPES.includes(envelope.type)) return "rejected";
|
|
1418
|
+
if (envelope.type === "agent.message.publish") {
|
|
1419
|
+
const message = await handleAgentMessagePublish(
|
|
1420
|
+
stores,
|
|
1421
|
+
deviceId,
|
|
1422
|
+
envelope.task_id,
|
|
1423
|
+
envelope.payload,
|
|
1424
|
+
agentMessageConsume);
|
|
1425
|
+
return message.outcome === "rejected" ? "rejected" : completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
|
|
1426
|
+
}
|
|
1152
1427
|
if (envelope.type === "agent.egress.reliable") {
|
|
1153
1428
|
const device = await stores.devices.get(deviceId);
|
|
1154
1429
|
if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
|
|
@@ -1159,19 +1434,21 @@ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds
|
|
|
1159
1434
|
payload: envelope.payload,
|
|
1160
1435
|
receiptId: crypto.randomUUID()
|
|
1161
1436
|
});
|
|
1162
|
-
|
|
1437
|
+
if (!outcome.created && !sameReliableEgress(outcome.record.payload, envelope.payload)) return "rejected";
|
|
1438
|
+
return completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
|
|
1163
1439
|
}
|
|
1164
1440
|
if (envelope.type === "agent.content.receipt") {
|
|
1165
1441
|
const device = await stores.devices.get(deviceId);
|
|
1166
1442
|
if (device === void 0 || device.revoked || envelope.payload.eventId !== envelope.payload.requestId || !hasCapabilities(device.capabilities, [contentReadCapability(envelope.payload.surface), AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
|
|
1167
1443
|
return "rejected";
|
|
1168
1444
|
}
|
|
1169
|
-
const request = await readContentRequest(stores, deviceId, envelope.payload.requestId);
|
|
1445
|
+
const request = await readContentRequest(stores, deviceId, envelope.payload.agentRef, envelope.payload.requestId);
|
|
1170
1446
|
if (request === void 0 || !matchesContentReadReceipt(request, envelope.payload)) return "rejected";
|
|
1171
|
-
const key = contentReceiptKey(deviceId, envelope.payload.requestId);
|
|
1447
|
+
const key = contentReceiptKey(deviceId, envelope.payload.agentRef, envelope.payload.requestId);
|
|
1172
1448
|
const body = JSON.stringify(envelope.payload);
|
|
1173
1449
|
const receipt = await stores.receipts.record({ key, body });
|
|
1174
|
-
|
|
1450
|
+
if (!receipt.created && receipt.receipt.body !== body) return "rejected";
|
|
1451
|
+
return completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
|
|
1175
1452
|
}
|
|
1176
1453
|
const taskId = envelope.task_id;
|
|
1177
1454
|
if (taskId === void 0) return "rejected";
|
|
@@ -1181,6 +1458,7 @@ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds
|
|
|
1181
1458
|
if (attempt?.agentRef !== void 0 && agentRefEchoRequired(envelope) && !sameAgentRef(attempt.agentRef, inboundAgentRef(envelope))) {
|
|
1182
1459
|
return "rejected";
|
|
1183
1460
|
}
|
|
1461
|
+
if (terminalStatus(envelope) !== void 0 && attempt === void 0) return "rejected";
|
|
1184
1462
|
if (envelope.type === "task.progress" && envelope.payload.events.length > 0) {
|
|
1185
1463
|
try {
|
|
1186
1464
|
validateActivityBatch(
|
|
@@ -1209,14 +1487,22 @@ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds
|
|
|
1209
1487
|
throw caught;
|
|
1210
1488
|
}
|
|
1211
1489
|
}
|
|
1212
|
-
if (await stores.dedup.checkAndRecord(deviceId, envelope.id)) return "duplicate";
|
|
1213
1490
|
await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, attempt?.agentRef);
|
|
1214
|
-
return
|
|
1491
|
+
return completeInboundEnvelope(stores, deviceId, envelope.id, attempt?.agentRef);
|
|
1492
|
+
}
|
|
1493
|
+
async function completeInboundEnvelope(stores, deviceId, envelopeId, agentRef) {
|
|
1494
|
+
const duplicate = agentRef === void 0 ? await stores.dedup.checkAndRecord(deviceId, envelopeId) : await stores.dedup.checkAndRecordAgent(deviceId, agentRef, envelopeId);
|
|
1495
|
+
return duplicate ? "duplicate" : "accepted";
|
|
1215
1496
|
}
|
|
1216
1497
|
async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, persistedAgentRef) {
|
|
1217
1498
|
switch (envelope.type) {
|
|
1218
1499
|
case "task.claim":
|
|
1219
|
-
await stores.tasks.claim({
|
|
1500
|
+
await stores.tasks.claim({
|
|
1501
|
+
taskId,
|
|
1502
|
+
deviceId,
|
|
1503
|
+
...envelope.payload.runtime === void 0 ? {} : { runtime: envelope.payload.runtime },
|
|
1504
|
+
...envelope.payload.capabilities === void 0 ? {} : { capabilities: envelope.payload.capabilities }
|
|
1505
|
+
});
|
|
1220
1506
|
return;
|
|
1221
1507
|
case "task.started":
|
|
1222
1508
|
await stores.tasks.recordStatus({
|
|
@@ -1226,11 +1512,7 @@ async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds
|
|
|
1226
1512
|
});
|
|
1227
1513
|
return;
|
|
1228
1514
|
case "task.decline":
|
|
1229
|
-
await stores
|
|
1230
|
-
taskId,
|
|
1231
|
-
status: "failed",
|
|
1232
|
-
...persistedAgentRef === void 0 ? {} : { agentRef: persistedAgentRef }
|
|
1233
|
-
});
|
|
1515
|
+
await recordTerminal(stores, taskId, envelope);
|
|
1234
1516
|
return;
|
|
1235
1517
|
case "task.progress":
|
|
1236
1518
|
if (envelope.payload.events.length > 0) {
|
|
@@ -1254,13 +1536,13 @@ async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds
|
|
|
1254
1536
|
await stores.approvals.append(approvalTimelineAppendInput(taskId, envelope));
|
|
1255
1537
|
return;
|
|
1256
1538
|
case "task.complete":
|
|
1257
|
-
await recordTerminal(stores, taskId, envelope
|
|
1539
|
+
await recordTerminal(stores, taskId, envelope);
|
|
1258
1540
|
return;
|
|
1259
1541
|
case "task.fail":
|
|
1260
|
-
await recordTerminal(stores, taskId, envelope
|
|
1542
|
+
await recordTerminal(stores, taskId, envelope);
|
|
1261
1543
|
return;
|
|
1262
1544
|
case "task.cancelled":
|
|
1263
|
-
await recordTerminal(stores, taskId, envelope
|
|
1545
|
+
await recordTerminal(stores, taskId, envelope);
|
|
1264
1546
|
return;
|
|
1265
1547
|
default:
|
|
1266
1548
|
return;
|
|
@@ -1294,14 +1576,18 @@ function approvalTimelineAppendInput(taskId, envelope) {
|
|
|
1294
1576
|
return void 0;
|
|
1295
1577
|
}
|
|
1296
1578
|
}
|
|
1297
|
-
async function recordTerminal(stores, taskId, envelope
|
|
1298
|
-
const
|
|
1299
|
-
const { created } = await stores.receipts.record({
|
|
1579
|
+
async function recordTerminal(stores, taskId, envelope) {
|
|
1580
|
+
const receipt = await stores.receipts.record({
|
|
1300
1581
|
key: terminalReceiptKey(taskId),
|
|
1301
1582
|
body: encodeEnvelope(envelope)
|
|
1302
1583
|
});
|
|
1584
|
+
const winner = terminalReceiptBinding(taskId, receipt.receipt.body);
|
|
1585
|
+
if (winner === void 0) {
|
|
1586
|
+
throw new Error(`terminal receipt for ${taskId} is not a canonical terminal binding`);
|
|
1587
|
+
}
|
|
1588
|
+
const attempt = await stores.tasks.get(taskId);
|
|
1303
1589
|
if (attempt?.cancellation !== void 0) {
|
|
1304
|
-
if (status === "cancelled") {
|
|
1590
|
+
if (winner.status === "cancelled") {
|
|
1305
1591
|
await stores.tasks.recordStatus({
|
|
1306
1592
|
taskId,
|
|
1307
1593
|
status: "cancelled",
|
|
@@ -1310,48 +1596,72 @@ async function recordTerminal(stores, taskId, envelope, status) {
|
|
|
1310
1596
|
}
|
|
1311
1597
|
return;
|
|
1312
1598
|
}
|
|
1313
|
-
if (!created) return;
|
|
1314
|
-
const cause = envelope.type === "task.fail" || envelope.type === "task.cancelled" ? envelope.payload.reason : void 0;
|
|
1315
1599
|
const recordedAttempt = await stores.tasks.recordStatus({
|
|
1316
1600
|
taskId,
|
|
1317
|
-
status,
|
|
1601
|
+
status: winner.status,
|
|
1318
1602
|
...attempt?.agentRef === void 0 ? {} : { agentRef: attempt.agentRef },
|
|
1319
|
-
...cause === void 0 ? {} : { terminalCause: cause }
|
|
1603
|
+
...winner.cause === void 0 ? {} : { terminalCause: winner.cause }
|
|
1320
1604
|
});
|
|
1321
|
-
if (recordedAttempt?.status !== status || recordedAttempt.cancellation !== void 0) return;
|
|
1605
|
+
if (recordedAttempt?.status !== winner.status || recordedAttempt.cancellation !== void 0) return;
|
|
1322
1606
|
await projectTerminalToReview(stores.board, taskId);
|
|
1323
1607
|
}
|
|
1608
|
+
function terminalStatus(envelope) {
|
|
1609
|
+
switch (envelope.type) {
|
|
1610
|
+
case "task.complete":
|
|
1611
|
+
return "complete";
|
|
1612
|
+
case "task.fail":
|
|
1613
|
+
case "task.decline":
|
|
1614
|
+
return "failed";
|
|
1615
|
+
case "task.cancelled":
|
|
1616
|
+
return "cancelled";
|
|
1617
|
+
default:
|
|
1618
|
+
return void 0;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function terminalReceiptBinding(taskId, body) {
|
|
1622
|
+
let envelope;
|
|
1623
|
+
try {
|
|
1624
|
+
envelope = decodeEnvelope(body);
|
|
1625
|
+
} catch {
|
|
1626
|
+
return void 0;
|
|
1627
|
+
}
|
|
1628
|
+
const status = terminalStatus(envelope);
|
|
1629
|
+
if (status === void 0 || envelope.task_id !== taskId || encodeEnvelope(envelope) !== body) return void 0;
|
|
1630
|
+
const cause = envelope.type === "task.fail" || envelope.type === "task.cancelled" || envelope.type === "task.decline" ? envelope.payload.reason : void 0;
|
|
1631
|
+
return { status, ...cause === void 0 ? {} : { cause } };
|
|
1632
|
+
}
|
|
1324
1633
|
|
|
1325
1634
|
// src/handlers/messages.ts
|
|
1635
|
+
var MESSAGES_JSON_BODY_MAX_BYTES = 2 * 1024 * 1024;
|
|
1326
1636
|
function messagesHandler(deps) {
|
|
1327
1637
|
return async (c) => {
|
|
1328
1638
|
const authenticated = await authenticateDevice(c, deps);
|
|
1329
1639
|
if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
|
|
1330
1640
|
const { device, stores } = authenticated;
|
|
1331
|
-
const
|
|
1641
|
+
const body = await readBoundedJsonBody(c, MESSAGES_JSON_BODY_MAX_BYTES);
|
|
1642
|
+
if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
|
|
1643
|
+
const parsed = MessagesSendRequestSchema.safeParse(body.body);
|
|
1332
1644
|
if (!parsed.success) return c.json({ error: "messages must be an array of envelopes" }, 400);
|
|
1333
1645
|
let accepted = 0;
|
|
1334
1646
|
let rejected = 0;
|
|
1335
1647
|
for (const envelope of parsed.data.messages) {
|
|
1336
|
-
const
|
|
1337
|
-
stores,
|
|
1338
|
-
device.deviceId,
|
|
1339
|
-
envelope.task_id,
|
|
1340
|
-
envelope.payload,
|
|
1341
|
-
deps.agentMessage?.consume
|
|
1342
|
-
) : void 0;
|
|
1343
|
-
const outcome = messageResult?.outcome ?? await handleInboundEnvelope(
|
|
1648
|
+
const outcome = await handleInboundEnvelope(
|
|
1344
1649
|
stores,
|
|
1345
1650
|
device.deviceId,
|
|
1346
1651
|
envelope,
|
|
1347
|
-
deps.activityBounds
|
|
1652
|
+
deps.activityBounds,
|
|
1653
|
+
envelope.type === "agent.message.publish" ? deps.agentMessage?.consume : void 0,
|
|
1654
|
+
deps.observer
|
|
1348
1655
|
);
|
|
1349
1656
|
if (outcome === "rate_limited") return c.json({ error: "rate limit exceeded" }, 429);
|
|
1350
|
-
if (envelope.type === "agent.message.publish"
|
|
1351
|
-
await
|
|
1657
|
+
if (envelope.type === "agent.message.publish") {
|
|
1658
|
+
const disposition = await readAgentMessageDisposition(stores, device.deviceId, envelope.task_id, envelope.payload);
|
|
1659
|
+
if (disposition !== void 0) {
|
|
1660
|
+
await deps.appendAgentMessageDisposition(stores, device.deviceId, envelope.task_id, disposition);
|
|
1661
|
+
}
|
|
1352
1662
|
}
|
|
1353
1663
|
if (envelope.type === "agent.egress.reliable" && (outcome === "accepted" || outcome === "duplicate")) {
|
|
1354
|
-
const record = await stores.egress.get(device.deviceId, envelope.payload.eventId);
|
|
1664
|
+
const record = await stores.egress.get(device.deviceId, envelope.payload.agentRef, envelope.payload.eventId);
|
|
1355
1665
|
if (record === void 0) {
|
|
1356
1666
|
throw new Error(`Accepted reliable egress ${envelope.payload.eventId} has no durable receipt record.`);
|
|
1357
1667
|
}
|
|
@@ -1961,7 +2271,7 @@ function truthGetHandler(deps) {
|
|
|
1961
2271
|
}
|
|
1962
2272
|
function truthPutHandler(deps) {
|
|
1963
2273
|
return async (c) => {
|
|
1964
|
-
const body = await
|
|
2274
|
+
const body = await readBoundedBody2(c, deps.maxRequestBytes);
|
|
1965
2275
|
if (body === "too_large") return c.json({ error: "truth request body too large" }, 413);
|
|
1966
2276
|
if (body === void 0) return c.json({ error: "invalid truth request body" }, 400);
|
|
1967
2277
|
const authenticated = await authenticateTruthRequest(c, deps, {
|
|
@@ -2093,7 +2403,7 @@ function decodeProofHeader(value) {
|
|
|
2093
2403
|
return void 0;
|
|
2094
2404
|
}
|
|
2095
2405
|
}
|
|
2096
|
-
async function
|
|
2406
|
+
async function readBoundedBody2(c, maximum) {
|
|
2097
2407
|
const length = c.req.header("content-length");
|
|
2098
2408
|
if (length !== void 0) {
|
|
2099
2409
|
const parsed = Number(length);
|
|
@@ -2170,11 +2480,11 @@ var CloudRouteRegistry = class {
|
|
|
2170
2480
|
return this.#app.fetch;
|
|
2171
2481
|
}
|
|
2172
2482
|
};
|
|
2173
|
-
function agentHomeProjectionRequestKey(deviceId, requestId) {
|
|
2174
|
-
return
|
|
2483
|
+
function agentHomeProjectionRequestKey(deviceId, agentRef, requestId) {
|
|
2484
|
+
return agentReliabilityKey("agent-home-projection-request", deviceId, agentRef, requestId);
|
|
2175
2485
|
}
|
|
2176
|
-
function agentHomeProjectionCompletionKey(deviceId, requestId) {
|
|
2177
|
-
return
|
|
2486
|
+
function agentHomeProjectionCompletionKey(deviceId, agentRef, requestId) {
|
|
2487
|
+
return agentReliabilityKey("agent-home-projection-completion", deviceId, agentRef, requestId);
|
|
2178
2488
|
}
|
|
2179
2489
|
function sameAgentHomeProjectionRequest(expected, actual) {
|
|
2180
2490
|
return expected.requestId === actual.requestId && expected.agentRef.agentId === actual.agentRef.agentId && expected.agentRef.profileRevision === actual.agentRef.profileRevision && expected.projectionHash === actual.projectionHash && JSON.stringify(expected.projection) === JSON.stringify(actual.projection);
|
|
@@ -2208,7 +2518,7 @@ function parseCompletionBody(body) {
|
|
|
2208
2518
|
}
|
|
2209
2519
|
}
|
|
2210
2520
|
async function readAgentHomeProjectionStatus(receipts, tenant, deviceId, input) {
|
|
2211
|
-
const storedRequest = await receipts.get(agentHomeProjectionRequestKey(deviceId, input.requestId));
|
|
2521
|
+
const storedRequest = await receipts.get(agentHomeProjectionRequestKey(deviceId, input.agentRef, input.requestId));
|
|
2212
2522
|
if (storedRequest === void 0) return void 0;
|
|
2213
2523
|
const request = parseRequestBody(storedRequest.body);
|
|
2214
2524
|
if (!statusInputMatchesAgentHomeProjection(request, input)) {
|
|
@@ -2217,7 +2527,7 @@ async function readAgentHomeProjectionStatus(receipts, tenant, deviceId, input)
|
|
|
2217
2527
|
`Agent-home projection request ${input.requestId} does not match its immutable desired fact.`
|
|
2218
2528
|
);
|
|
2219
2529
|
}
|
|
2220
|
-
const storedCompletion = await receipts.get(agentHomeProjectionCompletionKey(deviceId, input.requestId));
|
|
2530
|
+
const storedCompletion = await receipts.get(agentHomeProjectionCompletionKey(deviceId, request.agentRef, input.requestId));
|
|
2221
2531
|
if (storedCompletion === void 0) {
|
|
2222
2532
|
return {
|
|
2223
2533
|
tenantId: tenant,
|
|
@@ -2247,7 +2557,7 @@ async function readAgentHomeProjectionStatus(receipts, tenant, deviceId, input)
|
|
|
2247
2557
|
}
|
|
2248
2558
|
async function recordAgentHomeProjectionCompletion(receipts, tenant, deviceId, receiptInput) {
|
|
2249
2559
|
const receipt = AgentHomeProjectionCompletionRequestSchema.parse(receiptInput);
|
|
2250
|
-
const storedRequest = await receipts.get(agentHomeProjectionRequestKey(deviceId, receipt.requestId));
|
|
2560
|
+
const storedRequest = await receipts.get(agentHomeProjectionRequestKey(deviceId, receipt.agentRef, receipt.requestId));
|
|
2251
2561
|
if (storedRequest === void 0) {
|
|
2252
2562
|
throw new ByokCloudError(
|
|
2253
2563
|
"agent_home_projection_request_not_found",
|
|
@@ -2263,7 +2573,7 @@ async function recordAgentHomeProjectionCompletion(receipts, tenant, deviceId, r
|
|
|
2263
2573
|
}
|
|
2264
2574
|
const body = JSON.stringify(receipt);
|
|
2265
2575
|
const storedCompletion = await receipts.record({
|
|
2266
|
-
key: agentHomeProjectionCompletionKey(deviceId, receipt.requestId),
|
|
2576
|
+
key: agentHomeProjectionCompletionKey(deviceId, request.agentRef, receipt.requestId),
|
|
2267
2577
|
body
|
|
2268
2578
|
});
|
|
2269
2579
|
if (!storedCompletion.created && storedCompletion.receipt.body !== body) {
|
|
@@ -2317,6 +2627,21 @@ function projectTerminalResult(taskId, receipt) {
|
|
|
2317
2627
|
...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
|
|
2318
2628
|
recordedAt: receipt.recordedAt
|
|
2319
2629
|
};
|
|
2630
|
+
// `task.decline` is the pre-claim terminal (§3.2, `Offered -> Failed`).
|
|
2631
|
+
// It projects onto the same `failed` state as `task.fail` — there is no
|
|
2632
|
+
// separate declined state — and carries the device's own `retryable`
|
|
2633
|
+
// verbatim, exactly like `task.fail` does; this projection never decides
|
|
2634
|
+
// that a decline is retryable on the device's behalf.
|
|
2635
|
+
case "task.decline":
|
|
2636
|
+
return {
|
|
2637
|
+
taskId,
|
|
2638
|
+
state: "failed",
|
|
2639
|
+
...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
|
|
2640
|
+
reason: envelope.payload.reason,
|
|
2641
|
+
terminalCause: envelope.payload.reason,
|
|
2642
|
+
...envelope.payload.retryable !== void 0 ? { retryable: envelope.payload.retryable } : {},
|
|
2643
|
+
recordedAt: receipt.recordedAt
|
|
2644
|
+
};
|
|
2320
2645
|
case "task.cancelled":
|
|
2321
2646
|
return {
|
|
2322
2647
|
taskId,
|
|
@@ -2341,9 +2666,25 @@ var DEFAULT_LONG_POLL_HOLD_MS = 5e4;
|
|
|
2341
2666
|
var DEFAULT_LONG_POLL_INTERVAL_MS = 250;
|
|
2342
2667
|
var DEFAULT_EVENTS_PAGE_LIMIT = 50;
|
|
2343
2668
|
var AGENT_HOME_CONTRACT_CAPABILITY = "agent-home-contract";
|
|
2669
|
+
function isTerminalAttemptStatus(status) {
|
|
2670
|
+
return status === "complete" || status === "failed" || status === "cancelled";
|
|
2671
|
+
}
|
|
2344
2672
|
function sameAgentRef2(expected, actual) {
|
|
2345
2673
|
return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
|
|
2346
2674
|
}
|
|
2675
|
+
function uuidFromSha256(value) {
|
|
2676
|
+
const match = /^sha256:([0-9a-f]{64})$/.exec(value);
|
|
2677
|
+
if (match === null) throw new Error("CloudCrypto.sha256 must return canonical sha256 hex");
|
|
2678
|
+
const digest = match[1];
|
|
2679
|
+
if (digest === void 0) throw new Error("CloudCrypto.sha256 must return canonical sha256 hex");
|
|
2680
|
+
const hex = digest.slice(0, 32).split("");
|
|
2681
|
+
hex[12] = "8";
|
|
2682
|
+
hex[16] = ["8", "9", "a", "b"][Number.parseInt(hex[16], 16) & 3];
|
|
2683
|
+
return `${hex.slice(0, 8).join("")}-${hex.slice(8, 12).join("")}-${hex.slice(12, 16).join("")}-${hex.slice(16, 20).join("")}-${hex.slice(20).join("")}`;
|
|
2684
|
+
}
|
|
2685
|
+
function sameOfferEnvelope(expected, actual) {
|
|
2686
|
+
return expected.id === actual.id && expected.type === actual.type && expected.task_id === actual.task_id && JSON.stringify(expected.payload) === JSON.stringify(actual.payload);
|
|
2687
|
+
}
|
|
2347
2688
|
function createByokCloud(options) {
|
|
2348
2689
|
const declaration = parseDeclaration(options.capabilities);
|
|
2349
2690
|
assertNoOverDeclaration(
|
|
@@ -2365,7 +2706,11 @@ function createByokCloud(options) {
|
|
|
2365
2706
|
});
|
|
2366
2707
|
const deviceRouteDeps = {
|
|
2367
2708
|
root,
|
|
2368
|
-
bearer: {
|
|
2709
|
+
bearer: {
|
|
2710
|
+
tokenSigner: options.tokenSigner,
|
|
2711
|
+
devices: options.cloud.devices,
|
|
2712
|
+
...options.instanceProductId === void 0 ? {} : { instanceProductId: options.instanceProductId }
|
|
2713
|
+
}
|
|
2369
2714
|
};
|
|
2370
2715
|
const registry = new CloudRouteRegistry();
|
|
2371
2716
|
const activityBounds = {
|
|
@@ -2410,7 +2755,8 @@ function createByokCloud(options) {
|
|
|
2410
2755
|
appendReliableEgressAck: enqueueReliableEgressAck,
|
|
2411
2756
|
appendContentReceiptAck: enqueueContentReceiptAck,
|
|
2412
2757
|
agentMessage: options.agentMessage,
|
|
2413
|
-
appendAgentMessageDisposition: enqueueAgentMessageDisposition
|
|
2758
|
+
appendAgentMessageDisposition: enqueueAgentMessageDisposition,
|
|
2759
|
+
observer: options.observer
|
|
2414
2760
|
})
|
|
2415
2761
|
);
|
|
2416
2762
|
}
|
|
@@ -2522,7 +2868,10 @@ function createByokCloud(options) {
|
|
|
2522
2868
|
}
|
|
2523
2869
|
const contentProxy = options.blobContentProxy;
|
|
2524
2870
|
if (contentProxy !== void 0 && declares(declaration, CLOUD_CAPABILITIES.blobsContentProxy)) {
|
|
2525
|
-
const contentDeps = {
|
|
2871
|
+
const contentDeps = {
|
|
2872
|
+
contentProxy,
|
|
2873
|
+
maxUploadBytes: options.maxBlobSizeBytes ?? DEFAULT_MAX_BLOB_SIZE_BYTES
|
|
2874
|
+
};
|
|
2526
2875
|
registry.register(
|
|
2527
2876
|
{ method: "PUT", path: BYOK_BLOB_CONTENT_ROUTE, class: "presigned" },
|
|
2528
2877
|
blobUploadContentHandler(contentDeps)
|
|
@@ -2539,56 +2888,60 @@ function createByokCloud(options) {
|
|
|
2539
2888
|
async function enqueueTaskEnvelope(tenant, deviceId, requestedTaskId, agentRef, buildEnvelope, beforeMailbox) {
|
|
2540
2889
|
const stores = tenantStoresFor(controlPlane(tenant), root);
|
|
2541
2890
|
const taskId = requestedTaskId ?? `task_${options.crypto.randomUuid()}`;
|
|
2542
|
-
const messageId =
|
|
2891
|
+
const messageId = uuidFromSha256(
|
|
2892
|
+
await options.crypto.sha256(
|
|
2893
|
+
new TextEncoder().encode(
|
|
2894
|
+
JSON.stringify({ domain: "byok:task-offer", tenant, taskId, deviceId, agentRef })
|
|
2895
|
+
)
|
|
2896
|
+
)
|
|
2897
|
+
);
|
|
2543
2898
|
const reservation = agentRef === void 0 ? void 0 : await stores.tasks.reserveAgentOffer({ taskId, deviceId, agentRef });
|
|
2544
|
-
const openedBeforeAppend = reservation?.attempt;
|
|
2545
|
-
if (
|
|
2899
|
+
const openedBeforeAppend = reservation?.attempt ?? (agentRef === void 0 ? await stores.tasks.open({ taskId, deviceId }) : void 0);
|
|
2900
|
+
if (openedBeforeAppend === void 0 || openedBeforeAppend.deviceId !== deviceId || (agentRef === void 0 ? openedBeforeAppend.agentRef !== void 0 : openedBeforeAppend.agentRef === void 0 || !sameAgentRef2(agentRef, openedBeforeAppend.agentRef))) {
|
|
2546
2901
|
throw new ByokCloudError(
|
|
2547
|
-
"agent_ref_mismatch",
|
|
2548
|
-
`Task ${taskId} already has a different durable
|
|
2902
|
+
agentRef === void 0 ? "coordination_input_invalid" : "agent_ref_mismatch",
|
|
2903
|
+
`Task ${taskId} already has a different durable target or Agent identity.`
|
|
2549
2904
|
);
|
|
2550
2905
|
}
|
|
2551
|
-
|
|
2906
|
+
const offerReceiptKey = `task-offer:${messageId}`;
|
|
2907
|
+
const deliveryReceiptKey = `task-offer-delivered:${messageId}`;
|
|
2908
|
+
if (agentRef !== void 0 && reservation?.created === false && await stores.receipts.get(deliveryReceiptKey) !== void 0) {
|
|
2552
2909
|
throw new ByokCloudError(
|
|
2553
2910
|
"agent_task_already_exists",
|
|
2554
2911
|
`Task ${taskId} already has a durable Agent attempt and cannot be enqueued again.`
|
|
2555
2912
|
);
|
|
2556
2913
|
}
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
}
|
|
2573
|
-
});
|
|
2574
|
-
} catch (error) {
|
|
2575
|
-
if (openedBeforeAppend !== void 0) {
|
|
2576
|
-
try {
|
|
2577
|
-
await stores.tasks.recordStatus({
|
|
2578
|
-
taskId,
|
|
2579
|
-
status: "failed",
|
|
2580
|
-
agentRef,
|
|
2581
|
-
terminalCause: "mailbox append failed before Agent offer delivery"
|
|
2582
|
-
});
|
|
2583
|
-
} catch (cleanupError) {
|
|
2584
|
-
throw new AggregateError(
|
|
2585
|
-
[error, cleanupError],
|
|
2586
|
-
`Agent task ${taskId} could not be delivered or durably closed`
|
|
2587
|
-
);
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
2590
|
-
throw error;
|
|
2914
|
+
const proposed = buildEnvelope(taskId, 0, messageId);
|
|
2915
|
+
const proposedBody = JSON.stringify({
|
|
2916
|
+
deviceId,
|
|
2917
|
+
type: proposed.type,
|
|
2918
|
+
payload: proposed.payload
|
|
2919
|
+
});
|
|
2920
|
+
const recorded = await stores.receipts.record({
|
|
2921
|
+
key: offerReceiptKey,
|
|
2922
|
+
body: proposedBody
|
|
2923
|
+
});
|
|
2924
|
+
if (!recorded.created && recorded.receipt.body !== proposedBody) {
|
|
2925
|
+
throw new ByokCloudError(
|
|
2926
|
+
"coordination_input_invalid",
|
|
2927
|
+
`Task ${taskId} already has a different executable offer body.`
|
|
2928
|
+
);
|
|
2591
2929
|
}
|
|
2930
|
+
await beforeMailbox?.(stores, taskId);
|
|
2931
|
+
const message = await stores.mailbox.append({
|
|
2932
|
+
deviceId,
|
|
2933
|
+
messageId,
|
|
2934
|
+
materialize: async (seq) => {
|
|
2935
|
+
const envelope2 = buildEnvelope(taskId, seq, messageId);
|
|
2936
|
+
const body = encodeEnvelope(envelope2);
|
|
2937
|
+
const bytes = new TextEncoder().encode(body);
|
|
2938
|
+
return {
|
|
2939
|
+
body,
|
|
2940
|
+
bodyHash: contentHash(await options.crypto.sha256(bytes)),
|
|
2941
|
+
byteSize: BigInt(bytes.length)
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2592
2945
|
const envelope = decodeEnvelope(message.body);
|
|
2593
2946
|
if (envelope.seq !== message.seq) {
|
|
2594
2947
|
throw new ByokCloudError(
|
|
@@ -2596,11 +2949,22 @@ function createByokCloud(options) {
|
|
|
2596
2949
|
`Mailbox stored offer seq ${String(envelope.seq)} at row ${message.seq}; the daemon's redelivery cursor would be wrong.`
|
|
2597
2950
|
);
|
|
2598
2951
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2952
|
+
if (!sameOfferEnvelope(buildEnvelope(taskId, message.seq, messageId), envelope)) {
|
|
2953
|
+
throw new ByokCloudError(
|
|
2954
|
+
"coordination_input_invalid",
|
|
2955
|
+
`Task ${taskId} already has a different executable offer body.`
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
if (agentRef !== void 0) {
|
|
2959
|
+
const delivered = await stores.receipts.record({ key: deliveryReceiptKey, body: messageId });
|
|
2960
|
+
if (reservation?.created === false && !delivered.created) {
|
|
2961
|
+
throw new ByokCloudError(
|
|
2962
|
+
"agent_task_already_exists",
|
|
2963
|
+
`Task ${taskId} already has a durable Agent attempt and cannot be enqueued again.`
|
|
2964
|
+
);
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
return { taskId, seq: message.seq, envelope, attempt: openedBeforeAppend };
|
|
2604
2968
|
}
|
|
2605
2969
|
async function assertAgentCapabilities(tenant, deviceId, required) {
|
|
2606
2970
|
const device = await options.cloud.devices.get(tenant, deviceId);
|
|
@@ -2655,6 +3019,236 @@ function createByokCloud(options) {
|
|
|
2655
3019
|
}
|
|
2656
3020
|
return { seq: message.seq, envelope };
|
|
2657
3021
|
}
|
|
3022
|
+
async function agentControlEnvelopeId(domain, tenant, deviceId, agentRef, logicalRequestId) {
|
|
3023
|
+
return uuidFromSha256(
|
|
3024
|
+
await options.crypto.sha256(
|
|
3025
|
+
new TextEncoder().encode(
|
|
3026
|
+
JSON.stringify({ domain, tenant, deviceId, agentRef, logicalRequestId })
|
|
3027
|
+
)
|
|
3028
|
+
)
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
async function approvalDecisionSource(tenant, taskId, attempt, pending, decision) {
|
|
3032
|
+
const agent = attempt.agentRef;
|
|
3033
|
+
return uuidFromSha256(
|
|
3034
|
+
await options.crypto.sha256(
|
|
3035
|
+
new TextEncoder().encode(
|
|
3036
|
+
JSON.stringify({
|
|
3037
|
+
domain: "byok:approval-decision",
|
|
3038
|
+
tenant,
|
|
3039
|
+
taskId,
|
|
3040
|
+
deviceId: attempt.deviceId,
|
|
3041
|
+
ownerDeviceId: attempt.ownerDeviceId,
|
|
3042
|
+
agentRef: agent,
|
|
3043
|
+
requestSourceEnvelopeId: pending.sourceEnvelopeId,
|
|
3044
|
+
requestRevision: pending.revision,
|
|
3045
|
+
decision
|
|
3046
|
+
})
|
|
3047
|
+
)
|
|
3048
|
+
)
|
|
3049
|
+
);
|
|
3050
|
+
}
|
|
3051
|
+
function latestApprovalRequest(tail) {
|
|
3052
|
+
if (tail === void 0) return void 0;
|
|
3053
|
+
for (let index = tail.entries.length - 1; index >= 0; index -= 1) {
|
|
3054
|
+
const entry = tail.entries[index];
|
|
3055
|
+
if (entry.event.type !== "approval_requested") continue;
|
|
3056
|
+
return {
|
|
3057
|
+
sourceEnvelopeId: entry.sourceEnvelopeId,
|
|
3058
|
+
revision: entry.revision,
|
|
3059
|
+
...entry.event.approvalId === void 0 ? {} : { approvalId: entry.event.approvalId }
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
return void 0;
|
|
3063
|
+
}
|
|
3064
|
+
async function recordedHostDecision(tenant, taskId, attempt, tail, request) {
|
|
3065
|
+
for (const decision of ["approve", "reject"]) {
|
|
3066
|
+
const sourceEnvelopeId = await approvalDecisionSource(tenant, taskId, attempt, request, decision);
|
|
3067
|
+
const index = tail?.entries.findIndex((candidate) => candidate.sourceEnvelopeId === sourceEnvelopeId) ?? -1;
|
|
3068
|
+
const entry = index < 0 ? void 0 : tail.entries[index];
|
|
3069
|
+
if (entry?.event.type !== "approval_resolved" || entry.event.resolvedBy !== "host") continue;
|
|
3070
|
+
const laterResolution = tail.entries.slice(index + 1).some(
|
|
3071
|
+
(candidate) => candidate.event.type === "approval_resolved" && (request.approvalId === void 0 || candidate.event.approvalId === request.approvalId)
|
|
3072
|
+
);
|
|
3073
|
+
if (!laterResolution) {
|
|
3074
|
+
if (entry.event.decision === "reject") {
|
|
3075
|
+
return {
|
|
3076
|
+
decision: "reject",
|
|
3077
|
+
...entry.event.approvalId === void 0 ? {} : { approvalId: entry.event.approvalId },
|
|
3078
|
+
reason: entry.event.reason
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
return {
|
|
3082
|
+
decision: "approve",
|
|
3083
|
+
...entry.event.approvalId === void 0 ? {} : { approvalId: entry.event.approvalId }
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
return void 0;
|
|
3088
|
+
}
|
|
3089
|
+
async function resolveApproval(tenant, taskId, decision, opts) {
|
|
3090
|
+
const stores = tenantStoresFor(controlPlane(tenant), root);
|
|
3091
|
+
const attempt = await stores.tasks.get(taskId);
|
|
3092
|
+
if (attempt === void 0) {
|
|
3093
|
+
throw new ByokCloudError("task_not_found", `Task ${taskId} was not found for this tenant.`);
|
|
3094
|
+
}
|
|
3095
|
+
if (isTerminalAttemptStatus(attempt.status)) {
|
|
3096
|
+
throw new ByokCloudError(
|
|
3097
|
+
"task_not_awaiting_approval",
|
|
3098
|
+
`Task ${taskId} already reached ${attempt.status}; there is no approval left to ${decision}.`
|
|
3099
|
+
);
|
|
3100
|
+
}
|
|
3101
|
+
const tail = await stores.approvals.read(taskId);
|
|
3102
|
+
const pending = pendingApproval(tail);
|
|
3103
|
+
const latestRequest = latestApprovalRequest(tail);
|
|
3104
|
+
if (pending === void 0) {
|
|
3105
|
+
if (latestRequest !== void 0) {
|
|
3106
|
+
const recorded = await recordedHostDecision(tenant, taskId, attempt, tail, latestRequest);
|
|
3107
|
+
if (recorded !== void 0) {
|
|
3108
|
+
if (recorded.decision !== decision) {
|
|
3109
|
+
throw new ByokCloudError(
|
|
3110
|
+
"coordination_input_invalid",
|
|
3111
|
+
`Task ${taskId} already has the opposite host approval decision.`
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
if (recorded.decision === "reject" && recorded.reason !== (opts?.reason ?? null)) {
|
|
3115
|
+
throw new ByokCloudError(
|
|
3116
|
+
"coordination_input_invalid",
|
|
3117
|
+
`Task ${taskId} already has a host rejection with a different immutable reason.`
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
const ownerDeviceId2 = attempt.ownerDeviceId;
|
|
3121
|
+
if (ownerDeviceId2 === void 0) {
|
|
3122
|
+
throw new ByokCloudError(
|
|
3123
|
+
"task_not_awaiting_approval",
|
|
3124
|
+
`Task ${taskId} has no owning device to receive its recorded ${decision}.`
|
|
3125
|
+
);
|
|
3126
|
+
}
|
|
3127
|
+
const messageId2 = await approvalDecisionSource(tenant, taskId, attempt, latestRequest, decision);
|
|
3128
|
+
const targeting2 = recorded.approvalId === void 0 ? {} : { approvalId: recorded.approvalId };
|
|
3129
|
+
const recordedReason = recorded.decision === "reject" ? recorded.reason : void 0;
|
|
3130
|
+
return enqueueAgentControlEnvelope(
|
|
3131
|
+
tenant,
|
|
3132
|
+
ownerDeviceId2,
|
|
3133
|
+
messageId2,
|
|
3134
|
+
(seq) => decision === "approve" ? createEnvelope("task.approve", targeting2, { id: messageId2, taskId, seq }) : createEnvelope(
|
|
3135
|
+
"task.reject",
|
|
3136
|
+
{ ...targeting2, ...recordedReason === null || recordedReason === void 0 ? {} : { reason: recordedReason } },
|
|
3137
|
+
{ id: messageId2, taskId, seq }
|
|
3138
|
+
)
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
throw new ByokCloudError(
|
|
3143
|
+
"task_not_awaiting_approval",
|
|
3144
|
+
`Task ${taskId} has no pending approval to ${decision}.`
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
if (opts?.approvalId !== void 0 && pending.approvalId !== void 0 && opts.approvalId !== pending.approvalId) {
|
|
3148
|
+
throw new StaleApprovalError(taskId, opts.approvalId, pending.approvalId);
|
|
3149
|
+
}
|
|
3150
|
+
const ownerDeviceId = attempt.ownerDeviceId;
|
|
3151
|
+
if (ownerDeviceId === void 0) {
|
|
3152
|
+
throw new ByokCloudError(
|
|
3153
|
+
"task_not_awaiting_approval",
|
|
3154
|
+
`Task ${taskId} has no owning device; no runtime is paused on an approval to ${decision}.`
|
|
3155
|
+
);
|
|
3156
|
+
}
|
|
3157
|
+
const approvalId = pending.approvalId;
|
|
3158
|
+
const messageId = await approvalDecisionSource(tenant, taskId, attempt, pending, decision);
|
|
3159
|
+
const event = decision === "reject" ? {
|
|
3160
|
+
type: "approval_resolved",
|
|
3161
|
+
...approvalId === void 0 ? {} : { approvalId },
|
|
3162
|
+
decision: "reject",
|
|
3163
|
+
resolvedBy: "host",
|
|
3164
|
+
reason: opts?.reason ?? null,
|
|
3165
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3166
|
+
} : {
|
|
3167
|
+
type: "approval_resolved",
|
|
3168
|
+
...approvalId === void 0 ? {} : { approvalId },
|
|
3169
|
+
decision: "approve",
|
|
3170
|
+
resolvedBy: "host",
|
|
3171
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3172
|
+
};
|
|
3173
|
+
const resolved = await root.cloud.approvals.resolvePending(tenant, {
|
|
3174
|
+
taskId,
|
|
3175
|
+
sourceEnvelopeId: messageId,
|
|
3176
|
+
expectedSourceEnvelopeId: pending.sourceEnvelopeId,
|
|
3177
|
+
expectedRevision: pending.revision,
|
|
3178
|
+
event
|
|
3179
|
+
});
|
|
3180
|
+
let deliveredApprovalId = approvalId;
|
|
3181
|
+
let deliveredReason = decision === "reject" ? opts?.reason ?? null : void 0;
|
|
3182
|
+
if (resolved.status === "conflict" || resolved.status === "superseded" || resolved.status === "absent") {
|
|
3183
|
+
const currentTail = resolved.tail ?? await stores.approvals.read(taskId);
|
|
3184
|
+
const currentPending = pendingApproval(currentTail);
|
|
3185
|
+
const recorded = await recordedHostDecision(tenant, taskId, attempt, currentTail, pending);
|
|
3186
|
+
if (recorded?.decision === decision) {
|
|
3187
|
+
deliveredApprovalId = recorded.approvalId;
|
|
3188
|
+
if (recorded.decision === "reject") {
|
|
3189
|
+
if (recorded.reason !== deliveredReason) {
|
|
3190
|
+
throw new ByokCloudError(
|
|
3191
|
+
"coordination_input_invalid",
|
|
3192
|
+
`Task ${taskId} already has a host rejection with a different immutable reason.`
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
deliveredReason = recorded.reason;
|
|
3196
|
+
}
|
|
3197
|
+
} else if (recorded !== void 0) {
|
|
3198
|
+
throw new ByokCloudError(
|
|
3199
|
+
"coordination_input_invalid",
|
|
3200
|
+
`Task ${taskId} already has the opposite host approval decision.`
|
|
3201
|
+
);
|
|
3202
|
+
} else if (currentPending !== void 0 && opts?.approvalId !== void 0 && currentPending.approvalId !== void 0 && currentPending.approvalId !== opts.approvalId) {
|
|
3203
|
+
throw new StaleApprovalError(taskId, opts.approvalId, currentPending.approvalId);
|
|
3204
|
+
} else {
|
|
3205
|
+
throw new ByokCloudError(
|
|
3206
|
+
"task_not_awaiting_approval",
|
|
3207
|
+
`Task ${taskId} no longer has the pending approval to ${decision}.`
|
|
3208
|
+
);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
const targeting = deliveredApprovalId === void 0 ? {} : { approvalId: deliveredApprovalId };
|
|
3212
|
+
return enqueueAgentControlEnvelope(
|
|
3213
|
+
tenant,
|
|
3214
|
+
ownerDeviceId,
|
|
3215
|
+
messageId,
|
|
3216
|
+
(seq) => decision === "approve" ? createEnvelope("task.approve", targeting, { id: messageId, taskId, seq }) : createEnvelope(
|
|
3217
|
+
"task.reject",
|
|
3218
|
+
{ ...targeting, ...deliveredReason === null || deliveredReason === void 0 ? {} : { reason: deliveredReason } },
|
|
3219
|
+
{ id: messageId, taskId, seq }
|
|
3220
|
+
)
|
|
3221
|
+
);
|
|
3222
|
+
}
|
|
3223
|
+
async function steerTask(tenant, taskId, payload) {
|
|
3224
|
+
const stores = tenantStoresFor(controlPlane(tenant), root);
|
|
3225
|
+
const attempt = await stores.tasks.get(taskId);
|
|
3226
|
+
if (attempt === void 0) {
|
|
3227
|
+
throw new ByokCloudError("task_not_found", `Task ${taskId} was not found for this tenant.`);
|
|
3228
|
+
}
|
|
3229
|
+
if (isTerminalAttemptStatus(attempt.status)) {
|
|
3230
|
+
throw new SteerRejectedError(taskId, "task_terminal", attempt.status, attempt.claimedRuntime);
|
|
3231
|
+
}
|
|
3232
|
+
const ownerDeviceId = attempt.ownerDeviceId;
|
|
3233
|
+
if (attempt.status !== "running" || ownerDeviceId === void 0) {
|
|
3234
|
+
throw new SteerRejectedError(taskId, "task_not_running", attempt.status, attempt.claimedRuntime);
|
|
3235
|
+
}
|
|
3236
|
+
if (attempt.claimedRuntimeCapabilities?.steer !== true) {
|
|
3237
|
+
throw new SteerRejectedError(
|
|
3238
|
+
taskId,
|
|
3239
|
+
"steer_unsupported_runtime",
|
|
3240
|
+
attempt.status,
|
|
3241
|
+
attempt.claimedRuntime
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
const messageId = options.crypto.randomUuid();
|
|
3245
|
+
return enqueueAgentControlEnvelope(
|
|
3246
|
+
tenant,
|
|
3247
|
+
ownerDeviceId,
|
|
3248
|
+
messageId,
|
|
3249
|
+
(seq) => createEnvelope("task.steer", { text: payload.text }, { id: messageId, taskId, seq })
|
|
3250
|
+
);
|
|
3251
|
+
}
|
|
2658
3252
|
async function getAgentHomeProjectionStatus(tenant, deviceId, input) {
|
|
2659
3253
|
return readAgentHomeProjectionStatus(
|
|
2660
3254
|
tenantStoresFor(controlPlane(tenant), root).receipts,
|
|
@@ -2928,7 +3522,7 @@ function createByokCloud(options) {
|
|
|
2928
3522
|
]);
|
|
2929
3523
|
const stores = tenantStoresFor(controlPlane(tenant), root);
|
|
2930
3524
|
const persisted = await stores.receipts.record({
|
|
2931
|
-
key:
|
|
3525
|
+
key: agentReliabilityKey("agent-content-request", deviceId, payload.agentRef, payload.requestId),
|
|
2932
3526
|
body: JSON.stringify(payload)
|
|
2933
3527
|
});
|
|
2934
3528
|
if (!persisted.created && persisted.receipt.body !== JSON.stringify(payload)) {
|
|
@@ -2937,11 +3531,18 @@ function createByokCloud(options) {
|
|
|
2937
3531
|
`Request ${payload.requestId} already exists with a different immutable content-read body.`
|
|
2938
3532
|
);
|
|
2939
3533
|
}
|
|
3534
|
+
const messageId = await agentControlEnvelopeId(
|
|
3535
|
+
"byok:agent-content-read",
|
|
3536
|
+
tenant,
|
|
3537
|
+
deviceId,
|
|
3538
|
+
payload.agentRef,
|
|
3539
|
+
payload.requestId
|
|
3540
|
+
);
|
|
2940
3541
|
const control = await enqueueAgentControlEnvelope(
|
|
2941
3542
|
tenant,
|
|
2942
3543
|
deviceId,
|
|
2943
|
-
|
|
2944
|
-
(seq) => createEnvelope("agent.content.read", payload, { id:
|
|
3544
|
+
messageId,
|
|
3545
|
+
(seq) => createEnvelope("agent.content.read", payload, { id: messageId, seq })
|
|
2945
3546
|
);
|
|
2946
3547
|
if (control.envelope.type !== "agent.content.read" || JSON.stringify(control.envelope.payload) !== JSON.stringify(payload)) {
|
|
2947
3548
|
throw new ByokCloudError(
|
|
@@ -2960,7 +3561,7 @@ function createByokCloud(options) {
|
|
|
2960
3561
|
const stores = tenantStoresFor(controlPlane(tenant), root);
|
|
2961
3562
|
const requestBody = JSON.stringify(payload);
|
|
2962
3563
|
const persisted = await stores.receipts.record({
|
|
2963
|
-
key: agentHomeProjectionRequestKey(deviceId, payload.requestId),
|
|
3564
|
+
key: agentHomeProjectionRequestKey(deviceId, payload.agentRef, payload.requestId),
|
|
2964
3565
|
body: requestBody
|
|
2965
3566
|
});
|
|
2966
3567
|
const persistedPayload = AgentHomeProjectionPayloadSchema.parse(JSON.parse(persisted.receipt.body));
|
|
@@ -2970,11 +3571,18 @@ function createByokCloud(options) {
|
|
|
2970
3571
|
`Agent-home projection request ${payload.requestId} already exists with a different immutable desired body.`
|
|
2971
3572
|
);
|
|
2972
3573
|
}
|
|
3574
|
+
const messageId = await agentControlEnvelopeId(
|
|
3575
|
+
"byok:agent-home-projection",
|
|
3576
|
+
tenant,
|
|
3577
|
+
deviceId,
|
|
3578
|
+
payload.agentRef,
|
|
3579
|
+
payload.requestId
|
|
3580
|
+
);
|
|
2973
3581
|
const control = await enqueueAgentControlEnvelope(
|
|
2974
3582
|
tenant,
|
|
2975
3583
|
deviceId,
|
|
2976
|
-
|
|
2977
|
-
(seq) => createEnvelope("agent.home.projection", payload, { id:
|
|
3584
|
+
messageId,
|
|
3585
|
+
(seq) => createEnvelope("agent.home.projection", payload, { id: messageId, seq })
|
|
2978
3586
|
);
|
|
2979
3587
|
if (control.envelope.type !== "agent.home.projection" || !sameAgentHomeProjectionRequest(payload, control.envelope.payload)) {
|
|
2980
3588
|
throw new ByokCloudError(
|
|
@@ -2998,6 +3606,13 @@ function createByokCloud(options) {
|
|
|
2998
3606
|
getAgentHomeProjectionStatus,
|
|
2999
3607
|
completeAgentHomeProjection,
|
|
3000
3608
|
eraseAgentMemoryProjection,
|
|
3609
|
+
approveTask(tenant, taskId, opts) {
|
|
3610
|
+
return resolveApproval(tenant, taskId, "approve", opts);
|
|
3611
|
+
},
|
|
3612
|
+
rejectTask(tenant, taskId, opts) {
|
|
3613
|
+
return resolveApproval(tenant, taskId, "reject", opts);
|
|
3614
|
+
},
|
|
3615
|
+
steerTask,
|
|
3001
3616
|
async cancelTask(tenant, taskId, reason) {
|
|
3002
3617
|
const stores = tenantStoresFor(controlPlane(tenant), root);
|
|
3003
3618
|
const proposedMessageId = options.crypto.randomUuid();
|
|
@@ -3037,11 +3652,14 @@ function createByokCloud(options) {
|
|
|
3037
3652
|
readTaskAttempt(tenant, taskId) {
|
|
3038
3653
|
return tenantStoresFor(controlPlane(tenant), root).tasks.get(taskId);
|
|
3039
3654
|
},
|
|
3655
|
+
listTaskAttempts(tenant, query) {
|
|
3656
|
+
return tenantStoresFor(controlPlane(tenant), root).tasks.list(query);
|
|
3657
|
+
},
|
|
3040
3658
|
readTerminalReceipt(tenant, taskId) {
|
|
3041
3659
|
return tenantStoresFor(controlPlane(tenant), root).receipts.get(terminalReceiptKey(taskId));
|
|
3042
3660
|
},
|
|
3043
|
-
readAgentEgress(tenant, deviceId, eventId) {
|
|
3044
|
-
return tenantStoresFor(controlPlane(tenant), root).egress.get(deviceId, eventId);
|
|
3661
|
+
readAgentEgress(tenant, deviceId, agentRef, eventId) {
|
|
3662
|
+
return tenantStoresFor(controlPlane(tenant), root).egress.get(deviceId, agentRef, eventId);
|
|
3045
3663
|
},
|
|
3046
3664
|
async readTaskResult(tenant, taskId) {
|
|
3047
3665
|
const stores = tenantStoresFor(controlPlane(tenant), root);
|
|
@@ -3256,6 +3874,9 @@ var InMemoryBlobContentProxy = class {
|
|
|
3256
3874
|
const expected = await this.#registry.computeSig(blobId, action, exp);
|
|
3257
3875
|
return this.#registry.crypto.timingSafeEqual(expected, sig);
|
|
3258
3876
|
}
|
|
3877
|
+
async expectedUploadBytes(blobId) {
|
|
3878
|
+
return this.#registry.blobs.get(blobId)?.byteSize;
|
|
3879
|
+
}
|
|
3259
3880
|
async writeContent(blobId, data) {
|
|
3260
3881
|
const record = this.#registry.blobs.get(blobId);
|
|
3261
3882
|
if (record === void 0) return { ok: false, reason: "unknown blobId" };
|
|
@@ -3425,7 +4046,15 @@ var InMemoryInboundDedupStore = class {
|
|
|
3425
4046
|
this.#capacity = capacity;
|
|
3426
4047
|
}
|
|
3427
4048
|
async checkAndRecord(tenant, deviceId, envelopeId) {
|
|
3428
|
-
|
|
4049
|
+
return this.#checkAndRecord(tenantKey(tenant, deviceId), envelopeId);
|
|
4050
|
+
}
|
|
4051
|
+
async checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId) {
|
|
4052
|
+
return this.#checkAndRecord(
|
|
4053
|
+
tenantKey(tenant, agentReliabilityKey("inbound-dedup", deviceId, agentRef, "")),
|
|
4054
|
+
envelopeId
|
|
4055
|
+
);
|
|
4056
|
+
}
|
|
4057
|
+
#checkAndRecord(key, envelopeId) {
|
|
3429
4058
|
let ring = this.#rings.get(key);
|
|
3430
4059
|
if (ring === void 0) {
|
|
3431
4060
|
ring = /* @__PURE__ */ new Set();
|
|
@@ -3469,27 +4098,38 @@ var InMemoryNonceStore = class {
|
|
|
3469
4098
|
});
|
|
3470
4099
|
return nonce;
|
|
3471
4100
|
}
|
|
3472
|
-
async
|
|
4101
|
+
async consumeIfValid(tenant, deviceId, nonce) {
|
|
3473
4102
|
const record = this.#nonces.get(nonce);
|
|
3474
4103
|
if (record === void 0) return false;
|
|
3475
4104
|
if (record.used) return false;
|
|
3476
4105
|
if (record.owner !== tenantKey(tenant, deviceId)) return false;
|
|
3477
|
-
|
|
3478
|
-
}
|
|
3479
|
-
async markUsed(tenant, nonce) {
|
|
3480
|
-
const record = this.#nonces.get(nonce);
|
|
3481
|
-
if (record === void 0) return;
|
|
3482
|
-
if (!record.owner.startsWith(tenantKey(tenant, ""))) return;
|
|
4106
|
+
if (this.#clock.now().getTime() > record.expiresAtMs) return false;
|
|
3483
4107
|
record.used = true;
|
|
4108
|
+
return true;
|
|
3484
4109
|
}
|
|
3485
4110
|
};
|
|
3486
4111
|
|
|
3487
4112
|
// src/stores/in-memory/pairing-codes.ts
|
|
4113
|
+
function bindingOf(input) {
|
|
4114
|
+
return {
|
|
4115
|
+
deviceName: input.deviceName,
|
|
4116
|
+
devicePublicKey: input.devicePublicKey,
|
|
4117
|
+
proofKeyId: input.proofKeyId,
|
|
4118
|
+
proofKeyEpoch: input.proofKeyEpoch,
|
|
4119
|
+
...input.machineId === void 0 ? {} : { machineId: input.machineId }
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
function sameBinding(left, right) {
|
|
4123
|
+
return left.deviceName === right.deviceName && left.devicePublicKey === right.devicePublicKey && left.proofKeyId === right.proofKeyId && left.proofKeyEpoch === right.proofKeyEpoch && left.machineId === right.machineId;
|
|
4124
|
+
}
|
|
3488
4125
|
var InMemoryPairingCodeStore = class {
|
|
3489
4126
|
#codes = /* @__PURE__ */ new Map();
|
|
4127
|
+
#enrollmentTails = /* @__PURE__ */ new Map();
|
|
3490
4128
|
#clock;
|
|
3491
|
-
|
|
4129
|
+
#devices;
|
|
4130
|
+
constructor(clock, devices) {
|
|
3492
4131
|
this.#clock = clock;
|
|
4132
|
+
this.#devices = devices;
|
|
3493
4133
|
}
|
|
3494
4134
|
async issue(tenant, input) {
|
|
3495
4135
|
this.#codes.set(input.code, {
|
|
@@ -3499,13 +4139,42 @@ var InMemoryPairingCodeStore = class {
|
|
|
3499
4139
|
});
|
|
3500
4140
|
return { code: input.code, expiresAt: input.expiresAt };
|
|
3501
4141
|
}
|
|
3502
|
-
async
|
|
3503
|
-
const
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
4142
|
+
async redeemAndRegister(input) {
|
|
4143
|
+
const previous = this.#enrollmentTails.get(input.pairingCode) ?? Promise.resolve();
|
|
4144
|
+
let release;
|
|
4145
|
+
const current = new Promise((resolve) => {
|
|
4146
|
+
release = resolve;
|
|
4147
|
+
});
|
|
4148
|
+
this.#enrollmentTails.set(input.pairingCode, current);
|
|
4149
|
+
await previous;
|
|
4150
|
+
try {
|
|
4151
|
+
const record = this.#codes.get(input.pairingCode);
|
|
4152
|
+
if (record === void 0) return void 0;
|
|
4153
|
+
if (record.used) {
|
|
4154
|
+
const completion = record.completion;
|
|
4155
|
+
if (completion === void 0 || !sameBinding(completion.binding, bindingOf(input))) return void 0;
|
|
4156
|
+
const active = await this.#devices.get(completion.device.tenantId, completion.device.deviceId);
|
|
4157
|
+
return active === void 0 ? void 0 : completion.device;
|
|
4158
|
+
}
|
|
4159
|
+
if (this.#clock.now().getTime() > record.expiresAtMs) return void 0;
|
|
4160
|
+
const device = await this.#devices.register(record.claims.tenantId, {
|
|
4161
|
+
productId: record.claims.productId,
|
|
4162
|
+
deviceId: input.deviceId,
|
|
4163
|
+
deviceName: input.deviceName,
|
|
4164
|
+
devicePublicKey: input.devicePublicKey,
|
|
4165
|
+
proofKeyId: input.proofKeyId,
|
|
4166
|
+
proofKeyEpoch: input.proofKeyEpoch,
|
|
4167
|
+
...input.machineId === void 0 ? {} : { machineId: input.machineId }
|
|
4168
|
+
});
|
|
4169
|
+
record.used = true;
|
|
4170
|
+
record.completion = { device, binding: bindingOf(input) };
|
|
4171
|
+
return device;
|
|
4172
|
+
} finally {
|
|
4173
|
+
release();
|
|
4174
|
+
if (this.#enrollmentTails.get(input.pairingCode) === current) {
|
|
4175
|
+
this.#enrollmentTails.delete(input.pairingCode);
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
3509
4178
|
}
|
|
3510
4179
|
};
|
|
3511
4180
|
var InMemoryRequestReceiptStore = class {
|
|
@@ -3553,6 +4222,36 @@ var InMemoryProofRequestReceiptStore = class {
|
|
|
3553
4222
|
return this.#receipts.get(tenantKey(tenant, deviceId, requestId));
|
|
3554
4223
|
}
|
|
3555
4224
|
};
|
|
4225
|
+
|
|
4226
|
+
// src/stores/ports.ts
|
|
4227
|
+
var TASK_ATTEMPT_STATUSES = [
|
|
4228
|
+
"offered",
|
|
4229
|
+
"claimed",
|
|
4230
|
+
"running",
|
|
4231
|
+
"cancel_requested",
|
|
4232
|
+
"complete",
|
|
4233
|
+
"failed",
|
|
4234
|
+
"cancelled"
|
|
4235
|
+
];
|
|
4236
|
+
var BLOB_READ_ERROR_CODES = ["blob_upstream_unavailable", "blob_upstream_stream_interrupted"];
|
|
4237
|
+
var CLOUD_STORE_NAMES = [
|
|
4238
|
+
"activity",
|
|
4239
|
+
"approvals",
|
|
4240
|
+
"devices",
|
|
4241
|
+
"pairingCodes",
|
|
4242
|
+
"pairing",
|
|
4243
|
+
"nonces",
|
|
4244
|
+
"dedup",
|
|
4245
|
+
"tasks",
|
|
4246
|
+
"cancellations",
|
|
4247
|
+
"receipts",
|
|
4248
|
+
"egress",
|
|
4249
|
+
"proofReceipts",
|
|
4250
|
+
"blobs",
|
|
4251
|
+
"rateLimiter"
|
|
4252
|
+
];
|
|
4253
|
+
|
|
4254
|
+
// src/stores/in-memory/task-attempts.ts
|
|
3556
4255
|
function sameAgentRef3(left, right) {
|
|
3557
4256
|
return left?.agentId === right?.agentId && left?.profileRevision === right?.profileRevision;
|
|
3558
4257
|
}
|
|
@@ -3595,6 +4294,41 @@ var InMemoryTaskAttemptStore = class {
|
|
|
3595
4294
|
return { attempt, created: true };
|
|
3596
4295
|
});
|
|
3597
4296
|
}
|
|
4297
|
+
async reserveAgentMessage(tenant, input) {
|
|
4298
|
+
const key = tenantKey(tenant, input.taskId);
|
|
4299
|
+
return this.#state.mutate(key, () => {
|
|
4300
|
+
const existing = this.#state.messageAdmissions.get(key);
|
|
4301
|
+
if (existing !== void 0) {
|
|
4302
|
+
return existing.deviceId === input.deviceId && existing.messageId === input.messageId && existing.payloadBody === input.payloadBody ? "pending" : "rejected";
|
|
4303
|
+
}
|
|
4304
|
+
const attempt = this.#state.attempts.get(key);
|
|
4305
|
+
if (attempt === void 0 || attempt.deviceId !== input.deviceId || attempt.cancellation !== void 0 || !["offered", "claimed", "running"].includes(attempt.status)) return "rejected";
|
|
4306
|
+
this.#state.messageAdmissions.set(key, {
|
|
4307
|
+
deviceId: input.deviceId,
|
|
4308
|
+
messageId: input.messageId,
|
|
4309
|
+
payloadBody: input.payloadBody
|
|
4310
|
+
});
|
|
4311
|
+
return "reserved";
|
|
4312
|
+
});
|
|
4313
|
+
}
|
|
4314
|
+
async readAgentMessage(tenant, input) {
|
|
4315
|
+
const admission = this.#state.messageAdmissions.get(tenantKey(tenant, input.taskId));
|
|
4316
|
+
return admission !== void 0 && admission.deviceId === input.deviceId && admission.messageId === input.messageId && admission.payloadBody === input.payloadBody ? admission : void 0;
|
|
4317
|
+
}
|
|
4318
|
+
async finalizeAgentMessage(tenant, input) {
|
|
4319
|
+
const key = tenantKey(tenant, input.taskId);
|
|
4320
|
+
return this.#state.mutate(key, () => this.#terminalize(key, input));
|
|
4321
|
+
}
|
|
4322
|
+
#terminalize(key, input) {
|
|
4323
|
+
const admission = this.#state.messageAdmissions.get(key);
|
|
4324
|
+
if (admission === void 0 || admission.deviceId !== input.deviceId || admission.messageId !== input.messageId || admission.payloadBody !== input.payloadBody) {
|
|
4325
|
+
return void 0;
|
|
4326
|
+
}
|
|
4327
|
+
if (admission.terminalBody !== void 0) return admission;
|
|
4328
|
+
const terminal = { ...admission, terminalBody: input.terminalBody };
|
|
4329
|
+
this.#state.messageAdmissions.set(key, terminal);
|
|
4330
|
+
return terminal;
|
|
4331
|
+
}
|
|
3598
4332
|
async get(tenant, taskId) {
|
|
3599
4333
|
return this.#state.attempts.get(tenantKey(tenant, taskId));
|
|
3600
4334
|
}
|
|
@@ -3604,6 +4338,18 @@ var InMemoryTaskAttemptStore = class {
|
|
|
3604
4338
|
return attempt === void 0 ? [] : [attempt];
|
|
3605
4339
|
});
|
|
3606
4340
|
}
|
|
4341
|
+
async list(tenant, query) {
|
|
4342
|
+
assertTaskAttemptListLimit(query.limit);
|
|
4343
|
+
const cursor = query.cursor;
|
|
4344
|
+
const matches = [...this.#state.attempts.values()].filter((attempt) => attempt.tenantId === tenant).filter((attempt) => cursor === void 0 || attempt.taskId > cursor).sort((left, right) => left.taskId < right.taskId ? -1 : left.taskId > right.taskId ? 1 : 0);
|
|
4345
|
+
const attempts = matches.slice(0, query.limit);
|
|
4346
|
+
const hasMore = matches.length > attempts.length;
|
|
4347
|
+
const last = attempts.at(-1);
|
|
4348
|
+
return {
|
|
4349
|
+
attempts,
|
|
4350
|
+
...hasMore && last !== void 0 ? { nextCursor: last.taskId } : {}
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
3607
4353
|
async claim(tenant, input) {
|
|
3608
4354
|
const key = tenantKey(tenant, input.taskId);
|
|
3609
4355
|
return this.#state.mutate(key, () => {
|
|
@@ -3613,6 +4359,8 @@ var InMemoryTaskAttemptStore = class {
|
|
|
3613
4359
|
const claimed = {
|
|
3614
4360
|
...existing,
|
|
3615
4361
|
ownerDeviceId: input.deviceId,
|
|
4362
|
+
...input.runtime === void 0 ? {} : { claimedRuntime: input.runtime },
|
|
4363
|
+
...input.capabilities === void 0 ? {} : { claimedRuntimeCapabilities: { ...input.capabilities } },
|
|
3616
4364
|
status: "claimed",
|
|
3617
4365
|
updatedAt: this.#now()
|
|
3618
4366
|
};
|
|
@@ -3647,6 +4395,7 @@ var InMemoryTaskAttemptStore = class {
|
|
|
3647
4395
|
};
|
|
3648
4396
|
var InMemoryTaskAttemptState = class {
|
|
3649
4397
|
attempts = /* @__PURE__ */ new Map();
|
|
4398
|
+
messageAdmissions = /* @__PURE__ */ new Map();
|
|
3650
4399
|
#clock;
|
|
3651
4400
|
#mutationTails = /* @__PURE__ */ new Map();
|
|
3652
4401
|
constructor(clock) {
|
|
@@ -3734,6 +4483,14 @@ var InMemoryActivityStore = class {
|
|
|
3734
4483
|
const key = activityTailKey(tenant, input.taskId);
|
|
3735
4484
|
const existing = this.#tails.get(key);
|
|
3736
4485
|
const live = existing !== void 0 && receivedAt < existing.expiresAt ? existing : void 0;
|
|
4486
|
+
const sourceState = activitySourceBatchState(live?.entries ?? [], input);
|
|
4487
|
+
if (sourceState === "same") return live;
|
|
4488
|
+
if (sourceState === "conflict") {
|
|
4489
|
+
throw new ByokCloudError(
|
|
4490
|
+
"coordination_input_invalid",
|
|
4491
|
+
`Activity source envelope ${input.sourceEnvelopeId} already belongs to another canonical batch.`
|
|
4492
|
+
);
|
|
4493
|
+
}
|
|
3737
4494
|
const incoming = projectTimelineEvents(input, receivedAt);
|
|
3738
4495
|
for (const next of incoming) {
|
|
3739
4496
|
const collision = live?.entries.find(
|
|
@@ -3825,6 +4582,46 @@ var InMemoryApprovalTimelineStore = class {
|
|
|
3825
4582
|
this.#tails.set(key, tail);
|
|
3826
4583
|
return tail;
|
|
3827
4584
|
}
|
|
4585
|
+
async resolvePending(tenant, input) {
|
|
4586
|
+
const { capacity, ttlMs, event } = validateApprovalTimelineResolve(input);
|
|
4587
|
+
const now = this.clock.now();
|
|
4588
|
+
const receivedAt = now.toISOString();
|
|
4589
|
+
const key = approvalTimelineKey(tenant, input.taskId);
|
|
4590
|
+
const existing = this.#tails.get(key);
|
|
4591
|
+
const live = existing !== void 0 && receivedAt < existing.expiresAt ? existing : void 0;
|
|
4592
|
+
if (live === void 0) return { status: "absent" };
|
|
4593
|
+
const duplicate = live.entries.find((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId);
|
|
4594
|
+
if (duplicate !== void 0) {
|
|
4595
|
+
if (JSON.stringify(duplicate.event) !== JSON.stringify(event)) return { status: "conflict", tail: live };
|
|
4596
|
+
return { status: "replayed", tail: live };
|
|
4597
|
+
}
|
|
4598
|
+
const pending = pendingObservation(live.entries);
|
|
4599
|
+
if (pending === void 0) return { status: "absent", tail: live };
|
|
4600
|
+
if (pending.sourceEnvelopeId !== input.expectedSourceEnvelopeId || pending.revision !== input.expectedRevision) {
|
|
4601
|
+
return { status: "superseded", tail: live };
|
|
4602
|
+
}
|
|
4603
|
+
const revision = (live.cursor ?? 0) + 1;
|
|
4604
|
+
const observation = ApprovalObservationSchema.parse({
|
|
4605
|
+
taskId: input.taskId,
|
|
4606
|
+
sourceEnvelopeId: input.sourceEnvelopeId,
|
|
4607
|
+
revision,
|
|
4608
|
+
receivedAt,
|
|
4609
|
+
event
|
|
4610
|
+
});
|
|
4611
|
+
const allEntries = [...live.entries, observation];
|
|
4612
|
+
const evicted = Math.max(allEntries.length - capacity, 0);
|
|
4613
|
+
const tail = {
|
|
4614
|
+
tenantId: tenant,
|
|
4615
|
+
taskId: input.taskId,
|
|
4616
|
+
entries: allEntries.slice(evicted),
|
|
4617
|
+
cursor: revision,
|
|
4618
|
+
dropped: live.dropped + evicted,
|
|
4619
|
+
capacity,
|
|
4620
|
+
expiresAt: new Date(now.getTime() + ttlMs).toISOString()
|
|
4621
|
+
};
|
|
4622
|
+
this.#tails.set(key, tail);
|
|
4623
|
+
return { status: "applied", tail };
|
|
4624
|
+
}
|
|
3828
4625
|
async read(tenant, taskId) {
|
|
3829
4626
|
const key = approvalTimelineKey(tenant, taskId);
|
|
3830
4627
|
const tail = this.#tails.get(key);
|
|
@@ -3836,6 +4633,19 @@ var InMemoryApprovalTimelineStore = class {
|
|
|
3836
4633
|
return tail;
|
|
3837
4634
|
}
|
|
3838
4635
|
};
|
|
4636
|
+
function pendingObservation(entries) {
|
|
4637
|
+
let pending;
|
|
4638
|
+
for (const entry of entries) {
|
|
4639
|
+
if (entry.event.type === "approval_requested") {
|
|
4640
|
+
pending = entry;
|
|
4641
|
+
continue;
|
|
4642
|
+
}
|
|
4643
|
+
if (pending !== void 0 && (pending.event.approvalId === void 0 || pending.event.approvalId === entry.event.approvalId)) {
|
|
4644
|
+
pending = void 0;
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
return pending;
|
|
4648
|
+
}
|
|
3839
4649
|
var InMemoryAgentEgressStore = class {
|
|
3840
4650
|
#records = /* @__PURE__ */ new Map();
|
|
3841
4651
|
#clock;
|
|
@@ -3843,7 +4653,10 @@ var InMemoryAgentEgressStore = class {
|
|
|
3843
4653
|
this.#clock = clock;
|
|
3844
4654
|
}
|
|
3845
4655
|
async record(tenant, input) {
|
|
3846
|
-
const key = tenantKey(
|
|
4656
|
+
const key = tenantKey(
|
|
4657
|
+
tenant,
|
|
4658
|
+
agentReliabilityKey("agent-egress", input.deviceId, input.payload.agentRef, input.payload.eventId)
|
|
4659
|
+
);
|
|
3847
4660
|
const existing = this.#records.get(key);
|
|
3848
4661
|
if (existing !== void 0) return { record: existing, created: false };
|
|
3849
4662
|
const record = {
|
|
@@ -3856,8 +4669,8 @@ var InMemoryAgentEgressStore = class {
|
|
|
3856
4669
|
this.#records.set(key, record);
|
|
3857
4670
|
return { record, created: true };
|
|
3858
4671
|
}
|
|
3859
|
-
async get(tenant, deviceId, eventId) {
|
|
3860
|
-
return this.#records.get(tenantKey(tenant,
|
|
4672
|
+
async get(tenant, deviceId, agentRef, eventId) {
|
|
4673
|
+
return this.#records.get(tenantKey(tenant, agentReliabilityKey("agent-egress", deviceId, agentRef, eventId)));
|
|
3861
4674
|
}
|
|
3862
4675
|
};
|
|
3863
4676
|
|
|
@@ -3866,12 +4679,17 @@ function createInMemoryCloudStores(clock, crypto2, objects, mailbox) {
|
|
|
3866
4679
|
const blobs = createInMemoryBlobs(clock, crypto2, objects);
|
|
3867
4680
|
const taskState = new InMemoryTaskAttemptState(clock);
|
|
3868
4681
|
const tasks = new InMemoryTaskAttemptStore(clock, taskState);
|
|
4682
|
+
const devices = new InMemoryDeviceDirectory();
|
|
4683
|
+
const pairing = new InMemoryPairingCodeStore(clock, devices);
|
|
3869
4684
|
return {
|
|
3870
4685
|
stores: {
|
|
3871
4686
|
activity: new InMemoryActivityStore(clock),
|
|
3872
4687
|
approvals: new InMemoryApprovalTimelineStore(clock),
|
|
3873
|
-
devices
|
|
3874
|
-
|
|
4688
|
+
devices,
|
|
4689
|
+
// One code/device authority, projected into issuance and consumption
|
|
4690
|
+
// ports so neither side exposes the other's methods.
|
|
4691
|
+
pairingCodes: { issue: pairing.issue.bind(pairing) },
|
|
4692
|
+
pairing: { redeemAndRegister: pairing.redeemAndRegister.bind(pairing) },
|
|
3875
4693
|
nonces: new InMemoryNonceStore(clock, crypto2),
|
|
3876
4694
|
dedup: new InMemoryInboundDedupStore(),
|
|
3877
4695
|
tasks,
|
|
@@ -3919,7 +4737,9 @@ function createInMemoryByokCloud(options = {}) {
|
|
|
3919
4737
|
...options.truthCommitter === void 0 ? {} : { truthCommitter: options.truthCommitter },
|
|
3920
4738
|
...options.truthObjectDownloads === void 0 ? {} : { truthObjectDownloads: options.truthObjectDownloads },
|
|
3921
4739
|
...options.operatorId !== void 0 ? { operatorId: options.operatorId } : {},
|
|
4740
|
+
...options.instanceProductId === void 0 ? {} : { instanceProductId: options.instanceProductId },
|
|
3922
4741
|
...options.agentMessage === void 0 ? {} : { agentMessage: options.agentMessage },
|
|
4742
|
+
...options.observer === void 0 ? {} : { observer: options.observer },
|
|
3923
4743
|
...options.agentMemoryProjectionAuthorizer === void 0 ? {} : { agentMemoryProjectionAuthorizer: options.agentMemoryProjectionAuthorizer },
|
|
3924
4744
|
...options.agentMemoryProjectionStore === void 0 ? {} : { agentMemoryProjectionStore: options.agentMemoryProjectionStore },
|
|
3925
4745
|
...options.maxBlobSizeBytes !== void 0 ? { maxBlobSizeBytes: options.maxBlobSizeBytes } : {},
|
|
@@ -4283,42 +5103,27 @@ function parseSseFrame(frame) {
|
|
|
4283
5103
|
return { type: "board", item: BoardFeedItemSchema.parse(JSON.parse(data.join("\n"))) };
|
|
4284
5104
|
}
|
|
4285
5105
|
|
|
4286
|
-
// src/stores/ports.ts
|
|
4287
|
-
var TASK_ATTEMPT_STATUSES = [
|
|
4288
|
-
"offered",
|
|
4289
|
-
"claimed",
|
|
4290
|
-
"running",
|
|
4291
|
-
"cancel_requested",
|
|
4292
|
-
"complete",
|
|
4293
|
-
"failed",
|
|
4294
|
-
"cancelled"
|
|
4295
|
-
];
|
|
4296
|
-
var BLOB_READ_ERROR_CODES = ["blob_upstream_unavailable", "blob_upstream_stream_interrupted"];
|
|
4297
|
-
var CLOUD_STORE_NAMES = [
|
|
4298
|
-
"activity",
|
|
4299
|
-
"approvals",
|
|
4300
|
-
"devices",
|
|
4301
|
-
"pairingCodes",
|
|
4302
|
-
"nonces",
|
|
4303
|
-
"dedup",
|
|
4304
|
-
"tasks",
|
|
4305
|
-
"cancellations",
|
|
4306
|
-
"receipts",
|
|
4307
|
-
"egress",
|
|
4308
|
-
"proofReceipts",
|
|
4309
|
-
"blobs",
|
|
4310
|
-
"rateLimiter"
|
|
4311
|
-
];
|
|
4312
|
-
|
|
4313
5106
|
// src/stores/ports-contract.ts
|
|
4314
5107
|
var CLOUD_PORT_METHODS = {
|
|
4315
5108
|
activity: ["append", "read"],
|
|
4316
|
-
approvals: ["append", "read"],
|
|
5109
|
+
approvals: ["append", "read", "resolvePending"],
|
|
4317
5110
|
devices: ["register", "get", "revoke", "list", "readiness", "recordCapabilities", "resolveByDeviceId"],
|
|
4318
|
-
pairingCodes: ["issue"
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
5111
|
+
pairingCodes: ["issue"],
|
|
5112
|
+
pairing: ["redeemAndRegister"],
|
|
5113
|
+
nonces: ["issue", "consumeIfValid"],
|
|
5114
|
+
dedup: ["checkAndRecord", "checkAndRecordAgent"],
|
|
5115
|
+
tasks: [
|
|
5116
|
+
"open",
|
|
5117
|
+
"reserveAgentOffer",
|
|
5118
|
+
"reserveAgentMessage",
|
|
5119
|
+
"readAgentMessage",
|
|
5120
|
+
"finalizeAgentMessage",
|
|
5121
|
+
"get",
|
|
5122
|
+
"getMany",
|
|
5123
|
+
"list",
|
|
5124
|
+
"claim",
|
|
5125
|
+
"recordStatus"
|
|
5126
|
+
],
|
|
4322
5127
|
cancellations: ["request"],
|
|
4323
5128
|
receipts: ["record", "get"],
|
|
4324
5129
|
egress: ["record", "get"],
|
|
@@ -4334,6 +5139,7 @@ var CLOUD_PORT_INTERFACES = {
|
|
|
4334
5139
|
approvals: "ApprovalTimelineStore",
|
|
4335
5140
|
devices: "DeviceDirectory",
|
|
4336
5141
|
pairingCodes: "PairingCodeStore",
|
|
5142
|
+
pairing: "PairingEnrollment",
|
|
4337
5143
|
nonces: "NonceStore",
|
|
4338
5144
|
dedup: "InboundDedupStore",
|
|
4339
5145
|
tasks: "TaskAttemptStore",
|
|
@@ -4345,6 +5151,6 @@ var CLOUD_PORT_INTERFACES = {
|
|
|
4345
5151
|
rateLimiter: "InboundRateLimiter"
|
|
4346
5152
|
};
|
|
4347
5153
|
|
|
4348
|
-
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, InMemoryAgentMemoryProjectionAuthorizer, InMemoryAgentMemoryProjectionStore, 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, agentHomeProjectionCompletionKey, agentHomeProjectionRequestKey, approvalTimelineCursor, authenticateBearer, authenticateDeviceProof, authenticateHostedDeviceAssertion, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, projectTerminalResult, projectTimelineEvents, readAgentHomeProjectionStatus, recordAgentHomeProjectionCompletion, routeKey, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, verifyNonceSignature };
|
|
5154
|
+
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, InMemoryAgentMemoryProjectionAuthorizer, InMemoryAgentMemoryProjectionStore, 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, StaleApprovalError, SteerRejectedError, 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, activitySourceBatchState, agentHomeProjectionCompletionKey, agentHomeProjectionRequestKey, approvalTimelineCursor, assertTaskAttemptListLimit, authenticateBearer, authenticateDeviceProof, authenticateHostedDeviceAssertion, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, pendingApproval, projectTerminalResult, projectTimelineEvents, readAgentHomeProjectionStatus, recordAgentHomeProjectionCompletion, routeKey, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, validateApprovalTimelineResolve, verifyNonceSignature };
|
|
4349
5155
|
//# sourceMappingURL=index.js.map
|
|
4350
5156
|
//# sourceMappingURL=index.js.map
|