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.
Files changed (115) hide show
  1. package/README.ja.md +4 -2
  2. package/README.md +6 -2
  3. package/dist/cli/dist/accept.d.ts +2 -1
  4. package/dist/cli/dist/accept.js +18 -7
  5. package/dist/cli/dist/accept.js.map +1 -1
  6. package/dist/cli/dist/index.js +46 -13
  7. package/dist/cli/dist/index.js.map +1 -1
  8. package/dist/cli/dist/invite-links.d.ts +14 -0
  9. package/dist/cli/dist/invite-links.js +263 -0
  10. package/dist/cli/dist/invite-links.js.map +1 -0
  11. package/dist/cli/dist/invite-output.d.ts +1 -1
  12. package/dist/cli/dist/invite-output.js +3 -2
  13. package/dist/cli/dist/invite-output.js.map +1 -1
  14. package/dist/cli/dist/network.js +16 -23
  15. package/dist/cli/dist/network.js.map +1 -1
  16. package/dist/cli/dist/receipt.d.ts +12 -2
  17. package/dist/cli/dist/receipt.js +6 -5
  18. package/dist/cli/dist/receipt.js.map +1 -1
  19. package/dist/cli/dist/sdk.d.ts +6 -4
  20. package/dist/cli/dist/sdk.js +3 -2
  21. package/dist/cli/dist/sdk.js.map +1 -1
  22. package/dist/examples/two-owner-demo/dist/run-demo.js +3 -1
  23. package/dist/examples/two-owner-demo/dist/run-demo.js.map +1 -1
  24. package/dist/package.json +1 -1
  25. package/dist/packages/core/dist/context-grant.d.ts +13 -0
  26. package/dist/packages/core/dist/context-grant.js +65 -0
  27. package/dist/packages/core/dist/context-grant.js.map +1 -0
  28. package/dist/packages/core/dist/delegation.d.ts +17 -0
  29. package/dist/packages/core/dist/delegation.js +126 -0
  30. package/dist/packages/core/dist/delegation.js.map +1 -0
  31. package/dist/packages/core/dist/edge.d.ts +2 -0
  32. package/dist/packages/core/dist/edge.js +2 -0
  33. package/dist/packages/core/dist/edge.js.map +1 -1
  34. package/dist/packages/core/dist/index.d.ts +4 -0
  35. package/dist/packages/core/dist/index.js +4 -0
  36. package/dist/packages/core/dist/index.js.map +1 -1
  37. package/dist/packages/core/dist/invite-link.d.ts +72 -0
  38. package/dist/packages/core/dist/invite-link.js +225 -0
  39. package/dist/packages/core/dist/invite-link.js.map +1 -0
  40. package/dist/packages/core/dist/invite-sas.d.ts +7 -0
  41. package/dist/packages/core/dist/invite-sas.js +15 -0
  42. package/dist/packages/core/dist/invite-sas.js.map +1 -0
  43. package/dist/packages/core/dist/relay-auth.d.ts +1 -1
  44. package/dist/packages/core/dist/relay-auth.js +1 -1
  45. package/dist/packages/core/dist/relay-auth.js.map +1 -1
  46. package/dist/packages/core/dist/schema.d.ts +1 -1
  47. package/dist/packages/core/dist/schema.js +4 -0
  48. package/dist/packages/core/dist/schema.js.map +1 -1
  49. package/dist/packages/core/dist/types.d.ts +52 -0
  50. package/dist/packages/ledger/dist/index.d.ts +70 -2
  51. package/dist/packages/ledger/dist/index.js +404 -26
  52. package/dist/packages/ledger/dist/index.js.map +1 -1
  53. package/dist/packages/relay/dist/fetch.js +4 -0
  54. package/dist/packages/relay/dist/fetch.js.map +1 -1
  55. package/dist/packages/relay/dist/invites.d.ts +42 -0
  56. package/dist/packages/relay/dist/invites.js +347 -0
  57. package/dist/packages/relay/dist/invites.js.map +1 -0
  58. package/dist/packages/relay/dist/worker.d.ts +3 -0
  59. package/dist/packages/relay/dist/worker.js +21 -0
  60. package/dist/packages/relay/dist/worker.js.map +1 -1
  61. package/dist/packages/runtime/dist/a2a.js +1 -1
  62. package/dist/packages/runtime/dist/adapter.d.ts +1 -0
  63. package/dist/packages/runtime/dist/approval-providers.d.ts +4 -0
  64. package/dist/packages/runtime/dist/approval-providers.js +3 -0
  65. package/dist/packages/runtime/dist/approval-providers.js.map +1 -1
  66. package/dist/packages/runtime/dist/connection-persistence.d.ts +1 -1
  67. package/dist/packages/runtime/dist/context-grant-audit.d.ts +8 -0
  68. package/dist/packages/runtime/dist/context-grant-audit.js +41 -0
  69. package/dist/packages/runtime/dist/context-grant-audit.js.map +1 -0
  70. package/dist/packages/runtime/dist/delegation.d.ts +37 -0
  71. package/dist/packages/runtime/dist/delegation.js +125 -0
  72. package/dist/packages/runtime/dist/delegation.js.map +1 -0
  73. package/dist/packages/runtime/dist/gateway.d.ts +1 -0
  74. package/dist/packages/runtime/dist/gateway.js.map +1 -1
  75. package/dist/packages/runtime/dist/index.d.ts +2 -0
  76. package/dist/packages/runtime/dist/index.js +2 -0
  77. package/dist/packages/runtime/dist/index.js.map +1 -1
  78. package/dist/packages/runtime/dist/mcp-server.js +22 -8
  79. package/dist/packages/runtime/dist/mcp-server.js.map +1 -1
  80. package/dist/packages/runtime/dist/mcp-tool-catalog.js +44 -5
  81. package/dist/packages/runtime/dist/mcp-tool-catalog.js.map +1 -1
  82. package/dist/packages/runtime/dist/session-audit.d.ts +6 -1
  83. package/dist/packages/runtime/dist/session-audit.js +35 -4
  84. package/dist/packages/runtime/dist/session-audit.js.map +1 -1
  85. package/dist/packages/runtime/dist/session.d.ts +21 -1
  86. package/dist/packages/runtime/dist/session.js +427 -62
  87. package/dist/packages/runtime/dist/session.js.map +1 -1
  88. package/dist/packages/runtime/dist/task-journal.d.ts +10 -1
  89. package/dist/packages/runtime/dist/task-journal.js +26 -1
  90. package/dist/packages/runtime/dist/task-journal.js.map +1 -1
  91. package/dist/packages/runtime/dist/verifier.d.ts +5 -2
  92. package/dist/packages/runtime/dist/verifier.js +14 -4
  93. package/dist/packages/runtime/dist/verifier.js.map +1 -1
  94. package/dist/spec/audit-entry.schema.json +25 -1
  95. package/dist/spec/audit-evidence.schema.json +179 -0
  96. package/dist/spec/audit-file-record.schema.json +177 -0
  97. package/dist/spec/audit-interruption.schema.json +110 -0
  98. package/dist/spec/context-grant-audit-payload.schema.json +24 -0
  99. package/dist/spec/context-grant.schema.json +146 -0
  100. package/dist/spec/delegation-attachment.schema.json +167 -0
  101. package/dist/spec/delegation-link.schema.json +141 -0
  102. package/dist/spec/examples/context-pooling/approval_redelegation/sample-delegation-attachment.json +413 -0
  103. package/dist/spec/examples/context-pooling/hand_over/sample-context-grant.json +18 -0
  104. package/dist/spec/examples/context-pooling/hand_over/sample-task-manifest.0.2.0.json +94 -0
  105. package/dist/spec/examples/context-pooling/sample-context-grant-audit-payload.json +4 -0
  106. package/dist/spec/examples/context-pooling/view_only/sample-context-grant.json +18 -0
  107. package/dist/spec/examples/context-pooling/view_only/sample-task-manifest.0.2.0.json +94 -0
  108. package/dist/spec/examples/sample-audit-evidence.json +138 -0
  109. package/dist/spec/examples/sample-audit-file-record.json +31 -0
  110. package/dist/spec/examples/sample-audit-interruption.json +29 -0
  111. package/dist/spec/task-manifest.0.2.0.schema.json +87 -1
  112. package/docs/context-grants.md +221 -0
  113. package/docs/sdk.ja.md +4 -0
  114. package/docs/sdk.md +4 -0
  115. 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 { parseJsonl, verifyAudit } from '../../ledger/dist/index.js';
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
- const pending = this.intakeStore.pendingTask(record.requesterDid, record.taskId, record.proposalDigest);
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 (!proposal || digest({ manifest: proposal.proposal.payload, input: proposal.input }) !== record.proposalDigest)
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
- try {
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 (/^(PROTOCOL_|CONNECTION_|INVALID_CONNECTION_|QUARANTINE_|INSPECTION_|ACCESS_MODE_REMOVED|INVITE_REFERENCE_UNAVAILABLE)/.test(exact))
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
- return unwrapIdempotency(await this.keyedStore().run('acp_delegate_task', options.idempotencyKey, action, async (operation) => ({ isError: false, value: await this.delegate({ ...options,
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 = () => this.approvalProvider.approve({
2420
- taskId: manifest.taskId,
2421
- gate: 'final_verification',
2422
- target: data,
2423
- details: { manifest, deliverableDigest: data.artifact.payload.digest },
2424
- deadline: manifest.deadline,
2425
- signal,
2426
- });
2427
- const { result, verification, settlement } = await abortable(gateway.verifyAndSettle(wire.sealed(envelope), this.options.verificationEngine ?? new VerificationEngine(), approval, { allowUnencryptedEnvelopes: this.options.allowUnencryptedEnvelopes === true, onVerifiedDelivery: () => {
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
- !proposed || digest({ manifest: proposed.proposal.payload, input: proposed.input }) !== e.recovery.proposalDigest ||
2494
- !accepted || !verifyContract(accepted.contract) || digest(accepted.contract.manifest) !== digest(manifest) ||
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
- return {
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