@byok-sdk/cloud 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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, 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;
@@ -759,7 +783,15 @@ function tokenHandler(deps) {
759
783
  return c.json(response, 200);
760
784
  };
761
785
  }
762
- var CLOUD_PROTOCOL_CAPABILITIES = ["result-document"];
786
+ var CLOUD_PROTOCOL_CAPABILITIES = [
787
+ "result-document",
788
+ "agent-home-contract",
789
+ "agent-egress-policy",
790
+ "agent-egress-reliable-ack",
791
+ "agent-content-workspace-read",
792
+ "agent-content-transcript-read",
793
+ "agent-content-artifact-read"
794
+ ];
763
795
  function sleep(ms) {
764
796
  return new Promise((resolve) => setTimeout(resolve, ms));
765
797
  }
@@ -791,13 +823,15 @@ function eventsHandler(deps) {
791
823
  if (page.messages.length === 0) break;
792
824
  const decoded = page.messages.map((message) => decodeEnvelope(message.body));
793
825
  const offeredTaskIds = decoded.flatMap(
794
- (event) => (event.type === "task.offer" || event.type === "task.offer_with_toolsets") && event.task_id !== void 0 ? [event.task_id] : []
826
+ (event) => (event.type === "task.offer" || event.type === "task.offer_with_toolsets" || event.type === "task.offer_for_agent" || event.type === "task.offer_for_agent_with_egress") && event.task_id !== void 0 ? [event.task_id] : []
795
827
  );
796
828
  const attemptsByTaskId = Object.fromEntries(
797
829
  (await stores.tasks.getMany(offeredTaskIds)).map((attempt2) => [attempt2.taskId, attempt2])
798
830
  );
799
831
  const events = decoded.filter((event) => {
800
- if (event.type !== "task.offer" && event.type !== "task.offer_with_toolsets") return true;
832
+ if (event.type !== "task.offer" && event.type !== "task.offer_with_toolsets" && event.type !== "task.offer_for_agent" && event.type !== "task.offer_for_agent_with_egress") {
833
+ return true;
834
+ }
801
835
  return event.task_id === void 0 || attemptsByTaskId[event.task_id]?.cancellation === void 0;
802
836
  });
803
837
  if (events.length === 0 && page.hasMore) {
@@ -910,13 +944,103 @@ async function projectTerminalToReview(board, taskId) {
910
944
  function terminalReceiptKey(taskId) {
911
945
  return `task:${taskId}:terminal`;
912
946
  }
947
+ function sameAgentRef(expected, actual) {
948
+ return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
949
+ }
950
+ function sameReliableEgress(expected, actual) {
951
+ return sameAgentRef(expected.agentRef, actual.agentRef) && expected.sessionRef === actual.sessionRef && expected.policyRevision === actual.policyRevision && expected.eventId === actual.eventId && expected.cursor === actual.cursor && expected.contentHash === actual.contentHash && expected.byteCount === actual.byteCount && JSON.stringify(expected.payload) === JSON.stringify(actual.payload);
952
+ }
953
+ function contentReadCapability(surface) {
954
+ switch (surface) {
955
+ case "workspace":
956
+ return AGENT_CONTENT_WORKSPACE_READ_CAPABILITY;
957
+ case "transcript":
958
+ return AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY;
959
+ case "artifact":
960
+ return AGENT_CONTENT_ARTIFACT_READ_CAPABILITY;
961
+ }
962
+ }
963
+ function hasCapabilities(capabilities, required) {
964
+ return capabilities !== void 0 && required.every((capability) => capabilities.includes(capability));
965
+ }
966
+ function contentRequestReceiptKey(deviceId, requestId) {
967
+ return `agent-content-request:${deviceId}:${requestId}`;
968
+ }
969
+ function contentReceiptKey(deviceId, requestId) {
970
+ return `agent-content:${deviceId}:${requestId}`;
971
+ }
972
+ function matchesContentReadReceipt(request, receipt) {
973
+ return request.requestId === receipt.requestId && request.surface === receipt.surface && request.actor.kind === receipt.actor.kind && request.actor.id === receipt.actor.id && sameAgentRef(request.agentRef, receipt.agentRef) && request.sessionRef === receipt.sessionRef && request.runtime === receipt.runtime && request.cwd === receipt.cwd && request.policyRevision === receipt.policyRevision && request.target === receipt.target && request.mimeType === receipt.mimeType && request.decodeAs === receipt.decodeAs;
974
+ }
975
+ async function readContentRequest(stores, deviceId, requestId) {
976
+ const stored = await stores.receipts.get(contentRequestReceiptKey(deviceId, requestId));
977
+ if (stored === void 0) return void 0;
978
+ try {
979
+ return AgentContentReadPayloadSchema.parse(JSON.parse(stored.body));
980
+ } catch {
981
+ return void 0;
982
+ }
983
+ }
984
+ function inboundAgentRef(envelope) {
985
+ switch (envelope.type) {
986
+ case "task.claim":
987
+ case "task.decline":
988
+ case "task.complete":
989
+ case "task.fail":
990
+ case "task.cancelled":
991
+ return envelope.payload.agentRef;
992
+ default:
993
+ return void 0;
994
+ }
995
+ }
996
+ function agentRefEchoRequired(envelope) {
997
+ return envelope.type === "task.claim" || envelope.type === "task.decline" || envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled";
998
+ }
913
999
  async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS) {
914
1000
  if (!await stores.rateLimiter.consume(deviceId)) return "rate_limited";
1001
+ if (envelope.type === "conn.hello") {
1002
+ if (envelope.payload.deviceId !== deviceId) return "rejected";
1003
+ if (!envelope.payload.protocolVersions.includes(PROTOCOL_VERSION)) return "rejected";
1004
+ const device = await stores.devices.get(deviceId);
1005
+ if (device === void 0 || device.revoked || device.productId !== envelope.payload.productId) {
1006
+ return "rejected";
1007
+ }
1008
+ if (await stores.dedup.checkAndRecord(deviceId, envelope.id)) return "duplicate";
1009
+ return await stores.devices.recordCapabilities({ capabilities: envelope.payload.capabilities }) === void 0 ? "rejected" : "accepted";
1010
+ }
915
1011
  if (!DAEMON_TO_SERVER_TYPES.includes(envelope.type)) return "rejected";
1012
+ if (envelope.type === "agent.egress.reliable") {
1013
+ const device = await stores.devices.get(deviceId);
1014
+ if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
1015
+ return "rejected";
1016
+ }
1017
+ const outcome = await stores.egress.record({
1018
+ deviceId,
1019
+ payload: envelope.payload,
1020
+ receiptId: crypto.randomUUID()
1021
+ });
1022
+ return outcome.created || sameReliableEgress(outcome.record.payload, envelope.payload) ? outcome.created ? "accepted" : "duplicate" : "rejected";
1023
+ }
1024
+ if (envelope.type === "agent.content.receipt") {
1025
+ const device = await stores.devices.get(deviceId);
1026
+ if (device === void 0 || device.revoked || envelope.payload.eventId !== envelope.payload.requestId || !hasCapabilities(device.capabilities, [contentReadCapability(envelope.payload.surface), AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
1027
+ return "rejected";
1028
+ }
1029
+ const request = await readContentRequest(stores, deviceId, envelope.payload.requestId);
1030
+ if (request === void 0 || !matchesContentReadReceipt(request, envelope.payload)) return "rejected";
1031
+ const key = contentReceiptKey(deviceId, envelope.payload.requestId);
1032
+ const body = JSON.stringify(envelope.payload);
1033
+ const receipt = await stores.receipts.record({ key, body });
1034
+ return receipt.created ? "accepted" : receipt.receipt.body === body ? "duplicate" : "rejected";
1035
+ }
916
1036
  const taskId = envelope.task_id;
917
1037
  if (taskId === void 0) return "rejected";
918
1038
  const attempt = await stores.tasks.get(taskId);
1039
+ if (attempt?.agentRef !== void 0 && attempt.deviceId !== deviceId) return "rejected";
919
1040
  if (attempt?.ownerDeviceId !== void 0 && attempt.ownerDeviceId !== deviceId) return "rejected";
1041
+ if (attempt?.agentRef !== void 0 && agentRefEchoRequired(envelope) && !sameAgentRef(attempt.agentRef, inboundAgentRef(envelope))) {
1042
+ return "rejected";
1043
+ }
920
1044
  if (envelope.type === "task.progress" && envelope.payload.events.length > 0) {
921
1045
  try {
922
1046
  validateActivityBatch(
@@ -946,19 +1070,27 @@ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds
946
1070
  }
947
1071
  }
948
1072
  if (await stores.dedup.checkAndRecord(deviceId, envelope.id)) return "duplicate";
949
- await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds);
1073
+ await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, attempt?.agentRef);
950
1074
  return "accepted";
951
1075
  }
952
- async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds) {
1076
+ async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, persistedAgentRef) {
953
1077
  switch (envelope.type) {
954
1078
  case "task.claim":
955
1079
  await stores.tasks.claim({ taskId, deviceId });
956
1080
  return;
957
1081
  case "task.started":
958
- await stores.tasks.recordStatus({ taskId, status: "running" });
1082
+ await stores.tasks.recordStatus({
1083
+ taskId,
1084
+ status: "running",
1085
+ ...persistedAgentRef === void 0 ? {} : { agentRef: persistedAgentRef }
1086
+ });
959
1087
  return;
960
1088
  case "task.decline":
961
- await stores.tasks.recordStatus({ taskId, status: "failed" });
1089
+ await stores.tasks.recordStatus({
1090
+ taskId,
1091
+ status: "failed",
1092
+ ...persistedAgentRef === void 0 ? {} : { agentRef: persistedAgentRef }
1093
+ });
962
1094
  return;
963
1095
  case "task.progress":
964
1096
  if (envelope.payload.events.length > 0) {
@@ -1030,12 +1162,22 @@ async function recordTerminal(stores, taskId, envelope, status) {
1030
1162
  });
1031
1163
  if (attempt?.cancellation !== void 0) {
1032
1164
  if (status === "cancelled") {
1033
- await stores.tasks.recordStatus({ taskId, status: "cancelled" });
1165
+ await stores.tasks.recordStatus({
1166
+ taskId,
1167
+ status: "cancelled",
1168
+ ...attempt.agentRef === void 0 ? {} : { agentRef: attempt.agentRef }
1169
+ });
1034
1170
  }
1035
1171
  return;
1036
1172
  }
1037
1173
  if (!created) return;
1038
- const recordedAttempt = await stores.tasks.recordStatus({ taskId, status });
1174
+ const cause = envelope.type === "task.fail" || envelope.type === "task.cancelled" ? envelope.payload.reason : void 0;
1175
+ const recordedAttempt = await stores.tasks.recordStatus({
1176
+ taskId,
1177
+ status,
1178
+ ...attempt?.agentRef === void 0 ? {} : { agentRef: attempt.agentRef },
1179
+ ...cause === void 0 ? {} : { terminalCause: cause }
1180
+ });
1039
1181
  if (recordedAttempt?.status !== status || recordedAttempt.cancellation !== void 0) return;
1040
1182
  await projectTerminalToReview(stores.board, taskId);
1041
1183
  }
@@ -1058,6 +1200,16 @@ function messagesHandler(deps) {
1058
1200
  deps.activityBounds
1059
1201
  );
1060
1202
  if (outcome === "rate_limited") return c.json({ error: "rate limit exceeded" }, 429);
1203
+ if (envelope.type === "agent.egress.reliable" && (outcome === "accepted" || outcome === "duplicate")) {
1204
+ const record = await stores.egress.get(device.deviceId, envelope.payload.eventId);
1205
+ if (record === void 0) {
1206
+ throw new Error(`Accepted reliable egress ${envelope.payload.eventId} has no durable receipt record.`);
1207
+ }
1208
+ await deps.appendReliableEgressAck(stores, record);
1209
+ }
1210
+ if (envelope.type === "agent.content.receipt" && (outcome === "accepted" || outcome === "duplicate")) {
1211
+ await deps.appendContentReceiptAck(stores, device.deviceId, envelope.payload);
1212
+ }
1061
1213
  if (outcome === "rejected") rejected += 1;
1062
1214
  else accepted += 1;
1063
1215
  }
@@ -1825,6 +1977,7 @@ function projectTerminalResult(taskId, receipt) {
1825
1977
  return {
1826
1978
  taskId,
1827
1979
  state: "complete",
1980
+ ...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
1828
1981
  summary: envelope.payload.summary,
1829
1982
  sessionRef: envelope.payload.sessionRef,
1830
1983
  ...envelope.payload.artifactRefs !== void 0 ? { artifactRefs: envelope.payload.artifactRefs } : {},
@@ -1836,7 +1989,9 @@ function projectTerminalResult(taskId, receipt) {
1836
1989
  return {
1837
1990
  taskId,
1838
1991
  state: "failed",
1992
+ ...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
1839
1993
  reason: envelope.payload.reason,
1994
+ terminalCause: envelope.payload.reason,
1840
1995
  ...envelope.payload.retryable !== void 0 ? { retryable: envelope.payload.retryable } : {},
1841
1996
  ...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
1842
1997
  recordedAt: receipt.recordedAt
@@ -1845,7 +2000,9 @@ function projectTerminalResult(taskId, receipt) {
1845
2000
  return {
1846
2001
  taskId,
1847
2002
  state: "cancelled",
2003
+ ...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
1848
2004
  ...envelope.payload.reason !== void 0 ? { reason: envelope.payload.reason } : {},
2005
+ ...envelope.payload.reason === void 0 ? {} : { terminalCause: envelope.payload.reason },
1849
2006
  ...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
1850
2007
  recordedAt: receipt.recordedAt
1851
2008
  };
@@ -1862,6 +2019,10 @@ var DEFAULT_MAX_BLOB_SIZE_BYTES = 100 * 1024 * 1024;
1862
2019
  var DEFAULT_LONG_POLL_HOLD_MS = 5e4;
1863
2020
  var DEFAULT_LONG_POLL_INTERVAL_MS = 250;
1864
2021
  var DEFAULT_EVENTS_PAGE_LIMIT = 50;
2022
+ var AGENT_HOME_CONTRACT_CAPABILITY = "agent-home-contract";
2023
+ function sameAgentRef2(expected, actual) {
2024
+ return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
2025
+ }
1865
2026
  function createByokCloud(options) {
1866
2027
  const declaration = parseDeclaration(options.capabilities);
1867
2028
  assertNoOverDeclaration(
@@ -1911,7 +2072,12 @@ function createByokCloud(options) {
1911
2072
  if (declares(declaration, CLOUD_CAPABILITIES.messagesBatch)) {
1912
2073
  registry.register(
1913
2074
  { method: "POST", path: BYOK_MESSAGES_PATH, class: "device" },
1914
- messagesHandler({ ...deviceRouteDeps, activityBounds })
2075
+ messagesHandler({
2076
+ ...deviceRouteDeps,
2077
+ activityBounds,
2078
+ appendReliableEgressAck: enqueueReliableEgressAck,
2079
+ appendContentReceiptAck: enqueueContentReceiptAck
2080
+ })
1915
2081
  );
1916
2082
  }
1917
2083
  if (declares(declaration, CLOUD_CAPABILITIES.boardCoordination)) {
@@ -2029,16 +2195,98 @@ function createByokCloud(options) {
2029
2195
  function controlPlane(tenant) {
2030
2196
  return { kind: "control-plane", tenantId: tenant, operatorId };
2031
2197
  }
2032
- async function enqueueTaskEnvelope(tenant, deviceId, requestedTaskId, buildEnvelope) {
2198
+ async function enqueueTaskEnvelope(tenant, deviceId, requestedTaskId, agentRef, buildEnvelope) {
2033
2199
  const stores = tenantStoresFor(controlPlane(tenant), root);
2034
2200
  const taskId = requestedTaskId ?? `task_${options.crypto.randomUuid()}`;
2035
2201
  const messageId = options.crypto.randomUuid();
2202
+ const reservation = agentRef === void 0 ? void 0 : await stores.tasks.reserveAgentOffer({ taskId, deviceId, agentRef });
2203
+ const openedBeforeAppend = reservation?.attempt;
2204
+ if (agentRef !== void 0 && (openedBeforeAppend === void 0 || openedBeforeAppend.deviceId !== deviceId || openedBeforeAppend.agentRef === void 0 || !sameAgentRef2(agentRef, openedBeforeAppend.agentRef))) {
2205
+ throw new ByokCloudError(
2206
+ "agent_ref_mismatch",
2207
+ `Task ${taskId} already has a different durable Agent identity or target device.`
2208
+ );
2209
+ }
2210
+ if (reservation !== void 0 && !reservation.created) {
2211
+ throw new ByokCloudError(
2212
+ "agent_task_already_exists",
2213
+ `Task ${taskId} already has a durable Agent attempt and cannot be enqueued again.`
2214
+ );
2215
+ }
2216
+ let message;
2217
+ try {
2218
+ message = await stores.mailbox.append({
2219
+ deviceId,
2220
+ messageId,
2221
+ materialize: async (seq) => {
2222
+ const envelope2 = buildEnvelope(taskId, seq, messageId);
2223
+ const body = encodeEnvelope(envelope2);
2224
+ const bytes = new TextEncoder().encode(body);
2225
+ return {
2226
+ body,
2227
+ bodyHash: contentHash(await options.crypto.sha256(bytes)),
2228
+ byteSize: BigInt(bytes.length)
2229
+ };
2230
+ }
2231
+ });
2232
+ } catch (error) {
2233
+ if (openedBeforeAppend !== void 0) {
2234
+ try {
2235
+ await stores.tasks.recordStatus({
2236
+ taskId,
2237
+ status: "failed",
2238
+ agentRef,
2239
+ terminalCause: "mailbox append failed before Agent offer delivery"
2240
+ });
2241
+ } catch (cleanupError) {
2242
+ throw new AggregateError(
2243
+ [error, cleanupError],
2244
+ `Agent task ${taskId} could not be delivered or durably closed`
2245
+ );
2246
+ }
2247
+ }
2248
+ throw error;
2249
+ }
2250
+ const envelope = decodeEnvelope(message.body);
2251
+ if (envelope.seq !== message.seq) {
2252
+ throw new ByokCloudError(
2253
+ "mailbox_seq_mismatch",
2254
+ `Mailbox stored offer seq ${String(envelope.seq)} at row ${message.seq}; the daemon's redelivery cursor would be wrong.`
2255
+ );
2256
+ }
2257
+ const attempt = openedBeforeAppend ?? await stores.tasks.open({
2258
+ taskId,
2259
+ deviceId
2260
+ });
2261
+ return { taskId, seq: message.seq, envelope, attempt };
2262
+ }
2263
+ async function assertAgentCapabilities(tenant, deviceId, required) {
2264
+ const device = await options.cloud.devices.get(tenant, deviceId);
2265
+ if (device === void 0 || device.revoked || !device.capabilities || !required.every((capability) => device.capabilities.includes(capability))) {
2266
+ const missing = required.filter((capability) => !device?.capabilities?.includes(capability));
2267
+ throw new ByokCloudError(
2268
+ "agent_capability_missing",
2269
+ `Device ${deviceId} lacks durable Agent capabilities (${missing.join(", ")}); refusing before mailbox append.`
2270
+ );
2271
+ }
2272
+ }
2273
+ function contentReadCapability2(surface) {
2274
+ switch (surface) {
2275
+ case "workspace":
2276
+ return AGENT_CONTENT_WORKSPACE_READ_CAPABILITY;
2277
+ case "transcript":
2278
+ return AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY;
2279
+ case "artifact":
2280
+ return AGENT_CONTENT_ARTIFACT_READ_CAPABILITY;
2281
+ }
2282
+ }
2283
+ async function enqueueAgentControlEnvelope(tenant, deviceId, messageId, buildEnvelope) {
2284
+ const stores = tenantStoresFor(controlPlane(tenant), root);
2036
2285
  const message = await stores.mailbox.append({
2037
2286
  deviceId,
2038
2287
  messageId,
2039
2288
  materialize: async (seq) => {
2040
- const envelope2 = buildEnvelope(taskId, seq, messageId);
2041
- const body = encodeEnvelope(envelope2);
2289
+ const body = encodeEnvelope(buildEnvelope(seq));
2042
2290
  const bytes = new TextEncoder().encode(body);
2043
2291
  return {
2044
2292
  body,
@@ -2051,11 +2299,77 @@ function createByokCloud(options) {
2051
2299
  if (envelope.seq !== message.seq) {
2052
2300
  throw new ByokCloudError(
2053
2301
  "mailbox_seq_mismatch",
2054
- `Mailbox stored offer seq ${String(envelope.seq)} at row ${message.seq}; the daemon's redelivery cursor would be wrong.`
2302
+ `Mailbox stored Agent control seq ${String(envelope.seq)} at row ${message.seq}.`
2303
+ );
2304
+ }
2305
+ return { seq: message.seq, envelope };
2306
+ }
2307
+ async function enqueueReliableEgressAck(stores, record) {
2308
+ const payload = AgentEgressAckPayloadSchema.parse({
2309
+ agentRef: record.payload.agentRef,
2310
+ sessionRef: record.payload.sessionRef,
2311
+ policyRevision: record.payload.policyRevision,
2312
+ eventId: record.payload.eventId,
2313
+ cursor: record.payload.cursor,
2314
+ receiptId: record.receiptId
2315
+ });
2316
+ const message = await stores.mailbox.append({
2317
+ deviceId: record.deviceId,
2318
+ messageId: record.receiptId,
2319
+ materialize: async (seq) => {
2320
+ const body = encodeEnvelope(createEnvelope("agent.egress.ack", payload, { id: record.receiptId, seq }));
2321
+ const bytes = new TextEncoder().encode(body);
2322
+ return {
2323
+ body,
2324
+ bodyHash: contentHash(await options.crypto.sha256(bytes)),
2325
+ byteSize: BigInt(bytes.length)
2326
+ };
2327
+ }
2328
+ });
2329
+ const decoded = decodeEnvelope(message.body);
2330
+ if (decoded.type !== "agent.egress.ack" || decoded.seq !== message.seq || JSON.stringify(decoded.payload) !== JSON.stringify(payload)) {
2331
+ throw new ByokCloudError(
2332
+ "mailbox_receipt_mismatch",
2333
+ `Mailbox receipt ${record.receiptId} does not exactly acknowledge Agent egress ${record.payload.eventId}.`
2334
+ );
2335
+ }
2336
+ }
2337
+ async function enqueueContentReceiptAck(stores, deviceId, receipt) {
2338
+ const exactReceipt = AgentContentReceiptPayloadSchema.parse(receipt);
2339
+ if (exactReceipt.eventId !== exactReceipt.requestId) {
2340
+ throw new ByokCloudError("mailbox_receipt_mismatch", "Content receipt eventId must equal requestId.");
2341
+ }
2342
+ const payload = AgentEgressAckPayloadSchema.parse({
2343
+ agentRef: exactReceipt.agentRef,
2344
+ sessionRef: exactReceipt.sessionRef,
2345
+ policyRevision: exactReceipt.policyRevision,
2346
+ eventId: exactReceipt.eventId,
2347
+ cursor: exactReceipt.cursor,
2348
+ receiptId: exactReceipt.requestId
2349
+ });
2350
+ const message = await stores.mailbox.append({
2351
+ deviceId,
2352
+ // The request itself already occupies its bare request id in this
2353
+ // mailbox. A namespaced producer key keeps the ack idempotent without
2354
+ // returning that control envelope on duplicate receipt delivery.
2355
+ messageId: `agent-content-ack:${exactReceipt.requestId}`,
2356
+ materialize: async (seq) => {
2357
+ const body = encodeEnvelope(createEnvelope("agent.egress.ack", payload, { id: exactReceipt.requestId, seq }));
2358
+ const bytes = new TextEncoder().encode(body);
2359
+ return {
2360
+ body,
2361
+ bodyHash: contentHash(await options.crypto.sha256(bytes)),
2362
+ byteSize: BigInt(bytes.length)
2363
+ };
2364
+ }
2365
+ });
2366
+ const decoded = decodeEnvelope(message.body);
2367
+ if (decoded.type !== "agent.egress.ack" || decoded.seq !== message.seq || JSON.stringify(decoded.payload) !== JSON.stringify(payload)) {
2368
+ throw new ByokCloudError(
2369
+ "mailbox_receipt_mismatch",
2370
+ `Mailbox content receipt ${exactReceipt.requestId} does not exactly acknowledge its durable content fact.`
2055
2371
  );
2056
2372
  }
2057
- const attempt = await stores.tasks.open({ taskId, deviceId });
2058
- return { taskId, seq: message.seq, envelope, attempt };
2059
2373
  }
2060
2374
  return {
2061
2375
  fetch: registry.fetch,
@@ -2070,6 +2384,7 @@ function createByokCloud(options) {
2070
2384
  tenant,
2071
2385
  deviceId,
2072
2386
  input.taskId,
2387
+ void 0,
2073
2388
  (taskId, seq, messageId) => createEnvelope("task.offer", input.payload, { id: messageId, taskId, seq })
2074
2389
  );
2075
2390
  },
@@ -2078,9 +2393,69 @@ function createByokCloud(options) {
2078
2393
  tenant,
2079
2394
  deviceId,
2080
2395
  input.taskId,
2396
+ void 0,
2081
2397
  (taskId, seq, messageId) => createEnvelope("task.offer_with_toolsets", input.payload, { id: messageId, taskId, seq })
2082
2398
  );
2083
2399
  },
2400
+ async enqueueAgentOffer(tenant, deviceId, input) {
2401
+ await assertAgentCapabilities(tenant, deviceId, [AGENT_HOME_CONTRACT_CAPABILITY]);
2402
+ const payload = TaskOfferForAgentPayloadSchema.parse(input.payload);
2403
+ return enqueueTaskEnvelope(
2404
+ tenant,
2405
+ deviceId,
2406
+ input.taskId,
2407
+ payload.agentRef,
2408
+ (taskId, seq, messageId) => createEnvelope("task.offer_for_agent", payload, { id: messageId, taskId, seq })
2409
+ );
2410
+ },
2411
+ async enqueueAgentEgressOffer(tenant, deviceId, input) {
2412
+ await assertAgentCapabilities(tenant, deviceId, [
2413
+ AGENT_HOME_CONTRACT_CAPABILITY,
2414
+ AGENT_EGRESS_POLICY_CAPABILITY,
2415
+ AGENT_EGRESS_RELIABLE_ACK_CAPABILITY
2416
+ ]);
2417
+ const payload = TaskOfferForAgentWithEgressPayloadSchema.parse(input.payload);
2418
+ return enqueueTaskEnvelope(
2419
+ tenant,
2420
+ deviceId,
2421
+ input.taskId,
2422
+ payload.agentRef,
2423
+ (taskId, seq, messageId) => createEnvelope("task.offer_for_agent_with_egress", payload, { id: messageId, taskId, seq })
2424
+ );
2425
+ },
2426
+ async enqueueAgentContentRead(tenant, deviceId, input) {
2427
+ const payload = AgentContentReadPayloadSchema.parse(input.payload);
2428
+ await assertAgentCapabilities(tenant, deviceId, [
2429
+ AGENT_HOME_CONTRACT_CAPABILITY,
2430
+ AGENT_EGRESS_POLICY_CAPABILITY,
2431
+ AGENT_EGRESS_RELIABLE_ACK_CAPABILITY,
2432
+ contentReadCapability2(payload.surface)
2433
+ ]);
2434
+ const stores = tenantStoresFor(controlPlane(tenant), root);
2435
+ const persisted = await stores.receipts.record({
2436
+ key: `agent-content-request:${deviceId}:${payload.requestId}`,
2437
+ body: JSON.stringify(payload)
2438
+ });
2439
+ if (!persisted.created && persisted.receipt.body !== JSON.stringify(payload)) {
2440
+ throw new ByokCloudError(
2441
+ "agent_content_request_mismatch",
2442
+ `Request ${payload.requestId} already exists with a different immutable content-read body.`
2443
+ );
2444
+ }
2445
+ const control = await enqueueAgentControlEnvelope(
2446
+ tenant,
2447
+ deviceId,
2448
+ payload.requestId,
2449
+ (seq) => createEnvelope("agent.content.read", payload, { id: payload.requestId, seq })
2450
+ );
2451
+ if (control.envelope.type !== "agent.content.read" || JSON.stringify(control.envelope.payload) !== JSON.stringify(payload)) {
2452
+ throw new ByokCloudError(
2453
+ "agent_content_request_mismatch",
2454
+ `Request ${payload.requestId} already exists with a different immutable content-read body.`
2455
+ );
2456
+ }
2457
+ return control;
2458
+ },
2084
2459
  async cancelTask(tenant, taskId, reason) {
2085
2460
  const stores = tenantStoresFor(controlPlane(tenant), root);
2086
2461
  const proposedMessageId = options.crypto.randomUuid();
@@ -2123,6 +2498,9 @@ function createByokCloud(options) {
2123
2498
  readTerminalReceipt(tenant, taskId) {
2124
2499
  return tenantStoresFor(controlPlane(tenant), root).receipts.get(terminalReceiptKey(taskId));
2125
2500
  },
2501
+ readAgentEgress(tenant, deviceId, eventId) {
2502
+ return tenantStoresFor(controlPlane(tenant), root).egress.get(deviceId, eventId);
2503
+ },
2126
2504
  async readTaskResult(tenant, taskId) {
2127
2505
  const stores = tenantStoresFor(controlPlane(tenant), root);
2128
2506
  const attempt = await stores.tasks.get(taskId);
@@ -2130,7 +2508,9 @@ function createByokCloud(options) {
2130
2508
  return {
2131
2509
  taskId,
2132
2510
  state: "cancelled",
2511
+ ...attempt.agentRef === void 0 ? {} : { agentRef: attempt.agentRef },
2133
2512
  ...attempt.cancellation.reason === void 0 ? {} : { reason: attempt.cancellation.reason },
2513
+ ...attempt.cancellation.reason === void 0 ? {} : { terminalCause: attempt.cancellation.reason },
2134
2514
  recordedAt: attempt.cancellation.requestedAt
2135
2515
  };
2136
2516
  }
@@ -2235,9 +2615,9 @@ var InMemoryBlobRegistry = class {
2235
2615
  crypto;
2236
2616
  secret;
2237
2617
  urlTtlMs;
2238
- constructor(clock, crypto, options) {
2618
+ constructor(clock, crypto2, options) {
2239
2619
  this.clock = clock;
2240
- this.crypto = crypto;
2620
+ this.crypto = crypto2;
2241
2621
  this.secret = globalThis.crypto.getRandomValues(new Uint8Array(SIGNING_SECRET_BYTES));
2242
2622
  this.urlTtlMs = options.urlTtlMs ?? BLOB_URL_TTL_MS;
2243
2623
  }
@@ -2342,14 +2722,20 @@ var InMemoryBlobContentProxy = class {
2342
2722
  record.uploaded = true;
2343
2723
  return { ok: true };
2344
2724
  }
2725
+ /**
2726
+ * Never returns `{ok:false}`: this composition holds the bytes in the same
2727
+ * process, so there is no upstream to be unreachable and no stream to be
2728
+ * interrupted. Both `BlobReadErrorCode`s are structurally unreachable here
2729
+ * — a proxy that fetches from object storage is where they become live.
2730
+ */
2345
2731
  async readContent(blobId) {
2346
2732
  const record = this.#registry.blobs.get(blobId);
2347
2733
  if (record === void 0 || !record.uploaded || record.data === void 0) return void 0;
2348
- return { data: record.data, contentType: record.contentType };
2734
+ return { ok: true, content: { data: record.data, contentType: record.contentType } };
2349
2735
  }
2350
2736
  };
2351
- function createInMemoryBlobs(clock, crypto, objects, options = {}) {
2352
- const registry = new InMemoryBlobRegistry(clock, crypto, options);
2737
+ function createInMemoryBlobs(clock, crypto2, objects, options = {}) {
2738
+ const registry = new InMemoryBlobRegistry(clock, crypto2, options);
2353
2739
  return {
2354
2740
  blobs: new InMemoryCloudBlobStore(registry, objects),
2355
2741
  contentProxy: new InMemoryBlobContentProxy(registry)
@@ -2392,6 +2778,17 @@ var InMemoryDeviceDirectory = class {
2392
2778
  if (record === void 0) return;
2393
2779
  this.#byTenant.set(key, { ...record, revoked: true });
2394
2780
  }
2781
+ async recordCapabilities(tenant, input) {
2782
+ const key = tenantKey(tenant, input.deviceId);
2783
+ const record = this.#byTenant.get(key);
2784
+ if (record === void 0 || record.revoked) return void 0;
2785
+ const updated = {
2786
+ ...record,
2787
+ capabilities: Object.freeze([...input.capabilities])
2788
+ };
2789
+ this.#byTenant.set(key, updated);
2790
+ return updated;
2791
+ }
2395
2792
  async list(tenant) {
2396
2793
  const prefix = tenantKey(tenant, "");
2397
2794
  return [...this.#byTenant.entries()].filter(([key]) => key.startsWith(prefix)).map(([, record]) => record);
@@ -2477,9 +2874,9 @@ var InMemoryNonceStore = class {
2477
2874
  #clock;
2478
2875
  #crypto;
2479
2876
  #ttlMs;
2480
- constructor(clock, crypto, ttlMs = NONCE_TTL_MS) {
2877
+ constructor(clock, crypto2, ttlMs = NONCE_TTL_MS) {
2481
2878
  this.#clock = clock;
2482
- this.#crypto = crypto;
2879
+ this.#crypto = crypto2;
2483
2880
  this.#ttlMs = ttlMs;
2484
2881
  }
2485
2882
  /** Number of records currently held (post-sweep). Test-facing only. */
@@ -2583,6 +2980,9 @@ var InMemoryProofRequestReceiptStore = class {
2583
2980
  return this.#receipts.get(tenantKey(tenant, deviceId, requestId));
2584
2981
  }
2585
2982
  };
2983
+ function sameAgentRef3(left, right) {
2984
+ return left?.agentId === right?.agentId && left?.profileRevision === right?.profileRevision;
2985
+ }
2586
2986
  var InMemoryTaskAttemptStore = class {
2587
2987
  #state;
2588
2988
  constructor(clock, state = new InMemoryTaskAttemptState(clock)) {
@@ -2597,6 +2997,7 @@ var InMemoryTaskAttemptStore = class {
2597
2997
  tenantId: tenant,
2598
2998
  taskId: input.taskId,
2599
2999
  deviceId: input.deviceId,
3000
+ ...input.agentRef === void 0 ? {} : { agentRef: { ...input.agentRef } },
2600
3001
  status: "offered",
2601
3002
  updatedAt: this.#now()
2602
3003
  };
@@ -2604,6 +3005,23 @@ var InMemoryTaskAttemptStore = class {
2604
3005
  return attempt;
2605
3006
  });
2606
3007
  }
3008
+ async reserveAgentOffer(tenant, input) {
3009
+ const key = tenantKey(tenant, input.taskId);
3010
+ return this.#state.mutate(key, () => {
3011
+ const existing = this.#state.attempts.get(key);
3012
+ if (existing !== void 0) return { attempt: existing, created: false };
3013
+ const attempt = {
3014
+ tenantId: tenant,
3015
+ taskId: input.taskId,
3016
+ deviceId: input.deviceId,
3017
+ agentRef: { ...input.agentRef },
3018
+ status: "offered",
3019
+ updatedAt: this.#now()
3020
+ };
3021
+ this.#state.attempts.set(key, attempt);
3022
+ return { attempt, created: true };
3023
+ });
3024
+ }
2607
3025
  async get(tenant, taskId) {
2608
3026
  return this.#state.attempts.get(tenantKey(tenant, taskId));
2609
3027
  }
@@ -2634,12 +3052,18 @@ var InMemoryTaskAttemptStore = class {
2634
3052
  return this.#state.mutate(key, () => {
2635
3053
  const existing = this.#state.attempts.get(key);
2636
3054
  if (existing === void 0) return void 0;
3055
+ if (existing.agentRef === void 0 !== (input.agentRef === void 0) || existing.agentRef !== void 0 && !sameAgentRef3(existing.agentRef, input.agentRef)) return existing;
2637
3056
  if (existing.cancellation !== void 0) {
2638
3057
  if (input.status !== "cancelled" || existing.status === "cancelled") return existing;
2639
3058
  } else if (existing.status === "complete" || existing.status === "failed" || existing.status === "cancelled") {
2640
3059
  return existing;
2641
3060
  }
2642
- const updated = { ...existing, status: input.status, updatedAt: this.#now() };
3061
+ const updated = {
3062
+ ...existing,
3063
+ status: input.status,
3064
+ ...input.terminalCause === void 0 ? {} : { terminalCause: input.terminalCause },
3065
+ updatedAt: this.#now()
3066
+ };
2643
3067
  this.#state.attempts.set(key, updated);
2644
3068
  return updated;
2645
3069
  });
@@ -2839,10 +3263,34 @@ var InMemoryApprovalTimelineStore = class {
2839
3263
  return tail;
2840
3264
  }
2841
3265
  };
3266
+ var InMemoryAgentEgressStore = class {
3267
+ #records = /* @__PURE__ */ new Map();
3268
+ #clock;
3269
+ constructor(clock) {
3270
+ this.#clock = clock;
3271
+ }
3272
+ async record(tenant, input) {
3273
+ const key = tenantKey(tenant, `${input.deviceId}\0${input.payload.eventId}`);
3274
+ const existing = this.#records.get(key);
3275
+ if (existing !== void 0) return { record: existing, created: false };
3276
+ const record = {
3277
+ tenantId: tenant,
3278
+ deviceId: input.deviceId,
3279
+ payload: input.payload,
3280
+ receiptId: input.receiptId,
3281
+ recordedAt: this.#clock.now().toISOString()
3282
+ };
3283
+ this.#records.set(key, record);
3284
+ return { record, created: true };
3285
+ }
3286
+ async get(tenant, deviceId, eventId) {
3287
+ return this.#records.get(tenantKey(tenant, `${deviceId}\0${eventId}`));
3288
+ }
3289
+ };
2842
3290
 
2843
3291
  // src/stores/in-memory/index.ts
2844
- function createInMemoryCloudStores(clock, crypto, objects, mailbox) {
2845
- const blobs = createInMemoryBlobs(clock, crypto, objects);
3292
+ function createInMemoryCloudStores(clock, crypto2, objects, mailbox) {
3293
+ const blobs = createInMemoryBlobs(clock, crypto2, objects);
2846
3294
  const taskState = new InMemoryTaskAttemptState(clock);
2847
3295
  const tasks = new InMemoryTaskAttemptStore(clock, taskState);
2848
3296
  return {
@@ -2851,11 +3299,12 @@ function createInMemoryCloudStores(clock, crypto, objects, mailbox) {
2851
3299
  approvals: new InMemoryApprovalTimelineStore(clock),
2852
3300
  devices: new InMemoryDeviceDirectory(),
2853
3301
  pairingCodes: new InMemoryPairingCodeStore(clock),
2854
- nonces: new InMemoryNonceStore(clock, crypto),
3302
+ nonces: new InMemoryNonceStore(clock, crypto2),
2855
3303
  dedup: new InMemoryInboundDedupStore(),
2856
3304
  tasks,
2857
3305
  cancellations: new InMemoryTaskCancellationStore(taskState, mailbox),
2858
3306
  receipts: new InMemoryRequestReceiptStore(clock),
3307
+ egress: new InMemoryAgentEgressStore(clock),
2859
3308
  proofReceipts: new InMemoryProofRequestReceiptStore(clock),
2860
3309
  blobs: blobs.blobs,
2861
3310
  rateLimiter: new AllowAllRateLimiter()
@@ -2871,11 +3320,11 @@ function systemClock() {
2871
3320
  }
2872
3321
  function createInMemoryByokCloud(options = {}) {
2873
3322
  const clock = options.clock ?? systemClock();
2874
- const crypto = options.crypto ?? createWebCrypto();
3323
+ const crypto2 = options.crypto ?? createWebCrypto();
2875
3324
  const core = createInMemoryCoreStores({ clock }).stores;
2876
3325
  const { stores, blobContentProxy } = createInMemoryCloudStores(
2877
3326
  clock,
2878
- crypto,
3327
+ crypto2,
2879
3328
  core.objects,
2880
3329
  core.mailbox
2881
3330
  );
@@ -2888,7 +3337,7 @@ function createInMemoryByokCloud(options = {}) {
2888
3337
  // pairing is what keeps hosted-in-memory behavior identical to what it was
2889
3338
  // before the port narrowed.
2890
3339
  blobContentProxy,
2891
- crypto,
3340
+ crypto: crypto2,
2892
3341
  tokenSigner,
2893
3342
  clock,
2894
3343
  capabilities: options.capabilities ?? fullCapabilityDeclaration(),
@@ -2917,7 +3366,7 @@ function createInMemoryByokCloud(options = {}) {
2917
3366
  ...options.skillPacks === void 0 ? {} : { skillPacks: options.skillPacks },
2918
3367
  ...options.skillPackPageLimit === void 0 ? {} : { skillPackPageLimit: options.skillPackPageLimit }
2919
3368
  });
2920
- return { cloud, core, stores, blobContentProxy, clock, crypto };
3369
+ return { cloud, core, stores, blobContentProxy, clock, crypto: crypto2 };
2921
3370
  }
2922
3371
  function authenticateHostedDeviceAssertion(input, deps) {
2923
3372
  return authenticateDeviceAssertion(input, {
@@ -3085,6 +3534,7 @@ var TASK_ATTEMPT_STATUSES = [
3085
3534
  "failed",
3086
3535
  "cancelled"
3087
3536
  ];
3537
+ var BLOB_READ_ERROR_CODES = ["blob_upstream_unavailable", "blob_upstream_stream_interrupted"];
3088
3538
  var CLOUD_STORE_NAMES = [
3089
3539
  "activity",
3090
3540
  "approvals",
@@ -3095,6 +3545,7 @@ var CLOUD_STORE_NAMES = [
3095
3545
  "tasks",
3096
3546
  "cancellations",
3097
3547
  "receipts",
3548
+ "egress",
3098
3549
  "proofReceipts",
3099
3550
  "blobs",
3100
3551
  "rateLimiter"
@@ -3104,13 +3555,14 @@ var CLOUD_STORE_NAMES = [
3104
3555
  var CLOUD_PORT_METHODS = {
3105
3556
  activity: ["append", "read"],
3106
3557
  approvals: ["append", "read"],
3107
- devices: ["register", "get", "revoke", "list", "readiness", "resolveByDeviceId"],
3558
+ devices: ["register", "get", "revoke", "list", "readiness", "recordCapabilities", "resolveByDeviceId"],
3108
3559
  pairingCodes: ["issue", "redeem"],
3109
3560
  nonces: ["issue", "validate", "markUsed"],
3110
3561
  dedup: ["checkAndRecord"],
3111
- tasks: ["open", "get", "getMany", "claim", "recordStatus"],
3562
+ tasks: ["open", "reserveAgentOffer", "get", "getMany", "claim", "recordStatus"],
3112
3563
  cancellations: ["request"],
3113
3564
  receipts: ["record", "get"],
3565
+ egress: ["record", "get"],
3114
3566
  proofReceipts: ["record", "get"],
3115
3567
  // Three methods, not six: the byte-proxy trio moved to `BlobContentProxy`,
3116
3568
  // which is a composition input rather than a port and therefore has no row
@@ -3128,11 +3580,12 @@ var CLOUD_PORT_INTERFACES = {
3128
3580
  tasks: "TaskAttemptStore",
3129
3581
  cancellations: "TaskCancellationStore",
3130
3582
  receipts: "RequestReceiptStore",
3583
+ egress: "AgentEgressStore",
3131
3584
  proofReceipts: "ProofRequestReceiptStore",
3132
3585
  blobs: "CloudBlobStore",
3133
3586
  rateLimiter: "InboundRateLimiter"
3134
3587
  };
3135
3588
 
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 };
3589
+ export { ACCESS_TOKEN_TTL_SECONDS, AGENT_HOME_CONTRACT_CAPABILITY, APPROVAL_SUMMARY_MAX_BYTES, ActivityAppendRequestSchema, AllowAllRateLimiter, ApprovalObservationSchema, ApprovalTimelineEventSchema, BLOB_READ_ERROR_CODES, BLOB_URL_TTL_MS, BoardFeedClient, BoardFeedRetryableError, BoardFeedStoppedError, ByokCloudError, CLOUD_CAPABILITIES, CLOUD_ERROR_CODES, CLOUD_PORT_INTERFACES, CLOUD_PORT_METHODS, CLOUD_STORE_NAMES, CapabilitiesResponseSchema, CloudRouteRegistry, DEDUP_RING_CAPACITY, DEFAULT_ACTIVITY_BOUNDS, DEFAULT_ACTIVITY_CAPACITY, DEFAULT_ACTIVITY_MAX_BYTES, DEFAULT_ACTIVITY_MAX_EVENTS, DEFAULT_ACTIVITY_TTL_MS, DEFAULT_APPROVAL_TIMELINE_CAPACITY, DEFAULT_APPROVAL_TIMELINE_TTL_MS, DEFAULT_BOARD_CHANNEL_MAX_BYTES, DEFAULT_BOARD_PAGE_LIMIT, DEFAULT_BOARD_STREAM_HEARTBEAT_INTERVAL_MS, DEFAULT_BOARD_STREAM_QUERY_INTERVAL_MS, DEFAULT_BOARD_STREAM_RECONCILIATION_INTERVAL_MS, DEFAULT_BOARD_TITLE_MAX_BYTES, DEFAULT_DEVICE_PROOF_CLOCK_SKEW_MS, DEFAULT_DEVICE_PROOF_MAX_LIFETIME_MS, DEFAULT_EVENTS_PAGE_LIMIT, DEFAULT_LONG_POLL_HOLD_MS, DEFAULT_LONG_POLL_INTERVAL_MS, DEFAULT_MAX_BLOB_SIZE_BYTES, DEFAULT_MAX_TRUTH_REQUEST_BYTES, DEFAULT_PRESENCE_DETAIL_MAX_BYTES, DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS, DEFAULT_PRESENCE_TTL_MS, DEFAULT_SKILL_PACK_PAGE_LIMIT, DEVICE_IDENTITY_PROOF_KEY_EPOCH, DEVICE_IDENTITY_PROOF_KEY_ID, InMemoryActivityStore, InMemoryAgentEgressStore, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryDeviceDirectory, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryProofRequestReceiptStore, InMemoryRequestReceiptStore, InMemoryTaskAttemptStore, InMemoryTaskCancellationStore, MAX_DEVICE_PROOF_CLOCK_SKEW_MS, MAX_DEVICE_PROOF_HEADER_BYTES, MAX_DEVICE_PROOF_MAX_LIFETIME_MS, NONCE_TTL_MS, PAIRING_CODE_TTL_MS, ROUTE_CLASSES, ROUTE_METHODS, TASK_ATTEMPT_STATUSES, TRUTH_BATCH_MAX_RECORDS, TRUTH_INLINE_CONTENT_TYPE, TRUTH_LABEL_MAX_LENGTH, TRUTH_MANIFEST_MAX_LIMIT, TRUTH_RECORD_CAPABILITY, TRUTH_RECORD_KEY_MAX_LENGTH, TRUTH_REQUEST_ID_MAX_LENGTH, TimelineEventSchema, TruthBodyInputSchema, TruthCommitError, TruthCommitResponseSchema, TruthRecordKeySchema, TruthRecordMetadataSchema, TruthWriteRequestSchema, activityCursor, approvalTimelineCursor, authenticateBearer, authenticateDeviceProof, authenticateHostedDeviceAssertion, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, projectTerminalResult, projectTimelineEvents, routeKey, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, verifyNonceSignature };
3137
3590
  //# sourceMappingURL=index.js.map
3138
3591
  //# sourceMappingURL=index.js.map