@sema-agent/server 7.150.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.
- package/MIGRATION.md +451 -0
- package/README.md +15 -3
- package/README.zh-CN.md +10 -2
- package/USAGE.md +144 -87
- package/deploy/sema-up/README.md +11 -4
- package/deploy/sema-up/chart/Chart.yaml +1 -1
- package/deploy/sema-up/chart/templates/NOTES.txt +5 -0
- package/deploy/sema-up/chart/templates/_helpers.tpl +15 -16
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +5 -3
- package/deploy/sema-up/chart/templates/server.yaml +5 -3
- package/deploy/sema-up/chart/values-full-single.yaml +3 -1
- package/deploy/sema-up/chart/values.yaml +8 -1
- package/deploy/sema-up/cluster-up.sh +7 -6
- package/deploy/sema-up/kube-up.sh +6 -6
- package/deploy/sema-up/lib/flagshape.sh +36 -8
- package/deploy/sema-up/lib/helm-values-cluster.py +2 -0
- package/deploy/sema-up/lib/helm-values-kube.py +2 -0
- package/deploy/sema-up/machines.example.yaml +3 -1
- package/deploy/sema-up/sema-up.sh +22 -16
- package/dist/adoption/import.js +3 -3
- package/dist/approval-ask-audit-store.d.ts +1 -1
- package/dist/approval-ask-machine.d.ts +4 -10
- package/dist/approval-ask-retention.d.ts +1 -1
- package/dist/approval-card.d.ts +12 -14
- package/dist/approval-gate-currency.d.ts +9 -9
- package/dist/approval-gate-currency.js +3 -4
- package/dist/approval-hmac.d.ts +17 -1
- package/dist/approval-hmac.js +7 -1
- package/dist/approval-queue-kind.d.ts +1 -1
- package/dist/approval.d.ts +2 -2
- package/dist/approval.js +2 -2
- package/dist/auth-bridge.d.ts +20 -0
- package/dist/auth-bridge.js +9 -0
- package/dist/background-session-reap.d.ts +1 -1
- package/dist/bake-runner/main.d.ts +4 -2
- package/dist/bind-host.d.ts +3 -6
- package/dist/bind-host.js +4 -7
- package/dist/boot/budget-tracing.js +1 -1
- package/dist/boot/coordinators.d.ts +1 -1
- package/dist/boot/coordinators.js +9 -10
- package/dist/boot/device-lane.js +2 -0
- package/dist/boot/engine-lease.d.ts +5 -0
- package/dist/boot/engine-lease.js +1 -0
- package/dist/boot/execution-env.d.ts +9 -14
- package/dist/boot/execution-env.js +85 -22
- package/dist/boot/execution-lane.js +32 -5
- package/dist/boot/late-binds.d.ts +8 -4
- package/dist/boot/late-binds.js +7 -1
- package/dist/boot/leader.d.ts +31 -30
- package/dist/boot/leader.js +38 -28
- package/dist/boot/leg-path-adjudication.d.ts +40 -0
- package/dist/boot/leg-path-adjudication.js +18 -0
- package/dist/boot/memory-consolidation.d.ts +3 -2
- package/dist/boot/memory-consolidation.js +6 -6
- package/dist/boot/parked-revive-gate.js +3 -15
- package/dist/boot/reapers.d.ts +6 -1
- package/dist/boot/reapers.js +13 -2
- package/dist/boot/resolve-spec.d.ts +3 -3
- package/dist/boot/resolve-spec.js +43 -48
- package/dist/boot/runner-deps.d.ts +46 -35
- package/dist/boot/runner-deps.js +94 -91
- package/dist/boot/session-age-out.d.ts +36 -0
- package/dist/boot/session-age-out.js +101 -0
- package/dist/boot/session-faces.d.ts +5 -14
- package/dist/boot/session-faces.js +9 -5
- package/dist/boot/shutdown.d.ts +6 -0
- package/dist/boot/shutdown.js +10 -4
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-02-web-admin-pull.js +4 -5
- package/dist/boot/stage-05-execution-env.d.ts +5 -3
- package/dist/boot/stage-05-execution-env.js +3 -2
- package/dist/boot/stage-06-runners.d.ts +9 -4
- package/dist/boot/stage-06-runners.js +24 -9
- package/dist/boot/stage-07-capability-layer.d.ts +8 -4
- package/dist/boot/stage-07-capability-layer.js +3 -2
- package/dist/boot/stage-08-reapers.d.ts +8 -4
- package/dist/boot/stage-08-reapers.js +4 -2
- package/dist/boot/stage-09-leader.d.ts +12 -5
- package/dist/boot/stage-09-leader.js +14 -5
- package/dist/boot/stage-10-http-server.d.ts +19 -5
- package/dist/boot/stage-10-http-server.js +14 -21
- package/dist/boot/stores.js +7 -4
- package/dist/boot/web-admin.js +5 -8
- package/dist/boot/workflow-orchestration.d.ts +7 -2
- package/dist/boot/workflow-orchestration.js +24 -7
- package/dist/bounded-rotation.d.ts +29 -0
- package/dist/bounded-rotation.js +24 -0
- package/dist/capabilities/approvals-push.d.ts +3 -3
- package/dist/capabilities/scenarios.d.ts +28 -13
- package/dist/capabilities/scenarios.js +2 -2
- package/dist/capability-facts.d.ts +43 -32
- package/dist/capability-facts.js +45 -51
- package/dist/card-rule-offers.d.ts +32 -0
- package/dist/card-rule-offers.js +130 -0
- package/dist/center-credential.d.ts +25 -6
- package/dist/center-credential.js +25 -5
- package/dist/config-catalog.d.ts +26 -7
- package/dist/config-catalog.js +26 -22
- package/dist/config-dir-trust.js +4 -2
- package/dist/config-invariants.d.ts +20 -8
- package/dist/config-invariants.js +55 -12
- package/dist/config-lkg.d.ts +5 -19
- package/dist/config-lkg.js +8 -62
- package/dist/config-provider.d.ts +1 -1
- package/dist/config-root-migration.d.ts +2 -2
- package/dist/config-types.d.ts +38 -42
- package/dist/config.d.ts +58 -7
- package/dist/config.js +90 -40
- package/dist/control-socket.d.ts +51 -0
- package/dist/control-socket.js +115 -0
- package/dist/deployment-governance.d.ts +2 -1
- package/dist/deployment-shape.d.ts +9 -1
- package/dist/deployment-shape.js +2 -1
- package/dist/device-protocol.d.ts +5 -4
- package/dist/device-protocol.js +1 -1
- package/dist/device-ws-hub.d.ts +7 -0
- package/dist/device-ws-hub.js +10 -3
- package/dist/device-ws-protocol.d.ts +63 -10
- package/dist/device-ws-protocol.js +56 -25
- package/dist/elicitation.js +2 -2
- package/dist/engine-identity.d.ts +19 -7
- package/dist/engine-identity.js +10 -2
- package/dist/entry-mode-clamp-notice.d.ts +2 -1
- package/dist/entry-problems.d.ts +40 -3
- package/dist/entry-problems.js +3 -1
- package/dist/execution-env-refusal.d.ts +29 -0
- package/dist/execution-env-refusal.js +8 -0
- package/dist/execution-lane-caps.d.ts +3 -1
- package/dist/field-conflict.d.ts +1 -1
- package/dist/file-history-capture.d.ts +11 -4
- package/dist/file-history-capture.js +9 -6
- package/dist/file-history-disclosure.d.ts +7 -5
- package/dist/file-history-disclosure.js +7 -5
- package/dist/fleet/drain-entry.d.ts +4 -0
- package/dist/fleet/engine-signal.d.ts +12 -29
- package/dist/fleet/engine-signal.js +1 -7
- package/dist/fleet-client.js +2 -5
- package/dist/gate-settlement.d.ts +1 -8
- package/dist/gate-settlement.js +0 -4
- package/dist/history-anchor.js +1 -1
- package/dist/hitl-delivery.d.ts +2 -3
- package/dist/hitl-delivery.js +0 -2
- package/dist/hooks/hook-llm.js +4 -2
- package/dist/hooks/hook-runner.d.ts +3 -2
- package/dist/hooks/hook-runner.js +18 -3
- package/dist/host-decision.d.ts +11 -11
- package/dist/host-lsp-manager.d.ts +2 -2
- package/dist/http/active-run-conflict.d.ts +35 -2
- package/dist/http/active-run-conflict.js +48 -19
- package/dist/http/admission.js +17 -2
- package/dist/http/approval-sweeps.d.ts +1 -1
- package/dist/http/approval-sweeps.js +10 -7
- package/dist/http/checkpoint-ctx.d.ts +4 -0
- package/dist/http/checkpoint-ctx.js +2 -0
- package/dist/http/dispatch.d.ts +8 -3
- package/dist/http/dispatch.js +59 -32
- package/dist/http/durable-run-leg.js +16 -17
- package/dist/http/health-reader.d.ts +16 -9
- package/dist/http/health-reader.js +2 -1
- package/dist/http/health-self-report.d.ts +9 -1
- package/dist/http/health-self-report.js +3 -1
- package/dist/http/owner-access.d.ts +33 -14
- package/dist/http/owner-access.js +40 -10
- package/dist/http/principal-gate.d.ts +49 -21
- package/dist/http/principal-gate.js +61 -11
- package/dist/http/request-channel.d.ts +23 -0
- package/dist/http/request-channel.js +9 -0
- package/dist/http/request-guards.js +17 -2
- package/dist/http/resume-ctx-read.js +42 -3
- package/dist/http/resume-legs.d.ts +4 -3
- package/dist/http/resume-legs.js +367 -117
- package/dist/http/route-ctx.d.ts +91 -15
- package/dist/http/route-ctx.js +17 -0
- package/dist/http/route-table.d.ts +5 -1
- package/dist/http/route-table.js +4 -0
- package/dist/http/routes/admin-config-refresh.js +3 -3
- package/dist/http/routes/admin-drain.d.ts +3 -0
- package/dist/http/routes/admin-drain.js +7 -7
- package/dist/http/routes/admin-memory-optout.d.ts +1 -1
- package/dist/http/routes/admin-memory-optout.js +3 -3
- package/dist/http/routes/adoption.js +3 -5
- package/dist/http/routes/agents-roster.js +2 -3
- package/dist/http/routes/approvals-assistant.d.ts +2 -1
- package/dist/http/routes/approvals-assistant.js +99 -51
- package/dist/http/routes/background.d.ts +3 -3
- package/dist/http/routes/capabilities.d.ts +4 -0
- package/dist/http/routes/capabilities.js +38 -10
- package/dist/http/routes/config-catalog.js +2 -2
- package/dist/http/routes/devices.js +1 -1
- package/dist/http/routes/diagnostics.d.ts +3 -3
- package/dist/http/routes/diagnostics.js +19 -7
- package/dist/http/routes/fleet.js +10 -8
- package/dist/http/routes/images.js +10 -10
- package/dist/http/routes/memory-bundle.js +3 -3
- package/dist/http/routes/memory-compliance.js +3 -3
- package/dist/http/routes/memory-consolidation.js +2 -2
- package/dist/http/routes/memory-origin.js +3 -3
- package/dist/http/routes/memory-policy.js +2 -2
- package/dist/http/routes/observability.js +3 -3
- package/dist/http/routes/projects.d.ts +3 -2
- package/dist/http/routes/projects.js +4 -5
- package/dist/http/routes/retention-ops.js +3 -3
- package/dist/http/routes/rules.js +5 -5
- package/dist/http/routes/run-permission-mode.d.ts +2 -1
- package/dist/http/routes/run-permission-mode.js +2 -2
- package/dist/http/routes/runs.d.ts +4 -9
- package/dist/http/routes/runs.js +171 -65
- package/dist/http/routes/session-sync.d.ts +22 -9
- package/dist/http/routes/session-sync.js +204 -107
- package/dist/http/routes/sessions-list.js +13 -7
- package/dist/http/routes/sessions.js +30 -15
- package/dist/http/routes/tasks.js +46 -41
- package/dist/http/routes/trace-usage.js +2 -2
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/server.d.ts +30 -30
- package/dist/http/server.js +16 -4
- package/dist/http/submit-ctx.d.ts +1 -1
- package/dist/http/submit-ctx.js +7 -4
- package/dist/http/wire-types.d.ts +14 -4
- package/dist/human-facing-args.d.ts +33 -0
- package/dist/human-facing-args.js +33 -0
- package/dist/identity-source.d.ts +20 -3
- package/dist/identity-source.js +14 -0
- package/dist/index.d.ts +5 -1
- package/dist/k8s-api-credentials.d.ts +29 -0
- package/dist/k8s-api-credentials.js +25 -0
- package/dist/lane-home-probe.d.ts +3 -7
- package/dist/lane-home-probe.js +2 -2
- package/dist/leader/diffup.d.ts +19 -0
- package/dist/leader/diffup.js +7 -0
- package/dist/leader/leader.d.ts +116 -11
- package/dist/leader/leader.js +69 -5
- package/dist/leader/repair-wire.js +2 -2
- package/dist/leader/run-entry.d.ts +2 -1
- package/dist/leader/run-entry.js +1 -1
- package/dist/leader/wire.d.ts +42 -132
- package/dist/leader/wire.js +81 -67
- package/dist/leg-facts.d.ts +13 -5
- package/dist/leg-facts.js +2 -1
- package/dist/memory-scope.d.ts +23 -11
- package/dist/memory-scope.js +18 -6
- package/dist/memory-sync-client.d.ts +30 -2
- package/dist/memory-sync-client.js +16 -9
- package/dist/model-entry-headers.d.ts +9 -10
- package/dist/model-entry-headers.js +0 -4
- package/dist/model-entry-refusal.d.ts +7 -4
- package/dist/model-entry-refusal.js +11 -5
- package/dist/model-plane-face.d.ts +1 -1
- package/dist/model-plane-face.js +3 -1
- package/dist/model-route-endpoint.d.ts +13 -7
- package/dist/model-route-endpoint.js +14 -11
- package/dist/model-select.d.ts +22 -0
- package/dist/model-select.js +9 -0
- package/dist/model-unreachable-notice.d.ts +11 -0
- package/dist/model-unreachable-notice.js +4 -4
- package/dist/object-store-buckets.d.ts +640 -6
- package/dist/object-store-buckets.js +166 -0
- package/dist/observability/err-text.d.ts +7 -0
- package/dist/observability/err-text.js +5 -0
- package/dist/observability/fail-open.d.ts +25 -17
- package/dist/observability/fail-open.js +25 -17
- package/dist/observability/logger.js +3 -1
- package/dist/observability/metrics.js +1 -0
- package/dist/operator-roster.d.ts +68 -0
- package/dist/operator-roster.js +32 -0
- package/dist/orchestration/workflow-notify-journal.d.ts +18 -1
- package/dist/orchestration/workflow-notify-journal.js +12 -1
- package/dist/orchestration/workflow-park-decision.d.ts +70 -18
- package/dist/orchestration/workflow-park-decision.js +49 -10
- package/dist/orchestration/workflow-park-holder.d.ts +65 -0
- package/dist/orchestration/workflow-park-holder.js +98 -0
- package/dist/orchestration/workflow-record-fence.d.ts +36 -0
- package/dist/orchestration/workflow-record-fence.js +42 -0
- package/dist/outbound-dispatcher.d.ts +71 -8
- package/dist/outbound-dispatcher.js +48 -7
- package/dist/owner-faces.d.ts +2 -2
- package/dist/parked-decide.d.ts +106 -55
- package/dist/parked-decide.js +153 -117
- package/dist/peer-directory.d.ts +2 -2
- package/dist/peer-inbound-face.d.ts +2 -6
- package/dist/peer-inbound-face.js +0 -3
- package/dist/permission-mode-admission.d.ts +74 -5
- package/dist/permission-mode-admission.js +56 -3
- package/dist/plugins/adoption-log-sql.d.ts +3 -1
- package/dist/plugins/adoption-log-sql.js +7 -1
- package/dist/plugins/approval-ask-store-file.d.ts +14 -2
- package/dist/plugins/approval-ask-store-file.js +11 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +10 -1
- package/dist/plugins/approval-ask-store-memory.js +35 -10
- package/dist/plugins/approval-ask-store-sql.d.ts +69 -4
- package/dist/plugins/approval-ask-store-sql.js +45 -5
- package/dist/plugins/approval-exemption-store.js +3 -0
- package/dist/plugins/approval-nonce-store.js +2 -0
- package/dist/plugins/blob-backend.d.ts +83 -7
- package/dist/plugins/blob-backend.js +57 -6
- package/dist/plugins/breaker-state-sql.js +2 -0
- package/dist/plugins/caching-session-store.d.ts +9 -15
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/checkpoint-index-columns.d.ts +20 -16
- package/dist/plugins/checkpoint-index-columns.js +3 -6
- package/dist/plugins/checkpoint-store-face.d.ts +119 -61
- package/dist/plugins/checkpoint-store-face.js +134 -55
- package/dist/plugins/device-store-sql.js +6 -0
- package/dist/plugins/error-origin.d.ts +18 -0
- package/dist/plugins/error-origin.js +10 -0
- package/dist/plugins/execution-placement-sql.js +6 -6
- package/dist/plugins/image-bake-store-sql.js +6 -0
- package/dist/plugins/image-index-sql.js +4 -0
- package/dist/plugins/landed-attendants.d.ts +212 -0
- package/dist/plugins/landed-attendants.js +161 -0
- package/dist/plugins/leader-run-store-sql.js +4 -0
- package/dist/plugins/local-checkpoint-store.d.ts +13 -3
- package/dist/plugins/local-checkpoint-store.js +38 -49
- package/dist/plugins/local-session-live-reference.d.ts +40 -0
- package/dist/plugins/local-session-live-reference.js +39 -0
- package/dist/plugins/local-session-store.d.ts +71 -33
- package/dist/plugins/local-session-store.js +129 -27
- package/dist/plugins/memory-engine-pg.js +4 -0
- package/dist/plugins/memory-engine-sql-apply.js +5 -2
- package/dist/plugins/memory-engine-tidb.js +4 -0
- package/dist/plugins/memory-optout-grant-store-sql.js +4 -0
- package/dist/plugins/memory-sync-store-pg.js +5 -0
- package/dist/plugins/memory-sync-store-tidb.js +5 -0
- package/dist/plugins/one-time-migrations.d.ts +104 -80
- package/dist/plugins/one-time-migrations.js +226 -132
- package/dist/plugins/org-delivery-quota-store.js +4 -1
- package/dist/plugins/outcome-ledger-sql.js +2 -0
- package/dist/plugins/permission-rule-store-sql.js +11 -4
- package/dist/plugins/pg-cost-quota.js +4 -0
- package/dist/plugins/pg-pool.js +35 -10
- package/dist/plugins/pg-rate-limiter.js +2 -0
- package/dist/plugins/posix-shell-fs.d.ts +6 -1
- package/dist/plugins/posix-shell-fs.js +4 -1
- package/dist/plugins/remote-env-adb.d.ts +19 -15
- package/dist/plugins/remote-env-adb.js +41 -15
- package/dist/plugins/remote-env-connect-seat.d.ts +105 -0
- package/dist/plugins/remote-env-connect-seat.js +72 -0
- package/dist/plugins/remote-env-device.d.ts +22 -5
- package/dist/plugins/remote-env-device.js +106 -8
- package/dist/plugins/remote-env-e2b.d.ts +15 -6
- package/dist/plugins/remote-env-e2b.js +42 -32
- package/dist/plugins/remote-env-file-error.js +6 -2
- package/dist/plugins/remote-env-host.d.ts +46 -10
- package/dist/plugins/remote-env-host.js +124 -52
- package/dist/plugins/remote-env-k8s.d.ts +109 -41
- package/dist/plugins/remote-env-k8s.js +160 -54
- package/dist/plugins/remote-env-local-docker.d.ts +13 -8
- package/dist/plugins/remote-env-local-docker.js +34 -41
- package/dist/plugins/remote-env-ssh.d.ts +18 -12
- package/dist/plugins/remote-env-ssh.js +60 -59
- package/dist/plugins/remote-shell.d.ts +98 -1
- package/dist/plugins/remote-shell.js +218 -0
- package/dist/plugins/replica-registration.d.ts +1 -1
- package/dist/plugins/replica-registration.js +1 -1
- package/dist/plugins/resume-anchor-store-sql.d.ts +11 -2
- package/dist/plugins/resume-anchor-store-sql.js +12 -5
- package/dist/plugins/retention-lane-store-sql.js +4 -0
- package/dist/plugins/retention-sql.d.ts +20 -2
- package/dist/plugins/retention-sql.js +11 -0
- package/dist/plugins/run-store-sql.d.ts +21 -2
- package/dist/plugins/run-store-sql.js +31 -7
- package/dist/plugins/s3-presign.d.ts +23 -0
- package/dist/plugins/s3-presign.js +37 -3
- package/dist/plugins/scenario-prefix-switch-m42.d.ts +1 -1
- package/dist/plugins/scenario-prefix-switch-m42.js +2 -2
- package/dist/plugins/scheduler-support.d.ts +2 -2
- package/dist/plugins/scheduler-support.js +0 -3
- package/dist/plugins/schema-consistency.d.ts +4 -2
- package/dist/plugins/schema-consistency.js +19 -2
- package/dist/plugins/schema-indexes.js +2 -0
- package/dist/plugins/send-file-ledger.d.ts +2 -2
- package/dist/plugins/send-file-ledger.js +5 -2
- package/dist/plugins/send-user-file.d.ts +3 -4
- package/dist/plugins/send-user-file.js +56 -52
- package/dist/plugins/serving-requirement.d.ts +26 -7
- package/dist/plugins/serving-requirement.js +11 -4
- package/dist/plugins/session-birth.d.ts +6 -2
- package/dist/plugins/session-blob-claim-token.d.ts +15 -0
- package/dist/plugins/session-blob-claim-token.js +20 -0
- package/dist/plugins/session-blob-namespace.d.ts +46 -0
- package/dist/plugins/session-blob-namespace.js +116 -0
- package/dist/plugins/session-capture-record-store-sql.js +2 -0
- package/dist/plugins/session-ext-table.d.ts +14 -1
- package/dist/plugins/session-ext-table.js +5 -1
- package/dist/plugins/session-row-sql.d.ts +49 -2
- package/dist/plugins/session-row-sql.js +56 -13
- package/dist/plugins/session-store-sql.d.ts +34 -18
- package/dist/plugins/session-store-sql.js +98 -17
- package/dist/plugins/shared-memory-store-sql.d.ts +1 -0
- package/dist/plugins/shared-memory-store-sql.js +6 -2
- package/dist/plugins/snapshot-bucket-presence.d.ts +25 -2
- package/dist/plugins/snapshot-bucket-presence.js +8 -2
- package/dist/plugins/sql-driver.d.ts +2 -1
- package/dist/plugins/sql-driver.js +18 -3
- package/dist/plugins/sql-errors.d.ts +11 -6
- package/dist/plugins/sql-errors.js +5 -6
- package/dist/plugins/sql-json-column.d.ts +1 -1
- package/dist/plugins/stop-then-start-sentence.d.ts +10 -4
- package/dist/plugins/stop-then-start-sentence.js +14 -10
- package/dist/plugins/stop-then-start.d.ts +109 -0
- package/dist/plugins/stop-then-start.js +104 -0
- package/dist/plugins/store-backend.d.ts +84 -15
- package/dist/plugins/store-backend.js +236 -72
- package/dist/plugins/store-boundary.d.ts +57 -0
- package/dist/plugins/store-boundary.js +139 -0
- package/dist/plugins/store-contracts.d.ts +17 -0
- package/dist/plugins/store-fault-sentence-m53.d.ts +7 -0
- package/dist/plugins/store-fault-sentence-m53.js +25 -0
- package/dist/plugins/store-fault.d.ts +26 -7
- package/dist/plugins/store-fault.js +18 -2
- package/dist/plugins/task-attachment-store.d.ts +2 -1
- package/dist/plugins/task-attachment-store.js +14 -1
- package/dist/plugins/task-list-store-sql.d.ts +2 -2
- package/dist/plugins/task-list-store-sql.js +10 -8
- package/dist/plugins/tidb-cost-quota.js +4 -0
- package/dist/plugins/tidb-pool.js +21 -25
- package/dist/plugins/tidb-rate-limiter.js +4 -0
- package/dist/plugins/user-input-ledger-sql.js +4 -0
- package/dist/plugins/workflow-run-store-sql.d.ts +28 -10
- package/dist/plugins/workflow-run-store-sql.js +64 -4
- package/dist/plugins/workspace-custody.d.ts +4 -3
- package/dist/plugins/workspace-fill.d.ts +4 -7
- package/dist/plugins/workspace-fill.js +2 -1
- package/dist/plugins/write-behind-counter.js +5 -0
- package/dist/project-context.d.ts +0 -2
- package/dist/project-context.js +4 -8
- package/dist/project-registry.d.ts +46 -0
- package/dist/project-registry.js +44 -0
- package/dist/question.d.ts +2 -1
- package/dist/question.js +5 -13
- package/dist/request-form.d.ts +6 -1
- package/dist/request-form.js +4 -1
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +4 -0
- package/dist/request-value-closure.js +10 -0
- package/dist/rule-issue-reading.d.ts +1 -1
- package/dist/rules-consent.d.ts +1 -1
- package/dist/run-acceptance.d.ts +3 -2
- package/dist/run-acceptance.js +4 -6
- package/dist/run-local.js +9 -9
- package/dist/runs.d.ts +54 -16
- package/dist/runs.js +67 -13
- package/dist/runtime-governance.d.ts +5 -4
- package/dist/security.d.ts +30 -32
- package/dist/security.js +41 -7
- package/dist/sema-config-dir.d.ts +2 -1
- package/dist/server-secret-env.d.ts +12 -10
- package/dist/server-secret-env.js +24 -15
- package/dist/session-bindings.d.ts +82 -0
- package/dist/session-bindings.js +18 -0
- package/dist/session-children-retire.d.ts +2 -2
- package/dist/session-children-retire.js +2 -6
- package/dist/session-deletion.d.ts +16 -1
- package/dist/session-error-wire.d.ts +3 -3
- package/dist/session-error-wire.js +3 -3
- package/dist/session-incarnation.d.ts +10 -0
- package/dist/session-incarnation.js +10 -1
- package/dist/session-kind.d.ts +57 -0
- package/dist/session-kind.js +22 -0
- package/dist/session-row.d.ts +21 -1
- package/dist/session-row.js +7 -1
- package/dist/session-sync-kernel.d.ts +24 -14
- package/dist/session-sync-kernel.js +3 -6
- package/dist/session-sync.d.ts +25 -103
- package/dist/session-sync.js +14 -76
- package/dist/session-titler.d.ts +10 -3
- package/dist/session-titler.js +19 -6
- package/dist/ssh-host-key.d.ts +5 -0
- package/dist/ssh-host-key.js +3 -0
- package/dist/stores-sql/blob-index-sql.d.ts +30 -9
- package/dist/stores-sql/blob-index-sql.js +40 -21
- package/dist/stores-sql/checkpoint-store-sql.d.ts +54 -27
- package/dist/stores-sql/checkpoint-store-sql.js +70 -146
- package/dist/stores-sql/file-history-store-sql.d.ts +113 -15
- package/dist/stores-sql/file-history-store-sql.js +205 -95
- package/dist/stores-sql/pg-session-storage.d.ts +34 -7
- package/dist/stores-sql/pg-session-storage.js +37 -13
- package/dist/stores-sql/retention-store-sql.d.ts +1 -9
- package/dist/stores-sql/retention-store-sql.js +5 -19
- package/dist/stores-sql/session-policy-store-sql.d.ts +30 -7
- package/dist/stores-sql/session-policy-store-sql.js +61 -44
- package/dist/stores-sql/session-write-hooks.d.ts +11 -0
- package/dist/stores-sql/tidb-session-store.d.ts +58 -7
- package/dist/stores-sql/tidb-session-store.js +37 -13
- package/dist/submission-exit.d.ts +3 -2
- package/dist/submission-exit.js +9 -1
- package/dist/task-cwd.d.ts +2 -2
- package/dist/task-settings.d.ts +7 -5
- package/dist/task-settings.js +3 -3
- package/dist/terminal.d.ts +33 -2
- package/dist/terminal.js +139 -9
- package/dist/tool-approval.d.ts +465 -507
- package/dist/tool-approval.js +606 -1222
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/ledger-events.d.ts +3 -3
- package/dist/trace/project.d.ts +2 -3
- package/dist/trace/redact.js +3 -2
- package/dist/trace/self-minted-wire.d.ts +571 -178
- package/dist/trace/self-minted-wire.js +189 -56
- package/dist/web-admin-client/apply-effective.js +2 -2
- package/dist/web-admin-client/read-warnings.d.ts +2 -2
- package/dist/web-admin-client/read-warnings.js +1 -1
- package/dist/web-admin-client/types.d.ts +1 -1
- package/dist/workspace-space.d.ts +10 -4
- package/dist/workspace-space.js +4 -1
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +79 -28
- package/package.json +7 -3
- package/scripts/embed-minimal.mjs +3 -3
- package/wire-changes/server@7.151.0.json +762 -0
- package/wire-changes/server@7.152.0.json +900 -0
- package/dist/config-d-migration.d.ts +0 -37
- package/dist/config-d-migration.js +0 -60
- package/dist/http/session-inherit-env.d.ts +0 -36
- package/dist/http/session-inherit-env.js +0 -6
package/dist/tool-approval.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { readApprovalCard } from "./approval-content-kind.js";
|
|
2
|
-
import {
|
|
2
|
+
import { liveHostSource } from "./host-decision.js";
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
-
import { uuidv7, classifyAsk, foldArbitrationRow,
|
|
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 {
|
|
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,
|
|
18
|
-
import { askDemandsAPerson, storedPersonDemandRecord, currentParkArm,
|
|
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,10 +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";
|
|
29
|
+
import { boundedRotation } from "./bounded-rotation.js";
|
|
30
30
|
const defaultLogger = createLogger();
|
|
31
|
-
const MAX_APPROVAL_ARGS_BYTES = 16384;
|
|
32
31
|
const DEFAULT_APPROVAL_TTL_MS = 5 * 60_000;
|
|
33
32
|
const DEFAULT_UNREACHED_ASK_TTL_MS = 60 * 60_000;
|
|
34
33
|
const SUSPENDED_POLL_MAX_PER_TICK = 64;
|
|
@@ -41,10 +40,8 @@ export const MAX_ALLOW_SESSIONS = 4096;
|
|
|
41
40
|
const DEFAULT_ADMIT_MAX_PER_TASK = 32;
|
|
42
41
|
const DEFAULT_ADMIT_MAX_PER_OWNER = 256;
|
|
43
42
|
const DEFAULT_PARK_REDEMPTION_MAX = 2000;
|
|
44
|
-
const DEFAULT_PARK_REDEMPTION_TTL_MS = 60 * 60_000;
|
|
45
43
|
const DEFAULT_PARK_REDEMPTION_PER_PRINCIPAL_MAX = 128;
|
|
46
44
|
const PARK_REDEMPTION_ADMISSION_RETRY_AFTER_SEC = 5;
|
|
47
|
-
const PARKING_RETRY_AFTER_MS = 250;
|
|
48
45
|
export function approvalDecidePath(sessionId) {
|
|
49
46
|
return `/v1/approvals/${encodeURIComponent(sessionId)}/decide`;
|
|
50
47
|
}
|
|
@@ -68,11 +65,12 @@ function durableRuleMaterial(material) {
|
|
|
68
65
|
return { ruleCommand: material.command, ruleScopeRoot: material.scopeRoot ?? null };
|
|
69
66
|
}
|
|
70
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)";
|
|
71
69
|
export const TOOL_APPROVAL_READ_STATES = ["pending", "decided", "parked", "void"];
|
|
72
|
-
function decidedRowFacts(row) {
|
|
70
|
+
export function decidedRowFacts(row) {
|
|
73
71
|
if (row.state !== "DECIDED" || (row.decision !== "approve" && row.decision !== "deny") || row.provisional || row.decidedAtMs === null)
|
|
74
72
|
return undefined;
|
|
75
|
-
return { decision: row.decision
|
|
73
|
+
return { decision: row.decision, decidedAtMs: row.decidedAtMs, ...(row.decisionNote !== null ? { decisionNote: row.decisionNote } : {}) };
|
|
76
74
|
}
|
|
77
75
|
function buildAskRowReadProjection(row, nowMs) {
|
|
78
76
|
const frame = buildReplayFrame(row, nowMs);
|
|
@@ -85,6 +83,22 @@ function buildAskRowReadProjection(row, nowMs) {
|
|
|
85
83
|
};
|
|
86
84
|
}
|
|
87
85
|
export const ASK_DECISION_CONSUMER_ABSENT = "absent";
|
|
86
|
+
function standingParkedDecision(gate, row, idempotencyKey) {
|
|
87
|
+
if (gate?.kind !== "decided")
|
|
88
|
+
return undefined;
|
|
89
|
+
const decision = gate.decision;
|
|
90
|
+
if (row.decision === decision && row.decidedAtMs !== null && row.decidedAtMs >= gate.decidedAtMs) {
|
|
91
|
+
return {
|
|
92
|
+
kind: "standing",
|
|
93
|
+
decision,
|
|
94
|
+
decidedAtMs: row.decidedAtMs,
|
|
95
|
+
...(row.decisionNote !== null ? { decisionNote: row.decisionNote } : {}),
|
|
96
|
+
...(row.decisionActor !== null ? { actor: row.decisionActor } : {}),
|
|
97
|
+
byThisRequest: idempotencyKey !== undefined && row.idempotencyKey === idempotencyKey,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { kind: "standing", decision, decidedAtMs: gate.decidedAtMs, ...(gate.decisionNote !== undefined ? { decisionNote: gate.decisionNote } : {}), byThisRequest: false };
|
|
101
|
+
}
|
|
88
102
|
const MAX_INFLIGHT_TERMINAL_CLAIMS = 2;
|
|
89
103
|
const DURABLE_CONVERGE_BACKOFF_MS = [50, 100, 200, 400, 800, 1600];
|
|
90
104
|
export const DURABLE_CONVERGE_READ_TIMEOUT_MS = 2_000;
|
|
@@ -247,23 +261,11 @@ function mandatedFromSeat(seat) {
|
|
|
247
261
|
return false;
|
|
248
262
|
}
|
|
249
263
|
}
|
|
250
|
-
function
|
|
251
|
-
return row !== null &&
|
|
252
|
-
}
|
|
253
|
-
function askOwnerAdmitsAnswer(owner, principal) {
|
|
254
|
-
return keyAdmits("ask.answer", { subject: recoveredSubject(principal), operator: false }, ownerCellOf(owner));
|
|
255
|
-
}
|
|
256
|
-
function receiptAnswersNow(redeemedGeneration, openGenerationNow) {
|
|
257
|
-
return redeemedGeneration === undefined || openGenerationNow === undefined || openGenerationNow <= redeemedGeneration;
|
|
258
|
-
}
|
|
259
|
-
function waitingLegsReached(entry, othersReached) {
|
|
260
|
-
return (entry.adopted === true ? 0 : 1) + othersReached;
|
|
261
|
-
}
|
|
262
|
-
function withConsumerDisclosure(result, waitingLegs) {
|
|
263
|
-
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));
|
|
264
266
|
}
|
|
265
267
|
function livePendingRowOf(e) {
|
|
266
|
-
const listFrame = e.
|
|
268
|
+
const listFrame = e.cardFrame;
|
|
267
269
|
const approvalCard = e.adopted === true ? e.cardFrame.card.approvalCard : e.frame.approvalCard;
|
|
268
270
|
return {
|
|
269
271
|
approvalId: e.approvalId,
|
|
@@ -277,8 +279,8 @@ function livePendingRowOf(e) {
|
|
|
277
279
|
...(liveSessionAnswer(pendingEntryStandingAnswerFacts(e), e).home === "none" ? { rememberRefused: true } : {}),
|
|
278
280
|
...(listFrame.mandate !== undefined ? { mandate: listFrame.mandate } : {}),
|
|
279
281
|
...(listFrame.origin !== undefined ? { origin: listFrame.origin } : {}),
|
|
280
|
-
...(listFrame.
|
|
281
|
-
...(e.
|
|
282
|
+
...(listFrame.liveFactsUnknown === true ? { liveFactsUnknown: true } : {}),
|
|
283
|
+
...(e.sourceSessionId !== undefined ? { fromSubagent: true, sourceSessionId: e.sourceSessionId } : {}),
|
|
282
284
|
...(approvalCard !== undefined ? { approvalCard } : {}),
|
|
283
285
|
frame: listFrame,
|
|
284
286
|
...(e.adopted === true ? { consumerState: ASK_DECISION_CONSUMER_ABSENT } : {}),
|
|
@@ -286,102 +288,185 @@ function livePendingRowOf(e) {
|
|
|
286
288
|
}
|
|
287
289
|
const _legFamiliesAreFrameFamilies = true;
|
|
288
290
|
void _legFamiliesAreFrameFamilies;
|
|
289
|
-
function canPresentWithoutDurableRow(c) {
|
|
290
|
-
return c.family === "tool_approval";
|
|
291
|
-
}
|
|
292
291
|
export const PERSIST_RULE_TEXT_ERROR = `persistRule.rule must be a non-empty string of at most ${MAX_RULE_TEXT_CHARS} characters`;
|
|
293
292
|
export const PERSIST_RULE_EDITED_FLAG_ERROR = "persistRule.edited must be a boolean when present";
|
|
294
293
|
export const PERSIST_RULE_BATCH_INDEX_ERROR = "persistRule.batchOfferIndex must be a non-negative integer when present";
|
|
295
294
|
export const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOfferIndex is mutually exclusive with persistRule.rule / persistRule.edited — send exactly one arm";
|
|
296
|
-
export const READ_ROOT_GRANT_ALLOW_ONLY = 'readRootGrant is only meaningful with decision
|
|
297
|
-
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
|
|
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';
|
|
298
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
|
+
}
|
|
299
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`)";
|
|
300
|
-
export function
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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 } };
|
|
304
331
|
}
|
|
305
|
-
function
|
|
306
|
-
if (
|
|
307
|
-
return
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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);
|
|
339
433
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
note = rawNote;
|
|
347
|
-
}
|
|
348
|
-
if (retired !== undefined)
|
|
349
|
-
return { ok: false, error: retired.error };
|
|
350
|
-
const deciderReading = readDecider(body, false);
|
|
351
|
-
let decider;
|
|
352
|
-
switch (deciderReading.kind) {
|
|
353
|
-
case "outside_closed_set":
|
|
354
|
-
return { ok: false, error: deciderReading.message };
|
|
355
|
-
case "accepted":
|
|
356
|
-
decider = deciderReading.decider;
|
|
357
|
-
break;
|
|
358
|
-
default: {
|
|
359
|
-
const unreachable = deciderReading;
|
|
360
|
-
throw new Error(`unhandled decider reading: ${JSON.stringify(unreachable)}`);
|
|
361
|
-
}
|
|
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
|
+
};
|
|
362
440
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const readRootGrant = rawGrant;
|
|
369
|
-
return {
|
|
370
|
-
ok: true,
|
|
371
|
-
value: d,
|
|
372
|
-
...(d !== "deny" && u !== undefined ? { updatedInput: u } : {}),
|
|
373
|
-
...(persistRule !== undefined ? { persistRule } : {}),
|
|
374
|
-
...(note !== undefined ? { note } : {}),
|
|
375
|
-
decider,
|
|
376
|
-
...(readRootGrant !== undefined ? { readRootGrant } : {}),
|
|
377
|
-
};
|
|
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");
|
|
378
446
|
}
|
|
379
|
-
|
|
447
|
+
}
|
|
448
|
+
const heldLegEntries = new WeakMap();
|
|
449
|
+
export function askDecisionRecordOf(write) {
|
|
450
|
+
return {
|
|
451
|
+
decision: write.decision,
|
|
452
|
+
...(write.actor !== undefined ? { decisionActor: write.actor } : {}),
|
|
453
|
+
decisionNote: write.note ?? null,
|
|
454
|
+
idempotencyKey: write.idempotencyKey ?? null,
|
|
455
|
+
...liveHostSource(write.decider),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function liveDecideInputOf(write) {
|
|
459
|
+
return { ...askDecisionRecordOf(write), ...(write.decision === "approve" && write.updatedInput !== undefined ? { updatedInput: write.updatedInput } : {}) };
|
|
380
460
|
}
|
|
381
461
|
export function buildUnattendedPolicyRefusal(reason) {
|
|
382
462
|
return { allow: false, settledBy: POLICY_REFUSAL_DECISION.settledBy, reason };
|
|
383
463
|
}
|
|
384
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
|
+
}
|
|
385
470
|
if (site.arm === "frame_family_undeclared") {
|
|
386
471
|
return (`UNATTENDED_APPROVAL_POLICY=deny: this run has no live approval channel — the client that started it did not declare the "${site.family}" ` +
|
|
387
472
|
"approval frame family (settings.approvalFrameFamilies), so no approval card is shown to it, and an approval request no one can answer " +
|
|
@@ -393,37 +478,34 @@ function unattendedRefusalReason(site) {
|
|
|
393
478
|
function isChildAsk(req) {
|
|
394
479
|
return req.fromSubagent === true;
|
|
395
480
|
}
|
|
396
|
-
export function approverFacingArgs(args) {
|
|
397
|
-
return boundArgs(args).args;
|
|
481
|
+
export function approverFacingArgs(args, card) {
|
|
482
|
+
return boundArgs(args, card).args;
|
|
398
483
|
}
|
|
399
|
-
function boundArgs(args) {
|
|
400
|
-
const
|
|
401
|
-
|
|
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;
|
|
402
489
|
try {
|
|
403
490
|
hasBidi = hasBidiControls(JSON.stringify(args));
|
|
404
|
-
const text = JSON.stringify(redacted);
|
|
405
|
-
if (Buffer.byteLength(text, "utf8") > MAX_APPROVAL_ARGS_BYTES)
|
|
406
|
-
return { omitted: true, hasBidi };
|
|
407
491
|
}
|
|
408
492
|
catch {
|
|
409
493
|
return { omitted: true, hasBidi: false };
|
|
410
494
|
}
|
|
411
|
-
return { args:
|
|
495
|
+
return shown.kind === "whole" ? { args: shown.value, omitted: false, hasBidi } : { omitted: true, hasBidi };
|
|
412
496
|
}
|
|
413
497
|
export function resolveStreamApprovalGate(input) {
|
|
414
498
|
if (!input.toolApprovalEnabled)
|
|
415
499
|
return { active: false, reason: "no_tool_approval" };
|
|
416
|
-
if (!input.streamApprovalEnabled)
|
|
417
|
-
return { active: false, reason: "protocol_disabled" };
|
|
418
500
|
if (!input.backend)
|
|
419
501
|
return { active: false, reason: "no_backend" };
|
|
420
502
|
if (input.checkpointStore === undefined)
|
|
421
503
|
return { active: false, reason: "no_park_facility" };
|
|
422
504
|
return { active: true, askStore: input.backend.approvalAsk(), checkpointStore: input.checkpointStore };
|
|
423
505
|
}
|
|
424
|
-
export const APPROVAL_FRAME_FAMILIES = ["
|
|
506
|
+
export const APPROVAL_FRAME_FAMILIES = ["approval_request"];
|
|
425
507
|
export function presentedApprovalFrameFamily(streamApprovalOn) {
|
|
426
|
-
return streamApprovalOn ? "approval_request" :
|
|
508
|
+
return streamApprovalOn ? "approval_request" : undefined;
|
|
427
509
|
}
|
|
428
510
|
const APPROVAL_FRAME_FAMILIES_FIELD = "settings.approvalFrameFamilies";
|
|
429
511
|
const APPROVAL_FRAME_FAMILY_WORDS = APPROVAL_FRAME_FAMILIES.map((w) => `"${w}"`).join(" | ");
|
|
@@ -470,6 +552,8 @@ export function resolveApprovalLeg(input) {
|
|
|
470
552
|
if (input.windowMs === 0 || tightDeadline)
|
|
471
553
|
return { active, unattended: { arm: "window_zero" } };
|
|
472
554
|
const family = presentedApprovalFrameFamily(active);
|
|
555
|
+
if (family === undefined)
|
|
556
|
+
return { active, unattended: { arm: "card_protocol_absent" } };
|
|
473
557
|
if (!input.declaredFamilies.has(family))
|
|
474
558
|
return { active, unattended: { arm: "frame_family_undeclared", family } };
|
|
475
559
|
return {
|
|
@@ -511,18 +595,16 @@ function settleArgsOfDecided(outcome) {
|
|
|
511
595
|
...(!outcome.allow && outcome.reason !== undefined ? { denyReason: outcome.reason } : {}),
|
|
512
596
|
};
|
|
513
597
|
}
|
|
514
|
-
function verdictOfDecided(args) {
|
|
598
|
+
function verdictOfDecided(args, readRootGrant) {
|
|
515
599
|
return {
|
|
516
600
|
by: "human",
|
|
517
601
|
allowed: args.allowed,
|
|
518
602
|
...(args.updatedInput !== undefined ? { updatedInput: args.updatedInput } : {}),
|
|
519
603
|
...(args.source !== undefined ? { hostSource: args.source } : {}),
|
|
520
604
|
...(args.denyReason !== undefined ? { denyReason: args.denyReason } : {}),
|
|
605
|
+
...(args.allowed && readRootGrant !== undefined ? { readRootGrant } : {}),
|
|
521
606
|
};
|
|
522
607
|
}
|
|
523
|
-
function settleArgsOf(outcome) {
|
|
524
|
-
return outcome === "unavailable" ? "unavailable" : settleArgsOfDecided(outcome);
|
|
525
|
-
}
|
|
526
608
|
export class ToolApprovalCoordinator {
|
|
527
609
|
als = new AsyncLocalStorage();
|
|
528
610
|
pending = new Map();
|
|
@@ -562,8 +644,8 @@ export class ToolApprovalCoordinator {
|
|
|
562
644
|
admitMaxPerOwner;
|
|
563
645
|
admitByTask = new Map();
|
|
564
646
|
admitByOwner = new Map();
|
|
565
|
-
|
|
566
|
-
|
|
647
|
+
suspendedPollCursor = { offset: 0 };
|
|
648
|
+
adoptedPollCursor = { offset: 0 };
|
|
567
649
|
editedDecisionsInFlight = new Map();
|
|
568
650
|
storeErrorWarned = false;
|
|
569
651
|
storeErrorTally = 0;
|
|
@@ -573,7 +655,6 @@ export class ToolApprovalCoordinator {
|
|
|
573
655
|
ruleScopeRootFor;
|
|
574
656
|
parkRedemptions = new Map();
|
|
575
657
|
parkRedemptionMax;
|
|
576
|
-
parkRedemptionTtlMs;
|
|
577
658
|
parkRedemptionPerPrincipalMax;
|
|
578
659
|
parkedRedeem;
|
|
579
660
|
parkedGate;
|
|
@@ -585,28 +666,27 @@ export class ToolApprovalCoordinator {
|
|
|
585
666
|
parkCommits;
|
|
586
667
|
parksAwaitingCommit = new Set();
|
|
587
668
|
constructor(opts) {
|
|
588
|
-
this.ruleConsent = opts
|
|
589
|
-
this.ruleScopeRootFor = opts
|
|
590
|
-
this.governanceAskMarks = opts
|
|
591
|
-
this.ttlMs = opts
|
|
592
|
-
this.unreachedTtlMs = opts
|
|
593
|
-
this.askStore = opts
|
|
594
|
-
this.windowMarginMs = opts
|
|
595
|
-
this.admitMaxPerTask = opts
|
|
596
|
-
this.admitMaxPerOwner = opts
|
|
597
|
-
this.unattendedPolicy = opts
|
|
598
|
-
this.parkedRedeem = opts
|
|
599
|
-
this.parkedGate = opts
|
|
600
|
-
this.parkRedemptionMax = opts
|
|
601
|
-
this.
|
|
602
|
-
this.
|
|
603
|
-
this.
|
|
604
|
-
|
|
605
|
-
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) {
|
|
606
686
|
throw new Error("ToolApprovalCoordinator: deploymentListAsks needs sessionGrants — the deployment list's standing answers live in the session exemption store");
|
|
607
687
|
}
|
|
608
|
-
this.deploymentListAsks = opts
|
|
609
|
-
this.parkCommits = opts
|
|
688
|
+
this.deploymentListAsks = opts.deploymentListAsks;
|
|
689
|
+
this.parkCommits = opts.parkCommits;
|
|
610
690
|
this.parkCommits?.onParkCommitted((calls) => this.noteParkCommitted(calls));
|
|
611
691
|
}
|
|
612
692
|
unattendedOutcome(site) {
|
|
@@ -652,9 +732,9 @@ export class ToolApprovalCoordinator {
|
|
|
652
732
|
return this.unattendedOutcome(site.arm === "frame_family_undeclared" ? { arm: site.arm, family: site.family, ...facts } : { arm: site.arm, ...facts });
|
|
653
733
|
}
|
|
654
734
|
async landUnattendedOutcome(outcome, row) {
|
|
655
|
-
|
|
656
|
-
if (outcome === "unavailable" || !store)
|
|
735
|
+
if (outcome === "unavailable")
|
|
657
736
|
return outcome;
|
|
737
|
+
const store = this.askStore;
|
|
658
738
|
const dispatch = (res, where) => {
|
|
659
739
|
if (res.claimed) {
|
|
660
740
|
this.settleSameAskIdParkRoute(row.askId, APPROVAL_ENDING_CAUSES.UNDELIVERABLE);
|
|
@@ -678,8 +758,6 @@ export class ToolApprovalCoordinator {
|
|
|
678
758
|
return read?.kind === "decided" ? read.outcome : outcome;
|
|
679
759
|
}
|
|
680
760
|
admit(taskId, owner) {
|
|
681
|
-
if (!this.askStore)
|
|
682
|
-
return () => undefined;
|
|
683
761
|
const ownerKey = JSON.stringify(["owner", owner]);
|
|
684
762
|
const taskCount = this.admitByTask.get(taskId) ?? 0;
|
|
685
763
|
const ownerCount = this.admitByOwner.get(ownerKey) ?? 0;
|
|
@@ -738,103 +816,6 @@ export class ToolApprovalCoordinator {
|
|
|
738
816
|
return a === b;
|
|
739
817
|
return a.length === b.length && a.every((d, i) => d.segment === b[i].segment && d.reason === b[i].reason);
|
|
740
818
|
}
|
|
741
|
-
static ruleOfferTextsOverSize(offer) {
|
|
742
|
-
const over = (s) => s.length > MAX_RULE_TEXT_CHARS;
|
|
743
|
-
if (offer.kind === "single")
|
|
744
|
-
return over(offer.rule) || over(offer.command);
|
|
745
|
-
return offer.rules.some((r) => {
|
|
746
|
-
switch (r.kind) {
|
|
747
|
-
case "command":
|
|
748
|
-
return over(r.rule) || over(r.command);
|
|
749
|
-
case "directoryRead":
|
|
750
|
-
return over(r.rule) || over(r.directory);
|
|
751
|
-
default:
|
|
752
|
-
throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(r)}`);
|
|
753
|
-
}
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
static ruleOfferDerivedFactsAgree(offer, cardTool) {
|
|
757
|
-
const derive = (rule) => {
|
|
758
|
-
const p = parseRuleText(rule, "allow");
|
|
759
|
-
return "rule" in p ? p.rule : undefined;
|
|
760
|
-
};
|
|
761
|
-
if (offer.kind === "single") {
|
|
762
|
-
const d = derive(offer.rule);
|
|
763
|
-
return d !== undefined && d.rule === offer.rule && d.match === offer.match && d.command === offer.command && ToolApprovalCoordinator.singleOfferBelongsOnCard(d.match, d.tool, cardTool);
|
|
764
|
-
}
|
|
765
|
-
return offer.rules.every((m) => {
|
|
766
|
-
const d = derive(m.rule);
|
|
767
|
-
if (d === undefined || d.rule !== m.rule)
|
|
768
|
-
return false;
|
|
769
|
-
switch (m.kind) {
|
|
770
|
-
case "command":
|
|
771
|
-
return d.tool === "Bash" && d.match === m.match && d.command === m.command;
|
|
772
|
-
case "directoryRead":
|
|
773
|
-
return d.tool === "Read" && d.match === "subpath" && d.command === m.directory;
|
|
774
|
-
default:
|
|
775
|
-
throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(m)}`);
|
|
776
|
-
}
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
static singleOfferBelongsOnCard(match, ruleTool, cardTool) {
|
|
780
|
-
const form = CARD_RULE_FORM_OF_MATCH[match];
|
|
781
|
-
return ruleTool === cardTool && form !== undefined && form === cardRuleFormOf(cardTool);
|
|
782
|
-
}
|
|
783
|
-
static boundRuleOfferTexts(offers, cardTool) {
|
|
784
|
-
const over = (s) => s.length > MAX_RULE_TEXT_CHARS;
|
|
785
|
-
const clipSegment = (s) => (over(s) ? s.slice(0, MAX_RULE_TEXT_CHARS) : s);
|
|
786
|
-
const out = [];
|
|
787
|
-
for (const { offer } of readRuleOfferList(offers)) {
|
|
788
|
-
if (ToolApprovalCoordinator.ruleOfferTextsOverSize(offer))
|
|
789
|
-
return undefined;
|
|
790
|
-
if (!ToolApprovalCoordinator.ruleOfferDerivedFactsAgree(offer, cardTool)) {
|
|
791
|
-
defaultLogger.warn("rule_offer_dropped_derived_facts_disagree", {
|
|
792
|
-
kind: offer.kind,
|
|
793
|
-
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?)",
|
|
794
|
-
});
|
|
795
|
-
continue;
|
|
796
|
-
}
|
|
797
|
-
switch (offer.kind) {
|
|
798
|
-
case "single":
|
|
799
|
-
out.push({ kind: "single", rule: offer.rule, match: offer.match, command: offer.command });
|
|
800
|
-
break;
|
|
801
|
-
case "batch": {
|
|
802
|
-
const rules = [];
|
|
803
|
-
for (const r of offer.rules) {
|
|
804
|
-
switch (r.kind) {
|
|
805
|
-
case "command":
|
|
806
|
-
rules.push({ kind: "command", rule: r.rule, match: r.match, command: r.command, segment: clipSegment(r.segment) });
|
|
807
|
-
break;
|
|
808
|
-
case "directoryRead":
|
|
809
|
-
rules.push({ kind: "directoryRead", rule: r.rule, directory: r.directory, segment: clipSegment(r.segment) });
|
|
810
|
-
break;
|
|
811
|
-
default:
|
|
812
|
-
throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(r)}`);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
out.push({
|
|
816
|
-
kind: "batch",
|
|
817
|
-
rules,
|
|
818
|
-
uncoveredSegments: offer.uncoveredSegments,
|
|
819
|
-
...(offer.uncoveredDetail !== undefined ? { uncoveredDetail: offer.uncoveredDetail.map((d) => ({ segment: clipSegment(d.segment), reason: d.reason })) } : {}),
|
|
820
|
-
});
|
|
821
|
-
break;
|
|
822
|
-
}
|
|
823
|
-
default:
|
|
824
|
-
throw new Error(`unknown rule-offer kind: ${JSON.stringify(offer)}`);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return out;
|
|
828
|
-
}
|
|
829
|
-
static cardBindingOf(args, offers) {
|
|
830
|
-
const valueOffers = offers.filter((o) => o.kind === "single" && CARD_RULE_FORM_OF_MATCH[o.match] !== "command");
|
|
831
|
-
if (valueOffers.length > 0)
|
|
832
|
-
return valueOffers.length === 1 && offers.length === 1 ? valueOffers[0].command : undefined;
|
|
833
|
-
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
834
|
-
return undefined;
|
|
835
|
-
const command = args.command;
|
|
836
|
-
return typeof command === "string" && command !== "" ? command : undefined;
|
|
837
|
-
}
|
|
838
819
|
buildRuleLaneMaterial(req, owner, governanceForced, sessionId) {
|
|
839
820
|
if (this.ruleConsent === undefined)
|
|
840
821
|
return undefined;
|
|
@@ -842,14 +823,10 @@ export class ToolApprovalCoordinator {
|
|
|
842
823
|
return undefined;
|
|
843
824
|
if (owner === null)
|
|
844
825
|
return undefined;
|
|
845
|
-
const
|
|
846
|
-
if (!Array.isArray(rawOffers) || rawOffers.length === 0)
|
|
847
|
-
return undefined;
|
|
848
|
-
const capped = rawOffers.length > MAX_RULE_OFFERS ? rawOffers.slice(0, MAX_RULE_OFFERS) : rawOffers;
|
|
849
|
-
const offers = ToolApprovalCoordinator.boundRuleOfferTexts(capped, req.toolName);
|
|
826
|
+
const offers = readCardRuleOffers(req.ruleOffers, req.toolName);
|
|
850
827
|
if (offers === undefined)
|
|
851
828
|
return undefined;
|
|
852
|
-
const command =
|
|
829
|
+
const command = cardBindingOf(req.args, offers);
|
|
853
830
|
if (command === undefined)
|
|
854
831
|
return undefined;
|
|
855
832
|
const boundInputHash = readBoundInputHash(req);
|
|
@@ -1016,8 +993,6 @@ export class ToolApprovalCoordinator {
|
|
|
1016
993
|
};
|
|
1017
994
|
};
|
|
1018
995
|
async sessionGrantUncontestedByRow(origin, req) {
|
|
1019
|
-
if (!this.askStore)
|
|
1020
|
-
return true;
|
|
1021
996
|
const call = { taskId: origin.taskId, legKey: origin.legKey, toolCallId: req.toolCallId, sourceSessionId: req.sourceSessionId ?? origin.taskId };
|
|
1022
997
|
try {
|
|
1023
998
|
const rows = await this.withStoreDeadline(this.askStore.listByCall(call), "listByCall(session-grant-provenance)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
@@ -1029,7 +1004,7 @@ export class ToolApprovalCoordinator {
|
|
|
1029
1004
|
}
|
|
1030
1005
|
}
|
|
1031
1006
|
async probeRowWhileYielding(askId, budgetMs) {
|
|
1032
|
-
if (
|
|
1007
|
+
if (budgetMs <= 0)
|
|
1033
1008
|
return { kind: "none" };
|
|
1034
1009
|
const release = this.admitTerminalClaim(askId);
|
|
1035
1010
|
if (release === undefined)
|
|
@@ -1067,8 +1042,6 @@ export class ToolApprovalCoordinator {
|
|
|
1067
1042
|
return { kind: "decided", outcome: replayedOutcome };
|
|
1068
1043
|
}
|
|
1069
1044
|
hasOtherSuspendedOwner(askId, self) {
|
|
1070
|
-
if (askId === undefined)
|
|
1071
|
-
return false;
|
|
1072
1045
|
for (const e of this.pendingByAskId.get(askId) ?? []) {
|
|
1073
1046
|
if (e !== self && e.suspended === true && e.settledOutcome === undefined)
|
|
1074
1047
|
return true;
|
|
@@ -1107,8 +1080,6 @@ export class ToolApprovalCoordinator {
|
|
|
1107
1080
|
let inflight = 0;
|
|
1108
1081
|
let ownInflight = 0;
|
|
1109
1082
|
for (const r of this.parkRedemptions.values()) {
|
|
1110
|
-
if (r.inflight === undefined)
|
|
1111
|
-
continue;
|
|
1112
1083
|
inflight++;
|
|
1113
1084
|
if (r.owner === owner)
|
|
1114
1085
|
ownInflight++;
|
|
@@ -1126,80 +1097,86 @@ export class ToolApprovalCoordinator {
|
|
|
1126
1097
|
};
|
|
1127
1098
|
}
|
|
1128
1099
|
openParkRedemption(askId, owner) {
|
|
1129
|
-
const opened = { owner
|
|
1130
|
-
this.parkRedemptions.delete(askId);
|
|
1100
|
+
const opened = { owner };
|
|
1131
1101
|
this.parkRedemptions.set(askId, opened);
|
|
1132
1102
|
return opened;
|
|
1133
1103
|
}
|
|
1134
|
-
retainParkRedemption(askId, memo) {
|
|
1135
|
-
const now = Date.now();
|
|
1136
|
-
const effectivePerPrincipalMax = Math.min(this.parkRedemptionPerPrincipalMax, this.parkRedemptionMax);
|
|
1137
|
-
let receipts = 0;
|
|
1138
|
-
let ownReceipts = 0;
|
|
1139
|
-
let ownExpired;
|
|
1140
|
-
let ownOldest;
|
|
1141
|
-
let globalExpired;
|
|
1142
|
-
for (const [k, r] of this.parkRedemptions) {
|
|
1143
|
-
if (k === askId || r.redeemed === undefined || r.inflight !== undefined)
|
|
1144
|
-
continue;
|
|
1145
|
-
receipts++;
|
|
1146
|
-
const expired = now - r.stampedAtMs > this.parkRedemptionTtlMs;
|
|
1147
|
-
if (globalExpired === undefined && expired)
|
|
1148
|
-
globalExpired = k;
|
|
1149
|
-
if (r.owner !== memo.owner)
|
|
1150
|
-
continue;
|
|
1151
|
-
ownReceipts++;
|
|
1152
|
-
if (ownOldest === undefined)
|
|
1153
|
-
ownOldest = k;
|
|
1154
|
-
if (ownExpired === undefined && expired)
|
|
1155
|
-
ownExpired = k;
|
|
1156
|
-
}
|
|
1157
|
-
if (ownReceipts >= effectivePerPrincipalMax) {
|
|
1158
|
-
const victim = ownExpired ?? ownOldest;
|
|
1159
|
-
if (victim !== undefined)
|
|
1160
|
-
this.parkRedemptions.delete(victim);
|
|
1161
|
-
}
|
|
1162
|
-
else if (receipts >= this.parkRedemptionMax) {
|
|
1163
|
-
if (globalExpired === undefined) {
|
|
1164
|
-
recordFailOpen("server.approval.park-redemption-cap-refused", `owner=${memo.owner ?? "<none>"}`);
|
|
1165
|
-
if (this.parkRedemptions.get(askId) === memo)
|
|
1166
|
-
this.parkRedemptions.delete(askId);
|
|
1167
|
-
return false;
|
|
1168
|
-
}
|
|
1169
|
-
this.parkRedemptions.delete(globalExpired);
|
|
1170
|
-
}
|
|
1171
|
-
memo.stampedAtMs = now;
|
|
1172
|
-
return true;
|
|
1173
|
-
}
|
|
1174
1104
|
lookupParkRedemption(askId) {
|
|
1175
|
-
|
|
1176
|
-
if (r === undefined)
|
|
1177
|
-
return undefined;
|
|
1178
|
-
if (r.inflight === undefined && Date.now() - r.stampedAtMs > this.parkRedemptionTtlMs) {
|
|
1179
|
-
this.parkRedemptions.delete(askId);
|
|
1180
|
-
return undefined;
|
|
1181
|
-
}
|
|
1182
|
-
return r;
|
|
1105
|
+
return this.parkRedemptions.get(askId);
|
|
1183
1106
|
}
|
|
1184
|
-
notifyExternalDecision(askId, outcome) {
|
|
1107
|
+
notifyExternalDecision(askId, outcome, readRootGrant) {
|
|
1185
1108
|
const entries = this.pendingByAskId.get(askId);
|
|
1186
1109
|
if (!entries)
|
|
1187
1110
|
return { settled: 0 };
|
|
1188
1111
|
const args = settleArgsOfDecided(outcome);
|
|
1189
1112
|
let settled = 0;
|
|
1190
1113
|
for (const entry of [...entries]) {
|
|
1191
|
-
entry.settle(verdictOfDecided(args));
|
|
1114
|
+
entry.settle(verdictOfDecided(args, readRootGrant));
|
|
1192
1115
|
if (entry.adopted !== true)
|
|
1193
1116
|
settled += 1;
|
|
1194
1117
|
}
|
|
1195
1118
|
return { settled };
|
|
1196
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
|
+
}
|
|
1197
1175
|
emitRevokeTo(ctxs, frame) {
|
|
1198
1176
|
if (frame.askIds.length === 0)
|
|
1199
1177
|
return;
|
|
1200
1178
|
for (const c of ctxs)
|
|
1201
|
-
|
|
1202
|
-
void deliverHitlNotice(c.transport, frame);
|
|
1179
|
+
void deliverHitlNotice(c.transport, frame);
|
|
1203
1180
|
}
|
|
1204
1181
|
emitRevoke(frame, target) {
|
|
1205
1182
|
const targets = new Set();
|
|
@@ -1293,7 +1270,7 @@ export class ToolApprovalCoordinator {
|
|
|
1293
1270
|
const aliveCtxs = ctxs.filter((c) => !c.abortSignal?.aborted);
|
|
1294
1271
|
const reachability = aliveCtxs.length === 0 ? "unreached" : "reachable";
|
|
1295
1272
|
if (reachability === "unreached") {
|
|
1296
|
-
if (!this.unattendedRoutesToPark
|
|
1273
|
+
if (!this.unattendedRoutesToPark) {
|
|
1297
1274
|
return this.unattendedOutcome({ arm: "no_live_target", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
|
|
1298
1275
|
}
|
|
1299
1276
|
}
|
|
@@ -1311,19 +1288,18 @@ export class ToolApprovalCoordinator {
|
|
|
1311
1288
|
const id = uuidv7();
|
|
1312
1289
|
const child = isChildAsk(req);
|
|
1313
1290
|
const carrierLeg = child ? undefined : aliveCtxs.find((c) => c.taskId === origin.taskId);
|
|
1314
|
-
const bounded = boundArgs(req.args);
|
|
1315
1291
|
let effectiveGovernanceForced = governanceForced;
|
|
1316
1292
|
let ruleLaneMaterial = this.buildRuleLaneMaterial(req, origin.owner, governanceForced, origin.sessionId);
|
|
1317
1293
|
const mandateWord = readClosedWordSeat(req, "mandate");
|
|
1318
1294
|
const noRulingCause = readClosedWordSeat(req, "classifierNoRuling");
|
|
1319
1295
|
const grantAbsence = readClosedWordSeat(req, "boundGrantAbsence");
|
|
1320
1296
|
const cardWord = readApprovalCard(req.approvalCard);
|
|
1297
|
+
const bounded = boundArgs(req.args, cardWord);
|
|
1321
1298
|
const originWord = typeof req.origin === "string" && req.origin.length > 0 ? req.origin : undefined;
|
|
1322
1299
|
const askClassPair = readAskClassPair(req);
|
|
1323
1300
|
const askReasonsOfReq = readAskReasons(req);
|
|
1324
|
-
const requester = extensionRequesterOf(req.requestedBy, "
|
|
1301
|
+
const requester = extensionRequesterOf(req.requestedBy, "approval_request");
|
|
1325
1302
|
const frame = {
|
|
1326
|
-
type: "tool_approval",
|
|
1327
1303
|
approvalId: id,
|
|
1328
1304
|
toolName: req.toolName,
|
|
1329
1305
|
...(typeof req.toolCallId === "string" && req.toolCallId !== "" ? { toolCallId: req.toolCallId } : {}),
|
|
@@ -1371,9 +1347,12 @@ export class ToolApprovalCoordinator {
|
|
|
1371
1347
|
const windowBudgetMs = reachability === "unreached" ? this.unreachedTtlMs : this.ttlMs;
|
|
1372
1348
|
const effectiveWindowMs = effectiveAskWindowMs(windowBudgetMs, this.windowMarginMs, origin.legDeadlineMonotonic, performance.now());
|
|
1373
1349
|
const expiresAtMs = Math.ceil(Date.now() + effectiveWindowMs);
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
const
|
|
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));
|
|
1377
1356
|
let cardForFrame = card;
|
|
1378
1357
|
let persistedExpiresAtMs = expiresAtMs;
|
|
1379
1358
|
let factsOfRecord;
|
|
@@ -1384,11 +1363,10 @@ export class ToolApprovalCoordinator {
|
|
|
1384
1363
|
let abandonedAs;
|
|
1385
1364
|
const attemptCreatedAtMs = Date.now();
|
|
1386
1365
|
const finishAbandonedRow = (orphan) => {
|
|
1387
|
-
|
|
1388
|
-
if (!store || abandonedAs === undefined)
|
|
1366
|
+
if (abandonedAs === undefined)
|
|
1389
1367
|
return undefined;
|
|
1390
1368
|
if (abandonedAs === "void") {
|
|
1391
|
-
void
|
|
1369
|
+
void this.askStore
|
|
1392
1370
|
.transitionAsk(orphan.askId, "STREAM_PENDING", "VOID", { updatedAtMs: Date.now() })
|
|
1393
1371
|
.catch((err) => this.noteStoreError(err, "transitionAsk(abandon-late-ensure)"));
|
|
1394
1372
|
return undefined;
|
|
@@ -1405,142 +1383,131 @@ export class ToolApprovalCoordinator {
|
|
|
1405
1383
|
? this.unattendedOutcome({ arm: "replayed_parked", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability })
|
|
1406
1384
|
: replayed;
|
|
1407
1385
|
};
|
|
1408
|
-
|
|
1409
|
-
const
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
createdAtMs: attemptCreatedAtMs,
|
|
1432
|
-
}), "ensureAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
|
|
1433
|
-
if (late.row.state !== "STREAM_PENDING") {
|
|
1434
|
-
lateTerminalRow = late.row;
|
|
1435
|
-
this.dispatchClaimTruth(late.row.askId, this.classifyClaimLoss(late.row.askId, projectAskTerminalClaimTruth(late.row), "late:ensureAsk"), APPROVAL_ENDING_CAUSES.UNDELIVERABLE);
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
|
-
lateEnsuredRow = late.inserted ? { askId: late.row.askId, batchId: late.row.batchId } : undefined;
|
|
1439
|
-
if (lateEnsuredRow !== undefined)
|
|
1440
|
-
void finishAbandonedRow(lateEnsuredRow);
|
|
1441
|
-
});
|
|
1442
|
-
batchId = existingOrCreated.row.batchId;
|
|
1443
|
-
if (existingOrCreated.row.state !== "STREAM_PENDING") {
|
|
1444
|
-
releaseAdmission();
|
|
1445
|
-
return replayTerminalRow(existingOrCreated.row);
|
|
1446
|
-
}
|
|
1447
|
-
const persistedEnvelope = readStoredCardEnvelope(existingOrCreated.row.cardJson);
|
|
1448
|
-
const maxMintableWindowMs = Math.max(effectiveWindowMs, this.unreachedTtlMs);
|
|
1449
|
-
const sanePersistedDeadline = Number.isFinite(existingOrCreated.row.expiresAtMs) && existingOrCreated.row.expiresAtMs <= Date.now() + maxMintableWindowMs + DURABLE_CALL_TIMEOUT_MS;
|
|
1450
|
-
if (persistedEnvelope === undefined || !sanePersistedDeadline) {
|
|
1451
|
-
this.noteStoreError(new Error(`persisted approval row is unusable (envelopeOk=${persistedEnvelope !== undefined}, deadlineOk=${sanePersistedDeadline}, ` +
|
|
1452
|
-
`origin=${existingOrCreated.inserted ? "row-this-call-just-inserted" : "row-hit-idempotently"})`), "ensureAsk(persisted-row-unusable)");
|
|
1453
|
-
releaseAdmission();
|
|
1454
|
-
const unattended = this.unattendedOutcome({ arm: "persisted_row_unusable", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
|
|
1455
|
-
return existingOrCreated.inserted ? this.landUnattendedOutcome(unattended, existingOrCreated.row) : unattended;
|
|
1456
|
-
}
|
|
1457
|
-
persistedExpiresAtMs = existingOrCreated.row.expiresAtMs;
|
|
1458
|
-
entryExecutionLane = existingOrCreated.row.executionLane;
|
|
1459
|
-
cardForFrame = persistedEnvelope.card;
|
|
1460
|
-
recordCard = persistedEnvelope.card;
|
|
1461
|
-
factsOfRecord = existingOrCreated.inserted ? undefined : persistedEnvelope.liveFacts;
|
|
1462
|
-
if (factsOfRecord !== undefined)
|
|
1463
|
-
adoptStoredLiveFacts(frame, factsOfRecord);
|
|
1464
|
-
if (cardForFrame.governanceForced === true)
|
|
1465
|
-
frame.governanceForced = true;
|
|
1466
|
-
else
|
|
1467
|
-
delete frame.governanceForced;
|
|
1468
|
-
if (cardForFrame.allowRuleShadowed !== undefined)
|
|
1469
|
-
frame.allowRuleShadowed = cardForFrame.allowRuleShadowed;
|
|
1470
|
-
else
|
|
1471
|
-
delete frame.allowRuleShadowed;
|
|
1472
|
-
if (cardForFrame.inputHasBidi === true)
|
|
1473
|
-
frame.inputHasBidi = true;
|
|
1474
|
-
else
|
|
1475
|
-
delete frame.inputHasBidi;
|
|
1476
|
-
if (cardForFrame.denialLimitFallback !== undefined)
|
|
1477
|
-
frame.denialLimitFallback = cardForFrame.denialLimitFallback;
|
|
1478
|
-
else
|
|
1479
|
-
delete frame.denialLimitFallback;
|
|
1480
|
-
if (cardForFrame.governanceForced === true && !effectiveGovernanceForced) {
|
|
1481
|
-
effectiveGovernanceForced = true;
|
|
1482
|
-
ruleLaneMaterial = undefined;
|
|
1483
|
-
delete frame.ruleOffers;
|
|
1484
|
-
if (cardForFrame.ruleOffers !== undefined) {
|
|
1485
|
-
const { ruleOffers: _dropped, ...rest } = cardForFrame;
|
|
1486
|
-
cardForFrame = rest;
|
|
1487
|
-
}
|
|
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;
|
|
1488
1409
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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;
|
|
1497
1459
|
}
|
|
1498
1460
|
}
|
|
1499
|
-
|
|
1500
|
-
this.noteStoreError(
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
confirmedRowExists = confirmedRow !== null;
|
|
1507
|
-
if (confirmedRow !== null)
|
|
1508
|
-
batchId = confirmedRow.batchId;
|
|
1509
|
-
if (confirmedRow !== null)
|
|
1510
|
-
recordCard = storedPersonDemandRecord(confirmedRow.cardJson);
|
|
1511
|
-
indeterminate = false;
|
|
1512
|
-
break;
|
|
1513
|
-
}
|
|
1514
|
-
catch (readErr) {
|
|
1515
|
-
this.noteStoreError(readErr, "getAsk(ensureAsk-confirm)");
|
|
1516
|
-
if (attempt < DURABLE_CONVERGE_BACKOFF_MS.length)
|
|
1517
|
-
await new Promise((r) => setTimeout(r, DURABLE_CONVERGE_BACKOFF_MS[attempt]).unref?.());
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
if (lateTerminalRow !== undefined) {
|
|
1521
|
-
releaseAdmission();
|
|
1522
|
-
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;
|
|
1523
1468
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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;
|
|
1529
1485
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
const unattended = this.unattendedOutcome({ arm: "no_row_no_presenter", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
|
|
1535
|
-
const landed = abandonRow(unattended);
|
|
1536
|
-
releaseAdmission();
|
|
1537
|
-
return landed ?? unattended;
|
|
1538
|
-
}
|
|
1539
|
-
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?.());
|
|
1540
1490
|
}
|
|
1541
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
|
+
}
|
|
1542
1508
|
}
|
|
1543
|
-
const wireApprovalId = askId
|
|
1509
|
+
const wireApprovalId = askId;
|
|
1510
|
+
const rowCard = cardForFrame;
|
|
1544
1511
|
frame.approvalId = wireApprovalId;
|
|
1545
1512
|
let done = false;
|
|
1546
1513
|
let emitFailed = false;
|
|
@@ -1600,27 +1567,23 @@ export class ToolApprovalCoordinator {
|
|
|
1600
1567
|
for (const [sig, fn] of abortListeners)
|
|
1601
1568
|
sig.removeEventListener("abort", fn);
|
|
1602
1569
|
this.pending.delete(id);
|
|
1603
|
-
if (windowRouteUnavailable &&
|
|
1570
|
+
if (windowRouteUnavailable && this.unattendedRoutesToPark) {
|
|
1604
1571
|
parkRouted = true;
|
|
1605
1572
|
}
|
|
1606
1573
|
let lastLiveRegistration = false;
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
lastLiveRegistration = true;
|
|
1614
|
-
}
|
|
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;
|
|
1615
1580
|
}
|
|
1616
1581
|
}
|
|
1617
1582
|
lastOutcome = outcome;
|
|
1618
1583
|
lastCause = verdict.by === "system" ? verdict.cause : undefined;
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
this.notifyPresentedCardWatchers(askId, completeFrame);
|
|
1623
|
-
}
|
|
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);
|
|
1624
1587
|
releaseAdmission();
|
|
1625
1588
|
if (selfEntry)
|
|
1626
1589
|
selfEntry.settledOutcome = outcome;
|
|
@@ -1652,8 +1615,6 @@ export class ToolApprovalCoordinator {
|
|
|
1652
1615
|
resolveAllowed({ allowed, ...(allowed && updatedInput !== undefined ? { updatedInput } : {}), ...(allowed && readRootGrant !== undefined ? { readRootGrant } : {}) });
|
|
1653
1616
|
};
|
|
1654
1617
|
const broadcastClaimTruth = (read, armCause) => {
|
|
1655
|
-
if (askId === undefined)
|
|
1656
|
-
return;
|
|
1657
1618
|
this.dispatchClaimTruth(askId, read, armCause);
|
|
1658
1619
|
};
|
|
1659
1620
|
const settleFromClaimLoss = (read, armCause) => {
|
|
@@ -1682,8 +1643,6 @@ export class ToolApprovalCoordinator {
|
|
|
1682
1643
|
}
|
|
1683
1644
|
};
|
|
1684
1645
|
const lateClaimLossSettle = (current, intent, armCause) => {
|
|
1685
|
-
if (askId === undefined)
|
|
1686
|
-
return;
|
|
1687
1646
|
const read = this.classifyClaimLoss(askId, current, `late:${intent}`);
|
|
1688
1647
|
if (done) {
|
|
1689
1648
|
broadcastClaimTruth(read, armCause);
|
|
@@ -1700,10 +1659,6 @@ export class ToolApprovalCoordinator {
|
|
|
1700
1659
|
return this.withStoreDeadline(raw, label, timeoutMs, late);
|
|
1701
1660
|
};
|
|
1702
1661
|
const resolveLocalOutcome = async (arm) => {
|
|
1703
|
-
if (askId === undefined) {
|
|
1704
|
-
arm.settleByIntent();
|
|
1705
|
-
return;
|
|
1706
|
-
}
|
|
1707
1662
|
const pinnedAskId = askId;
|
|
1708
1663
|
let lastLoss;
|
|
1709
1664
|
const ladderStartedAt = Date.now();
|
|
@@ -1799,10 +1754,6 @@ export class ToolApprovalCoordinator {
|
|
|
1799
1754
|
};
|
|
1800
1755
|
const windowExpired = (hostSelfReport, cause) => {
|
|
1801
1756
|
void (async () => {
|
|
1802
|
-
if (!this.askStore || askId === undefined || batchId === undefined) {
|
|
1803
|
-
settleParkRouteIfUnsettled(hostSelfReport, cause);
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
1757
|
const store = this.askStore;
|
|
1807
1758
|
const pinnedAskId = askId;
|
|
1808
1759
|
const pinnedBatchId = batchId;
|
|
@@ -1833,19 +1784,11 @@ export class ToolApprovalCoordinator {
|
|
|
1833
1784
|
const forceParkNow = (cause) => {
|
|
1834
1785
|
if (done)
|
|
1835
1786
|
return;
|
|
1836
|
-
if (!this.askStore || askId === undefined || batchId === undefined) {
|
|
1837
|
-
settleParkRouteIfUnsettled(undefined, cause);
|
|
1838
|
-
return;
|
|
1839
|
-
}
|
|
1840
1787
|
windowExpired(undefined, cause);
|
|
1841
1788
|
};
|
|
1842
|
-
timer = setTimeout(() => windowExpired("timeout", APPROVAL_ENDING_CAUSES.TTL_EXPIRED),
|
|
1789
|
+
timer = setTimeout(() => windowExpired("timeout", APPROVAL_ENDING_CAUSES.TTL_EXPIRED), Math.max(0, persistedExpiresAtMs - Date.now()));
|
|
1843
1790
|
timer.unref?.();
|
|
1844
1791
|
const runCancel = async (cause) => {
|
|
1845
|
-
if (!this.askStore || askId === undefined || batchId === undefined) {
|
|
1846
|
-
settle({ by: "system", cause });
|
|
1847
|
-
return;
|
|
1848
|
-
}
|
|
1849
1792
|
if (this.hasOtherSuspendedOwner(askId, selfEntry)) {
|
|
1850
1793
|
settle({ by: "system", cause });
|
|
1851
1794
|
return;
|
|
@@ -1907,31 +1850,27 @@ export class ToolApprovalCoordinator {
|
|
|
1907
1850
|
const legGrantBinding = grantBindingOf(req);
|
|
1908
1851
|
const entryGrantBinding = factsOfRecord === undefined ? legGrantBinding : agreedGrantBinding(legGrantBinding, grantBindingOf(factsOfRecord));
|
|
1909
1852
|
const entryMandated = req.mandated === true || factsOfRecord?.mandated === true;
|
|
1910
|
-
const entryDemandsAPerson = askDemandsAPerson(req, recordCard) || [...(
|
|
1911
|
-
|
|
1853
|
+
const entryDemandsAPerson = askDemandsAPerson(req, recordCard) || [...(this.pendingByAskId.get(askId) ?? [])].some((s) => s.requiresRealApproval === true);
|
|
1854
|
+
const presentedCard = projectDecisionFacts(pendingEntryStandingAnswerFacts({
|
|
1912
1855
|
...(effectiveGovernanceForced ? { governanceForced: true } : {}),
|
|
1913
1856
|
...(entryDemandsAPerson ? { requiresRealApproval: true } : {}),
|
|
1914
1857
|
...(entryMandated ? { mandated: true } : {}),
|
|
1915
1858
|
grantBinding: entryGrantBinding,
|
|
1916
1859
|
classes: entryClasses,
|
|
1917
1860
|
...(entryDeploymentListQuestion ? { deploymentListQuestion: true } : {}),
|
|
1918
|
-
}), origin, frame,
|
|
1919
|
-
const cardFrame = askId
|
|
1920
|
-
? buildApprovalRequestFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, card: cardForFrame, expiresAtMs: persistedExpiresAtMs, nowMs: Date.now(), live: frame })
|
|
1921
|
-
: 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 });
|
|
1922
1863
|
const pendingEntry = {
|
|
1923
1864
|
settle,
|
|
1924
1865
|
forceParkNow,
|
|
1925
1866
|
settleParkRoute: (cause) => settleParkRouteIfUnsettled(undefined, cause),
|
|
1926
|
-
owner: origin.owner,
|
|
1927
|
-
ownerReadSeq: this.rowReadSeq,
|
|
1928
1867
|
...(primary ? { ctxTaskId: primary.taskId, originCtx: primary } : {}),
|
|
1929
1868
|
...(reachability === "unreached" ? { suspended: true } : {}),
|
|
1930
|
-
...(child ? {
|
|
1869
|
+
...(child ? { sourceSessionId: req.sourceSessionId } : primary ? { targetCtxs: new Set(aliveCtxs), onTargetGone } : {}),
|
|
1931
1870
|
...(origin.sessionId ? { sessionId: origin.sessionId } : {}),
|
|
1932
1871
|
...(origin.incarnation !== undefined ? { incarnation: origin.incarnation } : {}),
|
|
1933
1872
|
frame,
|
|
1934
|
-
|
|
1873
|
+
cardFrame,
|
|
1935
1874
|
approvalId: wireApprovalId,
|
|
1936
1875
|
registrationId: id,
|
|
1937
1876
|
toolName: req.toolName,
|
|
@@ -1941,31 +1880,29 @@ export class ToolApprovalCoordinator {
|
|
|
1941
1880
|
registeredAtMs: Date.now(),
|
|
1942
1881
|
expiresAtMs: persistedExpiresAtMs,
|
|
1943
1882
|
executionLane: entryExecutionLane,
|
|
1944
|
-
...(ruleLaneMaterial !== undefined ? { ruleLane: ruleLaneMaterial } : {}),
|
|
1945
1883
|
...(effectiveGovernanceForced ? { governanceForced: true } : {}),
|
|
1946
1884
|
...(entryDemandsAPerson ? { requiresRealApproval: true } : {}),
|
|
1947
1885
|
...(entryMandated ? { mandated: true } : {}),
|
|
1948
|
-
|
|
1886
|
+
askId,
|
|
1887
|
+
batchId,
|
|
1949
1888
|
};
|
|
1950
1889
|
selfEntry = pendingEntry;
|
|
1951
1890
|
this.pending.set(id, pendingEntry);
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
s.cardFrame = { ...s.cardFrame, card };
|
|
1968
|
-
}
|
|
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 };
|
|
1969
1906
|
}
|
|
1970
1907
|
}
|
|
1971
1908
|
this.askAudit?.recordAsk({
|
|
@@ -1988,73 +1925,49 @@ export class ToolApprovalCoordinator {
|
|
|
1988
1925
|
frame.serverNowMs = serverNowMs;
|
|
1989
1926
|
frame.expiresInMs = Math.max(0, persistedExpiresAtMs - serverNowMs);
|
|
1990
1927
|
}
|
|
1991
|
-
const cardCtxs = aliveCtxs
|
|
1992
|
-
const
|
|
1993
|
-
if (
|
|
1994
|
-
defaultLogger.warn("approval card leg cannot present this ask: it has no confirmed durable identity (no approval_request frame is minted without one)", {
|
|
1995
|
-
taskId: origin.taskId,
|
|
1996
|
-
approvalId: wireApprovalId,
|
|
1997
|
-
toolName: req.toolName,
|
|
1998
|
-
});
|
|
1999
|
-
}
|
|
2000
|
-
const cardRace = cardFrame !== undefined && cardCtxs.length > 0
|
|
2001
|
-
? createHitlOpenRace({ targets: cardCtxs.map((c) => ({ transport: c.transport, frame: cardFrame })), settled: allowedP })
|
|
2002
|
-
: undefined;
|
|
2003
|
-
const legacyRace = legacyCtxs.length > 0 ? createHitlOpenRace({ targets: legacyCtxs.map((c) => ({ transport: c.transport, frame })), settled: allowedP }) : undefined;
|
|
2004
|
-
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)
|
|
2005
1931
|
cardCtxs.forEach((c, i) => this.notePresentedCardRegistration(cardFrame.askId, c.transport, cardRace.acceptedLive(i)));
|
|
2006
1932
|
const emitAllP = (async () => {
|
|
2007
|
-
const
|
|
2008
|
-
const anySucceeded = reachability === "unreached" || this.hasOtherSuspendedOwner(askId, selfEntry) ||
|
|
1933
|
+
const landing = await cardRace?.landing;
|
|
1934
|
+
const anySucceeded = reachability === "unreached" || this.hasOtherSuspendedOwner(askId, selfEntry) || landing === "delivered";
|
|
2009
1935
|
emitSettled = true;
|
|
2010
1936
|
if (!anySucceeded && !done) {
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
return;
|
|
2018
|
-
emitFailed = true;
|
|
2019
|
-
settle({ by: "system", cause: APPROVAL_ENDING_CAUSES.BROADCAST_LOST });
|
|
2020
|
-
};
|
|
2021
|
-
await resolveLocalOutcome({
|
|
2022
|
-
intent: "unreachable",
|
|
2023
|
-
cause: APPROVAL_ENDING_CAUSES.BROADCAST_LOST,
|
|
2024
|
-
storeLabel: "claimTerminal(emit-failed)",
|
|
2025
|
-
claim: (timeoutMs) => boundedClaim(() => settleByPolicy(store, { askId: pinnedAskId, batchId: pinnedBatchId }, this.unattendedPolicy, APPROVAL_ENDING_CAUSES.BROADCAST_LOST), "claimTerminal(emit-failed)", timeoutMs, (late) => {
|
|
2026
|
-
if (!late.claimed) {
|
|
2027
|
-
lateClaimLossSettle(late.current, "unreachable", APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
|
|
2028
|
-
return;
|
|
2029
|
-
}
|
|
2030
|
-
this.settleSameAskIdParkRoute(pinnedAskId, APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
|
|
2031
|
-
this.settleVoidedSiblings(late.voidedSiblings, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN);
|
|
2032
|
-
}),
|
|
2033
|
-
onWon: (res) => {
|
|
2034
|
-
settleUnreachable();
|
|
2035
|
-
this.settleSameAskIdParkRoute(pinnedAskId, APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
|
|
2036
|
-
this.settleVoidedSiblings(res.voidedSiblings, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN);
|
|
2037
|
-
},
|
|
2038
|
-
settleByIntent: settleUnreachable,
|
|
2039
|
-
});
|
|
2040
|
-
}
|
|
2041
|
-
else if (!done) {
|
|
1937
|
+
const store = this.askStore;
|
|
1938
|
+
const pinnedAskId = askId;
|
|
1939
|
+
const pinnedBatchId = batchId;
|
|
1940
|
+
const settleUnreachable = () => {
|
|
1941
|
+
if (done)
|
|
1942
|
+
return;
|
|
2042
1943
|
emitFailed = true;
|
|
2043
1944
|
settle({ by: "system", cause: APPROVAL_ENDING_CAUSES.BROADCAST_LOST });
|
|
2044
|
-
}
|
|
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
|
+
});
|
|
2045
1965
|
}
|
|
2046
1966
|
})();
|
|
2047
1967
|
await Promise.race([emitAllP, allowedP]);
|
|
2048
1968
|
const settled = await allowedP;
|
|
2049
1969
|
if (cardRace !== undefined && completeFrame !== undefined)
|
|
2050
1970
|
cardRace.close(completeFrame);
|
|
2051
|
-
legacyRace?.close({
|
|
2052
|
-
type: "tool_approval_complete",
|
|
2053
|
-
approvalId: wireApprovalId,
|
|
2054
|
-
outcome: lastOutcome,
|
|
2055
|
-
...(lastCause !== undefined ? { cause: lastCause } : {}),
|
|
2056
|
-
...(parkRouted ? { parked: true } : {}),
|
|
2057
|
-
});
|
|
2058
1971
|
if (emitFailed)
|
|
2059
1972
|
return this.unattendedOutcome({ arm: "emit_failed", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
|
|
2060
1973
|
if (windowRouteUnavailable)
|
|
@@ -2063,110 +1976,6 @@ export class ToolApprovalCoordinator {
|
|
|
2063
1976
|
return this.unattendedOutcome({ arm: "emit_unsettled", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
|
|
2064
1977
|
return shapeOutcome(settled);
|
|
2065
1978
|
}
|
|
2066
|
-
respond(id, principal, body, httpReq, deployment) {
|
|
2067
|
-
const parsed = parseToolApprovalResponse(body);
|
|
2068
|
-
if (!parsed.ok)
|
|
2069
|
-
return { status: 400, body: refusalBody({ errorCode: "request.body_shape", message: parsed.error, facts: { ...(parsed.field !== undefined ? { field: parsed.field } : {}) } }) };
|
|
2070
|
-
if (parsed.readRootGrant !== undefined && deployment?.readRootGrantAccepted !== true) {
|
|
2071
|
-
return { status: 400, body: refusalBody({ errorCode: "request.body_shape", message: READ_ROOT_GRANT_NOT_ACCEPTED }) };
|
|
2072
|
-
}
|
|
2073
|
-
const entry = this.liveEntryFor(id);
|
|
2074
|
-
if (entry?.adopted === true)
|
|
2075
|
-
return this.respondByRow(entry, id, principal, parsed, httpReq);
|
|
2076
|
-
if (entry !== undefined && askOwnerAdmitsAnswer(entry.owner, principal))
|
|
2077
|
-
return this.answerRegistration(entry, entry.owner, id, principal, parsed, httpReq);
|
|
2078
|
-
if (entry !== undefined && this.registrationHasRow(entry))
|
|
2079
|
-
return this.respondByRow(entry, id, principal, parsed, httpReq);
|
|
2080
|
-
return this.respondLate(id, principal, parsed, httpReq);
|
|
2081
|
-
}
|
|
2082
|
-
registrationHasRow(entry) {
|
|
2083
|
-
return this.askStore !== undefined && entry.askId !== undefined;
|
|
2084
|
-
}
|
|
2085
|
-
async respondByRow(entry, id, principal, parsed, httpReq) {
|
|
2086
|
-
const store = this.askStore;
|
|
2087
|
-
if (!store)
|
|
2088
|
-
return this.askNotVisible();
|
|
2089
|
-
const row = await this.askRowFollowingOwner(store, id, "getAsk(respond-by-row)");
|
|
2090
|
-
if (!askRowVisibleTo(row, principal))
|
|
2091
|
-
return this.askNotVisible();
|
|
2092
|
-
if (row.state !== "STREAM_PENDING")
|
|
2093
|
-
return this.dispatchRowTruth(row, id, parsed, httpReq, principal);
|
|
2094
|
-
return this.answerRegistration(entry, row.owner, id, principal, parsed, httpReq);
|
|
2095
|
-
}
|
|
2096
|
-
answerRegistration(entry, judgedOwner, id, principal, parsed, httpReq) {
|
|
2097
|
-
const preRule = this.precheckEditedRuleBeforeDecision(entry, judgedOwner, parsed);
|
|
2098
|
-
if (preRule !== undefined)
|
|
2099
|
-
return preRule;
|
|
2100
|
-
if (parsed.value !== "deny" && policyCannotAnswer(heldAskOf(entry.requiresRealApproval === true), parsed.decider.decidedBy)) {
|
|
2101
|
-
return { status: 400, body: refusalBody({ errorCode: "request.body_shape", message: POLICY_CANNOT_ALLOW_PERSON_ASK }) };
|
|
2102
|
-
}
|
|
2103
|
-
const ruleEligibility = { eligible: true };
|
|
2104
|
-
const store = this.askStore;
|
|
2105
|
-
if (store && entry.askId !== undefined && entry.batchId !== undefined) {
|
|
2106
|
-
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));
|
|
2107
|
-
}
|
|
2108
|
-
return this.afterLiveDecision(this.finishRespond(entry, judgedOwner, parsed), entry, judgedOwner, parsed, principal, ruleEligibility);
|
|
2109
|
-
}
|
|
2110
|
-
afterLiveDecision(decided, entry, judgedOwner, parsed, principal, ruleEligibility) {
|
|
2111
|
-
const settled = this.recordDeploymentListAnswer(decided, entry, judgedOwner, parsed, principal, ruleEligibility);
|
|
2112
|
-
if (parsed.persistRule === undefined)
|
|
2113
|
-
return settled;
|
|
2114
|
-
return this.persistRuleAfterDecision(settled, entry, judgedOwner, parsed.persistRule, parsed.updatedInput !== undefined, ruleEligibility);
|
|
2115
|
-
}
|
|
2116
|
-
recordDeploymentListAnswer(decided, entry, judgedOwner, parsed, principal, eligibility) {
|
|
2117
|
-
const store = this.sessionGrants;
|
|
2118
|
-
const answer = liveSessionAnswer(pendingEntryStandingAnswerFacts(entry), entry);
|
|
2119
|
-
if (parsed.value !== "allow_session" || answer.home !== "server_store" || store === undefined)
|
|
2120
|
-
return decided;
|
|
2121
|
-
const sessionId = answer.sessionId;
|
|
2122
|
-
return (async () => {
|
|
2123
|
-
const result = await decided;
|
|
2124
|
-
if (result.status !== 200)
|
|
2125
|
-
return result;
|
|
2126
|
-
if (!eligibility.eligible)
|
|
2127
|
-
return { ...result, body: { ...result.body, rememberApplied: false } };
|
|
2128
|
-
let rememberApplied = false;
|
|
2129
|
-
try {
|
|
2130
|
-
await this.withStoreDeadline(store.grant(sessionId, entry.toolName, principal ?? judgedOwner), "sessionGrants.grant(deployment-list)", DURABLE_CALL_TIMEOUT_MS);
|
|
2131
|
-
rememberApplied = true;
|
|
2132
|
-
defaultLogger.info("approval_exemption_granted", { sessionId, toolName: entry.toolName, grantedBy: principal ?? judgedOwner });
|
|
2133
|
-
}
|
|
2134
|
-
catch (err) {
|
|
2135
|
-
defaultLogger.warn("approval_exemption_grant_failed", { sessionId, toolName: entry.toolName, error: String(err).slice(0, 200) });
|
|
2136
|
-
}
|
|
2137
|
-
return { ...result, body: { ...result.body, rememberApplied } };
|
|
2138
|
-
})();
|
|
2139
|
-
}
|
|
2140
|
-
precheckEditedRuleBeforeDecision(entry, judgedOwner, parsed) {
|
|
2141
|
-
const persistRule = parsed.persistRule;
|
|
2142
|
-
if (persistRule === undefined || persistRule.kind !== "text" || !persistRule.edited)
|
|
2143
|
-
return undefined;
|
|
2144
|
-
if (parsed.updatedInput !== undefined)
|
|
2145
|
-
return undefined;
|
|
2146
|
-
if (entry.governanceForced === true)
|
|
2147
|
-
return undefined;
|
|
2148
|
-
const command = entry.ruleLane?.command;
|
|
2149
|
-
if (this.ruleConsent === undefined || command === undefined || judgedOwner === null)
|
|
2150
|
-
return undefined;
|
|
2151
|
-
const verdict = precheckCardRuleText(persistRule.rule, command);
|
|
2152
|
-
if (verdict === undefined || verdict.ok)
|
|
2153
|
-
return undefined;
|
|
2154
|
-
return {
|
|
2155
|
-
status: 400,
|
|
2156
|
-
body: refusalBody({
|
|
2157
|
-
errorCode: "request.body_shape",
|
|
2158
|
-
message: verdict.message,
|
|
2159
|
-
facts: {
|
|
2160
|
-
...(verdict.code !== undefined ? { ruleRejectCode: verdict.code } : {}),
|
|
2161
|
-
},
|
|
2162
|
-
}),
|
|
2163
|
-
};
|
|
2164
|
-
}
|
|
2165
|
-
editedArmEcho(persistRule, persisted) {
|
|
2166
|
-
if (persistRule.kind !== "text" || !persistRule.edited || !persisted.ok || persisted.kind !== "single")
|
|
2167
|
-
return {};
|
|
2168
|
-
return { persistedRule: persisted.rule };
|
|
2169
|
-
}
|
|
2170
1979
|
notPending(cause) {
|
|
2171
1980
|
return {
|
|
2172
1981
|
status: 404,
|
|
@@ -2180,34 +1989,24 @@ export class ToolApprovalCoordinator {
|
|
|
2180
1989
|
};
|
|
2181
1990
|
}
|
|
2182
1991
|
askNotVisible() {
|
|
2183
|
-
return { status: 404, body: refusalBody({ errorCode: "not_found.ask", message:
|
|
1992
|
+
return { status: 404, body: refusalBody({ errorCode: "not_found.ask", message: ASK_NOT_FOUND_UNDER_TASK }) };
|
|
2184
1993
|
}
|
|
2185
|
-
|
|
2186
|
-
const entry = this.liveEntryFor(
|
|
2187
|
-
if (entry !== undefined
|
|
2188
|
-
return this.readByRow(entry,
|
|
2189
|
-
|
|
2190
|
-
const body = { state: "pending", ...livePendingRowOf(entry) };
|
|
2191
|
-
return { status: 200, body };
|
|
2192
|
-
}
|
|
2193
|
-
const store = this.askStore;
|
|
2194
|
-
if (!store)
|
|
2195
|
-
return this.askNotVisible();
|
|
2196
|
-
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);
|
|
2197
1999
|
}
|
|
2198
|
-
async readByRow(entry,
|
|
2199
|
-
const
|
|
2200
|
-
if (
|
|
2201
|
-
return this.askNotVisible();
|
|
2202
|
-
const row = await this.askRowFollowingOwner(store, id, "getAsk(read-by-row)");
|
|
2203
|
-
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") {
|
|
2204
2003
|
const body = { state: "pending", ...livePendingRowOf(entry) };
|
|
2205
2004
|
return { status: 200, body };
|
|
2206
2005
|
}
|
|
2207
|
-
return this.readStateOfRow(row, principal);
|
|
2006
|
+
return this.readStateOfRow(row, taskId, principal);
|
|
2208
2007
|
}
|
|
2209
|
-
async readAskRowState(
|
|
2210
|
-
return this.readStateOfRow(await this.askRowFor(
|
|
2008
|
+
async readAskRowState(taskId, askId, principal) {
|
|
2009
|
+
return this.readStateOfRow(await this.askRowFor(this.askStore, askId, "getAsk(read)"), taskId, principal);
|
|
2211
2010
|
}
|
|
2212
2011
|
async readParkedGate(row) {
|
|
2213
2012
|
const gate = this.parkedGate;
|
|
@@ -2221,12 +2020,8 @@ export class ToolApprovalCoordinator {
|
|
|
2221
2020
|
return undefined;
|
|
2222
2021
|
}
|
|
2223
2022
|
}
|
|
2224
|
-
async
|
|
2225
|
-
|
|
2226
|
-
return gate?.kind === "open" ? gate.generation : undefined;
|
|
2227
|
-
}
|
|
2228
|
-
async readStateOfRow(row, principal) {
|
|
2229
|
-
if (!askRowVisibleTo(row, principal))
|
|
2023
|
+
async readStateOfRow(row, taskId, principal) {
|
|
2024
|
+
if (!askRowVisibleUnder(row, taskId, principal))
|
|
2230
2025
|
return this.askNotVisible();
|
|
2231
2026
|
const projection = buildAskRowReadProjection(row, Date.now());
|
|
2232
2027
|
let body;
|
|
@@ -2286,12 +2081,6 @@ export class ToolApprovalCoordinator {
|
|
|
2286
2081
|
}
|
|
2287
2082
|
return { status: 200, body };
|
|
2288
2083
|
}
|
|
2289
|
-
respondLate(id, principal, parsed, httpReq) {
|
|
2290
|
-
const store = this.askStore;
|
|
2291
|
-
if (!store)
|
|
2292
|
-
return this.askNotVisible();
|
|
2293
|
-
return this.dispatchLateByRow(store, id, parsed, httpReq, principal);
|
|
2294
|
-
}
|
|
2295
2084
|
watchPresentedCards(askIds, deliver, connection) {
|
|
2296
2085
|
const watcher = { deliver, connection, askIds: new Set(), registrationsOnConnection: new Map() };
|
|
2297
2086
|
for (const askId of askIds) {
|
|
@@ -2356,16 +2145,10 @@ export class ToolApprovalCoordinator {
|
|
|
2356
2145
|
}
|
|
2357
2146
|
}
|
|
2358
2147
|
liveEntryFor(id) {
|
|
2359
|
-
const local = this.pending.get(id);
|
|
2360
|
-
if (local !== undefined)
|
|
2361
|
-
return local.askId === undefined ? local : undefined;
|
|
2362
2148
|
for (const e of this.pendingByAskId.get(id) ?? [])
|
|
2363
2149
|
return e;
|
|
2364
2150
|
return undefined;
|
|
2365
2151
|
}
|
|
2366
|
-
async dispatchLateByRow(store, id, parsed, httpReq, principal) {
|
|
2367
|
-
return this.dispatchRowTruth(await this.askRowFor(store, id, "getAsk(respond-late)"), id, parsed, httpReq, principal);
|
|
2368
|
-
}
|
|
2369
2152
|
async askRowFor(store, id, label) {
|
|
2370
2153
|
try {
|
|
2371
2154
|
return await this.withStoreDeadline(store.getAsk(id), label, DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
@@ -2375,106 +2158,12 @@ export class ToolApprovalCoordinator {
|
|
|
2375
2158
|
return null;
|
|
2376
2159
|
}
|
|
2377
2160
|
}
|
|
2378
|
-
async
|
|
2379
|
-
const
|
|
2380
|
-
const row = await this.askRowFor(store, id, label);
|
|
2381
|
-
if (row?.state === "STREAM_PENDING")
|
|
2382
|
-
this.followRowOwner(row.askId, row.owner, readSeq);
|
|
2383
|
-
return row;
|
|
2384
|
-
}
|
|
2385
|
-
async dispatchRowTruth(row, id, parsed, httpReq, principal) {
|
|
2386
|
-
if (!askRowVisibleTo(row, principal))
|
|
2387
|
-
return this.askNotVisible();
|
|
2388
|
-
switch (row.state) {
|
|
2389
|
-
case "DECIDED":
|
|
2390
|
-
return this.replayDecidedAsk(id, row, parsed);
|
|
2391
|
-
case "PARKED":
|
|
2392
|
-
return this.redeemParkedAsk(id, row, parsed, httpReq, principal);
|
|
2393
|
-
case "PARKING": {
|
|
2394
|
-
const parkSession = parkSessionOf(row);
|
|
2395
|
-
return {
|
|
2396
|
-
status: 202,
|
|
2397
|
-
body: {
|
|
2398
|
-
approvalId: id,
|
|
2399
|
-
askId: row.askId,
|
|
2400
|
-
reason: "parking_in_progress",
|
|
2401
|
-
message: parkSession !== undefined
|
|
2402
|
-
? "parking in progress — not applied; retry after retryAfterMs or answer through decidePath"
|
|
2403
|
-
: "parking in progress — not applied; retry after retryAfterMs or answer through the parent session's durable decide",
|
|
2404
|
-
retryAfterMs: PARKING_RETRY_AFTER_MS,
|
|
2405
|
-
...(parkSession !== undefined ? { decidePath: approvalDecidePath(parkSession) } : {}),
|
|
2406
|
-
...this.lateRuleRefusal(parsed),
|
|
2407
|
-
},
|
|
2408
|
-
};
|
|
2409
|
-
}
|
|
2410
|
-
case "DENIED":
|
|
2411
|
-
case "VOID": {
|
|
2412
|
-
return this.notPending(askRowEndingCause(row));
|
|
2413
|
-
}
|
|
2414
|
-
case "STREAM_PENDING":
|
|
2415
|
-
return this.notPending(NOT_PENDING_UNKNOWN_CAUSE);
|
|
2416
|
-
default: {
|
|
2417
|
-
const unhandled = row.state;
|
|
2418
|
-
defaultLogger.error("tool approval respond hit an unknown ask state", { askId: row.askId, state: unhandled });
|
|
2419
|
-
return this.notPending();
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
lateRuleRefusal(parsed) {
|
|
2424
|
-
if (parsed.persistRule === undefined)
|
|
2425
|
-
return {};
|
|
2426
|
-
return { rulePersisted: false, ruleRefusal: "rule_lane_unavailable" };
|
|
2427
|
-
}
|
|
2428
|
-
replayDecidedAsk(id, row, parsed) {
|
|
2429
|
-
const decided = decidedRowFacts(row);
|
|
2430
|
-
if (decided === undefined) {
|
|
2431
|
-
defaultLogger.error("tool approval late replay saw a DECIDED row that is not a usable human decision", {
|
|
2432
|
-
askId: row.askId,
|
|
2433
|
-
decision: row.decision,
|
|
2434
|
-
provisional: row.provisional,
|
|
2435
|
-
decidedAtMs: row.decidedAtMs,
|
|
2436
|
-
});
|
|
2437
|
-
return this.notPending();
|
|
2438
|
-
}
|
|
2439
|
-
this.notifyExternalDecision(row.askId, outcomeOfDecidedRow(row));
|
|
2440
|
-
return {
|
|
2441
|
-
status: 200,
|
|
2442
|
-
body: {
|
|
2443
|
-
approvalId: id,
|
|
2444
|
-
askId: row.askId,
|
|
2445
|
-
idempotent: true,
|
|
2446
|
-
...decided,
|
|
2447
|
-
...noteRecordedReceipt(parsed.note, noteLandedOnRow(parsed.note, row.decisionNote)),
|
|
2448
|
-
...this.lateRuleRefusal(parsed),
|
|
2449
|
-
},
|
|
2450
|
-
};
|
|
2451
|
-
}
|
|
2452
|
-
async redeemParkedAsk(id, row, parsed, httpReq, principal) {
|
|
2453
|
-
const redeem = this.parkedRedeem?.();
|
|
2454
|
-
if (redeem === undefined)
|
|
2455
|
-
return this.notPending();
|
|
2456
|
-
if (row.gateToken === null) {
|
|
2457
|
-
defaultLogger.error("tool approval parked row carries no gate token — nothing to redeem", { askId: row.askId });
|
|
2458
|
-
return this.notPending();
|
|
2459
|
-
}
|
|
2460
|
-
const openGenerationNow = this.lookupParkRedemption(row.askId) !== undefined ? await this.openGateGeneration(row) : undefined;
|
|
2461
|
-
const prior = this.lookupParkRedemption(row.askId);
|
|
2462
|
-
if (prior?.redeemed !== undefined && receiptAnswersNow(prior.redeemed.gateGeneration, openGenerationNow))
|
|
2463
|
-
return this.replayRedeemReceipt(id, prior, row, prior.redeemed, parsed);
|
|
2464
|
-
if (prior?.inflight !== undefined) {
|
|
2465
|
-
const shared = await prior.inflight;
|
|
2466
|
-
if (shared.status < 200 || shared.status >= 300)
|
|
2467
|
-
return shared;
|
|
2468
|
-
if (!receiptAnswersNow(shared.gateGeneration, openGenerationNow))
|
|
2469
|
-
return this.redeemParkedAsk(id, row, parsed, httpReq, principal);
|
|
2470
|
-
return this.replayRedeemReceipt(id, prior, row, { decision: shared.decision, body: shared.body, gateGeneration: shared.gateGeneration, ...(shared.rule?.terminal === true ? { rule: shared.rule.body } : {}) }, parsed);
|
|
2471
|
-
}
|
|
2161
|
+
async runParkedFlight(redeem, store, row, gateToken, request) {
|
|
2162
|
+
const decision = request.write.decision;
|
|
2472
2163
|
const admission = this.parkRedemptionAdmission(row.owner);
|
|
2473
2164
|
if (admission !== undefined)
|
|
2474
|
-
return admission;
|
|
2165
|
+
return { ...admission, body: admission.body, decision };
|
|
2475
2166
|
const memo = this.openParkRedemption(row.askId, row.owner);
|
|
2476
|
-
const decision = parsed.value === "deny" ? "deny" : "approve";
|
|
2477
|
-
const gateToken = row.gateToken;
|
|
2478
2167
|
const flight = (async () => {
|
|
2479
2168
|
let res;
|
|
2480
2169
|
try {
|
|
@@ -2484,12 +2173,14 @@ export class ToolApprovalCoordinator {
|
|
|
2484
2173
|
gateBoundCallId: row.gateBoundCallId,
|
|
2485
2174
|
gateBoundInputHash: row.gateBoundInputHash,
|
|
2486
2175
|
decision,
|
|
2487
|
-
...(
|
|
2488
|
-
decider:
|
|
2489
|
-
...(
|
|
2490
|
-
...(
|
|
2491
|
-
...(
|
|
2492
|
-
...(
|
|
2176
|
+
...(request.write.note !== undefined ? { note: request.write.note } : {}),
|
|
2177
|
+
decider: request.write.decider,
|
|
2178
|
+
...(request.updatedInput !== undefined && decision === "approve" ? { updatedInput: request.updatedInput } : {}),
|
|
2179
|
+
...(request.remember !== undefined ? { remember: request.remember } : {}),
|
|
2180
|
+
...(request.persistRule !== undefined ? { persistRule: request.persistRule } : {}),
|
|
2181
|
+
...(request.principal !== undefined ? { principal: request.principal } : {}),
|
|
2182
|
+
operator: request.operator,
|
|
2183
|
+
...(request.httpReq !== undefined ? { httpReq: request.httpReq } : {}),
|
|
2493
2184
|
});
|
|
2494
2185
|
}
|
|
2495
2186
|
catch (err) {
|
|
@@ -2498,353 +2189,109 @@ export class ToolApprovalCoordinator {
|
|
|
2498
2189
|
}
|
|
2499
2190
|
if (res.status < 200 || res.status >= 300)
|
|
2500
2191
|
return { ...res, decision };
|
|
2501
|
-
const
|
|
2502
|
-
return { ...res, decision, ...(
|
|
2192
|
+
const recorded = await this.recordParkedOutcome(store, row, request.write);
|
|
2193
|
+
return { ...res, decision, ...(recorded !== undefined ? { recorded } : {}) };
|
|
2503
2194
|
})();
|
|
2504
2195
|
memo.inflight = flight;
|
|
2505
|
-
let out;
|
|
2506
2196
|
try {
|
|
2507
|
-
|
|
2197
|
+
return await flight;
|
|
2508
2198
|
}
|
|
2509
2199
|
finally {
|
|
2510
|
-
memo.inflight = undefined;
|
|
2511
|
-
}
|
|
2512
|
-
if (out.status < 200 || out.status >= 300) {
|
|
2513
2200
|
if (this.parkRedemptions.get(row.askId) === memo)
|
|
2514
2201
|
this.parkRedemptions.delete(row.askId);
|
|
2515
|
-
return { status: out.status, body: out.body };
|
|
2516
2202
|
}
|
|
2517
|
-
const rule = out.rule;
|
|
2518
|
-
if (this.retainParkRedemption(row.askId, memo))
|
|
2519
|
-
memo.redeemed = { decision, body: out.body, gateGeneration: out.gateGeneration, ...(rule?.terminal === true ? { rule: rule.body } : {}) };
|
|
2520
|
-
return {
|
|
2521
|
-
status: out.status,
|
|
2522
|
-
body: {
|
|
2523
|
-
...out.body,
|
|
2524
|
-
approvalId: id,
|
|
2525
|
-
askId: row.askId,
|
|
2526
|
-
accepted: "parked_redeemed",
|
|
2527
|
-
...(parsed.value === "allow_session" ? { rememberApplied: out.rememberApplied === true } : {}),
|
|
2528
|
-
...(rule?.body ?? {}),
|
|
2529
|
-
},
|
|
2530
|
-
};
|
|
2531
2203
|
}
|
|
2532
|
-
async
|
|
2533
|
-
const persistRule = parsed.persistRule;
|
|
2534
|
-
if (persistRule === undefined)
|
|
2535
|
-
return undefined;
|
|
2536
|
-
const flag = (rulePersisted, ruleRefusal, echo) => ({
|
|
2537
|
-
body: { rulePersisted, ...(ruleRefusal !== undefined ? { ruleRefusal } : {}), ...(echo ?? {}) },
|
|
2538
|
-
terminal: true,
|
|
2539
|
-
});
|
|
2540
|
-
const lane = this.ruleConsent;
|
|
2541
|
-
if (lane === undefined || row.owner === null)
|
|
2542
|
-
return flag(false, "rule_lane_unavailable");
|
|
2543
|
-
const owner = row.owner;
|
|
2544
|
-
const envelope = readStoredCardEnvelope(row.cardJson);
|
|
2545
|
-
if (envelope === undefined)
|
|
2546
|
-
return flag(false, "rule_material_absent");
|
|
2547
|
-
const card = envelope.card;
|
|
2548
|
-
if (card.governanceForced === true)
|
|
2549
|
-
return flag(false, "rule_governance_forced");
|
|
2550
|
-
const command = row.ruleCommand;
|
|
2551
|
-
if (command === null)
|
|
2552
|
-
return flag(false, "rule_material_absent");
|
|
2553
|
-
if (parsed.updatedInput !== undefined)
|
|
2554
|
-
return flag(false, "rule_input_edited");
|
|
2555
|
-
const offers = card.ruleOffers ?? [];
|
|
2556
|
-
const scope = row.ruleScopeRoot !== null ? { kind: "project", root: row.ruleScopeRoot } : undefined;
|
|
2557
|
-
const common = {
|
|
2558
|
-
principal: owner,
|
|
2559
|
-
toolName: card.toolName,
|
|
2560
|
-
command,
|
|
2561
|
-
...(row.toolCallId !== "" ? { toolCallId: row.toolCallId } : {}),
|
|
2562
|
-
...(row.boundInputHash !== null ? { boundInputHash: row.boundInputHash } : {}),
|
|
2563
|
-
...(scope !== undefined ? { scope } : {}),
|
|
2564
|
-
};
|
|
2204
|
+
async recordParkedOutcome(store, row, write) {
|
|
2565
2205
|
try {
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
if (!persisted.ok) {
|
|
2572
|
-
defaultLogger.warn("permission rule batch was not persisted after a parked late decision", {
|
|
2573
|
-
askId: row.askId,
|
|
2574
|
-
reason: persisted.reason,
|
|
2575
|
-
...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}),
|
|
2576
|
-
});
|
|
2577
|
-
return flag(false, persisted.reason);
|
|
2578
|
-
}
|
|
2579
|
-
return flag(true, undefined, persisted.kind === "batch"
|
|
2580
|
-
? {
|
|
2581
|
-
persistedRules: persisted.rules,
|
|
2582
|
-
...(persisted.members !== undefined
|
|
2583
|
-
? { persistedRuleAnchors: persisted.members.map((m) => ({ offerIndex: persistRule.batchOfferIndex, memberIndex: m.memberIndex, rule: m.rule })) }
|
|
2584
|
-
: {}),
|
|
2585
|
-
}
|
|
2586
|
-
: {});
|
|
2587
|
-
}
|
|
2588
|
-
const ruleText = persistRule.rule;
|
|
2589
|
-
if (!persistRule.edited && !offers.some((o) => o.kind === "single" && o.rule === ruleText))
|
|
2590
|
-
return flag(false, "rule_not_offered");
|
|
2591
|
-
const persisted = await lane.persistCardRule({
|
|
2592
|
-
...common,
|
|
2593
|
-
redemption: persistRule.edited ? { kind: "edited", text: ruleText } : { kind: "offered", ruleText },
|
|
2594
|
-
});
|
|
2595
|
-
if (!persisted.ok) {
|
|
2596
|
-
defaultLogger.warn("permission rule was not persisted after a parked late decision", {
|
|
2597
|
-
askId: row.askId,
|
|
2598
|
-
reason: persisted.reason,
|
|
2599
|
-
...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}),
|
|
2600
|
-
});
|
|
2601
|
-
return flag(false, persisted.reason);
|
|
2602
|
-
}
|
|
2603
|
-
return flag(true, undefined, persistRule.edited && persisted.kind === "single" ? { persistedRule: persisted.rule } : {});
|
|
2206
|
+
const out = await this.writeAskDecision(store, row.askId, { kind: "parked" }, row.owner, write);
|
|
2207
|
+
if (out.ok)
|
|
2208
|
+
return out.row;
|
|
2209
|
+
recordFailOpen("server.approval.parked-decision-not-recorded", `askId=${row.askId} reason=${out.reason}`);
|
|
2210
|
+
return undefined;
|
|
2604
2211
|
}
|
|
2605
2212
|
catch (err) {
|
|
2606
|
-
this.noteStoreError(err, "
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
}
|
|
2610
|
-
async replayRedeemReceipt(id, memo, row, receipt, parsed) {
|
|
2611
|
-
let rule = receipt.decision === "approve" ? receipt.rule : this.lateRuleRefusal(parsed);
|
|
2612
|
-
if (receipt.decision === "approve" && rule === undefined && parsed.persistRule !== undefined) {
|
|
2613
|
-
const flight = memo.ruleInflight ?? this.persistRuleFromRow(row, parsed);
|
|
2614
|
-
const isOwner = memo.ruleInflight === undefined;
|
|
2615
|
-
if (isOwner)
|
|
2616
|
-
memo.ruleInflight = flight;
|
|
2617
|
-
let fresh;
|
|
2618
|
-
try {
|
|
2619
|
-
fresh = await flight;
|
|
2620
|
-
}
|
|
2621
|
-
finally {
|
|
2622
|
-
if (isOwner)
|
|
2623
|
-
memo.ruleInflight = undefined;
|
|
2624
|
-
}
|
|
2625
|
-
rule = fresh?.body;
|
|
2626
|
-
if (fresh?.terminal === true && memo.redeemed !== undefined)
|
|
2627
|
-
memo.redeemed.rule = fresh.body;
|
|
2213
|
+
this.noteStoreError(err, "recordParkedDecision");
|
|
2214
|
+
recordFailOpen("server.approval.parked-decision-not-recorded", `askId=${row.askId} reason=store_error`);
|
|
2215
|
+
return undefined;
|
|
2628
2216
|
}
|
|
2629
|
-
const sameDecision = (parsed.value === "deny") === (receipt.decision === "deny");
|
|
2630
|
-
return {
|
|
2631
|
-
status: 200,
|
|
2632
|
-
body: {
|
|
2633
|
-
...(sameDecision ? receipt.body : withoutSettlementEvidence(receipt.body)),
|
|
2634
|
-
approvalId: id,
|
|
2635
|
-
askId: row.askId,
|
|
2636
|
-
accepted: "parked_redeemed",
|
|
2637
|
-
idempotent: true,
|
|
2638
|
-
decision: receipt.decision === "approve" ? "allow" : "deny",
|
|
2639
|
-
...(rule ?? {}),
|
|
2640
|
-
},
|
|
2641
|
-
};
|
|
2642
2217
|
}
|
|
2643
|
-
async
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
});
|
|
2654
|
-
if (entry.governanceForced === true)
|
|
2655
|
-
return withFlag(false, "rule_governance_forced");
|
|
2656
|
-
if (lane === undefined || material === undefined || owner === null)
|
|
2657
|
-
return withFlag(false, "rule_lane_unavailable");
|
|
2658
|
-
if (!ruleEligibility.eligible)
|
|
2659
|
-
return withFlag(false, "rule_store_error");
|
|
2660
|
-
if (inputWasEdited)
|
|
2661
|
-
return withFlag(false, "rule_input_edited");
|
|
2662
|
-
if (ruleMaterialCarriesCredentialShape(material.command)) {
|
|
2663
|
-
defaultLogger.warn("approval rule material withheld on the live leg: the adjudicated command carries credential-shaped bytes", { commandChars: material.command.length });
|
|
2664
|
-
return withFlag(false, "rule_material_absent");
|
|
2665
|
-
}
|
|
2666
|
-
if (persistRule.kind === "batch") {
|
|
2667
|
-
const offer = material.offers[persistRule.batchOfferIndex];
|
|
2668
|
-
if (offer === undefined || offer.kind !== "batch")
|
|
2669
|
-
return withFlag(false, "rule_not_offered");
|
|
2670
|
-
try {
|
|
2671
|
-
const persisted = await lane.persistCardRule({
|
|
2672
|
-
principal: owner,
|
|
2673
|
-
toolName: entry.toolName,
|
|
2674
|
-
command: material.command,
|
|
2675
|
-
redemption: { kind: "batch", offer },
|
|
2676
|
-
...(material.toolCallId !== undefined ? { toolCallId: material.toolCallId } : {}),
|
|
2677
|
-
...(material.boundInputHash !== undefined ? { boundInputHash: material.boundInputHash } : {}),
|
|
2678
|
-
...(material.scopeRoot !== undefined ? { scope: { kind: "project", root: material.scopeRoot } } : {}),
|
|
2679
|
-
});
|
|
2680
|
-
if (!persisted.ok) {
|
|
2681
|
-
defaultLogger.warn("permission rule batch was not persisted after an approval", { reason: persisted.reason, ...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}) });
|
|
2682
|
-
return withFlag(false, persisted.reason);
|
|
2683
|
-
}
|
|
2684
|
-
return withFlag(true, undefined, persisted.kind === "batch"
|
|
2685
|
-
? {
|
|
2686
|
-
persistedRules: persisted.rules,
|
|
2687
|
-
...(persisted.members !== undefined
|
|
2688
|
-
? { persistedRuleAnchors: persisted.members.map((m) => ({ offerIndex: persistRule.batchOfferIndex, memberIndex: m.memberIndex, rule: m.rule })) }
|
|
2689
|
-
: {}),
|
|
2690
|
-
}
|
|
2691
|
-
: {});
|
|
2692
|
-
}
|
|
2693
|
-
catch (err) {
|
|
2694
|
-
this.noteStoreError(err, "persistCardRule(batch)");
|
|
2695
|
-
return withFlag(false, "rule_store_error");
|
|
2218
|
+
async decideParkedAsk(store, row, request, idempotencyKey) {
|
|
2219
|
+
for (;;) {
|
|
2220
|
+
const gate = await this.readParkedGate(row);
|
|
2221
|
+
const current = await this.askRowFor(store, row.askId, "getAsk(parked-decision)");
|
|
2222
|
+
if (current === null || current.state !== "PARKED" || !askRowCarriesOwner(current, row.owner))
|
|
2223
|
+
return { kind: "gone" };
|
|
2224
|
+
const inflight = this.lookupParkRedemption(row.askId)?.inflight;
|
|
2225
|
+
if (inflight !== undefined) {
|
|
2226
|
+
await inflight;
|
|
2227
|
+
continue;
|
|
2696
2228
|
}
|
|
2229
|
+
const standing = standingParkedDecision(gate, current, idempotencyKey);
|
|
2230
|
+
if (standing !== undefined)
|
|
2231
|
+
return standing;
|
|
2232
|
+
if (request.write.decision === "approve" && request.policyMayNotApprove === true)
|
|
2233
|
+
return { kind: "policy_refused" };
|
|
2234
|
+
const redeem = this.parkedRedeem?.();
|
|
2235
|
+
if (redeem === undefined || row.gateToken === null) {
|
|
2236
|
+
if (row.gateToken === null)
|
|
2237
|
+
defaultLogger.error("tool approval parked row carries no gate token — nothing to redeem", { askId: row.askId });
|
|
2238
|
+
return { kind: "gate_only" };
|
|
2239
|
+
}
|
|
2240
|
+
const out = await this.runParkedFlight(redeem, store, current, row.gateToken, request);
|
|
2241
|
+
if (out.status === 200)
|
|
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 } : {}) };
|
|
2243
|
+
if ((await this.readParkedGate(row))?.kind === "decided")
|
|
2244
|
+
continue;
|
|
2245
|
+
return { kind: "refused", status: out.status, body: out.body };
|
|
2697
2246
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2247
|
+
}
|
|
2248
|
+
async readParkedAskOfGate(sourceSessionId, binding) {
|
|
2249
|
+
const store = this.askStore;
|
|
2701
2250
|
try {
|
|
2702
|
-
const
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
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;
|
|
2257
|
+
}
|
|
2258
|
+
const gate = await this.readParkedGate(row);
|
|
2259
|
+
if (gate === undefined)
|
|
2260
|
+
return { kind: "unreadable" };
|
|
2261
|
+
if (gate.kind === "open")
|
|
2262
|
+
atGate.push(row);
|
|
2714
2263
|
}
|
|
2715
|
-
|
|
2264
|
+
if (atGate.length === 0)
|
|
2265
|
+
return { kind: "none" };
|
|
2266
|
+
return atGate.length === 1 ? { kind: "row", row: atGate[0] } : { kind: "unreadable" };
|
|
2716
2267
|
}
|
|
2717
2268
|
catch (err) {
|
|
2718
|
-
this.noteStoreError(err, "
|
|
2719
|
-
return
|
|
2269
|
+
this.noteStoreError(err, "listAsksAtGate");
|
|
2270
|
+
return { kind: "unreadable" };
|
|
2720
2271
|
}
|
|
2721
2272
|
}
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
}), "decideAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
|
|
2733
|
-
if (late.ok)
|
|
2273
|
+
writeAskDecision(store, askId, form, owner, write) {
|
|
2274
|
+
switch (form.kind) {
|
|
2275
|
+
case "live":
|
|
2276
|
+
return this.withStoreDeadline(store.decideAsk(askId, form.batchId, owner, liveDecideInputOf(write)), "decideAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
|
|
2277
|
+
if (!late.ok)
|
|
2278
|
+
return;
|
|
2279
|
+
if (decidedRowFacts(late.row) === undefined) {
|
|
2280
|
+
defaultLogger.error("ask decision landed after its deadline on a row that is not an authoritative human decision — not settled", { askId, state: late.row.state, provisional: late.row.provisional });
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2734
2283
|
this.notifyExternalDecision(askId, outcomeOfDecidedRow(late.row));
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
return { lost: true, row: decided.row };
|
|
2742
|
-
}
|
|
2743
|
-
}
|
|
2744
|
-
catch (err) {
|
|
2745
|
-
this.noteStoreError(err, "decideAsk");
|
|
2746
|
-
ruleEligibility.eligible = false;
|
|
2747
|
-
try {
|
|
2748
|
-
const confirmSeq = this.nextRowReadSeq();
|
|
2749
|
-
const row = await this.withStoreDeadline(askStore.getAsk(askId), "getAsk(decideAsk-confirm)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
2750
|
-
if (row?.state === "STREAM_PENDING")
|
|
2751
|
-
this.followRowOwner(askId, row.owner, confirmSeq);
|
|
2752
|
-
if (row === null || !askRowCarriesOwner(row, owner))
|
|
2753
|
-
return { lost: true, row: null };
|
|
2754
|
-
if (row.state === "DECIDED" && row.decision === (parsed.value === "deny" ? "deny" : "approve")) {
|
|
2755
|
-
winnerRow = row;
|
|
2756
|
-
ambiguousConfirmedRow = row;
|
|
2757
|
-
noteLanded = noteLandedOnRow(parsed.note, row.decisionNote);
|
|
2758
|
-
}
|
|
2759
|
-
else if (row.state !== "STREAM_PENDING") {
|
|
2760
|
-
return { lost: true, row };
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
catch (confirmErr) {
|
|
2764
|
-
this.noteStoreError(confirmErr, "getAsk(decideAsk-confirm)");
|
|
2765
|
-
return {
|
|
2766
|
-
lost: false,
|
|
2767
|
-
result: {
|
|
2768
|
-
status: 500,
|
|
2769
|
-
body: refusalBody({
|
|
2770
|
-
errorCode: "internal.error",
|
|
2771
|
-
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",
|
|
2772
|
-
}),
|
|
2773
|
-
},
|
|
2774
|
-
};
|
|
2775
|
-
}
|
|
2776
|
-
}
|
|
2777
|
-
const wantAllowed = parsed.value !== "deny";
|
|
2778
|
-
const decidedWon = winnerRow !== undefined;
|
|
2779
|
-
if (entry.adopted === true && !decidedWon)
|
|
2780
|
-
return { lost: false, result: this.notPending() };
|
|
2781
|
-
if (this.pending.get(entry.registrationId) !== entry) {
|
|
2782
|
-
const othersReached = winnerRow !== undefined ? this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow)).settled : 0;
|
|
2783
|
-
const converged = decidedWon && (entry.adopted === true || entry.settledOutcome === (wantAllowed ? SETTLE_OUTCOME.ALLOWED : SETTLE_OUTCOME.DENIED));
|
|
2784
|
-
if (!converged) {
|
|
2785
|
-
return { lost: false, result: this.notPending() };
|
|
2786
|
-
}
|
|
2787
|
-
return { lost: false, result: withConsumerDisclosure(this.finishRespond(entry, owner, parsed, { updatedInputDelivered: false, noteRecorded: noteLanded }), waitingLegsReached(entry, othersReached)) };
|
|
2788
|
-
}
|
|
2789
|
-
const payloadIsMine = ambiguousConfirmedRow === undefined ||
|
|
2790
|
-
approvalPayloadFingerprint({ updatedInput: ambiguousConfirmedRow.updatedInput }) === approvalPayloadFingerprint({ updatedInput: parsed.updatedInput });
|
|
2791
|
-
const result = this.finishRespond(entry, owner, parsed, {
|
|
2792
|
-
noteRecorded: noteLanded,
|
|
2793
|
-
...(winnerRow !== undefined ? { settleOutcome: outcomeOfDecidedRow(winnerRow), settleOutcomeIsThisRequest: ambiguousConfirmedRow === undefined } : {}),
|
|
2794
|
-
...(entry.adopted === true ? { updatedInputDelivered: false } : ambiguousConfirmedRow !== undefined ? { updatedInputDelivered: payloadIsMine } : {}),
|
|
2795
|
-
});
|
|
2796
|
-
const othersReached = winnerRow !== undefined ? this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow)).settled : 0;
|
|
2797
|
-
return { lost: false, result: withConsumerDisclosure(result, waitingLegsReached(entry, othersReached)) };
|
|
2798
|
-
}
|
|
2799
|
-
finishRespond(entry, judgedOwner, parsed, opts) {
|
|
2800
|
-
let rememberApplied;
|
|
2801
|
-
if (parsed.value === "allow_session") {
|
|
2802
|
-
const answer = liveSessionAnswer(pendingEntryStandingAnswerFacts(entry), entry);
|
|
2803
|
-
rememberApplied = answer.home === "seat";
|
|
2804
|
-
if (answer.home === "seat") {
|
|
2805
|
-
const grantKey = sessionAllowKey(judgedOwner, answer.seat, sessionAllowCategory(entry.toolName), entry.grantBinding);
|
|
2806
|
-
if (this.allowAllSessions.size >= MAX_ALLOW_SESSIONS) {
|
|
2807
|
-
const oldest = this.allowAllSessions.values().next().value;
|
|
2808
|
-
if (oldest !== undefined)
|
|
2809
|
-
this.allowAllSessions.delete(oldest);
|
|
2810
|
-
}
|
|
2811
|
-
this.allowAllSessions.add(grantKey);
|
|
2812
|
-
}
|
|
2813
|
-
else if (answer.home === "none" && answer.occupantUnreadable) {
|
|
2814
|
-
defaultLogger.warn("session_grant_refused_occupant_unreadable", { sessionId: entry.sessionId, toolName: entry.toolName });
|
|
2284
|
+
});
|
|
2285
|
+
case "parked":
|
|
2286
|
+
return this.withStoreDeadline(store.recordParkedDecision(askId, owner, askDecisionRecordOf(write)), "recordParkedDecision", DURABLE_CALL_TIMEOUT_MS);
|
|
2287
|
+
default: {
|
|
2288
|
+
const unhandled = form;
|
|
2289
|
+
throw new Error(`writeAskDecision: unhandled form ${JSON.stringify(unhandled)}`);
|
|
2815
2290
|
}
|
|
2816
2291
|
}
|
|
2817
|
-
const allowed = parsed.value !== "deny";
|
|
2818
|
-
const settleArgs = opts?.settleOutcome !== undefined
|
|
2819
|
-
? settleArgsOfDecided(opts.settleOutcome)
|
|
2820
|
-
: { allowed, updatedInput: parsed.updatedInput, source: liveHostSource(parsed.decider), ...(!allowed && parsed.note !== undefined ? { denyReason: parsed.note } : {}) };
|
|
2821
|
-
const readRootGrant = opts?.settleOutcome === undefined || opts.settleOutcomeIsThisRequest === true ? parsed.readRootGrant : undefined;
|
|
2822
|
-
entry.settle({
|
|
2823
|
-
by: "human",
|
|
2824
|
-
allowed: settleArgs.allowed,
|
|
2825
|
-
updatedInput: settleArgs.updatedInput,
|
|
2826
|
-
...(settleArgs.source !== undefined ? { hostSource: settleArgs.source } : {}),
|
|
2827
|
-
...(settleArgs.denyReason !== undefined ? { denyReason: settleArgs.denyReason } : {}),
|
|
2828
|
-
...(readRootGrant !== undefined ? { readRootGrant } : {}),
|
|
2829
|
-
});
|
|
2830
|
-
const updatedInputDelivered = opts?.updatedInputDelivered ?? true;
|
|
2831
|
-
const noteRecorded = opts?.noteRecorded ?? false;
|
|
2832
|
-
return {
|
|
2833
|
-
status: 200,
|
|
2834
|
-
body: {
|
|
2835
|
-
approvalId: entry.approvalId,
|
|
2836
|
-
delivery: "applied",
|
|
2837
|
-
decision: parsed.value,
|
|
2838
|
-
...(rememberApplied !== undefined ? { rememberApplied } : {}),
|
|
2839
|
-
...(parsed.updatedInput !== undefined && allowed && updatedInputDelivered ? { updatedInputForwarded: true } : {}),
|
|
2840
|
-
...noteRecordedReceipt(parsed.note, noteRecorded),
|
|
2841
|
-
},
|
|
2842
|
-
};
|
|
2843
2292
|
}
|
|
2844
2293
|
async adoptOpenAsks() {
|
|
2845
2294
|
const store = this.askStore;
|
|
2846
|
-
if (!store)
|
|
2847
|
-
return { adopted: 0, alreadyLive: 0, cardUnreadable: 0, capped: false };
|
|
2848
2295
|
let rows;
|
|
2849
2296
|
try {
|
|
2850
2297
|
rows = await this.withStoreDeadline(store.listByState("STREAM_PENDING", ADOPT_OPEN_ASKS_MAX + 1), "listByState(adopt)");
|
|
@@ -2912,11 +2359,9 @@ export class ToolApprovalCoordinator {
|
|
|
2912
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 }),
|
|
2913
2360
|
forceParkNow: release,
|
|
2914
2361
|
settleParkRoute: (cause) => leave({ outcome: SETTLE_OUTCOME.EXPIRED, cause, parked: this.unattendedRoutesToPark }),
|
|
2915
|
-
owner: row.owner,
|
|
2916
|
-
ownerReadSeq: this.rowReadSeq,
|
|
2917
2362
|
executionLane: row.executionLane,
|
|
2918
2363
|
...leg,
|
|
2919
|
-
...(card.fromSubagent === true ? {
|
|
2364
|
+
...(card.fromSubagent === true ? { sourceSessionId: row.sourceSessionId } : {}),
|
|
2920
2365
|
toolName: card.toolName,
|
|
2921
2366
|
...facts,
|
|
2922
2367
|
approvalId: row.askId,
|
|
@@ -2947,43 +2392,6 @@ export class ToolApprovalCoordinator {
|
|
|
2947
2392
|
for (const watcher of watchers)
|
|
2948
2393
|
watcher.askIds.delete(askId);
|
|
2949
2394
|
}
|
|
2950
|
-
followRowOwner(askId, owner, readSeq) {
|
|
2951
|
-
for (const e of this.pendingByAskId.get(askId) ?? []) {
|
|
2952
|
-
if (e.ownerReadSeq >= readSeq)
|
|
2953
|
-
continue;
|
|
2954
|
-
e.owner = owner;
|
|
2955
|
-
e.ownerReadSeq = readSeq;
|
|
2956
|
-
}
|
|
2957
|
-
}
|
|
2958
|
-
rowReadSeq = 0;
|
|
2959
|
-
nextRowReadSeq() {
|
|
2960
|
-
this.rowReadSeq += 1;
|
|
2961
|
-
return this.rowReadSeq;
|
|
2962
|
-
}
|
|
2963
|
-
async followRowOwners() {
|
|
2964
|
-
const store = this.askStore;
|
|
2965
|
-
const out = { read: 0, followed: 0, unreadable: 0 };
|
|
2966
|
-
if (!store)
|
|
2967
|
-
return out;
|
|
2968
|
-
for (const askId of [...this.pendingByAskId.keys()]) {
|
|
2969
|
-
out.read += 1;
|
|
2970
|
-
const readSeq = this.nextRowReadSeq();
|
|
2971
|
-
let row;
|
|
2972
|
-
try {
|
|
2973
|
-
row = await this.withStoreDeadline(store.getAsk(askId), "getAsk(follow-row-owners)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
2974
|
-
}
|
|
2975
|
-
catch (err) {
|
|
2976
|
-
this.noteStoreError(err, "getAsk(follow-row-owners)");
|
|
2977
|
-
out.unreadable += 1;
|
|
2978
|
-
continue;
|
|
2979
|
-
}
|
|
2980
|
-
if (row?.state !== "STREAM_PENDING")
|
|
2981
|
-
continue;
|
|
2982
|
-
this.followRowOwner(askId, row.owner, readSeq);
|
|
2983
|
-
out.followed += 1;
|
|
2984
|
-
}
|
|
2985
|
-
return out;
|
|
2986
|
-
}
|
|
2987
2395
|
waitingRegistrationCount(askId) {
|
|
2988
2396
|
let n = 0;
|
|
2989
2397
|
for (const e of this.pendingByAskId.get(askId) ?? [])
|
|
@@ -3001,9 +2409,7 @@ export class ToolApprovalCoordinator {
|
|
|
3001
2409
|
const store = this.askStore;
|
|
3002
2410
|
const ids = [...candidates.keys()];
|
|
3003
2411
|
const listed = await Promise.all([...candidates.values()].map(async (entry) => {
|
|
3004
|
-
|
|
3005
|
-
return inScope(entry.owner);
|
|
3006
|
-
const ask = await this.askRowFollowingOwner(store, entry.askId, "getAsk(list-live)");
|
|
2412
|
+
const ask = await this.askRowFor(store, entry.askId, "getAsk(list-live)");
|
|
3007
2413
|
return ask?.state === "STREAM_PENDING" && inScope(ask.owner);
|
|
3008
2414
|
}));
|
|
3009
2415
|
const rows = [];
|
|
@@ -3030,13 +2436,11 @@ export class ToolApprovalCoordinator {
|
|
|
3030
2436
|
}
|
|
3031
2437
|
async pollSuspendedDecisions() {
|
|
3032
2438
|
const store = this.askStore;
|
|
3033
|
-
if (!store)
|
|
3034
|
-
return { polled: 0, settled: 0, backlog: 0, truncated: false };
|
|
3035
2439
|
const waiting = [];
|
|
3036
2440
|
const adopted = [];
|
|
3037
2441
|
const seen = new Set();
|
|
3038
2442
|
for (const entry of this.pending.values()) {
|
|
3039
|
-
if (entry.
|
|
2443
|
+
if (entry.settledOutcome !== undefined || seen.has(entry.askId))
|
|
3040
2444
|
continue;
|
|
3041
2445
|
if (entry.suspended === true)
|
|
3042
2446
|
waiting.push(entry.askId);
|
|
@@ -3046,53 +2450,33 @@ export class ToolApprovalCoordinator {
|
|
|
3046
2450
|
continue;
|
|
3047
2451
|
seen.add(entry.askId);
|
|
3048
2452
|
}
|
|
3049
|
-
const waitingStart = waiting.length > 0 ? this.suspendedPollOffset % waiting.length : 0;
|
|
3050
|
-
const adoptedStart = adopted.length > 0 ? this.adoptedPollOffset % adopted.length : 0;
|
|
3051
|
-
const ordered = [
|
|
3052
|
-
...[...waiting.slice(waitingStart), ...waiting.slice(0, waitingStart)].map((askId) => ({ askId, tier: "waiting" })),
|
|
3053
|
-
...[...adopted.slice(adoptedStart), ...adopted.slice(0, adoptedStart)].map((askId) => ({ askId, tier: "adopted" })),
|
|
3054
|
-
];
|
|
3055
|
-
const deadlineMs = Date.now() + SUSPENDED_POLL_BUDGET_MS;
|
|
3056
2453
|
let settled = 0;
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
for (const { askId, tier } of ordered) {
|
|
3062
|
-
if (polled >= SUSPENDED_POLL_MAX_PER_TICK || Date.now() >= deadlineMs) {
|
|
3063
|
-
truncated = true;
|
|
3064
|
-
break;
|
|
3065
|
-
}
|
|
3066
|
-
polled++;
|
|
3067
|
-
if (tier === "waiting")
|
|
3068
|
-
this.suspendedPollOffset = waitingStart + ++polledWaiting;
|
|
3069
|
-
else
|
|
3070
|
-
this.adoptedPollOffset = adoptedStart + ++polledAdopted;
|
|
2454
|
+
const { visited: polled, truncated } = await boundedRotation([
|
|
2455
|
+
{ items: waiting, cursor: this.suspendedPollCursor },
|
|
2456
|
+
{ items: adopted, cursor: this.adoptedPollCursor },
|
|
2457
|
+
], { maxPerTick: SUSPENDED_POLL_MAX_PER_TICK, budgetMs: SUSPENDED_POLL_BUDGET_MS }, async (askId) => {
|
|
3071
2458
|
let row;
|
|
3072
|
-
const readSeq = this.nextRowReadSeq();
|
|
3073
2459
|
try {
|
|
3074
2460
|
row = await this.withStoreDeadline(store.getAsk(askId), "getAsk(poll-suspended)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
3075
2461
|
}
|
|
3076
2462
|
catch (err) {
|
|
3077
2463
|
this.noteStoreError(err, "getAsk(poll-suspended)");
|
|
3078
|
-
|
|
2464
|
+
return;
|
|
3079
2465
|
}
|
|
3080
2466
|
if (row === null) {
|
|
3081
2467
|
this.forgetAdoptedAsk(askId);
|
|
3082
|
-
|
|
3083
|
-
}
|
|
3084
|
-
if (row.state === "STREAM_PENDING") {
|
|
3085
|
-
this.followRowOwner(askId, row.owner, readSeq);
|
|
3086
|
-
continue;
|
|
2468
|
+
return;
|
|
3087
2469
|
}
|
|
2470
|
+
if (row.state === "STREAM_PENDING")
|
|
2471
|
+
return;
|
|
3088
2472
|
if (this.editedDecisionInFlight(askId))
|
|
3089
|
-
|
|
2473
|
+
return;
|
|
3090
2474
|
const endingCause = askRowEndingCause(row);
|
|
3091
2475
|
if (endingCause !== undefined) {
|
|
3092
2476
|
const before = this.waitingRegistrationCount(askId);
|
|
3093
2477
|
this.settleVoidedSiblings([askId], endingCause);
|
|
3094
2478
|
settled += before;
|
|
3095
|
-
|
|
2479
|
+
return;
|
|
3096
2480
|
}
|
|
3097
2481
|
const outcome = replayTerminalAskRow(row);
|
|
3098
2482
|
if (outcome === "unavailable") {
|
|
@@ -3100,11 +2484,11 @@ export class ToolApprovalCoordinator {
|
|
|
3100
2484
|
const parkCause = askRowParkTrigger(row) ?? (Date.now() >= row.expiresAtMs ? APPROVAL_ENDING_CAUSES.TTL_EXPIRED : APPROVAL_ENDING_CAUSES.SHUTDOWN_BEFORE_PARK);
|
|
3101
2485
|
this.settleSameAskIdParkRoute(askId, parkCause);
|
|
3102
2486
|
settled += before;
|
|
3103
|
-
|
|
2487
|
+
return;
|
|
3104
2488
|
}
|
|
3105
2489
|
const delivered = this.notifyExternalDecision(askId, outcome).settled;
|
|
3106
2490
|
settled += delivered;
|
|
3107
|
-
}
|
|
2491
|
+
});
|
|
3108
2492
|
const backlog = Math.max(0, waiting.length + adopted.length - polled);
|
|
3109
2493
|
if (truncated) {
|
|
3110
2494
|
defaultLogger.warn("approval_suspended_poll_truncated", {
|
|
@@ -3113,8 +2497,8 @@ export class ToolApprovalCoordinator {
|
|
|
3113
2497
|
backlog,
|
|
3114
2498
|
maxPerTick: SUSPENDED_POLL_MAX_PER_TICK,
|
|
3115
2499
|
budgetMs: SUSPENDED_POLL_BUDGET_MS,
|
|
3116
|
-
resumeFromOffset: this.
|
|
3117
|
-
resumeAdoptedFromOffset: this.
|
|
2500
|
+
resumeFromOffset: this.suspendedPollCursor.offset,
|
|
2501
|
+
resumeAdoptedFromOffset: this.adoptedPollCursor.offset,
|
|
3118
2502
|
});
|
|
3119
2503
|
}
|
|
3120
2504
|
return { polled, settled, backlog, truncated };
|