@sema-agent/server 7.151.0 → 7.152.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 (286) hide show
  1. package/MIGRATION.md +266 -0
  2. package/README.md +15 -3
  3. package/README.zh-CN.md +10 -2
  4. package/USAGE.md +93 -64
  5. package/deploy/sema-up/README.md +5 -4
  6. package/deploy/sema-up/cluster-up.sh +6 -6
  7. package/deploy/sema-up/kube-up.sh +5 -5
  8. package/deploy/sema-up/lib/flagshape.sh +36 -8
  9. package/deploy/sema-up/machines.example.yaml +3 -1
  10. package/deploy/sema-up/sema-up.sh +22 -16
  11. package/dist/approval-ask-audit-store.d.ts +1 -1
  12. package/dist/approval-ask-machine.d.ts +4 -10
  13. package/dist/approval-ask-retention.d.ts +1 -1
  14. package/dist/approval-card.d.ts +10 -12
  15. package/dist/approval-gate-currency.d.ts +8 -8
  16. package/dist/approval-gate-currency.js +3 -4
  17. package/dist/approval-hmac.d.ts +17 -1
  18. package/dist/approval-hmac.js +7 -1
  19. package/dist/approval-queue-kind.d.ts +1 -1
  20. package/dist/approval.d.ts +2 -2
  21. package/dist/approval.js +2 -2
  22. package/dist/bind-host.d.ts +1 -3
  23. package/dist/bind-host.js +2 -3
  24. package/dist/boot/coordinators.d.ts +1 -1
  25. package/dist/boot/coordinators.js +9 -10
  26. package/dist/boot/device-lane.js +2 -0
  27. package/dist/boot/execution-env.d.ts +1 -0
  28. package/dist/boot/execution-env.js +31 -5
  29. package/dist/boot/leader.d.ts +31 -30
  30. package/dist/boot/leader.js +38 -28
  31. package/dist/boot/leg-path-adjudication.d.ts +40 -0
  32. package/dist/boot/leg-path-adjudication.js +18 -0
  33. package/dist/boot/parked-revive-gate.js +3 -15
  34. package/dist/boot/reapers.d.ts +1 -1
  35. package/dist/boot/reapers.js +1 -1
  36. package/dist/boot/resolve-spec.js +12 -26
  37. package/dist/boot/runner-deps.d.ts +39 -35
  38. package/dist/boot/runner-deps.js +92 -89
  39. package/dist/boot/shutdown.d.ts +6 -0
  40. package/dist/boot/shutdown.js +10 -4
  41. package/dist/boot/stage-02-web-admin-pull.js +4 -5
  42. package/dist/boot/stage-05-execution-env.js +2 -1
  43. package/dist/boot/stage-06-runners.d.ts +2 -0
  44. package/dist/boot/stage-06-runners.js +10 -4
  45. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  46. package/dist/boot/stage-07-capability-layer.js +1 -1
  47. package/dist/boot/stage-08-reapers.d.ts +1 -0
  48. package/dist/boot/stage-09-leader.d.ts +1 -0
  49. package/dist/boot/stage-09-leader.js +2 -2
  50. package/dist/boot/stage-10-http-server.d.ts +8 -0
  51. package/dist/boot/stage-10-http-server.js +11 -15
  52. package/dist/boot/stores.js +7 -4
  53. package/dist/boot/web-admin.js +2 -7
  54. package/dist/capabilities/approvals-push.d.ts +3 -3
  55. package/dist/capabilities/scenarios.d.ts +28 -13
  56. package/dist/capabilities/scenarios.js +2 -2
  57. package/dist/capability-facts.d.ts +22 -20
  58. package/dist/capability-facts.js +18 -34
  59. package/dist/card-rule-offers.d.ts +32 -0
  60. package/dist/card-rule-offers.js +130 -0
  61. package/dist/config-catalog.js +12 -11
  62. package/dist/config-dir-trust.js +4 -2
  63. package/dist/config-invariants.d.ts +18 -7
  64. package/dist/config-invariants.js +51 -8
  65. package/dist/config-lkg.d.ts +5 -19
  66. package/dist/config-lkg.js +8 -62
  67. package/dist/config-provider.d.ts +1 -1
  68. package/dist/config-root-migration.d.ts +2 -2
  69. package/dist/config-types.d.ts +28 -38
  70. package/dist/config.d.ts +7 -5
  71. package/dist/config.js +55 -16
  72. package/dist/control-socket.d.ts +51 -0
  73. package/dist/control-socket.js +115 -0
  74. package/dist/deployment-governance.d.ts +2 -1
  75. package/dist/device-ws-hub.d.ts +7 -0
  76. package/dist/device-ws-hub.js +7 -1
  77. package/dist/elicitation.js +2 -2
  78. package/dist/entry-problems.d.ts +40 -3
  79. package/dist/entry-problems.js +3 -1
  80. package/dist/execution-env-refusal.d.ts +8 -2
  81. package/dist/execution-env-refusal.js +3 -0
  82. package/dist/fleet/engine-signal.d.ts +12 -29
  83. package/dist/fleet/engine-signal.js +1 -7
  84. package/dist/fleet-client.js +2 -5
  85. package/dist/gate-settlement.d.ts +1 -8
  86. package/dist/gate-settlement.js +0 -4
  87. package/dist/history-anchor.js +1 -1
  88. package/dist/hitl-delivery.d.ts +2 -3
  89. package/dist/hitl-delivery.js +0 -2
  90. package/dist/hooks/hook-llm.js +4 -2
  91. package/dist/hooks/hook-runner.d.ts +3 -2
  92. package/dist/hooks/hook-runner.js +16 -1
  93. package/dist/host-decision.d.ts +11 -11
  94. package/dist/http/active-run-conflict.d.ts +35 -2
  95. package/dist/http/active-run-conflict.js +48 -19
  96. package/dist/http/approval-sweeps.d.ts +1 -1
  97. package/dist/http/approval-sweeps.js +5 -2
  98. package/dist/http/checkpoint-ctx.d.ts +2 -0
  99. package/dist/http/checkpoint-ctx.js +1 -0
  100. package/dist/http/dispatch.d.ts +6 -2
  101. package/dist/http/dispatch.js +25 -5
  102. package/dist/http/durable-run-leg.js +0 -1
  103. package/dist/http/health-reader.d.ts +16 -9
  104. package/dist/http/health-reader.js +2 -1
  105. package/dist/http/health-self-report.js +1 -1
  106. package/dist/http/owner-access.d.ts +2 -3
  107. package/dist/http/owner-access.js +3 -4
  108. package/dist/http/principal-gate.d.ts +49 -21
  109. package/dist/http/principal-gate.js +61 -11
  110. package/dist/http/request-channel.d.ts +23 -0
  111. package/dist/http/request-channel.js +9 -0
  112. package/dist/http/request-guards.js +17 -2
  113. package/dist/http/resume-ctx-read.js +20 -3
  114. package/dist/http/resume-legs.d.ts +2 -2
  115. package/dist/http/resume-legs.js +111 -41
  116. package/dist/http/route-ctx.d.ts +73 -7
  117. package/dist/http/route-ctx.js +17 -0
  118. package/dist/http/route-table.d.ts +4 -0
  119. package/dist/http/route-table.js +4 -0
  120. package/dist/http/routes/admin-config-refresh.js +3 -3
  121. package/dist/http/routes/admin-drain.d.ts +3 -0
  122. package/dist/http/routes/admin-drain.js +7 -7
  123. package/dist/http/routes/admin-memory-optout.js +3 -3
  124. package/dist/http/routes/adoption.js +3 -5
  125. package/dist/http/routes/approvals-assistant.d.ts +2 -1
  126. package/dist/http/routes/approvals-assistant.js +95 -48
  127. package/dist/http/routes/capabilities.d.ts +4 -0
  128. package/dist/http/routes/capabilities.js +30 -5
  129. package/dist/http/routes/config-catalog.js +2 -2
  130. package/dist/http/routes/devices.js +1 -1
  131. package/dist/http/routes/diagnostics.d.ts +3 -3
  132. package/dist/http/routes/diagnostics.js +19 -7
  133. package/dist/http/routes/images.js +10 -10
  134. package/dist/http/routes/memory-bundle.js +3 -3
  135. package/dist/http/routes/memory-compliance.js +3 -3
  136. package/dist/http/routes/memory-consolidation.js +2 -2
  137. package/dist/http/routes/memory-origin.js +3 -3
  138. package/dist/http/routes/memory-policy.js +2 -2
  139. package/dist/http/routes/observability.js +3 -3
  140. package/dist/http/routes/retention-ops.js +3 -3
  141. package/dist/http/routes/rules.js +5 -5
  142. package/dist/http/routes/run-permission-mode.d.ts +2 -1
  143. package/dist/http/routes/run-permission-mode.js +2 -2
  144. package/dist/http/routes/runs.d.ts +4 -9
  145. package/dist/http/routes/runs.js +73 -33
  146. package/dist/http/routes/session-sync.d.ts +18 -6
  147. package/dist/http/routes/session-sync.js +38 -36
  148. package/dist/http/routes/sessions.js +20 -9
  149. package/dist/http/routes/tasks.js +28 -37
  150. package/dist/http/routes/workflows.js +1 -1
  151. package/dist/http/server.d.ts +9 -3
  152. package/dist/http/server.js +15 -3
  153. package/dist/http/submit-ctx.d.ts +1 -1
  154. package/dist/http/submit-ctx.js +2 -0
  155. package/dist/human-facing-args.d.ts +33 -0
  156. package/dist/human-facing-args.js +33 -0
  157. package/dist/index.d.ts +1 -1
  158. package/dist/k8s-api-credentials.d.ts +29 -0
  159. package/dist/k8s-api-credentials.js +25 -0
  160. package/dist/lane-home-probe.d.ts +1 -1
  161. package/dist/lane-home-probe.js +2 -1
  162. package/dist/leader/diffup.d.ts +19 -0
  163. package/dist/leader/diffup.js +7 -0
  164. package/dist/leader/leader.d.ts +114 -10
  165. package/dist/leader/leader.js +69 -5
  166. package/dist/leader/repair-wire.js +2 -2
  167. package/dist/leader/run-entry.d.ts +2 -1
  168. package/dist/leader/run-entry.js +1 -1
  169. package/dist/leader/wire.d.ts +42 -132
  170. package/dist/leader/wire.js +81 -67
  171. package/dist/memory-scope.d.ts +20 -9
  172. package/dist/memory-scope.js +15 -3
  173. package/dist/model-entry-headers.d.ts +9 -10
  174. package/dist/model-entry-headers.js +0 -4
  175. package/dist/model-entry-refusal.d.ts +7 -4
  176. package/dist/model-entry-refusal.js +11 -5
  177. package/dist/model-plane-face.d.ts +1 -1
  178. package/dist/model-plane-face.js +3 -1
  179. package/dist/model-route-endpoint.d.ts +13 -7
  180. package/dist/model-route-endpoint.js +14 -11
  181. package/dist/model-select.d.ts +22 -0
  182. package/dist/model-select.js +9 -0
  183. package/dist/model-unreachable-notice.d.ts +11 -0
  184. package/dist/model-unreachable-notice.js +4 -4
  185. package/dist/object-store-buckets.d.ts +640 -6
  186. package/dist/object-store-buckets.js +166 -0
  187. package/dist/observability/fail-open.d.ts +20 -16
  188. package/dist/observability/fail-open.js +20 -16
  189. package/dist/observability/logger.js +3 -1
  190. package/dist/operator-roster.d.ts +68 -0
  191. package/dist/operator-roster.js +32 -0
  192. package/dist/orchestration/workflow-park-decision.d.ts +1 -1
  193. package/dist/outbound-dispatcher.d.ts +71 -8
  194. package/dist/outbound-dispatcher.js +48 -7
  195. package/dist/owner-faces.d.ts +1 -1
  196. package/dist/parked-decide.d.ts +2 -2
  197. package/dist/permission-mode-admission.d.ts +59 -26
  198. package/dist/permission-mode-admission.js +37 -10
  199. package/dist/plugins/approval-ask-store-file.d.ts +4 -0
  200. package/dist/plugins/approval-ask-store-file.js +3 -0
  201. package/dist/plugins/approval-ask-store-memory.d.ts +4 -0
  202. package/dist/plugins/approval-ask-store-memory.js +7 -1
  203. package/dist/plugins/approval-ask-store-sql.d.ts +26 -2
  204. package/dist/plugins/approval-ask-store-sql.js +12 -1
  205. package/dist/plugins/blob-backend.d.ts +83 -7
  206. package/dist/plugins/blob-backend.js +57 -6
  207. package/dist/plugins/checkpoint-index-columns.d.ts +20 -16
  208. package/dist/plugins/checkpoint-index-columns.js +3 -6
  209. package/dist/plugins/checkpoint-store-face.d.ts +119 -61
  210. package/dist/plugins/checkpoint-store-face.js +134 -55
  211. package/dist/plugins/landed-attendants.d.ts +2 -2
  212. package/dist/plugins/local-checkpoint-store.d.ts +13 -3
  213. package/dist/plugins/local-checkpoint-store.js +38 -49
  214. package/dist/plugins/one-time-migrations.d.ts +51 -21
  215. package/dist/plugins/one-time-migrations.js +67 -45
  216. package/dist/plugins/pg-pool.js +11 -9
  217. package/dist/plugins/remote-env-host.d.ts +29 -10
  218. package/dist/plugins/remote-env-host.js +99 -19
  219. package/dist/plugins/remote-env-k8s.d.ts +68 -2
  220. package/dist/plugins/remote-env-k8s.js +107 -24
  221. package/dist/plugins/replica-registration.d.ts +1 -1
  222. package/dist/plugins/replica-registration.js +1 -1
  223. package/dist/plugins/s3-presign.d.ts +23 -0
  224. package/dist/plugins/s3-presign.js +37 -3
  225. package/dist/plugins/scheduler-support.d.ts +2 -2
  226. package/dist/plugins/scheduler-support.js +0 -3
  227. package/dist/plugins/schema-indexes.js +2 -0
  228. package/dist/plugins/send-file-ledger.d.ts +2 -2
  229. package/dist/plugins/send-file-ledger.js +2 -2
  230. package/dist/plugins/send-user-file.d.ts +3 -4
  231. package/dist/plugins/send-user-file.js +56 -52
  232. package/dist/plugins/session-blob-claim-token.d.ts +15 -0
  233. package/dist/plugins/session-blob-claim-token.js +20 -0
  234. package/dist/plugins/session-blob-namespace.d.ts +46 -0
  235. package/dist/plugins/session-blob-namespace.js +116 -0
  236. package/dist/plugins/snapshot-bucket-presence.d.ts +25 -2
  237. package/dist/plugins/snapshot-bucket-presence.js +8 -2
  238. package/dist/plugins/sql-json-column.d.ts +1 -1
  239. package/dist/plugins/store-backend.d.ts +44 -2
  240. package/dist/plugins/store-backend.js +75 -53
  241. package/dist/plugins/task-attachment-store.d.ts +2 -1
  242. package/dist/plugins/task-attachment-store.js +5 -1
  243. package/dist/plugins/tidb-pool.js +19 -24
  244. package/dist/plugins/workspace-custody.d.ts +4 -3
  245. package/dist/question.d.ts +2 -1
  246. package/dist/question.js +5 -13
  247. package/dist/request-form.d.ts +6 -1
  248. package/dist/request-form.js +4 -1
  249. package/dist/rule-issue-reading.d.ts +1 -1
  250. package/dist/rules-consent.d.ts +1 -1
  251. package/dist/run-local.js +8 -9
  252. package/dist/runs.d.ts +17 -8
  253. package/dist/runs.js +15 -2
  254. package/dist/runtime-governance.d.ts +2 -2
  255. package/dist/sema-config-dir.d.ts +2 -1
  256. package/dist/session-sync.d.ts +27 -11
  257. package/dist/session-sync.js +15 -7
  258. package/dist/stores-sql/blob-index-sql.d.ts +30 -9
  259. package/dist/stores-sql/blob-index-sql.js +40 -21
  260. package/dist/stores-sql/checkpoint-store-sql.d.ts +54 -27
  261. package/dist/stores-sql/checkpoint-store-sql.js +70 -146
  262. package/dist/stores-sql/file-history-store-sql.d.ts +64 -16
  263. package/dist/stores-sql/file-history-store-sql.js +100 -28
  264. package/dist/stores-sql/retention-store-sql.js +3 -9
  265. package/dist/task-settings.d.ts +6 -5
  266. package/dist/task-settings.js +3 -3
  267. package/dist/tool-approval.d.ts +342 -518
  268. package/dist/tool-approval.js +498 -1207
  269. package/dist/trace/core-keyset-guard.d.ts +1 -1
  270. package/dist/trace/ledger-events.d.ts +3 -3
  271. package/dist/trace/project.d.ts +2 -3
  272. package/dist/trace/redact.js +3 -2
  273. package/dist/trace/self-minted-wire.d.ts +287 -157
  274. package/dist/trace/self-minted-wire.js +93 -48
  275. package/dist/web-admin-client/apply-effective.js +2 -2
  276. package/dist/web-admin-client/read-warnings.d.ts +2 -2
  277. package/dist/web-admin-client/read-warnings.js +1 -1
  278. package/dist/web-admin-client/types.d.ts +1 -1
  279. package/dist/workspace-space.d.ts +10 -4
  280. package/dist/workspace-space.js +4 -1
  281. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +1 -1
  282. package/package.json +6 -2
  283. package/scripts/embed-minimal.mjs +3 -3
  284. package/wire-changes/server@7.152.0.json +900 -0
  285. package/dist/config-d-migration.d.ts +0 -37
  286. package/dist/config-d-migration.js +0 -60
@@ -1,21 +1,21 @@
1
1
  import { readApprovalCard } from "./approval-content-kind.js";
2
- import { heldAskOf, liveHostSource, readDecider, retiredDecisionKeyFacts, POLICY_CANNOT_ALLOW_PERSON_ASK } from "./host-decision.js";
2
+ import { liveHostSource } from "./host-decision.js";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
- import { uuidv7, classifyAsk, foldArbitrationRow, policyCannotAnswer, screenAskClassMembers, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
4
+ import { uuidv7, classifyAsk, foldArbitrationRow, screenAskClassMembers, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, pathConfinableWriteToolNames } from "@sema-agent/core";
5
5
  import { precheckCardRuleText } from "./rules-consent.js";
6
- import { CARD_RULE_FORM_OF_MATCH, cardRuleFormOf } from "./permission-rule-vocab.js";
6
+ import { cardBindingOf, readCardRuleOffers } from "./card-rule-offers.js";
7
+ import { displayRuleOfferText } from "./plugins/checkpoint-store-face.js";
7
8
  import { redactDeep, redactSecrets, redactSecretsWithArms } from "./trace/redact.js";
8
9
  import { seatText } from "./trace/exit-seat.js";
10
+ import { humanFacingArgs } from "./human-facing-args.js";
9
11
  import { hasBidiControls } from "./text-bidi.js";
10
- import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerprint.js";
11
12
  import { createLogger } from "./observability/logger.js";
12
- import { errorKindOf } from "./observability/err-text.js";
13
13
  import { createHitlOpenRace, deliverHitlNotice } from "./hitl-delivery.js";
14
14
  import { recordFailOpen } from "./observability/fail-open.js";
15
15
  import { ownerOfCheckpointScope } from "./security.js";
16
16
  import { keyAdmits, ownerCellOf, recoveredSubject } from "./owner-faces.js";
17
- import { deriveAskId, deriveAskScopedBatchId, deriveBatchId, MAX_DECISION_NOTE_CHARS, noteLandedOnRow, noteRecordedReceipt, readBoundInputHash } from "./approval-ask-machine.js";
18
- import { askDemandsAPerson, storedPersonDemandRecord, currentParkArm, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, adoptStoredLiveFacts, buildApprovalRequestFrame, buildReplayFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readProbeReason, readReadRootCandidate, readStoredCardEnvelope, readRuleEvidence, readAllowRuleShadowed, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readAskClassPair, readAskReasons, readClosedWordSeat, buildApprovalCompleteFrame, readRuleOfferList, } from "./approval-card.js";
17
+ import { deriveAskId, deriveAskScopedBatchId, deriveBatchId, readBoundInputHash } from "./approval-ask-machine.js";
18
+ import { askDemandsAPerson, storedPersonDemandRecord, currentParkArm, buildApprovalCard, buildApprovalCardEnvelope, adoptStoredLiveFacts, buildApprovalRequestFrame, buildReplayFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readProbeReason, readReadRootCandidate, readStoredCardEnvelope, readRuleEvidence, readAllowRuleShadowed, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readAskClassPair, readAskReasons, readClosedWordSeat, buildApprovalCompleteFrame, storedCardDemandsAPerson } from "./approval-card.js";
19
19
  import { askRowCarriesOwner, POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth } from "./plugins/approval-ask-store-sql.js";
20
20
  import { APPROVAL_ENDING_CAUSES, SETTLE_OUTCOME, askRowEndingCause, askRowParkTrigger } from "./approval-deny-reasons.js";
21
21
  import { settleByPolicy } from "./approval-policy-settlement.js";
@@ -25,11 +25,9 @@ import { agreedGrantBinding, grantBindingOf } from "./standing-answer-grant.js";
25
25
  import { operatorAsk } from "./operator-ask.js";
26
26
  import { APPROVAL_EXEMPTED_EVENT } from "./plugins/approval-exemption-store.js";
27
27
  import { refusalBody } from "./http/refusal-body.js";
28
- import { withoutSettlementEvidence } from "./gate-settlement.js";
29
28
  import { extensionRequesterOf } from "./trace/extension-call-face.js";
30
29
  import { boundedRotation } from "./bounded-rotation.js";
31
30
  const defaultLogger = createLogger();
32
- const MAX_APPROVAL_ARGS_BYTES = 16384;
33
31
  const DEFAULT_APPROVAL_TTL_MS = 5 * 60_000;
34
32
  const DEFAULT_UNREACHED_ASK_TTL_MS = 60 * 60_000;
35
33
  const SUSPENDED_POLL_MAX_PER_TICK = 64;
@@ -42,10 +40,8 @@ export const MAX_ALLOW_SESSIONS = 4096;
42
40
  const DEFAULT_ADMIT_MAX_PER_TASK = 32;
43
41
  const DEFAULT_ADMIT_MAX_PER_OWNER = 256;
44
42
  const DEFAULT_PARK_REDEMPTION_MAX = 2000;
45
- const DEFAULT_PARK_REDEMPTION_TTL_MS = 60 * 60_000;
46
43
  const DEFAULT_PARK_REDEMPTION_PER_PRINCIPAL_MAX = 128;
47
44
  const PARK_REDEMPTION_ADMISSION_RETRY_AFTER_SEC = 5;
48
- const PARKING_RETRY_AFTER_MS = 250;
49
45
  export function approvalDecidePath(sessionId) {
50
46
  return `/v1/approvals/${encodeURIComponent(sessionId)}/decide`;
51
47
  }
@@ -69,11 +65,12 @@ function durableRuleMaterial(material) {
69
65
  return { ruleCommand: material.command, ruleScopeRoot: material.scopeRoot ?? null };
70
66
  }
71
67
  export const NOT_PENDING_UNKNOWN_CAUSE = "unknown_or_other_replica";
68
+ export const ASK_NOT_FOUND_UNDER_TASK = "no such approval ask under this task (never existed, belongs to another task, or is not yours)";
72
69
  export const TOOL_APPROVAL_READ_STATES = ["pending", "decided", "parked", "void"];
73
70
  export function decidedRowFacts(row) {
74
71
  if (row.state !== "DECIDED" || (row.decision !== "approve" && row.decision !== "deny") || row.provisional || row.decidedAtMs === null)
75
72
  return undefined;
76
- return { decision: row.decision === "approve" ? "allow" : "deny", decidedAtMs: row.decidedAtMs, ...(row.decisionNote !== null ? { decisionNote: row.decisionNote } : {}) };
73
+ return { decision: row.decision, decidedAtMs: row.decidedAtMs, ...(row.decisionNote !== null ? { decisionNote: row.decisionNote } : {}) };
77
74
  }
78
75
  function buildAskRowReadProjection(row, nowMs) {
79
76
  const frame = buildReplayFrame(row, nowMs);
@@ -89,7 +86,7 @@ export const ASK_DECISION_CONSUMER_ABSENT = "absent";
89
86
  function standingParkedDecision(gate, row, idempotencyKey) {
90
87
  if (gate?.kind !== "decided")
91
88
  return undefined;
92
- const decision = gate.decision === "allow" ? "approve" : "deny";
89
+ const decision = gate.decision;
93
90
  if (row.decision === decision && row.decidedAtMs !== null && row.decidedAtMs >= gate.decidedAtMs) {
94
91
  return {
95
92
  kind: "standing",
@@ -264,23 +261,11 @@ function mandatedFromSeat(seat) {
264
261
  return false;
265
262
  }
266
263
  }
267
- function askRowVisibleTo(row, principal) {
268
- return row !== null && askOwnerAdmitsAnswer(row.owner, principal);
269
- }
270
- function askOwnerAdmitsAnswer(owner, principal) {
271
- return keyAdmits("ask.answer", { subject: recoveredSubject(principal), operator: false }, ownerCellOf(owner));
272
- }
273
- function receiptAnswersNow(redeemedGeneration, openGenerationNow) {
274
- return redeemedGeneration === undefined || openGenerationNow === undefined || openGenerationNow <= redeemedGeneration;
275
- }
276
- function waitingLegsReached(entry, othersReached) {
277
- return (entry.adopted === true ? 0 : 1) + othersReached;
278
- }
279
- function withConsumerDisclosure(result, waitingLegs) {
280
- return waitingLegs > 0 ? result : { status: result.status, body: { ...result.body, consumerState: ASK_DECISION_CONSUMER_ABSENT } };
264
+ function askRowVisibleUnder(row, taskId, principal) {
265
+ return row !== null && row.taskId === taskId && keyAdmits("ask.answer", { subject: recoveredSubject(principal), operator: false }, ownerCellOf(row.owner));
281
266
  }
282
267
  function livePendingRowOf(e) {
283
- const listFrame = e.adopted === true ? e.cardFrame : (e.cardFrame ?? e.frame);
268
+ const listFrame = e.cardFrame;
284
269
  const approvalCard = e.adopted === true ? e.cardFrame.card.approvalCard : e.frame.approvalCard;
285
270
  return {
286
271
  approvalId: e.approvalId,
@@ -294,8 +279,8 @@ function livePendingRowOf(e) {
294
279
  ...(liveSessionAnswer(pendingEntryStandingAnswerFacts(e), e).home === "none" ? { rememberRefused: true } : {}),
295
280
  ...(listFrame.mandate !== undefined ? { mandate: listFrame.mandate } : {}),
296
281
  ...(listFrame.origin !== undefined ? { origin: listFrame.origin } : {}),
297
- ...(listFrame.type === "approval_request" && listFrame.liveFactsUnknown === true ? { liveFactsUnknown: true } : {}),
298
- ...(e.originTaskId !== undefined ? { fromSubagent: true, originTaskId: e.originTaskId } : {}),
282
+ ...(listFrame.liveFactsUnknown === true ? { liveFactsUnknown: true } : {}),
283
+ ...(e.sourceSessionId !== undefined ? { fromSubagent: true, sourceSessionId: e.sourceSessionId } : {}),
299
284
  ...(approvalCard !== undefined ? { approvalCard } : {}),
300
285
  frame: listFrame,
301
286
  ...(e.adopted === true ? { consumerState: ASK_DECISION_CONSUMER_ABSENT } : {}),
@@ -303,98 +288,164 @@ function livePendingRowOf(e) {
303
288
  }
304
289
  const _legFamiliesAreFrameFamilies = true;
305
290
  void _legFamiliesAreFrameFamilies;
306
- function canPresentWithoutDurableRow(c) {
307
- return c.family === "tool_approval";
308
- }
309
291
  export const PERSIST_RULE_TEXT_ERROR = `persistRule.rule must be a non-empty string of at most ${MAX_RULE_TEXT_CHARS} characters`;
310
292
  export const PERSIST_RULE_EDITED_FLAG_ERROR = "persistRule.edited must be a boolean when present";
311
293
  export const PERSIST_RULE_BATCH_INDEX_ERROR = "persistRule.batchOfferIndex must be a non-negative integer when present";
312
294
  export const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOfferIndex is mutually exclusive with persistRule.rule / persistRule.edited — send exactly one arm";
313
- export const READ_ROOT_GRANT_ALLOW_ONLY = 'readRootGrant is only meaningful with decision "allow" — it is the card\'s "Yes, and add directory" answer (allow this call and add the card\'s readRootCandidate.dir to this run\'s read directories)';
314
- export const READ_ROOT_GRANT_NOT_ACCEPTED = 'readRootGrant is not accepted on this deployment — a read-directory grant is honored only on a single-user host-lane deployment (GET /v1/capabilities approvalReadRootGrant is false here); answer with plain decision "allow" instead';
295
+ export const READ_ROOT_GRANT_ALLOW_ONLY = 'readRootGrant is only meaningful with a plain allow-this-call decision (not a deny, not a session allow) — it is the card\'s "Yes, and add directory" answer (allow this call and add the card\'s readRootCandidate.dir to this run\'s read directories)';
296
+ export const READ_ROOT_GRANT_NOT_ACCEPTED = 'readRootGrant is not accepted on this deployment — a read-directory grant is honored only on a single-user host-lane deployment (GET /v1/capabilities approvalReadRootGrant is false here); answer with a plain allow-this-call decision instead';
315
297
  export const READ_ROOT_GRANT_SHAPE = `readRootGrant must be the card's readRootCandidate.dir sent back byte for byte — a non-empty string of at most ${READ_ROOT_CANDIDATE_DIR_MAX} characters`;
298
+ export const READ_ROOT_GRANT_LIVE_ONLY = "readRootGrant can only be honored while the card is live (an engine leg is waiting on it); this ask is parked, so the directory could not be added — answer it again without readRootGrant";
299
+ export function readReadRootGrantSlot(raw, plainApprove) {
300
+ if (raw === undefined)
301
+ return { ok: true };
302
+ if (!plainApprove)
303
+ return { ok: false, error: READ_ROOT_GRANT_ALLOW_ONLY };
304
+ if (typeof raw !== "string" || raw === "" || raw.length > READ_ROOT_CANDIDATE_DIR_MAX)
305
+ return { ok: false, error: READ_ROOT_GRANT_SHAPE };
306
+ return { ok: true, grant: raw };
307
+ }
316
308
  export const PERSIST_RULE_SCOPE_REFUSED = "this endpoint does not accept a rule scope — the server mints it from where the approval happened (send neither `scope` nor `persistRule.scope`)";
317
- export function parseToolApprovalResponse(body) {
318
- const retired = retiredDecisionKeyFacts(body);
319
- const parsed = parseToolApprovalFields(body, retired);
320
- return parsed.ok || retired === undefined ? parsed : { ...parsed, ...retired.extra };
309
+ export function readPersistRuleSlot(raw, decision) {
310
+ if (decision === "deny" || raw === undefined)
311
+ return { ok: true };
312
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
313
+ return { ok: false, error: "persistRule must be an object" };
314
+ if (raw.scope !== undefined)
315
+ return { ok: false, error: PERSIST_RULE_SCOPE_REFUSED };
316
+ const rule = raw.rule;
317
+ const edited = raw.edited;
318
+ const batchIdx = raw.batchOfferIndex;
319
+ if (batchIdx !== undefined) {
320
+ if (rule !== undefined || edited !== undefined)
321
+ return { ok: false, error: PERSIST_RULE_BATCH_EXCLUSIVE_ERROR };
322
+ if (typeof batchIdx !== "number" || !Number.isInteger(batchIdx) || batchIdx < 0)
323
+ return { ok: false, error: PERSIST_RULE_BATCH_INDEX_ERROR };
324
+ return { ok: true, slot: { kind: "batch", batchOfferIndex: batchIdx } };
325
+ }
326
+ if (typeof rule !== "string" || rule === "" || rule.length > MAX_RULE_TEXT_CHARS)
327
+ return { ok: false, error: PERSIST_RULE_TEXT_ERROR };
328
+ if (edited !== undefined && typeof edited !== "boolean")
329
+ return { ok: false, error: PERSIST_RULE_EDITED_FLAG_ERROR };
330
+ return { ok: true, slot: { kind: "text", rule, edited: edited === true } };
321
331
  }
322
- function parseToolApprovalFields(body, retired) {
323
- if (body === null || typeof body !== "object" || Array.isArray(body))
324
- return { ok: false, error: "body must be an object" };
325
- const d = body.decision;
326
- if (d === "allow" || d === "allow_session" || d === "deny") {
327
- const u = body.updatedInput;
328
- if (body.scope !== undefined)
329
- return { ok: false, error: PERSIST_RULE_SCOPE_REFUSED };
330
- const pr = body.persistRule;
331
- let persistRule;
332
- if (d !== "deny" && pr !== undefined) {
333
- if (pr === null || typeof pr !== "object" || Array.isArray(pr))
334
- return { ok: false, error: "persistRule must be an object" };
335
- if (pr.scope !== undefined)
336
- return { ok: false, error: PERSIST_RULE_SCOPE_REFUSED };
337
- const rule = pr.rule;
338
- const edited = pr.edited;
339
- const batchIdx = pr.batchOfferIndex;
340
- if (batchIdx !== undefined) {
341
- if (rule !== undefined || edited !== undefined)
342
- return { ok: false, error: PERSIST_RULE_BATCH_EXCLUSIVE_ERROR };
343
- if (typeof batchIdx !== "number" || !Number.isInteger(batchIdx) || batchIdx < 0) {
344
- return { ok: false, error: PERSIST_RULE_BATCH_INDEX_ERROR };
345
- }
346
- persistRule = { kind: "batch", batchOfferIndex: batchIdx };
347
- }
348
- else {
349
- if (typeof rule !== "string" || rule === "" || rule.length > MAX_RULE_TEXT_CHARS) {
350
- return { ok: false, error: PERSIST_RULE_TEXT_ERROR };
351
- }
352
- if (edited !== undefined && typeof edited !== "boolean")
353
- return { ok: false, error: PERSIST_RULE_EDITED_FLAG_ERROR };
354
- persistRule = { kind: "text", rule, edited: edited === true };
355
- }
332
+ function withheldForCredentialShape(command, where) {
333
+ if (!ruleMaterialCarriesCredentialShape(command))
334
+ return undefined;
335
+ defaultLogger.warn("approval rule material withheld: the adjudicated command carries credential-shaped bytes", { where, commandChars: command.length });
336
+ return { kind: "absent", refusal: "rule_material_absent" };
337
+ }
338
+ export function readCardRuleRecordOfAskRow(row, owner) {
339
+ const envelope = readStoredCardEnvelope(row.cardJson);
340
+ if (envelope === undefined)
341
+ return { kind: "absent", refusal: "rule_material_absent" };
342
+ if (envelope.card.governanceForced === true)
343
+ return { kind: "absent", refusal: "rule_governance_forced" };
344
+ if (owner === null)
345
+ return { kind: "absent", refusal: "rule_lane_unavailable" };
346
+ if (!askRowCarriesOwner(row, owner))
347
+ return { kind: "absent", refusal: "rule_material_absent" };
348
+ const command = row.ruleCommand;
349
+ if (command === null)
350
+ return { kind: "absent", refusal: "rule_material_absent" };
351
+ return withheldForCredentialShape(command, "ask_row") ?? {
352
+ kind: "record",
353
+ record: {
354
+ owner,
355
+ toolName: envelope.card.toolName,
356
+ command,
357
+ offers: envelope.card.ruleOffers ?? [],
358
+ ...(row.toolCallId !== "" ? { toolCallId: row.toolCallId } : {}),
359
+ ...(row.boundInputHash !== null ? { boundInputHash: row.boundInputHash } : {}),
360
+ ...(row.ruleScopeRoot !== null ? { scopeRoot: row.ruleScopeRoot } : {}),
361
+ },
362
+ };
363
+ }
364
+ export function cardRuleOffersOfAskRow(row) {
365
+ return readStoredCardEnvelope(row.cardJson)?.card.ruleOffers;
366
+ }
367
+ export function readCardRuleRecordOfParkRow(pa, facts) {
368
+ if (facts.governanceForced)
369
+ return { kind: "absent", refusal: "rule_governance_forced" };
370
+ if (facts.owner === null)
371
+ return { kind: "absent", refusal: "rule_lane_unavailable" };
372
+ const offers = readCardRuleOffers(pa.ruleOffers, pa.toolName);
373
+ if (offers === undefined)
374
+ return { kind: "absent", refusal: "rule_material_absent" };
375
+ const command = cardBindingOf(pa.args, offers);
376
+ if (command === undefined)
377
+ return { kind: "absent", refusal: "rule_material_absent" };
378
+ return withheldForCredentialShape(command, "park_row") ?? {
379
+ kind: "record",
380
+ record: {
381
+ owner: facts.owner,
382
+ toolName: pa.toolName,
383
+ command,
384
+ offers,
385
+ ...(typeof pa.toolCallId === "string" && pa.toolCallId !== "" ? { toolCallId: pa.toolCallId } : {}),
386
+ ...(typeof pa.boundInputHash === "string" && pa.boundInputHash !== "" ? { boundInputHash: pa.boundInputHash } : {}),
387
+ ...(facts.scopeRoot !== null ? { scopeRoot: facts.scopeRoot } : {}),
388
+ },
389
+ };
390
+ }
391
+ export async function redeemCardRule(lane, reading, slot, ctx) {
392
+ const refuse = (ruleRefusal) => ({ rulePersisted: false, ruleRefusal });
393
+ if (reading.kind === "absent" && reading.refusal === "rule_governance_forced")
394
+ return refuse("rule_governance_forced");
395
+ if (lane === undefined)
396
+ return refuse("rule_lane_unavailable");
397
+ if (reading.kind === "absent")
398
+ return refuse(reading.refusal);
399
+ if (!ctx.eligible)
400
+ return refuse("rule_store_error");
401
+ if (ctx.inputWasEdited)
402
+ return refuse("rule_input_edited");
403
+ const r = reading.record;
404
+ const common = {
405
+ principal: r.owner,
406
+ toolName: r.toolName,
407
+ command: r.command,
408
+ ...(r.toolCallId !== undefined ? { toolCallId: r.toolCallId } : {}),
409
+ ...(r.boundInputHash !== undefined ? { boundInputHash: r.boundInputHash } : {}),
410
+ ...(r.scopeRoot !== undefined ? { scope: { kind: "project", root: r.scopeRoot } } : {}),
411
+ };
412
+ let redemption;
413
+ if (slot.kind === "batch") {
414
+ const offer = r.offers[slot.batchOfferIndex];
415
+ if (offer === undefined || offer.kind !== "batch")
416
+ return refuse("rule_not_offered");
417
+ redemption = { kind: "batch", offer };
418
+ }
419
+ else if (slot.edited) {
420
+ redemption = { kind: "edited", text: slot.rule };
421
+ }
422
+ else {
423
+ const hits = r.offers.filter((o) => o.kind === "single" && (o.rule === slot.rule || displayRuleOfferText(o.rule, false) === slot.rule));
424
+ if (hits.length !== 1)
425
+ return refuse("rule_not_offered");
426
+ redemption = { kind: "offered", ruleText: hits[0].rule };
427
+ }
428
+ try {
429
+ const persisted = await lane.persistCardRule({ ...common, redemption });
430
+ if (!persisted.ok) {
431
+ defaultLogger.warn("permission rule was not persisted after an approval", { reason: persisted.reason, arm: redemption.kind, ...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}) });
432
+ return refuse(persisted.reason);
356
433
  }
357
- const rawNote = body.note;
358
- let note;
359
- if (rawNote !== undefined) {
360
- if (typeof rawNote !== "string" || rawNote.length > MAX_DECISION_NOTE_CHARS) {
361
- return { ok: false, error: `note must be a string of at most ${MAX_DECISION_NOTE_CHARS} characters` };
362
- }
363
- note = rawNote;
364
- }
365
- if (retired !== undefined)
366
- return { ok: false, error: retired.error };
367
- const deciderReading = readDecider(body, false);
368
- let decider;
369
- switch (deciderReading.kind) {
370
- case "outside_closed_set":
371
- return { ok: false, error: deciderReading.message };
372
- case "accepted":
373
- decider = deciderReading.decider;
374
- break;
375
- default: {
376
- const unreachable = deciderReading;
377
- throw new Error(`unhandled decider reading: ${JSON.stringify(unreachable)}`);
378
- }
434
+ if (persisted.kind === "batch" && slot.kind === "batch") {
435
+ return {
436
+ rulePersisted: true,
437
+ persistedRules: persisted.rules,
438
+ ...(persisted.members !== undefined ? { persistedRuleAnchors: persisted.members.map((m) => ({ offerIndex: slot.batchOfferIndex, memberIndex: m.memberIndex, rule: m.rule })) } : {}),
439
+ };
379
440
  }
380
- const rawGrant = body.readRootGrant;
381
- if (rawGrant !== undefined && d !== "allow")
382
- return { ok: false, error: READ_ROOT_GRANT_ALLOW_ONLY };
383
- if (rawGrant !== undefined && (typeof rawGrant !== "string" || rawGrant === "" || rawGrant.length > READ_ROOT_CANDIDATE_DIR_MAX))
384
- return { ok: false, error: READ_ROOT_GRANT_SHAPE };
385
- const readRootGrant = rawGrant;
386
- return {
387
- ok: true,
388
- value: d,
389
- ...(d !== "deny" && u !== undefined ? { updatedInput: u } : {}),
390
- ...(persistRule !== undefined ? { persistRule } : {}),
391
- ...(note !== undefined ? { note } : {}),
392
- decider,
393
- ...(readRootGrant !== undefined ? { readRootGrant } : {}),
394
- };
441
+ return { rulePersisted: true, ...(slot.kind === "text" && slot.edited && persisted.kind === "single" ? { persistedRule: persisted.rule } : {}) };
442
+ }
443
+ catch (err) {
444
+ defaultLogger.warn("permission rule store call failed — the approval stands, the rule was not persisted", { where: "persistCardRule", arm: redemption.kind, err: err instanceof Error ? err.message : String(err) });
445
+ return refuse("rule_store_error");
395
446
  }
396
- return { ok: false, error: 'decision must be one of "allow" | "allow_session" | "deny"' };
397
447
  }
448
+ const heldLegEntries = new WeakMap();
398
449
  export function askDecisionRecordOf(write) {
399
450
  return {
400
451
  decision: write.decision,
@@ -411,6 +462,11 @@ export function buildUnattendedPolicyRefusal(reason) {
411
462
  return { allow: false, settledBy: POLICY_REFUSAL_DECISION.settledBy, reason };
412
463
  }
413
464
  function unattendedRefusalReason(site) {
465
+ if (site.arm === "card_protocol_absent") {
466
+ return ("UNATTENDED_APPROVAL_POLICY=deny: this worker presents no approval cards — it has no durable approval ledger (no store backend, or no park " +
467
+ "facility), so no approval card is shown to anyone, and an approval request no one can answer is refused here instead of being parked " +
468
+ `for a person (arm: ${site.arm}).`);
469
+ }
414
470
  if (site.arm === "frame_family_undeclared") {
415
471
  return (`UNATTENDED_APPROVAL_POLICY=deny: this run has no live approval channel — the client that started it did not declare the "${site.family}" ` +
416
472
  "approval frame family (settings.approvalFrameFamilies), so no approval card is shown to it, and an approval request no one can answer " +
@@ -422,37 +478,34 @@ function unattendedRefusalReason(site) {
422
478
  function isChildAsk(req) {
423
479
  return req.fromSubagent === true;
424
480
  }
425
- export function approverFacingArgs(args) {
426
- return boundArgs(args).args;
481
+ export function approverFacingArgs(args, card) {
482
+ return boundArgs(args, card).args;
427
483
  }
428
- function boundArgs(args) {
429
- const redacted = redactDeep(args);
430
- let hasBidi = false;
484
+ function boundArgs(args, card) {
485
+ const shown = humanFacingArgs(args, card);
486
+ if (shown.kind === "omitted" && shown.bytes === null)
487
+ return { omitted: true, hasBidi: false };
488
+ let hasBidi;
431
489
  try {
432
490
  hasBidi = hasBidiControls(JSON.stringify(args));
433
- const text = JSON.stringify(redacted);
434
- if (Buffer.byteLength(text, "utf8") > MAX_APPROVAL_ARGS_BYTES)
435
- return { omitted: true, hasBidi };
436
491
  }
437
492
  catch {
438
493
  return { omitted: true, hasBidi: false };
439
494
  }
440
- return { args: redacted, omitted: false, hasBidi };
495
+ return shown.kind === "whole" ? { args: shown.value, omitted: false, hasBidi } : { omitted: true, hasBidi };
441
496
  }
442
497
  export function resolveStreamApprovalGate(input) {
443
498
  if (!input.toolApprovalEnabled)
444
499
  return { active: false, reason: "no_tool_approval" };
445
- if (!input.streamApprovalEnabled)
446
- return { active: false, reason: "protocol_disabled" };
447
500
  if (!input.backend)
448
501
  return { active: false, reason: "no_backend" };
449
502
  if (input.checkpointStore === undefined)
450
503
  return { active: false, reason: "no_park_facility" };
451
504
  return { active: true, askStore: input.backend.approvalAsk(), checkpointStore: input.checkpointStore };
452
505
  }
453
- export const APPROVAL_FRAME_FAMILIES = ["tool_approval", "approval_request"];
506
+ export const APPROVAL_FRAME_FAMILIES = ["approval_request"];
454
507
  export function presentedApprovalFrameFamily(streamApprovalOn) {
455
- return streamApprovalOn ? "approval_request" : "tool_approval";
508
+ return streamApprovalOn ? "approval_request" : undefined;
456
509
  }
457
510
  const APPROVAL_FRAME_FAMILIES_FIELD = "settings.approvalFrameFamilies";
458
511
  const APPROVAL_FRAME_FAMILY_WORDS = APPROVAL_FRAME_FAMILIES.map((w) => `"${w}"`).join(" | ");
@@ -499,6 +552,8 @@ export function resolveApprovalLeg(input) {
499
552
  if (input.windowMs === 0 || tightDeadline)
500
553
  return { active, unattended: { arm: "window_zero" } };
501
554
  const family = presentedApprovalFrameFamily(active);
555
+ if (family === undefined)
556
+ return { active, unattended: { arm: "card_protocol_absent" } };
502
557
  if (!input.declaredFamilies.has(family))
503
558
  return { active, unattended: { arm: "frame_family_undeclared", family } };
504
559
  return {
@@ -540,18 +595,16 @@ function settleArgsOfDecided(outcome) {
540
595
  ...(!outcome.allow && outcome.reason !== undefined ? { denyReason: outcome.reason } : {}),
541
596
  };
542
597
  }
543
- function verdictOfDecided(args) {
598
+ function verdictOfDecided(args, readRootGrant) {
544
599
  return {
545
600
  by: "human",
546
601
  allowed: args.allowed,
547
602
  ...(args.updatedInput !== undefined ? { updatedInput: args.updatedInput } : {}),
548
603
  ...(args.source !== undefined ? { hostSource: args.source } : {}),
549
604
  ...(args.denyReason !== undefined ? { denyReason: args.denyReason } : {}),
605
+ ...(args.allowed && readRootGrant !== undefined ? { readRootGrant } : {}),
550
606
  };
551
607
  }
552
- function settleArgsOf(outcome) {
553
- return outcome === "unavailable" ? "unavailable" : settleArgsOfDecided(outcome);
554
- }
555
608
  export class ToolApprovalCoordinator {
556
609
  als = new AsyncLocalStorage();
557
610
  pending = new Map();
@@ -602,7 +655,6 @@ export class ToolApprovalCoordinator {
602
655
  ruleScopeRootFor;
603
656
  parkRedemptions = new Map();
604
657
  parkRedemptionMax;
605
- parkRedemptionTtlMs;
606
658
  parkRedemptionPerPrincipalMax;
607
659
  parkedRedeem;
608
660
  parkedGate;
@@ -614,28 +666,27 @@ export class ToolApprovalCoordinator {
614
666
  parkCommits;
615
667
  parksAwaitingCommit = new Set();
616
668
  constructor(opts) {
617
- this.ruleConsent = opts?.ruleConsent;
618
- this.ruleScopeRootFor = opts?.ruleScopeRootFor;
619
- this.governanceAskMarks = opts?.governanceAskMarks;
620
- this.ttlMs = opts?.ttlMs ?? DEFAULT_APPROVAL_TTL_MS;
621
- this.unreachedTtlMs = opts?.unreachedTtlMs ?? DEFAULT_UNREACHED_ASK_TTL_MS;
622
- this.askStore = opts?.askStore;
623
- this.windowMarginMs = opts?.windowMarginMs ?? DEFAULT_WINDOW_MARGIN_MS;
624
- this.admitMaxPerTask = opts?.admitMaxPerTask ?? DEFAULT_ADMIT_MAX_PER_TASK;
625
- this.admitMaxPerOwner = opts?.admitMaxPerOwner ?? DEFAULT_ADMIT_MAX_PER_OWNER;
626
- this.unattendedPolicy = opts?.unattendedPolicy ?? "park";
627
- this.parkedRedeem = opts?.parkedRedeem;
628
- this.parkedGate = opts?.parkedGate;
629
- this.parkRedemptionMax = opts?.parkRedemptionMax ?? DEFAULT_PARK_REDEMPTION_MAX;
630
- this.parkRedemptionTtlMs = opts?.parkRedemptionTtlMs ?? DEFAULT_PARK_REDEMPTION_TTL_MS;
631
- this.parkRedemptionPerPrincipalMax = opts?.parkRedemptionPerPrincipalMax ?? DEFAULT_PARK_REDEMPTION_PER_PRINCIPAL_MAX;
632
- this.askAudit = opts?.askAudit;
633
- this.sessionGrants = opts?.sessionGrants;
634
- if (opts?.deploymentListAsks !== undefined && opts.sessionGrants === undefined) {
669
+ this.ruleConsent = opts.ruleConsent;
670
+ this.ruleScopeRootFor = opts.ruleScopeRootFor;
671
+ this.governanceAskMarks = opts.governanceAskMarks;
672
+ this.ttlMs = opts.ttlMs ?? DEFAULT_APPROVAL_TTL_MS;
673
+ this.unreachedTtlMs = opts.unreachedTtlMs ?? DEFAULT_UNREACHED_ASK_TTL_MS;
674
+ this.askStore = opts.askStore;
675
+ this.windowMarginMs = opts.windowMarginMs ?? DEFAULT_WINDOW_MARGIN_MS;
676
+ this.admitMaxPerTask = opts.admitMaxPerTask ?? DEFAULT_ADMIT_MAX_PER_TASK;
677
+ this.admitMaxPerOwner = opts.admitMaxPerOwner ?? DEFAULT_ADMIT_MAX_PER_OWNER;
678
+ this.unattendedPolicy = opts.unattendedPolicy ?? "park";
679
+ this.parkedRedeem = opts.parkedRedeem;
680
+ this.parkedGate = opts.parkedGate;
681
+ this.parkRedemptionMax = opts.parkRedemptionMax ?? DEFAULT_PARK_REDEMPTION_MAX;
682
+ this.parkRedemptionPerPrincipalMax = opts.parkRedemptionPerPrincipalMax ?? DEFAULT_PARK_REDEMPTION_PER_PRINCIPAL_MAX;
683
+ this.askAudit = opts.askAudit;
684
+ this.sessionGrants = opts.sessionGrants;
685
+ if (opts.deploymentListAsks !== undefined && opts.sessionGrants === undefined) {
635
686
  throw new Error("ToolApprovalCoordinator: deploymentListAsks needs sessionGrants — the deployment list's standing answers live in the session exemption store");
636
687
  }
637
- this.deploymentListAsks = opts?.deploymentListAsks;
638
- this.parkCommits = opts?.parkCommits;
688
+ this.deploymentListAsks = opts.deploymentListAsks;
689
+ this.parkCommits = opts.parkCommits;
639
690
  this.parkCommits?.onParkCommitted((calls) => this.noteParkCommitted(calls));
640
691
  }
641
692
  unattendedOutcome(site) {
@@ -681,9 +732,9 @@ export class ToolApprovalCoordinator {
681
732
  return this.unattendedOutcome(site.arm === "frame_family_undeclared" ? { arm: site.arm, family: site.family, ...facts } : { arm: site.arm, ...facts });
682
733
  }
683
734
  async landUnattendedOutcome(outcome, row) {
684
- const store = this.askStore;
685
- if (outcome === "unavailable" || !store)
735
+ if (outcome === "unavailable")
686
736
  return outcome;
737
+ const store = this.askStore;
687
738
  const dispatch = (res, where) => {
688
739
  if (res.claimed) {
689
740
  this.settleSameAskIdParkRoute(row.askId, APPROVAL_ENDING_CAUSES.UNDELIVERABLE);
@@ -707,8 +758,6 @@ export class ToolApprovalCoordinator {
707
758
  return read?.kind === "decided" ? read.outcome : outcome;
708
759
  }
709
760
  admit(taskId, owner) {
710
- if (!this.askStore)
711
- return () => undefined;
712
761
  const ownerKey = JSON.stringify(["owner", owner]);
713
762
  const taskCount = this.admitByTask.get(taskId) ?? 0;
714
763
  const ownerCount = this.admitByOwner.get(ownerKey) ?? 0;
@@ -767,103 +816,6 @@ export class ToolApprovalCoordinator {
767
816
  return a === b;
768
817
  return a.length === b.length && a.every((d, i) => d.segment === b[i].segment && d.reason === b[i].reason);
769
818
  }
770
- static ruleOfferTextsOverSize(offer) {
771
- const over = (s) => s.length > MAX_RULE_TEXT_CHARS;
772
- if (offer.kind === "single")
773
- return over(offer.rule) || over(offer.command);
774
- return offer.rules.some((r) => {
775
- switch (r.kind) {
776
- case "command":
777
- return over(r.rule) || over(r.command);
778
- case "directoryRead":
779
- return over(r.rule) || over(r.directory);
780
- default:
781
- throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(r)}`);
782
- }
783
- });
784
- }
785
- static ruleOfferDerivedFactsAgree(offer, cardTool) {
786
- const derive = (rule) => {
787
- const p = parseRuleText(rule, "allow");
788
- return "rule" in p ? p.rule : undefined;
789
- };
790
- if (offer.kind === "single") {
791
- const d = derive(offer.rule);
792
- return d !== undefined && d.rule === offer.rule && d.match === offer.match && d.command === offer.command && ToolApprovalCoordinator.singleOfferBelongsOnCard(d.match, d.tool, cardTool);
793
- }
794
- return offer.rules.every((m) => {
795
- const d = derive(m.rule);
796
- if (d === undefined || d.rule !== m.rule)
797
- return false;
798
- switch (m.kind) {
799
- case "command":
800
- return d.tool === "Bash" && d.match === m.match && d.command === m.command;
801
- case "directoryRead":
802
- return d.tool === "Read" && d.match === "subpath" && d.command === m.directory;
803
- default:
804
- throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(m)}`);
805
- }
806
- });
807
- }
808
- static singleOfferBelongsOnCard(match, ruleTool, cardTool) {
809
- const form = CARD_RULE_FORM_OF_MATCH[match];
810
- return ruleTool === cardTool && form !== undefined && form === cardRuleFormOf(cardTool);
811
- }
812
- static boundRuleOfferTexts(offers, cardTool) {
813
- const over = (s) => s.length > MAX_RULE_TEXT_CHARS;
814
- const clipSegment = (s) => (over(s) ? s.slice(0, MAX_RULE_TEXT_CHARS) : s);
815
- const out = [];
816
- for (const { offer } of readRuleOfferList(offers)) {
817
- if (ToolApprovalCoordinator.ruleOfferTextsOverSize(offer))
818
- return undefined;
819
- if (!ToolApprovalCoordinator.ruleOfferDerivedFactsAgree(offer, cardTool)) {
820
- defaultLogger.warn("rule_offer_dropped_derived_facts_disagree", {
821
- kind: offer.kind,
822
- note: "an offer's kind/match/command/directory seats do not agree with what the shared validator derives from its rule text, or it is not this card's tool / card form — dropped whole so the card never shows one grant while redeeming another (core/dist skew or forged material?)",
823
- });
824
- continue;
825
- }
826
- switch (offer.kind) {
827
- case "single":
828
- out.push({ kind: "single", rule: offer.rule, match: offer.match, command: offer.command });
829
- break;
830
- case "batch": {
831
- const rules = [];
832
- for (const r of offer.rules) {
833
- switch (r.kind) {
834
- case "command":
835
- rules.push({ kind: "command", rule: r.rule, match: r.match, command: r.command, segment: clipSegment(r.segment) });
836
- break;
837
- case "directoryRead":
838
- rules.push({ kind: "directoryRead", rule: r.rule, directory: r.directory, segment: clipSegment(r.segment) });
839
- break;
840
- default:
841
- throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(r)}`);
842
- }
843
- }
844
- out.push({
845
- kind: "batch",
846
- rules,
847
- uncoveredSegments: offer.uncoveredSegments,
848
- ...(offer.uncoveredDetail !== undefined ? { uncoveredDetail: offer.uncoveredDetail.map((d) => ({ segment: clipSegment(d.segment), reason: d.reason })) } : {}),
849
- });
850
- break;
851
- }
852
- default:
853
- throw new Error(`unknown rule-offer kind: ${JSON.stringify(offer)}`);
854
- }
855
- }
856
- return out;
857
- }
858
- static cardBindingOf(args, offers) {
859
- const valueOffers = offers.filter((o) => o.kind === "single" && CARD_RULE_FORM_OF_MATCH[o.match] !== "command");
860
- if (valueOffers.length > 0)
861
- return valueOffers.length === 1 && offers.length === 1 ? valueOffers[0].command : undefined;
862
- if (args === null || typeof args !== "object" || Array.isArray(args))
863
- return undefined;
864
- const command = args.command;
865
- return typeof command === "string" && command !== "" ? command : undefined;
866
- }
867
819
  buildRuleLaneMaterial(req, owner, governanceForced, sessionId) {
868
820
  if (this.ruleConsent === undefined)
869
821
  return undefined;
@@ -871,14 +823,10 @@ export class ToolApprovalCoordinator {
871
823
  return undefined;
872
824
  if (owner === null)
873
825
  return undefined;
874
- const rawOffers = req.ruleOffers;
875
- if (!Array.isArray(rawOffers) || rawOffers.length === 0)
876
- return undefined;
877
- const capped = rawOffers.length > MAX_RULE_OFFERS ? rawOffers.slice(0, MAX_RULE_OFFERS) : rawOffers;
878
- const offers = ToolApprovalCoordinator.boundRuleOfferTexts(capped, req.toolName);
826
+ const offers = readCardRuleOffers(req.ruleOffers, req.toolName);
879
827
  if (offers === undefined)
880
828
  return undefined;
881
- const command = ToolApprovalCoordinator.cardBindingOf(req.args, offers);
829
+ const command = cardBindingOf(req.args, offers);
882
830
  if (command === undefined)
883
831
  return undefined;
884
832
  const boundInputHash = readBoundInputHash(req);
@@ -1045,8 +993,6 @@ export class ToolApprovalCoordinator {
1045
993
  };
1046
994
  };
1047
995
  async sessionGrantUncontestedByRow(origin, req) {
1048
- if (!this.askStore)
1049
- return true;
1050
996
  const call = { taskId: origin.taskId, legKey: origin.legKey, toolCallId: req.toolCallId, sourceSessionId: req.sourceSessionId ?? origin.taskId };
1051
997
  try {
1052
998
  const rows = await this.withStoreDeadline(this.askStore.listByCall(call), "listByCall(session-grant-provenance)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
@@ -1058,7 +1004,7 @@ export class ToolApprovalCoordinator {
1058
1004
  }
1059
1005
  }
1060
1006
  async probeRowWhileYielding(askId, budgetMs) {
1061
- if (!this.askStore || budgetMs <= 0)
1007
+ if (budgetMs <= 0)
1062
1008
  return { kind: "none" };
1063
1009
  const release = this.admitTerminalClaim(askId);
1064
1010
  if (release === undefined)
@@ -1096,8 +1042,6 @@ export class ToolApprovalCoordinator {
1096
1042
  return { kind: "decided", outcome: replayedOutcome };
1097
1043
  }
1098
1044
  hasOtherSuspendedOwner(askId, self) {
1099
- if (askId === undefined)
1100
- return false;
1101
1045
  for (const e of this.pendingByAskId.get(askId) ?? []) {
1102
1046
  if (e !== self && e.suspended === true && e.settledOutcome === undefined)
1103
1047
  return true;
@@ -1136,8 +1080,6 @@ export class ToolApprovalCoordinator {
1136
1080
  let inflight = 0;
1137
1081
  let ownInflight = 0;
1138
1082
  for (const r of this.parkRedemptions.values()) {
1139
- if (r.inflight === undefined)
1140
- continue;
1141
1083
  inflight++;
1142
1084
  if (r.owner === owner)
1143
1085
  ownInflight++;
@@ -1155,80 +1097,86 @@ export class ToolApprovalCoordinator {
1155
1097
  };
1156
1098
  }
1157
1099
  openParkRedemption(askId, owner) {
1158
- const opened = { owner, stampedAtMs: Date.now() };
1159
- this.parkRedemptions.delete(askId);
1100
+ const opened = { owner };
1160
1101
  this.parkRedemptions.set(askId, opened);
1161
1102
  return opened;
1162
1103
  }
1163
- retainParkRedemption(askId, memo) {
1164
- const now = Date.now();
1165
- const effectivePerPrincipalMax = Math.min(this.parkRedemptionPerPrincipalMax, this.parkRedemptionMax);
1166
- let receipts = 0;
1167
- let ownReceipts = 0;
1168
- let ownExpired;
1169
- let ownOldest;
1170
- let globalExpired;
1171
- for (const [k, r] of this.parkRedemptions) {
1172
- if (k === askId || r.redeemed === undefined || r.inflight !== undefined)
1173
- continue;
1174
- receipts++;
1175
- const expired = now - r.stampedAtMs > this.parkRedemptionTtlMs;
1176
- if (globalExpired === undefined && expired)
1177
- globalExpired = k;
1178
- if (r.owner !== memo.owner)
1179
- continue;
1180
- ownReceipts++;
1181
- if (ownOldest === undefined)
1182
- ownOldest = k;
1183
- if (ownExpired === undefined && expired)
1184
- ownExpired = k;
1185
- }
1186
- if (ownReceipts >= effectivePerPrincipalMax) {
1187
- const victim = ownExpired ?? ownOldest;
1188
- if (victim !== undefined)
1189
- this.parkRedemptions.delete(victim);
1190
- }
1191
- else if (receipts >= this.parkRedemptionMax) {
1192
- if (globalExpired === undefined) {
1193
- recordFailOpen("server.approval.park-redemption-cap-refused", `owner=${memo.owner ?? "<none>"}`);
1194
- if (this.parkRedemptions.get(askId) === memo)
1195
- this.parkRedemptions.delete(askId);
1196
- return false;
1197
- }
1198
- this.parkRedemptions.delete(globalExpired);
1199
- }
1200
- memo.stampedAtMs = now;
1201
- return true;
1202
- }
1203
1104
  lookupParkRedemption(askId) {
1204
- const r = this.parkRedemptions.get(askId);
1205
- if (r === undefined)
1206
- return undefined;
1207
- if (r.inflight === undefined && Date.now() - r.stampedAtMs > this.parkRedemptionTtlMs) {
1208
- this.parkRedemptions.delete(askId);
1209
- return undefined;
1210
- }
1211
- return r;
1105
+ return this.parkRedemptions.get(askId);
1212
1106
  }
1213
- notifyExternalDecision(askId, outcome) {
1107
+ notifyExternalDecision(askId, outcome, readRootGrant) {
1214
1108
  const entries = this.pendingByAskId.get(askId);
1215
1109
  if (!entries)
1216
1110
  return { settled: 0 };
1217
1111
  const args = settleArgsOfDecided(outcome);
1218
1112
  let settled = 0;
1219
1113
  for (const entry of [...entries]) {
1220
- entry.settle(verdictOfDecided(args));
1114
+ entry.settle(verdictOfDecided(args, readRootGrant));
1221
1115
  if (entry.adopted !== true)
1222
1116
  settled += 1;
1223
1117
  }
1224
1118
  return { settled };
1225
1119
  }
1120
+ holdLiveLeg(askId) {
1121
+ const hold = { askId };
1122
+ const entry = this.pendingByAskId.get(askId)?.values().next().value;
1123
+ if (entry !== undefined)
1124
+ heldLegEntries.set(hold, entry);
1125
+ return hold;
1126
+ }
1127
+ askDemandsAPerson(row, held) {
1128
+ return storedCardDemandsAPerson(row.cardJson) || (held.askId === row.askId && heldLegEntries.get(held)?.requiresRealApproval === true);
1129
+ }
1130
+ async settleLiveDecision(row, held, judgedOwner, principal, extras) {
1131
+ const leg = held.askId === row.askId ? heldLegEntries.get(held) : undefined;
1132
+ const answer = extras.remember === "session" && leg !== undefined ? liveSessionAnswer(pendingEntryStandingAnswerFacts(leg), leg) : undefined;
1133
+ let rememberApplied = extras.remember === "session" ? false : undefined;
1134
+ if (leg !== undefined && answer?.home === "seat") {
1135
+ const grantKey = sessionAllowKey(judgedOwner, answer.seat, sessionAllowCategory(leg.toolName), leg.grantBinding);
1136
+ if (this.allowAllSessions.size >= MAX_ALLOW_SESSIONS) {
1137
+ const oldest = this.allowAllSessions.values().next().value;
1138
+ if (oldest !== undefined)
1139
+ this.allowAllSessions.delete(oldest);
1140
+ }
1141
+ this.allowAllSessions.add(grantKey);
1142
+ rememberApplied = true;
1143
+ }
1144
+ else if (answer?.home === "none" && answer.occupantUnreadable) {
1145
+ defaultLogger.warn("session_grant_refused_occupant_unreadable", { sessionId: leg?.sessionId, toolName: leg?.toolName });
1146
+ }
1147
+ const { settled } = this.notifyExternalDecision(row.askId, outcomeOfDecidedRow(row), extras.readRootGrant);
1148
+ const grants = this.sessionGrants;
1149
+ if (leg !== undefined && answer?.home === "server_store" && grants !== undefined) {
1150
+ try {
1151
+ await this.withStoreDeadline(grants.grant(answer.sessionId, leg.toolName, principal ?? judgedOwner), "sessionGrants.grant(deployment-list)", DURABLE_CALL_TIMEOUT_MS);
1152
+ rememberApplied = true;
1153
+ defaultLogger.info("approval_exemption_granted", { sessionId: answer.sessionId, toolName: leg.toolName, grantedBy: principal ?? judgedOwner });
1154
+ }
1155
+ catch (err) {
1156
+ defaultLogger.warn("approval_exemption_grant_failed", { sessionId: answer.sessionId, toolName: leg.toolName, error: String(err).slice(0, 200) });
1157
+ }
1158
+ }
1159
+ const rule = extras.persistRule !== undefined && row.decision === "approve"
1160
+ ? await redeemCardRule(this.ruleConsent, readCardRuleRecordOfAskRow(row, judgedOwner), extras.persistRule, { inputWasEdited: extras.inputWasEdited, eligible: true })
1161
+ : undefined;
1162
+ return { settled, ...(rememberApplied !== undefined ? { rememberApplied } : {}), ...(rule !== undefined ? { rule } : {}) };
1163
+ }
1164
+ precheckEditedRuleOfAskRow(row, judgedOwner, persistRule, inputWasEdited) {
1165
+ if (persistRule.kind !== "text" || !persistRule.edited || inputWasEdited || this.ruleConsent === undefined)
1166
+ return undefined;
1167
+ const reading = readCardRuleRecordOfAskRow(row, judgedOwner);
1168
+ if (reading.kind !== "record")
1169
+ return undefined;
1170
+ const verdict = precheckCardRuleText(persistRule.rule, reading.record.command);
1171
+ if (verdict === undefined || verdict.ok)
1172
+ return undefined;
1173
+ return { message: verdict.message, ...(verdict.code !== undefined ? { ruleRejectCode: verdict.code } : {}) };
1174
+ }
1226
1175
  emitRevokeTo(ctxs, frame) {
1227
1176
  if (frame.askIds.length === 0)
1228
1177
  return;
1229
1178
  for (const c of ctxs)
1230
- if (c.family === "approval_request")
1231
- void deliverHitlNotice(c.transport, frame);
1179
+ void deliverHitlNotice(c.transport, frame);
1232
1180
  }
1233
1181
  emitRevoke(frame, target) {
1234
1182
  const targets = new Set();
@@ -1322,7 +1270,7 @@ export class ToolApprovalCoordinator {
1322
1270
  const aliveCtxs = ctxs.filter((c) => !c.abortSignal?.aborted);
1323
1271
  const reachability = aliveCtxs.length === 0 ? "unreached" : "reachable";
1324
1272
  if (reachability === "unreached") {
1325
- if (!this.unattendedRoutesToPark || !this.askStore) {
1273
+ if (!this.unattendedRoutesToPark) {
1326
1274
  return this.unattendedOutcome({ arm: "no_live_target", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1327
1275
  }
1328
1276
  }
@@ -1340,19 +1288,18 @@ export class ToolApprovalCoordinator {
1340
1288
  const id = uuidv7();
1341
1289
  const child = isChildAsk(req);
1342
1290
  const carrierLeg = child ? undefined : aliveCtxs.find((c) => c.taskId === origin.taskId);
1343
- const bounded = boundArgs(req.args);
1344
1291
  let effectiveGovernanceForced = governanceForced;
1345
1292
  let ruleLaneMaterial = this.buildRuleLaneMaterial(req, origin.owner, governanceForced, origin.sessionId);
1346
1293
  const mandateWord = readClosedWordSeat(req, "mandate");
1347
1294
  const noRulingCause = readClosedWordSeat(req, "classifierNoRuling");
1348
1295
  const grantAbsence = readClosedWordSeat(req, "boundGrantAbsence");
1349
1296
  const cardWord = readApprovalCard(req.approvalCard);
1297
+ const bounded = boundArgs(req.args, cardWord);
1350
1298
  const originWord = typeof req.origin === "string" && req.origin.length > 0 ? req.origin : undefined;
1351
1299
  const askClassPair = readAskClassPair(req);
1352
1300
  const askReasonsOfReq = readAskReasons(req);
1353
- const requester = extensionRequesterOf(req.requestedBy, "tool_approval");
1301
+ const requester = extensionRequesterOf(req.requestedBy, "approval_request");
1354
1302
  const frame = {
1355
- type: "tool_approval",
1356
1303
  approvalId: id,
1357
1304
  toolName: req.toolName,
1358
1305
  ...(typeof req.toolCallId === "string" && req.toolCallId !== "" ? { toolCallId: req.toolCallId } : {}),
@@ -1400,9 +1347,12 @@ export class ToolApprovalCoordinator {
1400
1347
  const windowBudgetMs = reachability === "unreached" ? this.unreachedTtlMs : this.ttlMs;
1401
1348
  const effectiveWindowMs = effectiveAskWindowMs(windowBudgetMs, this.windowMarginMs, origin.legDeadlineMonotonic, performance.now());
1402
1349
  const expiresAtMs = Math.ceil(Date.now() + effectiveWindowMs);
1403
- let askId;
1404
- let batchId;
1405
- const card = this.askStore ? buildApprovalCard(frame, req, askDemandsAPerson(req, undefined)) : undefined;
1350
+ const sourceSessionId = req.sourceSessionId ?? origin.taskId;
1351
+ const runId = origin.taskId;
1352
+ const legKey = origin.legKey;
1353
+ const askId = deriveAskId(sourceSessionId, runId, legKey, req);
1354
+ let batchId = this.batchIdFor(askId, sourceSessionId, runId, legKey);
1355
+ const card = buildApprovalCard(frame, req, askDemandsAPerson(req, undefined));
1406
1356
  let cardForFrame = card;
1407
1357
  let persistedExpiresAtMs = expiresAtMs;
1408
1358
  let factsOfRecord;
@@ -1413,11 +1363,10 @@ export class ToolApprovalCoordinator {
1413
1363
  let abandonedAs;
1414
1364
  const attemptCreatedAtMs = Date.now();
1415
1365
  const finishAbandonedRow = (orphan) => {
1416
- const store = this.askStore;
1417
- if (!store || abandonedAs === undefined)
1366
+ if (abandonedAs === undefined)
1418
1367
  return undefined;
1419
1368
  if (abandonedAs === "void") {
1420
- void store
1369
+ void this.askStore
1421
1370
  .transitionAsk(orphan.askId, "STREAM_PENDING", "VOID", { updatedAtMs: Date.now() })
1422
1371
  .catch((err) => this.noteStoreError(err, "transitionAsk(abandon-late-ensure)"));
1423
1372
  return undefined;
@@ -1434,142 +1383,131 @@ export class ToolApprovalCoordinator {
1434
1383
  ? this.unattendedOutcome({ arm: "replayed_parked", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability })
1435
1384
  : replayed;
1436
1385
  };
1437
- if (this.askStore && card) {
1438
- const sourceSessionId = req.sourceSessionId ?? origin.taskId;
1439
- const runId = origin.taskId;
1440
- const legKey = origin.legKey;
1441
- askId = deriveAskId(sourceSessionId, runId, legKey, req);
1442
- batchId = this.batchIdFor(askId, sourceSessionId, runId, legKey);
1443
- try {
1444
- const existingOrCreated = await this.withStoreDeadline(this.askStore.ensureAsk({
1445
- askId,
1446
- taskId: origin.taskId,
1447
- sourceSessionId,
1448
- sessionId: origin.sessionId ?? origin.taskId,
1449
- owner: origin.owner,
1450
- executionLane: origin.executionLane,
1451
- batchId,
1452
- toolCallId: req.toolCallId,
1453
- legKey,
1454
- parentToolCallId: req.delegation?.parentToolCallId ?? null,
1455
- cardJson: buildApprovalCardEnvelope(askId, card, frame),
1456
- boundInputHash: readBoundInputHash(req),
1457
- ...durableRuleMaterial(ruleLaneMaterial),
1458
- schemaVersion: 1,
1459
- expiresAtMs,
1460
- createdAtMs: attemptCreatedAtMs,
1461
- }), "ensureAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
1462
- if (late.row.state !== "STREAM_PENDING") {
1463
- lateTerminalRow = late.row;
1464
- this.dispatchClaimTruth(late.row.askId, this.classifyClaimLoss(late.row.askId, projectAskTerminalClaimTruth(late.row), "late:ensureAsk"), APPROVAL_ENDING_CAUSES.UNDELIVERABLE);
1465
- return;
1466
- }
1467
- lateEnsuredRow = late.inserted ? { askId: late.row.askId, batchId: late.row.batchId } : undefined;
1468
- if (lateEnsuredRow !== undefined)
1469
- void finishAbandonedRow(lateEnsuredRow);
1470
- });
1471
- batchId = existingOrCreated.row.batchId;
1472
- if (existingOrCreated.row.state !== "STREAM_PENDING") {
1473
- releaseAdmission();
1474
- return replayTerminalRow(existingOrCreated.row);
1475
- }
1476
- const persistedEnvelope = readStoredCardEnvelope(existingOrCreated.row.cardJson);
1477
- const maxMintableWindowMs = Math.max(effectiveWindowMs, this.unreachedTtlMs);
1478
- const sanePersistedDeadline = Number.isFinite(existingOrCreated.row.expiresAtMs) && existingOrCreated.row.expiresAtMs <= Date.now() + maxMintableWindowMs + DURABLE_CALL_TIMEOUT_MS;
1479
- if (persistedEnvelope === undefined || !sanePersistedDeadline) {
1480
- this.noteStoreError(new Error(`persisted approval row is unusable (envelopeOk=${persistedEnvelope !== undefined}, deadlineOk=${sanePersistedDeadline}, ` +
1481
- `origin=${existingOrCreated.inserted ? "row-this-call-just-inserted" : "row-hit-idempotently"})`), "ensureAsk(persisted-row-unusable)");
1482
- releaseAdmission();
1483
- const unattended = this.unattendedOutcome({ arm: "persisted_row_unusable", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1484
- return existingOrCreated.inserted ? this.landUnattendedOutcome(unattended, existingOrCreated.row) : unattended;
1485
- }
1486
- persistedExpiresAtMs = existingOrCreated.row.expiresAtMs;
1487
- entryExecutionLane = existingOrCreated.row.executionLane;
1488
- cardForFrame = persistedEnvelope.card;
1489
- recordCard = persistedEnvelope.card;
1490
- factsOfRecord = existingOrCreated.inserted ? undefined : persistedEnvelope.liveFacts;
1491
- if (factsOfRecord !== undefined)
1492
- adoptStoredLiveFacts(frame, factsOfRecord);
1493
- if (cardForFrame.governanceForced === true)
1494
- frame.governanceForced = true;
1495
- else
1496
- delete frame.governanceForced;
1497
- if (cardForFrame.allowRuleShadowed !== undefined)
1498
- frame.allowRuleShadowed = cardForFrame.allowRuleShadowed;
1499
- else
1500
- delete frame.allowRuleShadowed;
1501
- if (cardForFrame.inputHasBidi === true)
1502
- frame.inputHasBidi = true;
1503
- else
1504
- delete frame.inputHasBidi;
1505
- if (cardForFrame.denialLimitFallback !== undefined)
1506
- frame.denialLimitFallback = cardForFrame.denialLimitFallback;
1507
- else
1508
- delete frame.denialLimitFallback;
1509
- if (cardForFrame.governanceForced === true && !effectiveGovernanceForced) {
1510
- effectiveGovernanceForced = true;
1511
- ruleLaneMaterial = undefined;
1512
- delete frame.ruleOffers;
1513
- if (cardForFrame.ruleOffers !== undefined) {
1514
- const { ruleOffers: _dropped, ...rest } = cardForFrame;
1515
- cardForFrame = rest;
1516
- }
1386
+ try {
1387
+ const existingOrCreated = await this.withStoreDeadline(this.askStore.ensureAsk({
1388
+ askId,
1389
+ taskId: origin.taskId,
1390
+ sourceSessionId,
1391
+ sessionId: origin.sessionId ?? origin.taskId,
1392
+ owner: origin.owner,
1393
+ executionLane: origin.executionLane,
1394
+ batchId,
1395
+ toolCallId: req.toolCallId,
1396
+ legKey,
1397
+ parentToolCallId: req.delegation?.parentToolCallId ?? null,
1398
+ cardJson: buildApprovalCardEnvelope(askId, card, frame),
1399
+ boundInputHash: readBoundInputHash(req),
1400
+ ...durableRuleMaterial(ruleLaneMaterial),
1401
+ schemaVersion: 1,
1402
+ expiresAtMs,
1403
+ createdAtMs: attemptCreatedAtMs,
1404
+ }), "ensureAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
1405
+ if (late.row.state !== "STREAM_PENDING") {
1406
+ lateTerminalRow = late.row;
1407
+ this.dispatchClaimTruth(late.row.askId, this.classifyClaimLoss(late.row.askId, projectAskTerminalClaimTruth(late.row), "late:ensureAsk"), APPROVAL_ENDING_CAUSES.UNDELIVERABLE);
1408
+ return;
1517
1409
  }
1518
- if (ruleLaneMaterial !== undefined && !ToolApprovalCoordinator.ruleOffersEqual(cardForFrame.ruleOffers, ruleLaneMaterial.offers)) {
1519
- this.noteStoreError(new Error("persisted approval row offers a different ruleOffers set than this leg — withdrawing the rule lane for this delivery"), "ensureAsk(rule-offers-drift)");
1520
- ruleLaneMaterial = undefined;
1521
- delete frame.ruleOffers;
1522
- if (cardForFrame.ruleOffers !== undefined) {
1523
- const { ruleOffers: _drifted, ...rest } = cardForFrame;
1524
- cardForFrame = rest;
1525
- }
1410
+ lateEnsuredRow = late.inserted ? { askId: late.row.askId, batchId: late.row.batchId } : undefined;
1411
+ if (lateEnsuredRow !== undefined)
1412
+ void finishAbandonedRow(lateEnsuredRow);
1413
+ });
1414
+ batchId = existingOrCreated.row.batchId;
1415
+ if (existingOrCreated.row.state !== "STREAM_PENDING") {
1416
+ releaseAdmission();
1417
+ return replayTerminalRow(existingOrCreated.row);
1418
+ }
1419
+ const persistedEnvelope = readStoredCardEnvelope(existingOrCreated.row.cardJson);
1420
+ const maxMintableWindowMs = Math.max(effectiveWindowMs, this.unreachedTtlMs);
1421
+ const sanePersistedDeadline = Number.isFinite(existingOrCreated.row.expiresAtMs) && existingOrCreated.row.expiresAtMs <= Date.now() + maxMintableWindowMs + DURABLE_CALL_TIMEOUT_MS;
1422
+ if (persistedEnvelope === undefined || !sanePersistedDeadline) {
1423
+ this.noteStoreError(new Error(`persisted approval row is unusable (envelopeOk=${persistedEnvelope !== undefined}, deadlineOk=${sanePersistedDeadline}, ` +
1424
+ `origin=${existingOrCreated.inserted ? "row-this-call-just-inserted" : "row-hit-idempotently"})`), "ensureAsk(persisted-row-unusable)");
1425
+ releaseAdmission();
1426
+ const unattended = this.unattendedOutcome({ arm: "persisted_row_unusable", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1427
+ return existingOrCreated.inserted ? this.landUnattendedOutcome(unattended, existingOrCreated.row) : unattended;
1428
+ }
1429
+ persistedExpiresAtMs = existingOrCreated.row.expiresAtMs;
1430
+ entryExecutionLane = existingOrCreated.row.executionLane;
1431
+ cardForFrame = persistedEnvelope.card;
1432
+ recordCard = persistedEnvelope.card;
1433
+ factsOfRecord = existingOrCreated.inserted ? undefined : persistedEnvelope.liveFacts;
1434
+ if (factsOfRecord !== undefined)
1435
+ adoptStoredLiveFacts(frame, factsOfRecord);
1436
+ if (cardForFrame.governanceForced === true)
1437
+ frame.governanceForced = true;
1438
+ else
1439
+ delete frame.governanceForced;
1440
+ if (cardForFrame.allowRuleShadowed !== undefined)
1441
+ frame.allowRuleShadowed = cardForFrame.allowRuleShadowed;
1442
+ else
1443
+ delete frame.allowRuleShadowed;
1444
+ if (cardForFrame.inputHasBidi === true)
1445
+ frame.inputHasBidi = true;
1446
+ else
1447
+ delete frame.inputHasBidi;
1448
+ if (cardForFrame.denialLimitFallback !== undefined)
1449
+ frame.denialLimitFallback = cardForFrame.denialLimitFallback;
1450
+ else
1451
+ delete frame.denialLimitFallback;
1452
+ if (cardForFrame.governanceForced === true && !effectiveGovernanceForced) {
1453
+ effectiveGovernanceForced = true;
1454
+ ruleLaneMaterial = undefined;
1455
+ delete frame.ruleOffers;
1456
+ if (cardForFrame.ruleOffers !== undefined) {
1457
+ const { ruleOffers: _dropped, ...rest } = cardForFrame;
1458
+ cardForFrame = rest;
1526
1459
  }
1527
1460
  }
1528
- catch (err) {
1529
- this.noteStoreError(err, "ensureAsk");
1530
- let confirmedRowExists = false;
1531
- let indeterminate = true;
1532
- for (let attempt = 0; attempt <= DURABLE_CONVERGE_BACKOFF_MS.length; attempt++) {
1533
- try {
1534
- const confirmedRow = await this.withStoreDeadline(this.askStore.getAsk(askId), "getAsk(ensureAsk-confirm)");
1535
- confirmedRowExists = confirmedRow !== null;
1536
- if (confirmedRow !== null)
1537
- batchId = confirmedRow.batchId;
1538
- if (confirmedRow !== null)
1539
- recordCard = storedPersonDemandRecord(confirmedRow.cardJson);
1540
- indeterminate = false;
1541
- break;
1542
- }
1543
- catch (readErr) {
1544
- this.noteStoreError(readErr, "getAsk(ensureAsk-confirm)");
1545
- if (attempt < DURABLE_CONVERGE_BACKOFF_MS.length)
1546
- await new Promise((r) => setTimeout(r, DURABLE_CONVERGE_BACKOFF_MS[attempt]).unref?.());
1547
- }
1548
- }
1549
- if (lateTerminalRow !== undefined) {
1550
- releaseAdmission();
1551
- return replayTerminalRow(lateTerminalRow);
1461
+ if (ruleLaneMaterial !== undefined && !ToolApprovalCoordinator.ruleOffersEqual(cardForFrame.ruleOffers, ruleLaneMaterial.offers)) {
1462
+ this.noteStoreError(new Error("persisted approval row offers a different ruleOffers set than this leg — withdrawing the rule lane for this delivery"), "ensureAsk(rule-offers-drift)");
1463
+ ruleLaneMaterial = undefined;
1464
+ delete frame.ruleOffers;
1465
+ if (cardForFrame.ruleOffers !== undefined) {
1466
+ const { ruleOffers: _drifted, ...rest } = cardForFrame;
1467
+ cardForFrame = rest;
1552
1468
  }
1553
- if (indeterminate) {
1554
- const unattended = this.unattendedOutcome({ arm: "ensure_indeterminate", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1555
- const landed = abandonRow(unattended);
1556
- releaseAdmission();
1557
- return landed ?? unattended;
1469
+ }
1470
+ }
1471
+ catch (err) {
1472
+ this.noteStoreError(err, "ensureAsk");
1473
+ let confirmedRowExists = false;
1474
+ let indeterminate = true;
1475
+ for (let attempt = 0; attempt <= DURABLE_CONVERGE_BACKOFF_MS.length; attempt++) {
1476
+ try {
1477
+ const confirmedRow = await this.withStoreDeadline(this.askStore.getAsk(askId), "getAsk(ensureAsk-confirm)");
1478
+ confirmedRowExists = confirmedRow !== null;
1479
+ if (confirmedRow !== null)
1480
+ batchId = confirmedRow.batchId;
1481
+ if (confirmedRow !== null)
1482
+ recordCard = storedPersonDemandRecord(confirmedRow.cardJson);
1483
+ indeterminate = false;
1484
+ break;
1558
1485
  }
1559
- if (!confirmedRowExists) {
1560
- askId = undefined;
1561
- batchId = undefined;
1562
- if (!aliveCtxs.some(canPresentWithoutDurableRow)) {
1563
- const unattended = this.unattendedOutcome({ arm: "no_row_no_presenter", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1564
- const landed = abandonRow(unattended);
1565
- releaseAdmission();
1566
- return landed ?? unattended;
1567
- }
1568
- void abandonRow(undefined);
1486
+ catch (readErr) {
1487
+ this.noteStoreError(readErr, "getAsk(ensureAsk-confirm)");
1488
+ if (attempt < DURABLE_CONVERGE_BACKOFF_MS.length)
1489
+ await new Promise((r) => setTimeout(r, DURABLE_CONVERGE_BACKOFF_MS[attempt]).unref?.());
1569
1490
  }
1570
1491
  }
1492
+ if (lateTerminalRow !== undefined) {
1493
+ releaseAdmission();
1494
+ return replayTerminalRow(lateTerminalRow);
1495
+ }
1496
+ if (indeterminate) {
1497
+ const unattended = this.unattendedOutcome({ arm: "ensure_indeterminate", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1498
+ const landed = abandonRow(unattended);
1499
+ releaseAdmission();
1500
+ return landed ?? unattended;
1501
+ }
1502
+ if (!confirmedRowExists) {
1503
+ const unattended = this.unattendedOutcome({ arm: "no_row_no_presenter", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1504
+ const landed = abandonRow(unattended);
1505
+ releaseAdmission();
1506
+ return landed ?? unattended;
1507
+ }
1571
1508
  }
1572
- const wireApprovalId = askId ?? id;
1509
+ const wireApprovalId = askId;
1510
+ const rowCard = cardForFrame;
1573
1511
  frame.approvalId = wireApprovalId;
1574
1512
  let done = false;
1575
1513
  let emitFailed = false;
@@ -1629,27 +1567,23 @@ export class ToolApprovalCoordinator {
1629
1567
  for (const [sig, fn] of abortListeners)
1630
1568
  sig.removeEventListener("abort", fn);
1631
1569
  this.pending.delete(id);
1632
- if (windowRouteUnavailable && askId !== undefined && batchId !== undefined && this.unattendedRoutesToPark) {
1570
+ if (windowRouteUnavailable && this.unattendedRoutesToPark) {
1633
1571
  parkRouted = true;
1634
1572
  }
1635
1573
  let lastLiveRegistration = false;
1636
- if (askId !== undefined) {
1637
- const siblings = this.pendingByAskId.get(askId);
1638
- if (siblings && selfEntry) {
1639
- siblings.delete(selfEntry);
1640
- if (siblings.size === 0) {
1641
- this.pendingByAskId.delete(askId);
1642
- lastLiveRegistration = true;
1643
- }
1574
+ const siblings = this.pendingByAskId.get(askId);
1575
+ if (siblings && selfEntry) {
1576
+ siblings.delete(selfEntry);
1577
+ if (siblings.size === 0) {
1578
+ this.pendingByAskId.delete(askId);
1579
+ lastLiveRegistration = true;
1644
1580
  }
1645
1581
  }
1646
1582
  lastOutcome = outcome;
1647
1583
  lastCause = verdict.by === "system" ? verdict.cause : undefined;
1648
- if (askId !== undefined) {
1649
- completeFrame = buildApprovalCompleteFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, outcome, ...(lastCause !== undefined ? { cause: lastCause } : {}), parked: parkRouted, nowMs: Date.now() });
1650
- if (lastLiveRegistration)
1651
- this.notifyPresentedCardWatchers(askId, completeFrame);
1652
- }
1584
+ completeFrame = buildApprovalCompleteFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, outcome, ...(lastCause !== undefined ? { cause: lastCause } : {}), parked: parkRouted, nowMs: Date.now() });
1585
+ if (lastLiveRegistration)
1586
+ this.notifyPresentedCardWatchers(askId, completeFrame);
1653
1587
  releaseAdmission();
1654
1588
  if (selfEntry)
1655
1589
  selfEntry.settledOutcome = outcome;
@@ -1681,8 +1615,6 @@ export class ToolApprovalCoordinator {
1681
1615
  resolveAllowed({ allowed, ...(allowed && updatedInput !== undefined ? { updatedInput } : {}), ...(allowed && readRootGrant !== undefined ? { readRootGrant } : {}) });
1682
1616
  };
1683
1617
  const broadcastClaimTruth = (read, armCause) => {
1684
- if (askId === undefined)
1685
- return;
1686
1618
  this.dispatchClaimTruth(askId, read, armCause);
1687
1619
  };
1688
1620
  const settleFromClaimLoss = (read, armCause) => {
@@ -1711,8 +1643,6 @@ export class ToolApprovalCoordinator {
1711
1643
  }
1712
1644
  };
1713
1645
  const lateClaimLossSettle = (current, intent, armCause) => {
1714
- if (askId === undefined)
1715
- return;
1716
1646
  const read = this.classifyClaimLoss(askId, current, `late:${intent}`);
1717
1647
  if (done) {
1718
1648
  broadcastClaimTruth(read, armCause);
@@ -1729,10 +1659,6 @@ export class ToolApprovalCoordinator {
1729
1659
  return this.withStoreDeadline(raw, label, timeoutMs, late);
1730
1660
  };
1731
1661
  const resolveLocalOutcome = async (arm) => {
1732
- if (askId === undefined) {
1733
- arm.settleByIntent();
1734
- return;
1735
- }
1736
1662
  const pinnedAskId = askId;
1737
1663
  let lastLoss;
1738
1664
  const ladderStartedAt = Date.now();
@@ -1828,10 +1754,6 @@ export class ToolApprovalCoordinator {
1828
1754
  };
1829
1755
  const windowExpired = (hostSelfReport, cause) => {
1830
1756
  void (async () => {
1831
- if (!this.askStore || askId === undefined || batchId === undefined) {
1832
- settleParkRouteIfUnsettled(hostSelfReport, cause);
1833
- return;
1834
- }
1835
1757
  const store = this.askStore;
1836
1758
  const pinnedAskId = askId;
1837
1759
  const pinnedBatchId = batchId;
@@ -1862,19 +1784,11 @@ export class ToolApprovalCoordinator {
1862
1784
  const forceParkNow = (cause) => {
1863
1785
  if (done)
1864
1786
  return;
1865
- if (!this.askStore || askId === undefined || batchId === undefined) {
1866
- settleParkRouteIfUnsettled(undefined, cause);
1867
- return;
1868
- }
1869
1787
  windowExpired(undefined, cause);
1870
1788
  };
1871
- timer = setTimeout(() => windowExpired("timeout", APPROVAL_ENDING_CAUSES.TTL_EXPIRED), this.askStore ? Math.max(0, persistedExpiresAtMs - Date.now()) : effectiveWindowMs);
1789
+ timer = setTimeout(() => windowExpired("timeout", APPROVAL_ENDING_CAUSES.TTL_EXPIRED), Math.max(0, persistedExpiresAtMs - Date.now()));
1872
1790
  timer.unref?.();
1873
1791
  const runCancel = async (cause) => {
1874
- if (!this.askStore || askId === undefined || batchId === undefined) {
1875
- settle({ by: "system", cause });
1876
- return;
1877
- }
1878
1792
  if (this.hasOtherSuspendedOwner(askId, selfEntry)) {
1879
1793
  settle({ by: "system", cause });
1880
1794
  return;
@@ -1936,31 +1850,27 @@ export class ToolApprovalCoordinator {
1936
1850
  const legGrantBinding = grantBindingOf(req);
1937
1851
  const entryGrantBinding = factsOfRecord === undefined ? legGrantBinding : agreedGrantBinding(legGrantBinding, grantBindingOf(factsOfRecord));
1938
1852
  const entryMandated = req.mandated === true || factsOfRecord?.mandated === true;
1939
- const entryDemandsAPerson = askDemandsAPerson(req, recordCard) || [...(askId !== undefined ? (this.pendingByAskId.get(askId) ?? []) : [])].some((s) => s.requiresRealApproval === true);
1940
- cardForFrame = projectDecisionFacts(pendingEntryStandingAnswerFacts({
1853
+ const entryDemandsAPerson = askDemandsAPerson(req, recordCard) || [...(this.pendingByAskId.get(askId) ?? [])].some((s) => s.requiresRealApproval === true);
1854
+ const presentedCard = projectDecisionFacts(pendingEntryStandingAnswerFacts({
1941
1855
  ...(effectiveGovernanceForced ? { governanceForced: true } : {}),
1942
1856
  ...(entryDemandsAPerson ? { requiresRealApproval: true } : {}),
1943
1857
  ...(entryMandated ? { mandated: true } : {}),
1944
1858
  grantBinding: entryGrantBinding,
1945
1859
  classes: entryClasses,
1946
1860
  ...(entryDeploymentListQuestion ? { deploymentListQuestion: true } : {}),
1947
- }), origin, frame, cardForFrame);
1948
- const cardFrame = askId !== undefined && cardForFrame !== undefined
1949
- ? buildApprovalRequestFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, card: cardForFrame, expiresAtMs: persistedExpiresAtMs, nowMs: Date.now(), live: frame })
1950
- : undefined;
1861
+ }), origin, frame, rowCard);
1862
+ const cardFrame = buildApprovalRequestFrame({ askId: wireApprovalId, taskId: origin.taskId, approvalId: wireApprovalId, card: presentedCard, expiresAtMs: persistedExpiresAtMs, nowMs: Date.now(), live: frame });
1951
1863
  const pendingEntry = {
1952
1864
  settle,
1953
1865
  forceParkNow,
1954
1866
  settleParkRoute: (cause) => settleParkRouteIfUnsettled(undefined, cause),
1955
- owner: origin.owner,
1956
- ownerReadSeq: this.rowReadSeq,
1957
1867
  ...(primary ? { ctxTaskId: primary.taskId, originCtx: primary } : {}),
1958
1868
  ...(reachability === "unreached" ? { suspended: true } : {}),
1959
- ...(child ? { originTaskId: req.sourceSessionId } : primary ? { targetCtxs: new Set(aliveCtxs), onTargetGone } : {}),
1869
+ ...(child ? { sourceSessionId: req.sourceSessionId } : primary ? { targetCtxs: new Set(aliveCtxs), onTargetGone } : {}),
1960
1870
  ...(origin.sessionId ? { sessionId: origin.sessionId } : {}),
1961
1871
  ...(origin.incarnation !== undefined ? { incarnation: origin.incarnation } : {}),
1962
1872
  frame,
1963
- ...(cardFrame !== undefined ? { cardFrame } : {}),
1873
+ cardFrame,
1964
1874
  approvalId: wireApprovalId,
1965
1875
  registrationId: id,
1966
1876
  toolName: req.toolName,
@@ -1970,31 +1880,29 @@ export class ToolApprovalCoordinator {
1970
1880
  registeredAtMs: Date.now(),
1971
1881
  expiresAtMs: persistedExpiresAtMs,
1972
1882
  executionLane: entryExecutionLane,
1973
- ...(ruleLaneMaterial !== undefined ? { ruleLane: ruleLaneMaterial } : {}),
1974
1883
  ...(effectiveGovernanceForced ? { governanceForced: true } : {}),
1975
1884
  ...(entryDemandsAPerson ? { requiresRealApproval: true } : {}),
1976
1885
  ...(entryMandated ? { mandated: true } : {}),
1977
- ...(askId !== undefined && batchId !== undefined ? { askId, batchId } : {}),
1886
+ askId,
1887
+ batchId,
1978
1888
  };
1979
1889
  selfEntry = pendingEntry;
1980
1890
  this.pending.set(id, pendingEntry);
1981
- if (askId !== undefined) {
1982
- this.releaseAdoptedRegistrations(askId);
1983
- let siblings = this.pendingByAskId.get(askId);
1984
- if (!siblings) {
1985
- siblings = new Set();
1986
- this.pendingByAskId.set(askId, siblings);
1987
- }
1988
- siblings.add(pendingEntry);
1989
- if (entryDemandsAPerson) {
1990
- for (const s of siblings) {
1991
- if (s.requiresRealApproval === true)
1992
- continue;
1993
- s.requiresRealApproval = true;
1994
- const card = projectDecisionFacts(pendingEntryStandingAnswerFacts(s), s, s.frame, s.cardFrame?.card);
1995
- if (s.cardFrame !== undefined && card !== undefined && card !== s.cardFrame.card)
1996
- s.cardFrame = { ...s.cardFrame, card };
1997
- }
1891
+ this.releaseAdoptedRegistrations(askId);
1892
+ let siblings = this.pendingByAskId.get(askId);
1893
+ if (!siblings) {
1894
+ siblings = new Set();
1895
+ this.pendingByAskId.set(askId, siblings);
1896
+ }
1897
+ siblings.add(pendingEntry);
1898
+ if (entryDemandsAPerson) {
1899
+ for (const s of siblings) {
1900
+ if (s.requiresRealApproval === true)
1901
+ continue;
1902
+ s.requiresRealApproval = true;
1903
+ const card = projectDecisionFacts(pendingEntryStandingAnswerFacts(s), s, s.frame, s.cardFrame?.card);
1904
+ if (s.cardFrame !== undefined && card !== undefined && card !== s.cardFrame.card)
1905
+ s.cardFrame = { ...s.cardFrame, card };
1998
1906
  }
1999
1907
  }
2000
1908
  this.askAudit?.recordAsk({
@@ -2017,73 +1925,49 @@ export class ToolApprovalCoordinator {
2017
1925
  frame.serverNowMs = serverNowMs;
2018
1926
  frame.expiresInMs = Math.max(0, persistedExpiresAtMs - serverNowMs);
2019
1927
  }
2020
- const cardCtxs = aliveCtxs.filter((c) => c.family === "approval_request");
2021
- const legacyCtxs = aliveCtxs.filter((c) => c.family === "tool_approval");
2022
- if (cardFrame === undefined && cardCtxs.length > 0) {
2023
- defaultLogger.warn("approval card leg cannot present this ask: it has no confirmed durable identity (no approval_request frame is minted without one)", {
2024
- taskId: origin.taskId,
2025
- approvalId: wireApprovalId,
2026
- toolName: req.toolName,
2027
- });
2028
- }
2029
- const cardRace = cardFrame !== undefined && cardCtxs.length > 0
2030
- ? createHitlOpenRace({ targets: cardCtxs.map((c) => ({ transport: c.transport, frame: cardFrame })), settled: allowedP })
2031
- : undefined;
2032
- const legacyRace = legacyCtxs.length > 0 ? createHitlOpenRace({ targets: legacyCtxs.map((c) => ({ transport: c.transport, frame })), settled: allowedP }) : undefined;
2033
- if (cardRace !== undefined && cardFrame !== undefined)
1928
+ const cardCtxs = aliveCtxs;
1929
+ const cardRace = cardCtxs.length > 0 ? createHitlOpenRace({ targets: cardCtxs.map((c) => ({ transport: c.transport, frame: cardFrame })), settled: allowedP }) : undefined;
1930
+ if (cardRace !== undefined)
2034
1931
  cardCtxs.forEach((c, i) => this.notePresentedCardRegistration(cardFrame.askId, c.transport, cardRace.acceptedLive(i)));
2035
1932
  const emitAllP = (async () => {
2036
- const landings = await Promise.all([cardRace?.landing, legacyRace?.landing]);
2037
- const anySucceeded = reachability === "unreached" || this.hasOtherSuspendedOwner(askId, selfEntry) || landings.includes("delivered");
1933
+ const landing = await cardRace?.landing;
1934
+ const anySucceeded = reachability === "unreached" || this.hasOtherSuspendedOwner(askId, selfEntry) || landing === "delivered";
2038
1935
  emitSettled = true;
2039
1936
  if (!anySucceeded && !done) {
2040
- if (this.askStore && askId !== undefined && batchId !== undefined) {
2041
- const store = this.askStore;
2042
- const pinnedAskId = askId;
2043
- const pinnedBatchId = batchId;
2044
- const settleUnreachable = () => {
2045
- if (done)
2046
- return;
2047
- emitFailed = true;
2048
- settle({ by: "system", cause: APPROVAL_ENDING_CAUSES.BROADCAST_LOST });
2049
- };
2050
- await resolveLocalOutcome({
2051
- intent: "unreachable",
2052
- cause: APPROVAL_ENDING_CAUSES.BROADCAST_LOST,
2053
- storeLabel: "claimTerminal(emit-failed)",
2054
- claim: (timeoutMs) => boundedClaim(() => settleByPolicy(store, { askId: pinnedAskId, batchId: pinnedBatchId }, this.unattendedPolicy, APPROVAL_ENDING_CAUSES.BROADCAST_LOST), "claimTerminal(emit-failed)", timeoutMs, (late) => {
2055
- if (!late.claimed) {
2056
- lateClaimLossSettle(late.current, "unreachable", APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
2057
- return;
2058
- }
2059
- this.settleSameAskIdParkRoute(pinnedAskId, APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
2060
- this.settleVoidedSiblings(late.voidedSiblings, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN);
2061
- }),
2062
- onWon: (res) => {
2063
- settleUnreachable();
2064
- this.settleSameAskIdParkRoute(pinnedAskId, APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
2065
- this.settleVoidedSiblings(res.voidedSiblings, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN);
2066
- },
2067
- settleByIntent: settleUnreachable,
2068
- });
2069
- }
2070
- else if (!done) {
1937
+ const store = this.askStore;
1938
+ const pinnedAskId = askId;
1939
+ const pinnedBatchId = batchId;
1940
+ const settleUnreachable = () => {
1941
+ if (done)
1942
+ return;
2071
1943
  emitFailed = true;
2072
1944
  settle({ by: "system", cause: APPROVAL_ENDING_CAUSES.BROADCAST_LOST });
2073
- }
1945
+ };
1946
+ await resolveLocalOutcome({
1947
+ intent: "unreachable",
1948
+ cause: APPROVAL_ENDING_CAUSES.BROADCAST_LOST,
1949
+ storeLabel: "claimTerminal(emit-failed)",
1950
+ claim: (timeoutMs) => boundedClaim(() => settleByPolicy(store, { askId: pinnedAskId, batchId: pinnedBatchId }, this.unattendedPolicy, APPROVAL_ENDING_CAUSES.BROADCAST_LOST), "claimTerminal(emit-failed)", timeoutMs, (late) => {
1951
+ if (!late.claimed) {
1952
+ lateClaimLossSettle(late.current, "unreachable", APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
1953
+ return;
1954
+ }
1955
+ this.settleSameAskIdParkRoute(pinnedAskId, APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
1956
+ this.settleVoidedSiblings(late.voidedSiblings, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN);
1957
+ }),
1958
+ onWon: (res) => {
1959
+ settleUnreachable();
1960
+ this.settleSameAskIdParkRoute(pinnedAskId, APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
1961
+ this.settleVoidedSiblings(res.voidedSiblings, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN);
1962
+ },
1963
+ settleByIntent: settleUnreachable,
1964
+ });
2074
1965
  }
2075
1966
  })();
2076
1967
  await Promise.race([emitAllP, allowedP]);
2077
1968
  const settled = await allowedP;
2078
1969
  if (cardRace !== undefined && completeFrame !== undefined)
2079
1970
  cardRace.close(completeFrame);
2080
- legacyRace?.close({
2081
- type: "tool_approval_complete",
2082
- approvalId: wireApprovalId,
2083
- outcome: lastOutcome,
2084
- ...(lastCause !== undefined ? { cause: lastCause } : {}),
2085
- ...(parkRouted ? { parked: true } : {}),
2086
- });
2087
1971
  if (emitFailed)
2088
1972
  return this.unattendedOutcome({ arm: "emit_failed", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
2089
1973
  if (windowRouteUnavailable)
@@ -2092,110 +1976,6 @@ export class ToolApprovalCoordinator {
2092
1976
  return this.unattendedOutcome({ arm: "emit_unsettled", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
2093
1977
  return shapeOutcome(settled);
2094
1978
  }
2095
- respond(id, principal, body, httpReq, deployment) {
2096
- const parsed = parseToolApprovalResponse(body);
2097
- if (!parsed.ok)
2098
- return { status: 400, body: refusalBody({ errorCode: "request.body_shape", message: parsed.error, facts: { ...(parsed.field !== undefined ? { field: parsed.field } : {}) } }) };
2099
- if (parsed.readRootGrant !== undefined && deployment?.readRootGrantAccepted !== true) {
2100
- return { status: 400, body: refusalBody({ errorCode: "request.body_shape", message: READ_ROOT_GRANT_NOT_ACCEPTED }) };
2101
- }
2102
- const entry = this.liveEntryFor(id);
2103
- if (entry?.adopted === true)
2104
- return this.respondByRow(entry, id, principal, parsed, httpReq);
2105
- if (entry !== undefined && askOwnerAdmitsAnswer(entry.owner, principal))
2106
- return this.answerRegistration(entry, entry.owner, id, principal, parsed, httpReq);
2107
- if (entry !== undefined && this.registrationHasRow(entry))
2108
- return this.respondByRow(entry, id, principal, parsed, httpReq);
2109
- return this.respondLate(id, principal, parsed, httpReq);
2110
- }
2111
- registrationHasRow(entry) {
2112
- return this.askStore !== undefined && entry.askId !== undefined;
2113
- }
2114
- async respondByRow(entry, id, principal, parsed, httpReq) {
2115
- const store = this.askStore;
2116
- if (!store)
2117
- return this.askNotVisible();
2118
- const row = await this.askRowFollowingOwner(store, id, "getAsk(respond-by-row)");
2119
- if (!askRowVisibleTo(row, principal))
2120
- return this.askNotVisible();
2121
- if (row.state !== "STREAM_PENDING")
2122
- return this.dispatchRowTruth(row, id, parsed, httpReq, principal);
2123
- return this.answerRegistration(entry, row.owner, id, principal, parsed, httpReq);
2124
- }
2125
- answerRegistration(entry, judgedOwner, id, principal, parsed, httpReq) {
2126
- const preRule = this.precheckEditedRuleBeforeDecision(entry, judgedOwner, parsed);
2127
- if (preRule !== undefined)
2128
- return preRule;
2129
- if (parsed.value !== "deny" && policyCannotAnswer(heldAskOf(entry.requiresRealApproval === true), parsed.decider.decidedBy)) {
2130
- return { status: 400, body: refusalBody({ errorCode: "request.body_shape", message: POLICY_CANNOT_ALLOW_PERSON_ASK }) };
2131
- }
2132
- const ruleEligibility = { eligible: true };
2133
- const store = this.askStore;
2134
- if (store && entry.askId !== undefined && entry.batchId !== undefined) {
2135
- return this.respondWithCas(store, entry.askId, entry.batchId, judgedOwner, entry, parsed, ruleEligibility).then((cas) => cas.lost ? this.dispatchRowTruth(cas.row, id, parsed, httpReq, principal) : this.afterLiveDecision(cas.result, entry, judgedOwner, parsed, principal, ruleEligibility));
2136
- }
2137
- return this.afterLiveDecision(this.finishRespond(entry, judgedOwner, parsed), entry, judgedOwner, parsed, principal, ruleEligibility);
2138
- }
2139
- afterLiveDecision(decided, entry, judgedOwner, parsed, principal, ruleEligibility) {
2140
- const settled = this.recordDeploymentListAnswer(decided, entry, judgedOwner, parsed, principal, ruleEligibility);
2141
- if (parsed.persistRule === undefined)
2142
- return settled;
2143
- return this.persistRuleAfterDecision(settled, entry, judgedOwner, parsed.persistRule, parsed.updatedInput !== undefined, ruleEligibility);
2144
- }
2145
- recordDeploymentListAnswer(decided, entry, judgedOwner, parsed, principal, eligibility) {
2146
- const store = this.sessionGrants;
2147
- const answer = liveSessionAnswer(pendingEntryStandingAnswerFacts(entry), entry);
2148
- if (parsed.value !== "allow_session" || answer.home !== "server_store" || store === undefined)
2149
- return decided;
2150
- const sessionId = answer.sessionId;
2151
- return (async () => {
2152
- const result = await decided;
2153
- if (result.status !== 200)
2154
- return result;
2155
- if (!eligibility.eligible)
2156
- return { ...result, body: { ...result.body, rememberApplied: false } };
2157
- let rememberApplied = false;
2158
- try {
2159
- await this.withStoreDeadline(store.grant(sessionId, entry.toolName, principal ?? judgedOwner), "sessionGrants.grant(deployment-list)", DURABLE_CALL_TIMEOUT_MS);
2160
- rememberApplied = true;
2161
- defaultLogger.info("approval_exemption_granted", { sessionId, toolName: entry.toolName, grantedBy: principal ?? judgedOwner });
2162
- }
2163
- catch (err) {
2164
- defaultLogger.warn("approval_exemption_grant_failed", { sessionId, toolName: entry.toolName, error: String(err).slice(0, 200) });
2165
- }
2166
- return { ...result, body: { ...result.body, rememberApplied } };
2167
- })();
2168
- }
2169
- precheckEditedRuleBeforeDecision(entry, judgedOwner, parsed) {
2170
- const persistRule = parsed.persistRule;
2171
- if (persistRule === undefined || persistRule.kind !== "text" || !persistRule.edited)
2172
- return undefined;
2173
- if (parsed.updatedInput !== undefined)
2174
- return undefined;
2175
- if (entry.governanceForced === true)
2176
- return undefined;
2177
- const command = entry.ruleLane?.command;
2178
- if (this.ruleConsent === undefined || command === undefined || judgedOwner === null)
2179
- return undefined;
2180
- const verdict = precheckCardRuleText(persistRule.rule, command);
2181
- if (verdict === undefined || verdict.ok)
2182
- return undefined;
2183
- return {
2184
- status: 400,
2185
- body: refusalBody({
2186
- errorCode: "request.body_shape",
2187
- message: verdict.message,
2188
- facts: {
2189
- ...(verdict.code !== undefined ? { ruleRejectCode: verdict.code } : {}),
2190
- },
2191
- }),
2192
- };
2193
- }
2194
- editedArmEcho(persistRule, persisted) {
2195
- if (persistRule.kind !== "text" || !persistRule.edited || !persisted.ok || persisted.kind !== "single")
2196
- return {};
2197
- return { persistedRule: persisted.rule };
2198
- }
2199
1979
  notPending(cause) {
2200
1980
  return {
2201
1981
  status: 404,
@@ -2209,34 +1989,24 @@ export class ToolApprovalCoordinator {
2209
1989
  };
2210
1990
  }
2211
1991
  askNotVisible() {
2212
- return { status: 404, body: refusalBody({ errorCode: "not_found.ask", message: "no such approval ask (never existed, no longer kept, or not yours)" }) };
1992
+ return { status: 404, body: refusalBody({ errorCode: "not_found.ask", message: ASK_NOT_FOUND_UNDER_TASK }) };
2213
1993
  }
2214
- readApproval(id, principal) {
2215
- const entry = this.liveEntryFor(id);
2216
- if (entry !== undefined && this.registrationHasRow(entry))
2217
- return this.readByRow(entry, id, principal);
2218
- if (entry !== undefined && askOwnerAdmitsAnswer(entry.owner, principal)) {
2219
- const body = { state: "pending", ...livePendingRowOf(entry) };
2220
- return { status: 200, body };
2221
- }
2222
- const store = this.askStore;
2223
- if (!store)
2224
- return this.askNotVisible();
2225
- return this.readAskRowState(store, id, principal);
1994
+ readAsk(taskId, askId, principal) {
1995
+ const entry = this.liveEntryFor(askId);
1996
+ if (entry !== undefined)
1997
+ return this.readByRow(entry, taskId, askId, principal);
1998
+ return this.readAskRowState(taskId, askId, principal);
2226
1999
  }
2227
- async readByRow(entry, id, principal) {
2228
- const store = this.askStore;
2229
- if (!store)
2230
- return this.askNotVisible();
2231
- const row = await this.askRowFollowingOwner(store, id, "getAsk(read-by-row)");
2232
- if (askRowVisibleTo(row, principal) && row.state === "STREAM_PENDING") {
2000
+ async readByRow(entry, taskId, askId, principal) {
2001
+ const row = await this.askRowFor(this.askStore, askId, "getAsk(read-by-row)");
2002
+ if (askRowVisibleUnder(row, taskId, principal) && row.state === "STREAM_PENDING") {
2233
2003
  const body = { state: "pending", ...livePendingRowOf(entry) };
2234
2004
  return { status: 200, body };
2235
2005
  }
2236
- return this.readStateOfRow(row, principal);
2006
+ return this.readStateOfRow(row, taskId, principal);
2237
2007
  }
2238
- async readAskRowState(store, id, principal) {
2239
- return this.readStateOfRow(await this.askRowFor(store, id, "getAsk(read)"), principal);
2008
+ async readAskRowState(taskId, askId, principal) {
2009
+ return this.readStateOfRow(await this.askRowFor(this.askStore, askId, "getAsk(read)"), taskId, principal);
2240
2010
  }
2241
2011
  async readParkedGate(row) {
2242
2012
  const gate = this.parkedGate;
@@ -2250,12 +2020,8 @@ export class ToolApprovalCoordinator {
2250
2020
  return undefined;
2251
2021
  }
2252
2022
  }
2253
- async openGateGeneration(row) {
2254
- const gate = await this.readParkedGate(row);
2255
- return gate?.kind === "open" ? gate.generation : undefined;
2256
- }
2257
- async readStateOfRow(row, principal) {
2258
- if (!askRowVisibleTo(row, principal))
2023
+ async readStateOfRow(row, taskId, principal) {
2024
+ if (!askRowVisibleUnder(row, taskId, principal))
2259
2025
  return this.askNotVisible();
2260
2026
  const projection = buildAskRowReadProjection(row, Date.now());
2261
2027
  let body;
@@ -2315,12 +2081,6 @@ export class ToolApprovalCoordinator {
2315
2081
  }
2316
2082
  return { status: 200, body };
2317
2083
  }
2318
- respondLate(id, principal, parsed, httpReq) {
2319
- const store = this.askStore;
2320
- if (!store)
2321
- return this.askNotVisible();
2322
- return this.dispatchLateByRow(store, id, parsed, httpReq, principal);
2323
- }
2324
2084
  watchPresentedCards(askIds, deliver, connection) {
2325
2085
  const watcher = { deliver, connection, askIds: new Set(), registrationsOnConnection: new Map() };
2326
2086
  for (const askId of askIds) {
@@ -2385,16 +2145,10 @@ export class ToolApprovalCoordinator {
2385
2145
  }
2386
2146
  }
2387
2147
  liveEntryFor(id) {
2388
- const local = this.pending.get(id);
2389
- if (local !== undefined)
2390
- return local.askId === undefined ? local : undefined;
2391
2148
  for (const e of this.pendingByAskId.get(id) ?? [])
2392
2149
  return e;
2393
2150
  return undefined;
2394
2151
  }
2395
- async dispatchLateByRow(store, id, parsed, httpReq, principal) {
2396
- return this.dispatchRowTruth(await this.askRowFor(store, id, "getAsk(respond-late)"), id, parsed, httpReq, principal);
2397
- }
2398
2152
  async askRowFor(store, id, label) {
2399
2153
  try {
2400
2154
  return await this.withStoreDeadline(store.getAsk(id), label, DURABLE_CONVERGE_READ_TIMEOUT_MS);
@@ -2404,129 +2158,6 @@ export class ToolApprovalCoordinator {
2404
2158
  return null;
2405
2159
  }
2406
2160
  }
2407
- async askRowFollowingOwner(store, id, label) {
2408
- const readSeq = this.nextRowReadSeq();
2409
- const row = await this.askRowFor(store, id, label);
2410
- if (row?.state === "STREAM_PENDING")
2411
- this.followRowOwner(row.askId, row.owner, readSeq);
2412
- return row;
2413
- }
2414
- async dispatchRowTruth(row, id, parsed, httpReq, principal) {
2415
- if (!askRowVisibleTo(row, principal))
2416
- return this.askNotVisible();
2417
- switch (row.state) {
2418
- case "DECIDED":
2419
- return this.replayDecidedAsk(id, row, parsed);
2420
- case "PARKED":
2421
- return this.redeemParkedAsk(id, row, parsed, httpReq, principal);
2422
- case "PARKING": {
2423
- const parkSession = parkSessionOf(row);
2424
- return {
2425
- status: 202,
2426
- body: {
2427
- approvalId: id,
2428
- askId: row.askId,
2429
- reason: "parking_in_progress",
2430
- message: parkSession !== undefined
2431
- ? "parking in progress — not applied; retry after retryAfterMs or answer through decidePath"
2432
- : "parking in progress — not applied; retry after retryAfterMs or answer through the parent session's durable decide",
2433
- retryAfterMs: PARKING_RETRY_AFTER_MS,
2434
- ...(parkSession !== undefined ? { decidePath: approvalDecidePath(parkSession) } : {}),
2435
- ...this.lateRuleRefusal(parsed),
2436
- },
2437
- };
2438
- }
2439
- case "DENIED":
2440
- case "VOID": {
2441
- return this.notPending(askRowEndingCause(row));
2442
- }
2443
- case "STREAM_PENDING":
2444
- return this.notPending(NOT_PENDING_UNKNOWN_CAUSE);
2445
- default: {
2446
- const unhandled = row.state;
2447
- defaultLogger.error("tool approval respond hit an unknown ask state", { askId: row.askId, state: unhandled });
2448
- return this.notPending();
2449
- }
2450
- }
2451
- }
2452
- lateRuleRefusal(parsed) {
2453
- if (parsed.persistRule === undefined)
2454
- return {};
2455
- return { rulePersisted: false, ruleRefusal: "rule_lane_unavailable" };
2456
- }
2457
- replayDecidedAsk(id, row, parsed) {
2458
- const decided = decidedRowFacts(row);
2459
- if (decided === undefined) {
2460
- defaultLogger.error("tool approval late replay saw a DECIDED row that is not a usable human decision", {
2461
- askId: row.askId,
2462
- decision: row.decision,
2463
- provisional: row.provisional,
2464
- decidedAtMs: row.decidedAtMs,
2465
- });
2466
- return this.notPending();
2467
- }
2468
- this.notifyExternalDecision(row.askId, outcomeOfDecidedRow(row));
2469
- return {
2470
- status: 200,
2471
- body: {
2472
- approvalId: id,
2473
- askId: row.askId,
2474
- idempotent: true,
2475
- ...decided,
2476
- ...noteRecordedReceipt(parsed.note, noteLandedOnRow(parsed.note, row.decisionNote)),
2477
- ...this.lateRuleRefusal(parsed),
2478
- },
2479
- };
2480
- }
2481
- async redeemParkedAsk(id, row, parsed, httpReq, principal) {
2482
- const redeem = this.parkedRedeem?.();
2483
- if (redeem === undefined)
2484
- return this.notPending();
2485
- if (row.gateToken === null) {
2486
- defaultLogger.error("tool approval parked row carries no gate token — nothing to redeem", { askId: row.askId });
2487
- return this.notPending();
2488
- }
2489
- const openGenerationNow = this.lookupParkRedemption(row.askId) !== undefined ? await this.openGateGeneration(row) : undefined;
2490
- const prior = this.lookupParkRedemption(row.askId);
2491
- if (prior?.redeemed !== undefined && receiptAnswersNow(prior.redeemed.gateGeneration, openGenerationNow))
2492
- return this.replayRedeemReceipt(id, prior, row, prior.redeemed, parsed, row);
2493
- if (prior?.inflight !== undefined) {
2494
- const shared = await prior.inflight;
2495
- if (shared.status < 200 || shared.status >= 300)
2496
- return shared;
2497
- if (!receiptAnswersNow(shared.gateGeneration, openGenerationNow))
2498
- return this.redeemParkedAsk(id, row, parsed, httpReq, principal);
2499
- return this.replayRedeemReceipt(id, prior, row, { decision: shared.decision, body: shared.body, gateGeneration: shared.gateGeneration, ...(shared.rule?.terminal === true ? { rule: shared.rule.body } : {}) }, parsed, shared.recorded ?? { decisionNote: null });
2500
- }
2501
- const store = this.askStore;
2502
- if (store === undefined)
2503
- return this.notPending();
2504
- const request = {
2505
- write: { decision: parsed.value === "deny" ? "deny" : "approve", decider: parsed.decider, ...(parsed.note !== undefined ? { note: parsed.note } : {}) },
2506
- ...(parsed.value === "allow_session" ? { remember: "session" } : {}),
2507
- ...(parsed.persistRule !== undefined ? { persistRule: parsed.persistRule } : {}),
2508
- ...(parsed.updatedInput !== undefined && parsed.value !== "deny" ? { updatedInput: parsed.updatedInput } : {}),
2509
- ...(principal !== undefined ? { principal } : {}),
2510
- operator: false,
2511
- ...(httpReq !== undefined ? { httpReq } : {}),
2512
- };
2513
- const out = await this.runParkedFlight(redeem, store, row, row.gateToken, request);
2514
- if (out.status < 200 || out.status >= 300)
2515
- return { status: out.status, body: out.body };
2516
- const rule = out.rule;
2517
- return {
2518
- status: out.status,
2519
- body: {
2520
- ...out.body,
2521
- approvalId: id,
2522
- askId: row.askId,
2523
- accepted: "parked_redeemed",
2524
- ...(parsed.value === "allow_session" ? { rememberApplied: out.rememberApplied === true } : {}),
2525
- ...noteRecordedReceipt(parsed.note, noteLandedOnRow(parsed.note, out.recorded?.decisionNote ?? null)),
2526
- ...(rule?.body ?? {}),
2527
- },
2528
- };
2529
- }
2530
2161
  async runParkedFlight(redeem, store, row, gateToken, request) {
2531
2162
  const decision = request.write.decision;
2532
2163
  const admission = this.parkRedemptionAdmission(row.owner);
@@ -2546,6 +2177,7 @@ export class ToolApprovalCoordinator {
2546
2177
  decider: request.write.decider,
2547
2178
  ...(request.updatedInput !== undefined && decision === "approve" ? { updatedInput: request.updatedInput } : {}),
2548
2179
  ...(request.remember !== undefined ? { remember: request.remember } : {}),
2180
+ ...(request.persistRule !== undefined ? { persistRule: request.persistRule } : {}),
2549
2181
  ...(request.principal !== undefined ? { principal: request.principal } : {}),
2550
2182
  operator: request.operator,
2551
2183
  ...(request.httpReq !== undefined ? { httpReq: request.httpReq } : {}),
@@ -2558,25 +2190,16 @@ export class ToolApprovalCoordinator {
2558
2190
  if (res.status < 200 || res.status >= 300)
2559
2191
  return { ...res, decision };
2560
2192
  const recorded = await this.recordParkedOutcome(store, row, request.write);
2561
- const rule = await this.persistRuleFromRow(row, request.persistRule, request.updatedInput !== undefined);
2562
- return { ...res, decision, ...(recorded !== undefined ? { recorded } : {}), ...(rule !== undefined ? { rule } : {}) };
2193
+ return { ...res, decision, ...(recorded !== undefined ? { recorded } : {}) };
2563
2194
  })();
2564
2195
  memo.inflight = flight;
2565
- let out;
2566
2196
  try {
2567
- out = await flight;
2197
+ return await flight;
2568
2198
  }
2569
2199
  finally {
2570
- memo.inflight = undefined;
2571
- }
2572
- if (out.status < 200 || out.status >= 300) {
2573
2200
  if (this.parkRedemptions.get(row.askId) === memo)
2574
2201
  this.parkRedemptions.delete(row.askId);
2575
- return out;
2576
2202
  }
2577
- if (this.retainParkRedemption(row.askId, memo))
2578
- memo.redeemed = { decision, body: out.body, gateGeneration: out.gateGeneration, ...(out.rule?.terminal === true ? { rule: out.rule.body } : {}) };
2579
- return out;
2580
2203
  }
2581
2204
  async recordParkedOutcome(store, row, write) {
2582
2205
  try {
@@ -2616,200 +2239,35 @@ export class ToolApprovalCoordinator {
2616
2239
  }
2617
2240
  const out = await this.runParkedFlight(redeem, store, current, row.gateToken, request);
2618
2241
  if (out.status === 200)
2619
- return { kind: "accepted", body: out.body, ...(out.recorded !== undefined ? { recorded: out.recorded } : {}) };
2242
+ return { kind: "accepted", body: out.body, ...(out.recorded !== undefined ? { recorded: out.recorded } : {}), ...(out.rememberApplied !== undefined ? { rememberApplied: out.rememberApplied } : {}), ...(out.rule !== undefined ? { rule: out.rule } : {}) };
2620
2243
  if ((await this.readParkedGate(row))?.kind === "decided")
2621
2244
  continue;
2622
2245
  return { kind: "refused", status: out.status, body: out.body };
2623
2246
  }
2624
2247
  }
2625
- async persistRuleFromRow(row, persistRule, inputWasEdited) {
2626
- if (persistRule === undefined)
2627
- return undefined;
2628
- const flag = (rulePersisted, ruleRefusal, echo) => ({
2629
- body: { rulePersisted, ...(ruleRefusal !== undefined ? { ruleRefusal } : {}), ...(echo ?? {}) },
2630
- terminal: true,
2631
- });
2632
- const lane = this.ruleConsent;
2633
- if (lane === undefined || row.owner === null)
2634
- return flag(false, "rule_lane_unavailable");
2635
- const owner = row.owner;
2636
- const envelope = readStoredCardEnvelope(row.cardJson);
2637
- if (envelope === undefined)
2638
- return flag(false, "rule_material_absent");
2639
- const card = envelope.card;
2640
- if (card.governanceForced === true)
2641
- return flag(false, "rule_governance_forced");
2642
- const command = row.ruleCommand;
2643
- if (command === null)
2644
- return flag(false, "rule_material_absent");
2645
- if (inputWasEdited)
2646
- return flag(false, "rule_input_edited");
2647
- const offers = card.ruleOffers ?? [];
2648
- const scope = row.ruleScopeRoot !== null ? { kind: "project", root: row.ruleScopeRoot } : undefined;
2649
- const common = {
2650
- principal: owner,
2651
- toolName: card.toolName,
2652
- command,
2653
- ...(row.toolCallId !== "" ? { toolCallId: row.toolCallId } : {}),
2654
- ...(row.boundInputHash !== null ? { boundInputHash: row.boundInputHash } : {}),
2655
- ...(scope !== undefined ? { scope } : {}),
2656
- };
2248
+ async readParkedAskOfGate(sourceSessionId, binding) {
2249
+ const store = this.askStore;
2657
2250
  try {
2658
- if (persistRule.kind === "batch") {
2659
- const offer = offers[persistRule.batchOfferIndex];
2660
- if (offer === undefined || offer.kind !== "batch")
2661
- return flag(false, "rule_not_offered");
2662
- const persisted = await lane.persistCardRule({ ...common, redemption: { kind: "batch", offer } });
2663
- if (!persisted.ok) {
2664
- defaultLogger.warn("permission rule batch was not persisted after a parked late decision", {
2665
- askId: row.askId,
2666
- reason: persisted.reason,
2667
- ...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}),
2668
- });
2669
- return flag(false, persisted.reason);
2670
- }
2671
- return flag(true, undefined, persisted.kind === "batch"
2672
- ? {
2673
- persistedRules: persisted.rules,
2674
- ...(persisted.members !== undefined
2675
- ? { persistedRuleAnchors: persisted.members.map((m) => ({ offerIndex: persistRule.batchOfferIndex, memberIndex: m.memberIndex, rule: m.rule })) }
2676
- : {}),
2677
- }
2678
- : {});
2679
- }
2680
- const ruleText = persistRule.rule;
2681
- if (!persistRule.edited && !offers.some((o) => o.kind === "single" && o.rule === ruleText))
2682
- return flag(false, "rule_not_offered");
2683
- const persisted = await lane.persistCardRule({
2684
- ...common,
2685
- redemption: persistRule.edited ? { kind: "edited", text: ruleText } : { kind: "offered", ruleText },
2686
- });
2687
- if (!persisted.ok) {
2688
- defaultLogger.warn("permission rule was not persisted after a parked late decision", {
2689
- askId: row.askId,
2690
- reason: persisted.reason,
2691
- ...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}),
2692
- });
2693
- return flag(false, persisted.reason);
2694
- }
2695
- return flag(true, undefined, persistRule.edited && persisted.kind === "single" ? { persistedRule: persisted.rule } : {});
2696
- }
2697
- catch (err) {
2698
- this.noteStoreError(err, "persistCardRule(parked-late)");
2699
- return { ...flag(false, "rule_store_error"), terminal: false };
2700
- }
2701
- }
2702
- async replayRedeemReceipt(id, memo, row, receipt, parsed, recordedNote) {
2703
- let rule = receipt.decision === "approve" ? receipt.rule : this.lateRuleRefusal(parsed);
2704
- if (receipt.decision === "approve" && rule === undefined && parsed.persistRule !== undefined) {
2705
- const flight = memo.ruleInflight ?? this.persistRuleFromRow(row, parsed.persistRule, parsed.updatedInput !== undefined);
2706
- const isOwner = memo.ruleInflight === undefined;
2707
- if (isOwner)
2708
- memo.ruleInflight = flight;
2709
- let fresh;
2710
- try {
2711
- fresh = await flight;
2712
- }
2713
- finally {
2714
- if (isOwner)
2715
- memo.ruleInflight = undefined;
2716
- }
2717
- rule = fresh?.body;
2718
- if (fresh?.terminal === true && memo.redeemed !== undefined)
2719
- memo.redeemed.rule = fresh.body;
2720
- }
2721
- const sameDecision = (parsed.value === "deny") === (receipt.decision === "deny");
2722
- return {
2723
- status: 200,
2724
- body: {
2725
- ...(sameDecision ? receipt.body : withoutSettlementEvidence(receipt.body)),
2726
- approvalId: id,
2727
- askId: row.askId,
2728
- accepted: "parked_redeemed",
2729
- idempotent: true,
2730
- decision: receipt.decision === "approve" ? "allow" : "deny",
2731
- ...noteRecordedReceipt(parsed.note, noteLandedOnRow(parsed.note, recordedNote.decisionNote)),
2732
- ...(rule ?? {}),
2733
- },
2734
- };
2735
- }
2736
- async persistRuleAfterDecision(settled, entry, judgedOwner, persistRule, inputWasEdited, ruleEligibility) {
2737
- const result = await settled;
2738
- if (result.status !== 200)
2739
- return result;
2740
- const lane = this.ruleConsent;
2741
- const material = entry.ruleLane;
2742
- const owner = judgedOwner;
2743
- const withFlag = (rulePersisted, ruleRefusal, echo) => ({
2744
- status: result.status,
2745
- body: { ...result.body, rulePersisted, ...(ruleRefusal !== undefined ? { ruleRefusal } : {}), ...(echo ?? {}) },
2746
- });
2747
- if (entry.governanceForced === true)
2748
- return withFlag(false, "rule_governance_forced");
2749
- if (lane === undefined || material === undefined || owner === null)
2750
- return withFlag(false, "rule_lane_unavailable");
2751
- if (!ruleEligibility.eligible)
2752
- return withFlag(false, "rule_store_error");
2753
- if (inputWasEdited)
2754
- return withFlag(false, "rule_input_edited");
2755
- if (ruleMaterialCarriesCredentialShape(material.command)) {
2756
- defaultLogger.warn("approval rule material withheld on the live leg: the adjudicated command carries credential-shaped bytes", { commandChars: material.command.length });
2757
- return withFlag(false, "rule_material_absent");
2758
- }
2759
- if (persistRule.kind === "batch") {
2760
- const offer = material.offers[persistRule.batchOfferIndex];
2761
- if (offer === undefined || offer.kind !== "batch")
2762
- return withFlag(false, "rule_not_offered");
2763
- try {
2764
- const persisted = await lane.persistCardRule({
2765
- principal: owner,
2766
- toolName: entry.toolName,
2767
- command: material.command,
2768
- redemption: { kind: "batch", offer },
2769
- ...(material.toolCallId !== undefined ? { toolCallId: material.toolCallId } : {}),
2770
- ...(material.boundInputHash !== undefined ? { boundInputHash: material.boundInputHash } : {}),
2771
- ...(material.scopeRoot !== undefined ? { scope: { kind: "project", root: material.scopeRoot } } : {}),
2772
- });
2773
- if (!persisted.ok) {
2774
- defaultLogger.warn("permission rule batch was not persisted after an approval", { reason: persisted.reason, ...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}) });
2775
- return withFlag(false, persisted.reason);
2251
+ const rows = await this.withStoreDeadline(store.listAsksAtGate(sourceSessionId, binding), "listAsksAtGate");
2252
+ const atGate = [];
2253
+ for (const row of rows) {
2254
+ if (row.state !== "PARKED") {
2255
+ atGate.push(row);
2256
+ continue;
2776
2257
  }
2777
- return withFlag(true, undefined, persisted.kind === "batch"
2778
- ? {
2779
- persistedRules: persisted.rules,
2780
- ...(persisted.members !== undefined
2781
- ? { persistedRuleAnchors: persisted.members.map((m) => ({ offerIndex: persistRule.batchOfferIndex, memberIndex: m.memberIndex, rule: m.rule })) }
2782
- : {}),
2783
- }
2784
- : {});
2785
- }
2786
- catch (err) {
2787
- this.noteStoreError(err, "persistCardRule(batch)");
2788
- return withFlag(false, "rule_store_error");
2789
- }
2790
- }
2791
- const ruleText = persistRule.rule;
2792
- if (!persistRule.edited && !material.offers.some((o) => o.kind === "single" && o.rule === ruleText))
2793
- return withFlag(false, "rule_not_offered");
2794
- try {
2795
- const persisted = await lane.persistCardRule({
2796
- principal: owner,
2797
- toolName: entry.toolName,
2798
- command: material.command,
2799
- redemption: persistRule.edited ? { kind: "edited", text: ruleText } : { kind: "offered", ruleText },
2800
- ...(material.toolCallId !== undefined ? { toolCallId: material.toolCallId } : {}),
2801
- ...(material.boundInputHash !== undefined ? { boundInputHash: material.boundInputHash } : {}),
2802
- ...(material.scopeRoot !== undefined ? { scope: { kind: "project", root: material.scopeRoot } } : {}),
2803
- });
2804
- if (!persisted.ok) {
2805
- defaultLogger.warn("permission rule was not persisted after an approval", { reason: persisted.reason, ...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}) });
2806
- return withFlag(false, persisted.reason);
2258
+ const gate = await this.readParkedGate(row);
2259
+ if (gate === undefined)
2260
+ return { kind: "unreadable" };
2261
+ if (gate.kind === "open")
2262
+ atGate.push(row);
2807
2263
  }
2808
- return withFlag(true, undefined, this.editedArmEcho(persistRule, persisted));
2264
+ if (atGate.length === 0)
2265
+ return { kind: "none" };
2266
+ return atGate.length === 1 ? { kind: "row", row: atGate[0] } : { kind: "unreadable" };
2809
2267
  }
2810
2268
  catch (err) {
2811
- this.noteStoreError(err, "persistCardRule");
2812
- return withFlag(false, "rule_store_error");
2269
+ this.noteStoreError(err, "listAsksAtGate");
2270
+ return { kind: "unreadable" };
2813
2271
  }
2814
2272
  }
2815
2273
  writeAskDecision(store, askId, form, owner, write) {
@@ -2832,129 +2290,8 @@ export class ToolApprovalCoordinator {
2832
2290
  }
2833
2291
  }
2834
2292
  }
2835
- async respondWithCas(askStore, askId, batchId, owner, entry, parsed, ruleEligibility) {
2836
- let winnerRow;
2837
- let ambiguousConfirmedRow;
2838
- let noteLanded = false;
2839
- try {
2840
- const decided = await this.writeAskDecision(askStore, askId, { kind: "live", batchId }, owner, {
2841
- decision: parsed.value === "deny" ? "deny" : "approve",
2842
- decider: parsed.decider,
2843
- ...(parsed.note !== undefined ? { note: parsed.note } : {}),
2844
- ...(parsed.updatedInput !== undefined ? { updatedInput: parsed.updatedInput } : {}),
2845
- });
2846
- if (decided.ok) {
2847
- winnerRow = decided.row;
2848
- noteLanded = true;
2849
- }
2850
- if (!decided.ok) {
2851
- return { lost: true, row: decided.row };
2852
- }
2853
- }
2854
- catch (err) {
2855
- this.noteStoreError(err, "decideAsk");
2856
- ruleEligibility.eligible = false;
2857
- try {
2858
- const confirmSeq = this.nextRowReadSeq();
2859
- const row = await this.withStoreDeadline(askStore.getAsk(askId), "getAsk(decideAsk-confirm)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
2860
- if (row?.state === "STREAM_PENDING")
2861
- this.followRowOwner(askId, row.owner, confirmSeq);
2862
- if (row === null || !askRowCarriesOwner(row, owner))
2863
- return { lost: true, row: null };
2864
- if (row.state === "DECIDED" && row.decision === (parsed.value === "deny" ? "deny" : "approve")) {
2865
- winnerRow = row;
2866
- ambiguousConfirmedRow = row;
2867
- noteLanded = noteLandedOnRow(parsed.note, row.decisionNote);
2868
- }
2869
- else if (row.state !== "STREAM_PENDING") {
2870
- return { lost: true, row };
2871
- }
2872
- }
2873
- catch (confirmErr) {
2874
- this.noteStoreError(confirmErr, "getAsk(decideAsk-confirm)");
2875
- return {
2876
- lost: false,
2877
- result: {
2878
- status: 500,
2879
- body: refusalBody({
2880
- errorCode: "internal.error",
2881
- message: "the approval store failed while recording this answer and could not be read back, so whether the answer was recorded is unknown — retry: a retry returns the recorded answer if it was recorded, or records it if it was not",
2882
- }),
2883
- },
2884
- };
2885
- }
2886
- }
2887
- const wantAllowed = parsed.value !== "deny";
2888
- const decidedWon = winnerRow !== undefined;
2889
- if (entry.adopted === true && !decidedWon)
2890
- return { lost: false, result: this.notPending() };
2891
- if (this.pending.get(entry.registrationId) !== entry) {
2892
- const othersReached = winnerRow !== undefined ? this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow)).settled : 0;
2893
- const converged = decidedWon && (entry.adopted === true || entry.settledOutcome === (wantAllowed ? SETTLE_OUTCOME.ALLOWED : SETTLE_OUTCOME.DENIED));
2894
- if (!converged) {
2895
- return { lost: false, result: this.notPending() };
2896
- }
2897
- return { lost: false, result: withConsumerDisclosure(this.finishRespond(entry, owner, parsed, { updatedInputDelivered: false, noteRecorded: noteLanded }), waitingLegsReached(entry, othersReached)) };
2898
- }
2899
- const payloadIsMine = ambiguousConfirmedRow === undefined ||
2900
- approvalPayloadFingerprint({ updatedInput: ambiguousConfirmedRow.updatedInput }) === approvalPayloadFingerprint({ updatedInput: parsed.updatedInput });
2901
- const result = this.finishRespond(entry, owner, parsed, {
2902
- noteRecorded: noteLanded,
2903
- ...(winnerRow !== undefined ? { settleOutcome: outcomeOfDecidedRow(winnerRow), settleOutcomeIsThisRequest: ambiguousConfirmedRow === undefined } : {}),
2904
- ...(entry.adopted === true ? { updatedInputDelivered: false } : ambiguousConfirmedRow !== undefined ? { updatedInputDelivered: payloadIsMine } : {}),
2905
- });
2906
- const othersReached = winnerRow !== undefined ? this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow)).settled : 0;
2907
- return { lost: false, result: withConsumerDisclosure(result, waitingLegsReached(entry, othersReached)) };
2908
- }
2909
- finishRespond(entry, judgedOwner, parsed, opts) {
2910
- let rememberApplied;
2911
- if (parsed.value === "allow_session") {
2912
- const answer = liveSessionAnswer(pendingEntryStandingAnswerFacts(entry), entry);
2913
- rememberApplied = answer.home === "seat";
2914
- if (answer.home === "seat") {
2915
- const grantKey = sessionAllowKey(judgedOwner, answer.seat, sessionAllowCategory(entry.toolName), entry.grantBinding);
2916
- if (this.allowAllSessions.size >= MAX_ALLOW_SESSIONS) {
2917
- const oldest = this.allowAllSessions.values().next().value;
2918
- if (oldest !== undefined)
2919
- this.allowAllSessions.delete(oldest);
2920
- }
2921
- this.allowAllSessions.add(grantKey);
2922
- }
2923
- else if (answer.home === "none" && answer.occupantUnreadable) {
2924
- defaultLogger.warn("session_grant_refused_occupant_unreadable", { sessionId: entry.sessionId, toolName: entry.toolName });
2925
- }
2926
- }
2927
- const allowed = parsed.value !== "deny";
2928
- const settleArgs = opts?.settleOutcome !== undefined
2929
- ? settleArgsOfDecided(opts.settleOutcome)
2930
- : { allowed, updatedInput: parsed.updatedInput, source: liveHostSource(parsed.decider), ...(!allowed && parsed.note !== undefined ? { denyReason: parsed.note } : {}) };
2931
- const readRootGrant = opts?.settleOutcome === undefined || opts.settleOutcomeIsThisRequest === true ? parsed.readRootGrant : undefined;
2932
- entry.settle({
2933
- by: "human",
2934
- allowed: settleArgs.allowed,
2935
- updatedInput: settleArgs.updatedInput,
2936
- ...(settleArgs.source !== undefined ? { hostSource: settleArgs.source } : {}),
2937
- ...(settleArgs.denyReason !== undefined ? { denyReason: settleArgs.denyReason } : {}),
2938
- ...(readRootGrant !== undefined ? { readRootGrant } : {}),
2939
- });
2940
- const updatedInputDelivered = opts?.updatedInputDelivered ?? true;
2941
- const noteRecorded = opts?.noteRecorded ?? false;
2942
- return {
2943
- status: 200,
2944
- body: {
2945
- approvalId: entry.approvalId,
2946
- delivery: "applied",
2947
- decision: parsed.value,
2948
- ...(rememberApplied !== undefined ? { rememberApplied } : {}),
2949
- ...(parsed.updatedInput !== undefined && allowed && updatedInputDelivered ? { updatedInputForwarded: true } : {}),
2950
- ...noteRecordedReceipt(parsed.note, noteRecorded),
2951
- },
2952
- };
2953
- }
2954
2293
  async adoptOpenAsks() {
2955
2294
  const store = this.askStore;
2956
- if (!store)
2957
- return { adopted: 0, alreadyLive: 0, cardUnreadable: 0, capped: false };
2958
2295
  let rows;
2959
2296
  try {
2960
2297
  rows = await this.withStoreDeadline(store.listByState("STREAM_PENDING", ADOPT_OPEN_ASKS_MAX + 1), "listByState(adopt)");
@@ -3022,11 +2359,9 @@ export class ToolApprovalCoordinator {
3022
2359
  settle: (verdict) => leave(verdict.by === "system" ? { outcome: SETTLE_OUTCOME.EXPIRED, cause: verdict.cause, parked: false } : { outcome: verdict.allowed ? SETTLE_OUTCOME.ALLOWED : SETTLE_OUTCOME.DENIED, parked: false }),
3023
2360
  forceParkNow: release,
3024
2361
  settleParkRoute: (cause) => leave({ outcome: SETTLE_OUTCOME.EXPIRED, cause, parked: this.unattendedRoutesToPark }),
3025
- owner: row.owner,
3026
- ownerReadSeq: this.rowReadSeq,
3027
2362
  executionLane: row.executionLane,
3028
2363
  ...leg,
3029
- ...(card.fromSubagent === true ? { originTaskId: row.sourceSessionId } : {}),
2364
+ ...(card.fromSubagent === true ? { sourceSessionId: row.sourceSessionId } : {}),
3030
2365
  toolName: card.toolName,
3031
2366
  ...facts,
3032
2367
  approvalId: row.askId,
@@ -3057,43 +2392,6 @@ export class ToolApprovalCoordinator {
3057
2392
  for (const watcher of watchers)
3058
2393
  watcher.askIds.delete(askId);
3059
2394
  }
3060
- followRowOwner(askId, owner, readSeq) {
3061
- for (const e of this.pendingByAskId.get(askId) ?? []) {
3062
- if (e.ownerReadSeq >= readSeq)
3063
- continue;
3064
- e.owner = owner;
3065
- e.ownerReadSeq = readSeq;
3066
- }
3067
- }
3068
- rowReadSeq = 0;
3069
- nextRowReadSeq() {
3070
- this.rowReadSeq += 1;
3071
- return this.rowReadSeq;
3072
- }
3073
- async followRowOwners() {
3074
- const store = this.askStore;
3075
- const out = { read: 0, followed: 0, unreadable: 0 };
3076
- if (!store)
3077
- return out;
3078
- for (const askId of [...this.pendingByAskId.keys()]) {
3079
- out.read += 1;
3080
- const readSeq = this.nextRowReadSeq();
3081
- let row;
3082
- try {
3083
- row = await this.withStoreDeadline(store.getAsk(askId), "getAsk(follow-row-owners)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
3084
- }
3085
- catch (err) {
3086
- this.noteStoreError(err, "getAsk(follow-row-owners)");
3087
- out.unreadable += 1;
3088
- continue;
3089
- }
3090
- if (row?.state !== "STREAM_PENDING")
3091
- continue;
3092
- this.followRowOwner(askId, row.owner, readSeq);
3093
- out.followed += 1;
3094
- }
3095
- return out;
3096
- }
3097
2395
  waitingRegistrationCount(askId) {
3098
2396
  let n = 0;
3099
2397
  for (const e of this.pendingByAskId.get(askId) ?? [])
@@ -3111,9 +2409,7 @@ export class ToolApprovalCoordinator {
3111
2409
  const store = this.askStore;
3112
2410
  const ids = [...candidates.keys()];
3113
2411
  const listed = await Promise.all([...candidates.values()].map(async (entry) => {
3114
- if (store === undefined || !this.registrationHasRow(entry))
3115
- return inScope(entry.owner);
3116
- const ask = await this.askRowFollowingOwner(store, entry.askId, "getAsk(list-live)");
2412
+ const ask = await this.askRowFor(store, entry.askId, "getAsk(list-live)");
3117
2413
  return ask?.state === "STREAM_PENDING" && inScope(ask.owner);
3118
2414
  }));
3119
2415
  const rows = [];
@@ -3140,13 +2436,11 @@ export class ToolApprovalCoordinator {
3140
2436
  }
3141
2437
  async pollSuspendedDecisions() {
3142
2438
  const store = this.askStore;
3143
- if (!store)
3144
- return { polled: 0, settled: 0, backlog: 0, truncated: false };
3145
2439
  const waiting = [];
3146
2440
  const adopted = [];
3147
2441
  const seen = new Set();
3148
2442
  for (const entry of this.pending.values()) {
3149
- if (entry.askId === undefined || entry.settledOutcome !== undefined || seen.has(entry.askId))
2443
+ if (entry.settledOutcome !== undefined || seen.has(entry.askId))
3150
2444
  continue;
3151
2445
  if (entry.suspended === true)
3152
2446
  waiting.push(entry.askId);
@@ -3162,7 +2456,6 @@ export class ToolApprovalCoordinator {
3162
2456
  { items: adopted, cursor: this.adoptedPollCursor },
3163
2457
  ], { maxPerTick: SUSPENDED_POLL_MAX_PER_TICK, budgetMs: SUSPENDED_POLL_BUDGET_MS }, async (askId) => {
3164
2458
  let row;
3165
- const readSeq = this.nextRowReadSeq();
3166
2459
  try {
3167
2460
  row = await this.withStoreDeadline(store.getAsk(askId), "getAsk(poll-suspended)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
3168
2461
  }
@@ -3174,10 +2467,8 @@ export class ToolApprovalCoordinator {
3174
2467
  this.forgetAdoptedAsk(askId);
3175
2468
  return;
3176
2469
  }
3177
- if (row.state === "STREAM_PENDING") {
3178
- this.followRowOwner(askId, row.owner, readSeq);
2470
+ if (row.state === "STREAM_PENDING")
3179
2471
  return;
3180
- }
3181
2472
  if (this.editedDecisionInFlight(askId))
3182
2473
  return;
3183
2474
  const endingCause = askRowEndingCause(row);