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.
- package/README.ja.md +492 -30
- package/README.md +522 -109
- package/dist/cli/dist/accept.d.ts +13 -0
- package/dist/cli/dist/accept.js +115 -0
- package/dist/cli/dist/accept.js.map +1 -0
- package/dist/cli/dist/approval-summary.d.ts +1 -1
- package/dist/cli/dist/board.d.ts +10 -9
- package/dist/cli/dist/board.js +62 -31
- package/dist/cli/dist/board.js.map +1 -1
- package/dist/cli/dist/command-identity.d.ts +6 -0
- package/dist/cli/dist/command-identity.js +13 -0
- package/dist/cli/dist/command-identity.js.map +1 -0
- package/dist/cli/dist/config.d.ts +3 -0
- package/dist/cli/dist/config.js +29 -0
- package/dist/cli/dist/config.js.map +1 -0
- package/dist/cli/dist/connections.d.ts +15 -0
- package/dist/cli/dist/connections.js +191 -0
- package/dist/cli/dist/connections.js.map +1 -0
- package/dist/cli/dist/contacts.d.ts +4 -3
- package/dist/cli/dist/contacts.js +39 -28
- package/dist/cli/dist/contacts.js.map +1 -1
- package/dist/cli/dist/doctor.d.ts +9 -0
- package/dist/cli/dist/doctor.js +46 -5
- package/dist/cli/dist/doctor.js.map +1 -1
- package/dist/cli/dist/error-messages.js +22 -0
- package/dist/cli/dist/error-messages.js.map +1 -1
- package/dist/cli/dist/index.js +44 -6
- package/dist/cli/dist/index.js.map +1 -1
- package/dist/cli/dist/invite-output.d.ts +14 -0
- package/dist/cli/dist/invite-output.js +66 -0
- package/dist/cli/dist/invite-output.js.map +1 -0
- package/dist/cli/dist/invite-share.d.ts +6 -3
- package/dist/cli/dist/invite-share.js +40 -15
- package/dist/cli/dist/invite-share.js.map +1 -1
- package/dist/cli/dist/invites-migrate.d.ts +3 -0
- package/dist/cli/dist/invites-migrate.js +33 -0
- package/dist/cli/dist/invites-migrate.js.map +1 -0
- package/dist/cli/dist/mcp.d.ts +7 -5
- package/dist/cli/dist/mcp.js +23 -18
- package/dist/cli/dist/mcp.js.map +1 -1
- package/dist/cli/dist/network.d.ts +11 -1
- package/dist/cli/dist/network.js +150 -87
- package/dist/cli/dist/network.js.map +1 -1
- package/dist/cli/dist/pending.d.ts +8 -0
- package/dist/cli/dist/pending.js +50 -0
- package/dist/cli/dist/pending.js.map +1 -0
- package/dist/cli/dist/public-settings.d.ts +5 -4
- package/dist/cli/dist/public-settings.js +13 -15
- package/dist/cli/dist/public-settings.js.map +1 -1
- package/dist/cli/dist/receipt.d.ts +1 -1
- package/dist/cli/dist/relay-auth.d.ts +3 -0
- package/dist/cli/dist/relay-auth.js +23 -0
- package/dist/cli/dist/relay-auth.js.map +1 -0
- package/dist/cli/dist/requests.d.ts +10 -0
- package/dist/cli/dist/requests.js +91 -0
- package/dist/cli/dist/requests.js.map +1 -0
- package/dist/cli/dist/sdk.d.ts +10 -0
- package/dist/cli/dist/sdk.js +7 -0
- package/dist/cli/dist/sdk.js.map +1 -0
- package/dist/cli/dist/setup-files.d.ts +24 -0
- package/dist/cli/dist/setup-files.js +190 -0
- package/dist/cli/dist/setup-files.js.map +1 -0
- package/dist/cli/dist/setup-hosts.d.ts +29 -0
- package/dist/cli/dist/setup-hosts.js +75 -0
- package/dist/cli/dist/setup-hosts.js.map +1 -0
- package/dist/cli/dist/setup.d.ts +61 -0
- package/dist/cli/dist/setup.js +243 -0
- package/dist/cli/dist/setup.js.map +1 -0
- package/dist/cli/dist/task-input.d.ts +1 -1
- package/dist/cli/dist/wake-hook.d.ts +1 -1
- package/dist/cli/dist/worker-json.d.ts +4 -0
- package/dist/cli/dist/worker-json.js +87 -0
- package/dist/cli/dist/worker-json.js.map +1 -0
- package/dist/examples/two-owner-demo/dist/run-demo.d.ts +13 -13
- package/dist/package.json +1 -1
- package/dist/packages/adapters/dist/command.d.ts +2 -2
- package/dist/packages/adapters/dist/credential-proxy.d.ts +2 -2
- package/dist/packages/adapters/dist/index.d.ts +1 -1
- package/dist/packages/adapters/dist/mcp.d.ts +2 -2
- package/dist/packages/adapters/dist/openai.d.ts +2 -2
- package/dist/packages/adapters/dist/sandbox.d.ts +2 -2
- package/dist/packages/core/dist/connection.d.ts +58 -0
- package/dist/packages/core/dist/connection.js +132 -0
- package/dist/packages/core/dist/connection.js.map +1 -0
- package/dist/packages/core/dist/delivery-status.d.ts +22 -0
- package/dist/packages/core/dist/delivery-status.js +53 -0
- package/dist/packages/core/dist/delivery-status.js.map +1 -0
- package/dist/packages/core/dist/discovery.d.ts +7 -6
- package/dist/packages/core/dist/discovery.js +19 -20
- package/dist/packages/core/dist/discovery.js.map +1 -1
- package/dist/packages/core/dist/edge.d.ts +1 -0
- package/dist/packages/core/dist/edge.js +1 -0
- package/dist/packages/core/dist/edge.js.map +1 -1
- package/dist/packages/core/dist/index.d.ts +3 -0
- package/dist/packages/core/dist/index.js +3 -0
- package/dist/packages/core/dist/index.js.map +1 -1
- package/dist/packages/core/dist/protocol.d.ts +24 -0
- package/dist/packages/core/dist/protocol.js +54 -0
- package/dist/packages/core/dist/protocol.js.map +1 -0
- package/dist/packages/core/dist/receipts.d.ts +1 -0
- package/dist/packages/core/dist/receipts.js +1 -1
- package/dist/packages/core/dist/receipts.js.map +1 -1
- package/dist/packages/core/dist/schema.d.ts +1 -0
- package/dist/packages/core/dist/schema.js +13 -2
- package/dist/packages/core/dist/schema.js.map +1 -1
- package/dist/packages/ledger/dist/index.d.ts +1 -1
- package/dist/packages/relay/dist/admission.d.ts +14 -0
- package/dist/packages/relay/dist/admission.js +70 -0
- package/dist/packages/relay/dist/admission.js.map +1 -0
- package/dist/packages/relay/dist/fetch.d.ts +8 -2
- package/dist/packages/relay/dist/fetch.js +41 -12
- package/dist/packages/relay/dist/fetch.js.map +1 -1
- package/dist/packages/relay/dist/index.d.ts +23 -12
- package/dist/packages/relay/dist/index.js +130 -78
- package/dist/packages/relay/dist/index.js.map +1 -1
- package/dist/packages/relay/dist/journal.d.ts +1 -1
- package/dist/packages/relay/dist/notifications.d.ts +1 -1
- package/dist/packages/relay/dist/retry.d.ts +23 -0
- package/dist/packages/relay/dist/retry.js +43 -1
- package/dist/packages/relay/dist/retry.js.map +1 -1
- package/dist/packages/relay/dist/ssrf.d.ts +3 -2
- package/dist/packages/relay/dist/ssrf.js.map +1 -1
- package/dist/packages/relay/dist/worker.js +1 -1
- package/dist/packages/relay/dist/worker.js.map +1 -1
- package/dist/packages/runtime/dist/a2a.d.ts +1 -1
- package/dist/packages/runtime/dist/a2a.js +1 -1
- package/dist/packages/runtime/dist/adapter.d.ts +2 -2
- package/dist/packages/runtime/dist/application-config.d.ts +20 -0
- package/dist/packages/runtime/dist/application-config.js +222 -0
- package/dist/packages/runtime/dist/application-config.js.map +1 -0
- package/dist/packages/runtime/dist/approval-providers.d.ts +24 -3
- package/dist/packages/runtime/dist/approval-providers.js +79 -3
- package/dist/packages/runtime/dist/approval-providers.js.map +1 -1
- package/dist/packages/runtime/dist/connection-admission-review.d.ts +39 -0
- package/dist/packages/runtime/dist/connection-admission-review.js +86 -0
- package/dist/packages/runtime/dist/connection-admission-review.js.map +1 -0
- package/dist/packages/runtime/dist/connection-persistence.d.ts +35 -0
- package/dist/packages/runtime/dist/connection-persistence.js +181 -0
- package/dist/packages/runtime/dist/connection-persistence.js.map +1 -0
- package/dist/packages/runtime/dist/connection-records.d.ts +167 -0
- package/dist/packages/runtime/dist/connection-records.js +132 -0
- package/dist/packages/runtime/dist/connection-records.js.map +1 -0
- package/dist/packages/runtime/dist/connection-router.d.ts +45 -0
- package/dist/packages/runtime/dist/connection-router.js +130 -0
- package/dist/packages/runtime/dist/connection-router.js.map +1 -0
- package/dist/packages/runtime/dist/connection-service.d.ts +159 -0
- package/dist/packages/runtime/dist/connection-service.js +239 -0
- package/dist/packages/runtime/dist/connection-service.js.map +1 -0
- package/dist/packages/runtime/dist/connection-transport.d.ts +14 -0
- package/dist/packages/runtime/dist/connection-transport.js +89 -0
- package/dist/packages/runtime/dist/connection-transport.js.map +1 -0
- package/dist/packages/runtime/dist/delivery-records.d.ts +180 -0
- package/dist/packages/runtime/dist/delivery-records.js +205 -0
- package/dist/packages/runtime/dist/delivery-records.js.map +1 -0
- package/dist/packages/runtime/dist/delivery-status-router.d.ts +27 -0
- package/dist/packages/runtime/dist/delivery-status-router.js +150 -0
- package/dist/packages/runtime/dist/delivery-status-router.js.map +1 -0
- package/dist/packages/runtime/dist/delivery-status.d.ts +43 -0
- package/dist/packages/runtime/dist/delivery-status.js +193 -0
- package/dist/packages/runtime/dist/delivery-status.js.map +1 -0
- package/dist/packages/runtime/dist/delivery-store.d.ts +125 -0
- package/dist/packages/runtime/dist/delivery-store.js +1047 -0
- package/dist/packages/runtime/dist/delivery-store.js.map +1 -0
- package/dist/packages/runtime/dist/gateway.d.ts +4 -3
- package/dist/packages/runtime/dist/gateway.js.map +1 -1
- package/dist/packages/runtime/dist/held-request-review.d.ts +49 -0
- package/dist/packages/runtime/dist/held-request-review.js +122 -0
- package/dist/packages/runtime/dist/held-request-review.js.map +1 -0
- package/dist/packages/runtime/dist/held-request-store.d.ts +112 -0
- package/dist/packages/runtime/dist/held-request-store.js +290 -0
- package/dist/packages/runtime/dist/held-request-store.js.map +1 -0
- package/dist/packages/runtime/dist/idempotency-store.d.ts +53 -0
- package/dist/packages/runtime/dist/idempotency-store.js +331 -0
- package/dist/packages/runtime/dist/idempotency-store.js.map +1 -0
- package/dist/packages/runtime/dist/inbox.d.ts +1 -1
- package/dist/packages/runtime/dist/index.d.ts +27 -1
- package/dist/packages/runtime/dist/index.js +26 -0
- package/dist/packages/runtime/dist/index.js.map +1 -1
- package/dist/packages/runtime/dist/invite-store.d.ts +76 -0
- package/dist/packages/runtime/dist/invite-store.js +337 -0
- package/dist/packages/runtime/dist/invite-store.js.map +1 -0
- package/dist/packages/runtime/dist/key-store.d.ts +77 -0
- package/dist/packages/runtime/dist/key-store.js +226 -0
- package/dist/packages/runtime/dist/key-store.js.map +1 -0
- package/dist/packages/runtime/dist/local-identity.d.ts +33 -0
- package/dist/packages/runtime/dist/local-identity.js +278 -0
- package/dist/packages/runtime/dist/local-identity.js.map +1 -0
- package/dist/packages/runtime/dist/mcp-rpc.d.ts +19 -0
- package/dist/packages/runtime/dist/mcp-rpc.js +175 -0
- package/dist/packages/runtime/dist/mcp-rpc.js.map +1 -0
- package/dist/packages/runtime/dist/mcp-server.d.ts +46 -19
- package/dist/packages/runtime/dist/mcp-server.js +427 -184
- package/dist/packages/runtime/dist/mcp-server.js.map +1 -1
- package/dist/packages/runtime/dist/mcp-tool-catalog.d.ts +1 -1
- package/dist/packages/runtime/dist/mcp-tool-catalog.js +172 -13
- package/dist/packages/runtime/dist/mcp-tool-catalog.js.map +1 -1
- package/dist/packages/runtime/dist/mcp.d.ts +2 -2
- package/dist/packages/runtime/dist/message-status.d.ts +32 -0
- package/dist/packages/runtime/dist/message-status.js +29 -0
- package/dist/packages/runtime/dist/message-status.js.map +1 -0
- package/dist/packages/runtime/dist/outbox.d.ts +91 -0
- package/dist/packages/runtime/dist/outbox.js +462 -0
- package/dist/packages/runtime/dist/outbox.js.map +1 -0
- package/dist/packages/runtime/dist/policy.d.ts +1 -1
- package/dist/packages/runtime/dist/protocol-peer-store.d.ts +40 -0
- package/dist/packages/runtime/dist/protocol-peer-store.js +176 -0
- package/dist/packages/runtime/dist/protocol-peer-store.js.map +1 -0
- package/dist/packages/runtime/dist/protocol-release.d.ts +26 -0
- package/dist/packages/runtime/dist/protocol-release.js +76 -0
- package/dist/packages/runtime/dist/protocol-release.js.map +1 -0
- package/dist/packages/runtime/dist/quota.d.ts +1 -1
- package/dist/packages/runtime/dist/relay-selection.d.ts +20 -0
- package/dist/packages/runtime/dist/relay-selection.js +34 -0
- package/dist/packages/runtime/dist/relay-selection.js.map +1 -0
- package/dist/packages/runtime/dist/requester-pending-store.d.ts +59 -0
- package/dist/packages/runtime/dist/requester-pending-store.js +266 -0
- package/dist/packages/runtime/dist/requester-pending-store.js.map +1 -0
- package/dist/packages/runtime/dist/secret-store.d.ts +19 -0
- package/dist/packages/runtime/dist/secret-store.js +51 -0
- package/dist/packages/runtime/dist/secret-store.js.map +1 -0
- package/dist/packages/runtime/dist/session-audit.d.ts +29 -7
- package/dist/packages/runtime/dist/session-audit.js +149 -29
- package/dist/packages/runtime/dist/session-audit.js.map +1 -1
- package/dist/packages/runtime/dist/session.d.ts +101 -9
- package/dist/packages/runtime/dist/session.js +696 -96
- package/dist/packages/runtime/dist/session.js.map +1 -1
- package/dist/packages/runtime/dist/task-journal.d.ts +18 -3
- package/dist/packages/runtime/dist/task-journal.js +271 -18
- package/dist/packages/runtime/dist/task-journal.js.map +1 -1
- package/dist/packages/runtime/dist/verifier.d.ts +3 -2
- package/dist/packages/runtime/dist/verifier.js +1 -0
- package/dist/packages/runtime/dist/verifier.js.map +1 -1
- package/dist/packages/runtime/dist/worker-trust-store.d.ts +111 -0
- package/dist/packages/runtime/dist/worker-trust-store.js +592 -0
- package/dist/packages/runtime/dist/worker-trust-store.js.map +1 -0
- package/dist/packages/sandbox/dist/container.d.ts +1 -1
- package/dist/packages/sandbox/dist/index.d.ts +1 -1
- package/dist/spec/README.ja.md +21 -1
- package/dist/spec/README.md +20 -1
- package/dist/spec/agent-card.schema.json +11 -1
- package/dist/spec/delivery-status.schema.json +106 -0
- package/dist/spec/examples/protocol-versions/sample-agent-card.json +35 -0
- package/dist/spec/examples/sample-delivery-status.json +21 -0
- package/dist/spec/examples/sample-task-manifest.0.1.0.json +173 -0
- package/dist/spec/examples/sample-task-manifest.0.2.0.json +173 -0
- package/dist/spec/task-manifest.0.1.0.schema.json +422 -0
- package/dist/spec/task-manifest.0.2.0.schema.json +422 -0
- package/dist/spec/task-manifest.schema.json +9 -418
- package/docs/sdk.ja.md +133 -0
- package/docs/sdk.md +138 -0
- 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,
|
|
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) =>
|
|
79
|
-
|
|
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
|
|
283
|
-
|
|
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(
|
|
540
|
+
expiresAt: new Date(now + options.inviteTtlSeconds * 1000).toISOString(),
|
|
417
541
|
} : {}),
|
|
418
|
-
|
|
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
|
|
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.
|
|
580
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (this.
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
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.
|
|
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) <=
|
|
814
|
-
Date.parse(manifest.createdAt) >
|
|
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.
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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) =>
|
|
1286
|
-
|
|
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 (
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
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,
|
|
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('
|
|
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
|
|
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
|
|
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.
|
|
2812
|
+
protocolVersion: options.protocolVersion ?? '0.2.0',
|
|
2213
2813
|
taskId: 'task_' + randomUUID().replaceAll('-', ''),
|
|
2214
2814
|
createdAt,
|
|
2215
2815
|
deadline,
|