sealbridge 0.4.2 → 0.5.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 +4 -2
- package/README.md +6 -2
- 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/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/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 +4 -0
- package/dist/packages/core/dist/index.js +4 -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/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/index.d.ts +2 -0
- package/dist/packages/runtime/dist/index.js +2 -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/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 +427 -62
- 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 +4 -0
- package/docs/sdk.md +4 -0
- 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 {
|
|
@@ -1003,6 +1076,7 @@ export class WorkerNode {
|
|
|
1003
1076
|
let m;
|
|
1004
1077
|
let auditPath;
|
|
1005
1078
|
let checkpoint;
|
|
1079
|
+
let auditStage;
|
|
1006
1080
|
const saveCheckpoint = (patch) => {
|
|
1007
1081
|
if (!checkpoint || !this.taskJournal)
|
|
1008
1082
|
return;
|
|
@@ -1013,6 +1087,16 @@ export class WorkerNode {
|
|
|
1013
1087
|
saveCheckpoint({ phase: 'terminal', outcome, notificationPending: true });
|
|
1014
1088
|
this.deliveryService?.store.receipt(first, Date.parse(m.deadline), 'terminal', outcome.code);
|
|
1015
1089
|
};
|
|
1090
|
+
const setAuditStage = (stage) => {
|
|
1091
|
+
if (this.options.auditInterruptionFormat !== 1 || !auditPath)
|
|
1092
|
+
return;
|
|
1093
|
+
// Format 1 requires a full, PROPOSE-only prefix. Grant audits use compact
|
|
1094
|
+
// proposal evidence and bilateral lifecycle entries, outside that scope.
|
|
1095
|
+
if (m.contextGrant)
|
|
1096
|
+
return;
|
|
1097
|
+
auditStage = stage;
|
|
1098
|
+
saveCheckpoint({ auditStage: stage });
|
|
1099
|
+
};
|
|
1016
1100
|
const acknowledgeProposal = async (signal) => {
|
|
1017
1101
|
this.deliveryService?.store.receipt(first, Date.parse(m.deadline), 'processed');
|
|
1018
1102
|
await acknowledgeRelay(relay, first, signal);
|
|
@@ -1032,6 +1116,18 @@ export class WorkerNode {
|
|
|
1032
1116
|
throw new Error('INVALID_PROPOSAL');
|
|
1033
1117
|
this.assertPeer(first.payload.senderDid);
|
|
1034
1118
|
validateManifest(manifest);
|
|
1119
|
+
validateContextGrantUse(manifest, data.input, this.protocol.now());
|
|
1120
|
+
if (manifest.contextGrant?.deliveryMode === 'approval_redelegation') {
|
|
1121
|
+
const facility = this.options.redelegation;
|
|
1122
|
+
if (!facility || !this.options.auditPath || !this.taskJournal)
|
|
1123
|
+
throw new Error('DELEGATION_INVALID');
|
|
1124
|
+
requireOwnerDecision(facility.ownerApprovalProvider);
|
|
1125
|
+
requireOwnerDecision(facility.requester.approvalProvider);
|
|
1126
|
+
if (facility.requester.options.owner.did !== this.options.owner.did)
|
|
1127
|
+
throw new Error('DELEGATION_INVALID');
|
|
1128
|
+
}
|
|
1129
|
+
if (manifest.contextGrant && this.options.inboxPath)
|
|
1130
|
+
throw new Error('CONTEXT_GRANT_INPUT_RETENTION_CONFLICT');
|
|
1035
1131
|
m = manifest;
|
|
1036
1132
|
auditPath = this.options.auditPath
|
|
1037
1133
|
? this.completed++ === 0 && !existsSync(this.options.auditPath)
|
|
@@ -1057,14 +1153,14 @@ export class WorkerNode {
|
|
|
1057
1153
|
}
|
|
1058
1154
|
}
|
|
1059
1155
|
catch (e) {
|
|
1060
|
-
this.reportRejection(first.payload.taskId, e);
|
|
1156
|
+
this.reportRejection(first.payload.taskId, e, !!data?.proposal?.payload?.contextGrant);
|
|
1061
1157
|
try {
|
|
1062
1158
|
await relay.send(signEnvelope({
|
|
1063
1159
|
taskId: first.payload.taskId,
|
|
1064
1160
|
recipientDid: first.payload.senderDid,
|
|
1065
1161
|
seq: 0,
|
|
1066
1162
|
eventType: 'ERROR',
|
|
1067
|
-
payload: asJson(rejectionBody(e)),
|
|
1163
|
+
payload: asJson(rejectionBody(e, !!data?.proposal?.payload?.contextGrant)),
|
|
1068
1164
|
}, this.options.owner, { encrypt: true }));
|
|
1069
1165
|
}
|
|
1070
1166
|
catch {
|
|
@@ -1078,7 +1174,7 @@ export class WorkerNode {
|
|
|
1078
1174
|
messageId: receiptFileId(first.payload.envelopeId), peerOwnerDid: m.requester.ownerDid,
|
|
1079
1175
|
relayUrl: this.options.relayUrl ?? (typeof this.options.relay === 'string' ? this.options.relay : this.agentCard.payload.relayEndpoint),
|
|
1080
1176
|
deliveryDeadline: Date.parse(m.deadline),
|
|
1081
|
-
} } : undefined);
|
|
1177
|
+
} } : undefined, m.contextGrant);
|
|
1082
1178
|
const cancelController = new AbortController();
|
|
1083
1179
|
wire.earlyCancel = () => cancelController.abort('CANCELLED');
|
|
1084
1180
|
let heartbeat;
|
|
@@ -1094,7 +1190,30 @@ export class WorkerNode {
|
|
|
1094
1190
|
}
|
|
1095
1191
|
};
|
|
1096
1192
|
let gateway, workspace;
|
|
1193
|
+
let childHostOpen = true;
|
|
1097
1194
|
let preserveWorkspace = false;
|
|
1195
|
+
let proposalAudited = false;
|
|
1196
|
+
const cleanupWorkspace = async () => {
|
|
1197
|
+
if (!workspace)
|
|
1198
|
+
return;
|
|
1199
|
+
const path = workspace;
|
|
1200
|
+
try {
|
|
1201
|
+
await rm(path, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
|
1202
|
+
workspace = undefined;
|
|
1203
|
+
}
|
|
1204
|
+
catch (error) {
|
|
1205
|
+
const code = error?.code ?? 'WORKSPACE_CLEANUP_FAILED';
|
|
1206
|
+
// Reporting residual files must not replace settlement or the execution error.
|
|
1207
|
+
try {
|
|
1208
|
+
if (this.options.onWorkspaceCleanupError)
|
|
1209
|
+
void Promise.resolve(this.options.onWorkspaceCleanupError({ taskId: m.taskId, workspaceDir: path, code }))
|
|
1210
|
+
.catch(() => { });
|
|
1211
|
+
else
|
|
1212
|
+
console.warn('Workspace cleanup incomplete (' + code + '): ' + path);
|
|
1213
|
+
}
|
|
1214
|
+
catch { }
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1098
1217
|
let trackedTaskId;
|
|
1099
1218
|
// Progress metrics are applied synchronously. The tail only serializes wire frames.
|
|
1100
1219
|
let progressTail = Promise.resolve();
|
|
@@ -1135,6 +1254,7 @@ export class WorkerNode {
|
|
|
1135
1254
|
if (reason)
|
|
1136
1255
|
throw new ProposalRejectedError(reason);
|
|
1137
1256
|
validateManifest(m);
|
|
1257
|
+
validateContextGrantUse(m, data.input, this.protocol.now());
|
|
1138
1258
|
if (!validateOutput(m.io.inputSchema, data.input))
|
|
1139
1259
|
throw new Error('INVALID_PROPOSAL');
|
|
1140
1260
|
if (this.options.executionTimeoutMs !== undefined && m.budget.maxWallClockSeconds * 1000 > this.options.executionTimeoutMs)
|
|
@@ -1226,21 +1346,31 @@ export class WorkerNode {
|
|
|
1226
1346
|
if (!contractProvider || peerTrust === 'limited' && (contractProvider.kind === 'policy' ||
|
|
1227
1347
|
contractProvider === this.approvalProvider && contractProvider.kind !== 'human'))
|
|
1228
1348
|
throw new Error('APPROVAL_CHANNEL_UNAVAILABLE');
|
|
1349
|
+
if (m.contextGrant?.deliveryMode === 'approval_redelegation')
|
|
1350
|
+
requireOwnerDecision(contractProvider);
|
|
1229
1351
|
await wire.receiveEvent('PROPOSE', first);
|
|
1352
|
+
proposalAudited = true;
|
|
1353
|
+
await wire.contextEvent('CONTEXT_GRANT');
|
|
1354
|
+
await wire.contextEvent('CONTEXT_MODE');
|
|
1355
|
+
setAuditStage('OWNER_APPROVAL');
|
|
1230
1356
|
const contract = createContract(m, data.proposal, this.options.owner);
|
|
1357
|
+
const openingStarted = m.contextGrant?.deliveryMode === 'approval_redelegation' ? Date.now() : 0;
|
|
1231
1358
|
const approval = await abortable(contractProvider.approve({
|
|
1232
1359
|
taskId: m.taskId,
|
|
1233
1360
|
gate: 'contract_acceptance',
|
|
1234
1361
|
target: contract.contractDigest,
|
|
1235
1362
|
details: {
|
|
1236
|
-
manifest: m,
|
|
1237
|
-
input: data.input,
|
|
1363
|
+
manifest: m.contextGrant?.deliveryMode === 'approval_redelegation' ? structuredClone(m) : m,
|
|
1364
|
+
input: m.contextGrant?.deliveryMode === 'approval_redelegation' ? structuredClone(data.input) : data.input,
|
|
1238
1365
|
capabilities: data.capabilityChains,
|
|
1239
1366
|
...(inspection ? { inspection } : {}),
|
|
1240
1367
|
from: first.payload.senderDid,
|
|
1241
1368
|
},
|
|
1242
1369
|
signal: activeSignal,
|
|
1243
1370
|
}), activeSignal);
|
|
1371
|
+
if (m.contextGrant?.deliveryMode === 'approval_redelegation')
|
|
1372
|
+
assertFreshDecision(approval, openingStarted);
|
|
1373
|
+
setAuditStage('OUT_OF_SCOPE');
|
|
1244
1374
|
if (this.trust(m.requester.ownerDid, m.protocolVersion) === 'unknown')
|
|
1245
1375
|
throw new Error('APPROVAL_CHANNEL_UNAVAILABLE');
|
|
1246
1376
|
this.assertCapabilitiesActive(data.capabilityChains, capabilityContext);
|
|
@@ -1249,8 +1379,11 @@ export class WorkerNode {
|
|
|
1249
1379
|
throw new Error('APPROVAL_CHANNEL_UNAVAILABLE');
|
|
1250
1380
|
gateway = new OwnerGateway(this.options.owner, contract, policy, this.revocation);
|
|
1251
1381
|
const accepted = gateway.accept(approval);
|
|
1382
|
+
validateContextGrantUse(m, data.input, this.protocol.now());
|
|
1383
|
+
await wire.contextEvent('CONTEXT_OPENED');
|
|
1252
1384
|
this.acceptedPeers.set(m.taskId, m.requester.ownerDid);
|
|
1253
|
-
saveCheckpoint({ phase: 'accepted'
|
|
1385
|
+
saveCheckpoint({ phase: 'accepted', ...(m.contextGrant?.deliveryMode === 'approval_redelegation'
|
|
1386
|
+
? { delegation: { parentContract: contract, attempts: [] } } : {}) });
|
|
1254
1387
|
if (inspection?.inspectionStatus === 'complete')
|
|
1255
1388
|
this.deliveryService?.recipientStatus.observe(first.payload.envelopeId, 'opened');
|
|
1256
1389
|
if (this.options.dailyLimit)
|
|
@@ -1287,7 +1420,10 @@ export class WorkerNode {
|
|
|
1287
1420
|
// Synchronous notification failures are also best effort.
|
|
1288
1421
|
}
|
|
1289
1422
|
}
|
|
1423
|
+
validateContextGrantUse(m, data.input, this.protocol.now());
|
|
1424
|
+
setAuditStage('ACCEPT_ACK_WAIT');
|
|
1290
1425
|
await wire.sendEvent('ACCEPT', { contract, transition: accepted, approval });
|
|
1426
|
+
setAuditStage('OUT_OF_SCOPE');
|
|
1291
1427
|
this.options.onEvent?.('ACCEPT', m.taskId);
|
|
1292
1428
|
await wire.sendEvent('START', gateway.transition('START'));
|
|
1293
1429
|
heartbeat = setTimeout(() => void sendHeartbeat(), this.options.heartbeatIntervalMs ?? 15_000);
|
|
@@ -1364,6 +1500,110 @@ export class WorkerNode {
|
|
|
1364
1500
|
void run.catch(() => undefined);
|
|
1365
1501
|
return run;
|
|
1366
1502
|
};
|
|
1503
|
+
const managedChildren = new Set();
|
|
1504
|
+
let childrenClosed = false;
|
|
1505
|
+
const delegationState = m.contextGrant?.deliveryMode === 'approval_redelegation'
|
|
1506
|
+
? { parentContract: structuredClone(contract), attempts: [] } : undefined;
|
|
1507
|
+
const persistChildren = () => saveCheckpoint({ delegation: structuredClone(delegationState) });
|
|
1508
|
+
const checkParent = () => {
|
|
1509
|
+
executionSignal.throwIfAborted();
|
|
1510
|
+
if (!childHostOpen || gateway.machine.state !== 'IN_PROGRESS' || wire.cancelHandled)
|
|
1511
|
+
throw new Error('DELEGATION_INVALID');
|
|
1512
|
+
this.assertPeer(m.requester.ownerDid, m.taskId);
|
|
1513
|
+
this.assertCapabilitiesActive(data.capabilityChains, capabilityContext);
|
|
1514
|
+
validateContextGrantUse(m, data.input, this.protocol.now());
|
|
1515
|
+
if (Date.parse(m.deadline) <= this.protocol.now())
|
|
1516
|
+
throw new Error('DELEGATION_INVALID');
|
|
1517
|
+
};
|
|
1518
|
+
const dispatchChild = async (request) => {
|
|
1519
|
+
if (childrenClosed || !delegationState)
|
|
1520
|
+
throw new Error('DELEGATION_INVALID');
|
|
1521
|
+
if (!request || Object.keys(request).some(k => !['to', 'relay', 'manifest', 'input', 'auditPath'].includes(k)))
|
|
1522
|
+
throw new Error('DELEGATION_INVALID');
|
|
1523
|
+
const child = structuredClone(request.manifest), input = structuredClone(request.input);
|
|
1524
|
+
if (Object.hasOwn(child, 'parentTaskId'))
|
|
1525
|
+
throw new Error('DELEGATION_INVALID');
|
|
1526
|
+
assertDelegationAttenuation(m, child);
|
|
1527
|
+
if (child.requester.ownerDid !== this.options.owner.did || child.taskId === m.taskId ||
|
|
1528
|
+
[m.requester.ownerDid, this.options.owner.did].includes(child.worker.ownerDid) ||
|
|
1529
|
+
delegationState.attempts.some(a => a.childTaskId === child.taskId))
|
|
1530
|
+
throw new Error('DELEGATION_INVALID');
|
|
1531
|
+
checkParent();
|
|
1532
|
+
validateContextGrantUse(child, input, this.protocol.now());
|
|
1533
|
+
const facility = this.options.redelegation;
|
|
1534
|
+
requireOwnerDecision(facility.ownerApprovalProvider);
|
|
1535
|
+
requireOwnerDecision(facility.requester.approvalProvider);
|
|
1536
|
+
if (facility.requester.options.owner.did !== this.options.owner.did)
|
|
1537
|
+
throw new Error('DELEGATION_INVALID');
|
|
1538
|
+
let attempt;
|
|
1539
|
+
const options = { to: request.to, relay: request.relay, manifest: child, input,
|
|
1540
|
+
title: child.task.title, description: child.task.description, auditPath: request.auditPath,
|
|
1541
|
+
signal: executionSignal, capabilityChains: [] };
|
|
1542
|
+
managedChildHooks.set(options, {
|
|
1543
|
+
prepare: async (manifest, selectedInput, peer) => {
|
|
1544
|
+
checkParent();
|
|
1545
|
+
assertDelegationAttenuation(m, manifest);
|
|
1546
|
+
if (peer !== child.worker.ownerDid || peer === m.requester.ownerDid || peer === this.options.owner.did)
|
|
1547
|
+
throw new Error('DELEGATION_INVALID');
|
|
1548
|
+
const values = { childManifestDigest: digest(manifest), childInputDigest: digest(selectedInput), delegateeOwnerDid: peer };
|
|
1549
|
+
const target = redelegationTarget(contract, values), started = Date.now();
|
|
1550
|
+
const receipt = await abortable(facility.ownerApprovalProvider.approve({ taskId: m.taskId, gate: 'redelegation',
|
|
1551
|
+
target: structuredClone(target), details: { parentManifest: structuredClone(m), childManifest: structuredClone(manifest),
|
|
1552
|
+
input: structuredClone(selectedInput), delegateeOwnerDid: peer, depthRemaining: 0,
|
|
1553
|
+
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.' },
|
|
1554
|
+
signal: executionSignal, deadline: m.deadline }), executionSignal);
|
|
1555
|
+
checkParent();
|
|
1556
|
+
validateContextGrantUse(manifest, selectedInput, this.protocol.now());
|
|
1557
|
+
assertFreshDecision(receipt, started);
|
|
1558
|
+
gateway.approvals.require(m.taskId, 'redelegation', target, receipt);
|
|
1559
|
+
if (delegationState.attempts.some(a => a.childTaskId === child.taskId))
|
|
1560
|
+
throw new Error('DELEGATION_INVALID');
|
|
1561
|
+
attempt = { childTaskId: child.taskId, ...values, approval: receipt, status: 'approved' };
|
|
1562
|
+
delegationState.attempts.push(attempt);
|
|
1563
|
+
persistChildren();
|
|
1564
|
+
},
|
|
1565
|
+
beforeTransmit: (manifest, selectedInput) => {
|
|
1566
|
+
checkParent();
|
|
1567
|
+
validateContextGrantUse(manifest, selectedInput, this.protocol.now());
|
|
1568
|
+
if (!attempt || digest(manifest) !== attempt.childManifestDigest || digest(selectedInput) !== attempt.childInputDigest ||
|
|
1569
|
+
!verifyApproval(attempt.approval, this.options.owner.did, m.taskId, 'redelegation', redelegationTarget(contract, attempt)))
|
|
1570
|
+
throw new Error('DELEGATION_APPROVAL_REQUIRED');
|
|
1571
|
+
attempt.status = 'dispatching';
|
|
1572
|
+
persistChildren();
|
|
1573
|
+
},
|
|
1574
|
+
onAcceptedContract: acceptedChild => {
|
|
1575
|
+
if (!attempt)
|
|
1576
|
+
throw new Error('DELEGATION_INVALID');
|
|
1577
|
+
// Keep uncertainty durable if signing or publication fails after authenticated ACCEPT.
|
|
1578
|
+
attempt.status = 'unresolved';
|
|
1579
|
+
persistChildren();
|
|
1580
|
+
checkParent();
|
|
1581
|
+
const link = createDelegationLink({ parentContract: contract, childContract: acceptedChild }, this.options.owner);
|
|
1582
|
+
attempt.proof = { link, childContract: structuredClone(acceptedChild) };
|
|
1583
|
+
attempt.status = 'accepted';
|
|
1584
|
+
persistChildren();
|
|
1585
|
+
},
|
|
1586
|
+
});
|
|
1587
|
+
try {
|
|
1588
|
+
const result = await facility.requester.delegate(options);
|
|
1589
|
+
if (!attempt?.proof || result.state !== 'SETTLED')
|
|
1590
|
+
throw new Error('DELEGATION_INVALID');
|
|
1591
|
+
attempt.status = 'completed';
|
|
1592
|
+
persistChildren();
|
|
1593
|
+
return result;
|
|
1594
|
+
}
|
|
1595
|
+
catch (error) {
|
|
1596
|
+
if (attempt) {
|
|
1597
|
+
attempt.status = attempt.proof || attempt.status === 'approved' || error instanceof PeerRejectedError && attempt.status !== 'unresolved'
|
|
1598
|
+
? 'failed' : 'unresolved';
|
|
1599
|
+
persistChildren();
|
|
1600
|
+
}
|
|
1601
|
+
throw error;
|
|
1602
|
+
}
|
|
1603
|
+
finally {
|
|
1604
|
+
managedChildHooks.delete(options);
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1367
1607
|
const context = {
|
|
1368
1608
|
taskId: m.taskId,
|
|
1369
1609
|
manifest: structuredClone(m),
|
|
@@ -1407,7 +1647,7 @@ export class WorkerNode {
|
|
|
1407
1647
|
}
|
|
1408
1648
|
if (metricsError) {
|
|
1409
1649
|
progressFailure ??= metricsError;
|
|
1410
|
-
const code = safeError(metricsError);
|
|
1650
|
+
const code = safeError(metricsError, !!m.contextGrant);
|
|
1411
1651
|
return enqueueProgress(async () => {
|
|
1412
1652
|
// Wait until any in-flight PROGRESS handshake finishes, then reject the session.
|
|
1413
1653
|
wireFailureSent = true;
|
|
@@ -1455,10 +1695,22 @@ export class WorkerNode {
|
|
|
1455
1695
|
return result;
|
|
1456
1696
|
},
|
|
1457
1697
|
};
|
|
1698
|
+
if (delegationState)
|
|
1699
|
+
context.redelegate = request => {
|
|
1700
|
+
const operation = dispatchChild(request);
|
|
1701
|
+
managedChildren.add(operation);
|
|
1702
|
+
void operation.finally(() => managedChildren.delete(operation)).catch(() => { });
|
|
1703
|
+
return operation;
|
|
1704
|
+
};
|
|
1458
1705
|
this.assertPeer(m.requester.ownerDid, m.taskId);
|
|
1459
1706
|
this.assertCapabilitiesActive(data.capabilityChains, capabilityContext);
|
|
1707
|
+
validateContextGrantUse(m, data.input, this.protocol.now());
|
|
1460
1708
|
this.deliveryService?.recipientStatus.handoff(first, m);
|
|
1461
1709
|
const output = await abortable(this.options.adapter.executeTask(context), executionSignal);
|
|
1710
|
+
childrenClosed = true;
|
|
1711
|
+
if (delegationState)
|
|
1712
|
+
await abortable(Promise.allSettled([...managedChildren]), executionSignal);
|
|
1713
|
+
const delegation = delegationState ? assembleDelegation(delegationState) : undefined;
|
|
1462
1714
|
if (!this.options.inboxPath)
|
|
1463
1715
|
await acknowledgeProposal(executionSignal);
|
|
1464
1716
|
// A task may return while a queued progress update is still waiting for its ACK.
|
|
@@ -1473,7 +1725,11 @@ export class WorkerNode {
|
|
|
1473
1725
|
const delivery = createDelivery(m.taskId, contract.contractDigest, output, gateway.quota.receipt(this.options.owner), this.options.owner, {
|
|
1474
1726
|
effectiveIsolationLevel,
|
|
1475
1727
|
...(sandboxBoundary ? { sandboxBoundary } : {}),
|
|
1476
|
-
});
|
|
1728
|
+
}, delegation);
|
|
1729
|
+
if (m.contextGrant && workspace) {
|
|
1730
|
+
await cleanupWorkspace();
|
|
1731
|
+
await wire.contextEvent('CONTEXT_DELETION_ATTEMPTED');
|
|
1732
|
+
}
|
|
1477
1733
|
const transition = gateway.transition('DELIVER');
|
|
1478
1734
|
saveCheckpoint({ phase: 'delivered' });
|
|
1479
1735
|
await wire.sendEvent('DELIVER', { ...delivery, transition });
|
|
@@ -1493,6 +1749,7 @@ export class WorkerNode {
|
|
|
1493
1749
|
return { taskId: m.taskId, state: 'SETTLED', deliverable: output, bundle };
|
|
1494
1750
|
}
|
|
1495
1751
|
catch (e) {
|
|
1752
|
+
childHostOpen = false;
|
|
1496
1753
|
if (checkpoint?.outcome?.result) {
|
|
1497
1754
|
// The bilateral checkpoint is already durable even if its final relay send failed.
|
|
1498
1755
|
await this.notifyOutcome(checkpoint).then(() => saveCheckpoint({ notificationPending: false })).catch(() => { });
|
|
@@ -1516,23 +1773,47 @@ export class WorkerNode {
|
|
|
1516
1773
|
return { taskId: m.taskId, state: 'CANCELLED', deliverable: null, bundle };
|
|
1517
1774
|
}
|
|
1518
1775
|
catch (inner) {
|
|
1519
|
-
await wire.rawSend('ERROR', { code: safeError(inner) }).catch(() => { });
|
|
1776
|
+
await wire.rawSend('ERROR', { code: safeError(inner, !!m.contextGrant) }).catch(() => { });
|
|
1520
1777
|
throw inner;
|
|
1521
1778
|
}
|
|
1522
1779
|
}
|
|
1523
1780
|
await progressTail;
|
|
1524
1781
|
preserveWorkspace = activeSignal.aborted || idleSignal.aborted;
|
|
1782
|
+
const timeoutReason = activeSignal.reason instanceof Error ? activeSignal.reason.message : activeSignal.reason;
|
|
1783
|
+
const interruptionReason = idleSignal.aborted ? 'WORKER_STOPPED' :
|
|
1784
|
+
activeSignal.aborted && ['TASK_TIMEOUT', 'PEER_WAIT_TIMEOUT'].includes(String(timeoutReason)) ? 'SESSION_TIMEOUT' : undefined;
|
|
1785
|
+
if (interruptionReason && auditStage && auditStage !== 'OUT_OF_SCOPE' && auditPath) {
|
|
1786
|
+
const entries = await wire.quiesceAudit();
|
|
1787
|
+
try {
|
|
1788
|
+
// No journal is available only to embedders without persisted task checkpoints.
|
|
1789
|
+
const record = checkpoint ?? { version: 1, taskId: m.taskId, requesterDid: m.requester.ownerDid,
|
|
1790
|
+
envelopeId: first.payload.envelopeId, proposalDigest: digest({ manifest: m, input: data.input }),
|
|
1791
|
+
phase: 'received', auditPath, auditStage, invocations: [] };
|
|
1792
|
+
this.closeInterruptedAudit(record, interruptionReason, entries, saved => {
|
|
1793
|
+
checkpoint = saved;
|
|
1794
|
+
this.saveTaskCheckpoint(saved);
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
catch {
|
|
1798
|
+
this.reportInterruptionFailure(m.taskId);
|
|
1799
|
+
}
|
|
1800
|
+
saveOutcome({ code: 'TASK_INTERRUPTED' });
|
|
1801
|
+
}
|
|
1802
|
+
else if (auditStage && !preserveWorkspace)
|
|
1803
|
+
setAuditStage('OUT_OF_SCOPE');
|
|
1525
1804
|
if (idleSignal.aborted)
|
|
1526
1805
|
throw e;
|
|
1527
|
-
const diagnostic = preserveWorkspace ? { code: 'TASK_INTERRUPTED' } : rejectionBody(progressFailure ?? e);
|
|
1806
|
+
const diagnostic = preserveWorkspace ? { code: 'TASK_INTERRUPTED' } : rejectionBody(progressFailure ?? e, !!m.contextGrant);
|
|
1528
1807
|
saveOutcome(diagnostic);
|
|
1529
1808
|
this.deliveryService?.recipientStatus.end(first.payload.envelopeId);
|
|
1530
1809
|
const refused = this.heldReview.list().find(r => r.taskId === m.taskId);
|
|
1531
1810
|
if (refused && diagnostic.code !== 'PENDING_OWNER_APPROVAL' && !preserveWorkspace)
|
|
1532
1811
|
this.heldReview.discard(refused.id);
|
|
1533
|
-
this.reportRejection(m.taskId, progressFailure ?? e);
|
|
1812
|
+
this.reportRejection(m.taskId, progressFailure ?? e, !!m.contextGrant);
|
|
1534
1813
|
if (!wireFailureSent) {
|
|
1535
1814
|
wireFailureSent = true;
|
|
1815
|
+
if (proposalAudited && !preserveWorkspace && diagnostic.code !== 'PENDING_OWNER_APPROVAL')
|
|
1816
|
+
await wire.contextEvent('CONTEXT_DISCARDED').catch(() => { });
|
|
1536
1817
|
await wire.rawSend('ERROR', diagnostic)
|
|
1537
1818
|
.then(async () => {
|
|
1538
1819
|
saveCheckpoint({ notificationPending: false });
|
|
@@ -1548,6 +1829,7 @@ export class WorkerNode {
|
|
|
1548
1829
|
throw e;
|
|
1549
1830
|
}
|
|
1550
1831
|
finally {
|
|
1832
|
+
childHostOpen = false;
|
|
1551
1833
|
this.deliveryService?.recipientStatus.end(first.payload.envelopeId);
|
|
1552
1834
|
heartbeatStopped = true;
|
|
1553
1835
|
clearTimeout(heartbeat);
|
|
@@ -1562,23 +1844,8 @@ export class WorkerNode {
|
|
|
1562
1844
|
gateway?.close();
|
|
1563
1845
|
if (idleSignal.aborted && checkpoint?.phase !== 'terminal')
|
|
1564
1846
|
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
|
-
}
|
|
1847
|
+
if (workspace && !preserveWorkspace)
|
|
1848
|
+
await cleanupWorkspace();
|
|
1582
1849
|
}
|
|
1583
1850
|
}
|
|
1584
1851
|
/**
|
|
@@ -1920,24 +2187,26 @@ class PendingTaskError extends Error {
|
|
|
1920
2187
|
function diagnosticError(code, field, expected, actual, hint) {
|
|
1921
2188
|
return new DiagnosticError(code, { field, expected: digest(expected), actual: digest(actual), hint });
|
|
1922
2189
|
}
|
|
1923
|
-
function rejectionBody(error) {
|
|
2190
|
+
function rejectionBody(error, contextGrant = false) {
|
|
1924
2191
|
if (error instanceof PendingTaskError)
|
|
1925
2192
|
return { code: 'PENDING_OWNER_APPROVAL', reason: error.reason };
|
|
1926
2193
|
if (error instanceof DiagnosticError)
|
|
1927
|
-
return { code: safeError(error), detail: error.detail };
|
|
2194
|
+
return { code: safeError(error, contextGrant), detail: error.detail };
|
|
1928
2195
|
if (error instanceof ProposalRejectedError)
|
|
1929
2196
|
return { code: error.code, reason: error.reason };
|
|
1930
|
-
if (safeError(error) === 'PENDING_OWNER_APPROVAL')
|
|
2197
|
+
if (safeError(error, contextGrant) === 'PENDING_OWNER_APPROVAL')
|
|
1931
2198
|
return { code: 'PENDING_OWNER_APPROVAL', reason: PENDING_MESSAGE };
|
|
1932
|
-
return { code: safeError(error) };
|
|
2199
|
+
return { code: safeError(error, contextGrant) };
|
|
1933
2200
|
}
|
|
1934
|
-
function safeError(e) {
|
|
2201
|
+
function safeError(e, contextGrant = false) {
|
|
1935
2202
|
const s = String(e);
|
|
1936
2203
|
const exact = e instanceof Error ? e.message.split(':')[0] : '';
|
|
1937
2204
|
if (['OUTBOX_STORE_INVALID', 'OUTBOX_STORE_BUSY', 'OUTBOX_STORE_UNWRITABLE', 'OUTBOX_FULL',
|
|
1938
|
-
'OUTBOX_DEADLINE_EXPIRED', 'DELIVERY_RECORD_CONFLICT'].includes(exact))
|
|
2205
|
+
'OUTBOX_DEADLINE_EXPIRED', 'DELIVERY_RECORD_CONFLICT', 'AUDIT_INTERRUPTION_WRITE_FAILED'].includes(exact))
|
|
1939
2206
|
return exact;
|
|
1940
|
-
if (
|
|
2207
|
+
if (contextGrant && ['DELEGATION_PROOF_REQUIRED', 'DELEGATION_INVALID', 'DELEGATION_APPROVAL_REQUIRED'].includes(exact))
|
|
2208
|
+
return exact;
|
|
2209
|
+
if (/^(CONTEXT_GRANT_|PROTOCOL_|CONNECTION_|INVALID_CONNECTION_|QUARANTINE_|INSPECTION_|ACCESS_MODE_REMOVED|INVITE_REFERENCE_UNAVAILABLE)/.test(exact))
|
|
1941
2210
|
return exact;
|
|
1942
2211
|
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
2212
|
if (s === 'Error: ' + code || s.startsWith('Error: ' + code + ':'))
|
|
@@ -1974,6 +2243,14 @@ function freezeApprovalAction(value) {
|
|
|
1974
2243
|
}
|
|
1975
2244
|
return value;
|
|
1976
2245
|
}
|
|
2246
|
+
// Module-private hooks cannot be supplied by task output or arbitrary delegate options.
|
|
2247
|
+
const managedChildHooks = new WeakMap();
|
|
2248
|
+
function assertFreshDecision(receipt, started) {
|
|
2249
|
+
const approved = Date.parse(receipt?.payload?.approvedAt), signed = Date.parse(receipt?.signature?.signedAt);
|
|
2250
|
+
if (!Number.isFinite(approved) || !Number.isFinite(signed) || approved < started || signed < started ||
|
|
2251
|
+
approved > Date.now() || signed > Date.now())
|
|
2252
|
+
throw new Error('DELEGATION_APPROVAL_REQUIRED');
|
|
2253
|
+
}
|
|
1977
2254
|
/** Completion failure preserves local verification without claiming a sealed checkpoint. */
|
|
1978
2255
|
export class IncompleteSettlementError extends Error {
|
|
1979
2256
|
incompleteReceipt;
|
|
@@ -2031,6 +2308,7 @@ export async function deliverRevocation(options) {
|
|
|
2031
2308
|
}
|
|
2032
2309
|
export class RequesterClient {
|
|
2033
2310
|
options;
|
|
2311
|
+
contextApprovals;
|
|
2034
2312
|
idempotency;
|
|
2035
2313
|
keyedStore() {
|
|
2036
2314
|
return this.idempotency ??= new IdempotencyStore(this.options.owner, {
|
|
@@ -2040,7 +2318,7 @@ export class RequesterClient {
|
|
|
2040
2318
|
keyedAction(options) {
|
|
2041
2319
|
const allowed = ['idempotencyKey', 'idempotencyOperation', 'idempotencyChildIndex', 'workerRef', 'pendingId', 'pendingRelayUrl',
|
|
2042
2320
|
'renegotiateProtocol', 'beforePropose', 'beforeTransmit', 'to', 'discover', 'relay', 'title', 'description', 'input',
|
|
2043
|
-
'outputSchema', 'budget', 'manifest', 'auditPath', 'signal', 'capabilities', 'capabilityChains', 'minimumIsolationLevel',
|
|
2321
|
+
'outputSchema', 'contextGrant', 'budget', 'manifest', 'auditPath', 'signal', 'capabilities', 'capabilityChains', 'minimumIsolationLevel',
|
|
2044
2322
|
'onProgress', 'workers', 'mode', 'quorumSize', 'completionTimeoutMs', 'timeoutMs', 'executionTimeoutMs', 'taskTimeoutMs',
|
|
2045
2323
|
'waitTimeoutMs', 'connectTimeoutMs', 'transportIdleTimeoutMs', 'heartbeatIntervalMs'];
|
|
2046
2324
|
if (Object.keys(options).some(k => !allowed.includes(k)))
|
|
@@ -2073,6 +2351,7 @@ export class RequesterClient {
|
|
|
2073
2351
|
validateSessionTimeouts(options);
|
|
2074
2352
|
this.approvalProvider =
|
|
2075
2353
|
options.approvalProvider ?? new InteractiveCliApprovalProvider(options.owner);
|
|
2354
|
+
this.contextApprovals = new ApprovalGate(options.owner.did, policy);
|
|
2076
2355
|
this.deliveryService = options.deliveryService;
|
|
2077
2356
|
if (this.deliveryService && this.deliveryService.owner.did !== options.owner.did)
|
|
2078
2357
|
throw deliveryError('OUTBOX_STORE_INVALID');
|
|
@@ -2128,24 +2407,40 @@ export class RequesterClient {
|
|
|
2128
2407
|
});
|
|
2129
2408
|
}
|
|
2130
2409
|
async delegate(options, hooks) {
|
|
2410
|
+
const managedChild = managedChildHooks.get(options);
|
|
2131
2411
|
if (options.idempotencyKey !== undefined && !options.idempotencyOperation) {
|
|
2132
2412
|
validateIdempotencyKey(options.idempotencyKey);
|
|
2133
2413
|
options = { ...options, input: structuredClone(options.input),
|
|
2134
2414
|
outputSchema: options.outputSchema && structuredClone(options.outputSchema),
|
|
2415
|
+
contextGrant: options.contextGrant && structuredClone(options.contextGrant),
|
|
2135
2416
|
budget: options.budget && structuredClone(options.budget),
|
|
2136
2417
|
capabilities: options.capabilities && structuredClone(options.capabilities) };
|
|
2137
2418
|
options = { ...options, manifest: options.manifest && structuredClone(options.manifest),
|
|
2138
2419
|
discover: options.discover && structuredClone(options.discover),
|
|
2139
2420
|
capabilityChains: options.capabilityChains && structuredClone(options.capabilityChains) };
|
|
2140
2421
|
const action = this.keyedAction(options);
|
|
2141
|
-
|
|
2422
|
+
const result = unwrapIdempotency(await this.keyedStore().run('acp_delegate_task', options.idempotencyKey, action, async (operation) => ({ isError: false, value: await this.delegate({ ...options,
|
|
2142
2423
|
idempotencyKey: undefined, idempotencyOperation: operation }, hooks) })));
|
|
2424
|
+
if ((options.manifest ? options.manifest.contextGrant : options.contextGrant)?.deliveryMode === 'approval_redelegation' &&
|
|
2425
|
+
result.state === 'SETTLED') {
|
|
2426
|
+
const proposed = result.bundle.entries.find(e => e.body.eventType === 'PROPOSE')?.body.payload;
|
|
2427
|
+
const accepted = result.bundle.entries.find(e => e.body.eventType === 'ACCEPT')?.body.payload;
|
|
2428
|
+
const delivered = result.bundle.entries.find(e => e.body.eventType === 'DELIVER')?.body.payload;
|
|
2429
|
+
if (!delivered || !verifyAudit(result.bundle).valid)
|
|
2430
|
+
throw new Error('DELEGATION_INVALID');
|
|
2431
|
+
verifyRecoveredDelegation(delivered, proposed, accepted);
|
|
2432
|
+
if (options.manifest && delivered.delegation && digest(delivered.delegation.parentContract.manifest) !== digest(options.manifest))
|
|
2433
|
+
throw new Error('DELEGATION_INVALID');
|
|
2434
|
+
}
|
|
2435
|
+
return result;
|
|
2143
2436
|
}
|
|
2144
2437
|
assertConnectionsOptions(this.options);
|
|
2145
2438
|
// Retain the reviewed values even if the caller changes its objects while awaiting consent.
|
|
2146
|
-
if (options.beforePropose
|
|
2439
|
+
if (options.beforePropose || managedChild ||
|
|
2440
|
+
(options.manifest ? options.manifest.contextGrant : options.contextGrant)?.deliveryMode === 'approval_redelegation')
|
|
2147
2441
|
options = { ...options,
|
|
2148
2442
|
input: structuredClone(options.input), manifest: options.manifest && structuredClone(options.manifest),
|
|
2443
|
+
contextGrant: options.contextGrant && structuredClone(options.contextGrant),
|
|
2149
2444
|
budget: options.budget && structuredClone(options.budget),
|
|
2150
2445
|
outputSchema: options.outputSchema && structuredClone(options.outputSchema),
|
|
2151
2446
|
capabilities: options.capabilities && structuredClone(options.capabilities),
|
|
@@ -2217,6 +2512,7 @@ export class RequesterClient {
|
|
|
2217
2512
|
options.budget.maxWallClockSeconds * 1000 > timeoutOptions.executionTimeoutMs)
|
|
2218
2513
|
throw new Error('INCOMPATIBLE_TIMEOUTS: requested budget exceeds executionTimeoutMs');
|
|
2219
2514
|
validateManifest(manifest);
|
|
2515
|
+
validateContextGrantUse(manifest, options.input, (this.options.now ?? Date.now)());
|
|
2220
2516
|
if (manifest.requester.ownerDid !== owner.did ||
|
|
2221
2517
|
manifest.worker?.ownerDid !== peer ||
|
|
2222
2518
|
Date.parse(manifest.deadline) <= Date.now())
|
|
@@ -2247,6 +2543,24 @@ export class RequesterClient {
|
|
|
2247
2543
|
if (operation)
|
|
2248
2544
|
await operation.prepare({ childIndex, manifest, input: options.input, relayUrl: stableRelayUrl ?? approvalRelay,
|
|
2249
2545
|
peerOwnerDid: peer, peerAgentDid: manifest.worker.agentDid, approvalTargetDigest });
|
|
2546
|
+
const approvalMode = manifest.contextGrant?.deliveryMode === 'approval_redelegation';
|
|
2547
|
+
const proposalTarget = () => ({ type: 'ContextGrantProposal', manifestDigest: digest(manifest),
|
|
2548
|
+
inputDigest: digest(options.input), workerOwnerDid: peer });
|
|
2549
|
+
let contextProposalApproval;
|
|
2550
|
+
if (approvalMode) {
|
|
2551
|
+
requireOwnerDecision(this.approvalProvider);
|
|
2552
|
+
const target = proposalTarget(), started = Date.now();
|
|
2553
|
+
const approvalSignal = AbortSignal.any([
|
|
2554
|
+
AbortSignal.timeout(Math.max(1, Math.min(Date.parse(manifest.deadline) - Date.now(), timeoutOptions.waitTimeoutMs ?? timeoutOptions.taskTimeoutMs ?? timeoutOptions.timeoutMs ?? 600000))),
|
|
2555
|
+
...(options.signal ? [options.signal] : []),
|
|
2556
|
+
]);
|
|
2557
|
+
contextProposalApproval = await abortable(this.approvalProvider.approve({ taskId: manifest.taskId, gate: 'contract_acceptance',
|
|
2558
|
+
target: structuredClone(target), details: { manifest: structuredClone(manifest), input: structuredClone(options.input) },
|
|
2559
|
+
signal: approvalSignal, deadline: manifest.deadline }), approvalSignal);
|
|
2560
|
+
assertFreshDecision(contextProposalApproval, started);
|
|
2561
|
+
this.contextApprovals.require(manifest.taskId, 'contract_acceptance', target, contextProposalApproval);
|
|
2562
|
+
validateContextGrantUse(manifest, options.input, (this.options.now ?? Date.now)());
|
|
2563
|
+
}
|
|
2250
2564
|
if (options.beforePropose) {
|
|
2251
2565
|
const approvalSignal = AbortSignal.any([
|
|
2252
2566
|
AbortSignal.timeout(Math.max(0, Date.parse(manifest.deadline) - Date.now())),
|
|
@@ -2278,6 +2592,8 @@ export class RequesterClient {
|
|
|
2278
2592
|
await operation.authorize(childIndex, {
|
|
2279
2593
|
targetDigest: approvalTargetDigest, approvedAt: (this.options.now ?? Date.now)(), policy: 'configured-auto',
|
|
2280
2594
|
});
|
|
2595
|
+
if (managedChild)
|
|
2596
|
+
await managedChild.prepare(manifest, options.input, peer);
|
|
2281
2597
|
await operation?.sending();
|
|
2282
2598
|
const wireDelivery = this.deliveryService && stableRelayUrl ? {
|
|
2283
2599
|
service: this.deliveryService, context: { messageId: '00000000-0000-4000-8000-000000000000',
|
|
@@ -2294,7 +2610,7 @@ export class RequesterClient {
|
|
|
2294
2610
|
const mailbox = inbox.attach(manifest.taskId);
|
|
2295
2611
|
const timeouts = new SessionTimeouts(timeoutOptions, manifest.deadline, bridge.signal);
|
|
2296
2612
|
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);
|
|
2613
|
+
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
2614
|
if (onProgress) {
|
|
2299
2615
|
wire.onProgress = async (progress, envelope) => {
|
|
2300
2616
|
await onProgress(progress, {
|
|
@@ -2391,6 +2707,17 @@ export class RequesterClient {
|
|
|
2391
2707
|
// Recheck the live phase immediately before transmission; owner review may cross a boundary.
|
|
2392
2708
|
this.protocol.outbound(invite?.agentCard ?? card, manifest.protocolVersion, peer, manifest.worker.agentDid);
|
|
2393
2709
|
options.beforeTransmit?.();
|
|
2710
|
+
validateContextGrantUse(manifest, options.input, (this.options.now ?? Date.now)());
|
|
2711
|
+
if (approvalMode && !verifyApproval(contextProposalApproval, owner.did, manifest.taskId, 'contract_acceptance', proposalTarget()))
|
|
2712
|
+
throw new Error('DELEGATION_APPROVAL_REQUIRED');
|
|
2713
|
+
if (approvalMode || managedChild) {
|
|
2714
|
+
options.signal?.throwIfAborted();
|
|
2715
|
+
if (Date.parse(manifest.deadline) <= Date.now())
|
|
2716
|
+
throw new Error('DELEGATION_INVALID');
|
|
2717
|
+
if (invite)
|
|
2718
|
+
parseAndVerifyInviteToken(options.to, Date.now(), { allowPrivateRelay: this.options.allowPrivateRelay });
|
|
2719
|
+
}
|
|
2720
|
+
managedChild?.beforeTransmit(manifest, options.input);
|
|
2394
2721
|
await wire.sendEvent('PROPOSE', {
|
|
2395
2722
|
proposal: signPayload(manifest, owner),
|
|
2396
2723
|
input: options.input,
|
|
@@ -2404,6 +2731,8 @@ export class RequesterClient {
|
|
|
2404
2731
|
digest(accept.contract.manifest) !== digest(manifest) ||
|
|
2405
2732
|
!verifyApproval(accept.approval, peer, manifest.taskId, 'contract_acceptance', accept.contract.contractDigest))
|
|
2406
2733
|
throw new Error('INVALID_ACCEPTANCE');
|
|
2734
|
+
validateContextGrantUse(manifest, options.input, (this.options.now ?? Date.now)());
|
|
2735
|
+
managedChild?.onAcceptedContract(accept.contract);
|
|
2407
2736
|
gateway = new OwnerGateway(owner, accept.contract, policy);
|
|
2408
2737
|
gateway.machine.apply(accept.transition);
|
|
2409
2738
|
gateway.machine.apply((await wire.receiveEvent('START')).payload.payload);
|
|
@@ -2416,15 +2745,32 @@ export class RequesterClient {
|
|
|
2416
2745
|
timeouts.wait();
|
|
2417
2746
|
gateway.machine.apply(data.transition);
|
|
2418
2747
|
hooks?.onDelivered?.();
|
|
2419
|
-
const approval = () =>
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2748
|
+
const approval = () => {
|
|
2749
|
+
if (approvalMode || managedChild)
|
|
2750
|
+
requireOwnerDecision(this.approvalProvider);
|
|
2751
|
+
const lineage = approvalMode ? verifyDelegationAttachment(data.delegation, accept.contract) : undefined;
|
|
2752
|
+
const started = approvalMode || managedChild ? Date.now() : 0;
|
|
2753
|
+
const pendingApproval = this.approvalProvider.approve({
|
|
2754
|
+
taskId: manifest.taskId,
|
|
2755
|
+
gate: 'final_verification',
|
|
2756
|
+
target: data,
|
|
2757
|
+
details: { manifest, deliverableDigest: data.artifact.payload.digest,
|
|
2758
|
+
...(approvalMode ? { delegateeOwnerDids: lineage.delegateeOwnerDids,
|
|
2759
|
+
delegationDigests: data.delegation.children.map(p => ({ linkDigest: digest(p.link),
|
|
2760
|
+
contractDigest: p.childContract.contractDigest, grantDigest: digest(p.childContract.manifest.contextGrant) })) } : {}) },
|
|
2761
|
+
deadline: manifest.deadline,
|
|
2762
|
+
signal,
|
|
2763
|
+
});
|
|
2764
|
+
if (!approvalMode && !managedChild)
|
|
2765
|
+
return pendingApproval;
|
|
2766
|
+
return pendingApproval.then(receipt => {
|
|
2767
|
+
assertFreshDecision(receipt, started);
|
|
2768
|
+
if (Date.parse(manifest.deadline) <= Date.now())
|
|
2769
|
+
throw new Error('DELEGATION_INVALID');
|
|
2770
|
+
return receipt;
|
|
2771
|
+
});
|
|
2772
|
+
};
|
|
2773
|
+
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
2774
|
if (!wireDelivery)
|
|
2429
2775
|
return;
|
|
2430
2776
|
wireDelivery.service.markPeerObserved(wireDelivery.context.messageId, { status: 'replied', envelopeId: wireDelivery.context.messageId,
|
|
@@ -2490,8 +2836,9 @@ export class RequesterClient {
|
|
|
2490
2836
|
?.body.payload;
|
|
2491
2837
|
if (cached.taskId !== manifest.taskId || !['SETTLED', 'CANCELLED'].includes(cached.state) ||
|
|
2492
2838
|
!verifyAudit(cached.bundle, { taskId: manifest.taskId, actors: [owner.did, peer] }).valid ||
|
|
2493
|
-
|
|
2494
|
-
!
|
|
2839
|
+
proposalAuditDigest(proposed) !== e.recovery.proposalDigest ||
|
|
2840
|
+
!(manifest.contextGrant ? verifyContextAcceptanceAudit(accepted, manifest) :
|
|
2841
|
+
accepted && verifyContract(accepted.contract) && digest(accepted.contract.manifest) === digest(manifest)) ||
|
|
2495
2842
|
(cached.state === 'CANCELLED' && (e.code !== 'TASK_CANCELLED' || cached.deliverable !== null ||
|
|
2496
2843
|
cached.bundle.entries.at(-1)?.body.eventType !== 'CANCEL')) ||
|
|
2497
2844
|
(cached.state === 'SETTLED' && (!delivered ||
|
|
@@ -2499,8 +2846,14 @@ export class RequesterClient {
|
|
|
2499
2846
|
digest(delivered.artifact.payload.data) !== digest(cached.deliverable) ||
|
|
2500
2847
|
cached.bundle.entries.at(-1)?.body.eventType !== 'SETTLED')))
|
|
2501
2848
|
throw new Error('INVALID_RECOVERY_OUTCOME');
|
|
2502
|
-
if (cached.state === 'SETTLED')
|
|
2849
|
+
if (cached.state === 'SETTLED') {
|
|
2850
|
+
if (manifest.contextGrant) {
|
|
2851
|
+
verifyRecoveredDelegation(delivered, proposed, accepted);
|
|
2852
|
+
if (delivered?.delegation && digest(delivered.delegation.parentContract.manifest) !== digest(manifest))
|
|
2853
|
+
throw new Error('INVALID_RECOVERY_OUTCOME');
|
|
2854
|
+
}
|
|
2503
2855
|
hooks?.onDelivered?.();
|
|
2856
|
+
}
|
|
2504
2857
|
return cached;
|
|
2505
2858
|
}
|
|
2506
2859
|
}
|
|
@@ -2519,11 +2872,11 @@ export class RequesterClient {
|
|
|
2519
2872
|
catch (inner) {
|
|
2520
2873
|
if (inner instanceof Error && inner.message === 'CANCELLED')
|
|
2521
2874
|
throw inner;
|
|
2522
|
-
await wire.rawSend('ERROR', { code: safeError(inner) }).catch(() => { });
|
|
2875
|
+
await wire.rawSend('ERROR', { code: safeError(inner, !!manifest.contextGrant) }).catch(() => { });
|
|
2523
2876
|
throw inner;
|
|
2524
2877
|
}
|
|
2525
2878
|
}
|
|
2526
|
-
await wire.rawSend('ERROR', { code: safeError(e) }).catch(() => { });
|
|
2879
|
+
await wire.rawSend('ERROR', { code: safeError(e, !!manifest.contextGrant) }).catch(() => { });
|
|
2527
2880
|
throw e;
|
|
2528
2881
|
}
|
|
2529
2882
|
catch (error) {
|
|
@@ -2549,6 +2902,7 @@ export class RequesterClient {
|
|
|
2549
2902
|
validateIdempotencyKey(options.idempotencyKey);
|
|
2550
2903
|
options = { ...options, input: structuredClone(options.input),
|
|
2551
2904
|
outputSchema: options.outputSchema && structuredClone(options.outputSchema),
|
|
2905
|
+
contextGrant: options.contextGrant && structuredClone(options.contextGrant),
|
|
2552
2906
|
budget: options.budget && structuredClone(options.budget),
|
|
2553
2907
|
capabilities: options.capabilities && structuredClone(options.capabilities) };
|
|
2554
2908
|
options = { ...options, workers: structuredClone(options.workers) };
|
|
@@ -2617,6 +2971,7 @@ export class RequesterClient {
|
|
|
2617
2971
|
description: options.description,
|
|
2618
2972
|
input: options.input,
|
|
2619
2973
|
outputSchema: options.outputSchema,
|
|
2974
|
+
contextGrant: options.contextGrant,
|
|
2620
2975
|
executionTimeoutMs: options.executionTimeoutMs,
|
|
2621
2976
|
timeoutMs: options.timeoutMs,
|
|
2622
2977
|
taskTimeoutMs: options.taskTimeoutMs,
|
|
@@ -2769,7 +3124,8 @@ function verifiedWorkerDid(bundle, expectedDid) {
|
|
|
2769
3124
|
const accept = bundle.entries.find((entry) => entry.body.eventType === 'ACCEPT');
|
|
2770
3125
|
const payload = accept?.body.payload;
|
|
2771
3126
|
const worker = payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
2772
|
-
? payload.contract?.manifest?.worker
|
|
3127
|
+
? payload.contract?.manifest?.worker ??
|
|
3128
|
+
payload.worker
|
|
2773
3129
|
: undefined;
|
|
2774
3130
|
const actors = bundle.checkpoint.actor.payload.actors;
|
|
2775
3131
|
if (!worker ||
|
|
@@ -2821,7 +3177,7 @@ async function canonicalizeDiscoveredWorkers(targets, options) {
|
|
|
2821
3177
|
export function buildTaskManifest(options) {
|
|
2822
3178
|
const now = (options.now ?? Date.now)();
|
|
2823
3179
|
const createdAt = new Date(now).toISOString(), deadline = new Date(now + (options.taskTimeoutMs ?? 600000)).toISOString();
|
|
2824
|
-
|
|
3180
|
+
const manifest = {
|
|
2825
3181
|
protocolVersion: options.protocolVersion ?? '0.2.0',
|
|
2826
3182
|
taskId: 'task_' + randomUUID().replaceAll('-', ''),
|
|
2827
3183
|
createdAt,
|
|
@@ -2861,5 +3217,14 @@ export function buildTaskManifest(options) {
|
|
|
2861
3217
|
},
|
|
2862
3218
|
hitlPolicy: { requiredGates: ['GATE_CONTRACT_ACCEPT', 'GATE_DELIVERY_ACCEPT'] },
|
|
2863
3219
|
};
|
|
3220
|
+
if (options.contextGrant) {
|
|
3221
|
+
manifest.contextGrant = buildContextGrant(options.contextGrant, createdAt);
|
|
3222
|
+
if (manifest.contextGrant.deliveryMode === 'approval_redelegation') {
|
|
3223
|
+
manifest.permissions.maxDelegationDepth = 1;
|
|
3224
|
+
manifest.hitlPolicy.requiredGates.push('GATE_SUB_DELEGATION');
|
|
3225
|
+
}
|
|
3226
|
+
validateManifest(manifest);
|
|
3227
|
+
}
|
|
3228
|
+
return manifest;
|
|
2864
3229
|
}
|
|
2865
3230
|
//# sourceMappingURL=session.js.map
|