sealbridge 0.2.2 → 0.3.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.
Files changed (251) hide show
  1. package/README.ja.md +492 -30
  2. package/README.md +522 -109
  3. package/dist/cli/dist/accept.d.ts +13 -0
  4. package/dist/cli/dist/accept.js +115 -0
  5. package/dist/cli/dist/accept.js.map +1 -0
  6. package/dist/cli/dist/approval-summary.d.ts +1 -1
  7. package/dist/cli/dist/board.d.ts +10 -9
  8. package/dist/cli/dist/board.js +62 -31
  9. package/dist/cli/dist/board.js.map +1 -1
  10. package/dist/cli/dist/command-identity.d.ts +6 -0
  11. package/dist/cli/dist/command-identity.js +13 -0
  12. package/dist/cli/dist/command-identity.js.map +1 -0
  13. package/dist/cli/dist/config.d.ts +3 -0
  14. package/dist/cli/dist/config.js +29 -0
  15. package/dist/cli/dist/config.js.map +1 -0
  16. package/dist/cli/dist/connections.d.ts +15 -0
  17. package/dist/cli/dist/connections.js +191 -0
  18. package/dist/cli/dist/connections.js.map +1 -0
  19. package/dist/cli/dist/contacts.d.ts +4 -3
  20. package/dist/cli/dist/contacts.js +39 -28
  21. package/dist/cli/dist/contacts.js.map +1 -1
  22. package/dist/cli/dist/doctor.d.ts +9 -0
  23. package/dist/cli/dist/doctor.js +46 -5
  24. package/dist/cli/dist/doctor.js.map +1 -1
  25. package/dist/cli/dist/error-messages.js +22 -0
  26. package/dist/cli/dist/error-messages.js.map +1 -1
  27. package/dist/cli/dist/index.js +44 -6
  28. package/dist/cli/dist/index.js.map +1 -1
  29. package/dist/cli/dist/invite-output.d.ts +14 -0
  30. package/dist/cli/dist/invite-output.js +66 -0
  31. package/dist/cli/dist/invite-output.js.map +1 -0
  32. package/dist/cli/dist/invite-share.d.ts +6 -3
  33. package/dist/cli/dist/invite-share.js +40 -15
  34. package/dist/cli/dist/invite-share.js.map +1 -1
  35. package/dist/cli/dist/invites-migrate.d.ts +3 -0
  36. package/dist/cli/dist/invites-migrate.js +33 -0
  37. package/dist/cli/dist/invites-migrate.js.map +1 -0
  38. package/dist/cli/dist/mcp.d.ts +7 -5
  39. package/dist/cli/dist/mcp.js +23 -18
  40. package/dist/cli/dist/mcp.js.map +1 -1
  41. package/dist/cli/dist/network.d.ts +11 -1
  42. package/dist/cli/dist/network.js +150 -87
  43. package/dist/cli/dist/network.js.map +1 -1
  44. package/dist/cli/dist/pending.d.ts +8 -0
  45. package/dist/cli/dist/pending.js +50 -0
  46. package/dist/cli/dist/pending.js.map +1 -0
  47. package/dist/cli/dist/public-settings.d.ts +5 -4
  48. package/dist/cli/dist/public-settings.js +13 -15
  49. package/dist/cli/dist/public-settings.js.map +1 -1
  50. package/dist/cli/dist/receipt.d.ts +1 -1
  51. package/dist/cli/dist/relay-auth.d.ts +3 -0
  52. package/dist/cli/dist/relay-auth.js +23 -0
  53. package/dist/cli/dist/relay-auth.js.map +1 -0
  54. package/dist/cli/dist/requests.d.ts +10 -0
  55. package/dist/cli/dist/requests.js +91 -0
  56. package/dist/cli/dist/requests.js.map +1 -0
  57. package/dist/cli/dist/sdk.d.ts +10 -0
  58. package/dist/cli/dist/sdk.js +7 -0
  59. package/dist/cli/dist/sdk.js.map +1 -0
  60. package/dist/cli/dist/setup-files.d.ts +24 -0
  61. package/dist/cli/dist/setup-files.js +190 -0
  62. package/dist/cli/dist/setup-files.js.map +1 -0
  63. package/dist/cli/dist/setup-hosts.d.ts +29 -0
  64. package/dist/cli/dist/setup-hosts.js +75 -0
  65. package/dist/cli/dist/setup-hosts.js.map +1 -0
  66. package/dist/cli/dist/setup.d.ts +61 -0
  67. package/dist/cli/dist/setup.js +243 -0
  68. package/dist/cli/dist/setup.js.map +1 -0
  69. package/dist/cli/dist/task-input.d.ts +1 -1
  70. package/dist/cli/dist/wake-hook.d.ts +1 -1
  71. package/dist/cli/dist/worker-json.d.ts +4 -0
  72. package/dist/cli/dist/worker-json.js +87 -0
  73. package/dist/cli/dist/worker-json.js.map +1 -0
  74. package/dist/examples/two-owner-demo/dist/run-demo.d.ts +13 -13
  75. package/dist/package.json +1 -1
  76. package/dist/packages/adapters/dist/command.d.ts +2 -2
  77. package/dist/packages/adapters/dist/credential-proxy.d.ts +2 -2
  78. package/dist/packages/adapters/dist/index.d.ts +1 -1
  79. package/dist/packages/adapters/dist/mcp.d.ts +2 -2
  80. package/dist/packages/adapters/dist/openai.d.ts +2 -2
  81. package/dist/packages/adapters/dist/sandbox.d.ts +2 -2
  82. package/dist/packages/core/dist/connection.d.ts +58 -0
  83. package/dist/packages/core/dist/connection.js +132 -0
  84. package/dist/packages/core/dist/connection.js.map +1 -0
  85. package/dist/packages/core/dist/delivery-status.d.ts +22 -0
  86. package/dist/packages/core/dist/delivery-status.js +53 -0
  87. package/dist/packages/core/dist/delivery-status.js.map +1 -0
  88. package/dist/packages/core/dist/discovery.d.ts +7 -6
  89. package/dist/packages/core/dist/discovery.js +19 -20
  90. package/dist/packages/core/dist/discovery.js.map +1 -1
  91. package/dist/packages/core/dist/edge.d.ts +1 -0
  92. package/dist/packages/core/dist/edge.js +1 -0
  93. package/dist/packages/core/dist/edge.js.map +1 -1
  94. package/dist/packages/core/dist/index.d.ts +3 -0
  95. package/dist/packages/core/dist/index.js +3 -0
  96. package/dist/packages/core/dist/index.js.map +1 -1
  97. package/dist/packages/core/dist/protocol.d.ts +24 -0
  98. package/dist/packages/core/dist/protocol.js +54 -0
  99. package/dist/packages/core/dist/protocol.js.map +1 -0
  100. package/dist/packages/core/dist/receipts.d.ts +1 -0
  101. package/dist/packages/core/dist/receipts.js +1 -1
  102. package/dist/packages/core/dist/receipts.js.map +1 -1
  103. package/dist/packages/core/dist/schema.d.ts +1 -0
  104. package/dist/packages/core/dist/schema.js +13 -2
  105. package/dist/packages/core/dist/schema.js.map +1 -1
  106. package/dist/packages/ledger/dist/index.d.ts +1 -1
  107. package/dist/packages/relay/dist/admission.d.ts +14 -0
  108. package/dist/packages/relay/dist/admission.js +70 -0
  109. package/dist/packages/relay/dist/admission.js.map +1 -0
  110. package/dist/packages/relay/dist/fetch.d.ts +8 -2
  111. package/dist/packages/relay/dist/fetch.js +41 -12
  112. package/dist/packages/relay/dist/fetch.js.map +1 -1
  113. package/dist/packages/relay/dist/index.d.ts +23 -12
  114. package/dist/packages/relay/dist/index.js +130 -78
  115. package/dist/packages/relay/dist/index.js.map +1 -1
  116. package/dist/packages/relay/dist/journal.d.ts +1 -1
  117. package/dist/packages/relay/dist/notifications.d.ts +1 -1
  118. package/dist/packages/relay/dist/retry.d.ts +23 -0
  119. package/dist/packages/relay/dist/retry.js +43 -1
  120. package/dist/packages/relay/dist/retry.js.map +1 -1
  121. package/dist/packages/relay/dist/ssrf.d.ts +3 -2
  122. package/dist/packages/relay/dist/ssrf.js.map +1 -1
  123. package/dist/packages/relay/dist/worker.js +1 -1
  124. package/dist/packages/relay/dist/worker.js.map +1 -1
  125. package/dist/packages/runtime/dist/a2a.d.ts +1 -1
  126. package/dist/packages/runtime/dist/a2a.js +1 -1
  127. package/dist/packages/runtime/dist/adapter.d.ts +2 -2
  128. package/dist/packages/runtime/dist/application-config.d.ts +20 -0
  129. package/dist/packages/runtime/dist/application-config.js +222 -0
  130. package/dist/packages/runtime/dist/application-config.js.map +1 -0
  131. package/dist/packages/runtime/dist/approval-providers.d.ts +24 -3
  132. package/dist/packages/runtime/dist/approval-providers.js +79 -3
  133. package/dist/packages/runtime/dist/approval-providers.js.map +1 -1
  134. package/dist/packages/runtime/dist/connection-admission-review.d.ts +39 -0
  135. package/dist/packages/runtime/dist/connection-admission-review.js +86 -0
  136. package/dist/packages/runtime/dist/connection-admission-review.js.map +1 -0
  137. package/dist/packages/runtime/dist/connection-persistence.d.ts +35 -0
  138. package/dist/packages/runtime/dist/connection-persistence.js +181 -0
  139. package/dist/packages/runtime/dist/connection-persistence.js.map +1 -0
  140. package/dist/packages/runtime/dist/connection-records.d.ts +167 -0
  141. package/dist/packages/runtime/dist/connection-records.js +132 -0
  142. package/dist/packages/runtime/dist/connection-records.js.map +1 -0
  143. package/dist/packages/runtime/dist/connection-router.d.ts +45 -0
  144. package/dist/packages/runtime/dist/connection-router.js +130 -0
  145. package/dist/packages/runtime/dist/connection-router.js.map +1 -0
  146. package/dist/packages/runtime/dist/connection-service.d.ts +159 -0
  147. package/dist/packages/runtime/dist/connection-service.js +239 -0
  148. package/dist/packages/runtime/dist/connection-service.js.map +1 -0
  149. package/dist/packages/runtime/dist/connection-transport.d.ts +14 -0
  150. package/dist/packages/runtime/dist/connection-transport.js +89 -0
  151. package/dist/packages/runtime/dist/connection-transport.js.map +1 -0
  152. package/dist/packages/runtime/dist/delivery-records.d.ts +180 -0
  153. package/dist/packages/runtime/dist/delivery-records.js +205 -0
  154. package/dist/packages/runtime/dist/delivery-records.js.map +1 -0
  155. package/dist/packages/runtime/dist/delivery-status-router.d.ts +27 -0
  156. package/dist/packages/runtime/dist/delivery-status-router.js +150 -0
  157. package/dist/packages/runtime/dist/delivery-status-router.js.map +1 -0
  158. package/dist/packages/runtime/dist/delivery-status.d.ts +43 -0
  159. package/dist/packages/runtime/dist/delivery-status.js +193 -0
  160. package/dist/packages/runtime/dist/delivery-status.js.map +1 -0
  161. package/dist/packages/runtime/dist/delivery-store.d.ts +125 -0
  162. package/dist/packages/runtime/dist/delivery-store.js +1047 -0
  163. package/dist/packages/runtime/dist/delivery-store.js.map +1 -0
  164. package/dist/packages/runtime/dist/gateway.d.ts +4 -3
  165. package/dist/packages/runtime/dist/gateway.js.map +1 -1
  166. package/dist/packages/runtime/dist/held-request-review.d.ts +49 -0
  167. package/dist/packages/runtime/dist/held-request-review.js +122 -0
  168. package/dist/packages/runtime/dist/held-request-review.js.map +1 -0
  169. package/dist/packages/runtime/dist/held-request-store.d.ts +112 -0
  170. package/dist/packages/runtime/dist/held-request-store.js +290 -0
  171. package/dist/packages/runtime/dist/held-request-store.js.map +1 -0
  172. package/dist/packages/runtime/dist/idempotency-store.d.ts +53 -0
  173. package/dist/packages/runtime/dist/idempotency-store.js +331 -0
  174. package/dist/packages/runtime/dist/idempotency-store.js.map +1 -0
  175. package/dist/packages/runtime/dist/inbox.d.ts +1 -1
  176. package/dist/packages/runtime/dist/index.d.ts +27 -1
  177. package/dist/packages/runtime/dist/index.js +26 -0
  178. package/dist/packages/runtime/dist/index.js.map +1 -1
  179. package/dist/packages/runtime/dist/invite-store.d.ts +76 -0
  180. package/dist/packages/runtime/dist/invite-store.js +337 -0
  181. package/dist/packages/runtime/dist/invite-store.js.map +1 -0
  182. package/dist/packages/runtime/dist/key-store.d.ts +77 -0
  183. package/dist/packages/runtime/dist/key-store.js +226 -0
  184. package/dist/packages/runtime/dist/key-store.js.map +1 -0
  185. package/dist/packages/runtime/dist/local-identity.d.ts +33 -0
  186. package/dist/packages/runtime/dist/local-identity.js +278 -0
  187. package/dist/packages/runtime/dist/local-identity.js.map +1 -0
  188. package/dist/packages/runtime/dist/mcp-rpc.d.ts +19 -0
  189. package/dist/packages/runtime/dist/mcp-rpc.js +175 -0
  190. package/dist/packages/runtime/dist/mcp-rpc.js.map +1 -0
  191. package/dist/packages/runtime/dist/mcp-server.d.ts +46 -19
  192. package/dist/packages/runtime/dist/mcp-server.js +427 -184
  193. package/dist/packages/runtime/dist/mcp-server.js.map +1 -1
  194. package/dist/packages/runtime/dist/mcp-tool-catalog.d.ts +1 -1
  195. package/dist/packages/runtime/dist/mcp-tool-catalog.js +172 -13
  196. package/dist/packages/runtime/dist/mcp-tool-catalog.js.map +1 -1
  197. package/dist/packages/runtime/dist/mcp.d.ts +2 -2
  198. package/dist/packages/runtime/dist/message-status.d.ts +32 -0
  199. package/dist/packages/runtime/dist/message-status.js +29 -0
  200. package/dist/packages/runtime/dist/message-status.js.map +1 -0
  201. package/dist/packages/runtime/dist/outbox.d.ts +91 -0
  202. package/dist/packages/runtime/dist/outbox.js +462 -0
  203. package/dist/packages/runtime/dist/outbox.js.map +1 -0
  204. package/dist/packages/runtime/dist/policy.d.ts +1 -1
  205. package/dist/packages/runtime/dist/protocol-peer-store.d.ts +40 -0
  206. package/dist/packages/runtime/dist/protocol-peer-store.js +176 -0
  207. package/dist/packages/runtime/dist/protocol-peer-store.js.map +1 -0
  208. package/dist/packages/runtime/dist/protocol-release.d.ts +26 -0
  209. package/dist/packages/runtime/dist/protocol-release.js +76 -0
  210. package/dist/packages/runtime/dist/protocol-release.js.map +1 -0
  211. package/dist/packages/runtime/dist/quota.d.ts +1 -1
  212. package/dist/packages/runtime/dist/relay-selection.d.ts +20 -0
  213. package/dist/packages/runtime/dist/relay-selection.js +34 -0
  214. package/dist/packages/runtime/dist/relay-selection.js.map +1 -0
  215. package/dist/packages/runtime/dist/requester-pending-store.d.ts +59 -0
  216. package/dist/packages/runtime/dist/requester-pending-store.js +266 -0
  217. package/dist/packages/runtime/dist/requester-pending-store.js.map +1 -0
  218. package/dist/packages/runtime/dist/secret-store.d.ts +19 -0
  219. package/dist/packages/runtime/dist/secret-store.js +51 -0
  220. package/dist/packages/runtime/dist/secret-store.js.map +1 -0
  221. package/dist/packages/runtime/dist/session-audit.d.ts +29 -7
  222. package/dist/packages/runtime/dist/session-audit.js +149 -29
  223. package/dist/packages/runtime/dist/session-audit.js.map +1 -1
  224. package/dist/packages/runtime/dist/session.d.ts +101 -9
  225. package/dist/packages/runtime/dist/session.js +696 -96
  226. package/dist/packages/runtime/dist/session.js.map +1 -1
  227. package/dist/packages/runtime/dist/task-journal.d.ts +18 -3
  228. package/dist/packages/runtime/dist/task-journal.js +271 -18
  229. package/dist/packages/runtime/dist/task-journal.js.map +1 -1
  230. package/dist/packages/runtime/dist/verifier.d.ts +3 -2
  231. package/dist/packages/runtime/dist/verifier.js +1 -0
  232. package/dist/packages/runtime/dist/verifier.js.map +1 -1
  233. package/dist/packages/runtime/dist/worker-trust-store.d.ts +111 -0
  234. package/dist/packages/runtime/dist/worker-trust-store.js +592 -0
  235. package/dist/packages/runtime/dist/worker-trust-store.js.map +1 -0
  236. package/dist/packages/sandbox/dist/container.d.ts +1 -1
  237. package/dist/packages/sandbox/dist/index.d.ts +1 -1
  238. package/dist/spec/README.ja.md +21 -1
  239. package/dist/spec/README.md +20 -1
  240. package/dist/spec/agent-card.schema.json +11 -1
  241. package/dist/spec/delivery-status.schema.json +106 -0
  242. package/dist/spec/examples/protocol-versions/sample-agent-card.json +35 -0
  243. package/dist/spec/examples/sample-delivery-status.json +21 -0
  244. package/dist/spec/examples/sample-task-manifest.0.1.0.json +173 -0
  245. package/dist/spec/examples/sample-task-manifest.0.2.0.json +173 -0
  246. package/dist/spec/task-manifest.0.1.0.schema.json +422 -0
  247. package/dist/spec/task-manifest.0.2.0.schema.json +422 -0
  248. package/dist/spec/task-manifest.schema.json +9 -418
  249. package/docs/sdk.ja.md +133 -0
  250. package/docs/sdk.md +138 -0
  251. package/package.json +15 -3
@@ -1,27 +1,38 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
+ import { IdempotencyStore, runtimeIdempotencyAction, unwrapIdempotency, validateIdempotencyKey } from './idempotency-store.js';
3
+ import { RecipientDeliveryCoordinator } from './delivery-status.js';
4
+ import { setDeliveryStatusReaderFactory } from './delivery-status-router.js';
5
+ import { PENDING_NEXT_STEP, pendingErrorCode } from './requester-pending-store.js';
2
6
  import { mkdtemp, rm } from 'node:fs/promises';
3
7
  import { consumeDailyTask } from './daily-limit.js';
4
8
  import { existsSync, openSync, closeSync, fstatSync, fchmodSync, appendFileSync, constants, fsyncSync, readFileSync, } from 'node:fs';
5
9
  import { join } from 'node:path';
6
10
  import { tmpdir } from 'node:os';
7
11
  import { randomUUID } from 'node:crypto';
8
- import { createContract, createCapability, createSignedAgentCard, createInviteToken, parseAndVerifyInviteToken, isInviteTarget, validateRelayEndpointUrl, signPayload, verifySigned, verifyEnvelope, verifyContract, verifyApproval, validateManifest, validateOutput, verifyCapabilityChain, capabilityIssuerControls, findDiscoveredAgentCard, discoverAgentCards, schemaNarrows, digest, openEnvelope, resolveDid, createRevocationNotice, verifyRevocationNotice, RevocationRegistry, signEnvelope, DEFAULT_CLOCK_SKEW_MS, OPEN_POLICY_FIELDS, } from '../../core/dist/index.js';
12
+ import { createContract, createCapability, createSignedAgentCard, createInviteToken, parseAndVerifyInviteToken, isInviteTarget, validateRelayEndpointUrl, signPayload, verifySigned, verifyEnvelope, verifyContract, verifyApproval, validateManifest, validateOutput, verifyCapabilityChain, capabilityIssuerControls, findDiscoveredAgentCard, discoverAgentCards, schemaNarrows, digest, openEnvelope, resolveDid, createRevocationNotice, verifyRevocationNotice, RevocationRegistry, signEnvelope, DEFAULT_CLOCK_SKEW_MS, assertConnectionsOptions, isConnectionEvent, } from '../../core/dist/index.js';
9
13
  import { parseJsonl, verifyAudit } from '../../ledger/dist/index.js';
10
- import { HttpRelayClient, waitForRelayRetry, relayDiagnostic, relayDeadline } from '../../relay/dist/index.js';
14
+ import { HttpRelayClient, waitForRelayRetry, relayDiagnostic, relayDeadline, validateRelayAuthRecoveryOptions } from '../../relay/dist/index.js';
11
15
  import { InteractiveCliApprovalProvider, ProposalRejectedError, evaluateContractAcceptance, } from './approval-providers.js';
12
16
  import { OwnerGateway } from './gateway.js';
13
17
  import { MCPProxy } from './mcp.js';
14
18
  import { VerificationEngine, createDelivery } from './verifier.js';
15
19
  import { asJson, receiveRelay, acknowledgeRelay, PeerRejectedError, SessionWire } from './session-audit.js';
16
20
  import { SessionTimeouts, validateSessionTimeouts, resolveSessionTimeouts } from './session-timeouts.js';
21
+ import { WorkerTrustStore, workerIntakePath, PENDING_MESSAGE } from './worker-trust-store.js';
22
+ import { LiveProtocol } from './protocol-release.js';
23
+ import { HeldRequestReview } from './held-request-review.js';
24
+ import { ConnectionRouter } from './connection-router.js';
25
+ import { connectionIdFor } from './connection-service.js';
26
+ import { openConnectionFrame } from './connection-transport.js';
17
27
  import { TaskJournal } from './task-journal.js';
28
+ import { GatewayDeliveryService } from './outbox.js';
29
+ import { deliveryError, receiptFileId } from './delivery-records.js';
18
30
  const policy = { requiredGates: [], autoApprove: [] };
19
31
  /** Daemon workers stay finite unless the owner raises the cap. */
20
32
  export const DEFAULT_MAX_CONCURRENT_TASKS = 8;
21
33
  function acceptanceFrom(options) {
22
34
  return {
23
35
  allowedRequesterDids: options.allowedRequesterDids,
24
- allowAnyRequester: options.allowAnyRequester,
25
36
  maxInputTokens: options.maxInputTokens,
26
37
  maxOutputTokens: options.maxOutputTokens,
27
38
  maxToolInvocations: options.maxToolInvocations,
@@ -55,6 +66,12 @@ function validateAcceptance(acceptance) {
55
66
  if (acceptance.allowedRequesterDids?.some((did) => typeof did !== 'string' || did.trim() === ''))
56
67
  throw new Error('INVALID_REQUESTER_DID');
57
68
  }
69
+ function deliveryPins(relay) {
70
+ return {
71
+ ...(relay.getDeliveryStatusPin ? { getDeliveryStatusPin: (id) => relay.getDeliveryStatusPin(id) } : {}),
72
+ ...(relay.restoreDeliveryStatusPin ? { restoreDeliveryStatusPin: (id, did) => relay.restoreDeliveryStatusPin(id, did) } : {}),
73
+ };
74
+ }
58
75
  function transport(relay, owner, security) {
59
76
  const client = typeof relay === 'string'
60
77
  ? new HttpRelayClient(relay.replace(/\/$/, ''), owner, security)
@@ -62,9 +79,12 @@ function transport(relay, owner, security) {
62
79
  client.setRecipientKey?.(owner);
63
80
  const bound = client.withRecipient?.(owner) ?? client;
64
81
  return {
82
+ ...deliveryPins(bound),
65
83
  get connectionStatus() { return bound.connectionStatus; },
66
84
  send: (envelope) => relayDeadline(Promise.resolve(bound.send(envelope)), security?.requestTimeoutMs ?? 45_000),
67
85
  receive: async (did, waitMs, signal, filter) => {
86
+ if (bound.managesRequestDeadlines)
87
+ return bound.receive(did, waitMs, signal, filter);
68
88
  const controller = new AbortController();
69
89
  const readSignal = AbortSignal.any([controller.signal, ...(signal ? [signal] : [])]);
70
90
  try {
@@ -75,8 +95,10 @@ function transport(relay, owner, security) {
75
95
  throw error;
76
96
  }
77
97
  },
78
- ...(bound.acknowledgeDelivery ? { acknowledgeDelivery: (did, id) => relayDeadline(Promise.resolve(bound.acknowledgeDelivery(did, id)), security?.requestTimeoutMs ?? 45_000) } : {}),
79
- ...(bound.deliveryStatus ? { deliveryStatus: (envelope, signal) => relayDeadline(Promise.resolve(bound.deliveryStatus(envelope, signal)), security?.requestTimeoutMs ?? 45_000, signal) } : {}),
98
+ ...(bound.acknowledgeDelivery ? { acknowledgeDelivery: (did, id, signal) => bound.managesRequestDeadlines ? bound.acknowledgeDelivery(did, id, signal) :
99
+ relayDeadline(Promise.resolve(bound.acknowledgeDelivery(did, id, signal)), security?.requestTimeoutMs ?? 45_000, signal) } : {}),
100
+ ...(bound.deliveryStatus ? { deliveryStatus: (envelope, signal) => bound.managesRequestDeadlines
101
+ ? bound.deliveryStatus(envelope, signal) : relayDeadline(Promise.resolve(bound.deliveryStatus(envelope, signal)), security?.requestTimeoutMs ?? 45_000, signal) } : {}),
80
102
  ...(bound.register ? { register: (card) => bound.register(card) } : {}),
81
103
  ...(bound.discover ? { discover: () => relayDeadline(Promise.resolve(bound.discover()), security?.requestTimeoutMs ?? 45_000) } : {}),
82
104
  };
@@ -135,9 +157,10 @@ class TaskMailbox {
135
157
  }
136
158
  function mailboxRelay(relay, mailbox) {
137
159
  return {
160
+ ...deliveryPins(relay),
138
161
  send: (envelope) => relay.send(envelope),
139
162
  ...(relay.deliveryStatus ? { deliveryStatus: (envelope, signal) => relay.deliveryStatus(envelope, signal) } : {}),
140
- ...(relay.acknowledgeDelivery ? { acknowledgeDelivery: (did, id) => relay.acknowledgeDelivery(did, id) } : {}),
163
+ ...(relay.acknowledgeDelivery ? { acknowledgeDelivery: (did, id, signal) => relay.acknowledgeDelivery(did, id, signal) } : {}),
141
164
  receive: (_recipientDid, _waitMs, signal) => mailbox.take(signal),
142
165
  };
143
166
  }
@@ -217,6 +240,23 @@ class RecipientInbox {
217
240
  readAbort;
218
241
  started = false;
219
242
  error;
243
+ connections;
244
+ delivery;
245
+ setDelivery(service, url, active) {
246
+ this.delivery = { service, url, active };
247
+ this.readAbort?.abort();
248
+ this.wake?.();
249
+ this.wake = undefined;
250
+ if (active && !this.started) {
251
+ this.started = true;
252
+ registerRequesterInbox(this.did, this);
253
+ void this.pump();
254
+ }
255
+ }
256
+ statusIds() {
257
+ return this.delivery && (this.delivery.active || this.mailboxes.size > 0) ? this.delivery.service.statusRouter.ids(this.delivery.url) : [];
258
+ }
259
+ setConnections(router) { this.connections = router; this.readAbort?.abort(); }
220
260
  constructor(relay, did) {
221
261
  this.relay = relay;
222
262
  this.did = did;
@@ -225,6 +265,10 @@ class RecipientInbox {
225
265
  return this.mailboxes.has(taskId);
226
266
  }
227
267
  deliverFromPeerRole(envelope) {
268
+ if (envelope.payload.eventType === 'DELIVERY_STATUS' && this.delivery) {
269
+ void this.delivery.service.statusRouter.route(envelope, this.relay).catch(e => this.delivery?.service.options.onError?.(e));
270
+ return true;
271
+ }
228
272
  const mailbox = this.mailboxes.get(envelope.payload.taskId);
229
273
  if (!mailbox)
230
274
  return false;
@@ -239,6 +283,8 @@ class RecipientInbox {
239
283
  return existing;
240
284
  const mailbox = new TaskMailbox();
241
285
  this.mailboxes.set(taskId, mailbox);
286
+ if (this.delivery || this.connections)
287
+ this.readAbort?.abort();
242
288
  registerRequesterInbox(this.did, this);
243
289
  this.wake?.();
244
290
  this.wake = undefined;
@@ -266,9 +312,9 @@ class RecipientInbox {
266
312
  }
267
313
  async pump() {
268
314
  for (;;) {
269
- if (this.mailboxes.size === 0) {
315
+ if (this.mailboxes.size === 0 && this.statusIds().length === 0) {
270
316
  await new Promise((resolve) => {
271
- if (this.mailboxes.size > 0) {
317
+ if (this.mailboxes.size > 0 || this.statusIds().length > 0) {
272
318
  resolve();
273
319
  return;
274
320
  }
@@ -279,9 +325,27 @@ class RecipientInbox {
279
325
  const controller = new AbortController();
280
326
  this.readAbort = controller;
281
327
  try {
282
- const envelope = await receiveRelay(this.relay, this.did, controller.signal, {
283
- role: 'requester',
284
- });
328
+ const statusIds = this.statusIds();
329
+ const readStatus = async () => statusIds.length ? await this.relay.receive(this.did, 0, controller.signal, { taskId: statusIds, lease: this.relay.acknowledgeDelivery !== undefined }) : undefined;
330
+ // Read the independent status namespace without a role filter so existing relays
331
+ // can forward it. Task reads retain bilateral role inference, including self-delegation.
332
+ const envelope = await readStatus() ?? await receiveRelay(this.relay, this.did, controller.signal, this.mailboxes.size === 0 ? { taskId: statusIds } : this.connections ? {
333
+ taskId: [...this.mailboxes.keys(), ...this.connections.options.store.controlSnapshot()
334
+ .filter(r => r.active).map(r => 'conn_' + r.offer.requestId)],
335
+ } : this.delivery ? { role: 'requester', taskId: [...this.mailboxes.keys()] } : { role: 'requester' }, undefined, readStatus);
336
+ if (envelope.payload.eventType === 'DELIVERY_STATUS' && this.delivery) {
337
+ try {
338
+ await this.delivery.service.statusRouter.route(envelope, this.relay);
339
+ }
340
+ catch (error) {
341
+ this.delivery.service.options.onError?.(error);
342
+ }
343
+ continue;
344
+ }
345
+ if (isConnectionEvent(envelope.payload.eventType) && this.connections) {
346
+ await this.connections.route(envelope);
347
+ continue;
348
+ }
285
349
  if (!this.mailboxes.has(envelope.payload.taskId) || this.isWorkerBoundFrame(envelope)) {
286
350
  if (forwardToWorkerListener(this.did, envelope))
287
351
  continue;
@@ -318,6 +382,9 @@ function recipientInbox(source, relay, did, relayUrl) {
318
382
  }
319
383
  return inbox;
320
384
  }
385
+ setDeliveryStatusReaderFactory((service, relay, url, active) => {
386
+ recipientInbox(service, relay, service.owner.did, url.replace(/\/$/, '')).setDelivery(service, url, active);
387
+ });
321
388
  /** Checks every backing list. Inbox notices and an owner-supplied registry both apply. */
322
389
  class CompositeRevocation {
323
390
  layers;
@@ -355,10 +422,18 @@ function asRevocationRegistry(list) {
355
422
  }
356
423
  export class WorkerNode {
357
424
  options;
425
+ deliveryService;
358
426
  relay;
359
427
  agentCard;
360
428
  approvalProvider;
361
429
  acceptance;
430
+ intakeStore;
431
+ connectionStore;
432
+ heldReview;
433
+ connections;
434
+ protocol;
435
+ acceptedPeers = new Map();
436
+ registered = false;
362
437
  completed = 0;
363
438
  activeTasks = 0;
364
439
  taskJournal;
@@ -379,7 +454,18 @@ export class WorkerNode {
379
454
  revocation;
380
455
  constructor(options) {
381
456
  this.options = options;
457
+ assertConnectionsOptions(options);
458
+ validateRelayAuthRecoveryOptions(options);
382
459
  validateSessionTimeouts(options);
460
+ this.protocol = new LiveProtocol(options);
461
+ this.intakeStore = options.intakeStore ?? new WorkerTrustStore(options.owner, {
462
+ path: options.identityOptions ? workerIntakePath(options.owner.did, options.identityOptions) : undefined,
463
+ fs: options.identityOptions?.fs, platform: options.identityOptions?.platform, now: options.now
464
+ });
465
+ this.connectionStore = options.connectionStore ?? (this.intakeStore instanceof WorkerTrustStore ? this.intakeStore : new WorkerTrustStore(options.owner, { now: options.now }));
466
+ if (this.connectionStore.ownerDid !== options.owner.did)
467
+ throw new Error('CONNECTION_OWNER_MISMATCH');
468
+ this.heldReview = options.heldReview ?? new HeldRequestReview(options.owner, { ...options.identityOptions, now: options.now });
383
469
  if (options.inviteTtlSeconds !== undefined && (!Number.isSafeInteger(options.inviteTtlSeconds) ||
384
470
  options.inviteTtlSeconds < 1 || options.inviteTtlSeconds > 2592000))
385
471
  throw new Error('INVALID_INVITE_TTL');
@@ -388,8 +474,6 @@ export class WorkerNode {
388
474
  this.taskJournal = new TaskJournal(persistencePath + '.tasks.jsonl', options.owner);
389
475
  if (options.dailyLimit && (!options.dailyLimit.path || !Number.isSafeInteger(options.dailyLimit.maxTasks) || options.dailyLimit.maxTasks < 1))
390
476
  throw new Error('INVALID_DAILY_LIMIT');
391
- if (options.accessMode !== undefined && !['invite-only', 'open'].includes(options.accessMode))
392
- throw new Error('INVALID_ACCESS_MODE');
393
477
  this.acceptance = acceptanceFrom(options);
394
478
  validateAcceptance(this.acceptance);
395
479
  this.revocation = options.revocation
@@ -401,34 +485,90 @@ export class WorkerNode {
401
485
  allowPrivateRelay: options.allowPrivateRelay,
402
486
  lookup: options.lookup,
403
487
  requestTimeoutMs: options.connectTimeoutMs,
488
+ maxAuthRecoveryAttempts: options.maxAuthRecoveryAttempts,
489
+ authRecoveryTimeoutMs: options.authRecoveryTimeoutMs,
404
490
  }));
491
+ this.deliveryService = (typeof options.relay === 'string' || options.relayUrl)
492
+ ? options.deliveryService ?? (typeof options.relay === 'string' || options.deliveryOptions
493
+ ? new GatewayDeliveryService(options.owner, { ...options.deliveryOptions,
494
+ identityOptions: options.deliveryOptions?.identityOptions ?? options.identityOptions,
495
+ now: options.deliveryOptions?.now ?? options.now, requestTimeoutMs: options.connectTimeoutMs,
496
+ resolveTransport: options.deliveryOptions?.resolveTransport ?? ((url, owner) => transport(url, owner, options)),
497
+ assertSendAllowed: async (context) => {
498
+ this.assertPeer(context.peerOwnerDid, context.envelope.payload.taskId);
499
+ await options.deliveryOptions?.assertSendAllowed?.(context);
500
+ } }) : undefined) : undefined;
501
+ if (this.deliveryService && this.deliveryService.owner.did !== options.owner.did)
502
+ throw deliveryError('OUTBOX_STORE_INVALID');
503
+ if (this.deliveryService) {
504
+ this.deliveryService.recipientStatus = new RecipientDeliveryCoordinator(this.deliveryService, {
505
+ relayUrl: typeof options.relay === 'string' ? options.relay : options.relayUrl,
506
+ connectionState: peer => this.connectionStore.connectionState(peer),
507
+ onPublish: () => { void this.deliveryService.drainStatuses(); },
508
+ });
509
+ this.heldReview.store.setTransitionSink(this.deliveryService.recipientStatus.transition);
510
+ this.deliveryService.onMaintenance(() => {
511
+ this.deliveryService.recipientStatus.reconcile(this.heldReview.store.records());
512
+ if (this.heldReview.list().some(r => r.state === 'expired'))
513
+ this.heldReview.expire();
514
+ });
515
+ }
516
+ if (!persistencePath && this.deliveryService) {
517
+ this.taskJournal = new TaskJournal(join(this.deliveryService.store.root, 'tasks.jsonl'), options.owner, this.deliveryService.store.limits.recordBytes, 'process');
518
+ this.deliveryService.onMaintenance(async (signal) => {
519
+ if (!existsSync(this.taskJournal.path))
520
+ return;
521
+ const store = this.deliveryService.store, retained = new Set();
522
+ for await (const r of store.scan('receipts', signal))
523
+ if (r.retainUntil > store.now())
524
+ retained.add(r.taskId);
525
+ await this.taskJournal.compactInBackground(retained, change => store.transaction(change), signal);
526
+ });
527
+ }
405
528
  this.approvalProvider =
406
529
  options.approvalProvider ?? new InteractiveCliApprovalProvider(options.owner);
407
530
  const agentDid = options.agentDid ?? options.agentKey?.did ?? options.owner.did;
531
+ const now = this.protocol.now();
408
532
  this.agentCard = createSignedAgentCard({
409
- name: options.name ?? options.adapter.name,
533
+ name: options.name ?? options.adapter.name, now,
410
534
  agentDid,
411
535
  relayEndpoint: typeof options.relay === 'string'
412
536
  ? options.relay
413
537
  : (options.relayUrl ?? 'http://in-memory.invalid'),
414
538
  skills: options.adapter.skills,
415
539
  ...(options.inviteTtlSeconds !== undefined ? {
416
- expiresAt: new Date(Date.now() + options.inviteTtlSeconds * 1000).toISOString(),
540
+ expiresAt: new Date(now + options.inviteTtlSeconds * 1000).toISOString(),
417
541
  } : {}),
418
- ...(options.accessMode ? { accessMode: options.accessMode } : {}),
419
- ...(options.openPolicy !== undefined ? { openPolicy: options.openPolicy } : {}),
542
+ protocolVersions: this.protocol.receiveVersions(),
420
543
  }, options.owner, agentDid === options.owner.did ? undefined : options.agentKey);
544
+ this.connections = new ConnectionRouter({ owner: options.owner, agentCard: this.agentCard,
545
+ relayUrl: this.agentCard.payload.relayEndpoint, relay: this.relay, store: this.connectionStore,
546
+ review: this.heldReview, now: options.now, humanReview: options.connectionReview,
547
+ revocations: this.revocation, allowPrivateRelay: options.allowPrivateRelay,
548
+ assertPeer: did => { this.assertPeer(did); }, onConnectionEvent: options.onConnectionEvent });
421
549
  }
422
550
  invite(expiresInSeconds = this.options.inviteTtlSeconds ?? 86400) {
423
- return createInviteToken({ agentCard: this.agentCard, expiresInSeconds }, this.options.owner);
551
+ return createInviteToken({ agentCard: this.agentCard, expiresInSeconds, issuedAt: new Date(this.protocol.now()).toISOString() }, this.options.owner);
424
552
  }
425
553
  async register() {
554
+ const protocolVersions = this.protocol.receiveVersions();
555
+ if (JSON.stringify(protocolVersions) !== JSON.stringify(this.agentCard.payload.protocolVersions) || Date.parse(this.agentCard.payload.expiresAt) <= this.protocol.now()) {
556
+ const now = this.protocol.now();
557
+ this.agentCard = createSignedAgentCard({ ...this.agentCard.payload, protocolVersions, now,
558
+ expiresAt: new Date(now + (this.options.inviteTtlSeconds ?? 86400) * 1000).toISOString() }, this.options.owner, this.options.agentKey);
559
+ }
560
+ this.connections.service.setAgentCard(this.agentCard);
426
561
  await this.relay.register?.(this.agentCard);
562
+ this.registered = true;
427
563
  }
428
564
  stop() {
565
+ this.stopDelivery();
429
566
  this.controller.abort('WORKER_STOPPED');
567
+ this.taskJournal?.close();
430
568
  this.listenerStatus('stopped');
431
569
  }
570
+ async startDelivery() { await this.deliveryService?.start(); }
571
+ stopDelivery() { this.deliveryService?.stop(); }
432
572
  /** Public registration is opt-in, including for PolicyAutoApprovalProvider. */
433
573
  publishes() {
434
574
  return this.options.publishToRegistry === true;
@@ -535,7 +675,7 @@ export class WorkerNode {
535
675
  if (!idleSignal.aborted) {
536
676
  // A rejected one-shot task may be followed by another start on this node.
537
677
  // Only a terminal listener failure shuts down the worker and all of its tasks.
538
- if (error instanceof Error && 'code' in error && error.code === 'RELAY_TERMINAL') {
678
+ if (error instanceof Error && 'code' in error && ['RELAY_TERMINAL', 'RELAY_AUTH_RECOVERY_EXHAUSTED'].includes(String(error.code))) {
539
679
  this.listenerStatus('unavailable', error);
540
680
  for (const mailbox of this.mailboxes.values())
541
681
  mailbox.close(error);
@@ -549,9 +689,23 @@ export class WorkerNode {
549
689
  this.listenerStatus('stopped');
550
690
  unregister();
551
691
  await Promise.allSettled([...jobs]);
692
+ this.taskJournal?.close();
552
693
  }
553
694
  return this.lastResult;
554
695
  }
696
+ assertPeer(did, taskId) {
697
+ if (this.options.allowedPeerDids !== undefined && !this.options.allowedPeerDids.includes(did))
698
+ throw new Error('PEER_NOT_ALLOWED');
699
+ if ((this.intakeStore.level(did) === 'denied' || this.connectionStore.connectionState(did) === 'blocked') && (!taskId || this.acceptedPeers.get(taskId) !== did))
700
+ throw new Error('REQUEST_DENIED');
701
+ return this.options.checkPeer?.(did);
702
+ }
703
+ trust(did, protocolVersion) {
704
+ const contactTrust = this.assertPeer(did);
705
+ if (contactTrust === 'trusted' || this.connectionStore.hasExplicitLegacyGrant(did))
706
+ return 'trusted';
707
+ return protocolVersion === '0.2.0' && this.connectionStore.connectionState(did) === 'connected' ? 'limited' : 'unknown';
708
+ }
555
709
  async notifyOutcome(record, outcome = record.outcome) {
556
710
  const send = (recovery) => this.relay.send(signEnvelope({ taskId: record.taskId,
557
711
  recipientDid: record.requesterDid, seq: 0, eventType: 'ERROR',
@@ -573,25 +727,48 @@ export class WorkerNode {
573
727
  await send({ proposalDigest: record.proposalDigest, ...(outcome.result ? { result: outcome.result } : {}) });
574
728
  }
575
729
  }
730
+ saveTaskCheckpoint(record) {
731
+ if (!this.taskJournal)
732
+ return;
733
+ if (this.deliveryService && !this.options.inboxPath && !this.options.auditPath) {
734
+ this.deliveryService.store.transaction(() => { this.taskJournal.reload(); this.taskJournal.save(record); });
735
+ }
736
+ else
737
+ this.taskJournal.save(record);
738
+ }
576
739
  async reconcileTasks() {
577
740
  if (this.reconciled || !this.taskJournal)
578
741
  return;
579
- this.taskJournal.load();
580
- for (const saved of this.taskJournal.tasks.values()) {
742
+ if (this.deliveryService && !this.options.inboxPath && !this.options.auditPath)
743
+ this.deliveryService.store.transaction(() => this.taskJournal.reload());
744
+ else
745
+ this.taskJournal.load();
746
+ for (const saved of [...this.taskJournal.tasks.values()]) {
581
747
  let record = saved;
748
+ const pending = this.intakeStore.pendingTask(record.requesterDid, record.taskId, record.proposalDigest);
582
749
  if (record.phase !== 'terminal') {
583
750
  const result = this.sealedTaskResult(record);
584
751
  // Adapters expose executeTask only. Unknown execution/tool results cannot be retried safely.
585
- record = { ...record, phase: 'terminal', outcome: result
752
+ record = { ...record, phase: 'terminal', outcome: pending ? { code: 'PENDING_OWNER_APPROVAL' } : result
586
753
  ? { code: result.state === 'SETTLED' ? 'TASK_SETTLED' : 'TASK_CANCELLED', result }
587
754
  : { code: 'TASK_INTERRUPTED' }, notificationPending: true };
588
- this.taskJournal.save(record);
589
- if (!result)
755
+ this.saveTaskCheckpoint(record);
756
+ if (!result && !pending)
590
757
  this.options.onEvent?.('INTERRUPTED', record.taskId, { code: 'TASK_INTERRUPTED' });
591
758
  }
592
759
  if (record.notificationPending) {
760
+ try {
761
+ this.assertPeer(record.requesterDid);
762
+ }
763
+ catch {
764
+ continue;
765
+ }
766
+ if (pending) {
767
+ record = { ...record, phase: 'terminal', outcome: { code: 'PENDING_OWNER_APPROVAL' } };
768
+ this.saveTaskCheckpoint(record);
769
+ }
593
770
  await this.notifyOutcome(record);
594
- this.taskJournal.save({ ...record, notificationPending: false });
771
+ this.saveTaskCheckpoint({ ...record, notificationPending: false });
595
772
  }
596
773
  }
597
774
  this.reconciled = true;
@@ -620,6 +797,14 @@ export class WorkerNode {
620
797
  if (!this.taskJournal || envelope.payload.eventType !== 'PROPOSE' || envelope.payload.seq !== 0 ||
621
798
  !verifyEnvelope(envelope, this.options.owner.did))
622
799
  return false;
800
+ try {
801
+ this.assertPeer(envelope.payload.senderDid);
802
+ }
803
+ catch (error) {
804
+ this.reportRejection(envelope.payload.taskId, error);
805
+ await acknowledgeRelay(this.relay, envelope);
806
+ return true;
807
+ }
623
808
  let data;
624
809
  try {
625
810
  data = openEnvelope(envelope, this.options.owner, sessionDecryptOptions(this.options.allowUnencryptedEnvelopes));
@@ -632,7 +817,18 @@ export class WorkerNode {
632
817
  manifest.taskId !== envelope.payload.taskId || manifest.requester?.ownerDid !== envelope.payload.senderDid)
633
818
  return false;
634
819
  const proposalDigest = digest({ manifest, input: data.input });
820
+ if (this.deliveryService && !this.options.inboxPath && !this.options.auditPath)
821
+ this.deliveryService.store.transaction(() => this.taskJournal.reload());
635
822
  let record = this.taskJournal.tasks.get(manifest.taskId);
823
+ const exactReceipt = this.deliveryService?.store.read('receipts', envelope.payload.envelopeId);
824
+ const receipt = exactReceipt ?? this.deliveryService?.store.snapshot('receipts').find(r => r.taskId === manifest.taskId);
825
+ if (receipt && (receipt.senderDid !== envelope.payload.senderDid || exactReceipt && receipt.envelopeDigest !== digest(envelope) || receipt.taskId !== manifest.taskId))
826
+ throw deliveryError('DELIVERY_RECORD_CONFLICT');
827
+ if (!record && receipt) {
828
+ record = { version: 1, taskId: manifest.taskId, requesterDid: envelope.payload.senderDid,
829
+ envelopeId: envelope.payload.envelopeId, proposalDigest, phase: 'terminal', invocations: [], outcome: { code: 'TASK_INTERRUPTED' } };
830
+ this.saveTaskCheckpoint(record);
831
+ }
636
832
  // Older inboxes have intake evidence but no execution checkpoint. Never execute those tasks again.
637
833
  if (!record && this.options.inboxPath && existsSync(this.options.inboxPath)) {
638
834
  for (const line of readFileSync(this.options.inboxPath, 'utf8').split('\n')) {
@@ -767,24 +963,40 @@ export class WorkerNode {
767
963
  throw failure;
768
964
  }
769
965
  async runOnce(signal) {
770
- await this.reconcileTasks();
771
- const idleSignal = AbortSignal.any([this.controller.signal, ...(signal ? [signal] : [])]);
772
- // One or more leading REVOKE envelopes are consumed before the task PROPOSE.
773
- const first = await receiveRelay(this.relay, this.options.owner.did, idleSignal, {
774
- role: 'worker',
775
- });
776
- if (await this.recoverProposal(first)) {
777
- if (this.lastResult?.taskId === first.payload.taskId)
778
- return this.lastResult;
779
- throw new Error(this.taskJournal?.tasks.get(first.payload.taskId)?.outcome?.code ?? 'TASK_INTERRUPTED');
966
+ try {
967
+ await this.reconcileTasks();
968
+ const idleSignal = AbortSignal.any([this.controller.signal, ...(signal ? [signal] : [])]);
969
+ // One or more leading REVOKE envelopes are consumed before the task PROPOSE.
970
+ const first = await receiveRelay(this.relay, this.options.owner.did, idleSignal, {
971
+ role: 'worker',
972
+ });
973
+ if (await this.recoverProposal(first)) {
974
+ if (this.lastResult?.taskId === first.payload.taskId)
975
+ return this.lastResult;
976
+ throw new Error(this.taskJournal?.tasks.get(first.payload.taskId)?.outcome?.code ?? 'TASK_INTERRUPTED');
977
+ }
978
+ return await this.executeTask(first, idleSignal, this.relay);
979
+ }
980
+ finally {
981
+ this.taskJournal?.close();
780
982
  }
781
- return this.executeTask(first, idleSignal, this.relay);
983
+ }
984
+ async holdTask(first, manifest, input) {
985
+ const contentDigest = digest({ manifest, input });
986
+ this.connectionStore.consumeAdmission(manifest.requester.ownerDid, manifest.taskId);
987
+ const previous = this.heldReview.list().find(r => r.taskId === manifest.taskId && r.senderOwnerDid === manifest.requester.ownerDid);
988
+ const held = previous ?? this.heldReview.hold({ kind: 'task', senderOwnerDid: manifest.requester.ownerDid,
989
+ taskId: manifest.taskId, contentDigest, deadline: Date.parse(manifest.deadline), envelope: first });
990
+ if (held.contentDigest !== contentDigest)
991
+ throw new Error('INVALID_PROPOSAL');
992
+ return this.heldReview.inspect(held.id);
782
993
  }
783
994
  async executeTask(first, idleSignal, relay, earlyRejection) {
784
995
  if (!verifyEnvelope(first, this.options.owner.did) ||
785
996
  first.payload.eventType !== 'PROPOSE' ||
786
997
  first.payload.seq !== 0)
787
998
  throw new Error('INVALID_PROPOSAL_ENVELOPE');
999
+ this.assertPeer(first.payload.senderDid);
788
1000
  // PROPOSE arrives before the session wire is constructed. Open it with the same rule,
789
1001
  // and return an encrypted ERROR frame if pre-wire validation fails so the requester does not hang.
790
1002
  let data;
@@ -795,10 +1007,15 @@ export class WorkerNode {
795
1007
  if (!checkpoint || !this.taskJournal)
796
1008
  return;
797
1009
  checkpoint = { ...checkpoint, ...patch };
798
- this.taskJournal.save(checkpoint);
1010
+ this.saveTaskCheckpoint(checkpoint);
799
1011
  };
800
1012
  const saveOutcome = (outcome) => {
801
1013
  saveCheckpoint({ phase: 'terminal', outcome, notificationPending: true });
1014
+ this.deliveryService?.store.receipt(first, Date.parse(m.deadline), 'terminal', outcome.code);
1015
+ };
1016
+ const acknowledgeProposal = async (signal) => {
1017
+ this.deliveryService?.store.receipt(first, Date.parse(m.deadline), 'processed');
1018
+ await acknowledgeRelay(relay, first, signal);
802
1019
  };
803
1020
  try {
804
1021
  data = openEnvelope(first, this.options.owner, sessionDecryptOptions(this.options.allowUnencryptedEnvelopes));
@@ -810,9 +1027,11 @@ export class WorkerNode {
810
1027
  if (!verifySigned(data.proposal, manifest.requester.ownerDid) ||
811
1028
  manifest.worker?.ownerDid !== this.options.owner.did ||
812
1029
  manifest.worker.agentDid !== this.agentCard.payload.agentDid ||
813
- Date.parse(manifest.deadline) <= Date.now() ||
814
- Date.parse(manifest.createdAt) > Date.now() + DEFAULT_CLOCK_SKEW_MS)
1030
+ Date.parse(manifest.deadline) <= this.protocol.now() ||
1031
+ Date.parse(manifest.createdAt) > this.protocol.now() + DEFAULT_CLOCK_SKEW_MS)
815
1032
  throw new Error('INVALID_PROPOSAL');
1033
+ this.assertPeer(first.payload.senderDid);
1034
+ validateManifest(manifest);
816
1035
  m = manifest;
817
1036
  auditPath = this.options.auditPath
818
1037
  ? this.completed++ === 0 && !existsSync(this.options.auditPath)
@@ -824,7 +1043,17 @@ export class WorkerNode {
824
1043
  checkpoint = { version: 1, taskId: m.taskId, requesterDid: m.requester.ownerDid,
825
1044
  envelopeId: first.payload.envelopeId, proposalDigest: digest({ manifest: m, input: data.input }),
826
1045
  phase: 'received', auditPath, invocations: [] };
827
- this.taskJournal.save(checkpoint);
1046
+ if (this.deliveryService) {
1047
+ this.deliveryService.store.transaction(() => {
1048
+ this.taskJournal.reload();
1049
+ if (this.taskJournal.tasks.has(m.taskId) || this.deliveryService.store.snapshot('receipts').some(r => r.taskId === m.taskId))
1050
+ throw deliveryError('DELIVERY_RECORD_CONFLICT');
1051
+ this.deliveryService.store.receiptLocked(first, Date.parse(m.deadline), 'persisted');
1052
+ this.taskJournal.save(checkpoint);
1053
+ });
1054
+ }
1055
+ else
1056
+ this.taskJournal.save(checkpoint);
828
1057
  }
829
1058
  }
830
1059
  catch (e) {
@@ -845,7 +1074,11 @@ export class WorkerNode {
845
1074
  }
846
1075
  const timeouts = new SessionTimeouts(this.options, m.deadline, idleSignal);
847
1076
  const activeSignal = timeouts.signal;
848
- const wire = new SessionWire(relay, this.options.owner, m.requester.ownerDid, m.taskId, [m.requester.ownerDid, this.options.owner.did], activeSignal, auditPath, this.options.allowUnencryptedEnvelopes === true);
1077
+ const wire = new SessionWire(relay, this.options.owner, m.requester.ownerDid, m.taskId, [m.requester.ownerDid, this.options.owner.did], activeSignal, auditPath, this.options.allowUnencryptedEnvelopes === true, true, this.deliveryService ? { service: this.deliveryService, context: {
1078
+ messageId: receiptFileId(first.payload.envelopeId), peerOwnerDid: m.requester.ownerDid,
1079
+ relayUrl: this.options.relayUrl ?? (typeof this.options.relay === 'string' ? this.options.relay : this.agentCard.payload.relayEndpoint),
1080
+ deliveryDeadline: Date.parse(m.deadline),
1081
+ } } : undefined);
849
1082
  const cancelController = new AbortController();
850
1083
  wire.earlyCancel = () => cancelController.abort('CANCELLED');
851
1084
  let heartbeat;
@@ -868,27 +1101,29 @@ export class WorkerNode {
868
1101
  let progressFailure;
869
1102
  let wireFailureSent = false;
870
1103
  try {
871
- await wire.receiveEvent('PROPOSE', first);
872
1104
  this.assertInviteActive(data);
873
- if (this.options.accessMode === 'open') {
874
- for (const field of OPEN_POLICY_FIELDS) {
875
- const limit = this.agentCard.payload.openPolicy?.[field];
876
- if (limit !== undefined && m.budget[field] > limit)
877
- throw diagnosticError('TASK_SCOPE_REJECTED', 'budget.' + field, limit, m.budget[field], 'Use a budget within the advertised policy.');
1105
+ let peerDisplayName;
1106
+ if (this.relay.discover) {
1107
+ const cards = await this.relay.discover();
1108
+ const sender = findDiscoveredAgentCard(cards, m.requester.agentDid, this.protocol.now());
1109
+ if (sender?.payload.ownerDid === m.requester.ownerDid && sender.payload.relayEndpoint === this.agentCard.payload.relayEndpoint) {
1110
+ this.protocol.observe(sender, cards);
1111
+ peerDisplayName = sender.payload.name;
878
1112
  }
879
- const skills = this.agentCard.payload.skills;
880
- const selected = m.task.skillId ? skills.filter((skill) => skill.id === m.task.skillId) : skills;
881
- if ((m.task.skillId && selected.length === 0) ||
882
- (m.task.taskType && !selected.some((skill) => skill.taskTypes?.includes(m.task.taskType))) ||
883
- (m.task.skillId && !m.task.taskType && selected.some((skill) => skill.taskTypes?.length)))
884
- throw diagnosticError('TASK_SCOPE_REJECTED', 'task.scope', skills, m.task, 'Select an advertised skill and task type, or omit both for direct delegation.');
885
1113
  }
1114
+ this.protocol.inbound(m.protocolVersion, m.requester.ownerDid, m.requester.agentDid);
1115
+ const skills = this.agentCard.payload.skills;
1116
+ const selected = m.task.skillId ? skills.filter(skill => skill.id === m.task.skillId) : skills;
1117
+ if ((m.task.skillId && selected.length === 0) ||
1118
+ (m.task.taskType && !selected.some(skill => skill.taskTypes?.includes(m.task.taskType))) ||
1119
+ (m.task.skillId && !m.task.taskType && selected.some(skill => skill.taskTypes?.length)))
1120
+ throw diagnosticError('TASK_SCOPE_REJECTED', 'task.scope', skills, m.task, 'Select an advertised skill and task type, or omit both for direct delegation.');
886
1121
  const capabilityContext = {
887
1122
  rootDid: m.requester.ownerDid,
888
1123
  audience: this.options.owner.did,
889
1124
  taskId: m.taskId,
890
1125
  };
891
- if (!Array.isArray(data.capabilityChains))
1126
+ if (!Array.isArray(data.capabilityChains) || data.capabilityChains.length > 32)
892
1127
  throw new Error('CAPABILITY_VIOLATION');
893
1128
  for (const chain of data.capabilityChains)
894
1129
  verifyCapabilityChain(chain, capabilityContext);
@@ -910,8 +1145,86 @@ export class WorkerNode {
910
1145
  (!Number.isSafeInteger(data.peerIdleTimeoutMs) || data.peerIdleTimeoutMs < 1 ||
911
1146
  (this.options.heartbeatIntervalMs ?? 15_000) >= data.peerIdleTimeoutMs))
912
1147
  throw diagnosticError('INCOMPATIBLE_TIMEOUTS', 'peerIdleTimeoutMs', { greaterThan: this.options.heartbeatIntervalMs ?? 15_000 }, data.peerIdleTimeoutMs, 'Raise the requester idle timeout or shorten the worker heartbeat interval before accepting.');
1148
+ // Validate capability scope before the admission queue or an owner prompt.
1149
+ const scopedTools = new Set();
1150
+ for (const chain of data.capabilityChains) {
1151
+ const leaf = verifyCapabilityChain(chain, capabilityContext).payload;
1152
+ const grant = m.permissions.grantedCapabilities.find(g => g.resource === leaf.resource &&
1153
+ leaf.allowedTools.every(t => g.allowedTools.includes(t)) && leaf.maxCalls <= g.maxInvocations &&
1154
+ Date.parse(leaf.expiresAt) <= Date.parse(g.expiresAt));
1155
+ if (!grant || leaf.maxDepth > m.permissions.maxDelegationDepth || leaf.allowedTools.some(t => scopedTools.has(t) || !schemaNarrows(leaf.argConstraints[t], grant.argConstraints ?? { type: 'object' })))
1156
+ throw new Error('CAPABILITY_VIOLATION');
1157
+ leaf.allowedTools.forEach(t => scopedTools.add(t));
1158
+ }
1159
+ if (this.options.requireConnection && (m.protocolVersion !== '0.2.0' || this.connectionStore.connectionState(m.requester.ownerDid) !== 'connected'))
1160
+ throw new Error('CONNECTION_REQUIRED');
1161
+ const proposalDigest = digest({ manifest: m, input: data.input });
1162
+ if (data.connectionRequest) {
1163
+ if (m.protocolVersion !== '0.2.0')
1164
+ throw new Error('INVALID_CONNECTION_REQUEST');
1165
+ this.connections.service.validateRequest(data.connectionRequest);
1166
+ const frame = openConnectionFrame(data.connectionRequest, this.options.owner);
1167
+ if (frame.event.payload.type !== 'ConnectionRequest' || frame.event.payload.fromOwnerDid !== m.requester.ownerDid ||
1168
+ frame.event.payload.originProposalDigest !== proposalDigest)
1169
+ throw new Error('INVALID_CONNECTION_REQUEST');
1170
+ }
1171
+ const previouslyPending = this.intakeStore.pendingTask(m.requester.ownerDid, m.taskId, proposalDigest);
1172
+ let inspection;
1173
+ this.deliveryService?.recipientStatus.begin(first.payload.envelopeId);
1174
+ // Persist the immutable ciphertext before any connection mutation or owner acknowledgement.
1175
+ if (this.trust(m.requester.ownerDid, m.protocolVersion) !== 'trusted') {
1176
+ inspection = await this.holdTask(first, m, data.input);
1177
+ if (inspection.inspectionStatus !== 'complete' || m.protocolVersion === '0.1.0')
1178
+ this.intakeStore.enqueue(m, data.input);
1179
+ }
1180
+ // All identity, schema, scope, capability and host checks precede durable consent.
1181
+ let inviteIneligible = false;
1182
+ if (data.inviteToken) {
1183
+ const invite = parseAndVerifyInviteToken(data.inviteToken, this.protocol.now(), { allowPrivateRelay: true });
1184
+ const outcome = this.connectionStore.redeemTaskInvite(invite, { peerOwnerDid: m.requester.ownerDid,
1185
+ peerAgentDid: m.requester.agentDid, ...(peerDisplayName !== undefined ? { peerDisplayName } : {}),
1186
+ relayUrl: this.agentCard.payload.relayEndpoint }, connectionIdFor(this.options.owner.did, m.requester.ownerDid));
1187
+ inviteIneligible = outcome === 'other' || outcome === 'inactive';
1188
+ if (outcome === 'first')
1189
+ this.options.onConnectionEvent?.({ event: 'connected', peerOwnerDid: m.requester.ownerDid,
1190
+ peerAgentDid: m.requester.agentDid, connectionId: connectionIdFor(this.options.owner.did, m.requester.ownerDid),
1191
+ source: 'invite', connectedAt: this.protocol.now() });
1192
+ }
1193
+ if (data.connectionRequest)
1194
+ await this.connections.consume(data.connectionRequest);
1195
+ const peerTrust = inviteIneligible ? 'unknown' : this.trust(m.requester.ownerDid, m.protocolVersion);
1196
+ this.deliveryService?.recipientStatus.admit(first, m, !previouslyPending && peerTrust === 'limited', !!inspection, inspection?.receivedAt);
1197
+ this.deliveryService?.recipientStatus.end(first.payload.envelopeId);
1198
+ if (previouslyPending && peerTrust === 'trusted')
1199
+ throw new Error('PENDING_OWNER_APPROVAL');
1200
+ if (peerTrust !== 'trusted') {
1201
+ inspection ??= await this.holdTask(first, m, data.input);
1202
+ if (previouslyPending || peerTrust === 'unknown' || this.intakeStore.pendingTask(m.requester.ownerDid, m.taskId, proposalDigest) ||
1203
+ inspection.inspectionStatus !== 'complete') {
1204
+ this.intakeStore.enqueue(m, data.input);
1205
+ if (this.connectionStore.connectionState(m.requester.ownerDid) === 'none') {
1206
+ const receivedAt = this.protocol.now();
1207
+ try {
1208
+ this.connectionStore.recordIncoming({ peerOwnerDid: m.requester.ownerDid, peerAgentDid: m.requester.agentDid,
1209
+ ...(peerDisplayName !== undefined ? { peerDisplayName } : {}),
1210
+ relayUrl: this.agentCard.payload.relayEndpoint, incoming: { requestId: m.taskId, requestDigest: proposalDigest,
1211
+ receivedAt, expiresAt: receivedAt + 14 * 86400000 } });
1212
+ }
1213
+ catch (error) {
1214
+ if (!(error instanceof Error) || error.message !== 'CONNECTION_REQUEST_COOLDOWN')
1215
+ throw error;
1216
+ }
1217
+ }
1218
+ throw new Error('PENDING_OWNER_APPROVAL');
1219
+ }
1220
+ }
1221
+ const contractProvider = peerTrust === 'limited' ? this.options.ownerApprovalProvider : this.approvalProvider;
1222
+ if (!contractProvider || peerTrust === 'limited' && (contractProvider.kind === 'policy' ||
1223
+ contractProvider === this.approvalProvider && contractProvider.kind !== 'human'))
1224
+ throw new Error('APPROVAL_CHANNEL_UNAVAILABLE');
1225
+ await wire.receiveEvent('PROPOSE', first);
913
1226
  const contract = createContract(m, data.proposal, this.options.owner);
914
- const approval = await abortable(this.approvalProvider.approve({
1227
+ const approval = await abortable(contractProvider.approve({
915
1228
  taskId: m.taskId,
916
1229
  gate: 'contract_acceptance',
917
1230
  target: contract.contractDigest,
@@ -919,13 +1232,23 @@ export class WorkerNode {
919
1232
  manifest: m,
920
1233
  input: data.input,
921
1234
  capabilities: data.capabilityChains,
1235
+ ...(inspection ? { inspection } : {}),
922
1236
  from: first.payload.senderDid,
923
1237
  },
924
1238
  signal: activeSignal,
925
1239
  }), activeSignal);
1240
+ if (this.trust(m.requester.ownerDid, m.protocolVersion) === 'unknown')
1241
+ throw new Error('APPROVAL_CHANNEL_UNAVAILABLE');
1242
+ this.assertCapabilitiesActive(data.capabilityChains, capabilityContext);
1243
+ this.assertPeer(m.requester.ownerDid);
1244
+ if (peerTrust === 'trusted' && this.trust(m.requester.ownerDid, m.protocolVersion) !== 'trusted')
1245
+ throw new Error('APPROVAL_CHANNEL_UNAVAILABLE');
926
1246
  gateway = new OwnerGateway(this.options.owner, contract, policy, this.revocation);
927
1247
  const accepted = gateway.accept(approval);
1248
+ this.acceptedPeers.set(m.taskId, m.requester.ownerDid);
928
1249
  saveCheckpoint({ phase: 'accepted' });
1250
+ if (inspection?.inspectionStatus === 'complete')
1251
+ this.deliveryService?.recipientStatus.observe(first.payload.envelopeId, 'opened');
929
1252
  if (this.options.dailyLimit)
930
1253
  consumeDailyTask(this.options.dailyLimit.path, this.options.dailyLimit.maxTasks, m.taskId);
931
1254
  if (this.options.inboxPath) {
@@ -951,7 +1274,7 @@ export class WorkerNode {
951
1274
  finally {
952
1275
  closeSync(fd);
953
1276
  }
954
- await acknowledgeRelay(relay, first);
1277
+ await acknowledgeProposal(activeSignal);
955
1278
  // Notification errors must not fail a persisted task.
956
1279
  try {
957
1280
  void Promise.resolve(this.options.onInboxRecord?.(record)).catch(() => undefined);
@@ -1008,6 +1331,7 @@ export class WorkerNode {
1008
1331
  isActive: () => gateway.machine.state === 'IN_PROGRESS',
1009
1332
  });
1010
1333
  });
1334
+ this.assertPeer(m.requester.ownerDid, m.taskId);
1011
1335
  workspace = await mkdtemp(join(tmpdir(), 'acp-adapter-'));
1012
1336
  saveCheckpoint({ phase: 'running', workspace });
1013
1337
  let invocationCount = 0;
@@ -1100,6 +1424,7 @@ export class WorkerNode {
1100
1424
  sandboxBoundary = boundary;
1101
1425
  },
1102
1426
  callTool: async (tool, args) => {
1427
+ this.assertPeer(m.requester.ownerDid, m.taskId);
1103
1428
  executionSignal.throwIfAborted();
1104
1429
  if (!args || typeof args !== 'object' || Array.isArray(args))
1105
1430
  throw new Error('CAPABILITY_VIOLATION: arguments');
@@ -1126,9 +1451,12 @@ export class WorkerNode {
1126
1451
  return result;
1127
1452
  },
1128
1453
  };
1454
+ this.assertPeer(m.requester.ownerDid, m.taskId);
1455
+ this.assertCapabilitiesActive(data.capabilityChains, capabilityContext);
1456
+ this.deliveryService?.recipientStatus.handoff(first, m);
1129
1457
  const output = await abortable(this.options.adapter.executeTask(context), executionSignal);
1130
1458
  if (!this.options.inboxPath)
1131
- await acknowledgeRelay(relay, first);
1459
+ await acknowledgeProposal(executionSignal);
1132
1460
  // A task may return while a queued progress update is still waiting for its ACK.
1133
1461
  // Skip the drain once the task is already aborted, cancelled, or over budget.
1134
1462
  if (!executionSignal.aborted && !wire.cancelHandled) {
@@ -1194,11 +1522,19 @@ export class WorkerNode {
1194
1522
  throw e;
1195
1523
  const diagnostic = preserveWorkspace ? { code: 'TASK_INTERRUPTED' } : rejectionBody(progressFailure ?? e);
1196
1524
  saveOutcome(diagnostic);
1525
+ this.deliveryService?.recipientStatus.end(first.payload.envelopeId);
1526
+ const refused = this.heldReview.list().find(r => r.taskId === m.taskId);
1527
+ if (refused && diagnostic.code !== 'PENDING_OWNER_APPROVAL' && !preserveWorkspace)
1528
+ this.heldReview.discard(refused.id);
1197
1529
  this.reportRejection(m.taskId, progressFailure ?? e);
1198
1530
  if (!wireFailureSent) {
1199
1531
  wireFailureSent = true;
1200
1532
  await wire.rawSend('ERROR', diagnostic)
1201
- .then(() => saveCheckpoint({ notificationPending: false })).catch(() => { });
1533
+ .then(async () => {
1534
+ saveCheckpoint({ notificationPending: false });
1535
+ if (diagnostic.code === 'PENDING_OWNER_APPROVAL')
1536
+ await acknowledgeProposal();
1537
+ }).catch(() => { });
1202
1538
  }
1203
1539
  // A quota abort already surfaces as EXECUTION_ABORTED. Keep that error.
1204
1540
  // A caught INVALID_PROGRESS still fails the session with the sticky error.
@@ -1208,10 +1544,15 @@ export class WorkerNode {
1208
1544
  throw e;
1209
1545
  }
1210
1546
  finally {
1547
+ this.deliveryService?.recipientStatus.end(first.payload.envelopeId);
1211
1548
  heartbeatStopped = true;
1212
1549
  clearTimeout(heartbeat);
1213
1550
  timeouts.close();
1214
1551
  wire.close();
1552
+ this.acceptedPeers.delete(m.taskId);
1553
+ const completedHeld = this.heldReview.list().find(r => r.taskId === m.taskId);
1554
+ if (completedHeld && gateway && ['SETTLED', 'CANCELLED'].includes(gateway.machine.state))
1555
+ this.heldReview.discard(completedHeld.id, gateway.machine.state === 'SETTLED' ? 'completed' : 'closed');
1215
1556
  if (trackedTaskId)
1216
1557
  this.liveChains.delete(trackedTaskId);
1217
1558
  gateway?.close();
@@ -1254,10 +1595,16 @@ export class WorkerNode {
1254
1595
  revocationAwareRelay(relay) {
1255
1596
  const receive = async (recipientDid, waitMs, signal, filter) => {
1256
1597
  for (;;) {
1598
+ if (this.registered && JSON.stringify(this.protocol.receiveVersions()) !== JSON.stringify(this.agentCard.payload.protocolVersions))
1599
+ await this.register();
1257
1600
  const forwarded = this.forwardedQueue.shift();
1258
1601
  if (forwarded) {
1259
1602
  if (this.consumeRevocation(forwarded))
1260
1603
  continue;
1604
+ if (isConnectionEvent(forwarded.payload.eventType)) {
1605
+ await this.connections.route(forwarded);
1606
+ continue;
1607
+ }
1261
1608
  return forwarded;
1262
1609
  }
1263
1610
  const envelope = await relay.receive(recipientDid, waitMs, signal, filter ?? { role: 'worker' });
@@ -1266,14 +1613,31 @@ export class WorkerNode {
1266
1613
  if (afterWait) {
1267
1614
  if (this.consumeRevocation(afterWait))
1268
1615
  continue;
1616
+ if (isConnectionEvent(afterWait.payload.eventType)) {
1617
+ await this.connections.route(afterWait);
1618
+ continue;
1619
+ }
1269
1620
  return afterWait;
1270
1621
  }
1271
1622
  return undefined;
1272
1623
  }
1624
+ if (envelope.payload.eventType === 'DELIVERY_STATUS' && this.deliveryService) {
1625
+ try {
1626
+ await this.deliveryService.statusRouter.route(envelope, relay);
1627
+ }
1628
+ catch (error) {
1629
+ this.deliveryService.options.onError?.(error);
1630
+ }
1631
+ continue;
1632
+ }
1273
1633
  if (this.consumeRevocation(envelope)) {
1274
1634
  await acknowledgeRelay(relay, envelope);
1275
1635
  continue;
1276
1636
  }
1637
+ if (isConnectionEvent(envelope.payload.eventType)) {
1638
+ await this.connections.route(envelope);
1639
+ continue;
1640
+ }
1277
1641
  if (this.isRequesterBoundFrame(envelope) &&
1278
1642
  forwardToRequesterInbox(this.options.owner.did, envelope))
1279
1643
  continue;
@@ -1281,9 +1645,13 @@ export class WorkerNode {
1281
1645
  }
1282
1646
  };
1283
1647
  return {
1648
+ ...deliveryPins(relay),
1284
1649
  get connectionStatus() { return relay.connectionStatus; },
1285
- send: (envelope) => relay.send(envelope),
1286
- ...(relay.acknowledgeDelivery ? { acknowledgeDelivery: (did, id) => relay.acknowledgeDelivery(did, id) } : {}),
1650
+ send: (envelope) => {
1651
+ this.assertPeer(envelope.payload.recipientDid, envelope.payload.taskId);
1652
+ return relay.send(envelope);
1653
+ },
1654
+ ...(relay.acknowledgeDelivery ? { acknowledgeDelivery: (did, id, signal) => relay.acknowledgeDelivery(did, id, AbortSignal.any([this.controller.signal, ...(signal ? [signal] : [])])) } : {}),
1287
1655
  receive,
1288
1656
  ...(relay.register ? { register: (card) => relay.register(card) } : {}),
1289
1657
  ...(relay.discover ? { discover: () => relay.discover() } : {}),
@@ -1353,27 +1721,21 @@ export class WorkerNode {
1353
1721
  }
1354
1722
  assertInviteActive(data) {
1355
1723
  const token = data.inviteToken;
1356
- if (typeof token !== 'string' || token.length === 0) {
1357
- if (this.options.accessMode !== 'open' &&
1358
- !this.options.allowedRequesterDids?.includes(data.proposal.payload.requester.ownerDid))
1359
- throw new Error('INVITE_REQUIRED');
1360
- return;
1361
- }
1362
- let invite;
1363
- try {
1364
- invite = parseAndVerifyInviteToken(token, Date.now(), { allowPrivateRelay: true });
1365
- }
1366
- catch (error) {
1367
- if (this.options.accessMode === 'open' && error instanceof Error && error.message === 'INVITE_EXPIRED')
1368
- return;
1369
- throw error;
1370
- }
1724
+ if (token === undefined)
1725
+ return false;
1726
+ if (typeof token !== 'string' || !token)
1727
+ throw new Error('INVALID_OR_EXPIRED_INVITE');
1728
+ const invite = parseAndVerifyInviteToken(token, this.protocol.now(), { allowPrivateRelay: true, allowExpired: true });
1371
1729
  if (invite.ownerDid !== this.options.owner.did ||
1372
1730
  invite.agentDid !== this.agentCard.payload.agentDid ||
1373
1731
  invite.relayUrl !== this.agentCard.payload.relayEndpoint)
1374
1732
  throw new Error('INVITE_WORKER_MISMATCH');
1375
1733
  this.revocation.assertNotRevoked('invite', digest(invite));
1376
1734
  this.revocation.assertNotRevoked('invite', token);
1735
+ if (Math.min(Date.parse(invite.expiresAt), Date.parse(invite.agentCard.payload.expiresAt)) <= this.protocol.now()) {
1736
+ throw new Error('INVITE_EXPIRED');
1737
+ }
1738
+ return true;
1377
1739
  }
1378
1740
  /**
1379
1741
  * Before ACCEPT, every verified capability chain is checked against revocation
@@ -1552,11 +1914,19 @@ function rejectionBody(error) {
1552
1914
  return { code: safeError(error), detail: error.detail };
1553
1915
  if (error instanceof ProposalRejectedError)
1554
1916
  return { code: error.code, reason: error.reason };
1917
+ if (safeError(error) === 'PENDING_OWNER_APPROVAL')
1918
+ return { code: 'PENDING_OWNER_APPROVAL', reason: PENDING_MESSAGE };
1555
1919
  return { code: safeError(error) };
1556
1920
  }
1557
1921
  function safeError(e) {
1558
1922
  const s = String(e);
1559
- for (const code of ['INCOMPATIBLE_TIMEOUTS', 'CLAIM_LOST', 'DAILY_LIMIT_REACHED', 'DAILY_TASK_REPLAY', 'DAILY_LIMIT_CLOCK_ROLLBACK', 'DAILY_LIMIT_BUSY', 'DAILY_LIMIT_INVALID', 'UNENCRYPTED_ENVELOPE_REJECTED', 'INVITE_REQUIRED', 'INVITE_EXPIRED', 'INVITE_WORKER_MISMATCH', 'TASK_SCOPE_REJECTED'])
1923
+ const exact = e instanceof Error ? e.message.split(':')[0] : '';
1924
+ if (['OUTBOX_STORE_INVALID', 'OUTBOX_STORE_BUSY', 'OUTBOX_STORE_UNWRITABLE', 'OUTBOX_FULL',
1925
+ 'OUTBOX_DEADLINE_EXPIRED', 'DELIVERY_RECORD_CONFLICT'].includes(exact))
1926
+ return exact;
1927
+ if (/^(PROTOCOL_|CONNECTION_|INVALID_CONNECTION_|QUARANTINE_|INSPECTION_|ACCESS_MODE_REMOVED|INVITE_REFERENCE_UNAVAILABLE)/.test(exact))
1928
+ return exact;
1929
+ for (const code of ['PENDING_OWNER_APPROVAL', 'PEER_NOT_ALLOWED', 'REQUEST_DENIED', 'INTAKE_QUEUE_FULL', 'INTAKE_RATE_LIMITED', 'INTAKE_STORE_INVALID', 'INTAKE_STORE_BUSY', 'INTAKE_STORE_UNWRITABLE', 'APPROVAL_CHANNEL_UNAVAILABLE', 'CONTACT_REVOKED', 'CONTACT_NOT_ALLOWED', 'INCOMPATIBLE_TIMEOUTS', 'CLAIM_LOST', 'DAILY_LIMIT_REACHED', 'DAILY_TASK_REPLAY', 'DAILY_LIMIT_CLOCK_ROLLBACK', 'DAILY_LIMIT_BUSY', 'DAILY_LIMIT_INVALID', 'UNENCRYPTED_ENVELOPE_REJECTED', 'INVITE_REQUIRED', 'INVITE_EXPIRED', 'INVITE_WORKER_MISMATCH', 'TASK_SCOPE_REJECTED'])
1560
1930
  if (s === 'Error: ' + code || s.startsWith('Error: ' + code + ':'))
1561
1931
  return code;
1562
1932
  if (s.includes('TOKEN_REVOKED'))
@@ -1583,6 +1953,14 @@ async function abortable(promise, signal) {
1583
1953
  signal.removeEventListener('abort', listener);
1584
1954
  }
1585
1955
  }
1956
+ function freezeApprovalAction(value) {
1957
+ if (value && typeof value === 'object') {
1958
+ for (const child of Object.values(value))
1959
+ freezeApprovalAction(child);
1960
+ Object.freeze(value);
1961
+ }
1962
+ return value;
1963
+ }
1586
1964
  /** Completion failure preserves local verification without claiming a sealed checkpoint. */
1587
1965
  export class IncompleteSettlementError extends Error {
1588
1966
  incompleteReceipt;
@@ -1640,13 +2018,71 @@ export async function deliverRevocation(options) {
1640
2018
  }
1641
2019
  export class RequesterClient {
1642
2020
  options;
2021
+ idempotency;
2022
+ keyedStore() {
2023
+ return this.idempotency ??= new IdempotencyStore(this.options.owner, {
2024
+ ...this.options.identityOptions, now: this.options.now, ...this.options.idempotencyOptions,
2025
+ });
2026
+ }
2027
+ keyedAction(options) {
2028
+ const allowed = ['idempotencyKey', 'idempotencyOperation', 'idempotencyChildIndex', 'workerRef', 'pendingId', 'pendingRelayUrl',
2029
+ 'renegotiateProtocol', 'beforePropose', 'beforeTransmit', 'to', 'discover', 'relay', 'title', 'description', 'input',
2030
+ 'outputSchema', 'budget', 'manifest', 'auditPath', 'signal', 'capabilities', 'capabilityChains', 'minimumIsolationLevel',
2031
+ 'onProgress', 'workers', 'mode', 'quorumSize', 'completionTimeoutMs', 'timeoutMs', 'executionTimeoutMs', 'taskTimeoutMs',
2032
+ 'waitTimeoutMs', 'connectTimeoutMs', 'transportIdleTimeoutMs', 'heartbeatIntervalMs'];
2033
+ if (Object.keys(options).some(k => !allowed.includes(k)))
2034
+ throw new Error('INVALID_IDEMPOTENCY_ARGUMENT');
2035
+ const timeouts = resolveSessionTimeouts(this.options, options);
2036
+ const normalized = { ...options, ...timeouts, relay: ('relay' in options ? options.relay : undefined) ?? this.options.relay,
2037
+ outputSchema: options.outputSchema ?? {}, capabilities: options.capabilities ?? [],
2038
+ minimumIsolationLevel: options.minimumIsolationLevel ?? 'subprocess_restricted',
2039
+ budget: { costBearer: 'worker_byok', maxInputTokens: 50000, maxOutputTokens: 10000,
2040
+ maxToolInvocations: 10, maxWallClockSeconds: timeouts.executionTimeoutMs !== undefined
2041
+ ? Math.floor(timeouts.executionTimeoutMs / 1000) : 60, onOverrun: 'hard_stop', ...options.budget },
2042
+ taskTimeoutMs: timeouts.taskTimeoutMs ?? timeouts.timeoutMs ?? 600_000, timeoutMs: undefined,
2043
+ waitTimeoutMs: timeouts.waitTimeoutMs ?? 600_000, connectTimeoutMs: timeouts.connectTimeoutMs ?? 45_000,
2044
+ transportIdleTimeoutMs: timeouts.transportIdleTimeoutMs ?? 90_000, heartbeatIntervalMs: timeouts.heartbeatIntervalMs ?? 15_000,
2045
+ ...('workers' in options ? { workers: structuredClone(options.workers),
2046
+ completionTimeoutMs: options.completionTimeoutMs ?? 45_000,
2047
+ ...(options.mode === 'quorum' ? { quorumSize: options.quorumSize ?? Math.floor(options.workers.length / 2) + 1 } : {}) } : {}),
2048
+ };
2049
+ return runtimeIdempotencyAction(normalized);
2050
+ }
2051
+ deliveryService;
2052
+ deliveryStarted = false;
1643
2053
  approvalProvider;
2054
+ protocol;
1644
2055
  constructor(options) {
1645
2056
  this.options = options;
2057
+ assertConnectionsOptions(options);
2058
+ this.protocol = new LiveProtocol(options);
2059
+ validateRelayAuthRecoveryOptions(options);
1646
2060
  validateSessionTimeouts(options);
1647
2061
  this.approvalProvider =
1648
2062
  options.approvalProvider ?? new InteractiveCliApprovalProvider(options.owner);
2063
+ this.deliveryService = options.deliveryService;
2064
+ if (this.deliveryService && this.deliveryService.owner.did !== options.owner.did)
2065
+ throw deliveryError('OUTBOX_STORE_INVALID');
2066
+ if (!this.deliveryService && (typeof options.relay === 'string' || options.deliveryOptions))
2067
+ this.deliveryService = this.createDelivery();
2068
+ }
2069
+ createDelivery() {
2070
+ return new GatewayDeliveryService(this.options.owner, { ...this.options.deliveryOptions,
2071
+ identityOptions: this.options.deliveryOptions?.identityOptions ?? this.options.identityOptions,
2072
+ now: this.options.deliveryOptions?.now ?? this.options.now, requestTimeoutMs: this.options.connectTimeoutMs,
2073
+ resolveTransport: this.options.deliveryOptions?.resolveTransport ?? ((url, owner) => transport(url, owner, this.options)),
2074
+ assertSendAllowed: async (context) => {
2075
+ const store = this.options.connectionStore ?? new WorkerTrustStore(this.options.owner, {
2076
+ path: workerIntakePath(this.options.owner.did, this.options.identityOptions),
2077
+ fs: this.options.identityOptions?.fs, platform: this.options.identityOptions?.platform, now: this.options.now
2078
+ });
2079
+ if (store.connectionState(context.peerOwnerDid) === 'blocked')
2080
+ throw new Error('REQUEST_DENIED');
2081
+ await this.options.deliveryOptions?.assertSendAllowed?.(context);
2082
+ } });
1649
2083
  }
2084
+ async startDelivery() { this.deliveryStarted = true; await this.deliveryService?.start(); }
2085
+ stopDelivery() { this.deliveryStarted = false; this.deliveryService?.stop(); }
1650
2086
  /**
1651
2087
  * Send an encrypted REVOKE for an invite or capability to a worker inbox.
1652
2088
  * `to` is a worker DID or an acp-invite token. The notice is signed by this client.
@@ -1679,6 +2115,28 @@ export class RequesterClient {
1679
2115
  });
1680
2116
  }
1681
2117
  async delegate(options, hooks) {
2118
+ if (options.idempotencyKey !== undefined && !options.idempotencyOperation) {
2119
+ validateIdempotencyKey(options.idempotencyKey);
2120
+ options = { ...options, input: structuredClone(options.input),
2121
+ outputSchema: options.outputSchema && structuredClone(options.outputSchema),
2122
+ budget: options.budget && structuredClone(options.budget),
2123
+ capabilities: options.capabilities && structuredClone(options.capabilities) };
2124
+ options = { ...options, manifest: options.manifest && structuredClone(options.manifest),
2125
+ discover: options.discover && structuredClone(options.discover),
2126
+ capabilityChains: options.capabilityChains && structuredClone(options.capabilityChains) };
2127
+ const action = this.keyedAction(options);
2128
+ return unwrapIdempotency(await this.keyedStore().run('acp_delegate_task', options.idempotencyKey, action, async (operation) => ({ isError: false, value: await this.delegate({ ...options,
2129
+ idempotencyKey: undefined, idempotencyOperation: operation }, hooks) })));
2130
+ }
2131
+ assertConnectionsOptions(this.options);
2132
+ // Retain the reviewed values even if the caller changes its objects while awaiting consent.
2133
+ if (options.beforePropose)
2134
+ options = { ...options,
2135
+ input: structuredClone(options.input), manifest: options.manifest && structuredClone(options.manifest),
2136
+ budget: options.budget && structuredClone(options.budget),
2137
+ outputSchema: options.outputSchema && structuredClone(options.outputSchema),
2138
+ capabilities: options.capabilities && structuredClone(options.capabilities),
2139
+ };
1682
2140
  const timeoutOptions = resolveSessionTimeouts(this.options, options);
1683
2141
  validateSessionTimeouts(timeoutOptions);
1684
2142
  if (timeoutOptions.executionTimeoutMs !== undefined && timeoutOptions.executionTimeoutMs < 1000)
@@ -1689,6 +2147,9 @@ export class RequesterClient {
1689
2147
  allowPrivateRelay: this.options.allowPrivateRelay,
1690
2148
  })
1691
2149
  : undefined;
2150
+ assertConnectionsOptions(options);
2151
+ if (options.workerRef && !invite)
2152
+ throw new Error('INVITE_REFERENCE_UNAVAILABLE');
1692
2153
  const relayOption = options.relay ?? this.options.relay ?? invite?.relayUrl;
1693
2154
  if (!relayOption)
1694
2155
  throw new Error('RELAY_REQUIRED');
@@ -1702,24 +2163,30 @@ export class RequesterClient {
1702
2163
  allowPrivateRelay: this.options.allowPrivateRelay,
1703
2164
  lookup: this.options.lookup,
1704
2165
  requestTimeoutMs: timeoutOptions.connectTimeoutMs,
2166
+ maxAuthRecoveryAttempts: this.options.maxAuthRecoveryAttempts,
2167
+ authRecoveryTimeoutMs: this.options.authRecoveryTimeoutMs,
1705
2168
  }), owner = this.options.owner, inbox = typeof relayOption === 'string'
1706
- ? recipientInbox(this, relay, owner.did, relayOption.replace(/\/$/, ''))
1707
- : recipientInbox(relayOption, relay, owner.did);
2169
+ ? recipientInbox(this.deliveryService ?? this, relay, owner.did, relayOption.replace(/\/$/, ''))
2170
+ : recipientInbox(this.deliveryService ?? relayOption, relay, owner.did, this.deliveryService ? options.pendingRelayUrl ?? this.options.pendingRelayUrl : undefined);
1708
2171
  const rawCards = !invite && relay.discover ? await relay.discover() : [];
1709
2172
  if (!Array.isArray(rawCards))
1710
2173
  throw new Error('INVALID_AGENT_CARD');
1711
2174
  if (invite && options.discover)
1712
2175
  throw new Error('CONFLICTING_DISCOVERY_TARGET');
1713
2176
  const card = options.discover
1714
- ? discoverAgentCards(rawCards, { ...options.discover, accessMode: 'open',
2177
+ ? discoverAgentCards(rawCards, { ...options.discover,
1715
2178
  ...(options.to ? { did: options.to } : {}) })[0]
1716
2179
  : findDiscoveredAgentCard(rawCards, options.to);
1717
2180
  if (options.discover && !card)
1718
- throw new Error('OPEN_WORKER_NOT_FOUND');
2181
+ throw new Error('WORKER_NOT_FOUND');
1719
2182
  const peer = invite?.ownerDid ?? card?.payload.ownerDid ?? options.to;
2183
+ const protocolVersion = this.protocol.outbound(invite?.agentCard ?? card, options.renegotiateProtocol ? undefined : options.manifest?.protocolVersion, peer, invite?.agentDid ?? card?.payload.agentDid ?? peer);
2184
+ if (options.manifest && options.renegotiateProtocol)
2185
+ options = { ...options, manifest: { ...options.manifest, protocolVersion } };
1720
2186
  const manifest = options.manifest ??
1721
2187
  buildTaskManifest({
1722
2188
  ...options,
2189
+ protocolVersion,
1723
2190
  taskTimeoutMs: timeoutOptions.taskTimeoutMs ?? timeoutOptions.timeoutMs,
1724
2191
  budget: { ...options.budget, ...(timeoutOptions.executionTimeoutMs !== undefined
1725
2192
  ? { maxWallClockSeconds: options.budget?.maxWallClockSeconds ?? Math.floor(timeoutOptions.executionTimeoutMs / 1000) } : {}) },
@@ -1730,14 +2197,6 @@ export class RequesterClient {
1730
2197
  taskType: options.discover?.taskType ?? (card?.payload.skills.length === 1 &&
1731
2198
  card.payload.skills[0].taskTypes?.length === 1 ? card.payload.skills[0].taskTypes[0] : undefined),
1732
2199
  });
1733
- // Cap only implicit defaults; explicit budgets remain subject to worker admission.
1734
- if (!options.manifest && card?.payload.accessMode === 'open') {
1735
- for (const field of OPEN_POLICY_FIELDS) {
1736
- const limit = card.payload.openPolicy?.[field];
1737
- if (limit !== undefined && options.budget?.[field] === undefined)
1738
- manifest.budget[field] = Math.min(manifest.budget[field], limit);
1739
- }
1740
- }
1741
2200
  if (options.manifest && timeoutOptions.executionTimeoutMs !== undefined &&
1742
2201
  manifest.budget.maxWallClockSeconds * 1000 > timeoutOptions.executionTimeoutMs)
1743
2202
  throw new Error('INCOMPATIBLE_TIMEOUTS: manifest budget exceeds executionTimeoutMs');
@@ -1759,13 +2218,70 @@ export class RequesterClient {
1759
2218
  }
1760
2219
  if (!validateOutput(manifest.io.inputSchema, options.input))
1761
2220
  throw new Error('INPUT_SCHEMA');
2221
+ if (!this.deliveryService && typeof relayOption === 'string') {
2222
+ this.deliveryService = this.createDelivery();
2223
+ if (this.deliveryStarted)
2224
+ await this.deliveryService.start();
2225
+ }
2226
+ await this.deliveryService?.prepare(options.signal);
2227
+ this.deliveryService?.store.checkAvailable();
2228
+ const operation = options.idempotencyOperation, childIndex = options.idempotencyChildIndex ?? 0;
2229
+ const stableRelayUrl = typeof relayOption === 'string' ? relayOption : options.pendingRelayUrl ?? this.options.pendingRelayUrl;
2230
+ const approvalRelay = typeof relayOption === 'string' ? relayOption
2231
+ : 'url' in relayOption && typeof relayOption.url === 'string' ? relayOption.url : 'injected transport';
2232
+ const approvalTargetDigest = digest({ manifest, description: manifest.task.description, input: options.input,
2233
+ relay: approvalRelay, fanOut: operation?.children ?? 1 });
2234
+ if (operation)
2235
+ await operation.prepare({ childIndex, manifest, input: options.input, relayUrl: stableRelayUrl ?? approvalRelay,
2236
+ peerOwnerDid: peer, peerAgentDid: manifest.worker.agentDid, approvalTargetDigest });
2237
+ if (options.beforePropose) {
2238
+ const approvalSignal = AbortSignal.any([
2239
+ AbortSignal.timeout(Math.max(0, Date.parse(manifest.deadline) - Date.now())),
2240
+ ...(options.signal ? [options.signal] : []),
2241
+ ]);
2242
+ try {
2243
+ const evidence = await abortable(options.beforePropose({
2244
+ manifest: freezeApprovalAction(manifest), input: freezeApprovalAction(options.input),
2245
+ relay: typeof relayOption === 'string' ? relayOption
2246
+ : 'url' in relayOption && typeof relayOption.url === 'string' ? relayOption.url : 'injected transport',
2247
+ signal: approvalSignal,
2248
+ }), approvalSignal);
2249
+ approvalSignal.throwIfAborted();
2250
+ await operation?.authorize(childIndex, evidence);
2251
+ }
2252
+ catch (error) {
2253
+ if (approvalSignal.aborted)
2254
+ throw new Error(options.signal?.aborted ? 'APPROVAL_ABORTED' : 'APPROVAL_TIMEOUT');
2255
+ throw error;
2256
+ }
2257
+ if (invite)
2258
+ parseAndVerifyInviteToken(options.to, Date.now(), {
2259
+ allowPrivateRelay: this.options.allowPrivateRelay,
2260
+ });
2261
+ if (Date.parse(manifest.deadline) <= Date.now())
2262
+ throw new Error('APPROVAL_TIMEOUT');
2263
+ }
2264
+ if (operation && !options.beforePropose)
2265
+ await operation.authorize(childIndex, {
2266
+ targetDigest: approvalTargetDigest, approvedAt: (this.options.now ?? Date.now)(), policy: 'configured-auto',
2267
+ });
2268
+ await operation?.sending();
2269
+ const wireDelivery = this.deliveryService && stableRelayUrl ? {
2270
+ service: this.deliveryService, context: { messageId: '00000000-0000-4000-8000-000000000000',
2271
+ peerOwnerDid: peer, relayUrl: stableRelayUrl, deliveryDeadline: Date.parse(manifest.deadline),
2272
+ message: { taskId: manifest.taskId, peerAgentDid: manifest.worker.agentDid,
2273
+ protocolVersion: manifest.protocolVersion,
2274
+ ...(operation ? { operationId: operation.operationId } : {}) } },
2275
+ } : undefined;
1762
2276
  const auditPath = options.auditPath ?? this.options.auditPath;
1763
2277
  freshAudit(auditPath);
1764
2278
  const bridge = new AbortController();
2279
+ if (this.deliveryService && stableRelayUrl)
2280
+ inbox.setDelivery(this.deliveryService, stableRelayUrl, this.deliveryStarted || this.deliveryService.statusRouter.active);
1765
2281
  const mailbox = inbox.attach(manifest.taskId);
1766
2282
  const timeouts = new SessionTimeouts(timeoutOptions, manifest.deadline, bridge.signal);
1767
2283
  const signal = timeouts.signal;
1768
- const wire = new SessionWire(mailboxRelay(relay, mailbox), owner, peer, manifest.taskId, [owner.did, peer], signal, auditPath, this.options.allowUnencryptedEnvelopes === true);
2284
+ const wire = new SessionWire(mailboxRelay(relay, mailbox), owner, peer, manifest.taskId, [owner.did, peer], signal, auditPath, this.options.allowUnencryptedEnvelopes === true, false, wireDelivery);
1769
2285
  if (onProgress) {
1770
2286
  wire.onProgress = async (progress, envelope) => {
1771
2287
  await onProgress(progress, {
@@ -1799,6 +2315,8 @@ export class RequesterClient {
1799
2315
  .catch(() => undefined);
1800
2316
  };
1801
2317
  const onUserAbort = () => {
2318
+ if (wireDelivery)
2319
+ wireDelivery.service.closeMessage(wireDelivery.context.messageId, 'CANCELLED');
1802
2320
  const state = gateway?.machine.state;
1803
2321
  if (state === 'IN_PROGRESS')
1804
2322
  startCancel();
@@ -1827,12 +2345,46 @@ export class RequesterClient {
1827
2345
  const leaf = createCapability({ ...root.payload, audience: peer, maxDepth: 0 }, owner, root);
1828
2346
  return [root, leaf];
1829
2347
  });
2348
+ let connectionRequest;
2349
+ let connectionRouter;
2350
+ if (this.options.connectionReview && protocolVersion === '0.2.0') {
2351
+ const relayUrl = invite?.relayUrl ?? card?.payload.relayEndpoint ?? this.options.pendingRelayUrl;
2352
+ if (!relayUrl)
2353
+ throw new Error('RELAY_REQUIRED');
2354
+ const store = this.options.connectionStore ?? new WorkerTrustStore(owner, { path: this.options.identityOptions
2355
+ ? workerIntakePath(owner.did, this.options.identityOptions) : undefined, now: this.options.now });
2356
+ if (store.ownerDid !== owner.did)
2357
+ throw new Error('CONNECTION_OWNER_MISMATCH');
2358
+ if (store.connectionState(peer) !== 'connected') {
2359
+ connectionRouter = new ConnectionRouter({ owner, relay, relayUrl, store,
2360
+ agentCard: createSignedAgentCard({ name: 'Sealbridge requester', agentDid: owner.did,
2361
+ relayEndpoint: relayUrl, skills: [], protocolVersions: this.protocol.receiveVersions() }, owner),
2362
+ review: new HeldRequestReview(owner, { ...this.options.identityOptions, now: this.options.now }),
2363
+ humanReview: this.options.connectionReview, now: this.options.now, allowPrivateRelay: this.options.allowPrivateRelay,
2364
+ onConnectionEvent: this.options.onConnectionEvent });
2365
+ try {
2366
+ const request = await connectionRouter.service.request({ toOwnerDid: peer,
2367
+ ...(invite ? { inviteToken: options.to } : card ? { peerAgentCard: card } : {}), originProposalDigest: digest({ manifest, input: options.input }), deferDelivery: true });
2368
+ connectionRequest = store.controlSnapshot().find(r => r.offer.requestId === request.requestId)?.outbox[0]?.envelope;
2369
+ }
2370
+ catch (error) {
2371
+ if (!(error instanceof Error) || error.message !== 'APPROVAL_REJECTED')
2372
+ throw error;
2373
+ }
2374
+ }
2375
+ }
2376
+ if (connectionRouter)
2377
+ inbox.setConnections(connectionRouter);
2378
+ // Recheck the live phase immediately before transmission; owner review may cross a boundary.
2379
+ this.protocol.outbound(invite?.agentCard ?? card, manifest.protocolVersion, peer, manifest.worker.agentDid);
2380
+ options.beforeTransmit?.();
1830
2381
  await wire.sendEvent('PROPOSE', {
1831
2382
  proposal: signPayload(manifest, owner),
1832
2383
  input: options.input,
1833
2384
  capabilityChains,
1834
2385
  peerIdleTimeoutMs: timeoutOptions.transportIdleTimeoutMs ?? 90_000,
1835
2386
  ...(invite ? { inviteToken: options.to } : {}),
2387
+ ...(connectionRequest ? { connectionRequest } : {}),
1836
2388
  });
1837
2389
  const accept = (await wire.receiveEvent('ACCEPT')).payload.payload;
1838
2390
  if (!verifyContract(accept.contract) ||
@@ -1856,9 +2408,15 @@ export class RequesterClient {
1856
2408
  gate: 'final_verification',
1857
2409
  target: data,
1858
2410
  details: { manifest, deliverableDigest: data.artifact.payload.digest },
2411
+ deadline: manifest.deadline,
1859
2412
  signal,
1860
2413
  });
1861
- const { result, verification, settlement } = await abortable(gateway.verifyAndSettle(wire.sealed(envelope), this.options.verificationEngine ?? new VerificationEngine(), approval, { allowUnencryptedEnvelopes: this.options.allowUnencryptedEnvelopes === true }), signal);
2414
+ const { result, verification, settlement } = await abortable(gateway.verifyAndSettle(wire.sealed(envelope), this.options.verificationEngine ?? new VerificationEngine(), approval, { allowUnencryptedEnvelopes: this.options.allowUnencryptedEnvelopes === true, onVerifiedDelivery: () => {
2415
+ if (!wireDelivery)
2416
+ return;
2417
+ wireDelivery.service.markPeerObserved(wireDelivery.context.messageId, { status: 'replied', envelopeId: wireDelivery.context.messageId,
2418
+ replyEnvelopeId: receiptFileId(envelope.payload.envelopeId) });
2419
+ } }), signal);
1862
2420
  if (!result.accepted || !settlement) {
1863
2421
  await wire.sendEvent('VERIFY_REJECT', verification);
1864
2422
  throw new Error('VERIFICATION_REJECTED: ' + result.reason);
@@ -1884,6 +2442,27 @@ export class RequesterClient {
1884
2442
  };
1885
2443
  }
1886
2444
  catch (e) {
2445
+ if (e instanceof PeerRejectedError && wireDelivery) {
2446
+ if (e.code === 'PENDING_OWNER_APPROVAL')
2447
+ wireDelivery.service.markPeerObserved(wireDelivery.context.messageId, { status: 'received_held', envelopeId: wireDelivery.context.messageId });
2448
+ wireDelivery.service.closeMessage(wireDelivery.context.messageId, e.code ?? 'PEER_REJECTED');
2449
+ }
2450
+ if (e instanceof PeerRejectedError && e.code === 'PENDING_OWNER_APPROVAL' && this.options.pendingStore) {
2451
+ let pendingId = options.pendingId, pendingStoreError;
2452
+ if (!pendingId) {
2453
+ try {
2454
+ pendingId = this.options.pendingStore.save({ ...(options.workerRef
2455
+ ? { workerRef: options.workerRef } : { to: invite ? options.to : peer }),
2456
+ relayUrl: typeof relayOption === 'string' ? relayOption : options.pendingRelayUrl ?? this.options.pendingRelayUrl ??
2457
+ ('url' in relayOption && typeof relayOption.url === 'string' ? relayOption.url : undefined),
2458
+ manifest, input: options.input, timeouts: timeoutOptions });
2459
+ }
2460
+ catch (error) {
2461
+ pendingStoreError = pendingErrorCode(error);
2462
+ }
2463
+ }
2464
+ Object.assign(e, { pendingId, pendingStoreError, originalTaskId: manifest.taskId, nextStep: PENDING_NEXT_STEP });
2465
+ }
1887
2466
  try {
1888
2467
  if (e instanceof PeerRejectedError && e.recovery) {
1889
2468
  if (e.recovery.proposalDigest !== digest({ manifest, input: options.input }))
@@ -1912,11 +2491,11 @@ export class RequesterClient {
1912
2491
  return cached;
1913
2492
  }
1914
2493
  }
1915
- if (verifiedReceipt)
2494
+ if (!wire.hasCommittedSend || e instanceof PeerRejectedError || verifiedReceipt)
1916
2495
  throw e;
1917
2496
  // A closed recipient inbox is terminal for every attached task. Do not wait
1918
2497
  // for another relay send (or cancellation handshake) on the failed connection.
1919
- if (e instanceof Error && 'code' in e && ['RELAY_TERMINAL', 'RELAY_ENVELOPE_EXPIRED'].includes(String(e.code)))
2498
+ if (e instanceof Error && 'code' in e && ['RELAY_TERMINAL', 'RELAY_AUTH_RECOVERY_EXHAUSTED', 'RELAY_ENVELOPE_EXPIRED'].includes(String(e.code)))
1920
2499
  throw e;
1921
2500
  if (cancelStarted) {
1922
2501
  try {
@@ -1953,6 +2532,17 @@ export class RequesterClient {
1953
2532
  }
1954
2533
  }
1955
2534
  async delegateMulti(options) {
2535
+ if (options.idempotencyKey !== undefined && !options.idempotencyOperation) {
2536
+ validateIdempotencyKey(options.idempotencyKey);
2537
+ options = { ...options, input: structuredClone(options.input),
2538
+ outputSchema: options.outputSchema && structuredClone(options.outputSchema),
2539
+ budget: options.budget && structuredClone(options.budget),
2540
+ capabilities: options.capabilities && structuredClone(options.capabilities) };
2541
+ options = { ...options, workers: structuredClone(options.workers) };
2542
+ const action = this.keyedAction(options);
2543
+ return unwrapIdempotency(await this.keyedStore().run('acp_delegate_multi', options.idempotencyKey, action, async (operation) => ({ isError: false, value: await this.delegateMulti({ ...options,
2544
+ idempotencyKey: undefined, idempotencyOperation: operation }) }), { children: options.workers.length }));
2545
+ }
1956
2546
  if (!Array.isArray(options.workers) || options.workers.length === 0)
1957
2547
  throw new Error('WORKERS_REQUIRED');
1958
2548
  if (options.mode !== 'race' && options.mode !== 'quorum')
@@ -1986,6 +2576,8 @@ export class RequesterClient {
1986
2576
  const receipts = [];
1987
2577
  const incompleteReceipts = [];
1988
2578
  let failures = 0;
2579
+ const approvalFailures = [];
2580
+ const allApprovalFailed = () => approvalFailures.length === count ? approvalFailures[0] : undefined;
1989
2581
  let winnerDigest;
1990
2582
  const abortUnfinished = (reason) => {
1991
2583
  controllers.forEach((controller, index) => {
@@ -2024,6 +2616,9 @@ export class RequesterClient {
2024
2616
  minimumIsolationLevel: options.minimumIsolationLevel,
2025
2617
  signal,
2026
2618
  onProgress: options.onProgress,
2619
+ beforePropose: options.beforePropose,
2620
+ idempotencyOperation: options.idempotencyOperation,
2621
+ idempotencyChildIndex: index,
2027
2622
  auditPath: options.auditPath
2028
2623
  ? options.auditPath.replace(/(\.jsonl)?$/, '.' + index + '.jsonl')
2029
2624
  : undefined,
@@ -2056,6 +2651,10 @@ export class RequesterClient {
2056
2651
  catch (error) {
2057
2652
  if (error instanceof IncompleteSettlementError)
2058
2653
  incompleteReceipts.push(error.incompleteReceipt);
2654
+ await options.idempotencyOperation?.childReady(index, error);
2655
+ if ((options.beforePropose || options.idempotencyOperation) && error instanceof Error &&
2656
+ /^(APPROVAL_|IDEMPOTENCY_)/.test(error.message))
2657
+ approvalFailures.push(error);
2059
2658
  finished[index] = true;
2060
2659
  failures += 1;
2061
2660
  if (options.mode === 'quorum' && !winnerDigest && failures > count - quorumSize)
@@ -2072,7 +2671,7 @@ export class RequesterClient {
2072
2671
  winner = await Promise.any(jobs);
2073
2672
  }
2074
2673
  catch {
2075
- throw Object.assign(new Error('RACE_FAILED'), { receipts, incompleteReceipts });
2674
+ throw Object.assign(allApprovalFailed() ?? new Error('RACE_FAILED'), { receipts, incompleteReceipts });
2076
2675
  }
2077
2676
  abortUnfinished('RACE_LOST');
2078
2677
  await Promise.allSettled(jobs);
@@ -2099,7 +2698,7 @@ export class RequesterClient {
2099
2698
  }
2100
2699
  await Promise.allSettled(jobs);
2101
2700
  if (!winnerDigest)
2102
- throw Object.assign(new Error('QUORUM_NOT_REACHED'), { receipts, incompleteReceipts });
2701
+ throw Object.assign(allApprovalFailed() ?? new Error('QUORUM_NOT_REACHED'), { receipts, incompleteReceipts });
2103
2702
  const seenWinners = new Set();
2104
2703
  const winners = receipts.filter((receipt) => {
2105
2704
  if (receipt.outputDigest !== winnerDigest || seenWinners.has(receipt.workerDid))
@@ -2207,9 +2806,10 @@ async function canonicalizeDiscoveredWorkers(targets, options) {
2207
2806
  }));
2208
2807
  }
2209
2808
  export function buildTaskManifest(options) {
2210
- const createdAt = new Date().toISOString(), deadline = new Date(Date.now() + (options.taskTimeoutMs ?? 600000)).toISOString();
2809
+ const now = (options.now ?? Date.now)();
2810
+ const createdAt = new Date(now).toISOString(), deadline = new Date(now + (options.taskTimeoutMs ?? 600000)).toISOString();
2211
2811
  return {
2212
- protocolVersion: '0.1.0',
2812
+ protocolVersion: options.protocolVersion ?? '0.2.0',
2213
2813
  taskId: 'task_' + randomUUID().replaceAll('-', ''),
2214
2814
  createdAt,
2215
2815
  deadline,