@byok-sdk/cloud 0.6.0 → 0.7.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/index.js CHANGED
@@ -1,6 +1,6 @@
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, DAEMON_TO_SERVER_TYPES, encodeEnvelope, decodeEnvelope, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, BYOK_CAPABILITIES_PATH, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, BYOK_BOARD_PATH, BYOK_BOARD_CLAIM_ROUTE, BYOK_BOARD_UNCLAIM_ROUTE, BYOK_BOARD_STATUS_ROUTE, BYOK_BOARD_STREAM_PATH, BYOK_PRESENCE_PATH, BYOK_ACTIVITY_PATH, BYOK_RECORDS_PATH, BYOK_RECORD_ROUTE, BYOK_SKILL_PACKS_PATH, BYOK_SKILL_PACK_FILE_ROUTE, BYOK_BLOBS_PATH, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, createEnvelope, PairRequestSchema, ChallengeRequestSchema, TokenRequestSchema, MessagesSendRequestSchema, PresencePublishRequestSchema, CreateBlobRequestSchema, byokBlobContentPath } from '@byok-sdk/protocol';
3
+ import { AgentEventOrUnknownSchema, PROTOCOL_VERSION, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, encodeEnvelope, decodeEnvelope, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, BYOK_CAPABILITIES_PATH, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, BYOK_BOARD_PATH, BYOK_BOARD_CLAIM_ROUTE, BYOK_BOARD_UNCLAIM_ROUTE, BYOK_BOARD_STATUS_ROUTE, BYOK_BOARD_STREAM_PATH, BYOK_PRESENCE_PATH, BYOK_ACTIVITY_PATH, BYOK_RECORDS_PATH, BYOK_RECORD_ROUTE, BYOK_SKILL_PACKS_PATH, BYOK_SKILL_PACK_FILE_ROUTE, BYOK_BLOBS_PATH, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, createEnvelope, AgentContentReadPayloadSchema, AGENT_EGRESS_POLICY_CAPABILITY, TaskOfferForAgentWithEgressPayloadSchema, TaskOfferForAgentPayloadSchema, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, PairRequestSchema, PairResponseSchema, ChallengeRequestSchema, TokenRequestSchema, MessagesSendRequestSchema, PresencePublishRequestSchema, CreateBlobRequestSchema, byokBlobContentPath, AgentEgressAckPayloadSchema, AgentContentReceiptPayloadSchema } from '@byok-sdk/protocol';
4
4
  import { z } from 'zod';
5
5
  import { Hono } from 'hono';
6
6
 
@@ -160,8 +160,8 @@ function createHmacTokenSigner(secret, clock) {
160
160
  }
161
161
  };
162
162
  }
163
- function verifyNonceSignature(crypto, devicePublicKey, nonce, signature) {
164
- return crypto.verifyEd25519(devicePublicKey, NONCE_SIGNING_DOMAIN + nonce, signature);
163
+ function verifyNonceSignature(crypto2, devicePublicKey, nonce, signature) {
164
+ return crypto2.verifyEd25519(devicePublicKey, NONCE_SIGNING_DOMAIN + nonce, signature);
165
165
  }
166
166
 
167
167
  // src/auth/plane.ts
@@ -170,13 +170,13 @@ var PAIRING_CODE_LENGTH = 8;
170
170
  var DEVICE_IDENTITY_PROOF_KEY_ID = "identity";
171
171
  var DEVICE_IDENTITY_PROOF_KEY_EPOCH = 0;
172
172
  function createAuthPlane(deps) {
173
- const { stores, crypto, clock, tokenSigner } = deps;
173
+ const { stores, crypto: crypto2, clock, tokenSigner } = deps;
174
174
  const ttlSeconds = deps.accessTokenTtlSeconds ?? ACCESS_TOKEN_TTL_SECONDS;
175
175
  return {
176
176
  async createPairingCode(tenant, input) {
177
177
  const expiresAt = new Date(clock.now().getTime() + (input.ttlMs ?? PAIRING_CODE_TTL_MS)).toISOString();
178
178
  return stores.pairingCodes.issue(tenant, {
179
- code: crypto.randomPairingCode(PAIRING_CODE_LENGTH),
179
+ code: crypto2.randomPairingCode(PAIRING_CODE_LENGTH),
180
180
  productId: input.productId,
181
181
  expiresAt
182
182
  });
@@ -186,7 +186,7 @@ function createAuthPlane(deps) {
186
186
  if (claims === void 0) return void 0;
187
187
  return stores.devices.register(claims.tenantId, {
188
188
  productId: claims.productId,
189
- deviceId: `dev_${crypto.randomUuid()}`,
189
+ deviceId: `dev_${crypto2.randomUuid()}`,
190
190
  deviceName: input.deviceName,
191
191
  devicePublicKey: input.devicePublicKey,
192
192
  proofKeyId: DEVICE_IDENTITY_PROOF_KEY_ID,
@@ -208,7 +208,7 @@ function createAuthPlane(deps) {
208
208
  return stores.nonces.markUsed(device.tenantId, nonce);
209
209
  },
210
210
  verifySignature(device, nonce, signature) {
211
- return verifyNonceSignature(crypto, device.devicePublicKey, nonce, signature);
211
+ return verifyNonceSignature(crypto2, device.devicePublicKey, nonce, signature);
212
212
  },
213
213
  async mintAccessToken(device) {
214
214
  const accessToken = await tokenSigner.sign(
@@ -319,7 +319,17 @@ var CLOUD_ERROR_CODES = {
319
319
  /** A progress/activity batch exceeded the configured event or byte ceiling. */
320
320
  activity_batch_too_large: "activity_batch_too_large",
321
321
  /** Host control-plane task lookup is tenant-closed and found no task. */
322
- task_not_found: "task_not_found"
322
+ task_not_found: "task_not_found",
323
+ /** Durable target-device capability is absent, revoked, or unavailable. */
324
+ agent_capability_missing: "agent_capability_missing",
325
+ /** Inbound Agent identity did not exactly match the offered identity. */
326
+ agent_ref_mismatch: "agent_ref_mismatch",
327
+ /** A strict Agent task id already names a durable attempt and cannot be re-enqueued. */
328
+ agent_task_already_exists: "agent_task_already_exists",
329
+ /** A first-write-wins Agent control record was replayed with a different body. */
330
+ agent_content_request_mismatch: "agent_content_request_mismatch",
331
+ /** A durable mailbox receipt id resolved to an envelope other than its exact acknowledgement. */
332
+ mailbox_receipt_mismatch: "mailbox_receipt_mismatch"
323
333
  };
324
334
  var ByokCloudError = class extends Error {
325
335
  code;
@@ -527,10 +537,15 @@ function tenantStoresFor(principal, root) {
527
537
  get: (deviceId) => cloud.devices.get(tenant, deviceId),
528
538
  list: () => cloud.devices.list(tenant),
529
539
  revoke: (deviceId) => cloud.devices.revoke(tenant, deviceId),
540
+ recordCapabilities: (input) => principal.kind === "device" ? cloud.devices.recordCapabilities(tenant, {
541
+ deviceId: principal.deviceId,
542
+ capabilities: input.capabilities
543
+ }) : Promise.resolve(void 0),
530
544
  readiness: () => cloud.devices.readiness(tenant, core.presence)
531
545
  },
532
546
  tasks: {
533
547
  open: (input) => cloud.tasks.open(tenant, input),
548
+ reserveAgentOffer: (input) => cloud.tasks.reserveAgentOffer(tenant, input),
534
549
  get: (taskId) => cloud.tasks.get(tenant, taskId),
535
550
  getMany: (taskIds) => cloud.tasks.getMany(tenant, taskIds),
536
551
  claim: (input) => cloud.tasks.claim(tenant, input),
@@ -546,6 +561,10 @@ function tenantStoresFor(principal, root) {
546
561
  record: (input) => cloud.receipts.record(tenant, input),
547
562
  get: (key) => cloud.receipts.get(tenant, key)
548
563
  },
564
+ egress: {
565
+ record: (input) => cloud.egress.record(tenant, input),
566
+ get: (deviceId, eventId) => cloud.egress.get(tenant, deviceId, eventId)
567
+ },
549
568
  blobs: {
550
569
  createUpload: (reservation) => cloud.blobs.createUpload(tenant, reservation),
551
570
  observeUpload: (blobId, reservation) => cloud.blobs.observeUpload(tenant, blobId, reservation),
@@ -685,11 +704,16 @@ function blobDownloadContentHandler(deps) {
685
704
  if (!query.success || !await deps.contentProxy.verifySignedUrl(blobId, "get", query.data.sig, query.data.exp)) {
686
705
  return c.json({ error: "invalid or expired signature" }, 401);
687
706
  }
688
- const content = await deps.contentProxy.readContent(blobId);
689
- if (content === void 0) return c.json({ error: "blob not found" }, 404);
690
- return c.body(new Uint8Array(content.data), 200, { "content-type": content.contentType });
707
+ const result = await deps.contentProxy.readContent(blobId);
708
+ if (result === void 0) return c.json({ error: "blob not found" }, 404);
709
+ if (!result.ok) return c.json({ error: result.code }, BLOB_READ_ERROR_HTTP_STATUS[result.code]);
710
+ return c.body(new Uint8Array(result.content.data), 200, { "content-type": result.content.contentType });
691
711
  };
692
712
  }
713
+ var BLOB_READ_ERROR_HTTP_STATUS = {
714
+ blob_upstream_unavailable: 502,
715
+ blob_upstream_stream_interrupted: 502
716
+ };
693
717
  function idempotencyKey(c) {
694
718
  const value = c.req.header("Idempotency-Key");
695
719
  if (value === void 0 || value.length === 0 || value.length > 200) return void 0;
@@ -726,7 +750,12 @@ function pairHandler(deps) {
726
750
  const device = await deps.auth.redeemAndRegister(parsed.data);
727
751
  if (device === void 0) return c.json({ error: "invalid pairing code" }, 401);
728
752
  const { accessToken, expiresAt } = await deps.auth.mintAccessToken(device);
729
- const response = { deviceId: device.deviceId, accessToken, refreshHint: expiresAt };
753
+ const response = PairResponseSchema.parse({
754
+ deviceId: device.deviceId,
755
+ accessToken,
756
+ refreshHint: expiresAt,
757
+ tenantId: device.tenantId
758
+ });
730
759
  return c.json(response, 200);
731
760
  };
732
761
  }
@@ -759,7 +788,15 @@ function tokenHandler(deps) {
759
788
  return c.json(response, 200);
760
789
  };
761
790
  }
762
- var CLOUD_PROTOCOL_CAPABILITIES = ["result-document"];
791
+ var CLOUD_PROTOCOL_CAPABILITIES = [
792
+ "result-document",
793
+ "agent-home-contract",
794
+ "agent-egress-policy",
795
+ "agent-egress-reliable-ack",
796
+ "agent-content-workspace-read",
797
+ "agent-content-transcript-read",
798
+ "agent-content-artifact-read"
799
+ ];
763
800
  function sleep(ms) {
764
801
  return new Promise((resolve) => setTimeout(resolve, ms));
765
802
  }
@@ -791,13 +828,15 @@ function eventsHandler(deps) {
791
828
  if (page.messages.length === 0) break;
792
829
  const decoded = page.messages.map((message) => decodeEnvelope(message.body));
793
830
  const offeredTaskIds = decoded.flatMap(
794
- (event) => (event.type === "task.offer" || event.type === "task.offer_with_toolsets") && event.task_id !== void 0 ? [event.task_id] : []
831
+ (event) => (event.type === "task.offer" || event.type === "task.offer_with_toolsets" || event.type === "task.offer_for_agent" || event.type === "task.offer_for_agent_with_egress") && event.task_id !== void 0 ? [event.task_id] : []
795
832
  );
796
833
  const attemptsByTaskId = Object.fromEntries(
797
834
  (await stores.tasks.getMany(offeredTaskIds)).map((attempt2) => [attempt2.taskId, attempt2])
798
835
  );
799
836
  const events = decoded.filter((event) => {
800
- if (event.type !== "task.offer" && event.type !== "task.offer_with_toolsets") return true;
837
+ if (event.type !== "task.offer" && event.type !== "task.offer_with_toolsets" && event.type !== "task.offer_for_agent" && event.type !== "task.offer_for_agent_with_egress") {
838
+ return true;
839
+ }
801
840
  return event.task_id === void 0 || attemptsByTaskId[event.task_id]?.cancellation === void 0;
802
841
  });
803
842
  if (events.length === 0 && page.hasMore) {
@@ -910,13 +949,103 @@ async function projectTerminalToReview(board, taskId) {
910
949
  function terminalReceiptKey(taskId) {
911
950
  return `task:${taskId}:terminal`;
912
951
  }
952
+ function sameAgentRef(expected, actual) {
953
+ return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
954
+ }
955
+ function sameReliableEgress(expected, actual) {
956
+ return sameAgentRef(expected.agentRef, actual.agentRef) && expected.sessionRef === actual.sessionRef && expected.policyRevision === actual.policyRevision && expected.eventId === actual.eventId && expected.cursor === actual.cursor && expected.contentHash === actual.contentHash && expected.byteCount === actual.byteCount && JSON.stringify(expected.payload) === JSON.stringify(actual.payload);
957
+ }
958
+ function contentReadCapability(surface) {
959
+ switch (surface) {
960
+ case "workspace":
961
+ return AGENT_CONTENT_WORKSPACE_READ_CAPABILITY;
962
+ case "transcript":
963
+ return AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY;
964
+ case "artifact":
965
+ return AGENT_CONTENT_ARTIFACT_READ_CAPABILITY;
966
+ }
967
+ }
968
+ function hasCapabilities(capabilities, required) {
969
+ return capabilities !== void 0 && required.every((capability) => capabilities.includes(capability));
970
+ }
971
+ function contentRequestReceiptKey(deviceId, requestId) {
972
+ return `agent-content-request:${deviceId}:${requestId}`;
973
+ }
974
+ function contentReceiptKey(deviceId, requestId) {
975
+ return `agent-content:${deviceId}:${requestId}`;
976
+ }
977
+ function matchesContentReadReceipt(request, receipt) {
978
+ 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;
979
+ }
980
+ async function readContentRequest(stores, deviceId, requestId) {
981
+ const stored = await stores.receipts.get(contentRequestReceiptKey(deviceId, requestId));
982
+ if (stored === void 0) return void 0;
983
+ try {
984
+ return AgentContentReadPayloadSchema.parse(JSON.parse(stored.body));
985
+ } catch {
986
+ return void 0;
987
+ }
988
+ }
989
+ function inboundAgentRef(envelope) {
990
+ switch (envelope.type) {
991
+ case "task.claim":
992
+ case "task.decline":
993
+ case "task.complete":
994
+ case "task.fail":
995
+ case "task.cancelled":
996
+ return envelope.payload.agentRef;
997
+ default:
998
+ return void 0;
999
+ }
1000
+ }
1001
+ function agentRefEchoRequired(envelope) {
1002
+ return envelope.type === "task.claim" || envelope.type === "task.decline" || envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled";
1003
+ }
913
1004
  async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS) {
914
1005
  if (!await stores.rateLimiter.consume(deviceId)) return "rate_limited";
1006
+ if (envelope.type === "conn.hello") {
1007
+ if (envelope.payload.deviceId !== deviceId) return "rejected";
1008
+ if (!envelope.payload.protocolVersions.includes(PROTOCOL_VERSION)) return "rejected";
1009
+ const device = await stores.devices.get(deviceId);
1010
+ if (device === void 0 || device.revoked || device.productId !== envelope.payload.productId) {
1011
+ return "rejected";
1012
+ }
1013
+ if (await stores.dedup.checkAndRecord(deviceId, envelope.id)) return "duplicate";
1014
+ return await stores.devices.recordCapabilities({ capabilities: envelope.payload.capabilities }) === void 0 ? "rejected" : "accepted";
1015
+ }
915
1016
  if (!DAEMON_TO_SERVER_TYPES.includes(envelope.type)) return "rejected";
1017
+ if (envelope.type === "agent.egress.reliable") {
1018
+ const device = await stores.devices.get(deviceId);
1019
+ if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
1020
+ return "rejected";
1021
+ }
1022
+ const outcome = await stores.egress.record({
1023
+ deviceId,
1024
+ payload: envelope.payload,
1025
+ receiptId: crypto.randomUUID()
1026
+ });
1027
+ return outcome.created || sameReliableEgress(outcome.record.payload, envelope.payload) ? outcome.created ? "accepted" : "duplicate" : "rejected";
1028
+ }
1029
+ if (envelope.type === "agent.content.receipt") {
1030
+ const device = await stores.devices.get(deviceId);
1031
+ if (device === void 0 || device.revoked || envelope.payload.eventId !== envelope.payload.requestId || !hasCapabilities(device.capabilities, [contentReadCapability(envelope.payload.surface), AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
1032
+ return "rejected";
1033
+ }
1034
+ const request = await readContentRequest(stores, deviceId, envelope.payload.requestId);
1035
+ if (request === void 0 || !matchesContentReadReceipt(request, envelope.payload)) return "rejected";
1036
+ const key = contentReceiptKey(deviceId, envelope.payload.requestId);
1037
+ const body = JSON.stringify(envelope.payload);
1038
+ const receipt = await stores.receipts.record({ key, body });
1039
+ return receipt.created ? "accepted" : receipt.receipt.body === body ? "duplicate" : "rejected";
1040
+ }
916
1041
  const taskId = envelope.task_id;
917
1042
  if (taskId === void 0) return "rejected";
918
1043
  const attempt = await stores.tasks.get(taskId);
1044
+ if (attempt?.agentRef !== void 0 && attempt.deviceId !== deviceId) return "rejected";
919
1045
  if (attempt?.ownerDeviceId !== void 0 && attempt.ownerDeviceId !== deviceId) return "rejected";
1046
+ if (attempt?.agentRef !== void 0 && agentRefEchoRequired(envelope) && !sameAgentRef(attempt.agentRef, inboundAgentRef(envelope))) {
1047
+ return "rejected";
1048
+ }
920
1049
  if (envelope.type === "task.progress" && envelope.payload.events.length > 0) {
921
1050
  try {
922
1051
  validateActivityBatch(
@@ -946,19 +1075,27 @@ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds
946
1075
  }
947
1076
  }
948
1077
  if (await stores.dedup.checkAndRecord(deviceId, envelope.id)) return "duplicate";
949
- await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds);
1078
+ await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, attempt?.agentRef);
950
1079
  return "accepted";
951
1080
  }
952
- async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds) {
1081
+ async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, persistedAgentRef) {
953
1082
  switch (envelope.type) {
954
1083
  case "task.claim":
955
1084
  await stores.tasks.claim({ taskId, deviceId });
956
1085
  return;
957
1086
  case "task.started":
958
- await stores.tasks.recordStatus({ taskId, status: "running" });
1087
+ await stores.tasks.recordStatus({
1088
+ taskId,
1089
+ status: "running",
1090
+ ...persistedAgentRef === void 0 ? {} : { agentRef: persistedAgentRef }
1091
+ });
959
1092
  return;
960
1093
  case "task.decline":
961
- await stores.tasks.recordStatus({ taskId, status: "failed" });
1094
+ await stores.tasks.recordStatus({
1095
+ taskId,
1096
+ status: "failed",
1097
+ ...persistedAgentRef === void 0 ? {} : { agentRef: persistedAgentRef }
1098
+ });
962
1099
  return;
963
1100
  case "task.progress":
964
1101
  if (envelope.payload.events.length > 0) {
@@ -1030,12 +1167,22 @@ async function recordTerminal(stores, taskId, envelope, status) {
1030
1167
  });
1031
1168
  if (attempt?.cancellation !== void 0) {
1032
1169
  if (status === "cancelled") {
1033
- await stores.tasks.recordStatus({ taskId, status: "cancelled" });
1170
+ await stores.tasks.recordStatus({
1171
+ taskId,
1172
+ status: "cancelled",
1173
+ ...attempt.agentRef === void 0 ? {} : { agentRef: attempt.agentRef }
1174
+ });
1034
1175
  }
1035
1176
  return;
1036
1177
  }
1037
1178
  if (!created) return;
1038
- const recordedAttempt = await stores.tasks.recordStatus({ taskId, status });
1179
+ const cause = envelope.type === "task.fail" || envelope.type === "task.cancelled" ? envelope.payload.reason : void 0;
1180
+ const recordedAttempt = await stores.tasks.recordStatus({
1181
+ taskId,
1182
+ status,
1183
+ ...attempt?.agentRef === void 0 ? {} : { agentRef: attempt.agentRef },
1184
+ ...cause === void 0 ? {} : { terminalCause: cause }
1185
+ });
1039
1186
  if (recordedAttempt?.status !== status || recordedAttempt.cancellation !== void 0) return;
1040
1187
  await projectTerminalToReview(stores.board, taskId);
1041
1188
  }
@@ -1058,6 +1205,16 @@ function messagesHandler(deps) {
1058
1205
  deps.activityBounds
1059
1206
  );
1060
1207
  if (outcome === "rate_limited") return c.json({ error: "rate limit exceeded" }, 429);
1208
+ if (envelope.type === "agent.egress.reliable" && (outcome === "accepted" || outcome === "duplicate")) {
1209
+ const record = await stores.egress.get(device.deviceId, envelope.payload.eventId);
1210
+ if (record === void 0) {
1211
+ throw new Error(`Accepted reliable egress ${envelope.payload.eventId} has no durable receipt record.`);
1212
+ }
1213
+ await deps.appendReliableEgressAck(stores, record);
1214
+ }
1215
+ if (envelope.type === "agent.content.receipt" && (outcome === "accepted" || outcome === "duplicate")) {
1216
+ await deps.appendContentReceiptAck(stores, device.deviceId, envelope.payload);
1217
+ }
1061
1218
  if (outcome === "rejected") rejected += 1;
1062
1219
  else accepted += 1;
1063
1220
  }
@@ -1825,6 +1982,7 @@ function projectTerminalResult(taskId, receipt) {
1825
1982
  return {
1826
1983
  taskId,
1827
1984
  state: "complete",
1985
+ ...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
1828
1986
  summary: envelope.payload.summary,
1829
1987
  sessionRef: envelope.payload.sessionRef,
1830
1988
  ...envelope.payload.artifactRefs !== void 0 ? { artifactRefs: envelope.payload.artifactRefs } : {},
@@ -1836,7 +1994,9 @@ function projectTerminalResult(taskId, receipt) {
1836
1994
  return {
1837
1995
  taskId,
1838
1996
  state: "failed",
1997
+ ...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
1839
1998
  reason: envelope.payload.reason,
1999
+ terminalCause: envelope.payload.reason,
1840
2000
  ...envelope.payload.retryable !== void 0 ? { retryable: envelope.payload.retryable } : {},
1841
2001
  ...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
1842
2002
  recordedAt: receipt.recordedAt
@@ -1845,7 +2005,9 @@ function projectTerminalResult(taskId, receipt) {
1845
2005
  return {
1846
2006
  taskId,
1847
2007
  state: "cancelled",
2008
+ ...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
1848
2009
  ...envelope.payload.reason !== void 0 ? { reason: envelope.payload.reason } : {},
2010
+ ...envelope.payload.reason === void 0 ? {} : { terminalCause: envelope.payload.reason },
1849
2011
  ...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
1850
2012
  recordedAt: receipt.recordedAt
1851
2013
  };
@@ -1862,6 +2024,10 @@ var DEFAULT_MAX_BLOB_SIZE_BYTES = 100 * 1024 * 1024;
1862
2024
  var DEFAULT_LONG_POLL_HOLD_MS = 5e4;
1863
2025
  var DEFAULT_LONG_POLL_INTERVAL_MS = 250;
1864
2026
  var DEFAULT_EVENTS_PAGE_LIMIT = 50;
2027
+ var AGENT_HOME_CONTRACT_CAPABILITY = "agent-home-contract";
2028
+ function sameAgentRef2(expected, actual) {
2029
+ return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
2030
+ }
1865
2031
  function createByokCloud(options) {
1866
2032
  const declaration = parseDeclaration(options.capabilities);
1867
2033
  assertNoOverDeclaration(
@@ -1911,7 +2077,12 @@ function createByokCloud(options) {
1911
2077
  if (declares(declaration, CLOUD_CAPABILITIES.messagesBatch)) {
1912
2078
  registry.register(
1913
2079
  { method: "POST", path: BYOK_MESSAGES_PATH, class: "device" },
1914
- messagesHandler({ ...deviceRouteDeps, activityBounds })
2080
+ messagesHandler({
2081
+ ...deviceRouteDeps,
2082
+ activityBounds,
2083
+ appendReliableEgressAck: enqueueReliableEgressAck,
2084
+ appendContentReceiptAck: enqueueContentReceiptAck
2085
+ })
1915
2086
  );
1916
2087
  }
1917
2088
  if (declares(declaration, CLOUD_CAPABILITIES.boardCoordination)) {
@@ -2029,16 +2200,98 @@ function createByokCloud(options) {
2029
2200
  function controlPlane(tenant) {
2030
2201
  return { kind: "control-plane", tenantId: tenant, operatorId };
2031
2202
  }
2032
- async function enqueueTaskEnvelope(tenant, deviceId, requestedTaskId, buildEnvelope) {
2203
+ async function enqueueTaskEnvelope(tenant, deviceId, requestedTaskId, agentRef, buildEnvelope) {
2033
2204
  const stores = tenantStoresFor(controlPlane(tenant), root);
2034
2205
  const taskId = requestedTaskId ?? `task_${options.crypto.randomUuid()}`;
2035
2206
  const messageId = options.crypto.randomUuid();
2207
+ const reservation = agentRef === void 0 ? void 0 : await stores.tasks.reserveAgentOffer({ taskId, deviceId, agentRef });
2208
+ const openedBeforeAppend = reservation?.attempt;
2209
+ if (agentRef !== void 0 && (openedBeforeAppend === void 0 || openedBeforeAppend.deviceId !== deviceId || openedBeforeAppend.agentRef === void 0 || !sameAgentRef2(agentRef, openedBeforeAppend.agentRef))) {
2210
+ throw new ByokCloudError(
2211
+ "agent_ref_mismatch",
2212
+ `Task ${taskId} already has a different durable Agent identity or target device.`
2213
+ );
2214
+ }
2215
+ if (reservation !== void 0 && !reservation.created) {
2216
+ throw new ByokCloudError(
2217
+ "agent_task_already_exists",
2218
+ `Task ${taskId} already has a durable Agent attempt and cannot be enqueued again.`
2219
+ );
2220
+ }
2221
+ let message;
2222
+ try {
2223
+ message = await stores.mailbox.append({
2224
+ deviceId,
2225
+ messageId,
2226
+ materialize: async (seq) => {
2227
+ const envelope2 = buildEnvelope(taskId, seq, messageId);
2228
+ const body = encodeEnvelope(envelope2);
2229
+ const bytes = new TextEncoder().encode(body);
2230
+ return {
2231
+ body,
2232
+ bodyHash: contentHash(await options.crypto.sha256(bytes)),
2233
+ byteSize: BigInt(bytes.length)
2234
+ };
2235
+ }
2236
+ });
2237
+ } catch (error) {
2238
+ if (openedBeforeAppend !== void 0) {
2239
+ try {
2240
+ await stores.tasks.recordStatus({
2241
+ taskId,
2242
+ status: "failed",
2243
+ agentRef,
2244
+ terminalCause: "mailbox append failed before Agent offer delivery"
2245
+ });
2246
+ } catch (cleanupError) {
2247
+ throw new AggregateError(
2248
+ [error, cleanupError],
2249
+ `Agent task ${taskId} could not be delivered or durably closed`
2250
+ );
2251
+ }
2252
+ }
2253
+ throw error;
2254
+ }
2255
+ const envelope = decodeEnvelope(message.body);
2256
+ if (envelope.seq !== message.seq) {
2257
+ throw new ByokCloudError(
2258
+ "mailbox_seq_mismatch",
2259
+ `Mailbox stored offer seq ${String(envelope.seq)} at row ${message.seq}; the daemon's redelivery cursor would be wrong.`
2260
+ );
2261
+ }
2262
+ const attempt = openedBeforeAppend ?? await stores.tasks.open({
2263
+ taskId,
2264
+ deviceId
2265
+ });
2266
+ return { taskId, seq: message.seq, envelope, attempt };
2267
+ }
2268
+ async function assertAgentCapabilities(tenant, deviceId, required) {
2269
+ const device = await options.cloud.devices.get(tenant, deviceId);
2270
+ if (device === void 0 || device.revoked || !device.capabilities || !required.every((capability) => device.capabilities.includes(capability))) {
2271
+ const missing = required.filter((capability) => !device?.capabilities?.includes(capability));
2272
+ throw new ByokCloudError(
2273
+ "agent_capability_missing",
2274
+ `Device ${deviceId} lacks durable Agent capabilities (${missing.join(", ")}); refusing before mailbox append.`
2275
+ );
2276
+ }
2277
+ }
2278
+ function contentReadCapability2(surface) {
2279
+ switch (surface) {
2280
+ case "workspace":
2281
+ return AGENT_CONTENT_WORKSPACE_READ_CAPABILITY;
2282
+ case "transcript":
2283
+ return AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY;
2284
+ case "artifact":
2285
+ return AGENT_CONTENT_ARTIFACT_READ_CAPABILITY;
2286
+ }
2287
+ }
2288
+ async function enqueueAgentControlEnvelope(tenant, deviceId, messageId, buildEnvelope) {
2289
+ const stores = tenantStoresFor(controlPlane(tenant), root);
2036
2290
  const message = await stores.mailbox.append({
2037
2291
  deviceId,
2038
2292
  messageId,
2039
2293
  materialize: async (seq) => {
2040
- const envelope2 = buildEnvelope(taskId, seq, messageId);
2041
- const body = encodeEnvelope(envelope2);
2294
+ const body = encodeEnvelope(buildEnvelope(seq));
2042
2295
  const bytes = new TextEncoder().encode(body);
2043
2296
  return {
2044
2297
  body,
@@ -2051,11 +2304,77 @@ function createByokCloud(options) {
2051
2304
  if (envelope.seq !== message.seq) {
2052
2305
  throw new ByokCloudError(
2053
2306
  "mailbox_seq_mismatch",
2054
- `Mailbox stored offer seq ${String(envelope.seq)} at row ${message.seq}; the daemon's redelivery cursor would be wrong.`
2307
+ `Mailbox stored Agent control seq ${String(envelope.seq)} at row ${message.seq}.`
2308
+ );
2309
+ }
2310
+ return { seq: message.seq, envelope };
2311
+ }
2312
+ async function enqueueReliableEgressAck(stores, record) {
2313
+ const payload = AgentEgressAckPayloadSchema.parse({
2314
+ agentRef: record.payload.agentRef,
2315
+ sessionRef: record.payload.sessionRef,
2316
+ policyRevision: record.payload.policyRevision,
2317
+ eventId: record.payload.eventId,
2318
+ cursor: record.payload.cursor,
2319
+ receiptId: record.receiptId
2320
+ });
2321
+ const message = await stores.mailbox.append({
2322
+ deviceId: record.deviceId,
2323
+ messageId: record.receiptId,
2324
+ materialize: async (seq) => {
2325
+ const body = encodeEnvelope(createEnvelope("agent.egress.ack", payload, { id: record.receiptId, seq }));
2326
+ const bytes = new TextEncoder().encode(body);
2327
+ return {
2328
+ body,
2329
+ bodyHash: contentHash(await options.crypto.sha256(bytes)),
2330
+ byteSize: BigInt(bytes.length)
2331
+ };
2332
+ }
2333
+ });
2334
+ const decoded = decodeEnvelope(message.body);
2335
+ if (decoded.type !== "agent.egress.ack" || decoded.seq !== message.seq || JSON.stringify(decoded.payload) !== JSON.stringify(payload)) {
2336
+ throw new ByokCloudError(
2337
+ "mailbox_receipt_mismatch",
2338
+ `Mailbox receipt ${record.receiptId} does not exactly acknowledge Agent egress ${record.payload.eventId}.`
2339
+ );
2340
+ }
2341
+ }
2342
+ async function enqueueContentReceiptAck(stores, deviceId, receipt) {
2343
+ const exactReceipt = AgentContentReceiptPayloadSchema.parse(receipt);
2344
+ if (exactReceipt.eventId !== exactReceipt.requestId) {
2345
+ throw new ByokCloudError("mailbox_receipt_mismatch", "Content receipt eventId must equal requestId.");
2346
+ }
2347
+ const payload = AgentEgressAckPayloadSchema.parse({
2348
+ agentRef: exactReceipt.agentRef,
2349
+ sessionRef: exactReceipt.sessionRef,
2350
+ policyRevision: exactReceipt.policyRevision,
2351
+ eventId: exactReceipt.eventId,
2352
+ cursor: exactReceipt.cursor,
2353
+ receiptId: exactReceipt.requestId
2354
+ });
2355
+ const message = await stores.mailbox.append({
2356
+ deviceId,
2357
+ // The request itself already occupies its bare request id in this
2358
+ // mailbox. A namespaced producer key keeps the ack idempotent without
2359
+ // returning that control envelope on duplicate receipt delivery.
2360
+ messageId: `agent-content-ack:${exactReceipt.requestId}`,
2361
+ materialize: async (seq) => {
2362
+ const body = encodeEnvelope(createEnvelope("agent.egress.ack", payload, { id: exactReceipt.requestId, seq }));
2363
+ const bytes = new TextEncoder().encode(body);
2364
+ return {
2365
+ body,
2366
+ bodyHash: contentHash(await options.crypto.sha256(bytes)),
2367
+ byteSize: BigInt(bytes.length)
2368
+ };
2369
+ }
2370
+ });
2371
+ const decoded = decodeEnvelope(message.body);
2372
+ if (decoded.type !== "agent.egress.ack" || decoded.seq !== message.seq || JSON.stringify(decoded.payload) !== JSON.stringify(payload)) {
2373
+ throw new ByokCloudError(
2374
+ "mailbox_receipt_mismatch",
2375
+ `Mailbox content receipt ${exactReceipt.requestId} does not exactly acknowledge its durable content fact.`
2055
2376
  );
2056
2377
  }
2057
- const attempt = await stores.tasks.open({ taskId, deviceId });
2058
- return { taskId, seq: message.seq, envelope, attempt };
2059
2378
  }
2060
2379
  return {
2061
2380
  fetch: registry.fetch,
@@ -2070,6 +2389,7 @@ function createByokCloud(options) {
2070
2389
  tenant,
2071
2390
  deviceId,
2072
2391
  input.taskId,
2392
+ void 0,
2073
2393
  (taskId, seq, messageId) => createEnvelope("task.offer", input.payload, { id: messageId, taskId, seq })
2074
2394
  );
2075
2395
  },
@@ -2078,9 +2398,69 @@ function createByokCloud(options) {
2078
2398
  tenant,
2079
2399
  deviceId,
2080
2400
  input.taskId,
2401
+ void 0,
2081
2402
  (taskId, seq, messageId) => createEnvelope("task.offer_with_toolsets", input.payload, { id: messageId, taskId, seq })
2082
2403
  );
2083
2404
  },
2405
+ async enqueueAgentOffer(tenant, deviceId, input) {
2406
+ await assertAgentCapabilities(tenant, deviceId, [AGENT_HOME_CONTRACT_CAPABILITY]);
2407
+ const payload = TaskOfferForAgentPayloadSchema.parse(input.payload);
2408
+ return enqueueTaskEnvelope(
2409
+ tenant,
2410
+ deviceId,
2411
+ input.taskId,
2412
+ payload.agentRef,
2413
+ (taskId, seq, messageId) => createEnvelope("task.offer_for_agent", payload, { id: messageId, taskId, seq })
2414
+ );
2415
+ },
2416
+ async enqueueAgentEgressOffer(tenant, deviceId, input) {
2417
+ await assertAgentCapabilities(tenant, deviceId, [
2418
+ AGENT_HOME_CONTRACT_CAPABILITY,
2419
+ AGENT_EGRESS_POLICY_CAPABILITY,
2420
+ AGENT_EGRESS_RELIABLE_ACK_CAPABILITY
2421
+ ]);
2422
+ const payload = TaskOfferForAgentWithEgressPayloadSchema.parse(input.payload);
2423
+ return enqueueTaskEnvelope(
2424
+ tenant,
2425
+ deviceId,
2426
+ input.taskId,
2427
+ payload.agentRef,
2428
+ (taskId, seq, messageId) => createEnvelope("task.offer_for_agent_with_egress", payload, { id: messageId, taskId, seq })
2429
+ );
2430
+ },
2431
+ async enqueueAgentContentRead(tenant, deviceId, input) {
2432
+ const payload = AgentContentReadPayloadSchema.parse(input.payload);
2433
+ await assertAgentCapabilities(tenant, deviceId, [
2434
+ AGENT_HOME_CONTRACT_CAPABILITY,
2435
+ AGENT_EGRESS_POLICY_CAPABILITY,
2436
+ AGENT_EGRESS_RELIABLE_ACK_CAPABILITY,
2437
+ contentReadCapability2(payload.surface)
2438
+ ]);
2439
+ const stores = tenantStoresFor(controlPlane(tenant), root);
2440
+ const persisted = await stores.receipts.record({
2441
+ key: `agent-content-request:${deviceId}:${payload.requestId}`,
2442
+ body: JSON.stringify(payload)
2443
+ });
2444
+ if (!persisted.created && persisted.receipt.body !== JSON.stringify(payload)) {
2445
+ throw new ByokCloudError(
2446
+ "agent_content_request_mismatch",
2447
+ `Request ${payload.requestId} already exists with a different immutable content-read body.`
2448
+ );
2449
+ }
2450
+ const control = await enqueueAgentControlEnvelope(
2451
+ tenant,
2452
+ deviceId,
2453
+ payload.requestId,
2454
+ (seq) => createEnvelope("agent.content.read", payload, { id: payload.requestId, seq })
2455
+ );
2456
+ if (control.envelope.type !== "agent.content.read" || JSON.stringify(control.envelope.payload) !== JSON.stringify(payload)) {
2457
+ throw new ByokCloudError(
2458
+ "agent_content_request_mismatch",
2459
+ `Request ${payload.requestId} already exists with a different immutable content-read body.`
2460
+ );
2461
+ }
2462
+ return control;
2463
+ },
2084
2464
  async cancelTask(tenant, taskId, reason) {
2085
2465
  const stores = tenantStoresFor(controlPlane(tenant), root);
2086
2466
  const proposedMessageId = options.crypto.randomUuid();
@@ -2123,6 +2503,9 @@ function createByokCloud(options) {
2123
2503
  readTerminalReceipt(tenant, taskId) {
2124
2504
  return tenantStoresFor(controlPlane(tenant), root).receipts.get(terminalReceiptKey(taskId));
2125
2505
  },
2506
+ readAgentEgress(tenant, deviceId, eventId) {
2507
+ return tenantStoresFor(controlPlane(tenant), root).egress.get(deviceId, eventId);
2508
+ },
2126
2509
  async readTaskResult(tenant, taskId) {
2127
2510
  const stores = tenantStoresFor(controlPlane(tenant), root);
2128
2511
  const attempt = await stores.tasks.get(taskId);
@@ -2130,7 +2513,9 @@ function createByokCloud(options) {
2130
2513
  return {
2131
2514
  taskId,
2132
2515
  state: "cancelled",
2516
+ ...attempt.agentRef === void 0 ? {} : { agentRef: attempt.agentRef },
2133
2517
  ...attempt.cancellation.reason === void 0 ? {} : { reason: attempt.cancellation.reason },
2518
+ ...attempt.cancellation.reason === void 0 ? {} : { terminalCause: attempt.cancellation.reason },
2134
2519
  recordedAt: attempt.cancellation.requestedAt
2135
2520
  };
2136
2521
  }
@@ -2235,9 +2620,9 @@ var InMemoryBlobRegistry = class {
2235
2620
  crypto;
2236
2621
  secret;
2237
2622
  urlTtlMs;
2238
- constructor(clock, crypto, options) {
2623
+ constructor(clock, crypto2, options) {
2239
2624
  this.clock = clock;
2240
- this.crypto = crypto;
2625
+ this.crypto = crypto2;
2241
2626
  this.secret = globalThis.crypto.getRandomValues(new Uint8Array(SIGNING_SECRET_BYTES));
2242
2627
  this.urlTtlMs = options.urlTtlMs ?? BLOB_URL_TTL_MS;
2243
2628
  }
@@ -2342,14 +2727,20 @@ var InMemoryBlobContentProxy = class {
2342
2727
  record.uploaded = true;
2343
2728
  return { ok: true };
2344
2729
  }
2730
+ /**
2731
+ * Never returns `{ok:false}`: this composition holds the bytes in the same
2732
+ * process, so there is no upstream to be unreachable and no stream to be
2733
+ * interrupted. Both `BlobReadErrorCode`s are structurally unreachable here
2734
+ * — a proxy that fetches from object storage is where they become live.
2735
+ */
2345
2736
  async readContent(blobId) {
2346
2737
  const record = this.#registry.blobs.get(blobId);
2347
2738
  if (record === void 0 || !record.uploaded || record.data === void 0) return void 0;
2348
- return { data: record.data, contentType: record.contentType };
2739
+ return { ok: true, content: { data: record.data, contentType: record.contentType } };
2349
2740
  }
2350
2741
  };
2351
- function createInMemoryBlobs(clock, crypto, objects, options = {}) {
2352
- const registry = new InMemoryBlobRegistry(clock, crypto, options);
2742
+ function createInMemoryBlobs(clock, crypto2, objects, options = {}) {
2743
+ const registry = new InMemoryBlobRegistry(clock, crypto2, options);
2353
2744
  return {
2354
2745
  blobs: new InMemoryCloudBlobStore(registry, objects),
2355
2746
  contentProxy: new InMemoryBlobContentProxy(registry)
@@ -2392,6 +2783,17 @@ var InMemoryDeviceDirectory = class {
2392
2783
  if (record === void 0) return;
2393
2784
  this.#byTenant.set(key, { ...record, revoked: true });
2394
2785
  }
2786
+ async recordCapabilities(tenant, input) {
2787
+ const key = tenantKey(tenant, input.deviceId);
2788
+ const record = this.#byTenant.get(key);
2789
+ if (record === void 0 || record.revoked) return void 0;
2790
+ const updated = {
2791
+ ...record,
2792
+ capabilities: Object.freeze([...input.capabilities])
2793
+ };
2794
+ this.#byTenant.set(key, updated);
2795
+ return updated;
2796
+ }
2395
2797
  async list(tenant) {
2396
2798
  const prefix = tenantKey(tenant, "");
2397
2799
  return [...this.#byTenant.entries()].filter(([key]) => key.startsWith(prefix)).map(([, record]) => record);
@@ -2477,9 +2879,9 @@ var InMemoryNonceStore = class {
2477
2879
  #clock;
2478
2880
  #crypto;
2479
2881
  #ttlMs;
2480
- constructor(clock, crypto, ttlMs = NONCE_TTL_MS) {
2882
+ constructor(clock, crypto2, ttlMs = NONCE_TTL_MS) {
2481
2883
  this.#clock = clock;
2482
- this.#crypto = crypto;
2884
+ this.#crypto = crypto2;
2483
2885
  this.#ttlMs = ttlMs;
2484
2886
  }
2485
2887
  /** Number of records currently held (post-sweep). Test-facing only. */
@@ -2583,6 +2985,9 @@ var InMemoryProofRequestReceiptStore = class {
2583
2985
  return this.#receipts.get(tenantKey(tenant, deviceId, requestId));
2584
2986
  }
2585
2987
  };
2988
+ function sameAgentRef3(left, right) {
2989
+ return left?.agentId === right?.agentId && left?.profileRevision === right?.profileRevision;
2990
+ }
2586
2991
  var InMemoryTaskAttemptStore = class {
2587
2992
  #state;
2588
2993
  constructor(clock, state = new InMemoryTaskAttemptState(clock)) {
@@ -2597,6 +3002,7 @@ var InMemoryTaskAttemptStore = class {
2597
3002
  tenantId: tenant,
2598
3003
  taskId: input.taskId,
2599
3004
  deviceId: input.deviceId,
3005
+ ...input.agentRef === void 0 ? {} : { agentRef: { ...input.agentRef } },
2600
3006
  status: "offered",
2601
3007
  updatedAt: this.#now()
2602
3008
  };
@@ -2604,6 +3010,23 @@ var InMemoryTaskAttemptStore = class {
2604
3010
  return attempt;
2605
3011
  });
2606
3012
  }
3013
+ async reserveAgentOffer(tenant, input) {
3014
+ const key = tenantKey(tenant, input.taskId);
3015
+ return this.#state.mutate(key, () => {
3016
+ const existing = this.#state.attempts.get(key);
3017
+ if (existing !== void 0) return { attempt: existing, created: false };
3018
+ const attempt = {
3019
+ tenantId: tenant,
3020
+ taskId: input.taskId,
3021
+ deviceId: input.deviceId,
3022
+ agentRef: { ...input.agentRef },
3023
+ status: "offered",
3024
+ updatedAt: this.#now()
3025
+ };
3026
+ this.#state.attempts.set(key, attempt);
3027
+ return { attempt, created: true };
3028
+ });
3029
+ }
2607
3030
  async get(tenant, taskId) {
2608
3031
  return this.#state.attempts.get(tenantKey(tenant, taskId));
2609
3032
  }
@@ -2634,12 +3057,18 @@ var InMemoryTaskAttemptStore = class {
2634
3057
  return this.#state.mutate(key, () => {
2635
3058
  const existing = this.#state.attempts.get(key);
2636
3059
  if (existing === void 0) return void 0;
3060
+ if (existing.agentRef === void 0 !== (input.agentRef === void 0) || existing.agentRef !== void 0 && !sameAgentRef3(existing.agentRef, input.agentRef)) return existing;
2637
3061
  if (existing.cancellation !== void 0) {
2638
3062
  if (input.status !== "cancelled" || existing.status === "cancelled") return existing;
2639
3063
  } else if (existing.status === "complete" || existing.status === "failed" || existing.status === "cancelled") {
2640
3064
  return existing;
2641
3065
  }
2642
- const updated = { ...existing, status: input.status, updatedAt: this.#now() };
3066
+ const updated = {
3067
+ ...existing,
3068
+ status: input.status,
3069
+ ...input.terminalCause === void 0 ? {} : { terminalCause: input.terminalCause },
3070
+ updatedAt: this.#now()
3071
+ };
2643
3072
  this.#state.attempts.set(key, updated);
2644
3073
  return updated;
2645
3074
  });
@@ -2839,10 +3268,34 @@ var InMemoryApprovalTimelineStore = class {
2839
3268
  return tail;
2840
3269
  }
2841
3270
  };
3271
+ var InMemoryAgentEgressStore = class {
3272
+ #records = /* @__PURE__ */ new Map();
3273
+ #clock;
3274
+ constructor(clock) {
3275
+ this.#clock = clock;
3276
+ }
3277
+ async record(tenant, input) {
3278
+ const key = tenantKey(tenant, `${input.deviceId}\0${input.payload.eventId}`);
3279
+ const existing = this.#records.get(key);
3280
+ if (existing !== void 0) return { record: existing, created: false };
3281
+ const record = {
3282
+ tenantId: tenant,
3283
+ deviceId: input.deviceId,
3284
+ payload: input.payload,
3285
+ receiptId: input.receiptId,
3286
+ recordedAt: this.#clock.now().toISOString()
3287
+ };
3288
+ this.#records.set(key, record);
3289
+ return { record, created: true };
3290
+ }
3291
+ async get(tenant, deviceId, eventId) {
3292
+ return this.#records.get(tenantKey(tenant, `${deviceId}\0${eventId}`));
3293
+ }
3294
+ };
2842
3295
 
2843
3296
  // src/stores/in-memory/index.ts
2844
- function createInMemoryCloudStores(clock, crypto, objects, mailbox) {
2845
- const blobs = createInMemoryBlobs(clock, crypto, objects);
3297
+ function createInMemoryCloudStores(clock, crypto2, objects, mailbox) {
3298
+ const blobs = createInMemoryBlobs(clock, crypto2, objects);
2846
3299
  const taskState = new InMemoryTaskAttemptState(clock);
2847
3300
  const tasks = new InMemoryTaskAttemptStore(clock, taskState);
2848
3301
  return {
@@ -2851,11 +3304,12 @@ function createInMemoryCloudStores(clock, crypto, objects, mailbox) {
2851
3304
  approvals: new InMemoryApprovalTimelineStore(clock),
2852
3305
  devices: new InMemoryDeviceDirectory(),
2853
3306
  pairingCodes: new InMemoryPairingCodeStore(clock),
2854
- nonces: new InMemoryNonceStore(clock, crypto),
3307
+ nonces: new InMemoryNonceStore(clock, crypto2),
2855
3308
  dedup: new InMemoryInboundDedupStore(),
2856
3309
  tasks,
2857
3310
  cancellations: new InMemoryTaskCancellationStore(taskState, mailbox),
2858
3311
  receipts: new InMemoryRequestReceiptStore(clock),
3312
+ egress: new InMemoryAgentEgressStore(clock),
2859
3313
  proofReceipts: new InMemoryProofRequestReceiptStore(clock),
2860
3314
  blobs: blobs.blobs,
2861
3315
  rateLimiter: new AllowAllRateLimiter()
@@ -2871,11 +3325,11 @@ function systemClock() {
2871
3325
  }
2872
3326
  function createInMemoryByokCloud(options = {}) {
2873
3327
  const clock = options.clock ?? systemClock();
2874
- const crypto = options.crypto ?? createWebCrypto();
3328
+ const crypto2 = options.crypto ?? createWebCrypto();
2875
3329
  const core = createInMemoryCoreStores({ clock }).stores;
2876
3330
  const { stores, blobContentProxy } = createInMemoryCloudStores(
2877
3331
  clock,
2878
- crypto,
3332
+ crypto2,
2879
3333
  core.objects,
2880
3334
  core.mailbox
2881
3335
  );
@@ -2888,7 +3342,7 @@ function createInMemoryByokCloud(options = {}) {
2888
3342
  // pairing is what keeps hosted-in-memory behavior identical to what it was
2889
3343
  // before the port narrowed.
2890
3344
  blobContentProxy,
2891
- crypto,
3345
+ crypto: crypto2,
2892
3346
  tokenSigner,
2893
3347
  clock,
2894
3348
  capabilities: options.capabilities ?? fullCapabilityDeclaration(),
@@ -2917,7 +3371,7 @@ function createInMemoryByokCloud(options = {}) {
2917
3371
  ...options.skillPacks === void 0 ? {} : { skillPacks: options.skillPacks },
2918
3372
  ...options.skillPackPageLimit === void 0 ? {} : { skillPackPageLimit: options.skillPackPageLimit }
2919
3373
  });
2920
- return { cloud, core, stores, blobContentProxy, clock, crypto };
3374
+ return { cloud, core, stores, blobContentProxy, clock, crypto: crypto2 };
2921
3375
  }
2922
3376
  function authenticateHostedDeviceAssertion(input, deps) {
2923
3377
  return authenticateDeviceAssertion(input, {
@@ -3085,6 +3539,7 @@ var TASK_ATTEMPT_STATUSES = [
3085
3539
  "failed",
3086
3540
  "cancelled"
3087
3541
  ];
3542
+ var BLOB_READ_ERROR_CODES = ["blob_upstream_unavailable", "blob_upstream_stream_interrupted"];
3088
3543
  var CLOUD_STORE_NAMES = [
3089
3544
  "activity",
3090
3545
  "approvals",
@@ -3095,6 +3550,7 @@ var CLOUD_STORE_NAMES = [
3095
3550
  "tasks",
3096
3551
  "cancellations",
3097
3552
  "receipts",
3553
+ "egress",
3098
3554
  "proofReceipts",
3099
3555
  "blobs",
3100
3556
  "rateLimiter"
@@ -3104,13 +3560,14 @@ var CLOUD_STORE_NAMES = [
3104
3560
  var CLOUD_PORT_METHODS = {
3105
3561
  activity: ["append", "read"],
3106
3562
  approvals: ["append", "read"],
3107
- devices: ["register", "get", "revoke", "list", "readiness", "resolveByDeviceId"],
3563
+ devices: ["register", "get", "revoke", "list", "readiness", "recordCapabilities", "resolveByDeviceId"],
3108
3564
  pairingCodes: ["issue", "redeem"],
3109
3565
  nonces: ["issue", "validate", "markUsed"],
3110
3566
  dedup: ["checkAndRecord"],
3111
- tasks: ["open", "get", "getMany", "claim", "recordStatus"],
3567
+ tasks: ["open", "reserveAgentOffer", "get", "getMany", "claim", "recordStatus"],
3112
3568
  cancellations: ["request"],
3113
3569
  receipts: ["record", "get"],
3570
+ egress: ["record", "get"],
3114
3571
  proofReceipts: ["record", "get"],
3115
3572
  // Three methods, not six: the byte-proxy trio moved to `BlobContentProxy`,
3116
3573
  // which is a composition input rather than a port and therefore has no row
@@ -3128,11 +3585,12 @@ var CLOUD_PORT_INTERFACES = {
3128
3585
  tasks: "TaskAttemptStore",
3129
3586
  cancellations: "TaskCancellationStore",
3130
3587
  receipts: "RequestReceiptStore",
3588
+ egress: "AgentEgressStore",
3131
3589
  proofReceipts: "ProofRequestReceiptStore",
3132
3590
  blobs: "CloudBlobStore",
3133
3591
  rateLimiter: "InboundRateLimiter"
3134
3592
  };
3135
3593
 
3136
- export { ACCESS_TOKEN_TTL_SECONDS, APPROVAL_SUMMARY_MAX_BYTES, ActivityAppendRequestSchema, AllowAllRateLimiter, ApprovalObservationSchema, ApprovalTimelineEventSchema, BLOB_URL_TTL_MS, BoardFeedClient, BoardFeedRetryableError, BoardFeedStoppedError, ByokCloudError, CLOUD_CAPABILITIES, CLOUD_ERROR_CODES, CLOUD_PORT_INTERFACES, CLOUD_PORT_METHODS, CLOUD_STORE_NAMES, CapabilitiesResponseSchema, CloudRouteRegistry, DEDUP_RING_CAPACITY, DEFAULT_ACTIVITY_BOUNDS, DEFAULT_ACTIVITY_CAPACITY, DEFAULT_ACTIVITY_MAX_BYTES, DEFAULT_ACTIVITY_MAX_EVENTS, DEFAULT_ACTIVITY_TTL_MS, DEFAULT_APPROVAL_TIMELINE_CAPACITY, DEFAULT_APPROVAL_TIMELINE_TTL_MS, DEFAULT_BOARD_CHANNEL_MAX_BYTES, DEFAULT_BOARD_PAGE_LIMIT, DEFAULT_BOARD_STREAM_HEARTBEAT_INTERVAL_MS, DEFAULT_BOARD_STREAM_QUERY_INTERVAL_MS, DEFAULT_BOARD_STREAM_RECONCILIATION_INTERVAL_MS, DEFAULT_BOARD_TITLE_MAX_BYTES, DEFAULT_DEVICE_PROOF_CLOCK_SKEW_MS, DEFAULT_DEVICE_PROOF_MAX_LIFETIME_MS, DEFAULT_EVENTS_PAGE_LIMIT, DEFAULT_LONG_POLL_HOLD_MS, DEFAULT_LONG_POLL_INTERVAL_MS, DEFAULT_MAX_BLOB_SIZE_BYTES, DEFAULT_MAX_TRUTH_REQUEST_BYTES, DEFAULT_PRESENCE_DETAIL_MAX_BYTES, DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS, DEFAULT_PRESENCE_TTL_MS, DEFAULT_SKILL_PACK_PAGE_LIMIT, DEVICE_IDENTITY_PROOF_KEY_EPOCH, DEVICE_IDENTITY_PROOF_KEY_ID, InMemoryActivityStore, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryDeviceDirectory, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryProofRequestReceiptStore, InMemoryRequestReceiptStore, InMemoryTaskAttemptStore, InMemoryTaskCancellationStore, MAX_DEVICE_PROOF_CLOCK_SKEW_MS, MAX_DEVICE_PROOF_HEADER_BYTES, MAX_DEVICE_PROOF_MAX_LIFETIME_MS, NONCE_TTL_MS, PAIRING_CODE_TTL_MS, ROUTE_CLASSES, ROUTE_METHODS, TASK_ATTEMPT_STATUSES, TRUTH_BATCH_MAX_RECORDS, TRUTH_INLINE_CONTENT_TYPE, TRUTH_LABEL_MAX_LENGTH, TRUTH_MANIFEST_MAX_LIMIT, TRUTH_RECORD_CAPABILITY, TRUTH_RECORD_KEY_MAX_LENGTH, TRUTH_REQUEST_ID_MAX_LENGTH, TimelineEventSchema, TruthBodyInputSchema, TruthCommitError, TruthCommitResponseSchema, TruthRecordKeySchema, TruthRecordMetadataSchema, TruthWriteRequestSchema, activityCursor, approvalTimelineCursor, authenticateBearer, authenticateDeviceProof, authenticateHostedDeviceAssertion, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, projectTerminalResult, projectTimelineEvents, routeKey, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, verifyNonceSignature };
3594
+ export { ACCESS_TOKEN_TTL_SECONDS, AGENT_HOME_CONTRACT_CAPABILITY, APPROVAL_SUMMARY_MAX_BYTES, ActivityAppendRequestSchema, AllowAllRateLimiter, ApprovalObservationSchema, ApprovalTimelineEventSchema, BLOB_READ_ERROR_CODES, BLOB_URL_TTL_MS, BoardFeedClient, BoardFeedRetryableError, BoardFeedStoppedError, ByokCloudError, CLOUD_CAPABILITIES, CLOUD_ERROR_CODES, CLOUD_PORT_INTERFACES, CLOUD_PORT_METHODS, CLOUD_STORE_NAMES, CapabilitiesResponseSchema, CloudRouteRegistry, DEDUP_RING_CAPACITY, DEFAULT_ACTIVITY_BOUNDS, DEFAULT_ACTIVITY_CAPACITY, DEFAULT_ACTIVITY_MAX_BYTES, DEFAULT_ACTIVITY_MAX_EVENTS, DEFAULT_ACTIVITY_TTL_MS, DEFAULT_APPROVAL_TIMELINE_CAPACITY, DEFAULT_APPROVAL_TIMELINE_TTL_MS, DEFAULT_BOARD_CHANNEL_MAX_BYTES, DEFAULT_BOARD_PAGE_LIMIT, DEFAULT_BOARD_STREAM_HEARTBEAT_INTERVAL_MS, DEFAULT_BOARD_STREAM_QUERY_INTERVAL_MS, DEFAULT_BOARD_STREAM_RECONCILIATION_INTERVAL_MS, DEFAULT_BOARD_TITLE_MAX_BYTES, DEFAULT_DEVICE_PROOF_CLOCK_SKEW_MS, DEFAULT_DEVICE_PROOF_MAX_LIFETIME_MS, DEFAULT_EVENTS_PAGE_LIMIT, DEFAULT_LONG_POLL_HOLD_MS, DEFAULT_LONG_POLL_INTERVAL_MS, DEFAULT_MAX_BLOB_SIZE_BYTES, DEFAULT_MAX_TRUTH_REQUEST_BYTES, DEFAULT_PRESENCE_DETAIL_MAX_BYTES, DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS, DEFAULT_PRESENCE_TTL_MS, DEFAULT_SKILL_PACK_PAGE_LIMIT, DEVICE_IDENTITY_PROOF_KEY_EPOCH, DEVICE_IDENTITY_PROOF_KEY_ID, InMemoryActivityStore, InMemoryAgentEgressStore, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryDeviceDirectory, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryProofRequestReceiptStore, InMemoryRequestReceiptStore, InMemoryTaskAttemptStore, InMemoryTaskCancellationStore, MAX_DEVICE_PROOF_CLOCK_SKEW_MS, MAX_DEVICE_PROOF_HEADER_BYTES, MAX_DEVICE_PROOF_MAX_LIFETIME_MS, NONCE_TTL_MS, PAIRING_CODE_TTL_MS, ROUTE_CLASSES, ROUTE_METHODS, TASK_ATTEMPT_STATUSES, TRUTH_BATCH_MAX_RECORDS, TRUTH_INLINE_CONTENT_TYPE, TRUTH_LABEL_MAX_LENGTH, TRUTH_MANIFEST_MAX_LIMIT, TRUTH_RECORD_CAPABILITY, TRUTH_RECORD_KEY_MAX_LENGTH, TRUTH_REQUEST_ID_MAX_LENGTH, TimelineEventSchema, TruthBodyInputSchema, TruthCommitError, TruthCommitResponseSchema, TruthRecordKeySchema, TruthRecordMetadataSchema, TruthWriteRequestSchema, activityCursor, approvalTimelineCursor, authenticateBearer, authenticateDeviceProof, authenticateHostedDeviceAssertion, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, projectTerminalResult, projectTimelineEvents, routeKey, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, verifyNonceSignature };
3137
3595
  //# sourceMappingURL=index.js.map
3138
3596
  //# sourceMappingURL=index.js.map