@sema-agent/core 5.32.0 → 5.33.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 +63 -0
- package/dist/agents/cascade.d.ts +49 -1
- package/dist/agents/cascade.js +2 -2
- package/dist/agents/verify.d.ts +70 -4
- package/dist/agents/verify.js +62 -16
- package/dist/core/checkpoint-store.d.ts +95 -0
- package/dist/core/checkpoint-store.js +40 -0
- package/dist/core/hooks.d.ts +14 -6
- package/dist/core/hooks.js +14 -3
- package/dist/core/memory-engine/file-backend.d.ts +172 -22
- package/dist/core/memory-engine/file-backend.js +877 -79
- package/dist/core/memory-engine/memory-backend-contract.js +33 -0
- package/dist/core/runner/assemble-result.d.ts +7 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +72 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +126 -0
- package/dist/core/runner/prepare-config-doors.d.ts +140 -0
- package/dist/core/runner/prepare-config-doors.js +250 -0
- package/dist/core/runner/prepare-safety-scan.d.ts +53 -0
- package/dist/core/runner/prepare-safety-scan.js +80 -0
- package/dist/core/runner/prepare-task.d.ts +27 -81
- package/dist/core/runner/prepare-task.js +83 -586
- package/dist/core/runner/prepare-workspace-restore.d.ts +102 -0
- package/dist/core/runner/prepare-workspace-restore.js +144 -0
- package/dist/core/runner/runtask.js +8 -2
- package/dist/core/tool-policy.d.ts +25 -0
- package/dist/core/types.d.ts +140 -13
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/orchestration/workflow-governance.d.ts +6 -4
- package/dist/tools/fs/bash-readonly-classifier.d.ts +9 -3
- package/dist/tools/fs/bash-readonly-classifier.js +4 -1
- package/dist/tools/fs/fs-bash.d.ts +19 -3
- package/dist/tools/fs/fs-bash.js +26 -1
- package/dist/tools/fs/index.d.ts +20 -4
- package/dist/tools/fs/index.js +4 -1
- package/dist/tools/fs/read-deny.d.ts +66 -8
- package/dist/tools/fs/read-deny.js +75 -39
- package/dist/tools/fs/read-face.d.ts +3 -2
- package/dist/tools/fs/search.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.33.0 — 2026-08-14
|
|
4
|
+
|
|
5
|
+
### BREAKING (operational)
|
|
6
|
+
|
|
7
|
+
- memory-engine file backend: ledger schema v2 (design/186). Slug/scope rebinds and cross-scope
|
|
8
|
+
transfers are journaled transactions (`transfers.jsonl` + journaled rebind with a tri-state
|
|
9
|
+
projection probe); a v2 journal is REFUSED LOUDLY by pre-5.33 readers. Mixed-version deployments:
|
|
10
|
+
drain writers BEFORE downgrading (ship post carries the three-step rollback and the drain
|
|
11
|
+
precondition). Downgrade is fail-closed by design, not an oversight.
|
|
12
|
+
|
|
13
|
+
### Behavior changes (named)
|
|
14
|
+
|
|
15
|
+
- read-deny built-in table is deployment-configurable (#245, ruled): four tiers
|
|
16
|
+
(credentials 18 / shell-history 2 / browser 9 / wallet 6); the two shell-history rows
|
|
17
|
+
(`.bash_history` / `.zsh_history`) are OFF by default (ruled widening). Knobs:
|
|
18
|
+
`readDenyBuiltinTiers` (whole tiers) + `readDenyBuiltinExclude` (canonical pattern names),
|
|
19
|
+
deployment seats only (TaskSpec carries neither), every bad value refuses loudly. Persisted
|
|
20
|
+
checkpoint face rows freeze EXPLICIT additions only — built-in liveness stays a deployment
|
|
21
|
+
authority across suspend/resume (deploy-time exclude applies to resumed legs; explicit denies
|
|
22
|
+
survive verbatim).
|
|
23
|
+
- verify/fix hands-less legs run the FULL read-face resolution (#242): one unified else-branch
|
|
24
|
+
(resolver + one deny compile over deps∪spec∪seed + carrier). Five narrowings enumerated in the
|
|
25
|
+
ship post; consumers keying on seat ABSENCE must re-key (absence now = "prepare never completed").
|
|
26
|
+
- built-in deny config snapshot (#245 interference fix): prepare reads
|
|
27
|
+
`readDenyBuiltinTiers` / `readDenyBuiltinExclude` ONCE and every station eats the snapshot —
|
|
28
|
+
previously the toolkit mount re-read the caller-retained arrays past an await, so a hot-reload
|
|
29
|
+
mutation mid-prepare could give one task two different built-in deny sets.
|
|
30
|
+
- internals activity sink containment (#248): a throwing `onActivity` no longer fails the leg it
|
|
31
|
+
fires on (previously: leg terminated `failed` misclassified as `provider.error`; inside a cascade
|
|
32
|
+
the false FAIL escalated and re-ran non-idempotent writes — measured). Contained via the RB-463
|
|
33
|
+
safe-notifier; first failure per site disclosed via `console.warn`, later ones counted.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Structured reversibility-probe cause (#239): `ToolSpec.reversibilityProbe` may return a
|
|
38
|
+
`ReversibilityVerdict` (`{reversible, reason?, cause?}`); `AskRequest.probeCause` /
|
|
39
|
+
`RiskDescriptor.probeCause` carry `{code, roots: {shown, total}, further?}` with per-item caps,
|
|
40
|
+
percent-encoded separators and undercount coercion. The cause NEVER enters the ask `message`
|
|
41
|
+
(that text feeds the auto-mode classifier). #95 closed by the same structure.
|
|
42
|
+
- TaskResult observation seats `effectiveReadFace` / `effectiveReadDenyPatterns` (#240, widened by
|
|
43
|
+
#242): present on every leg that completes prepare — including every hands-less leg — and ride
|
|
44
|
+
every terminal, not just `completed`. verifyCompleted folds them stricter-wins into the returned
|
|
45
|
+
result (`carryFrozenPosture`) so a chained gate cannot widen from a fix leg's own seats.
|
|
46
|
+
- verify/cascade run-internals seat (#246): `runWithVerification` / `resumeWithVerification` /
|
|
47
|
+
`runCascade` / `verifyCompleted` / `runDeveloperTask` take a tail-optional `internals` —
|
|
48
|
+
forwarded WHOLE by identity to every leg; `isolation` is refused loudly
|
|
49
|
+
(`config.verify_internals_invalid`, distinct door / before-leg phases, re-checked before every
|
|
50
|
+
dispatch; the before-leg refusal is ordered AFTER the durable-pause hand-back). `RunInternals` +
|
|
51
|
+
`ResolvedWorkspace` are now package-root type exports.
|
|
52
|
+
- prepare-task phase extraction (design/238 batches 1+2): prepare-config-doors / safety-scan /
|
|
53
|
+
acquire-reconcile / workspace-restore phase modules, verbatim slices behind Input/Result seams;
|
|
54
|
+
phase-API rules piece ratified (D-9). Internal restructuring — zero wire change; body span
|
|
55
|
+
ratchet banked 10138 → 9420. Both new modules joined the fail-open scan surface.
|
|
56
|
+
- Retry-progress disclosure documented as built (#223): the per-attempt `status` TaskEvent family
|
|
57
|
+
(phase/attempt/maxRetries/retryInSec/errClass, 30s slice replay, exactly-one terminal frame) has
|
|
58
|
+
shipped since v5.22.0; the EngineNotice family table now records the negative space (no notice
|
|
59
|
+
family duplicates it).
|
|
60
|
+
|
|
61
|
+
### Notes
|
|
62
|
+
|
|
63
|
+
- #244 (scan-vs-mount TOCTOU): ruled — roster snapshot form, lands in a normal window (not this one).
|
|
64
|
+
- Governed workflow key candidates (excludeTools/handsReadOnly/shellGate): ruled OBSERVED-NOT-ADDED.
|
|
65
|
+
|
|
3
66
|
## 5.32.0 — 2026-08-14
|
|
4
67
|
|
|
5
68
|
No BREAKING changes. One additive disclosure surface; zero resolution-result changes (the read-face
|
package/dist/agents/cascade.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Runner } from "../core/runner/runtask.js";
|
|
2
|
+
import type { RunInternals } from "../core/runner/prepare-task.js";
|
|
2
3
|
import type { ModelRef, TaskResult, TaskSpec } from "../core/types.js";
|
|
3
4
|
/**
|
|
4
5
|
* Quality-gate cascade (design/27). Run the SAME task across a ladder of models (cheapest → strongest);
|
|
@@ -153,4 +154,51 @@ export interface CascadeRunResult extends TaskResult {
|
|
|
153
154
|
* (`effect: "write"`), those execute once PER rung — ensure idempotency, or gate before the side effect
|
|
154
155
|
* (e.g. a structured-output gate). `TaskSpec` never guaranteed idempotency; cascade makes it explicit.
|
|
155
156
|
*/
|
|
156
|
-
export declare function runCascade(runner: Runner, spec: TaskSpec, config: CascadeConfig
|
|
157
|
+
export declare function runCascade(runner: Runner, spec: TaskSpec, config: CascadeConfig,
|
|
158
|
+
/**
|
|
159
|
+
* The TRUSTED, run-scoped internals channel — the same object `Runner.runTask` takes as its second
|
|
160
|
+
* argument — forwarded UNCHANGED to EVERY rung.
|
|
161
|
+
*
|
|
162
|
+
* WHY THE SEAT EXISTS: `runCascade` returns a `TaskResult` and DRAINS each rung's event stream
|
|
163
|
+
* internally, so the host never holds a `TaskStream` for any rung. Without this parameter a
|
|
164
|
+
* deployment can observe nothing a rung did — tool beats, forwarded child progress, the resolved
|
|
165
|
+
* workspace — only the per-rung {@link CascadeAttempt} audit rows and the final result. Wiring an
|
|
166
|
+
* observation sink here (`onActivity`, `onForwardEvent`, `onWorkspaceResolved`) is the only way in.
|
|
167
|
+
*
|
|
168
|
+
* ATTRIBUTION: rungs run SEQUENTIALLY and each is dispatched with the SAME object, so one sink
|
|
169
|
+
* receives the rungs' beats CONCATENATED in ladder order. `ToolActivity` carries no rung identity of
|
|
170
|
+
* its own — the frames of two rungs running the same tool are indistinguishable in content, so the
|
|
171
|
+
* boundary a host keys on is {@link CascadeConfig.onRung}, which fires only after its own rung's
|
|
172
|
+
* beats have been delivered (the sole rung that ends without one is a durable pause, which returns
|
|
173
|
+
* immediately). Two honesty notes: SUBAGENT events forwarded through `onForwardEvent` do carry their
|
|
174
|
+
* own `sourceTaskId`, and a BACKGROUND child outlives the rung that spawned it — its frames can
|
|
175
|
+
* arrive after that rung's `onRung`, i.e. inside the next rung's segment.
|
|
176
|
+
*
|
|
177
|
+
* FAILURE-ISOLATED since #248, by the same primitive as {@link CascadeConfig.onRung}: engine-invoked
|
|
178
|
+
* through a run-scoped safe notifier — a throwing `onActivity` is contained (first failure per site
|
|
179
|
+
* disclosed via `console.warn`, later ones counted) and no longer faults the rung. Before #248 the
|
|
180
|
+
* fault AMPLIFIED: the faulted rung was rejected by the gate, the cascade escalated, and a
|
|
181
|
+
* non-idempotent tool the rung had ALREADY executed ran AGAIN on the next rung — the MEASURED
|
|
182
|
+
* HAZARD pin that recorded that now pins containment.
|
|
183
|
+
*
|
|
184
|
+
* FORWARDED WHOLE, NOT FILTERED: every field rides every rung, including the ref-valued ones
|
|
185
|
+
* (`peerSelfRef`, `ownOrgAdmissionRef`) whose own contract is one ref per RUN — wiring those here
|
|
186
|
+
* makes the ladder's rungs share one carrier. The seat is meant for observation sinks; anything else
|
|
187
|
+
* is a choice the caller is making explicitly, not a default this layer applies.
|
|
188
|
+
*
|
|
189
|
+
* `isolation: "worktree"` in particular rides through here, where the verification gate REFUSES it —
|
|
190
|
+
* an asymmetry with a reason. That field mints a NEW workspace per RUN. The gate's runs are one
|
|
191
|
+
* artifact judged and repaired by several legs, so separate workspaces would have the verifier
|
|
192
|
+
* grading files the implementation never wrote to; a ladder's rungs are INDEPENDENT COLD RE-RUNS of
|
|
193
|
+
* the same task (this function's own contract), so one workspace per rung is the coherent reading —
|
|
194
|
+
* each attempt gets its own tree. What a host must then know, stated without promises this function
|
|
195
|
+
* cannot keep: the returned result is ONE rung's, its work lives in THAT rung's workspace, and no
|
|
196
|
+
* field of the result names it — `onWorkspaceResolved` is the only place the path appears, once per
|
|
197
|
+
* rung in ladder order. Whether the path is still readable afterwards is the FACTORY's contract, not
|
|
198
|
+
* this function's: each rung's factory-minted env is torn down at the end of that rung's run, and
|
|
199
|
+
* what teardown does to the workspace is the factory's business.
|
|
200
|
+
*
|
|
201
|
+
* Absent ⇒ every rung is dispatched exactly as before (the call site passes `undefined`, which is
|
|
202
|
+
* what an omitted optional argument already was).
|
|
203
|
+
*/
|
|
204
|
+
internals?: RunInternals): Promise<CascadeRunResult>;
|
package/dist/agents/cascade.js
CHANGED
|
@@ -14,7 +14,7 @@ function createDefaultGate(spec) {
|
|
|
14
14
|
const requiresStructured = spec.outputSchema != null;
|
|
15
15
|
return (result) => result.status === "completed" && (!requiresStructured || result.structuredOutput !== undefined);
|
|
16
16
|
}
|
|
17
|
-
export async function runCascade(runner, spec, config) {
|
|
17
|
+
export async function runCascade(runner, spec, config, internals) {
|
|
18
18
|
const ladder = config.ladder;
|
|
19
19
|
if (!ladder || ladder.length === 0) {
|
|
20
20
|
throw new Error("runCascade: config.ladder must have at least one rung");
|
|
@@ -82,7 +82,7 @@ export async function runCascade(runner, spec, config) {
|
|
|
82
82
|
: rungAbort.signal;
|
|
83
83
|
let result;
|
|
84
84
|
try {
|
|
85
|
-
result = await runner.runTask({ ...specBase, model: rung.model, ...(rung.overrides ?? {}), ...(rungSignal ? { signal: rungSignal } : {}) });
|
|
85
|
+
result = await runner.runTask({ ...specBase, model: rung.model, ...(rung.overrides ?? {}), ...(rungSignal ? { signal: rungSignal } : {}) }, internals);
|
|
86
86
|
}
|
|
87
87
|
finally {
|
|
88
88
|
if (deadlineTimer !== undefined)
|
package/dist/agents/verify.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type, type Static } from "typebox";
|
|
2
2
|
import type { Runner, ResumeTaskConfig } from "../core/runner/runtask.js";
|
|
3
|
+
import type { RunInternals } from "../core/runner/prepare-task.js";
|
|
3
4
|
import type { CheckpointGate, CheckpointToken, ResumeOutcome } from "../core/checkpoint-store.js";
|
|
4
5
|
import type { ModelRef, TaskResult, TaskSpec, ToolSpec } from "../core/types.js";
|
|
5
6
|
/**
|
|
@@ -217,7 +218,54 @@ export interface VerificationResult extends TaskResult {
|
|
|
217
218
|
* override is given, the verifier uses {@link STATIC_VERIFICATION_PROMPT} (read the diff/results; don't try
|
|
218
219
|
* to execute — that was L2's job), avoiding the "PARTIAL on everything" failure in a read-only sandbox.
|
|
219
220
|
*/
|
|
220
|
-
export declare function verifyCompleted(runner: Runner, result: TaskResult, specBase: ResumeTaskConfig, objective: string, config: VerifyConfig
|
|
221
|
+
export declare function verifyCompleted(runner: Runner, result: TaskResult, specBase: ResumeTaskConfig, objective: string, config: VerifyConfig,
|
|
222
|
+
/**
|
|
223
|
+
* The TRUSTED, run-scoped internals channel — the same object `Runner.runTask` takes as its second
|
|
224
|
+
* argument — forwarded UNCHANGED to EVERY leg this gate drives: the verifier run of each round and
|
|
225
|
+
* each fix continuation (and, from {@link runWithVerification} / {@link resumeWithVerification} /
|
|
226
|
+
* {@link runDeveloperTask}, the implementation leg as well). This doc is the contract for the whole
|
|
227
|
+
* family; the sibling entries carry the same parameter and point here.
|
|
228
|
+
*
|
|
229
|
+
* WHY THE SEAT EXISTS: these entries return a `TaskResult` and DRAIN each leg's event stream
|
|
230
|
+
* internally, so the host never holds a `TaskStream` for any of them. Without this parameter a
|
|
231
|
+
* deployment can observe nothing a leg did — tool beats, forwarded child progress, the resolved
|
|
232
|
+
* workspace — only the final result. Wiring an observation sink here (`onActivity`,
|
|
233
|
+
* `onForwardEvent`, `onWorkspaceResolved`) is the only way in.
|
|
234
|
+
*
|
|
235
|
+
* ATTRIBUTION — WEAK, and deliberately stated as such: the legs run SEQUENTIALLY (impl → verifier →
|
|
236
|
+
* fix → verifier → …) and each is dispatched with the SAME object, so a leg's own tool beats are all
|
|
237
|
+
* delivered before the next leg is dispatched. That ORDERING is the whole of it. `ToolActivity`
|
|
238
|
+
* carries no leg identity, so generic activity CANNOT be attributed to a leg from the frame alone:
|
|
239
|
+
* two adjacent legs calling the same tool are indistinguishable, and the structured-output tool is
|
|
240
|
+
* no verifier marker either (an impl spec carrying its own `outputSchema` mounts it too).
|
|
241
|
+
* {@link VerifyConfig.onRound} bounds a ROUND, not a leg — the segment before it holds the impl (or
|
|
242
|
+
* fix) leg's beats AND that round's verifier beats together. A host that needs true per-leg
|
|
243
|
+
* attribution must drive the legs itself (one `runner.runTask` per leg) rather than read it out of
|
|
244
|
+
* this stream. Two further honesty notes: SUBAGENT events forwarded through `onForwardEvent` do
|
|
245
|
+
* carry their own `sourceTaskId`, and a BACKGROUND child outlives the leg that spawned it — its
|
|
246
|
+
* frames can arrive after that leg returned, i.e. interleaved into a later leg's segment.
|
|
247
|
+
*
|
|
248
|
+
* FAILURE-ISOLATED since #248, by the same primitive as {@link VerifyConfig.onRound} /
|
|
249
|
+
* {@link VerifyConfig.onVerifierPause}: the fields on this object are invoked by the ENGINE on the
|
|
250
|
+
* run's own path, through a run-scoped safe notifier — a throwing `onActivity` is contained (first
|
|
251
|
+
* failure per site disclosed via `console.warn`, later ones counted) and never faults the leg it
|
|
252
|
+
* fires on. Before #248 a throw here failed the leg (misclassified `provider.error`) and an impl
|
|
253
|
+
* leg was reported incomplete — the MEASURED HAZARD pins that recorded that now pin containment.
|
|
254
|
+
*
|
|
255
|
+
* FORWARDED WHOLE, NOT FILTERED, with ONE refusal: every field rides every leg, including the
|
|
256
|
+
* ref-valued ones (`peerSelfRef`, `ownOrgAdmissionRef`) whose own contract is one ref per RUN —
|
|
257
|
+
* wiring those here makes the gate's legs share one carrier. The seat is meant for observation
|
|
258
|
+
* sinks; anything else is a choice the caller is making explicitly, not a default this layer
|
|
259
|
+
* applies. The single exception is `isolation`, which is REFUSED — at the entry door and again
|
|
260
|
+
* before each leg is dispatched, typed `config.verify_internals_invalid` — because it would mint a
|
|
261
|
+
* separate workspace per leg and break the one invariant the gate is for. The refusal's own doc
|
|
262
|
+
* (`refuseUnhonorableInternals`) carries the reasoning, the re-check rule, and what a mid-gate
|
|
263
|
+
* refusal does and does not undo.
|
|
264
|
+
*
|
|
265
|
+
* Absent ⇒ every leg is dispatched exactly as before (each call site passes `undefined`, which is
|
|
266
|
+
* what an omitted optional argument already was).
|
|
267
|
+
*/
|
|
268
|
+
internals?: RunInternals): Promise<VerificationResult>;
|
|
221
269
|
/**
|
|
222
270
|
* Run an implementation task, then gate it behind an independent falsification-style verifier, looping
|
|
223
271
|
* fix→re-verify until PASS (or a round cap). Returns the implementation result plus the
|
|
@@ -226,7 +274,12 @@ export declare function verifyCompleted(runner: Runner, result: TaskResult, spec
|
|
|
226
274
|
* failed-with-token (verdict `unverified`); the caller approves and calls {@link resumeWithVerification}
|
|
227
275
|
* to resume AND verify (design/51 P1-b: the durable + HITL + verify integration).
|
|
228
276
|
*/
|
|
229
|
-
export declare function runWithVerification(runner: Runner, implSpec: TaskSpec, config?: VerifyConfig
|
|
277
|
+
export declare function runWithVerification(runner: Runner, implSpec: TaskSpec, config?: VerifyConfig,
|
|
278
|
+
/** The trusted run-scoped internals channel, forwarded UNCHANGED to every leg this entry drives —
|
|
279
|
+
* the implementation run, then (on completion) the verifier + fix legs of the gate. Full contract,
|
|
280
|
+
* including the attribution and forwarded-whole rules, on {@link verifyCompleted}. Absent ⇒ every
|
|
281
|
+
* leg is dispatched exactly as before. */
|
|
282
|
+
internals?: RunInternals): Promise<VerificationResult>;
|
|
230
283
|
/**
|
|
231
284
|
* Resume a durable-suspended implementation task (design/45 F4) **and** verify it on completion — the
|
|
232
285
|
* durable + HITL + verify integration (design/51 P1-b). The mirror of {@link runWithVerification} for the
|
|
@@ -242,7 +295,14 @@ export declare function runWithVerification(runner: Runner, implSpec: TaskSpec,
|
|
|
242
295
|
* after approval, so a pre-suspend diff would grade stale code. The verifier already verifies the current working
|
|
243
296
|
* tree (verifierObjective), so dropping the stale diff closes the timing window.
|
|
244
297
|
*/
|
|
245
|
-
export declare function resumeWithVerification(runner: Runner, token: CheckpointToken, outcome: ResumeOutcome, taskConfig: ResumeTaskConfig, objective: string, config?: VerifyConfig
|
|
298
|
+
export declare function resumeWithVerification(runner: Runner, token: CheckpointToken, outcome: ResumeOutcome, taskConfig: ResumeTaskConfig, objective: string, config?: VerifyConfig,
|
|
299
|
+
/** The trusted run-scoped internals channel, forwarded UNCHANGED to every leg this entry drives —
|
|
300
|
+
* `Runner.resume`'s own internals argument for the RESUMED implementation leg, then (on completion)
|
|
301
|
+
* the verifier + fix legs of the gate. Note that this seat carries the resume-path duty of the same
|
|
302
|
+
* channel as well: a checkpoint suspended under OPAQUE inherited parent constraints re-supplies them
|
|
303
|
+
* through `internals.inheritedGate.parentConstraints`, which had no way in here before. Full
|
|
304
|
+
* contract on {@link verifyCompleted}. Absent ⇒ every leg is dispatched exactly as before. */
|
|
305
|
+
internals?: RunInternals): Promise<VerificationResult>;
|
|
246
306
|
export interface DeveloperTaskConfig extends VerifyConfig {
|
|
247
307
|
/** Run the verification gate. Default `true` (that's the point of developer mode). Set `false` for prompt-only. */
|
|
248
308
|
verify?: boolean;
|
|
@@ -255,4 +315,10 @@ export interface DeveloperTaskConfig extends VerifyConfig {
|
|
|
255
315
|
* Pair with a role map that gives a strong implementation model and cheaper helper/verifier models for
|
|
256
316
|
* "auto model selection" (design/28 §3.2): `roles: { default: strong, subagent: cheap, verifier: strong }`.
|
|
257
317
|
*/
|
|
258
|
-
export declare function runDeveloperTask(runner: Runner, spec: TaskSpec, config?: DeveloperTaskConfig
|
|
318
|
+
export declare function runDeveloperTask(runner: Runner, spec: TaskSpec, config?: DeveloperTaskConfig,
|
|
319
|
+
/** The trusted run-scoped internals channel, forwarded UNCHANGED to every leg this entry drives — in
|
|
320
|
+
* BOTH arms: the lone implementation run when `verify: false`, and the whole gate otherwise. The two
|
|
321
|
+
* arms differ in ONE respect: the gate's door refusal of `isolation` (see {@link verifyCompleted})
|
|
322
|
+
* applies only to the gated arm, since `verify: false` is a single run that can honor it. Full
|
|
323
|
+
* contract on {@link verifyCompleted}. Absent ⇒ every leg is dispatched exactly as before. */
|
|
324
|
+
internals?: RunInternals): Promise<VerificationResult>;
|
package/dist/agents/verify.js
CHANGED
|
@@ -61,6 +61,24 @@ export const VerdictSchema = Type.Object({
|
|
|
61
61
|
});
|
|
62
62
|
const VERIFY_ON_ROUND_SITE = "verify.onRound";
|
|
63
63
|
const VERIFY_ON_VERIFIER_PAUSE_SITE = "verify.onVerifierPause";
|
|
64
|
+
const VERIFY_INTERNALS_ERROR_CODE = "config.verify_internals_invalid";
|
|
65
|
+
function verifyInternalsError(message) {
|
|
66
|
+
const e = new Error(message);
|
|
67
|
+
e.code = VERIFY_INTERNALS_ERROR_CODE;
|
|
68
|
+
return e;
|
|
69
|
+
}
|
|
70
|
+
function refuseUnhonorableInternals(internals, phase) {
|
|
71
|
+
if (internals?.isolation !== undefined) {
|
|
72
|
+
throw verifyInternalsError(`the verification gate cannot honor internals.isolation ("${internals.isolation}"): it mints a new workspace per RUN, and this gate runs several of them ` +
|
|
73
|
+
`(implementation, verifier, and one per fix round), so the verifier would judge a checkout the implementation never wrote to. Isolate at the ` +
|
|
74
|
+
`execution-env factory instead, so every leg of the gate resolves the same root. ` +
|
|
75
|
+
(phase === "door"
|
|
76
|
+
? `This call dispatched no leg before refusing.`
|
|
77
|
+
: `The object gained the field AFTER this call's entry check (it is forwarded by identity, so anything running between legs can set it): ` +
|
|
78
|
+
`no FURTHER leg was dispatched. What already ran is not undone and is not returned — this throws instead of returning a result, so an ` +
|
|
79
|
+
`implementation leg keeps whatever it did (a consumed checkpoint stays consumed, its session stays the caller's to reclaim).`));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
64
82
|
function fixObjective(findings) {
|
|
65
83
|
const list = findings.length
|
|
66
84
|
? findings.map((f) => `- ${sanitizeUntrustedText(f)}`).join("\n")
|
|
@@ -81,16 +99,37 @@ function verifierObjective(originalObjective, impl, evidence) {
|
|
|
81
99
|
}
|
|
82
100
|
return parts.join("\n\n");
|
|
83
101
|
}
|
|
84
|
-
export async function verifyCompleted(runner, result, specBase, objective, config) {
|
|
102
|
+
export async function verifyCompleted(runner, result, specBase, objective, config, internals) {
|
|
103
|
+
refuseUnhonorableInternals(internals, "door");
|
|
85
104
|
if (result.status !== "completed") {
|
|
86
105
|
return { ...result, verification: { verdict: "unverified", unverifiedReason: "impl_incomplete", rounds: 0, findings: [] } };
|
|
87
106
|
}
|
|
88
107
|
const maxRounds = Number.isFinite(config.maxRounds) ? Math.max(1, Math.floor(config.maxRounds)) : 2;
|
|
89
108
|
const notifier = createSafeNotifier(config.onNotifyError !== undefined ? { onError: config.onNotifyError } : undefined);
|
|
90
109
|
const verifierTools = config.verifierTools ?? (specBase.tools ?? []).filter((t) => t.effect === "read");
|
|
110
|
+
const foldedRootsFace = specBase.readFace === "roots" || result.effectiveReadFace === "roots";
|
|
111
|
+
const foldedReadDeny = [...(specBase.readDenyPatterns ?? []), ...(result.effectiveReadDenyPatterns ?? [])];
|
|
112
|
+
const carryFrozenPosture = (r) => {
|
|
113
|
+
const face = result.effectiveReadFace === "roots" ? "roots" : r.effectiveReadFace;
|
|
114
|
+
const keyOf = (e) => `${e.caseSensitive ? "s" : "i"} ${e.pattern}`;
|
|
115
|
+
const seen = new Set((r.effectiveReadDenyPatterns ?? []).map(keyOf));
|
|
116
|
+
const merged = [...(r.effectiveReadDenyPatterns ?? [])];
|
|
117
|
+
for (const e of result.effectiveReadDenyPatterns ?? []) {
|
|
118
|
+
if (!seen.has(keyOf(e))) {
|
|
119
|
+
seen.add(keyOf(e));
|
|
120
|
+
merged.push({ ...e });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...r,
|
|
125
|
+
...(face !== undefined ? { effectiveReadFace: face } : {}),
|
|
126
|
+
...(merged.length > 0 ? { effectiveReadDenyPatterns: merged } : {}),
|
|
127
|
+
};
|
|
128
|
+
};
|
|
91
129
|
const evidenceMode = config.evidence != null && config.evidence.trim() !== "";
|
|
92
130
|
const verifierPrompt = config.verifierPrompt ?? (evidenceMode ? STATIC_VERIFICATION_PROMPT : VERIFICATION_PROMPT);
|
|
93
131
|
const runVerifier = async (impl, round) => {
|
|
132
|
+
refuseUnhonorableInternals(internals, "before_leg");
|
|
94
133
|
const roundEvidence = round === 1 ? config.evidence : undefined;
|
|
95
134
|
const v = await runner.runTask({
|
|
96
135
|
objective: verifierObjective(objective, impl, roundEvidence),
|
|
@@ -99,8 +138,8 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
|
|
|
99
138
|
roles: specBase.roles,
|
|
100
139
|
tools: verifierTools,
|
|
101
140
|
handsReadOnly: config.verifierHandsReadOnly ?? true,
|
|
102
|
-
...(
|
|
103
|
-
...(
|
|
141
|
+
...(foldedRootsFace ? { readFace: "roots" } : {}),
|
|
142
|
+
...(foldedReadDeny.length > 0 ? { readDenyPatterns: [...foldedReadDeny] } : {}),
|
|
104
143
|
interactiveTools: false,
|
|
105
144
|
outputSchema: VerdictSchema,
|
|
106
145
|
enableBlockedReport: false,
|
|
@@ -113,7 +152,7 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
|
|
|
113
152
|
...(specBase.clientContext !== undefined ? { clientContext: { ...specBase.clientContext } } : {}),
|
|
114
153
|
...(specBase.promptProfile !== undefined ? { promptProfile: specBase.promptProfile } : {}),
|
|
115
154
|
signal: specBase.signal,
|
|
116
|
-
});
|
|
155
|
+
}, internals);
|
|
117
156
|
try {
|
|
118
157
|
const cost = (v.stats.costMicroUsd ?? 0) + (v.stats.nested?.costMicroUsd ?? 0);
|
|
119
158
|
return { verdict: v.structuredOutput, cost, ...(isDurablePause(v.status) ? { paused: v } : {}) };
|
|
@@ -183,39 +222,46 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
|
|
|
183
222
|
break;
|
|
184
223
|
if (config.costCeilingMicroUsd != null && spend >= config.costCeilingMicroUsd)
|
|
185
224
|
break;
|
|
225
|
+
refuseUnhonorableInternals(internals, "before_leg");
|
|
186
226
|
current = await runner.runTask({
|
|
187
227
|
...specBase,
|
|
188
228
|
sessionId: current.sessionId,
|
|
189
229
|
objective: fixObjective(outcome.findings),
|
|
190
|
-
|
|
230
|
+
...(foldedRootsFace ? { readFace: "roots" } : {}),
|
|
231
|
+
...(foldedReadDeny.length > 0 ? { readDenyPatterns: [...foldedReadDeny] } : {}),
|
|
232
|
+
}, internals);
|
|
191
233
|
spend += (current.stats.costMicroUsd ?? 0) + (current.stats.nested?.costMicroUsd ?? 0);
|
|
192
234
|
if (isDurablePause(current.status)) {
|
|
193
|
-
return { ...mapNestedSuspend(current), verification: outcome };
|
|
235
|
+
return { ...mapNestedSuspend(carryFrozenPosture(current)), verification: outcome };
|
|
194
236
|
}
|
|
195
237
|
if (current.status !== "completed")
|
|
196
238
|
break;
|
|
197
239
|
}
|
|
198
|
-
return { ...current, verification: outcome };
|
|
240
|
+
return { ...carryFrozenPosture(current), verification: outcome };
|
|
199
241
|
}
|
|
200
|
-
export async function runWithVerification(runner, implSpec, config = {}) {
|
|
201
|
-
|
|
242
|
+
export async function runWithVerification(runner, implSpec, config = {}, internals) {
|
|
243
|
+
refuseUnhonorableInternals(internals, "door");
|
|
244
|
+
const result = await runner.runTask(implSpec, internals);
|
|
202
245
|
if (isDurablePause(result.status)) {
|
|
203
246
|
return { ...mapNestedSuspend(result), verification: { verdict: "unverified", unverifiedReason: result.status === "needs_review" ? "needs_review" : "suspended", rounds: 0, findings: [] } };
|
|
204
247
|
}
|
|
205
|
-
|
|
248
|
+
refuseUnhonorableInternals(internals, "before_leg");
|
|
249
|
+
return verifyCompleted(runner, result, implSpec, implSpec.objective, config, internals);
|
|
206
250
|
}
|
|
207
|
-
export async function resumeWithVerification(runner, token, outcome, taskConfig, objective, config = {}) {
|
|
208
|
-
|
|
251
|
+
export async function resumeWithVerification(runner, token, outcome, taskConfig, objective, config = {}, internals) {
|
|
252
|
+
refuseUnhonorableInternals(internals, "door");
|
|
253
|
+
const result = await runner.resume(token, outcome, taskConfig, internals);
|
|
209
254
|
if (isDurablePause(result.status)) {
|
|
210
255
|
return { ...mapNestedSuspend(result), verification: { verdict: "unverified", unverifiedReason: result.status === "needs_review" ? "needs_review" : "suspended", rounds: 0, findings: [] } };
|
|
211
256
|
}
|
|
212
|
-
|
|
257
|
+
refuseUnhonorableInternals(internals, "before_leg");
|
|
258
|
+
return verifyCompleted(runner, result, taskConfig, objective, config, internals);
|
|
213
259
|
}
|
|
214
|
-
export async function runDeveloperTask(runner, spec, config = {}) {
|
|
260
|
+
export async function runDeveloperTask(runner, spec, config = {}, internals) {
|
|
215
261
|
const implSpec = { systemPrompt: CODE_AGENT_PROMPT, ...spec };
|
|
216
262
|
if (config.verify === false) {
|
|
217
|
-
const result = await runner.runTask(implSpec);
|
|
263
|
+
const result = await runner.runTask(implSpec, internals);
|
|
218
264
|
return { ...result, verification: { verdict: "unverified", unverifiedReason: "opted_out", rounds: 0, findings: [] } };
|
|
219
265
|
}
|
|
220
|
-
return runWithVerification(runner, implSpec, config);
|
|
266
|
+
return runWithVerification(runner, implSpec, config, internals);
|
|
221
267
|
}
|
|
@@ -140,7 +140,94 @@ export interface RiskDescriptor {
|
|
|
140
140
|
* carries the same disclosure the synchronous ask does. The inbox renders "their rule is alive,
|
|
141
141
|
* just outranked" instead of the person concluding their rule silently broke. */
|
|
142
142
|
shadowedRule?: string;
|
|
143
|
+
/** backlog #239 (additive): WHY the reversibility probe did not clear this call, in the probe's own
|
|
144
|
+
* words — the cause the tighten acted on, which until now existed only inside the probe and left the
|
|
145
|
+
* card showing WHAT is gated with no account of WHY. Present only for a `"maybe"`-tier tighten whose
|
|
146
|
+
* probe supplied a cause (an `"always"`-tier tighten runs no probe; a timeout/throw yields no verdict).
|
|
147
|
+
* Neutralized + capped at {@link PROBE_REASON_MAX} — it is deployment-authored text on a human trust
|
|
148
|
+
* boundary, so it is DISPLAY/TRIAGE metadata only, never adjudication input.
|
|
149
|
+
*
|
|
150
|
+
* The UNSTRUCTURED face: a deployment's own probe writes prose here and this engine cannot say what it
|
|
151
|
+
* means. An engine-minted cause uses {@link probeCause} instead — see that field for why. */
|
|
152
|
+
probeReason?: string;
|
|
153
|
+
/** backlog #239 (additive): the STRUCTURED cause, for a probe whose verdict this engine understands —
|
|
154
|
+
* the built-in shell probe is the first. Carries a machine-readable {@link ProbeCause.code} and the
|
|
155
|
+
* operand families as ARRAYS with honest totals, so a consumer renders its own sentence in its own
|
|
156
|
+
* language instead of receiving one this engine had to write in advance.
|
|
157
|
+
*
|
|
158
|
+
* The prose sibling above is what this replaces for engine-minted causes, and the reason is not
|
|
159
|
+
* aesthetic: a sentence asserting what a command DOES is false for the demotion's documented
|
|
160
|
+
* over-fire, a sentence asserting what its OPERANDS are is false when the check synthesizes one, and
|
|
161
|
+
* serializing a variable-length multi-part disclosure into one length-capped string silently deletes
|
|
162
|
+
* whichever part sorts last. A code cannot be false and an array cannot be truncated into a lie. */
|
|
163
|
+
probeCause?: ProbeCause;
|
|
164
|
+
}
|
|
165
|
+
/** backlog #239: one operand family inside a {@link ProbeCause} — the entries a card may NAME, plus how
|
|
166
|
+
* many the family really has. `shown.length` ≤ {@link total}, and a consumer that renders only `shown`
|
|
167
|
+
* is still obliged to tell the person when `total` exceeds it: the count is the part that cannot be
|
|
168
|
+
* cut, and "there is a second unreviewed operand" is exactly what a reader cannot recover from the
|
|
169
|
+
* command text. */
|
|
170
|
+
export interface ProbeCauseOperands {
|
|
171
|
+
/** The named entries. Each `inlineUntrusted`-neutralized and capped at {@link PROBE_CAUSE_PATH_MAX}
|
|
172
|
+
* INDIVIDUALLY, so one pathological entry costs only itself. Bounded in count by the minting probe. */
|
|
173
|
+
readonly shown: readonly string[];
|
|
174
|
+
/** How many entries the family has in total — `shown.length` when nothing was elided. A non-negative
|
|
175
|
+
* integer; never a lie by omission. */
|
|
176
|
+
readonly total: number;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* backlog #239: an ENGINE-MINTED, structured account of why a reversibility probe did not clear a call.
|
|
180
|
+
*
|
|
181
|
+
* DISPLAY/TRIAGE metadata, exactly like the rest of {@link RiskDescriptor} — nothing reads it to gate,
|
|
182
|
+
* budget or suppress anything, and it deliberately never reaches the ask's `message` (that string is
|
|
183
|
+
* handed to the auto-mode classifier, which may answer an ask with `allow`; a probe must not own an
|
|
184
|
+
* input to the decider that clears it).
|
|
185
|
+
*
|
|
186
|
+
* STRUCTURE IS THE CONTRACT. The entries are model-influenced text, so a consumer must render them as
|
|
187
|
+
* DATA — never re-derive structure by splitting or joining them. Structural characters are
|
|
188
|
+
* percent-encoded at the mint precisely so that a path cannot spell whatever grammar it is rendered
|
|
189
|
+
* into, but the array is the boundary that makes that guarantee cheap to keep.
|
|
190
|
+
*/
|
|
191
|
+
export interface ProbeCause {
|
|
192
|
+
/** Machine-readable cause id, `<domain>.<snake_case>` like this codebase's other notice codes. The
|
|
193
|
+
* consumer maps it to its own prose. A code is the one part of a cause that cannot become false as
|
|
194
|
+
* the check's edge cases are discovered. */
|
|
195
|
+
readonly code: string;
|
|
196
|
+
/** The paths the check TOOK as traversal roots — deduplicated and resolved, which is why they are
|
|
197
|
+
* described by what the check did with them rather than by what the caller wrote: a lexical operand
|
|
198
|
+
* walk has no option-value table (`du -d 2 sub` takes `2`), and it synthesizes the working directory
|
|
199
|
+
* when no operand is written at all (`du --help`). */
|
|
200
|
+
readonly roots: ProbeCauseOperands;
|
|
201
|
+
/** Operands the SAME verdict left unreviewed for a different reason (an unexpanded glob). Present only
|
|
202
|
+
* when non-empty. Named separately so a card can say what else this one approval clears — a partial
|
|
203
|
+
* account of an approval's reach is worse than none. */
|
|
204
|
+
readonly further?: ProbeCauseOperands;
|
|
143
205
|
}
|
|
206
|
+
/** backlog #239: the bloat ceiling on {@link RiskDescriptor.probeReason} — one sentence of cause, sized
|
|
207
|
+
* like its {@link RiskDescriptor.shadowedRule} sibling. The neutralizer is the injection ceiling; this
|
|
208
|
+
* is only the length one. Exported (module-level, not part of the package's public surface) so the gate
|
|
209
|
+
* that sanitizes at the entry boundary and the descriptor builder that re-sanitizes at the persist
|
|
210
|
+
* boundary cannot cap the same value at two different lengths. */
|
|
211
|
+
export declare const PROBE_REASON_MAX = 200;
|
|
212
|
+
/** backlog #239: the per-ENTRY ceiling inside a {@link ProbeCause} operand family, sized like the
|
|
213
|
+
* descriptor's own {@link RiskDescriptor.touchedPaths} entries (the same thing on the same card). Per
|
|
214
|
+
* entry, not per cause: that is the whole point of the structured shape — one pathological path costs
|
|
215
|
+
* only itself, where a single joined string let it consume every other entry's room. */
|
|
216
|
+
export declare const PROBE_CAUSE_PATH_MAX = 200;
|
|
217
|
+
/**
|
|
218
|
+
* backlog #239: the ONE validator for a {@link ProbeCause} arriving from a probe.
|
|
219
|
+
*
|
|
220
|
+
* The value crosses a trust boundary twice — once at the gate (where a deployment-supplied probe hands
|
|
221
|
+
* it over) and once at the persist boundary (where {@link buildRiskDescriptor} is an exported function a
|
|
222
|
+
* caller may reach directly) — so both call it rather than each writing its own idea of "valid". A
|
|
223
|
+
* malformed cause yields `undefined`: the ASK still stands (the verdict was never this member's to
|
|
224
|
+
* decide), the card simply carries no account. Fail-closed on the disclosure, never on the gate.
|
|
225
|
+
*
|
|
226
|
+
* Every field is re-derived rather than trusted: strings are neutralized and capped individually, the
|
|
227
|
+
* shown list is clamped, and `total` is coerced to a non-negative integer that cannot understate what is
|
|
228
|
+
* shown (a `total` below `shown.length` would turn an honest count into a false reassurance).
|
|
229
|
+
*/
|
|
230
|
+
export declare function normalizeProbeCause(value: unknown): ProbeCause | undefined;
|
|
144
231
|
/**
|
|
145
232
|
* design/80 §D-E: the DETERMINISTIC severity tier (1..5) for an escalation checkpoint, a PURE function of the
|
|
146
233
|
* tripped {@link RiskDescriptor.axes} (NO LLM, NO clock/random) so the inbox's sort order is stable and
|
|
@@ -273,6 +360,14 @@ export declare function buildRiskDescriptor(input: {
|
|
|
273
360
|
shellGateDoctrine?: "classify" | "always";
|
|
274
361
|
/** #144: the matched-but-outranked persisted rule to persist (see {@link RiskDescriptor.shadowedRule}). */
|
|
275
362
|
shadowedRule?: string;
|
|
363
|
+
/** backlog #239: the reversibility probe's cause for not clearing this call (see
|
|
364
|
+
* {@link RiskDescriptor.probeReason}). Re-neutralized here like its shadowed-rule sibling: this
|
|
365
|
+
* function is exported, so a caller reaching it directly must get the same redaction the gate applies. */
|
|
366
|
+
probeReason?: string;
|
|
367
|
+
/** backlog #239: the STRUCTURED cause (see {@link RiskDescriptor.probeCause}). Re-validated here for
|
|
368
|
+
* the same reason the prose sibling is re-neutralized — this function is exported, so a caller
|
|
369
|
+
* reaching it directly must not be able to persist a shape the gate would have rejected. */
|
|
370
|
+
probeCause?: unknown;
|
|
276
371
|
}): RiskDescriptor;
|
|
277
372
|
/**
|
|
278
373
|
* #130/#131/#120 (2026-08-10) — the durable record of an ask's `requiresRealApproval` bit, which used
|
|
@@ -5,6 +5,39 @@ import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
|
5
5
|
export function mintCheckpointToken() {
|
|
6
6
|
return randomBytes(16).toString("hex");
|
|
7
7
|
}
|
|
8
|
+
export const PROBE_REASON_MAX = 200;
|
|
9
|
+
export const PROBE_CAUSE_PATH_MAX = 200;
|
|
10
|
+
const PROBE_CAUSE_MAX_SHOWN = 8;
|
|
11
|
+
export function normalizeProbeCause(value) {
|
|
12
|
+
if (!isPlainRecord(value))
|
|
13
|
+
return undefined;
|
|
14
|
+
const code = safeDataValue(value, "code");
|
|
15
|
+
if (typeof code !== "string" || code.trim().length === 0)
|
|
16
|
+
return undefined;
|
|
17
|
+
const family = (raw) => {
|
|
18
|
+
if (!isPlainRecord(raw))
|
|
19
|
+
return undefined;
|
|
20
|
+
const shownRaw = safeDataValue(raw, "shown");
|
|
21
|
+
if (!Array.isArray(shownRaw))
|
|
22
|
+
return undefined;
|
|
23
|
+
const shown = shownRaw
|
|
24
|
+
.filter((p) => typeof p === "string")
|
|
25
|
+
.slice(0, PROBE_CAUSE_MAX_SHOWN)
|
|
26
|
+
.map((p) => inlineUntrusted(p, PROBE_CAUSE_PATH_MAX));
|
|
27
|
+
const totalRaw = safeDataValue(raw, "total");
|
|
28
|
+
const total = typeof totalRaw === "number" && Number.isFinite(totalRaw) ? Math.max(0, Math.floor(totalRaw)) : shown.length;
|
|
29
|
+
return { shown, total: Math.max(total, shown.length) };
|
|
30
|
+
};
|
|
31
|
+
const roots = family(safeDataValue(value, "roots"));
|
|
32
|
+
if (roots === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
const further = family(safeDataValue(value, "further"));
|
|
35
|
+
return {
|
|
36
|
+
code: inlineUntrusted(code, PROBE_REASON_MAX),
|
|
37
|
+
roots,
|
|
38
|
+
...(further !== undefined && further.total > 0 ? { further } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
8
41
|
export function riskSeverity(axes) {
|
|
9
42
|
const egress = axes.egress === true;
|
|
10
43
|
const irreversible = axes.irreversible === true;
|
|
@@ -121,6 +154,13 @@ export function buildRiskDescriptor(input) {
|
|
|
121
154
|
toolName,
|
|
122
155
|
...(input.shellGated && input.shellGateDoctrine !== undefined ? { shellGateDoctrine: input.shellGateDoctrine } : {}),
|
|
123
156
|
...(input.shadowedRule !== undefined ? { shadowedRule: inlineUntrusted(input.shadowedRule, 200) } : {}),
|
|
157
|
+
...(input.probeReason !== undefined ? { probeReason: inlineUntrusted(input.probeReason, PROBE_REASON_MAX) } : {}),
|
|
158
|
+
...(() => {
|
|
159
|
+
if (input.probeCause === undefined)
|
|
160
|
+
return {};
|
|
161
|
+
const cause = normalizeProbeCause(input.probeCause);
|
|
162
|
+
return cause !== undefined ? { probeCause: cause } : {};
|
|
163
|
+
})(),
|
|
124
164
|
...(summary !== undefined ? { summary } : {}),
|
|
125
165
|
...(touchedPaths !== undefined ? { touchedPaths } : {}),
|
|
126
166
|
};
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -645,7 +645,14 @@ export interface ToolGateInput {
|
|
|
645
645
|
* read the ask's origin on the durable route exactly as the synchronous mints read it off the
|
|
646
646
|
* decision (a hook-raised ask is one no persisted rule may clear, so its parked card must not
|
|
647
647
|
* offer to mint one). */
|
|
648
|
-
askDecisionReason?: DecisionReason
|
|
648
|
+
askDecisionReason?: DecisionReason,
|
|
649
|
+
/** backlog #239: the reversibility probe's cause for the tighten (the surviving ask's `probeReason`)
|
|
650
|
+
* — threaded so the park mint's risk descriptor carries on the durable route the same account of
|
|
651
|
+
* WHY the call was gated that the synchronous card gets. Already neutralized + capped at the gate. */
|
|
652
|
+
probeReason?: string,
|
|
653
|
+
/** backlog #239: the STRUCTURED cause (the surviving ask's `probeCause`) — same durable-route errand
|
|
654
|
+
* as `probeReason`, already validated at the gate. */
|
|
655
|
+
probeCause?: import("./checkpoint-store.js").ProbeCause) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
|
|
649
656
|
/**
|
|
650
657
|
* design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
|
|
651
658
|
* before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
|
|
@@ -680,12 +687,13 @@ export interface ToolGateInput {
|
|
|
680
687
|
* tool argument). Called ONLY when `irreversibility === "maybe"` && the surviving decision is `allow`,
|
|
681
688
|
* time-bounded by {@link approvalTimeoutMs}. Fail-closed: anything other than `{ reversible: true }`
|
|
682
689
|
* (including a timeout or a throw) tightens to `ask`. A probe is never trusted to AUTO-ALLOW past the gate.
|
|
690
|
+
*
|
|
691
|
+
* An optional `reason` on a TIGHTENING verdict names the cause. The gate neutralizes + caps it and
|
|
692
|
+
* carries it onto both approval routes (`AskRequest.probeReason`, `RiskDescriptor.probeReason`) — see
|
|
693
|
+
* `ToolSpec.reversibilityProbe`. Display/triage only: `reversible` alone decides the verdict, so a
|
|
694
|
+
* probe cannot widen anything through this member.
|
|
683
695
|
*/
|
|
684
|
-
reversibilityProbe?: (args: unknown) =>
|
|
685
|
-
reversible: boolean;
|
|
686
|
-
} | Promise<{
|
|
687
|
-
reversible: boolean;
|
|
688
|
-
}>;
|
|
696
|
+
reversibilityProbe?: (args: unknown) => import("./types.js").ReversibilityVerdict | Promise<import("./types.js").ReversibilityVerdict>;
|
|
689
697
|
/** design/77 §4: deadline (ms) for {@link reversibilityProbe}; on timeout the gate fails closed to `ask`. */
|
|
690
698
|
approvalTimeoutMs?: number;
|
|
691
699
|
/** design/77 §4: the task abort signal — bounds {@link reversibilityProbe} by the task's real deadline
|