@byok-sdk/cloud 0.13.0 → 0.15.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.
@@ -14,12 +14,46 @@ export declare const ActivityAppendRequestSchema: z.ZodObject<{
14
14
  tool: z.ZodString;
15
15
  input: z.ZodOptional<z.ZodUnknown>;
16
16
  toolCallId: z.ZodOptional<z.ZodString>;
17
+ spill: z.ZodOptional<z.ZodObject<{
18
+ field: z.ZodEnum<{
19
+ input: "input";
20
+ output: "output";
21
+ }>;
22
+ totalBytes: z.ZodNumber;
23
+ omittedBytes: z.ZodNumber;
24
+ contentType: z.ZodLiteral<"application/json">;
25
+ blob: z.ZodOptional<z.ZodObject<{
26
+ blobId: z.ZodString;
27
+ contentHash: z.ZodString;
28
+ size: z.ZodNumber;
29
+ contentType: z.ZodString;
30
+ url: z.ZodOptional<z.ZodString>;
31
+ }, z.core.$strip>>;
32
+ unstoredReason: z.ZodOptional<z.ZodString>;
33
+ }, z.core.$strict>>;
17
34
  }, z.core.$strip>, z.ZodObject<{
18
35
  type: z.ZodLiteral<"tool_result">;
19
36
  tool: z.ZodString;
20
37
  output: z.ZodOptional<z.ZodUnknown>;
21
38
  toolCallId: z.ZodOptional<z.ZodString>;
22
39
  isError: z.ZodOptional<z.ZodBoolean>;
40
+ spill: z.ZodOptional<z.ZodObject<{
41
+ field: z.ZodEnum<{
42
+ input: "input";
43
+ output: "output";
44
+ }>;
45
+ totalBytes: z.ZodNumber;
46
+ omittedBytes: z.ZodNumber;
47
+ contentType: z.ZodLiteral<"application/json">;
48
+ blob: z.ZodOptional<z.ZodObject<{
49
+ blobId: z.ZodString;
50
+ contentHash: z.ZodString;
51
+ size: z.ZodNumber;
52
+ contentType: z.ZodString;
53
+ url: z.ZodOptional<z.ZodString>;
54
+ }, z.core.$strip>>;
55
+ unstoredReason: z.ZodOptional<z.ZodString>;
56
+ }, z.core.$strict>>;
23
57
  }, z.core.$strip>, z.ZodObject<{
24
58
  type: z.ZodLiteral<"artifact">;
25
59
  name: z.ZodString;
@@ -59,12 +93,46 @@ export declare const TimelineEventSchema: z.ZodObject<{
59
93
  tool: z.ZodString;
60
94
  input: z.ZodOptional<z.ZodUnknown>;
61
95
  toolCallId: z.ZodOptional<z.ZodString>;
96
+ spill: z.ZodOptional<z.ZodObject<{
97
+ field: z.ZodEnum<{
98
+ input: "input";
99
+ output: "output";
100
+ }>;
101
+ totalBytes: z.ZodNumber;
102
+ omittedBytes: z.ZodNumber;
103
+ contentType: z.ZodLiteral<"application/json">;
104
+ blob: z.ZodOptional<z.ZodObject<{
105
+ blobId: z.ZodString;
106
+ contentHash: z.ZodString;
107
+ size: z.ZodNumber;
108
+ contentType: z.ZodString;
109
+ url: z.ZodOptional<z.ZodString>;
110
+ }, z.core.$strip>>;
111
+ unstoredReason: z.ZodOptional<z.ZodString>;
112
+ }, z.core.$strict>>;
62
113
  }, z.core.$strip>, z.ZodObject<{
63
114
  type: z.ZodLiteral<"tool_result">;
64
115
  tool: z.ZodString;
65
116
  output: z.ZodOptional<z.ZodUnknown>;
66
117
  toolCallId: z.ZodOptional<z.ZodString>;
67
118
  isError: z.ZodOptional<z.ZodBoolean>;
119
+ spill: z.ZodOptional<z.ZodObject<{
120
+ field: z.ZodEnum<{
121
+ input: "input";
122
+ output: "output";
123
+ }>;
124
+ totalBytes: z.ZodNumber;
125
+ omittedBytes: z.ZodNumber;
126
+ contentType: z.ZodLiteral<"application/json">;
127
+ blob: z.ZodOptional<z.ZodObject<{
128
+ blobId: z.ZodString;
129
+ contentHash: z.ZodString;
130
+ size: z.ZodNumber;
131
+ contentType: z.ZodString;
132
+ url: z.ZodOptional<z.ZodString>;
133
+ }, z.core.$strip>>;
134
+ unstoredReason: z.ZodOptional<z.ZodString>;
135
+ }, z.core.$strict>>;
68
136
  }, z.core.$strip>, z.ZodObject<{
69
137
  type: z.ZodLiteral<"artifact">;
70
138
  name: z.ZodString;
package/dist/inbound.d.ts CHANGED
@@ -13,13 +13,12 @@
13
13
  * authenticated long-poll `conn.hello` capability snapshot handled below.
14
14
  * A server -> daemon type arriving inbound, or anything unrecognized, is
15
15
  * rejected before it is dispatched or counted accepted.
16
- * 2. **ownership** — an envelope for a task already owned by a DIFFERENT
17
- * device is dropped, never force-failed: force-failing on an authz mismatch
16
+ * 2. **ownership** — an envelope must name an existing task targeted at this
17
+ * exact device, and a task already owned by a DIFFERENT device is dropped,
18
+ * never force-failed: force-failing on an authz mismatch
18
19
  * would let an attacker who merely guesses a `taskId` kill the real owner's
19
- * task. A task with no owner yet, or that this tenant does not have at all,
20
- * is not rejected here — the store's own no-op-on-missing behavior covers
21
- * the latter, and it covers it per tenant, so a guessed id from another
22
- * tenant writes nothing anywhere.
20
+ * task. Unknown task ids are rejected before any task-scoped projection or
21
+ * dedup fact can be written.
23
22
  * 3. **apply/recover** — every lifecycle side effect is idempotent under the
24
23
  * envelope's own durable identity. A retry after a partial failure resumes
25
24
  * this step rather than being hidden by transport admission.
package/dist/index.js CHANGED
@@ -1,11 +1,30 @@
1
1
  import { BOARD_STATUSES, CapabilityDeclarationSchema, NONCE_SIGNING_DOMAIN, hasCapability, isTenantId, tenantId, principalTenant, parseDeviceProofEnvelope, deviceProofSigningInput, contentHash, parseCapabilityDeclaration, ByokCoreError, tenantKey, PRESENCE_LEVELS, createInMemoryCoreStores, authenticateDeviceAssertion, assertCapability, isCoreConflictError, isCoreError, DEVICE_PROOF_HEADER, TRUTH_RECORD_KINDS, STORAGE_ERROR_CODES, STORAGE_ERROR_HTTP_STATUS } from '@byok-sdk/core';
2
2
  export { DEVICE_PROOF_HEADER, NONCE_SIGNING_DOMAIN, isTenantId, tenantId } from '@byok-sdk/core';
3
- import { AgentEventOrUnknownSchema, PROTOCOL_VERSION, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, encodeEnvelope, decodeEnvelope, AgentHomeProjectionCompletionRequestSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, BYOK_CAPABILITIES_PATH, BYOK_AGENT_MEMORY_PROJECTIONS_PATH, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, BYOK_AGENT_HOME_PROJECTION_COMPLETION_ROUTE, BYOK_BOARD_PATH, BYOK_BOARD_CLAIM_ROUTE, BYOK_BOARD_UNCLAIM_ROUTE, BYOK_BOARD_STATUS_ROUTE, BYOK_BOARD_STREAM_PATH, BYOK_PRESENCE_PATH, BYOK_ACTIVITY_PATH, BYOK_RECORDS_PATH, BYOK_RECORD_ROUTE, BYOK_SKILL_PACKS_PATH, BYOK_SKILL_PACK_FILE_ROUTE, BYOK_BLOBS_PATH, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, createEnvelope, AgentHomeProjectionPayloadSchema, AgentContentReadPayloadSchema, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, TaskOfferForAgentWithEgressFreshPayloadSchema, AgentMessageServerContextSchema, TaskOfferForAgentWithEgressPayloadSchema, TaskOfferForAgentPayloadSchema, AgentMemoryProjectionMutationSchema, AgentMemoryProjectionReceiptSchema, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionEraseResultSchema, STRICT_AGENT_ONLY_CAPABILITY, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AgentRefSchema, PairRequestSchema, PairResponseSchema, ChallengeRequestSchema, TokenRequestSchema, AgentMemoryProjectionCommitRequestSchema, agentMemoryProjectionBase64UrlByteLength, MessagesSendRequestSchema, PresencePublishRequestSchema, CreateBlobRequestSchema, byokBlobContentPath, AgentEgressAckPayloadSchema, AgentContentReceiptPayloadSchema } from '@byok-sdk/protocol';
3
+ import { AgentEventOrUnknownSchema, PROTOCOL_VERSION, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, encodeEnvelope, decodeEnvelope, AgentHomeProjectionCompletionRequestSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, BYOK_CAPABILITIES_PATH, BYOK_AGENT_MEMORY_PROJECTIONS_PATH, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, BYOK_AGENT_HOME_PROJECTION_COMPLETION_ROUTE, BYOK_BOARD_PATH, BYOK_BOARD_CLAIM_ROUTE, BYOK_BOARD_UNCLAIM_ROUTE, BYOK_BOARD_STATUS_ROUTE, BYOK_BOARD_STREAM_PATH, BYOK_PRESENCE_PATH, BYOK_ACTIVITY_PATH, BYOK_RECORDS_PATH, BYOK_RECORD_ROUTE, BYOK_SKILL_PACKS_PATH, BYOK_SKILL_PACK_FILE_ROUTE, BYOK_BLOBS_PATH, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, createEnvelope, AgentHomeProjectionPayloadSchema, AgentContentReadPayloadSchema, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, TaskOfferForAgentWithEgressFreshPayloadSchema, AgentMessageServerContextSchema, TaskOfferForAgentWithEgressPayloadSchema, TaskOfferForAgentPayloadSchema, AgentMemoryProjectionMutationSchema, AgentMemoryProjectionReceiptSchema, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionEraseResultSchema, HarnessIdSchema, STRICT_AGENT_ONLY_CAPABILITY, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AgentRefSchema, PairRequestSchema, PairResponseSchema, ChallengeRequestSchema, TokenRequestSchema, AgentMemoryProjectionCommitRequestSchema, agentMemoryProjectionBase64UrlByteLength, MAILBOX_READ_AHEAD_MAX_SEQS, MAILBOX_READ_AHEAD_CAPABILITY, MessagesSendRequestSchema, PresencePublishRequestSchema, CreateBlobRequestSchema, byokBlobContentPath, AgentEgressAckPayloadSchema, AgentContentReceiptPayloadSchema } from '@byok-sdk/protocol';
4
4
  import { z } from 'zod';
5
5
  import { Hono } from 'hono';
6
6
 
7
7
  // src/index.ts
8
8
 
9
+ // src/offer-identity.ts
10
+ function uuidFromSha256(value) {
11
+ const match = /^sha256:([0-9a-f]{64})$/.exec(value);
12
+ if (match === null) throw new Error("CloudCrypto.sha256 must return canonical sha256 hex");
13
+ const digest = match[1];
14
+ if (digest === void 0) throw new Error("CloudCrypto.sha256 must return canonical sha256 hex");
15
+ const hex = digest.slice(0, 32).split("");
16
+ hex[12] = "8";
17
+ hex[16] = ["8", "9", "a", "b"][Number.parseInt(hex[16], 16) & 3];
18
+ return `${hex.slice(0, 8).join("")}-${hex.slice(8, 12).join("")}-${hex.slice(12, 16).join("")}-${hex.slice(16, 20).join("")}-${hex.slice(20).join("")}`;
19
+ }
20
+ async function taskOfferMessageId(tenant, taskId, deviceId, agentRef, sha256) {
21
+ return uuidFromSha256(await sha256(new TextEncoder().encode(JSON.stringify({ domain: "byok:task-offer", tenant, taskId, deviceId, agentRef }))));
22
+ }
23
+ async function hashOfferBytes(bytes) {
24
+ const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(bytes)));
25
+ return `sha256:${Array.from(hash, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
26
+ }
27
+
9
28
  // src/approval-control.ts
10
29
  var StaleApprovalError = class extends Error {
11
30
  constructor(taskId, requestedApprovalId, currentApprovalId) {
@@ -668,7 +687,8 @@ function tenantStoresFor(principal, root) {
668
687
  revoke: (deviceId) => cloud.devices.revoke(tenant, deviceId),
669
688
  recordCapabilities: (input) => principal.kind === "device" ? cloud.devices.recordCapabilities(tenant, {
670
689
  deviceId: principal.deviceId,
671
- capabilities: input.capabilities
690
+ capabilities: input.capabilities,
691
+ ...input.harnesses === void 0 ? {} : { harnesses: input.harnesses }
672
692
  }) : Promise.resolve(void 0),
673
693
  readiness: () => cloud.devices.readiness(tenant, core.presence)
674
694
  },
@@ -1029,6 +1049,8 @@ function tokenHandler(deps) {
1029
1049
  };
1030
1050
  }
1031
1051
  var CLOUD_PROTOCOL_CAPABILITIES = [
1052
+ "custom-harness",
1053
+ MAILBOX_READ_AHEAD_CAPABILITY,
1032
1054
  "result-document",
1033
1055
  // The kernel's inbound handles `task.approval_resolved` on both the plain
1034
1056
  // and the approval-timeline paths (`packages/cloud/src/inbound.ts:576,610`).
@@ -1065,7 +1087,13 @@ function eventsHandler(deps) {
1065
1087
  }
1066
1088
  cursor = parsedCursor;
1067
1089
  }
1090
+ const afterRaw = c.req.query("afterSeq");
1091
+ const afterSeq = afterRaw === void 0 ? cursor : Number(afterRaw);
1092
+ if (!Number.isSafeInteger(afterSeq) || afterSeq < cursor || afterSeq - cursor > MAILBOX_READ_AHEAD_MAX_SEQS) {
1093
+ return c.json({ error: "invalid read-ahead offset" }, 400);
1094
+ }
1068
1095
  const acked = await stores.mailbox.readCursor(device.deviceId);
1096
+ if (afterRaw !== void 0 && afterSeq > acked.deliveredSeq) return c.json({ error: "read offset exceeds delivered watermark" }, 409);
1069
1097
  if (cursor > acked.ackedSeq) {
1070
1098
  try {
1071
1099
  await stores.mailbox.advanceCursor({ deviceId: device.deviceId, ackedSeq: cursor });
@@ -1078,7 +1106,7 @@ function eventsHandler(deps) {
1078
1106
  }
1079
1107
  const attempts = Math.max(1, Math.ceil(deps.longPollHoldMs / deps.longPollIntervalMs));
1080
1108
  for (let attempt = 0; attempt < attempts; attempt += 1) {
1081
- let scanCursor = cursor;
1109
+ let scanCursor = afterSeq;
1082
1110
  while (true) {
1083
1111
  const page = await stores.mailbox.readAfter({
1084
1112
  deviceId: device.deviceId,
@@ -1122,7 +1150,7 @@ function eventsHandler(deps) {
1122
1150
  }
1123
1151
  const response = {
1124
1152
  events: [],
1125
- cursor,
1153
+ cursor: afterSeq,
1126
1154
  capabilities: CLOUD_PROTOCOL_CAPABILITIES
1127
1155
  };
1128
1156
  return c.json(response, 200);
@@ -1385,6 +1413,7 @@ function agentRefEchoRequired(envelope) {
1385
1413
  return envelope.type === "task.claim" || envelope.type === "task.decline" || envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled";
1386
1414
  }
1387
1415
  async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS, agentMessageConsume, observer) {
1416
+ if (envelope.v !== PROTOCOL_VERSION) return "rejected";
1388
1417
  const outcome = await applyInboundGate(
1389
1418
  stores,
1390
1419
  deviceId,
@@ -1404,12 +1433,13 @@ async function applyInboundGate(stores, deviceId, envelope, activityBounds, agen
1404
1433
  if (!await stores.rateLimiter.consume(deviceId)) return "rate_limited";
1405
1434
  if (envelope.type === "conn.hello") {
1406
1435
  if (envelope.payload.deviceId !== deviceId) return "rejected";
1436
+ if (envelope.payload.harnesses !== void 0 && !envelope.payload.capabilities.includes("custom-harness")) return "rejected";
1407
1437
  if (!envelope.payload.protocolVersions.includes(PROTOCOL_VERSION)) return "rejected";
1408
1438
  const device = await stores.devices.get(deviceId);
1409
1439
  if (device === void 0 || device.revoked || device.productId !== envelope.payload.productId) {
1410
1440
  return "rejected";
1411
1441
  }
1412
- if (await stores.devices.recordCapabilities({ capabilities: envelope.payload.capabilities }) === void 0) {
1442
+ if (await stores.devices.recordCapabilities({ capabilities: envelope.payload.capabilities, ...envelope.payload.harnesses === void 0 ? {} : { harnesses: envelope.payload.harnesses } }) === void 0) {
1413
1443
  return "rejected";
1414
1444
  }
1415
1445
  return completeInboundEnvelope(stores, deviceId, envelope.id, void 0);
@@ -1453,12 +1483,40 @@ async function applyInboundGate(stores, deviceId, envelope, activityBounds, agen
1453
1483
  const taskId = envelope.task_id;
1454
1484
  if (taskId === void 0) return "rejected";
1455
1485
  const attempt = await stores.tasks.get(taskId);
1456
- if (attempt?.agentRef !== void 0 && attempt.deviceId !== deviceId) return "rejected";
1486
+ if (attempt === void 0 || attempt.deviceId !== deviceId) return "rejected";
1457
1487
  if (attempt?.ownerDeviceId !== void 0 && attempt.ownerDeviceId !== deviceId) return "rejected";
1488
+ if (envelope.type === "task.claim" && envelope.payload.deviceId !== deviceId) return "rejected";
1489
+ if (envelope.type === "task.claim") {
1490
+ const harnessId = envelope.payload.harnessId;
1491
+ if (harnessId !== void 0 && envelope.payload.runtime !== void 0) return "rejected";
1492
+ if (attempt.ownerDeviceId !== void 0 && (harnessId !== void 0 || attempt.claimedHarnessId !== void 0) && harnessId !== attempt.claimedHarnessId) return "rejected";
1493
+ const device = await stores.devices.get(deviceId);
1494
+ if (harnessId !== void 0 && (!device?.capabilities?.includes("custom-harness") || !device.harnesses?.some((harness) => harness.id === harnessId))) return "rejected";
1495
+ const messageId = await taskOfferMessageId(stores.tenant, taskId, deviceId, attempt.agentRef, hashOfferBytes);
1496
+ const offer = await stores.receipts.get(`task-offer:${messageId}`);
1497
+ if (offer !== void 0) {
1498
+ const sealed = JSON.parse(offer.body);
1499
+ if (harnessId !== void 0 && (sealed.payload.runtime !== void 0 || sealed.payload.dispatchSelection !== void 0)) return "rejected";
1500
+ if (sealed.payload.harnessId !== void 0 && sealed.payload.harnessId !== harnessId) return "rejected";
1501
+ } else if (harnessId !== void 0) return "rejected";
1502
+ }
1503
+ let unclaimedInterruption = false;
1504
+ if (envelope.type === "task.fail" && envelope.payload.recovery !== void 0) {
1505
+ if (envelope.payload.reason !== "daemon_interrupted" || envelope.payload.retryable !== false) return "rejected";
1506
+ const messageId = await taskOfferMessageId(stores.tenant, taskId, deviceId, attempt.agentRef, hashOfferBytes);
1507
+ if (envelope.payload.recovery.offerId !== messageId) return "rejected";
1508
+ const offer = await stores.receipts.get(`task-offer:${messageId}`);
1509
+ if (offer === void 0) return "rejected";
1510
+ const sealed = JSON.parse(offer.body);
1511
+ const harnessId = envelope.payload.harnessId;
1512
+ if (sealed.payload.harnessId !== void 0 && sealed.payload.harnessId !== harnessId) return "rejected";
1513
+ if (harnessId !== void 0 && (sealed.payload.runtime !== void 0 || sealed.payload.dispatchSelection !== void 0)) return "rejected";
1514
+ unclaimedInterruption = attempt.ownerDeviceId === void 0 && attempt.claimedHarnessId === void 0;
1515
+ }
1516
+ if ((envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled") && !unclaimedInterruption && envelope.payload.harnessId !== attempt.claimedHarnessId) return "rejected";
1458
1517
  if (attempt?.agentRef !== void 0 && agentRefEchoRequired(envelope) && !sameAgentRef(attempt.agentRef, inboundAgentRef(envelope))) {
1459
1518
  return "rejected";
1460
1519
  }
1461
- if (terminalStatus(envelope) !== void 0 && attempt === void 0) return "rejected";
1462
1520
  if (envelope.type === "task.progress" && envelope.payload.events.length > 0) {
1463
1521
  try {
1464
1522
  validateActivityBatch(
@@ -1487,7 +1545,8 @@ async function applyInboundGate(stores, deviceId, envelope, activityBounds, agen
1487
1545
  throw caught;
1488
1546
  }
1489
1547
  }
1490
- await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, attempt?.agentRef);
1548
+ const lifecycleOutcome = await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, attempt?.agentRef);
1549
+ if (lifecycleOutcome === "rejected") return "rejected";
1491
1550
  return completeInboundEnvelope(stores, deviceId, envelope.id, attempt?.agentRef);
1492
1551
  }
1493
1552
  async function completeInboundEnvelope(stores, deviceId, envelopeId, agentRef) {
@@ -1496,14 +1555,17 @@ async function completeInboundEnvelope(stores, deviceId, envelopeId, agentRef) {
1496
1555
  }
1497
1556
  async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, persistedAgentRef) {
1498
1557
  switch (envelope.type) {
1499
- case "task.claim":
1500
- await stores.tasks.claim({
1558
+ case "task.claim": {
1559
+ const claimed = await stores.tasks.claim({
1501
1560
  taskId,
1502
1561
  deviceId,
1503
1562
  ...envelope.payload.runtime === void 0 ? {} : { runtime: envelope.payload.runtime },
1563
+ ...envelope.payload.harnessId === void 0 ? {} : { harnessId: envelope.payload.harnessId },
1504
1564
  ...envelope.payload.capabilities === void 0 ? {} : { capabilities: envelope.payload.capabilities }
1505
1565
  });
1566
+ if (claimed === void 0 || claimed.ownerDeviceId !== deviceId || claimed.claimedRuntime !== envelope.payload.runtime || claimed.claimedHarnessId !== envelope.payload.harnessId) return "rejected";
1506
1567
  return;
1568
+ }
1507
1569
  case "task.started":
1508
1570
  await stores.tasks.recordStatus({
1509
1571
  taskId,
@@ -1587,13 +1649,11 @@ async function recordTerminal(stores, taskId, envelope) {
1587
1649
  }
1588
1650
  const attempt = await stores.tasks.get(taskId);
1589
1651
  if (attempt?.cancellation !== void 0) {
1590
- if (winner.status === "cancelled") {
1591
- await stores.tasks.recordStatus({
1592
- taskId,
1593
- status: "cancelled",
1594
- ...attempt.agentRef === void 0 ? {} : { agentRef: attempt.agentRef }
1595
- });
1596
- }
1652
+ await stores.tasks.recordStatus({
1653
+ taskId,
1654
+ status: "cancelled",
1655
+ ...attempt.agentRef === void 0 ? {} : { agentRef: attempt.agentRef }
1656
+ });
1597
1657
  return;
1598
1658
  }
1599
1659
  const recordedAttempt = await stores.tasks.recordStatus({
@@ -1602,7 +1662,16 @@ async function recordTerminal(stores, taskId, envelope) {
1602
1662
  ...attempt?.agentRef === void 0 ? {} : { agentRef: attempt.agentRef },
1603
1663
  ...winner.cause === void 0 ? {} : { terminalCause: winner.cause }
1604
1664
  });
1605
- if (recordedAttempt?.status !== winner.status || recordedAttempt.cancellation !== void 0) return;
1665
+ if (recordedAttempt?.cancellation !== void 0 || recordedAttempt?.status !== winner.status) {
1666
+ if (recordedAttempt?.cancellation !== void 0) {
1667
+ await stores.tasks.recordStatus({
1668
+ taskId,
1669
+ status: "cancelled",
1670
+ ...recordedAttempt.agentRef === void 0 ? {} : { agentRef: recordedAttempt.agentRef }
1671
+ });
1672
+ }
1673
+ return;
1674
+ }
1606
1675
  await projectTerminalToReview(stores.board, taskId);
1607
1676
  }
1608
1677
  function terminalStatus(envelope) {
@@ -2613,6 +2682,7 @@ function projectTerminalResult(taskId, receipt) {
2613
2682
  sessionRef: envelope.payload.sessionRef,
2614
2683
  ...envelope.payload.artifactRefs !== void 0 ? { artifactRefs: envelope.payload.artifactRefs } : {},
2615
2684
  ...envelope.payload.document !== void 0 ? { document: envelope.payload.document } : {},
2685
+ ...envelope.payload.harnessId === void 0 ? {} : { harnessId: envelope.payload.harnessId },
2616
2686
  ...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
2617
2687
  recordedAt: receipt.recordedAt
2618
2688
  };
@@ -2621,9 +2691,11 @@ function projectTerminalResult(taskId, receipt) {
2621
2691
  taskId,
2622
2692
  state: "failed",
2623
2693
  ...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
2694
+ ...envelope.payload.recovery === void 0 ? {} : { recovery: envelope.payload.recovery },
2624
2695
  reason: envelope.payload.reason,
2625
2696
  terminalCause: envelope.payload.reason,
2626
2697
  ...envelope.payload.retryable !== void 0 ? { retryable: envelope.payload.retryable } : {},
2698
+ ...envelope.payload.harnessId === void 0 ? {} : { harnessId: envelope.payload.harnessId },
2627
2699
  ...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
2628
2700
  recordedAt: receipt.recordedAt
2629
2701
  };
@@ -2649,6 +2721,7 @@ function projectTerminalResult(taskId, receipt) {
2649
2721
  ...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
2650
2722
  ...envelope.payload.reason !== void 0 ? { reason: envelope.payload.reason } : {},
2651
2723
  ...envelope.payload.reason === void 0 ? {} : { terminalCause: envelope.payload.reason },
2724
+ ...envelope.payload.harnessId === void 0 ? {} : { harnessId: envelope.payload.harnessId },
2652
2725
  ...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
2653
2726
  recordedAt: receipt.recordedAt
2654
2727
  };
@@ -2672,16 +2745,6 @@ function isTerminalAttemptStatus(status) {
2672
2745
  function sameAgentRef2(expected, actual) {
2673
2746
  return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
2674
2747
  }
2675
- function uuidFromSha256(value) {
2676
- const match = /^sha256:([0-9a-f]{64})$/.exec(value);
2677
- if (match === null) throw new Error("CloudCrypto.sha256 must return canonical sha256 hex");
2678
- const digest = match[1];
2679
- if (digest === void 0) throw new Error("CloudCrypto.sha256 must return canonical sha256 hex");
2680
- const hex = digest.slice(0, 32).split("");
2681
- hex[12] = "8";
2682
- hex[16] = ["8", "9", "a", "b"][Number.parseInt(hex[16], 16) & 3];
2683
- return `${hex.slice(0, 8).join("")}-${hex.slice(8, 12).join("")}-${hex.slice(12, 16).join("")}-${hex.slice(16, 20).join("")}-${hex.slice(20).join("")}`;
2684
- }
2685
2748
  function sameOfferEnvelope(expected, actual) {
2686
2749
  return expected.id === actual.id && expected.type === actual.type && expected.task_id === actual.task_id && JSON.stringify(expected.payload) === JSON.stringify(actual.payload);
2687
2750
  }
@@ -2888,13 +2951,16 @@ function createByokCloud(options) {
2888
2951
  async function enqueueTaskEnvelope(tenant, deviceId, requestedTaskId, agentRef, buildEnvelope, beforeMailbox) {
2889
2952
  const stores = tenantStoresFor(controlPlane(tenant), root);
2890
2953
  const taskId = requestedTaskId ?? `task_${options.crypto.randomUuid()}`;
2891
- const messageId = uuidFromSha256(
2892
- await options.crypto.sha256(
2893
- new TextEncoder().encode(
2894
- JSON.stringify({ domain: "byok:task-offer", tenant, taskId, deviceId, agentRef })
2895
- )
2896
- )
2897
- );
2954
+ const messageId = await taskOfferMessageId(tenant, taskId, deviceId, agentRef, (bytes) => options.crypto.sha256(bytes));
2955
+ const proposed = buildEnvelope(taskId, 0, messageId);
2956
+ const selection = proposed.payload;
2957
+ if (selection.harnessId !== void 0) {
2958
+ HarnessIdSchema.parse(selection.harnessId);
2959
+ if (selection.runtime !== void 0 || selection.dispatchSelection !== void 0) throw new ByokCloudError("coordination_input_invalid", "harnessId conflicts with built-in runtime selection");
2960
+ await assertAgentCapabilities(tenant, deviceId, ["custom-harness"]);
2961
+ const device = await options.cloud.devices.get(tenant, deviceId);
2962
+ if (!device?.harnesses?.some((harness) => harness.id === selection.harnessId)) throw new ByokCloudError("coordination_input_invalid", "requested custom harness is not advertised by this device");
2963
+ }
2898
2964
  const reservation = agentRef === void 0 ? void 0 : await stores.tasks.reserveAgentOffer({ taskId, deviceId, agentRef });
2899
2965
  const openedBeforeAppend = reservation?.attempt ?? (agentRef === void 0 ? await stores.tasks.open({ taskId, deviceId }) : void 0);
2900
2966
  if (openedBeforeAppend === void 0 || openedBeforeAppend.deviceId !== deviceId || (agentRef === void 0 ? openedBeforeAppend.agentRef !== void 0 : openedBeforeAppend.agentRef === void 0 || !sameAgentRef2(agentRef, openedBeforeAppend.agentRef))) {
@@ -2905,13 +2971,12 @@ function createByokCloud(options) {
2905
2971
  }
2906
2972
  const offerReceiptKey = `task-offer:${messageId}`;
2907
2973
  const deliveryReceiptKey = `task-offer-delivered:${messageId}`;
2908
- if (agentRef !== void 0 && reservation?.created === false && await stores.receipts.get(deliveryReceiptKey) !== void 0) {
2974
+ if (await stores.receipts.get(deliveryReceiptKey) !== void 0) {
2909
2975
  throw new ByokCloudError(
2910
- "agent_task_already_exists",
2911
- `Task ${taskId} already has a durable Agent attempt and cannot be enqueued again.`
2976
+ agentRef === void 0 ? "coordination_input_invalid" : "agent_task_already_exists",
2977
+ `Task ${taskId} already has a durable delivered attempt and cannot be enqueued again.`
2912
2978
  );
2913
2979
  }
2914
- const proposed = buildEnvelope(taskId, 0, messageId);
2915
2980
  const proposedBody = JSON.stringify({
2916
2981
  deviceId,
2917
2982
  type: proposed.type,
@@ -2955,14 +3020,12 @@ function createByokCloud(options) {
2955
3020
  `Task ${taskId} already has a different executable offer body.`
2956
3021
  );
2957
3022
  }
2958
- if (agentRef !== void 0) {
2959
- const delivered = await stores.receipts.record({ key: deliveryReceiptKey, body: messageId });
2960
- if (reservation?.created === false && !delivered.created) {
2961
- throw new ByokCloudError(
2962
- "agent_task_already_exists",
2963
- `Task ${taskId} already has a durable Agent attempt and cannot be enqueued again.`
2964
- );
2965
- }
3023
+ const delivered = await stores.receipts.record({ key: deliveryReceiptKey, body: messageId });
3024
+ if (agentRef !== void 0 && reservation?.created === false && !delivered.created) {
3025
+ throw new ByokCloudError(
3026
+ "agent_task_already_exists",
3027
+ `Task ${taskId} already has a durable Agent attempt and cannot be enqueued again.`
3028
+ );
2966
3029
  }
2967
3030
  return { taskId, seq: message.seq, envelope, attempt: openedBeforeAppend };
2968
3031
  }
@@ -3978,7 +4041,8 @@ var InMemoryDeviceDirectory = class {
3978
4041
  if (record === void 0 || record.revoked) return void 0;
3979
4042
  const updated = {
3980
4043
  ...record,
3981
- capabilities: Object.freeze([...input.capabilities])
4044
+ capabilities: Object.freeze([...input.capabilities]),
4045
+ harnesses: Object.freeze((input.harnesses ?? []).map((value) => Object.freeze({ ...value })))
3982
4046
  };
3983
4047
  this.#byTenant.set(key, updated);
3984
4048
  return updated;
@@ -4355,11 +4419,12 @@ var InMemoryTaskAttemptStore = class {
4355
4419
  return this.#state.mutate(key, () => {
4356
4420
  const existing = this.#state.attempts.get(key);
4357
4421
  if (existing === void 0) return void 0;
4358
- if (existing.ownerDeviceId !== void 0 || existing.cancellation !== void 0 || existing.status !== "offered") return existing;
4422
+ if (existing.deviceId !== input.deviceId || existing.ownerDeviceId !== void 0 || existing.cancellation !== void 0 || existing.status !== "offered") return existing;
4359
4423
  const claimed = {
4360
4424
  ...existing,
4361
4425
  ownerDeviceId: input.deviceId,
4362
4426
  ...input.runtime === void 0 ? {} : { claimedRuntime: input.runtime },
4427
+ ...input.harnessId === void 0 ? {} : { claimedHarnessId: input.harnessId },
4363
4428
  ...input.capabilities === void 0 ? {} : { claimedRuntimeCapabilities: { ...input.capabilities } },
4364
4429
  status: "claimed",
4365
4430
  updatedAt: this.#now()