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