sealbridge 0.4.2 → 0.6.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 +6 -4
- package/README.md +9 -4
- package/dist/cli/dist/accept.d.ts +2 -1
- package/dist/cli/dist/accept.js +18 -7
- package/dist/cli/dist/accept.js.map +1 -1
- package/dist/cli/dist/index.js +46 -13
- package/dist/cli/dist/index.js.map +1 -1
- package/dist/cli/dist/invite-links.d.ts +14 -0
- package/dist/cli/dist/invite-links.js +263 -0
- package/dist/cli/dist/invite-links.js.map +1 -0
- package/dist/cli/dist/invite-output.d.ts +1 -1
- package/dist/cli/dist/invite-output.js +3 -2
- package/dist/cli/dist/invite-output.js.map +1 -1
- package/dist/cli/dist/network.js +16 -23
- package/dist/cli/dist/network.js.map +1 -1
- package/dist/cli/dist/receipt.d.ts +12 -2
- package/dist/cli/dist/receipt.js +6 -5
- package/dist/cli/dist/receipt.js.map +1 -1
- package/dist/cli/dist/sdk.d.ts +6 -4
- package/dist/cli/dist/sdk.js +3 -2
- package/dist/cli/dist/sdk.js.map +1 -1
- package/dist/cli/dist/task-review.d.ts +1 -8
- package/dist/cli/dist/task-review.js +4 -17
- package/dist/cli/dist/task-review.js.map +1 -1
- package/dist/cli/dist/tasks.js +1 -1
- package/dist/cli/dist/tasks.js.map +1 -1
- package/dist/examples/two-owner-demo/dist/run-demo.js +3 -1
- package/dist/examples/two-owner-demo/dist/run-demo.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/packages/core/dist/content-inspection-rules.d.ts +22 -0
- package/dist/packages/core/dist/content-inspection-rules.js +34 -0
- package/dist/packages/core/dist/content-inspection-rules.js.map +1 -0
- package/dist/packages/core/dist/content-inspection.d.ts +43 -0
- package/dist/packages/core/dist/content-inspection.js +192 -0
- package/dist/packages/core/dist/content-inspection.js.map +1 -0
- package/dist/packages/core/dist/context-grant.d.ts +13 -0
- package/dist/packages/core/dist/context-grant.js +65 -0
- package/dist/packages/core/dist/context-grant.js.map +1 -0
- package/dist/packages/core/dist/delegation.d.ts +17 -0
- package/dist/packages/core/dist/delegation.js +126 -0
- package/dist/packages/core/dist/delegation.js.map +1 -0
- package/dist/packages/core/dist/edge.d.ts +2 -0
- package/dist/packages/core/dist/edge.js +2 -0
- package/dist/packages/core/dist/edge.js.map +1 -1
- package/dist/packages/core/dist/index.d.ts +6 -0
- package/dist/packages/core/dist/index.js +6 -0
- package/dist/packages/core/dist/index.js.map +1 -1
- package/dist/packages/core/dist/invite-link.d.ts +72 -0
- package/dist/packages/core/dist/invite-link.js +225 -0
- package/dist/packages/core/dist/invite-link.js.map +1 -0
- package/dist/packages/core/dist/invite-sas.d.ts +7 -0
- package/dist/packages/core/dist/invite-sas.js +15 -0
- package/dist/packages/core/dist/invite-sas.js.map +1 -0
- package/dist/packages/core/dist/relay-auth.d.ts +1 -1
- package/dist/packages/core/dist/relay-auth.js +1 -1
- package/dist/packages/core/dist/relay-auth.js.map +1 -1
- package/dist/packages/core/dist/schema.d.ts +1 -1
- package/dist/packages/core/dist/schema.js +4 -0
- package/dist/packages/core/dist/schema.js.map +1 -1
- package/dist/packages/core/dist/types.d.ts +52 -0
- package/dist/packages/ledger/dist/index.d.ts +70 -2
- package/dist/packages/ledger/dist/index.js +404 -26
- package/dist/packages/ledger/dist/index.js.map +1 -1
- package/dist/packages/relay/dist/fetch.js +4 -0
- package/dist/packages/relay/dist/fetch.js.map +1 -1
- package/dist/packages/relay/dist/invites.d.ts +42 -0
- package/dist/packages/relay/dist/invites.js +347 -0
- package/dist/packages/relay/dist/invites.js.map +1 -0
- package/dist/packages/relay/dist/worker.d.ts +3 -0
- package/dist/packages/relay/dist/worker.js +21 -0
- package/dist/packages/relay/dist/worker.js.map +1 -1
- package/dist/packages/runtime/dist/a2a.js +1 -1
- package/dist/packages/runtime/dist/adapter.d.ts +1 -0
- package/dist/packages/runtime/dist/application-config.js +69 -6
- package/dist/packages/runtime/dist/application-config.js.map +1 -1
- package/dist/packages/runtime/dist/approval-providers.d.ts +4 -0
- package/dist/packages/runtime/dist/approval-providers.js +3 -0
- package/dist/packages/runtime/dist/approval-providers.js.map +1 -1
- package/dist/packages/runtime/dist/connection-persistence.d.ts +1 -1
- package/dist/packages/runtime/dist/context-grant-audit.d.ts +8 -0
- package/dist/packages/runtime/dist/context-grant-audit.js +41 -0
- package/dist/packages/runtime/dist/context-grant-audit.js.map +1 -0
- package/dist/packages/runtime/dist/delegation.d.ts +37 -0
- package/dist/packages/runtime/dist/delegation.js +125 -0
- package/dist/packages/runtime/dist/delegation.js.map +1 -0
- package/dist/packages/runtime/dist/gateway.d.ts +1 -0
- package/dist/packages/runtime/dist/gateway.js.map +1 -1
- package/dist/packages/runtime/dist/held-request-store.d.ts +2 -0
- package/dist/packages/runtime/dist/held-request-store.js +3 -2
- package/dist/packages/runtime/dist/held-request-store.js.map +1 -1
- package/dist/packages/runtime/dist/held-task-content.d.ts +14 -0
- package/dist/packages/runtime/dist/held-task-content.js +33 -0
- package/dist/packages/runtime/dist/held-task-content.js.map +1 -0
- package/dist/packages/runtime/dist/index.d.ts +4 -0
- package/dist/packages/runtime/dist/index.js +4 -0
- package/dist/packages/runtime/dist/index.js.map +1 -1
- package/dist/packages/runtime/dist/mcp-server.js +22 -8
- package/dist/packages/runtime/dist/mcp-server.js.map +1 -1
- package/dist/packages/runtime/dist/mcp-tool-catalog.js +44 -5
- package/dist/packages/runtime/dist/mcp-tool-catalog.js.map +1 -1
- package/dist/packages/runtime/dist/rule-content-review-child.d.ts +1 -0
- package/dist/packages/runtime/dist/rule-content-review-child.js +25 -0
- package/dist/packages/runtime/dist/rule-content-review-child.js.map +1 -0
- package/dist/packages/runtime/dist/rule-content-review.d.ts +9 -0
- package/dist/packages/runtime/dist/rule-content-review.js +217 -0
- package/dist/packages/runtime/dist/rule-content-review.js.map +1 -0
- package/dist/packages/runtime/dist/session-audit.d.ts +6 -1
- package/dist/packages/runtime/dist/session-audit.js +35 -4
- package/dist/packages/runtime/dist/session-audit.js.map +1 -1
- package/dist/packages/runtime/dist/session.d.ts +21 -1
- package/dist/packages/runtime/dist/session.js +429 -63
- package/dist/packages/runtime/dist/session.js.map +1 -1
- package/dist/packages/runtime/dist/task-journal.d.ts +10 -1
- package/dist/packages/runtime/dist/task-journal.js +26 -1
- package/dist/packages/runtime/dist/task-journal.js.map +1 -1
- package/dist/packages/runtime/dist/verifier.d.ts +5 -2
- package/dist/packages/runtime/dist/verifier.js +14 -4
- package/dist/packages/runtime/dist/verifier.js.map +1 -1
- package/dist/spec/audit-entry.schema.json +25 -1
- package/dist/spec/audit-evidence.schema.json +179 -0
- package/dist/spec/audit-file-record.schema.json +177 -0
- package/dist/spec/audit-interruption.schema.json +110 -0
- package/dist/spec/context-grant-audit-payload.schema.json +24 -0
- package/dist/spec/context-grant.schema.json +146 -0
- package/dist/spec/delegation-attachment.schema.json +167 -0
- package/dist/spec/delegation-link.schema.json +141 -0
- package/dist/spec/examples/context-pooling/approval_redelegation/sample-delegation-attachment.json +413 -0
- package/dist/spec/examples/context-pooling/hand_over/sample-context-grant.json +18 -0
- package/dist/spec/examples/context-pooling/hand_over/sample-task-manifest.0.2.0.json +94 -0
- package/dist/spec/examples/context-pooling/sample-context-grant-audit-payload.json +4 -0
- package/dist/spec/examples/context-pooling/view_only/sample-context-grant.json +18 -0
- package/dist/spec/examples/context-pooling/view_only/sample-task-manifest.0.2.0.json +94 -0
- package/dist/spec/examples/sample-audit-evidence.json +138 -0
- package/dist/spec/examples/sample-audit-file-record.json +31 -0
- package/dist/spec/examples/sample-audit-interruption.json +29 -0
- package/dist/spec/task-manifest.0.2.0.schema.json +87 -1
- package/docs/context-grants.md +221 -0
- package/docs/sdk.ja.md +108 -2
- package/docs/sdk.md +120 -2
- package/package.json +3 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { ApprovalGate } from './policy.js';
|
|
3
|
+
import { assembleDelegation, redelegationTarget, requireOwnerDecision, verifyRecoveredDelegation, verifyDelegationAttachment } from './delegation.js';
|
|
2
4
|
import { IdempotencyStore, runtimeIdempotencyAction, unwrapIdempotency, validateIdempotencyKey } from './idempotency-store.js';
|
|
3
5
|
import { RecipientDeliveryCoordinator } from './delivery-status.js';
|
|
4
6
|
import { setDeliveryStatusReaderFactory } from './delivery-status-router.js';
|
|
@@ -9,8 +11,9 @@ import { existsSync, openSync, closeSync, fstatSync, fchmodSync, appendFileSync,
|
|
|
9
11
|
import { join } from 'node:path';
|
|
10
12
|
import { tmpdir } from 'node:os';
|
|
11
13
|
import { randomUUID } from 'node:crypto';
|
|
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';
|
|
13
|
-
import {
|
|
14
|
+
import { createContract, assertDelegationAttenuation, createDelegationLink, 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, buildContextGrant, validateContextGrantUse, } from '../../core/dist/index.js';
|
|
15
|
+
import { CONTEXT_GRANT_AUDIT_EVENTS, proposalAuditDigest, verifyContextAcceptanceAudit } from './context-grant-audit.js';
|
|
16
|
+
import { parseJsonl, verifyAudit, createAuditInterruption, JsonlStore } from '../../ledger/dist/index.js';
|
|
14
17
|
import { HttpRelayClient, waitForRelayRetry, relayDiagnostic, relayDeadline, validateRelayAuthRecoveryOptions } from '../../relay/dist/index.js';
|
|
15
18
|
import { InteractiveCliApprovalProvider, ProposalRejectedError, evaluateContractAcceptance, } from './approval-providers.js';
|
|
16
19
|
import { OwnerGateway } from './gateway.js';
|
|
@@ -175,6 +178,7 @@ const WORKER_BOUND_EVENTS = new Set([
|
|
|
175
178
|
'CHECKPOINT',
|
|
176
179
|
]);
|
|
177
180
|
const REQUESTER_BOUND_EVENTS = new Set([
|
|
181
|
+
...CONTEXT_GRANT_AUDIT_EVENTS,
|
|
178
182
|
'ACCEPT',
|
|
179
183
|
'ATTEST',
|
|
180
184
|
'START',
|
|
@@ -457,6 +461,8 @@ export class WorkerNode {
|
|
|
457
461
|
assertConnectionsOptions(options);
|
|
458
462
|
validateRelayAuthRecoveryOptions(options);
|
|
459
463
|
validateSessionTimeouts(options);
|
|
464
|
+
if (options.auditInterruptionFormat !== undefined && options.auditInterruptionFormat !== 1)
|
|
465
|
+
throw new Error('UNSUPPORTED_AUDIT_INTERRUPTION_FORMAT');
|
|
460
466
|
this.protocol = new LiveProtocol(options);
|
|
461
467
|
this.intakeStore = options.intakeStore ?? new WorkerTrustStore(options.owner, {
|
|
462
468
|
path: options.identityOptions ? workerIntakePath(options.owner.did, options.identityOptions) : undefined,
|
|
@@ -707,6 +713,23 @@ export class WorkerNode {
|
|
|
707
713
|
return protocolVersion === '0.2.0' && this.connectionStore.connectionState(did) === 'connected' ? 'limited' : 'unknown';
|
|
708
714
|
}
|
|
709
715
|
async notifyOutcome(record, outcome = record.outcome) {
|
|
716
|
+
if (outcome.result?.state === 'SETTLED') {
|
|
717
|
+
const entries = outcome.result.bundle.entries;
|
|
718
|
+
const proposal = entries.find(e => e.body.eventType === 'PROPOSE')?.body.payload;
|
|
719
|
+
const accepted = entries.find(e => e.body.eventType === 'ACCEPT')?.body.payload;
|
|
720
|
+
const delivered = entries.find(e => e.body.eventType === 'DELIVER')?.body.payload;
|
|
721
|
+
const retainedManifest = accepted?.contract?.manifest ??
|
|
722
|
+
proposal?.proposal?.payload;
|
|
723
|
+
const grantBearing = record.delegation || (retainedManifest ? !!retainedManifest.contextGrant :
|
|
724
|
+
!!proposal?.grantDigest);
|
|
725
|
+
if (grantBearing) {
|
|
726
|
+
if (!delivered || proposalAuditDigest(proposal) !== record.proposalDigest)
|
|
727
|
+
throw new Error('DELEGATION_INVALID');
|
|
728
|
+
verifyRecoveredDelegation(delivered, proposal, accepted, record.delegation?.parentContract);
|
|
729
|
+
if (record.delegation && digest(assembleDelegation(record.delegation)) !== digest(delivered.delegation))
|
|
730
|
+
throw new Error('DELEGATION_INVALID');
|
|
731
|
+
}
|
|
732
|
+
}
|
|
710
733
|
const send = (recovery) => this.relay.send(signEnvelope({ taskId: record.taskId,
|
|
711
734
|
recipientDid: record.requesterDid, seq: 0, eventType: 'ERROR',
|
|
712
735
|
payload: asJson({ code: outcome.code, recovery }),
|
|
@@ -736,6 +759,38 @@ export class WorkerNode {
|
|
|
736
759
|
else
|
|
737
760
|
this.taskJournal.save(record);
|
|
738
761
|
}
|
|
762
|
+
/** Intent is durable before audit append; retries reuse the entire original signed record. */
|
|
763
|
+
closeInterruptedAudit(record, reason, expectedEntries, save = record => this.saveTaskCheckpoint(record)) {
|
|
764
|
+
if (!record.auditPath || !record.auditStage || record.auditStage === 'OUT_OF_SCOPE')
|
|
765
|
+
return record;
|
|
766
|
+
const store = new JsonlStore(record.auditPath);
|
|
767
|
+
const entries = store.read();
|
|
768
|
+
if (expectedEntries && digest(entries) !== digest(expectedEntries))
|
|
769
|
+
throw new Error('AUDIT_PREFIX_MISMATCH');
|
|
770
|
+
const proposal = entries[0]?.body.payload;
|
|
771
|
+
if (!proposal || digest({ manifest: proposal.proposal.payload, input: proposal.input }) !== record.proposalDigest)
|
|
772
|
+
throw new Error('AUDIT_PREFIX_MISMATCH');
|
|
773
|
+
if (!record.auditClosure) {
|
|
774
|
+
const interruption = createAuditInterruption(entries, this.options.owner, record.auditStage, reason);
|
|
775
|
+
record = { ...record, auditClosure: { state: 'pending', interruption } };
|
|
776
|
+
}
|
|
777
|
+
store.closeInterrupted({ entries, interruption: record.auditClosure.interruption }, () => save(record));
|
|
778
|
+
const written = { ...record, auditClosure: { ...record.auditClosure, state: 'written' } };
|
|
779
|
+
save(written);
|
|
780
|
+
return written;
|
|
781
|
+
}
|
|
782
|
+
reportInterruptionFailure(taskId) {
|
|
783
|
+
// Keep the original task outcome separately; never expose arbitrary I/O exception text.
|
|
784
|
+
try {
|
|
785
|
+
this.reportRejection(taskId, new Error('AUDIT_INTERRUPTION_WRITE_FAILED'));
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
try {
|
|
789
|
+
this.options.onEvent?.('REJECTED', taskId, { code: 'AUDIT_INTERRUPTION_WRITE_FAILED' });
|
|
790
|
+
}
|
|
791
|
+
catch { }
|
|
792
|
+
}
|
|
793
|
+
}
|
|
739
794
|
async reconcileTasks() {
|
|
740
795
|
if (this.reconciled || !this.taskJournal)
|
|
741
796
|
return;
|
|
@@ -745,7 +800,20 @@ export class WorkerNode {
|
|
|
745
800
|
this.taskJournal.load();
|
|
746
801
|
for (const saved of [...this.taskJournal.tasks.values()]) {
|
|
747
802
|
let record = saved;
|
|
748
|
-
|
|
803
|
+
// Pending intents are retried even if the business outcome was already made terminal.
|
|
804
|
+
// Old files have no explicit auditStage and are never backfilled, even with opt-in enabled.
|
|
805
|
+
if (record.auditStage && record.auditStage !== 'OUT_OF_SCOPE' &&
|
|
806
|
+
!record.outcome?.result && record.auditClosure?.state !== 'written' &&
|
|
807
|
+
(record.phase !== 'terminal' || record.outcome?.code === 'TASK_INTERRUPTED' || record.auditClosure?.state === 'pending')) {
|
|
808
|
+
try {
|
|
809
|
+
record = this.closeInterruptedAudit(record, 'WORKER_RESTART_OBSERVED');
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
record = this.taskJournal.tasks.get(record.taskId) ?? record;
|
|
813
|
+
this.reportInterruptionFailure(record.taskId);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const pending = !record.auditClosure && this.intakeStore.pendingTask(record.requesterDid, record.taskId, record.proposalDigest);
|
|
749
817
|
if (record.phase !== 'terminal') {
|
|
750
818
|
const result = this.sealedTaskResult(record);
|
|
751
819
|
// Adapters expose executeTask only. Unknown execution/tool results cannot be retried safely.
|
|
@@ -781,14 +849,19 @@ export class WorkerNode {
|
|
|
781
849
|
if (!verifyAudit(bundle, { taskId: record.taskId, actors: [record.requesterDid, this.options.owner.did] }).valid)
|
|
782
850
|
return;
|
|
783
851
|
const proposal = bundle.entries.find((entry) => entry.body.eventType === 'PROPOSE')?.body.payload;
|
|
784
|
-
if (
|
|
852
|
+
if (proposalAuditDigest(proposal) !== record.proposalDigest)
|
|
785
853
|
return;
|
|
786
854
|
const last = bundle.entries.at(-1)?.body.eventType;
|
|
787
855
|
if (last === 'CANCEL')
|
|
788
856
|
return { taskId: record.taskId, state: 'CANCELLED', deliverable: null, bundle };
|
|
789
857
|
const delivery = bundle.entries.find((entry) => entry.body.eventType === 'DELIVER')?.body.payload;
|
|
790
|
-
if (last === 'SETTLED' && delivery)
|
|
858
|
+
if (last === 'SETTLED' && delivery) {
|
|
859
|
+
const accepted = bundle.entries.find(entry => entry.body.eventType === 'ACCEPT')?.body.payload;
|
|
860
|
+
verifyRecoveredDelegation(delivery, proposal, accepted, record.delegation?.parentContract);
|
|
861
|
+
if (record.delegation && digest(assembleDelegation(record.delegation)) !== digest(delivery.delegation))
|
|
862
|
+
return;
|
|
791
863
|
return { taskId: record.taskId, state: 'SETTLED', deliverable: delivery.artifact.payload.data, bundle };
|
|
864
|
+
}
|
|
792
865
|
}
|
|
793
866
|
catch { /* An unsealed or partial audit is evidence to preserve, not authorization to rerun. */ }
|
|
794
867
|
return undefined;
|
|
@@ -873,8 +946,8 @@ export class WorkerNode {
|
|
|
873
946
|
return true;
|
|
874
947
|
}
|
|
875
948
|
/** Local signed diagnostics are separate from the bilateral hash-chain ledger. */
|
|
876
|
-
reportRejection(taskId, error) {
|
|
877
|
-
const diagnostic = rejectionBody(error);
|
|
949
|
+
reportRejection(taskId, error, contextGrant = false) {
|
|
950
|
+
const diagnostic = rejectionBody(error, contextGrant);
|
|
878
951
|
if (this.options.auditPath) {
|
|
879
952
|
const fd = openSync(this.options.auditPath + '.errors.jsonl', constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600);
|
|
880
953
|
try {
|
|
@@ -986,7 +1059,8 @@ export class WorkerNode {
|
|
|
986
1059
|
this.connectionStore.consumeAdmission(manifest.requester.ownerDid, manifest.taskId);
|
|
987
1060
|
const previous = this.heldReview.list().find(r => r.taskId === manifest.taskId && r.senderOwnerDid === manifest.requester.ownerDid);
|
|
988
1061
|
const held = previous ?? this.heldReview.hold({ kind: 'task', senderOwnerDid: manifest.requester.ownerDid,
|
|
989
|
-
taskId: manifest.taskId, contentDigest, deadline: Date.parse(manifest.deadline), envelope: first
|
|
1062
|
+
taskId: manifest.taskId, contentDigest, deadline: Date.parse(manifest.deadline), envelope: first,
|
|
1063
|
+
...(manifest.contextGrant ? { grantExpiresAt: Date.parse(manifest.contextGrant.expiresAt) } : {}) });
|
|
990
1064
|
if (held.contentDigest !== contentDigest)
|
|
991
1065
|
throw new Error('INVALID_PROPOSAL');
|
|
992
1066
|
return this.heldReview.inspect(held.id);
|
|
@@ -1003,6 +1077,7 @@ export class WorkerNode {
|
|
|
1003
1077
|
let m;
|
|
1004
1078
|
let auditPath;
|
|
1005
1079
|
let checkpoint;
|
|
1080
|
+
let auditStage;
|
|
1006
1081
|
const saveCheckpoint = (patch) => {
|
|
1007
1082
|
if (!checkpoint || !this.taskJournal)
|
|
1008
1083
|
return;
|
|
@@ -1013,6 +1088,16 @@ export class WorkerNode {
|
|
|
1013
1088
|
saveCheckpoint({ phase: 'terminal', outcome, notificationPending: true });
|
|
1014
1089
|
this.deliveryService?.store.receipt(first, Date.parse(m.deadline), 'terminal', outcome.code);
|
|
1015
1090
|
};
|
|
1091
|
+
const setAuditStage = (stage) => {
|
|
1092
|
+
if (this.options.auditInterruptionFormat !== 1 || !auditPath)
|
|
1093
|
+
return;
|
|
1094
|
+
// Format 1 requires a full, PROPOSE-only prefix. Grant audits use compact
|
|
1095
|
+
// proposal evidence and bilateral lifecycle entries, outside that scope.
|
|
1096
|
+
if (m.contextGrant)
|
|
1097
|
+
return;
|
|
1098
|
+
auditStage = stage;
|
|
1099
|
+
saveCheckpoint({ auditStage: stage });
|
|
1100
|
+
};
|
|
1016
1101
|
const acknowledgeProposal = async (signal) => {
|
|
1017
1102
|
this.deliveryService?.store.receipt(first, Date.parse(m.deadline), 'processed');
|
|
1018
1103
|
await acknowledgeRelay(relay, first, signal);
|
|
@@ -1032,6 +1117,18 @@ export class WorkerNode {
|
|
|
1032
1117
|
throw new Error('INVALID_PROPOSAL');
|
|
1033
1118
|
this.assertPeer(first.payload.senderDid);
|
|
1034
1119
|
validateManifest(manifest);
|
|
1120
|
+
validateContextGrantUse(manifest, data.input, this.protocol.now());
|
|
1121
|
+
if (manifest.contextGrant?.deliveryMode === 'approval_redelegation') {
|
|
1122
|
+
const facility = this.options.redelegation;
|
|
1123
|
+
if (!facility || !this.options.auditPath || !this.taskJournal)
|
|
1124
|
+
throw new Error('DELEGATION_INVALID');
|
|
1125
|
+
requireOwnerDecision(facility.ownerApprovalProvider);
|
|
1126
|
+
requireOwnerDecision(facility.requester.approvalProvider);
|
|
1127
|
+
if (facility.requester.options.owner.did !== this.options.owner.did)
|
|
1128
|
+
throw new Error('DELEGATION_INVALID');
|
|
1129
|
+
}
|
|
1130
|
+
if (manifest.contextGrant && this.options.inboxPath)
|
|
1131
|
+
throw new Error('CONTEXT_GRANT_INPUT_RETENTION_CONFLICT');
|
|
1035
1132
|
m = manifest;
|
|
1036
1133
|
auditPath = this.options.auditPath
|
|
1037
1134
|
? this.completed++ === 0 && !existsSync(this.options.auditPath)
|
|
@@ -1057,14 +1154,14 @@ export class WorkerNode {
|
|
|
1057
1154
|
}
|
|
1058
1155
|
}
|
|
1059
1156
|
catch (e) {
|
|
1060
|
-
this.reportRejection(first.payload.taskId, e);
|
|
1157
|
+
this.reportRejection(first.payload.taskId, e, !!data?.proposal?.payload?.contextGrant);
|
|
1061
1158
|
try {
|
|
1062
1159
|
await relay.send(signEnvelope({
|
|
1063
1160
|
taskId: first.payload.taskId,
|
|
1064
1161
|
recipientDid: first.payload.senderDid,
|
|
1065
1162
|
seq: 0,
|
|
1066
1163
|
eventType: 'ERROR',
|
|
1067
|
-
payload: asJson(rejectionBody(e)),
|
|
1164
|
+
payload: asJson(rejectionBody(e, !!data?.proposal?.payload?.contextGrant)),
|
|
1068
1165
|
}, this.options.owner, { encrypt: true }));
|
|
1069
1166
|
}
|
|
1070
1167
|
catch {
|
|
@@ -1078,7 +1175,7 @@ export class WorkerNode {
|
|
|
1078
1175
|
messageId: receiptFileId(first.payload.envelopeId), peerOwnerDid: m.requester.ownerDid,
|
|
1079
1176
|
relayUrl: this.options.relayUrl ?? (typeof this.options.relay === 'string' ? this.options.relay : this.agentCard.payload.relayEndpoint),
|
|
1080
1177
|
deliveryDeadline: Date.parse(m.deadline),
|
|
1081
|
-
} } : undefined);
|
|
1178
|
+
} } : undefined, m.contextGrant);
|
|
1082
1179
|
const cancelController = new AbortController();
|
|
1083
1180
|
wire.earlyCancel = () => cancelController.abort('CANCELLED');
|
|
1084
1181
|
let heartbeat;
|
|
@@ -1094,7 +1191,30 @@ export class WorkerNode {
|
|
|
1094
1191
|
}
|
|
1095
1192
|
};
|
|
1096
1193
|
let gateway, workspace;
|
|
1194
|
+
let childHostOpen = true;
|
|
1097
1195
|
let preserveWorkspace = false;
|
|
1196
|
+
let proposalAudited = false;
|
|
1197
|
+
const cleanupWorkspace = async () => {
|
|
1198
|
+
if (!workspace)
|
|
1199
|
+
return;
|
|
1200
|
+
const path = workspace;
|
|
1201
|
+
try {
|
|
1202
|
+
await rm(path, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
|
1203
|
+
workspace = undefined;
|
|
1204
|
+
}
|
|
1205
|
+
catch (error) {
|
|
1206
|
+
const code = error?.code ?? 'WORKSPACE_CLEANUP_FAILED';
|
|
1207
|
+
// Reporting residual files must not replace settlement or the execution error.
|
|
1208
|
+
try {
|
|
1209
|
+
if (this.options.onWorkspaceCleanupError)
|
|
1210
|
+
void Promise.resolve(this.options.onWorkspaceCleanupError({ taskId: m.taskId, workspaceDir: path, code }))
|
|
1211
|
+
.catch(() => { });
|
|
1212
|
+
else
|
|
1213
|
+
console.warn('Workspace cleanup incomplete (' + code + '): ' + path);
|
|
1214
|
+
}
|
|
1215
|
+
catch { }
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1098
1218
|
let trackedTaskId;
|
|
1099
1219
|
// Progress metrics are applied synchronously. The tail only serializes wire frames.
|
|
1100
1220
|
let progressTail = Promise.resolve();
|
|
@@ -1135,6 +1255,7 @@ export class WorkerNode {
|
|
|
1135
1255
|
if (reason)
|
|
1136
1256
|
throw new ProposalRejectedError(reason);
|
|
1137
1257
|
validateManifest(m);
|
|
1258
|
+
validateContextGrantUse(m, data.input, this.protocol.now());
|
|
1138
1259
|
if (!validateOutput(m.io.inputSchema, data.input))
|
|
1139
1260
|
throw new Error('INVALID_PROPOSAL');
|
|
1140
1261
|
if (this.options.executionTimeoutMs !== undefined && m.budget.maxWallClockSeconds * 1000 > this.options.executionTimeoutMs)
|
|
@@ -1226,21 +1347,31 @@ export class WorkerNode {
|
|
|
1226
1347
|
if (!contractProvider || peerTrust === 'limited' && (contractProvider.kind === 'policy' ||
|
|
1227
1348
|
contractProvider === this.approvalProvider && contractProvider.kind !== 'human'))
|
|
1228
1349
|
throw new Error('APPROVAL_CHANNEL_UNAVAILABLE');
|
|
1350
|
+
if (m.contextGrant?.deliveryMode === 'approval_redelegation')
|
|
1351
|
+
requireOwnerDecision(contractProvider);
|
|
1229
1352
|
await wire.receiveEvent('PROPOSE', first);
|
|
1353
|
+
proposalAudited = true;
|
|
1354
|
+
await wire.contextEvent('CONTEXT_GRANT');
|
|
1355
|
+
await wire.contextEvent('CONTEXT_MODE');
|
|
1356
|
+
setAuditStage('OWNER_APPROVAL');
|
|
1230
1357
|
const contract = createContract(m, data.proposal, this.options.owner);
|
|
1358
|
+
const openingStarted = m.contextGrant?.deliveryMode === 'approval_redelegation' ? Date.now() : 0;
|
|
1231
1359
|
const approval = await abortable(contractProvider.approve({
|
|
1232
1360
|
taskId: m.taskId,
|
|
1233
1361
|
gate: 'contract_acceptance',
|
|
1234
1362
|
target: contract.contractDigest,
|
|
1235
1363
|
details: {
|
|
1236
|
-
manifest: m,
|
|
1237
|
-
input: data.input,
|
|
1364
|
+
manifest: m.contextGrant?.deliveryMode === 'approval_redelegation' ? structuredClone(m) : m,
|
|
1365
|
+
input: m.contextGrant?.deliveryMode === 'approval_redelegation' ? structuredClone(data.input) : data.input,
|
|
1238
1366
|
capabilities: data.capabilityChains,
|
|
1239
1367
|
...(inspection ? { inspection } : {}),
|
|
1240
1368
|
from: first.payload.senderDid,
|
|
1241
1369
|
},
|
|
1242
1370
|
signal: activeSignal,
|
|
1243
1371
|
}), activeSignal);
|
|
1372
|
+
if (m.contextGrant?.deliveryMode === 'approval_redelegation')
|
|
1373
|
+
assertFreshDecision(approval, openingStarted);
|
|
1374
|
+
setAuditStage('OUT_OF_SCOPE');
|
|
1244
1375
|
if (this.trust(m.requester.ownerDid, m.protocolVersion) === 'unknown')
|
|
1245
1376
|
throw new Error('APPROVAL_CHANNEL_UNAVAILABLE');
|
|
1246
1377
|
this.assertCapabilitiesActive(data.capabilityChains, capabilityContext);
|
|
@@ -1249,8 +1380,11 @@ export class WorkerNode {
|
|
|
1249
1380
|
throw new Error('APPROVAL_CHANNEL_UNAVAILABLE');
|
|
1250
1381
|
gateway = new OwnerGateway(this.options.owner, contract, policy, this.revocation);
|
|
1251
1382
|
const accepted = gateway.accept(approval);
|
|
1383
|
+
validateContextGrantUse(m, data.input, this.protocol.now());
|
|
1384
|
+
await wire.contextEvent('CONTEXT_OPENED');
|
|
1252
1385
|
this.acceptedPeers.set(m.taskId, m.requester.ownerDid);
|
|
1253
|
-
saveCheckpoint({ phase: 'accepted'
|
|
1386
|
+
saveCheckpoint({ phase: 'accepted', ...(m.contextGrant?.deliveryMode === 'approval_redelegation'
|
|
1387
|
+
? { delegation: { parentContract: contract, attempts: [] } } : {}) });
|
|
1254
1388
|
if (inspection?.inspectionStatus === 'complete')
|
|
1255
1389
|
this.deliveryService?.recipientStatus.observe(first.payload.envelopeId, 'opened');
|
|
1256
1390
|
if (this.options.dailyLimit)
|
|
@@ -1287,7 +1421,10 @@ export class WorkerNode {
|
|
|
1287
1421
|
// Synchronous notification failures are also best effort.
|
|
1288
1422
|
}
|
|
1289
1423
|
}
|
|
1424
|
+
validateContextGrantUse(m, data.input, this.protocol.now());
|
|
1425
|
+
setAuditStage('ACCEPT_ACK_WAIT');
|
|
1290
1426
|
await wire.sendEvent('ACCEPT', { contract, transition: accepted, approval });
|
|
1427
|
+
setAuditStage('OUT_OF_SCOPE');
|
|
1291
1428
|
this.options.onEvent?.('ACCEPT', m.taskId);
|
|
1292
1429
|
await wire.sendEvent('START', gateway.transition('START'));
|
|
1293
1430
|
heartbeat = setTimeout(() => void sendHeartbeat(), this.options.heartbeatIntervalMs ?? 15_000);
|
|
@@ -1364,6 +1501,110 @@ export class WorkerNode {
|
|
|
1364
1501
|
void run.catch(() => undefined);
|
|
1365
1502
|
return run;
|
|
1366
1503
|
};
|
|
1504
|
+
const managedChildren = new Set();
|
|
1505
|
+
let childrenClosed = false;
|
|
1506
|
+
const delegationState = m.contextGrant?.deliveryMode === 'approval_redelegation'
|
|
1507
|
+
? { parentContract: structuredClone(contract), attempts: [] } : undefined;
|
|
1508
|
+
const persistChildren = () => saveCheckpoint({ delegation: structuredClone(delegationState) });
|
|
1509
|
+
const checkParent = () => {
|
|
1510
|
+
executionSignal.throwIfAborted();
|
|
1511
|
+
if (!childHostOpen || gateway.machine.state !== 'IN_PROGRESS' || wire.cancelHandled)
|
|
1512
|
+
throw new Error('DELEGATION_INVALID');
|
|
1513
|
+
this.assertPeer(m.requester.ownerDid, m.taskId);
|
|
1514
|
+
this.assertCapabilitiesActive(data.capabilityChains, capabilityContext);
|
|
1515
|
+
validateContextGrantUse(m, data.input, this.protocol.now());
|
|
1516
|
+
if (Date.parse(m.deadline) <= this.protocol.now())
|
|
1517
|
+
throw new Error('DELEGATION_INVALID');
|
|
1518
|
+
};
|
|
1519
|
+
const dispatchChild = async (request) => {
|
|
1520
|
+
if (childrenClosed || !delegationState)
|
|
1521
|
+
throw new Error('DELEGATION_INVALID');
|
|
1522
|
+
if (!request || Object.keys(request).some(k => !['to', 'relay', 'manifest', 'input', 'auditPath'].includes(k)))
|
|
1523
|
+
throw new Error('DELEGATION_INVALID');
|
|
1524
|
+
const child = structuredClone(request.manifest), input = structuredClone(request.input);
|
|
1525
|
+
if (Object.hasOwn(child, 'parentTaskId'))
|
|
1526
|
+
throw new Error('DELEGATION_INVALID');
|
|
1527
|
+
assertDelegationAttenuation(m, child);
|
|
1528
|
+
if (child.requester.ownerDid !== this.options.owner.did || child.taskId === m.taskId ||
|
|
1529
|
+
[m.requester.ownerDid, this.options.owner.did].includes(child.worker.ownerDid) ||
|
|
1530
|
+
delegationState.attempts.some(a => a.childTaskId === child.taskId))
|
|
1531
|
+
throw new Error('DELEGATION_INVALID');
|
|
1532
|
+
checkParent();
|
|
1533
|
+
validateContextGrantUse(child, input, this.protocol.now());
|
|
1534
|
+
const facility = this.options.redelegation;
|
|
1535
|
+
requireOwnerDecision(facility.ownerApprovalProvider);
|
|
1536
|
+
requireOwnerDecision(facility.requester.approvalProvider);
|
|
1537
|
+
if (facility.requester.options.owner.did !== this.options.owner.did)
|
|
1538
|
+
throw new Error('DELEGATION_INVALID');
|
|
1539
|
+
let attempt;
|
|
1540
|
+
const options = { to: request.to, relay: request.relay, manifest: child, input,
|
|
1541
|
+
title: child.task.title, description: child.task.description, auditPath: request.auditPath,
|
|
1542
|
+
signal: executionSignal, capabilityChains: [] };
|
|
1543
|
+
managedChildHooks.set(options, {
|
|
1544
|
+
prepare: async (manifest, selectedInput, peer) => {
|
|
1545
|
+
checkParent();
|
|
1546
|
+
assertDelegationAttenuation(m, manifest);
|
|
1547
|
+
if (peer !== child.worker.ownerDid || peer === m.requester.ownerDid || peer === this.options.owner.did)
|
|
1548
|
+
throw new Error('DELEGATION_INVALID');
|
|
1549
|
+
const values = { childManifestDigest: digest(manifest), childInputDigest: digest(selectedInput), delegateeOwnerDid: peer };
|
|
1550
|
+
const target = redelegationTarget(contract, values), started = Date.now();
|
|
1551
|
+
const receipt = await abortable(facility.ownerApprovalProvider.approve({ taskId: m.taskId, gate: 'redelegation',
|
|
1552
|
+
target: structuredClone(target), details: { parentManifest: structuredClone(m), childManifest: structuredClone(manifest),
|
|
1553
|
+
input: structuredClone(selectedInput), delegateeOwnerDid: peer, depthRemaining: 0,
|
|
1554
|
+
disclosure: 'The root owner receives the complete signed parent and child contracts, including declarations and owner DIDs. Confirm this input, angle and task serve the parent purpose.' },
|
|
1555
|
+
signal: executionSignal, deadline: m.deadline }), executionSignal);
|
|
1556
|
+
checkParent();
|
|
1557
|
+
validateContextGrantUse(manifest, selectedInput, this.protocol.now());
|
|
1558
|
+
assertFreshDecision(receipt, started);
|
|
1559
|
+
gateway.approvals.require(m.taskId, 'redelegation', target, receipt);
|
|
1560
|
+
if (delegationState.attempts.some(a => a.childTaskId === child.taskId))
|
|
1561
|
+
throw new Error('DELEGATION_INVALID');
|
|
1562
|
+
attempt = { childTaskId: child.taskId, ...values, approval: receipt, status: 'approved' };
|
|
1563
|
+
delegationState.attempts.push(attempt);
|
|
1564
|
+
persistChildren();
|
|
1565
|
+
},
|
|
1566
|
+
beforeTransmit: (manifest, selectedInput) => {
|
|
1567
|
+
checkParent();
|
|
1568
|
+
validateContextGrantUse(manifest, selectedInput, this.protocol.now());
|
|
1569
|
+
if (!attempt || digest(manifest) !== attempt.childManifestDigest || digest(selectedInput) !== attempt.childInputDigest ||
|
|
1570
|
+
!verifyApproval(attempt.approval, this.options.owner.did, m.taskId, 'redelegation', redelegationTarget(contract, attempt)))
|
|
1571
|
+
throw new Error('DELEGATION_APPROVAL_REQUIRED');
|
|
1572
|
+
attempt.status = 'dispatching';
|
|
1573
|
+
persistChildren();
|
|
1574
|
+
},
|
|
1575
|
+
onAcceptedContract: acceptedChild => {
|
|
1576
|
+
if (!attempt)
|
|
1577
|
+
throw new Error('DELEGATION_INVALID');
|
|
1578
|
+
// Keep uncertainty durable if signing or publication fails after authenticated ACCEPT.
|
|
1579
|
+
attempt.status = 'unresolved';
|
|
1580
|
+
persistChildren();
|
|
1581
|
+
checkParent();
|
|
1582
|
+
const link = createDelegationLink({ parentContract: contract, childContract: acceptedChild }, this.options.owner);
|
|
1583
|
+
attempt.proof = { link, childContract: structuredClone(acceptedChild) };
|
|
1584
|
+
attempt.status = 'accepted';
|
|
1585
|
+
persistChildren();
|
|
1586
|
+
},
|
|
1587
|
+
});
|
|
1588
|
+
try {
|
|
1589
|
+
const result = await facility.requester.delegate(options);
|
|
1590
|
+
if (!attempt?.proof || result.state !== 'SETTLED')
|
|
1591
|
+
throw new Error('DELEGATION_INVALID');
|
|
1592
|
+
attempt.status = 'completed';
|
|
1593
|
+
persistChildren();
|
|
1594
|
+
return result;
|
|
1595
|
+
}
|
|
1596
|
+
catch (error) {
|
|
1597
|
+
if (attempt) {
|
|
1598
|
+
attempt.status = attempt.proof || attempt.status === 'approved' || error instanceof PeerRejectedError && attempt.status !== 'unresolved'
|
|
1599
|
+
? 'failed' : 'unresolved';
|
|
1600
|
+
persistChildren();
|
|
1601
|
+
}
|
|
1602
|
+
throw error;
|
|
1603
|
+
}
|
|
1604
|
+
finally {
|
|
1605
|
+
managedChildHooks.delete(options);
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1367
1608
|
const context = {
|
|
1368
1609
|
taskId: m.taskId,
|
|
1369
1610
|
manifest: structuredClone(m),
|
|
@@ -1407,7 +1648,7 @@ export class WorkerNode {
|
|
|
1407
1648
|
}
|
|
1408
1649
|
if (metricsError) {
|
|
1409
1650
|
progressFailure ??= metricsError;
|
|
1410
|
-
const code = safeError(metricsError);
|
|
1651
|
+
const code = safeError(metricsError, !!m.contextGrant);
|
|
1411
1652
|
return enqueueProgress(async () => {
|
|
1412
1653
|
// Wait until any in-flight PROGRESS handshake finishes, then reject the session.
|
|
1413
1654
|
wireFailureSent = true;
|
|
@@ -1455,10 +1696,22 @@ export class WorkerNode {
|
|
|
1455
1696
|
return result;
|
|
1456
1697
|
},
|
|
1457
1698
|
};
|
|
1699
|
+
if (delegationState)
|
|
1700
|
+
context.redelegate = request => {
|
|
1701
|
+
const operation = dispatchChild(request);
|
|
1702
|
+
managedChildren.add(operation);
|
|
1703
|
+
void operation.finally(() => managedChildren.delete(operation)).catch(() => { });
|
|
1704
|
+
return operation;
|
|
1705
|
+
};
|
|
1458
1706
|
this.assertPeer(m.requester.ownerDid, m.taskId);
|
|
1459
1707
|
this.assertCapabilitiesActive(data.capabilityChains, capabilityContext);
|
|
1708
|
+
validateContextGrantUse(m, data.input, this.protocol.now());
|
|
1460
1709
|
this.deliveryService?.recipientStatus.handoff(first, m);
|
|
1461
1710
|
const output = await abortable(this.options.adapter.executeTask(context), executionSignal);
|
|
1711
|
+
childrenClosed = true;
|
|
1712
|
+
if (delegationState)
|
|
1713
|
+
await abortable(Promise.allSettled([...managedChildren]), executionSignal);
|
|
1714
|
+
const delegation = delegationState ? assembleDelegation(delegationState) : undefined;
|
|
1462
1715
|
if (!this.options.inboxPath)
|
|
1463
1716
|
await acknowledgeProposal(executionSignal);
|
|
1464
1717
|
// A task may return while a queued progress update is still waiting for its ACK.
|
|
@@ -1473,7 +1726,11 @@ export class WorkerNode {
|
|
|
1473
1726
|
const delivery = createDelivery(m.taskId, contract.contractDigest, output, gateway.quota.receipt(this.options.owner), this.options.owner, {
|
|
1474
1727
|
effectiveIsolationLevel,
|
|
1475
1728
|
...(sandboxBoundary ? { sandboxBoundary } : {}),
|
|
1476
|
-
});
|
|
1729
|
+
}, delegation);
|
|
1730
|
+
if (m.contextGrant && workspace) {
|
|
1731
|
+
await cleanupWorkspace();
|
|
1732
|
+
await wire.contextEvent('CONTEXT_DELETION_ATTEMPTED');
|
|
1733
|
+
}
|
|
1477
1734
|
const transition = gateway.transition('DELIVER');
|
|
1478
1735
|
saveCheckpoint({ phase: 'delivered' });
|
|
1479
1736
|
await wire.sendEvent('DELIVER', { ...delivery, transition });
|
|
@@ -1493,6 +1750,7 @@ export class WorkerNode {
|
|
|
1493
1750
|
return { taskId: m.taskId, state: 'SETTLED', deliverable: output, bundle };
|
|
1494
1751
|
}
|
|
1495
1752
|
catch (e) {
|
|
1753
|
+
childHostOpen = false;
|
|
1496
1754
|
if (checkpoint?.outcome?.result) {
|
|
1497
1755
|
// The bilateral checkpoint is already durable even if its final relay send failed.
|
|
1498
1756
|
await this.notifyOutcome(checkpoint).then(() => saveCheckpoint({ notificationPending: false })).catch(() => { });
|
|
@@ -1516,23 +1774,47 @@ export class WorkerNode {
|
|
|
1516
1774
|
return { taskId: m.taskId, state: 'CANCELLED', deliverable: null, bundle };
|
|
1517
1775
|
}
|
|
1518
1776
|
catch (inner) {
|
|
1519
|
-
await wire.rawSend('ERROR', { code: safeError(inner) }).catch(() => { });
|
|
1777
|
+
await wire.rawSend('ERROR', { code: safeError(inner, !!m.contextGrant) }).catch(() => { });
|
|
1520
1778
|
throw inner;
|
|
1521
1779
|
}
|
|
1522
1780
|
}
|
|
1523
1781
|
await progressTail;
|
|
1524
1782
|
preserveWorkspace = activeSignal.aborted || idleSignal.aborted;
|
|
1783
|
+
const timeoutReason = activeSignal.reason instanceof Error ? activeSignal.reason.message : activeSignal.reason;
|
|
1784
|
+
const interruptionReason = idleSignal.aborted ? 'WORKER_STOPPED' :
|
|
1785
|
+
activeSignal.aborted && ['TASK_TIMEOUT', 'PEER_WAIT_TIMEOUT'].includes(String(timeoutReason)) ? 'SESSION_TIMEOUT' : undefined;
|
|
1786
|
+
if (interruptionReason && auditStage && auditStage !== 'OUT_OF_SCOPE' && auditPath) {
|
|
1787
|
+
const entries = await wire.quiesceAudit();
|
|
1788
|
+
try {
|
|
1789
|
+
// No journal is available only to embedders without persisted task checkpoints.
|
|
1790
|
+
const record = checkpoint ?? { version: 1, taskId: m.taskId, requesterDid: m.requester.ownerDid,
|
|
1791
|
+
envelopeId: first.payload.envelopeId, proposalDigest: digest({ manifest: m, input: data.input }),
|
|
1792
|
+
phase: 'received', auditPath, auditStage, invocations: [] };
|
|
1793
|
+
this.closeInterruptedAudit(record, interruptionReason, entries, saved => {
|
|
1794
|
+
checkpoint = saved;
|
|
1795
|
+
this.saveTaskCheckpoint(saved);
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
catch {
|
|
1799
|
+
this.reportInterruptionFailure(m.taskId);
|
|
1800
|
+
}
|
|
1801
|
+
saveOutcome({ code: 'TASK_INTERRUPTED' });
|
|
1802
|
+
}
|
|
1803
|
+
else if (auditStage && !preserveWorkspace)
|
|
1804
|
+
setAuditStage('OUT_OF_SCOPE');
|
|
1525
1805
|
if (idleSignal.aborted)
|
|
1526
1806
|
throw e;
|
|
1527
|
-
const diagnostic = preserveWorkspace ? { code: 'TASK_INTERRUPTED' } : rejectionBody(progressFailure ?? e);
|
|
1807
|
+
const diagnostic = preserveWorkspace ? { code: 'TASK_INTERRUPTED' } : rejectionBody(progressFailure ?? e, !!m.contextGrant);
|
|
1528
1808
|
saveOutcome(diagnostic);
|
|
1529
1809
|
this.deliveryService?.recipientStatus.end(first.payload.envelopeId);
|
|
1530
1810
|
const refused = this.heldReview.list().find(r => r.taskId === m.taskId);
|
|
1531
1811
|
if (refused && diagnostic.code !== 'PENDING_OWNER_APPROVAL' && !preserveWorkspace)
|
|
1532
1812
|
this.heldReview.discard(refused.id);
|
|
1533
|
-
this.reportRejection(m.taskId, progressFailure ?? e);
|
|
1813
|
+
this.reportRejection(m.taskId, progressFailure ?? e, !!m.contextGrant);
|
|
1534
1814
|
if (!wireFailureSent) {
|
|
1535
1815
|
wireFailureSent = true;
|
|
1816
|
+
if (proposalAudited && !preserveWorkspace && diagnostic.code !== 'PENDING_OWNER_APPROVAL')
|
|
1817
|
+
await wire.contextEvent('CONTEXT_DISCARDED').catch(() => { });
|
|
1536
1818
|
await wire.rawSend('ERROR', diagnostic)
|
|
1537
1819
|
.then(async () => {
|
|
1538
1820
|
saveCheckpoint({ notificationPending: false });
|
|
@@ -1548,6 +1830,7 @@ export class WorkerNode {
|
|
|
1548
1830
|
throw e;
|
|
1549
1831
|
}
|
|
1550
1832
|
finally {
|
|
1833
|
+
childHostOpen = false;
|
|
1551
1834
|
this.deliveryService?.recipientStatus.end(first.payload.envelopeId);
|
|
1552
1835
|
heartbeatStopped = true;
|
|
1553
1836
|
clearTimeout(heartbeat);
|
|
@@ -1562,23 +1845,8 @@ export class WorkerNode {
|
|
|
1562
1845
|
gateway?.close();
|
|
1563
1846
|
if (idleSignal.aborted && checkpoint?.phase !== 'terminal')
|
|
1564
1847
|
preserveWorkspace = true;
|
|
1565
|
-
if (workspace && !preserveWorkspace)
|
|
1566
|
-
|
|
1567
|
-
await rm(workspace, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
|
1568
|
-
}
|
|
1569
|
-
catch (error) {
|
|
1570
|
-
const code = error?.code ?? 'WORKSPACE_CLEANUP_FAILED';
|
|
1571
|
-
// Reporting residual files must not replace settlement or the execution error.
|
|
1572
|
-
try {
|
|
1573
|
-
if (this.options.onWorkspaceCleanupError)
|
|
1574
|
-
void Promise.resolve(this.options.onWorkspaceCleanupError({ taskId: m.taskId, workspaceDir: workspace, code }))
|
|
1575
|
-
.catch(() => { });
|
|
1576
|
-
else
|
|
1577
|
-
console.warn('Workspace cleanup incomplete (' + code + '): ' + workspace);
|
|
1578
|
-
}
|
|
1579
|
-
catch { }
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1848
|
+
if (workspace && !preserveWorkspace)
|
|
1849
|
+
await cleanupWorkspace();
|
|
1582
1850
|
}
|
|
1583
1851
|
}
|
|
1584
1852
|
/**
|
|
@@ -1920,24 +2188,26 @@ class PendingTaskError extends Error {
|
|
|
1920
2188
|
function diagnosticError(code, field, expected, actual, hint) {
|
|
1921
2189
|
return new DiagnosticError(code, { field, expected: digest(expected), actual: digest(actual), hint });
|
|
1922
2190
|
}
|
|
1923
|
-
function rejectionBody(error) {
|
|
2191
|
+
function rejectionBody(error, contextGrant = false) {
|
|
1924
2192
|
if (error instanceof PendingTaskError)
|
|
1925
2193
|
return { code: 'PENDING_OWNER_APPROVAL', reason: error.reason };
|
|
1926
2194
|
if (error instanceof DiagnosticError)
|
|
1927
|
-
return { code: safeError(error), detail: error.detail };
|
|
2195
|
+
return { code: safeError(error, contextGrant), detail: error.detail };
|
|
1928
2196
|
if (error instanceof ProposalRejectedError)
|
|
1929
2197
|
return { code: error.code, reason: error.reason };
|
|
1930
|
-
if (safeError(error) === 'PENDING_OWNER_APPROVAL')
|
|
2198
|
+
if (safeError(error, contextGrant) === 'PENDING_OWNER_APPROVAL')
|
|
1931
2199
|
return { code: 'PENDING_OWNER_APPROVAL', reason: PENDING_MESSAGE };
|
|
1932
|
-
return { code: safeError(error) };
|
|
2200
|
+
return { code: safeError(error, contextGrant) };
|
|
1933
2201
|
}
|
|
1934
|
-
function safeError(e) {
|
|
2202
|
+
function safeError(e, contextGrant = false) {
|
|
1935
2203
|
const s = String(e);
|
|
1936
2204
|
const exact = e instanceof Error ? e.message.split(':')[0] : '';
|
|
1937
2205
|
if (['OUTBOX_STORE_INVALID', 'OUTBOX_STORE_BUSY', 'OUTBOX_STORE_UNWRITABLE', 'OUTBOX_FULL',
|
|
1938
|
-
'OUTBOX_DEADLINE_EXPIRED', 'DELIVERY_RECORD_CONFLICT'].includes(exact))
|
|
2206
|
+
'OUTBOX_DEADLINE_EXPIRED', 'DELIVERY_RECORD_CONFLICT', 'AUDIT_INTERRUPTION_WRITE_FAILED'].includes(exact))
|
|
1939
2207
|
return exact;
|
|
1940
|
-
if (
|
|
2208
|
+
if (contextGrant && ['DELEGATION_PROOF_REQUIRED', 'DELEGATION_INVALID', 'DELEGATION_APPROVAL_REQUIRED'].includes(exact))
|
|
2209
|
+
return exact;
|
|
2210
|
+
if (/^(CONTEXT_GRANT_|PROTOCOL_|CONNECTION_|INVALID_CONNECTION_|QUARANTINE_|INSPECTION_|ACCESS_MODE_REMOVED|INVITE_REFERENCE_UNAVAILABLE)/.test(exact))
|
|
1941
2211
|
return exact;
|
|
1942
2212
|
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'])
|
|
1943
2213
|
if (s === 'Error: ' + code || s.startsWith('Error: ' + code + ':'))
|
|
@@ -1974,6 +2244,14 @@ function freezeApprovalAction(value) {
|
|
|
1974
2244
|
}
|
|
1975
2245
|
return value;
|
|
1976
2246
|
}
|
|
2247
|
+
// Module-private hooks cannot be supplied by task output or arbitrary delegate options.
|
|
2248
|
+
const managedChildHooks = new WeakMap();
|
|
2249
|
+
function assertFreshDecision(receipt, started) {
|
|
2250
|
+
const approved = Date.parse(receipt?.payload?.approvedAt), signed = Date.parse(receipt?.signature?.signedAt);
|
|
2251
|
+
if (!Number.isFinite(approved) || !Number.isFinite(signed) || approved < started || signed < started ||
|
|
2252
|
+
approved > Date.now() || signed > Date.now())
|
|
2253
|
+
throw new Error('DELEGATION_APPROVAL_REQUIRED');
|
|
2254
|
+
}
|
|
1977
2255
|
/** Completion failure preserves local verification without claiming a sealed checkpoint. */
|
|
1978
2256
|
export class IncompleteSettlementError extends Error {
|
|
1979
2257
|
incompleteReceipt;
|
|
@@ -2031,6 +2309,7 @@ export async function deliverRevocation(options) {
|
|
|
2031
2309
|
}
|
|
2032
2310
|
export class RequesterClient {
|
|
2033
2311
|
options;
|
|
2312
|
+
contextApprovals;
|
|
2034
2313
|
idempotency;
|
|
2035
2314
|
keyedStore() {
|
|
2036
2315
|
return this.idempotency ??= new IdempotencyStore(this.options.owner, {
|
|
@@ -2040,7 +2319,7 @@ export class RequesterClient {
|
|
|
2040
2319
|
keyedAction(options) {
|
|
2041
2320
|
const allowed = ['idempotencyKey', 'idempotencyOperation', 'idempotencyChildIndex', 'workerRef', 'pendingId', 'pendingRelayUrl',
|
|
2042
2321
|
'renegotiateProtocol', 'beforePropose', 'beforeTransmit', 'to', 'discover', 'relay', 'title', 'description', 'input',
|
|
2043
|
-
'outputSchema', 'budget', 'manifest', 'auditPath', 'signal', 'capabilities', 'capabilityChains', 'minimumIsolationLevel',
|
|
2322
|
+
'outputSchema', 'contextGrant', 'budget', 'manifest', 'auditPath', 'signal', 'capabilities', 'capabilityChains', 'minimumIsolationLevel',
|
|
2044
2323
|
'onProgress', 'workers', 'mode', 'quorumSize', 'completionTimeoutMs', 'timeoutMs', 'executionTimeoutMs', 'taskTimeoutMs',
|
|
2045
2324
|
'waitTimeoutMs', 'connectTimeoutMs', 'transportIdleTimeoutMs', 'heartbeatIntervalMs'];
|
|
2046
2325
|
if (Object.keys(options).some(k => !allowed.includes(k)))
|
|
@@ -2073,6 +2352,7 @@ export class RequesterClient {
|
|
|
2073
2352
|
validateSessionTimeouts(options);
|
|
2074
2353
|
this.approvalProvider =
|
|
2075
2354
|
options.approvalProvider ?? new InteractiveCliApprovalProvider(options.owner);
|
|
2355
|
+
this.contextApprovals = new ApprovalGate(options.owner.did, policy);
|
|
2076
2356
|
this.deliveryService = options.deliveryService;
|
|
2077
2357
|
if (this.deliveryService && this.deliveryService.owner.did !== options.owner.did)
|
|
2078
2358
|
throw deliveryError('OUTBOX_STORE_INVALID');
|
|
@@ -2128,24 +2408,40 @@ export class RequesterClient {
|
|
|
2128
2408
|
});
|
|
2129
2409
|
}
|
|
2130
2410
|
async delegate(options, hooks) {
|
|
2411
|
+
const managedChild = managedChildHooks.get(options);
|
|
2131
2412
|
if (options.idempotencyKey !== undefined && !options.idempotencyOperation) {
|
|
2132
2413
|
validateIdempotencyKey(options.idempotencyKey);
|
|
2133
2414
|
options = { ...options, input: structuredClone(options.input),
|
|
2134
2415
|
outputSchema: options.outputSchema && structuredClone(options.outputSchema),
|
|
2416
|
+
contextGrant: options.contextGrant && structuredClone(options.contextGrant),
|
|
2135
2417
|
budget: options.budget && structuredClone(options.budget),
|
|
2136
2418
|
capabilities: options.capabilities && structuredClone(options.capabilities) };
|
|
2137
2419
|
options = { ...options, manifest: options.manifest && structuredClone(options.manifest),
|
|
2138
2420
|
discover: options.discover && structuredClone(options.discover),
|
|
2139
2421
|
capabilityChains: options.capabilityChains && structuredClone(options.capabilityChains) };
|
|
2140
2422
|
const action = this.keyedAction(options);
|
|
2141
|
-
|
|
2423
|
+
const result = unwrapIdempotency(await this.keyedStore().run('acp_delegate_task', options.idempotencyKey, action, async (operation) => ({ isError: false, value: await this.delegate({ ...options,
|
|
2142
2424
|
idempotencyKey: undefined, idempotencyOperation: operation }, hooks) })));
|
|
2425
|
+
if ((options.manifest ? options.manifest.contextGrant : options.contextGrant)?.deliveryMode === 'approval_redelegation' &&
|
|
2426
|
+
result.state === 'SETTLED') {
|
|
2427
|
+
const proposed = result.bundle.entries.find(e => e.body.eventType === 'PROPOSE')?.body.payload;
|
|
2428
|
+
const accepted = result.bundle.entries.find(e => e.body.eventType === 'ACCEPT')?.body.payload;
|
|
2429
|
+
const delivered = result.bundle.entries.find(e => e.body.eventType === 'DELIVER')?.body.payload;
|
|
2430
|
+
if (!delivered || !verifyAudit(result.bundle).valid)
|
|
2431
|
+
throw new Error('DELEGATION_INVALID');
|
|
2432
|
+
verifyRecoveredDelegation(delivered, proposed, accepted);
|
|
2433
|
+
if (options.manifest && delivered.delegation && digest(delivered.delegation.parentContract.manifest) !== digest(options.manifest))
|
|
2434
|
+
throw new Error('DELEGATION_INVALID');
|
|
2435
|
+
}
|
|
2436
|
+
return result;
|
|
2143
2437
|
}
|
|
2144
2438
|
assertConnectionsOptions(this.options);
|
|
2145
2439
|
// Retain the reviewed values even if the caller changes its objects while awaiting consent.
|
|
2146
|
-
if (options.beforePropose
|
|
2440
|
+
if (options.beforePropose || managedChild ||
|
|
2441
|
+
(options.manifest ? options.manifest.contextGrant : options.contextGrant)?.deliveryMode === 'approval_redelegation')
|
|
2147
2442
|
options = { ...options,
|
|
2148
2443
|
input: structuredClone(options.input), manifest: options.manifest && structuredClone(options.manifest),
|
|
2444
|
+
contextGrant: options.contextGrant && structuredClone(options.contextGrant),
|
|
2149
2445
|
budget: options.budget && structuredClone(options.budget),
|
|
2150
2446
|
outputSchema: options.outputSchema && structuredClone(options.outputSchema),
|
|
2151
2447
|
capabilities: options.capabilities && structuredClone(options.capabilities),
|
|
@@ -2217,6 +2513,7 @@ export class RequesterClient {
|
|
|
2217
2513
|
options.budget.maxWallClockSeconds * 1000 > timeoutOptions.executionTimeoutMs)
|
|
2218
2514
|
throw new Error('INCOMPATIBLE_TIMEOUTS: requested budget exceeds executionTimeoutMs');
|
|
2219
2515
|
validateManifest(manifest);
|
|
2516
|
+
validateContextGrantUse(manifest, options.input, (this.options.now ?? Date.now)());
|
|
2220
2517
|
if (manifest.requester.ownerDid !== owner.did ||
|
|
2221
2518
|
manifest.worker?.ownerDid !== peer ||
|
|
2222
2519
|
Date.parse(manifest.deadline) <= Date.now())
|
|
@@ -2247,6 +2544,24 @@ export class RequesterClient {
|
|
|
2247
2544
|
if (operation)
|
|
2248
2545
|
await operation.prepare({ childIndex, manifest, input: options.input, relayUrl: stableRelayUrl ?? approvalRelay,
|
|
2249
2546
|
peerOwnerDid: peer, peerAgentDid: manifest.worker.agentDid, approvalTargetDigest });
|
|
2547
|
+
const approvalMode = manifest.contextGrant?.deliveryMode === 'approval_redelegation';
|
|
2548
|
+
const proposalTarget = () => ({ type: 'ContextGrantProposal', manifestDigest: digest(manifest),
|
|
2549
|
+
inputDigest: digest(options.input), workerOwnerDid: peer });
|
|
2550
|
+
let contextProposalApproval;
|
|
2551
|
+
if (approvalMode) {
|
|
2552
|
+
requireOwnerDecision(this.approvalProvider);
|
|
2553
|
+
const target = proposalTarget(), started = Date.now();
|
|
2554
|
+
const approvalSignal = AbortSignal.any([
|
|
2555
|
+
AbortSignal.timeout(Math.max(1, Math.min(Date.parse(manifest.deadline) - Date.now(), timeoutOptions.waitTimeoutMs ?? timeoutOptions.taskTimeoutMs ?? timeoutOptions.timeoutMs ?? 600000))),
|
|
2556
|
+
...(options.signal ? [options.signal] : []),
|
|
2557
|
+
]);
|
|
2558
|
+
contextProposalApproval = await abortable(this.approvalProvider.approve({ taskId: manifest.taskId, gate: 'contract_acceptance',
|
|
2559
|
+
target: structuredClone(target), details: { manifest: structuredClone(manifest), input: structuredClone(options.input) },
|
|
2560
|
+
signal: approvalSignal, deadline: manifest.deadline }), approvalSignal);
|
|
2561
|
+
assertFreshDecision(contextProposalApproval, started);
|
|
2562
|
+
this.contextApprovals.require(manifest.taskId, 'contract_acceptance', target, contextProposalApproval);
|
|
2563
|
+
validateContextGrantUse(manifest, options.input, (this.options.now ?? Date.now)());
|
|
2564
|
+
}
|
|
2250
2565
|
if (options.beforePropose) {
|
|
2251
2566
|
const approvalSignal = AbortSignal.any([
|
|
2252
2567
|
AbortSignal.timeout(Math.max(0, Date.parse(manifest.deadline) - Date.now())),
|
|
@@ -2278,6 +2593,8 @@ export class RequesterClient {
|
|
|
2278
2593
|
await operation.authorize(childIndex, {
|
|
2279
2594
|
targetDigest: approvalTargetDigest, approvedAt: (this.options.now ?? Date.now)(), policy: 'configured-auto',
|
|
2280
2595
|
});
|
|
2596
|
+
if (managedChild)
|
|
2597
|
+
await managedChild.prepare(manifest, options.input, peer);
|
|
2281
2598
|
await operation?.sending();
|
|
2282
2599
|
const wireDelivery = this.deliveryService && stableRelayUrl ? {
|
|
2283
2600
|
service: this.deliveryService, context: { messageId: '00000000-0000-4000-8000-000000000000',
|
|
@@ -2294,7 +2611,7 @@ export class RequesterClient {
|
|
|
2294
2611
|
const mailbox = inbox.attach(manifest.taskId);
|
|
2295
2612
|
const timeouts = new SessionTimeouts(timeoutOptions, manifest.deadline, bridge.signal);
|
|
2296
2613
|
const signal = timeouts.signal;
|
|
2297
|
-
const wire = new SessionWire(mailboxRelay(relay, mailbox), owner, peer, manifest.taskId, [owner.did, peer], signal, auditPath, this.options.allowUnencryptedEnvelopes === true, false, wireDelivery);
|
|
2614
|
+
const wire = new SessionWire(mailboxRelay(relay, mailbox), owner, peer, manifest.taskId, [owner.did, peer], signal, auditPath, this.options.allowUnencryptedEnvelopes === true, false, wireDelivery, manifest.contextGrant);
|
|
2298
2615
|
if (onProgress) {
|
|
2299
2616
|
wire.onProgress = async (progress, envelope) => {
|
|
2300
2617
|
await onProgress(progress, {
|
|
@@ -2391,6 +2708,17 @@ export class RequesterClient {
|
|
|
2391
2708
|
// Recheck the live phase immediately before transmission; owner review may cross a boundary.
|
|
2392
2709
|
this.protocol.outbound(invite?.agentCard ?? card, manifest.protocolVersion, peer, manifest.worker.agentDid);
|
|
2393
2710
|
options.beforeTransmit?.();
|
|
2711
|
+
validateContextGrantUse(manifest, options.input, (this.options.now ?? Date.now)());
|
|
2712
|
+
if (approvalMode && !verifyApproval(contextProposalApproval, owner.did, manifest.taskId, 'contract_acceptance', proposalTarget()))
|
|
2713
|
+
throw new Error('DELEGATION_APPROVAL_REQUIRED');
|
|
2714
|
+
if (approvalMode || managedChild) {
|
|
2715
|
+
options.signal?.throwIfAborted();
|
|
2716
|
+
if (Date.parse(manifest.deadline) <= Date.now())
|
|
2717
|
+
throw new Error('DELEGATION_INVALID');
|
|
2718
|
+
if (invite)
|
|
2719
|
+
parseAndVerifyInviteToken(options.to, Date.now(), { allowPrivateRelay: this.options.allowPrivateRelay });
|
|
2720
|
+
}
|
|
2721
|
+
managedChild?.beforeTransmit(manifest, options.input);
|
|
2394
2722
|
await wire.sendEvent('PROPOSE', {
|
|
2395
2723
|
proposal: signPayload(manifest, owner),
|
|
2396
2724
|
input: options.input,
|
|
@@ -2404,6 +2732,8 @@ export class RequesterClient {
|
|
|
2404
2732
|
digest(accept.contract.manifest) !== digest(manifest) ||
|
|
2405
2733
|
!verifyApproval(accept.approval, peer, manifest.taskId, 'contract_acceptance', accept.contract.contractDigest))
|
|
2406
2734
|
throw new Error('INVALID_ACCEPTANCE');
|
|
2735
|
+
validateContextGrantUse(manifest, options.input, (this.options.now ?? Date.now)());
|
|
2736
|
+
managedChild?.onAcceptedContract(accept.contract);
|
|
2407
2737
|
gateway = new OwnerGateway(owner, accept.contract, policy);
|
|
2408
2738
|
gateway.machine.apply(accept.transition);
|
|
2409
2739
|
gateway.machine.apply((await wire.receiveEvent('START')).payload.payload);
|
|
@@ -2416,15 +2746,32 @@ export class RequesterClient {
|
|
|
2416
2746
|
timeouts.wait();
|
|
2417
2747
|
gateway.machine.apply(data.transition);
|
|
2418
2748
|
hooks?.onDelivered?.();
|
|
2419
|
-
const approval = () =>
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2749
|
+
const approval = () => {
|
|
2750
|
+
if (approvalMode || managedChild)
|
|
2751
|
+
requireOwnerDecision(this.approvalProvider);
|
|
2752
|
+
const lineage = approvalMode ? verifyDelegationAttachment(data.delegation, accept.contract) : undefined;
|
|
2753
|
+
const started = approvalMode || managedChild ? Date.now() : 0;
|
|
2754
|
+
const pendingApproval = this.approvalProvider.approve({
|
|
2755
|
+
taskId: manifest.taskId,
|
|
2756
|
+
gate: 'final_verification',
|
|
2757
|
+
target: data,
|
|
2758
|
+
details: { manifest, deliverableDigest: data.artifact.payload.digest,
|
|
2759
|
+
...(approvalMode ? { delegateeOwnerDids: lineage.delegateeOwnerDids,
|
|
2760
|
+
delegationDigests: data.delegation.children.map(p => ({ linkDigest: digest(p.link),
|
|
2761
|
+
contractDigest: p.childContract.contractDigest, grantDigest: digest(p.childContract.manifest.contextGrant) })) } : {}) },
|
|
2762
|
+
deadline: manifest.deadline,
|
|
2763
|
+
signal,
|
|
2764
|
+
});
|
|
2765
|
+
if (!approvalMode && !managedChild)
|
|
2766
|
+
return pendingApproval;
|
|
2767
|
+
return pendingApproval.then(receipt => {
|
|
2768
|
+
assertFreshDecision(receipt, started);
|
|
2769
|
+
if (Date.parse(manifest.deadline) <= Date.now())
|
|
2770
|
+
throw new Error('DELEGATION_INVALID');
|
|
2771
|
+
return receipt;
|
|
2772
|
+
});
|
|
2773
|
+
};
|
|
2774
|
+
const { result, verification, settlement } = await abortable(gateway.verifyAndSettle(wire.sealed(envelope), this.options.verificationEngine ?? new VerificationEngine(), approval, { ...(approvalMode ? { ownerDecision: this.approvalProvider.ownerDecision } : {}), allowUnencryptedEnvelopes: this.options.allowUnencryptedEnvelopes === true, onVerifiedDelivery: () => {
|
|
2428
2775
|
if (!wireDelivery)
|
|
2429
2776
|
return;
|
|
2430
2777
|
wireDelivery.service.markPeerObserved(wireDelivery.context.messageId, { status: 'replied', envelopeId: wireDelivery.context.messageId,
|
|
@@ -2490,8 +2837,9 @@ export class RequesterClient {
|
|
|
2490
2837
|
?.body.payload;
|
|
2491
2838
|
if (cached.taskId !== manifest.taskId || !['SETTLED', 'CANCELLED'].includes(cached.state) ||
|
|
2492
2839
|
!verifyAudit(cached.bundle, { taskId: manifest.taskId, actors: [owner.did, peer] }).valid ||
|
|
2493
|
-
|
|
2494
|
-
!
|
|
2840
|
+
proposalAuditDigest(proposed) !== e.recovery.proposalDigest ||
|
|
2841
|
+
!(manifest.contextGrant ? verifyContextAcceptanceAudit(accepted, manifest) :
|
|
2842
|
+
accepted && verifyContract(accepted.contract) && digest(accepted.contract.manifest) === digest(manifest)) ||
|
|
2495
2843
|
(cached.state === 'CANCELLED' && (e.code !== 'TASK_CANCELLED' || cached.deliverable !== null ||
|
|
2496
2844
|
cached.bundle.entries.at(-1)?.body.eventType !== 'CANCEL')) ||
|
|
2497
2845
|
(cached.state === 'SETTLED' && (!delivered ||
|
|
@@ -2499,8 +2847,14 @@ export class RequesterClient {
|
|
|
2499
2847
|
digest(delivered.artifact.payload.data) !== digest(cached.deliverable) ||
|
|
2500
2848
|
cached.bundle.entries.at(-1)?.body.eventType !== 'SETTLED')))
|
|
2501
2849
|
throw new Error('INVALID_RECOVERY_OUTCOME');
|
|
2502
|
-
if (cached.state === 'SETTLED')
|
|
2850
|
+
if (cached.state === 'SETTLED') {
|
|
2851
|
+
if (manifest.contextGrant) {
|
|
2852
|
+
verifyRecoveredDelegation(delivered, proposed, accepted);
|
|
2853
|
+
if (delivered?.delegation && digest(delivered.delegation.parentContract.manifest) !== digest(manifest))
|
|
2854
|
+
throw new Error('INVALID_RECOVERY_OUTCOME');
|
|
2855
|
+
}
|
|
2503
2856
|
hooks?.onDelivered?.();
|
|
2857
|
+
}
|
|
2504
2858
|
return cached;
|
|
2505
2859
|
}
|
|
2506
2860
|
}
|
|
@@ -2519,11 +2873,11 @@ export class RequesterClient {
|
|
|
2519
2873
|
catch (inner) {
|
|
2520
2874
|
if (inner instanceof Error && inner.message === 'CANCELLED')
|
|
2521
2875
|
throw inner;
|
|
2522
|
-
await wire.rawSend('ERROR', { code: safeError(inner) }).catch(() => { });
|
|
2876
|
+
await wire.rawSend('ERROR', { code: safeError(inner, !!manifest.contextGrant) }).catch(() => { });
|
|
2523
2877
|
throw inner;
|
|
2524
2878
|
}
|
|
2525
2879
|
}
|
|
2526
|
-
await wire.rawSend('ERROR', { code: safeError(e) }).catch(() => { });
|
|
2880
|
+
await wire.rawSend('ERROR', { code: safeError(e, !!manifest.contextGrant) }).catch(() => { });
|
|
2527
2881
|
throw e;
|
|
2528
2882
|
}
|
|
2529
2883
|
catch (error) {
|
|
@@ -2549,6 +2903,7 @@ export class RequesterClient {
|
|
|
2549
2903
|
validateIdempotencyKey(options.idempotencyKey);
|
|
2550
2904
|
options = { ...options, input: structuredClone(options.input),
|
|
2551
2905
|
outputSchema: options.outputSchema && structuredClone(options.outputSchema),
|
|
2906
|
+
contextGrant: options.contextGrant && structuredClone(options.contextGrant),
|
|
2552
2907
|
budget: options.budget && structuredClone(options.budget),
|
|
2553
2908
|
capabilities: options.capabilities && structuredClone(options.capabilities) };
|
|
2554
2909
|
options = { ...options, workers: structuredClone(options.workers) };
|
|
@@ -2617,6 +2972,7 @@ export class RequesterClient {
|
|
|
2617
2972
|
description: options.description,
|
|
2618
2973
|
input: options.input,
|
|
2619
2974
|
outputSchema: options.outputSchema,
|
|
2975
|
+
contextGrant: options.contextGrant,
|
|
2620
2976
|
executionTimeoutMs: options.executionTimeoutMs,
|
|
2621
2977
|
timeoutMs: options.timeoutMs,
|
|
2622
2978
|
taskTimeoutMs: options.taskTimeoutMs,
|
|
@@ -2769,7 +3125,8 @@ function verifiedWorkerDid(bundle, expectedDid) {
|
|
|
2769
3125
|
const accept = bundle.entries.find((entry) => entry.body.eventType === 'ACCEPT');
|
|
2770
3126
|
const payload = accept?.body.payload;
|
|
2771
3127
|
const worker = payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
2772
|
-
? payload.contract?.manifest?.worker
|
|
3128
|
+
? payload.contract?.manifest?.worker ??
|
|
3129
|
+
payload.worker
|
|
2773
3130
|
: undefined;
|
|
2774
3131
|
const actors = bundle.checkpoint.actor.payload.actors;
|
|
2775
3132
|
if (!worker ||
|
|
@@ -2821,7 +3178,7 @@ async function canonicalizeDiscoveredWorkers(targets, options) {
|
|
|
2821
3178
|
export function buildTaskManifest(options) {
|
|
2822
3179
|
const now = (options.now ?? Date.now)();
|
|
2823
3180
|
const createdAt = new Date(now).toISOString(), deadline = new Date(now + (options.taskTimeoutMs ?? 600000)).toISOString();
|
|
2824
|
-
|
|
3181
|
+
const manifest = {
|
|
2825
3182
|
protocolVersion: options.protocolVersion ?? '0.2.0',
|
|
2826
3183
|
taskId: 'task_' + randomUUID().replaceAll('-', ''),
|
|
2827
3184
|
createdAt,
|
|
@@ -2861,5 +3218,14 @@ export function buildTaskManifest(options) {
|
|
|
2861
3218
|
},
|
|
2862
3219
|
hitlPolicy: { requiredGates: ['GATE_CONTRACT_ACCEPT', 'GATE_DELIVERY_ACCEPT'] },
|
|
2863
3220
|
};
|
|
3221
|
+
if (options.contextGrant) {
|
|
3222
|
+
manifest.contextGrant = buildContextGrant(options.contextGrant, createdAt);
|
|
3223
|
+
if (manifest.contextGrant.deliveryMode === 'approval_redelegation') {
|
|
3224
|
+
manifest.permissions.maxDelegationDepth = 1;
|
|
3225
|
+
manifest.hitlPolicy.requiredGates.push('GATE_SUB_DELEGATION');
|
|
3226
|
+
}
|
|
3227
|
+
validateManifest(manifest);
|
|
3228
|
+
}
|
|
3229
|
+
return manifest;
|
|
2864
3230
|
}
|
|
2865
3231
|
//# sourceMappingURL=session.js.map
|