sealbridge 0.4.2 → 0.6.0

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