@sema-agent/core 7.5.1 → 7.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/dist/agents/cascade.d.ts +2 -2
- package/dist/agents/cascade.js +12 -10
- package/dist/agents/repair-loop.d.ts +5 -3
- package/dist/agents/repair-loop.js +13 -15
- package/dist/agents/subagent.d.ts +24 -42
- package/dist/agents/subagent.js +119 -105
- package/dist/agents/suspend-guard.d.ts +31 -19
- package/dist/agents/suspend-guard.js +14 -8
- package/dist/agents/teacher.js +9 -9
- package/dist/agents/team.d.ts +4 -3
- package/dist/agents/team.js +10 -8
- package/dist/agents/verify.d.ts +3 -3
- package/dist/agents/verify.js +17 -17
- package/dist/core/a2a.js +2 -1
- package/dist/core/ask-origin.d.ts +60 -7
- package/dist/core/ask-origin.js +26 -1
- package/dist/core/checkpoint-store.d.ts +78 -76
- package/dist/core/checkpoint-store.js +17 -1
- package/dist/core/gate-outcome.d.ts +189 -0
- package/dist/core/gate-outcome.js +70 -0
- package/dist/core/hooks.d.ts +18 -92
- package/dist/core/hooks.js +88 -85
- package/dist/core/mcp-failure.d.ts +104 -0
- package/dist/core/mcp-failure.js +128 -0
- package/dist/core/mcp.d.ts +21 -77
- package/dist/core/mcp.js +76 -150
- package/dist/core/pause-registry.d.ts +131 -0
- package/dist/core/pause-registry.js +27 -0
- package/dist/core/protocol-table.d.ts +5 -0
- package/dist/core/protocol-table.js +1 -0
- package/dist/core/runner/abort-race.d.ts +41 -0
- package/dist/core/runner/abort-race.js +38 -0
- package/dist/core/runner/assemble-result.d.ts +32 -41
- package/dist/core/runner/assemble-result.js +55 -74
- package/dist/core/runner/checkpoint-scope.d.ts +15 -3
- package/dist/core/runner/checkpoint-scope.js +3 -0
- package/dist/core/runner/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/content-ask-bindings.d.ts +27 -0
- package/dist/core/runner/content-ask-bindings.js +1 -0
- package/dist/core/runner/contracts.d.ts +144 -50
- package/dist/core/runner/denial-limit-arms.d.ts +24 -1
- package/dist/core/runner/denial-limit-arms.js +24 -3
- package/dist/core/runner/gate-exit.d.ts +74 -0
- package/dist/core/runner/gate-exit.js +55 -0
- package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
- package/dist/core/runner/inherited-ask-grants.js +29 -0
- package/dist/core/runner/park-commit.d.ts +102 -0
- package/dist/core/runner/park-commit.js +31 -0
- package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
- package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
- package/dist/core/runner/prepare-ask-lane.d.ts +107 -0
- package/dist/core/runner/prepare-ask-lane.js +131 -0
- package/dist/core/runner/prepare-boundary-parks.d.ts +102 -0
- package/dist/core/runner/prepare-boundary-parks.js +169 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
- package/dist/core/runner/prepare-context-lane.d.ts +119 -0
- package/dist/core/runner/prepare-context-lane.js +230 -0
- package/dist/core/runner/prepare-gate-stations.d.ts +174 -0
- package/dist/core/runner/prepare-gate-stations.js +265 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
- package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
- package/dist/core/runner/prepare-memory-engine-session.js +233 -0
- package/dist/core/runner/prepare-memory.d.ts +44 -26
- package/dist/core/runner/prepare-park-ask.d.ts +162 -0
- package/dist/core/runner/prepare-park-ask.js +377 -0
- package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
- package/dist/core/runner/prepare-policy-chain.js +584 -0
- package/dist/core/runner/prepare-project-context.d.ts +1 -13
- package/dist/core/runner/prepare-project-context.js +1 -3
- package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
- package/dist/core/runner/prepare-prompt-assembly.js +162 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
- package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
- package/dist/core/runner/prepare-protocol-tools.js +0 -3
- package/dist/core/runner/prepare-question-face.d.ts +3 -21
- package/dist/core/runner/prepare-question-face.js +2 -1
- package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
- package/dist/core/runner/prepare-safety-scan.js +1 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
- package/dist/core/runner/prepare-suspend-saga.js +308 -0
- package/dist/core/runner/prepare-task.d.ts +9 -136
- package/dist/core/runner/prepare-task.js +47 -2745
- package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
- package/dist/core/runner/prepare-turn-wiring.js +201 -0
- package/dist/core/runner/prepare-wiring-manifest.d.ts +17 -17
- package/dist/core/runner/prepare-wiring-manifest.js +16 -10
- package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
- package/dist/core/runner/prepare-workspace-restore.js +3 -16
- package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
- package/dist/core/runner/prompt-hash-salt.js +2 -0
- package/dist/core/runner/remote-env-retry.d.ts +29 -0
- package/dist/core/runner/remote-env-retry.js +16 -0
- package/dist/core/runner/runtask.d.ts +16 -31
- package/dist/core/runner/runtask.js +110 -121
- package/dist/core/runner/terminal-projection.d.ts +22 -0
- package/dist/core/runner/terminal-projection.js +28 -0
- package/dist/core/session.d.ts +12 -0
- package/dist/core/session.js +3 -0
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
- package/dist/core/terminal-cause.d.ts +137 -0
- package/dist/core/terminal-cause.js +9 -0
- package/dist/core/tool-policy.d.ts +43 -139
- package/dist/core/tool-policy.js +79 -112
- package/dist/core/types.d.ts +67 -164
- package/dist/core/wiring-manifest.d.ts +6 -3
- package/dist/core/workflow-journal-store.js +3 -4
- package/dist/engine/harness/agent-harness.d.ts +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -5
- package/dist/orchestration/builtin-workflows.d.ts +2 -2
- package/dist/orchestration/builtin-workflows.js +1 -1
- package/dist/orchestration/goal.js +8 -7
- package/dist/orchestration/run-spec.js +5 -3
- package/dist/orchestration/run-workflow-tool.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.js +4 -4
- package/dist/orchestration/workflow-governance.d.ts +4 -4
- package/dist/orchestration/workflow-governance.js +4 -2
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-primitives.js +1 -1
- package/dist/orchestration/workflow.d.ts +11 -0
- package/dist/orchestration/workflow.js +64 -39
- package/dist/prompts/supervisor.d.ts +1 -1
- package/dist/prompts/supervisor.js +3 -3
- package/dist/scenarios/scenario-registry.js +1 -1
- package/package.json +3 -1
- package/test/export-surface.snapshot.json +74 -22
- /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
- /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commit-side vocabulary of a durable park: which holder a COMMITTED checkpoint publishes to, the abort-classification
|
|
3
|
+
* marker the loop stamps on the contaminated siblings, and the deadline arithmetic every mint shares (the retention
|
|
4
|
+
* defaults and the one TTL sanitizer). Moved whole out of the orchestrator so the boundary-parks phase (resource /
|
|
5
|
+
* platform / review) and the park-ask phase (the approval park) reach DOWN for the same publication seam and the same
|
|
6
|
+
* numbers, and the driver keeps reaching down for the marker its harness stamps. Pure: no engine state; the only clock
|
|
7
|
+
* is the caller's.
|
|
8
|
+
*/
|
|
9
|
+
import type { CheckpointGate, CheckpointToken, PendingAction } from "../checkpoint-store.js";
|
|
10
|
+
import type { WorkspaceHandle } from "../remote-env.js";
|
|
11
|
+
import type { Prepared } from "./contracts.js";
|
|
12
|
+
/** design/74 (HIGH-2): default resource-checkpoint retention (30 days, the spec's
|
|
13
|
+
* `RESOURCE_SUSPEND_TTL_SEC`). Stamped onto the checkpoint `deadline` so the durable reaper can GC an
|
|
14
|
+
* ORPHANED resource suspend; a legitimate auto-resume fires long before this. Override via
|
|
15
|
+
* `TaskSpec.resourceSuspend.ttlMs`. */
|
|
16
|
+
export declare const DEFAULT_RESOURCE_TTL_MS: number;
|
|
17
|
+
/**
|
|
18
|
+
* design/164 件五 — how long AFTER a governance window frees a `usage_window` checkpoint stays reapable-free.
|
|
19
|
+
* The retention TTL answers "was this abandoned?"; a usage-window suspend is not abandoned while the window
|
|
20
|
+
* it waits on is still full, so its deadline is pushed to `retryAfterMs + this`. One hour gives a host's
|
|
21
|
+
* scheduler a realistic chance to pick the resume up (a cron tick, a queue drain) before the row is
|
|
22
|
+
* treated as garbage — small next to the 30-day retention it is compared against, and large next to any
|
|
23
|
+
* polling interval a driver would sanely use.
|
|
24
|
+
*/
|
|
25
|
+
export declare const USAGE_WINDOW_REAP_MARGIN_MS: number;
|
|
26
|
+
/** design/80 D-2 (BLOCKER): abandonment TTL stamped on an UNATTENDED human/irreversible_ask suspend (no
|
|
27
|
+
* `durableApproval` = no operator/resolver wired). Without it the checkpoint has `deadline=undefined` and the
|
|
28
|
+
* reaper (which filters on a non-null deadline) can NEVER GC it → a pinned session + a stranded paused VM +
|
|
29
|
+
* an orphan checkpoint leak forever. A `durableApproval` suspend keeps the operator's `ttlMs` (undefined =
|
|
30
|
+
* unbounded by the operator's choice; D-D `terminalAt` is the eventual liveness backstop there). 30 days. */
|
|
31
|
+
export declare const DEFAULT_UNATTENDED_APPROVAL_TTL_MS: number;
|
|
32
|
+
/**
|
|
33
|
+
* RB-68 (2026-07-25, red probe): a caller-supplied TTL is only a TTL if it is a finite, positive number.
|
|
34
|
+
* `TaskSpec.durableApproval.ttlMs` / `resourceSuspend.ttlMs` are typed as a bare `number`, and the mint
|
|
35
|
+
* arithmetic was `Date.now() + ttlMs` with NO validation — so `Number(process.env.APPROVAL_TTL_MS)` on an
|
|
36
|
+
* unset/malformed env var (a config-center miss, an upstream arithmetic slip) silently produced
|
|
37
|
+
* `deadline = NaN`. Every reaper comparison is `deadline <= cutoff`, and `NaN <= anything` is FALSE, so a
|
|
38
|
+
* checkpoint that LOOKS bounded (it carries a ttlMs and a deadline) became permanently IMMORTAL: no reap
|
|
39
|
+
* cutoff, however far in the future, could ever expire it → a pinned session + a stranded paused VM, and
|
|
40
|
+
* a misconfigured deployment silently defeating its own abandoned-approval cleanup with no error anywhere.
|
|
41
|
+
*
|
|
42
|
+
* Handling follows the codebase's established treatment of malformed numeric config — sanitize to the
|
|
43
|
+
* documented default rather than throw — `task-registry-shared.ts`'s `TASK_OUTPUT_DEFAULT_CHARS` fallbacks
|
|
44
|
+
* (non-numeric / non-finite / non-positive all return the documented default) and `media-byte-cap.ts`'s
|
|
45
|
+
* `!Number.isFinite(limitBytes) || limitBytes <= 0` guard are the two live precedents: garbage in a bound
|
|
46
|
+
* field must never widen the bound, and
|
|
47
|
+
* killing an in-flight task over it would cost more than it saves. `undefined` (the deliberate
|
|
48
|
+
* "unbounded, D-D terminalAt is the backstop" choice) is preserved as-is and is NOT what this rejects.
|
|
49
|
+
*/
|
|
50
|
+
export declare function sanitizedTtlMs(ttlMs: number | undefined): number | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* WHICH tool call a committed durable park is holding this run — `undefined` when nothing parked, or
|
|
53
|
+
* when the park that did commit holds no call (a resource slice, a plan review).
|
|
54
|
+
*
|
|
55
|
+
* SINGLE derivation on purpose. Two consumers need this answer: the abort-classification seam, which
|
|
56
|
+
* stamps it into the `details` of the results the loop mints for the contaminated siblings, and the
|
|
57
|
+
* `tool_end` projection, which puts it on the wire. Reading it from one function keeps the two from
|
|
58
|
+
* disagreeing about what a pause with no call means. They are not otherwise
|
|
59
|
+
* interchangeable: the frame face applies strictly narrower conditions on top of this answer (see
|
|
60
|
+
* `tool_end.gatedCallId`), so a frame may omit an id this function returns — never the reverse.
|
|
61
|
+
* "Which holder wins" is no longer a question: there is one holder, and a pause that holds no call
|
|
62
|
+
* publishes absence.
|
|
63
|
+
*
|
|
64
|
+
* The frame side must not read the id back out of a tool RESULT even though the marker is there: a
|
|
65
|
+
* result's `details` is written by the tool (and replaceable by post-tool hooks), so lifting a
|
|
66
|
+
* cross-call attribution from it would let any failing tool name an arbitrary call and put a phantom
|
|
67
|
+
* approval wait on someone else's frame. Same rule, same reason, as `tool_end.gate`.
|
|
68
|
+
*
|
|
69
|
+
*/
|
|
70
|
+
export declare function gatedCallIdOf(p: Pick<Prepared, "pausedRef">): string | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* The abort-classification marker for the loop's short-circuit "Operation aborted" family: when a gate
|
|
73
|
+
* committed a durable park this run (`pausedRef.current`, whatever the pause's kind), the contaminated
|
|
74
|
+
* sibling frames carry something a consumer can key
|
|
75
|
+
* on instead of parsing the (contract-stable) result text (ruled 2026-08-04). The commit is published
|
|
76
|
+
* BEFORE the abort fires, so this read is race-free at mint time.
|
|
77
|
+
*
|
|
78
|
+
* `gatedCallId` NAMES the call the gate is holding, so a consumer can attribute a contaminated frame to
|
|
79
|
+
* its cause instead of inferring the pair from batch adjacency. Absent, never approximated: a park that
|
|
80
|
+
* holds no tool call (a resource slice, a plan review) publishes no id, and this marker does not invent
|
|
81
|
+
* one from whatever call happens to be in flight.
|
|
82
|
+
*
|
|
83
|
+
* Module-level rather than a closure in `prepareTask` (design/238 D-7: extract, don't accrete) — the
|
|
84
|
+
* marker's shape and its rules belong beside {@link gatedCallIdOf}, the derivation it reads.
|
|
85
|
+
*/
|
|
86
|
+
export declare function parkContaminationMarker(refs: Pick<Prepared, "pausedRef">): Record<string, unknown> | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* THE COMMIT-SIDE PUBLICATION. Called AFTER `commitSuspendSaga` returns true (the checkpoint is durably
|
|
89
|
+
* committed) to publish the run's one committed pause for the run loop's terminal assembly. There is no
|
|
90
|
+
* routing: the cause carries the committed gate whole, and WHICH terminal status it reads
|
|
91
|
+
* (`suspended` / `needs_review`) is the gate kind's registry row — the two holders the review family and
|
|
92
|
+
* the approval family used to be split across, and the branch that chose between them, are gone.
|
|
93
|
+
* (Review checkpoints themselves are minted by a PROFILE, out of core scope; core owns this seam and the
|
|
94
|
+
* gate/status/resume discriminants.)
|
|
95
|
+
*
|
|
96
|
+
* Module-level, taking the holder as a parameter (design/238 D-7: extract, don't accrete) — beside the
|
|
97
|
+
* two readers of what it publishes ({@link gatedCallIdOf}, {@link parkContaminationMarker}).
|
|
98
|
+
*/
|
|
99
|
+
export declare function publishCommittedSuspend(refs: Pick<Prepared, "pausedRef">, token: CheckpointToken, gate: CheckpointGate, scope: string, remoteHandle: WorkspaceHandle | undefined, checkpointId?: string,
|
|
100
|
+
/** The COMMITTED row's pendingAction — passed whole, not as a pre-extracted id, so no call site can
|
|
101
|
+
* hand this seam an id the committed row does not actually park on. */
|
|
102
|
+
pendingAction?: PendingAction): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const DEFAULT_RESOURCE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
2
|
+
export const USAGE_WINDOW_REAP_MARGIN_MS = 60 * 60 * 1000;
|
|
3
|
+
export const DEFAULT_UNATTENDED_APPROVAL_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
4
|
+
export function sanitizedTtlMs(ttlMs) {
|
|
5
|
+
if (ttlMs === undefined)
|
|
6
|
+
return undefined;
|
|
7
|
+
return Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : DEFAULT_RESOURCE_TTL_MS;
|
|
8
|
+
}
|
|
9
|
+
export function gatedCallIdOf(p) {
|
|
10
|
+
return p.pausedRef.current?.gatedCallId;
|
|
11
|
+
}
|
|
12
|
+
export function parkContaminationMarker(refs) {
|
|
13
|
+
if (refs.pausedRef.current === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
const gatedCallId = gatedCallIdOf(refs);
|
|
16
|
+
return { code: "gate.parked", ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
|
|
17
|
+
}
|
|
18
|
+
export function publishCommittedSuspend(refs, token, gate, scope, remoteHandle, checkpointId, pendingAction) {
|
|
19
|
+
const pause = {
|
|
20
|
+
cause: {
|
|
21
|
+
kind: "paused",
|
|
22
|
+
gate,
|
|
23
|
+
token,
|
|
24
|
+
...(checkpointId !== undefined ? { checkpointId } : {}),
|
|
25
|
+
...(remoteHandle !== undefined ? { restoreMode: remoteHandle.restoreMode === "park_only" ? "park_only" : "snapshot" } : {}),
|
|
26
|
+
},
|
|
27
|
+
scope,
|
|
28
|
+
...(pendingAction?.kind === "tool_approval" ? { gatedCallId: pendingAction.toolCallId } : {}),
|
|
29
|
+
};
|
|
30
|
+
refs.pausedRef.current = pause;
|
|
31
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* design/389 —
|
|
2
|
+
* design/389 — the permission-rule LANES the unified store feeds (prepare-path machinery, design/390 L1): the two gate
|
|
3
|
+
* lanes, and — since the policy-chain phase took the chain assembly — the rule-offer factory every ask-mint site calls.
|
|
3
4
|
*
|
|
4
5
|
* `prepareTask` used to assemble the personal lane inline (a store read + a hand-spliced session
|
|
5
6
|
* overlay + two admission arms) and the org lane from a second seam. Both now read the ONE store query —
|
|
@@ -23,8 +24,8 @@
|
|
|
23
24
|
* silence the ungated-write warning for deployments that wired no policy at all.
|
|
24
25
|
*/
|
|
25
26
|
import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js";
|
|
26
|
-
import type
|
|
27
|
-
import type { PersistedAllowRule } from "../permission-rule-model.js";
|
|
27
|
+
import { persistedRuleMandateOf, type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit } from "../hooks.js";
|
|
28
|
+
import type { PersistedAllowRule, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
|
|
28
29
|
import { type OrgRuleResolution } from "../permission-rule-org.js";
|
|
29
30
|
import type { PermissionRuleStoreProvider } from "../permission-rule-provider.js";
|
|
30
31
|
/** The one SHELL tool the persisted-rule lane speaks for. */
|
|
@@ -130,3 +131,108 @@ export declare function createPermissionRuleLanes(cfg: {
|
|
|
130
131
|
* lane took the read. */
|
|
131
132
|
onDisclosure: (message: string) => void;
|
|
132
133
|
}): PermissionRuleLanes;
|
|
134
|
+
/** The closed reason set for the rule-offer factory's empty answer (#490 修②) — the same three
|
|
135
|
+
* spellings the `AskRequest` and durable-row seats declare, kept at the one factory that fills both.
|
|
136
|
+
* Deliberately not exported: this is a wire vocabulary, and its two faces declare it literally (the
|
|
137
|
+
* `previewWithheld` precedent) so a consumer reads the closed set on the type it is holding rather
|
|
138
|
+
* than through an import. */
|
|
139
|
+
type RuleOffersAbsence = "mandated" | "lane_cannot_speak" | "shadowed";
|
|
140
|
+
/** The surviving-ask facts the rule-offer factory reads (see {@link createRuleOffersOf}): the
|
|
141
|
+
* synchronous mint sites spread the surviving decision, which carries them; the park leg threads
|
|
142
|
+
* each as its own parameter. */
|
|
143
|
+
interface RuleOffersAskFacts {
|
|
144
|
+
requiresRealApproval?: boolean;
|
|
145
|
+
persistedRuleShadowed?: string;
|
|
146
|
+
decisionReason?: import("../tool-policy.js").DecisionReason;
|
|
147
|
+
/** #457 ④: an EXPLICIT `ask` permission rule matched this call (design/127 DSL, stamped at
|
|
148
|
+
* the rule policy's three matched-ask exits — bare, covering and param). */
|
|
149
|
+
matchedAskRule?: string;
|
|
150
|
+
/** #502: the engine-stamped probe mandate (the reversibility probe declared this call's
|
|
151
|
+
* demotion structural — for the built-in shell probe, a read outside the session's
|
|
152
|
+
* allowed directories). */
|
|
153
|
+
probeMandated?: boolean;
|
|
154
|
+
inheritedUnresolved?: boolean;
|
|
155
|
+
/** The ask is being resolved at an ANCESTOR's frozen approver (the three inherited-lane
|
|
156
|
+
* mint sites pass it literally) — this task's rule lane never adjudicates it. */
|
|
157
|
+
ancestorResolved?: boolean;
|
|
158
|
+
/** design/375 §5.2②: the surviving ask's engine-stamped per-segment coverage table — the
|
|
159
|
+
* batch offer carries exactly the segments this table calls uncovered. Absent ⇒ all
|
|
160
|
+
* segments read as uncovered (over-offer, the safe direction). */
|
|
161
|
+
segmentCoverage?: readonly SegmentCoverage[];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* design/179 §4 — the rule forms that could cover this exact call, for a surface's "stop asking me
|
|
165
|
+
* this" option (extracted from the prepare body, the spliceSessionOverlayRows precedent; the maker
|
|
166
|
+
* captures the per-task wiring once and the returned factory serves all five ask-mint sites — the
|
|
167
|
+
* approval-preview field is a standing example of what happens otherwise: it reached two of the
|
|
168
|
+
* five, so a delegated child's card was silently poorer than the top-level one).
|
|
169
|
+
*
|
|
170
|
+
* Empty unless a rule lane is armed — offering an option that redemption would refuse is worse than
|
|
171
|
+
* offering none — and empty for any call the lane cannot speak for (a compound, a redirection, a
|
|
172
|
+
* substitution, or a tool that is not the shell tool).
|
|
173
|
+
*
|
|
174
|
+
* Empty, too, on any ask a persisted rule could never CLEAR — "allow rules silence the classifier's
|
|
175
|
+
* questions, never a mandated one". Two doors, matching the two halves of that boundary:
|
|
176
|
+
* · the marks half, judged from the SAME resolved sources the gate input is built from, through the
|
|
177
|
+
* gate's own single-source predicate (an operator's shellGate:"always", the tool's own
|
|
178
|
+
* egress/irreversibility marks, and — #502 — the PER-CALL member that predicate also takes: a
|
|
179
|
+
* demotion this call's reversibility probe declared structural, read off the ask the gate
|
|
180
|
+
* stamped rather than off the tool seat, because it is true of one call and not of the seat) —
|
|
181
|
+
* a drift here would offer a rule the lane then refuses to honor;
|
|
182
|
+
* · the decision half (`ask`, REQUIRED at every mint site so a site added later cannot skip it):
|
|
183
|
+
* `requiresRealApproval` (org/governance — only judgment clears it), the shadowed-rule
|
|
184
|
+
* disclosure (the standing proof that a matching rule does not clear THIS ask), a hook-raised
|
|
185
|
+
* ask (`decisionReason === "hook"` — the lane's own clearing conjuncts refuse hook asks, so a
|
|
186
|
+
* rule minted from one would never silence it), an EXPLICIT `ask` rule match (`matchedAskRule`
|
|
187
|
+
* — see below), and an inherited-unresolved marked call (the
|
|
188
|
+
* ask is an ANCESTOR's authority, which this task's rule lane never adjudicates). Offering
|
|
189
|
+
* "stop asking me this" on a card that will keep asking lets a person mint a rule that never
|
|
190
|
+
* takes effect where they minted it.
|
|
191
|
+
*
|
|
192
|
+
* #457 ④ (CC 2.1.245 toolPolicy 对表, F11: "wildcard `ask` … keeps every matching tool behind a
|
|
193
|
+
* per-call prompt (no persistent always-allow)") — `matchedAskRule` was the one conjunct of the
|
|
194
|
+
* lane's own clearing predicate this door did not mirror, and the asymmetry was live: the
|
|
195
|
+
* persisted-rule lane refuses to clear ANY ask carrying it (`hooks.ts`, the
|
|
196
|
+
* `decision.matchedAskRule === undefined` conjunct on its allow arm), so a person who wrote a
|
|
197
|
+
* standing `Bash` `ask` rule and had no allow rule yet was still offered "allow, and stop asking
|
|
198
|
+
* me this" — and the rule they minted came back on the very next call as a SHADOWED match
|
|
199
|
+
* (`persistedRuleShadowed`, which this door was already refusing). The offer was only ever
|
|
200
|
+
* suppressed one call too late, after the useless rule existed. The shadowed conjunct stays: it
|
|
201
|
+
* covers the person who ALREADY has the allow rule; this one covers the person about to mint it.
|
|
202
|
+
*
|
|
203
|
+
* #490 修② — every door above that answers "no offers" now also names WHICH door, on the
|
|
204
|
+
* `ruleOffersAbsence` seat (`AskRequest`'s and the durable row's, ONE factory so the two faces
|
|
205
|
+
* cannot disagree). The doctrine it serves is the loud-bad-value one, applied to an absence: a
|
|
206
|
+
* surface reading an empty card could not tell "there is nothing this lane could offer" from
|
|
207
|
+
* "offers were deliberately suppressed", and a person reading it could not tell "write the rule
|
|
208
|
+
* yourself" from "no rule can excuse this approval". The seat is CLOSED
|
|
209
|
+
* (`mandated | lane_cannot_speak | shadowed`), mutually exclusive with `ruleOffers` by
|
|
210
|
+
* construction, and structural-door silent (see the first two guards).
|
|
211
|
+
*/
|
|
212
|
+
export declare function createRuleOffersOf(cfg: {
|
|
213
|
+
/** `permissionRuleLane !== undefined` at wiring time — the armed-lane structural door. */
|
|
214
|
+
laneArmed: boolean;
|
|
215
|
+
principal: string | undefined;
|
|
216
|
+
/** `deps.localOwnerRules === true` — the declared local-owner exception to the anonymous floor. */
|
|
217
|
+
localOwnerDeclared: boolean;
|
|
218
|
+
/** The live mark sources the gate input is built from — passed as REFERENCES (read per call, so a
|
|
219
|
+
* late-registered tool seat is seen exactly as the gate sees it). */
|
|
220
|
+
egressTools: {
|
|
221
|
+
has(name: string): boolean;
|
|
222
|
+
};
|
|
223
|
+
irreversibilityTier: {
|
|
224
|
+
get(name: string): Parameters<typeof persistedRuleMandateOf>[0]["irreversibility"];
|
|
225
|
+
};
|
|
226
|
+
shellGatedBash: boolean;
|
|
227
|
+
taskRoot: string | undefined;
|
|
228
|
+
/** The live tracked-cwd ref (read per call — the relative-cd resolution base, adversarial-review P1/r3). */
|
|
229
|
+
cwdRef: {
|
|
230
|
+
current: string;
|
|
231
|
+
} | undefined;
|
|
232
|
+
deniesDirectoryRead: ((directory: string) => boolean) | undefined;
|
|
233
|
+
}): (toolName: string, args: unknown, ask: RuleOffersAskFacts | undefined) => {
|
|
234
|
+
ruleOffers?: readonly RuleOffer[];
|
|
235
|
+
ruleOffersAbsence?: RuleOffersAbsence;
|
|
236
|
+
execCwd?: string;
|
|
237
|
+
};
|
|
238
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { persistedRuleMandateOf } from "../hooks.js";
|
|
2
|
+
import { directoryRuleAdmits, eligiblePersisted, findAdmittingRule, lexicalNormalAbsolutePathOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
|
|
2
3
|
import { orgRuleVerdictFor } from "../permission-rule-org.js";
|
|
3
4
|
export const PERSISTED_RULE_TOOL = "Bash";
|
|
4
5
|
export const DIRECTORY_RULE_TOOL = "Read";
|
|
@@ -138,3 +139,48 @@ export function createPermissionRuleLanes(cfg) {
|
|
|
138
139
|
: undefined;
|
|
139
140
|
return { personal, org };
|
|
140
141
|
}
|
|
142
|
+
export function createRuleOffersOf(cfg) {
|
|
143
|
+
return (toolName, args, ask) => {
|
|
144
|
+
if (!cfg.laneArmed || toolName !== PERSISTED_RULE_TOOL)
|
|
145
|
+
return {};
|
|
146
|
+
if ((cfg.principal === undefined || cfg.principal === "") && !cfg.localOwnerDeclared)
|
|
147
|
+
return {};
|
|
148
|
+
const closedDoor = (() => {
|
|
149
|
+
if (ask?.requiresRealApproval === true)
|
|
150
|
+
return "mandated";
|
|
151
|
+
if (ask?.persistedRuleShadowed !== undefined)
|
|
152
|
+
return "shadowed";
|
|
153
|
+
if (ask?.decisionReason === "hook")
|
|
154
|
+
return "mandated";
|
|
155
|
+
if (ask?.matchedAskRule !== undefined)
|
|
156
|
+
return "shadowed";
|
|
157
|
+
if (ask?.inheritedUnresolved === true)
|
|
158
|
+
return "mandated";
|
|
159
|
+
if (ask?.ancestorResolved === true)
|
|
160
|
+
return "mandated";
|
|
161
|
+
return undefined;
|
|
162
|
+
})();
|
|
163
|
+
if (closedDoor !== undefined)
|
|
164
|
+
return { ruleOffersAbsence: closedDoor };
|
|
165
|
+
if (persistedRuleMandateOf({
|
|
166
|
+
egress: cfg.egressTools.has(toolName),
|
|
167
|
+
irreversibility: cfg.irreversibilityTier.get(toolName),
|
|
168
|
+
shellGated: cfg.shellGatedBash,
|
|
169
|
+
probeMandated: ask?.probeMandated === true,
|
|
170
|
+
}) !== undefined) {
|
|
171
|
+
return { ruleOffersAbsence: "mandated" };
|
|
172
|
+
}
|
|
173
|
+
const command = args?.command;
|
|
174
|
+
if (typeof command !== "string")
|
|
175
|
+
return { ruleOffersAbsence: "lane_cannot_speak" };
|
|
176
|
+
const offers = suggestRulesForCommand(command, {
|
|
177
|
+
...(ask?.segmentCoverage !== undefined ? { coverage: ask.segmentCoverage } : {}),
|
|
178
|
+
...(cfg.taskRoot !== undefined ? { scope: { kind: "project", root: cfg.taskRoot }, cwd: cfg.taskRoot } : {}),
|
|
179
|
+
...(cfg.cwdRef?.current !== undefined ? { execCwd: cfg.cwdRef.current } : {}),
|
|
180
|
+
...(cfg.deniesDirectoryRead !== undefined ? { deniesDirectoryRead: cfg.deniesDirectoryRead } : {}),
|
|
181
|
+
});
|
|
182
|
+
return offers.length > 0
|
|
183
|
+
? { ruleOffers: offers, ...(cfg.cwdRef?.current !== undefined ? { execCwd: cfg.cwdRef.current } : {}) }
|
|
184
|
+
: { ruleOffersAbsence: "lane_cannot_speak" };
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/390 §1.2 M19 (ask lane) — prepareTask's IN-STREAM ASK LANE, verbatim from the driver's gate-machinery block:
|
|
3
|
+
* the per-call signal composition bound to this task's run signal, the abort-bound `adjudicate` wrapper (the budget
|
|
4
|
+
* snapshot and the live tracked cwd stamped onto every request), the approval-preview projection over the live roster,
|
|
5
|
+
* the `resolveAskBound` mint (the inherited-unavailable intercept, the duplicate-frame grant reuse, the human-review
|
|
6
|
+
* ledger, the bare-human-rejection record), the pre-wrapped deny observer and the hook-crash notifier. The three
|
|
7
|
+
* module-scope helpers only this segment called moved with it (the deny-observer factory, the preview sanitizer and
|
|
8
|
+
* resolver). The interface is the dependency list the segment had implicitly (design/238 R-1) — the closures below
|
|
9
|
+
* used to capture every seat here off the driver's scope.
|
|
10
|
+
*
|
|
11
|
+
* SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (every await lives inside the closures
|
|
12
|
+
* it builds, which run per tool call), so the phase adds no yield between the wiring-manifest station before it and
|
|
13
|
+
* the suspend saga after it (design/390 §1.5: a stretch with no await is extracted as a sync function). Read-stability
|
|
14
|
+
* of the host handles for the call: {@link RunInternals} (`@contract prepare.deps-read-stable`).
|
|
15
|
+
*
|
|
16
|
+
* The lane exists exactly when the wiring-manifest phase's `gateMachineryActive` holds — the driver's `if` around the
|
|
17
|
+
* whole block became this phase's one early return (the block's other four phases key off the lane's presence), so
|
|
18
|
+
* the orchestrator grows no branch.
|
|
19
|
+
*/
|
|
20
|
+
import type { AgentTool } from "../../internal/harness.js";
|
|
21
|
+
import type { AutoModeDenialTracker } from "../auto-mode.js";
|
|
22
|
+
import { type Hooks } from "../hooks.js";
|
|
23
|
+
import { type OnAsk, type ToolCallRequest, type ToolPolicy } from "../tool-policy.js";
|
|
24
|
+
import type { RunnerDeps, TaskSpec } from "../types.js";
|
|
25
|
+
import type { CwdRef } from "../../tools/fs/fs-shared.js";
|
|
26
|
+
import type { AskLane, Prepared } from "./contracts.js";
|
|
27
|
+
import { consumeInheritedAskGrant } from "./inherited-ask-grants.js";
|
|
28
|
+
import type { createRuleOffersOf } from "./permission-rule-lanes.js";
|
|
29
|
+
export interface PrepareAskLaneInput {
|
|
30
|
+
/** borrowed-readonly — the wiring-manifest phase's ONE activation predicate; false ⇒ this phase builds nothing and
|
|
31
|
+
* returns an absent lane. */
|
|
32
|
+
gateMachineryActive: boolean;
|
|
33
|
+
/** borrowed-readonly — prepare's abort seat; only `.signal` is read (composed into every per-call signal and bound
|
|
34
|
+
* onto the deny observer). Never aborted here. */
|
|
35
|
+
abortController: {
|
|
36
|
+
readonly signal: AbortSignal;
|
|
37
|
+
};
|
|
38
|
+
/** borrowed-readonly — the policy chain's effective policy, or undefined (then no `adjudicate` wrapper is built and
|
|
39
|
+
* the gate reads a missing policy as allow). Called at RUN time, per tool call, through the abort-bound wrapper. */
|
|
40
|
+
effectivePolicy: ToolPolicy | undefined;
|
|
41
|
+
/** borrowed-readonly — the per-leg-immutable durable budget snapshot stamped onto EVERY adjudicated request. Same
|
|
42
|
+
* object every call; the policy cannot write it back. */
|
|
43
|
+
budgetSnapshot: NonNullable<ToolCallRequest["budget"]>;
|
|
44
|
+
/** borrowed-readonly — the hand's live tracked cwd, or undefined (hands-less). Read per call at RUN time (`current`),
|
|
45
|
+
* never captured: the fs hand tools resolve their relative paths against this very ref. Not written here. */
|
|
46
|
+
handsCwdRef: CwdRef | undefined;
|
|
47
|
+
/** borrowed-readonly — the shared mount roster; read at RUN time by the approval-preview projection (alias-aware
|
|
48
|
+
* lookup over whatever the roster holds when the ask is minted). Never mutated here. */
|
|
49
|
+
tools: AgentTool[];
|
|
50
|
+
/** borrowed-mutable — the inherited-unavailable marker set. Writer here: `resolveAskBound` consumes (`delete`) a
|
|
51
|
+
* marked call at its fail-closed intercept; the policy-chain phase records, the gate station sweeps. */
|
|
52
|
+
inheritedUnavailableAsks: Set<string>;
|
|
53
|
+
/** borrowed-mutable — the duplicate-frame ask-grant record. Writer here: the grant-reuse consume half
|
|
54
|
+
* (`consumeInheritedAskGrant`); the policy-chain phase records, the gate station sweeps. */
|
|
55
|
+
inheritedAskGrants: Parameters<typeof consumeInheritedAskGrant>[0];
|
|
56
|
+
/** borrowed-readonly — the run's own approver seat (spec over deps), or undefined: the resolve target, the
|
|
57
|
+
* live-approver test the carry mint reads, and the grant-reuse identity law's approver. */
|
|
58
|
+
onAsk: OnAsk | undefined;
|
|
59
|
+
/** borrowed-mutable — the per-task human-review accumulator (`Prepared.humanReviewRef`). Writers here: every
|
|
60
|
+
* synchronous resolve that a person actually judged (count / totalWaitMs / gates push); runtask adds the durable
|
|
61
|
+
* resume latency. */
|
|
62
|
+
humanReviewRef: Prepared["humanReviewRef"];
|
|
63
|
+
/** borrowed-readonly — the injectable wall clock every human-time observable reads (`Prepared.now`). */
|
|
64
|
+
now: () => number;
|
|
65
|
+
/** borrowed-readonly — the policy-chain phase's rule-offer factory (the suggestion members the ask mint threads). */
|
|
66
|
+
ruleOffersOf: ReturnType<typeof createRuleOffersOf>;
|
|
67
|
+
/** borrowed-readonly — the policy-chain phase's Runner-filled source identity (one factory, every ask mint site). */
|
|
68
|
+
askSourceIdentity: () => {
|
|
69
|
+
principal?: string;
|
|
70
|
+
sourceTaskId?: string;
|
|
71
|
+
fromSubagent?: true;
|
|
72
|
+
sourceAgentName?: string;
|
|
73
|
+
isDelegatedChild?: true;
|
|
74
|
+
};
|
|
75
|
+
/** borrowed-readonly — the policy-chain phase's per-tool risk-axes projection (`riskAxes` on the ask). */
|
|
76
|
+
riskAxesOf: (toolName: string) => {
|
|
77
|
+
riskAxes?: {
|
|
78
|
+
irreversible?: boolean;
|
|
79
|
+
egress?: boolean;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
/** borrowed-mutable — the auto-mode denial tracker, or undefined. Writer here: the carry mint arms the auto-deny
|
|
83
|
+
* window on it iff the seat about to be called is live (the tracker's one writer). */
|
|
84
|
+
autoModeDenialTracking: AutoModeDenialTracker | undefined;
|
|
85
|
+
/** borrowed-readonly — the REBOUND spec; only `taskId` is read (the late-settlement observer's tag). */
|
|
86
|
+
spec: Pick<TaskSpec, "taskId">;
|
|
87
|
+
/** borrowed-readonly — the deployment seats this lane reports on, as a Pick over the SAME `deps` object (receiver
|
|
88
|
+
* preserved for `deps.onError?.()`): the late-settlement observer's notice/error sinks and the hook-crash notifier. */
|
|
89
|
+
deps: Pick<RunnerDeps, "onNotice" | "onError">;
|
|
90
|
+
/** borrowed-readonly — the acquired session id (the observer's tag, the hook-phase error tag). */
|
|
91
|
+
sessionId: string;
|
|
92
|
+
/** borrowed-readonly — the run's invocation id (the observer's tag). */
|
|
93
|
+
runId: string;
|
|
94
|
+
/** borrowed-readonly — the resolved hook record; only `permissionDenied` is read (the deny observer's seat). */
|
|
95
|
+
hooks: Hooks | undefined;
|
|
96
|
+
/** borrowed-readonly — the validated per-prepare hook seat bound (the deny observer's timeout). */
|
|
97
|
+
hookTimeoutMs: number;
|
|
98
|
+
/** borrowed-readonly — the own hook-crash reporter (the deny observer's swallow-guarded report sink). */
|
|
99
|
+
notifyOwnHookCrash: (err: unknown) => void;
|
|
100
|
+
}
|
|
101
|
+
export interface PrepareAskLaneResult {
|
|
102
|
+
/** owned — the lane, or undefined exactly when `gateMachineryActive` is false (then the suspend saga, the boundary
|
|
103
|
+
* parks, the park closure and the gate station are not built either — they key off this seat). */
|
|
104
|
+
askLane: AskLane | undefined;
|
|
105
|
+
}
|
|
106
|
+
/** The M19 ask-lane phase body — prepareTask's in-stream ask stretch, verbatim (see the module header). */
|
|
107
|
+
export declare function prepareAskLane(input: PrepareAskLaneInput): PrepareAskLaneResult;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { primaryActivityArg } from "../arg-summary.js";
|
|
2
|
+
import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
|
|
3
|
+
import { isLiveApproverSeat, resolveAsk } from "../tool-policy.js";
|
|
4
|
+
import { composeCallSignal, raceAbort } from "./abort-race.js";
|
|
5
|
+
import { gateAskCarry, lateAskSettlementObserver } from "./denial-limit-arms.js";
|
|
6
|
+
import { consumeInheritedAskGrant } from "./inherited-ask-grants.js";
|
|
7
|
+
function createDenyObserverNotifier(hooks, hookTimeoutMs, signal, report) {
|
|
8
|
+
if (hooks?.permissionDenied === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
return async (payload) => {
|
|
11
|
+
try {
|
|
12
|
+
const seat = await runHookSeat("permissionDenied", { timeoutMs: hookTimeoutMs, signal }, (sig) => hooks.permissionDenied({ ...payload, signal: sig }));
|
|
13
|
+
if (seat.expired)
|
|
14
|
+
report(hookSeatExpiredError("permissionDenied", hookTimeoutMs, seat.cause, "the deny observation was abandoned; the deny itself is unchanged"));
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
report(err);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const sanitizePreview = (node, depth = 0) => {
|
|
22
|
+
if (depth > 6)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (typeof node === "string") {
|
|
25
|
+
return node.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "\u2400");
|
|
26
|
+
}
|
|
27
|
+
if (node === null || typeof node !== "object")
|
|
28
|
+
return node;
|
|
29
|
+
if (Array.isArray(node))
|
|
30
|
+
return node.map((v) => sanitizePreview(v, depth + 1));
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [k, v] of Object.entries(node)) {
|
|
33
|
+
out[sanitizePreview(k, depth + 1)] = sanitizePreview(v, depth + 1);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
};
|
|
37
|
+
function resolveApprovalPreview(tools, toolName, args) {
|
|
38
|
+
const t = tools.find((x) => x.name === toolName || (x.aliases?.includes(toolName) ?? false));
|
|
39
|
+
if (t?.approvalPreview === undefined)
|
|
40
|
+
return {};
|
|
41
|
+
try {
|
|
42
|
+
const raw = t.approvalPreview(args);
|
|
43
|
+
if (raw === undefined)
|
|
44
|
+
return {};
|
|
45
|
+
const bytes = JSON.stringify(raw);
|
|
46
|
+
if (bytes === undefined)
|
|
47
|
+
return { withheld: "unavailable" };
|
|
48
|
+
if (bytes.length > 16_384) {
|
|
49
|
+
return { preview: { truncated: true, note: `approval preview exceeded 16KiB (${bytes.length} chars serialized)` }, withheld: "oversize" };
|
|
50
|
+
}
|
|
51
|
+
return { preview: sanitizePreview(raw) };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { withheld: "unavailable" };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function prepareAskLane(input) {
|
|
58
|
+
const { gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash } = input;
|
|
59
|
+
if (!gateMachineryActive)
|
|
60
|
+
return { askLane: undefined };
|
|
61
|
+
const composedCallSignal = (callSignal) => composeCallSignal(abortController.signal, callSignal);
|
|
62
|
+
const adjudicate = effectivePolicy
|
|
63
|
+
? (req, callSignal) => {
|
|
64
|
+
const signal = composedCallSignal(callSignal);
|
|
65
|
+
return raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}) }, signal)), signal, () => ({
|
|
66
|
+
action: "deny",
|
|
67
|
+
message: "policy check aborted (task timed out or cancelled)",
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
: undefined;
|
|
71
|
+
const approvalPreviewOf = (toolName, args) => resolveApprovalPreview(tools, toolName, args);
|
|
72
|
+
const resolveAskBound = async (decision, req, callSignal) => {
|
|
73
|
+
if (inheritedUnavailableAsks.delete(req.toolCallId)) {
|
|
74
|
+
return {
|
|
75
|
+
action: "deny",
|
|
76
|
+
message: `approval for "${req.toolName}" requires an ancestor task's approval that cannot be resolved here ` +
|
|
77
|
+
`(no live approver reachable, or a durable-park mandate applies), and no durable approval park is ` +
|
|
78
|
+
`available — denied fail-closed (the inherited constraint stands).`,
|
|
79
|
+
decisionReason: "mode",
|
|
80
|
+
settlement: { kind: "approver_unavailable", who: { party: "none" }, when: now() },
|
|
81
|
+
approverUnavailable: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const grantReuse = consumeInheritedAskGrant(inheritedAskGrants, onAsk, humanReviewRef, decision, req);
|
|
85
|
+
if (grantReuse !== undefined)
|
|
86
|
+
return grantReuse;
|
|
87
|
+
const t0 = now();
|
|
88
|
+
const resolved = await resolveAsk({
|
|
89
|
+
toolName: req.toolName,
|
|
90
|
+
toolCallId: req.toolCallId,
|
|
91
|
+
args: req.args,
|
|
92
|
+
...(() => {
|
|
93
|
+
const p = approvalPreviewOf(req.toolName, req.args);
|
|
94
|
+
return { ...(p.preview !== undefined ? { preview: p.preview } : {}), ...(p.withheld !== undefined ? { previewWithheld: p.withheld } : {}) };
|
|
95
|
+
})(),
|
|
96
|
+
...ruleOffersOf(req.toolName, req.args, decision.action === "ask" ? decision : undefined),
|
|
97
|
+
message: decision.message ?? `approval required for "${req.toolName}"`,
|
|
98
|
+
...askSourceIdentity(),
|
|
99
|
+
...riskAxesOf(req.toolName),
|
|
100
|
+
...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
101
|
+
...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
|
|
102
|
+
...(decision.action === "ask" ? gateAskCarry(decision, isLiveApproverSeat(onAsk), autoModeDenialTracking) : {}),
|
|
103
|
+
...(decision.action === "ask" && decision.probeReason !== undefined ? { probeReason: decision.probeReason } : {}),
|
|
104
|
+
...(decision.action === "ask" && decision.probeCause !== undefined ? { probeCause: decision.probeCause } : {}),
|
|
105
|
+
...(decision.action === "ask" && decision.ruleEvidence !== undefined ? { ruleEvidence: decision.ruleEvidence } : {}),
|
|
106
|
+
}, onAsk, composedCallSignal(callSignal), lateAskSettlementObserver({ toolName: req.toolName, toolCallId: req.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
|
|
107
|
+
const waitMs = Math.max(0, now() - t0);
|
|
108
|
+
if (resolved.approverUnavailable !== true && resolved.settlement?.kind !== "task_aborted") {
|
|
109
|
+
humanReviewRef.count += 1;
|
|
110
|
+
humanReviewRef.totalWaitMs += waitMs;
|
|
111
|
+
const toolArg = primaryActivityArg(req.args);
|
|
112
|
+
humanReviewRef.gates.push({
|
|
113
|
+
kind: "human",
|
|
114
|
+
waitMs,
|
|
115
|
+
decision: resolved.action === "allow" ? "allow" : "deny",
|
|
116
|
+
toolName: req.toolName,
|
|
117
|
+
...(toolArg !== undefined ? { toolArg } : {}),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return resolved;
|
|
121
|
+
};
|
|
122
|
+
const notifyPermissionDenied = createDenyObserverNotifier(hooks, hookTimeoutMs, abortController.signal, notifyOwnHookCrash);
|
|
123
|
+
const notifyHookError = (err) => {
|
|
124
|
+
try {
|
|
125
|
+
deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId });
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
return { askLane: { composedCallSignal, adjudicate, approvalPreviewOf, resolveAskBound, notifyPermissionDenied, notifyHookError } };
|
|
131
|
+
}
|