@sema-agent/core 5.29.0 → 5.31.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 +141 -0
- package/dist/agents/send-message-tool.js +2 -0
- package/dist/agents/subagent.d.ts +2 -0
- package/dist/agents/subagent.js +6 -0
- package/dist/agents/teacher.js +2 -0
- package/dist/agents/verify.js +2 -0
- package/dist/core/auto-compaction.d.ts +5 -1
- package/dist/core/auto-compaction.js +10 -1
- package/dist/core/checkpoint-store.d.ts +51 -5
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/hooks.d.ts +12 -1
- package/dist/core/hooks.js +8 -2
- package/dist/core/permission-rules.js +2 -2
- package/dist/core/runner/prepare-task.d.ts +21 -5
- package/dist/core/runner/prepare-task.js +115 -26
- package/dist/core/runner/runtask.js +29 -3
- package/dist/core/runner/session-rule-policy.d.ts +3 -2
- package/dist/core/runner/tool-output-projection.js +1 -1
- package/dist/core/sensitive-path-policy.js +5 -16
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +4 -1
- package/dist/core/store-contracts/tool-result-store-contract.js +26 -1
- package/dist/core/tighten-task-spec.js +18 -0
- package/dist/core/tool-policy.d.ts +20 -1
- package/dist/core/tool-policy.js +31 -4
- package/dist/core/tool-result-store.d.ts +6 -4
- package/dist/core/tool-result-store.js +3 -1
- package/dist/core/types.d.ts +63 -1
- package/dist/engine/harness/types.d.ts +10 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/orchestration/run-workflow-tool.d.ts +26 -0
- package/dist/orchestration/run-workflow-tool.js +7 -4
- package/dist/orchestration/workflow-governance.d.ts +53 -3
- package/dist/orchestration/workflow-governance.js +162 -25
- package/dist/orchestration/workflow-primitives.d.ts +15 -1
- package/dist/orchestration/workflow-primitives.js +13 -2
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/packs/sema-default.js +2 -2
- package/dist/prompt-assembly/types.d.ts +4 -0
- package/dist/prompts/default.d.ts +14 -9
- package/dist/prompts/default.js +13 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +53 -4
- package/dist/tools/fs/bash-readonly-classifier.js +148 -16
- package/dist/tools/fs/fs-bash.d.ts +7 -0
- package/dist/tools/fs/fs-bash.js +8 -3
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-pdf.js +2 -2
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +11 -7
- package/dist/tools/fs/fs-search-tools.d.ts +4 -2
- package/dist/tools/fs/fs-search-tools.js +15 -8
- package/dist/tools/fs/fs-shared.d.ts +5 -1
- package/dist/tools/fs/fs-shared.js +8 -3
- package/dist/tools/fs/index.d.ts +18 -0
- package/dist/tools/fs/index.js +13 -2
- package/dist/tools/fs/read-deny.d.ts +110 -0
- package/dist/tools/fs/read-deny.js +159 -0
- package/dist/tools/fs/read-face.d.ts +49 -0
- package/dist/tools/fs/read-face.js +38 -0
- package/dist/tools/fs/repo-map.d.ts +3 -1
- package/dist/tools/fs/repo-map.js +11 -5
- package/dist/tools/fs/safety.d.ts +34 -11
- package/dist/tools/fs/safety.js +108 -8
- package/dist/tools/fs/search.d.ts +54 -5
- package/dist/tools/fs/search.js +107 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,146 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.31.0 — 2026-08-14
|
|
4
|
+
|
|
5
|
+
No API-BREAKING changes. Behavior narrowings are all tighten-direction (auto-allow → ask, or
|
|
6
|
+
map → refuse) and named below; two additive surfaces (an optional contract-kit callback, a
|
|
7
|
+
workflow-governance notice family).
|
|
8
|
+
|
|
9
|
+
### Narrowed (behavior)
|
|
10
|
+
|
|
11
|
+
- **Recursive/expanding read forms demote from auto-allow to ask when the sensitive-path deny set
|
|
12
|
+
is in force.** The bash read-only classifier's traversal set is not in the command text —
|
|
13
|
+
`grep -r . dir` names `dir`, not the `.ssh` it reaches — so under a wired deny judge, a
|
|
14
|
+
recursive verb (`grep -r/-R`, `ls -R`, `du`, `find`, `rg`/`ag`/`ack`/`tree` — recursive by
|
|
15
|
+
default, `tar` creation modes, `diff -r`) with a path operand is UNDECIDED and asks. Per-verb
|
|
16
|
+
getopt-aware form table (BSD/GNU divergences resolve demote-ward; divergent option-letter
|
|
17
|
+
ownership is scanned, not skipped); a bare `-` operand keeps stdin meaning only where the
|
|
18
|
+
command grants it (ls/du open a file literally named `-`); a recursive verb with NO operand
|
|
19
|
+
demotes too — the implicit cwd is the traversal root (`du`, `ls -R`, `tree` traverse `.` when
|
|
20
|
+
given nothing). A persisted allow rule clears the ask permanently (the rule lane absorbs the
|
|
21
|
+
one-time friction).
|
|
22
|
+
- **The classifier's operand walk honors POSIX end-of-options on EVERY face** (the same window's
|
|
23
|
+
`--` correction, disclosed as its own item because it flips behavior in BOTH directions on the
|
|
24
|
+
`bash_readonly` face too, which has no ask channel): past a bare `--`, a dash-leading token is
|
|
25
|
+
an OPERAND. Spellings like `cat -- -f/etc/passwd` used to be refused on a path extracted from
|
|
26
|
+
inside the token as if it were an option (a false refusal — the real program opens a relative
|
|
27
|
+
file literally named `-f/etc/passwd`); they now execute. Conversely a dash-named operand
|
|
28
|
+
(`cat -- -dir a.txt`) now enters symlink/boundary checking it previously skipped, so a
|
|
29
|
+
dash-named symlink pointing out of the roots is now refused. No new admission lands outside
|
|
30
|
+
the containment roots.
|
|
31
|
+
- **Win32 extended-length (`\\?\`) spellings map to DOS form only when the strip is an identity.**
|
|
32
|
+
Non-mappable bodies (`Volume{GUID}`, `GLOBALROOT`, device names, drive-relative `\\?\C:`,
|
|
33
|
+
forward-slash separators, empty/`.`/`..` components, trailing dots/spaces, reserved DOS device
|
|
34
|
+
components — `CONIN$`, superscript `COM¹` aliases included) now REFUSE by name instead of
|
|
35
|
+
silently resolving to a different file than the spelling denotes (approval-to-execution drift).
|
|
36
|
+
`WIN_RESERVED_RE` widens to the full documented reserved set (COM0/LPT0, superscript aliases,
|
|
37
|
+
`CONIN$`/`CONOUT$`) for every win-form key. UNC tail segments fold Win32 trailing dots/spaces so
|
|
38
|
+
a deny pattern matches the alias spelling.
|
|
39
|
+
- **The transcript-replay and startup-seed legs judge with the resolved read face** (open-face
|
|
40
|
+
twins of 5.30.0's attachment fix): a file legitimately read out-of-root under `readFace:"open"`
|
|
41
|
+
re-seeds across turns instead of being silently re-locked. A garbage `readFace` value now
|
|
42
|
+
refuses loudly on hands-less runs too (the value screen is unconditional).
|
|
43
|
+
- **Workflow governance: a governed script may declare the two read-face TIGHTENINGS**
|
|
44
|
+
(`readFace:"roots"` — admitted by VALUE, `"open"` never crosses — and `readDenyPatterns`,
|
|
45
|
+
grammar-checked by the child's own compiler). Every stripped unknown key is announced via the
|
|
46
|
+
new `workflow.governance_key_stripped` notice (one aggregated notice per governed build); the
|
|
47
|
+
WHITELIST_KEYS comment no longer claims a refusal rule that never existed.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **`toolResultStoreContract` gains an optional third parameter `onOptionalMember`** — the
|
|
52
|
+
observable channel for optional-member coverage (`deleteBySession`: `verified` | `absent`).
|
|
53
|
+
Previously absence was "reported" via an always-true assert message that only prints on failure,
|
|
54
|
+
so verified and absent produced byte-identical green runs. A present-but-uncallable member now
|
|
55
|
+
fails loudly as a defect. Additive: omitting the callback keeps prior behavior.
|
|
56
|
+
- **`PgToolResultStore.deleteBySession`** — the pg sibling of the file backend's session sweep
|
|
57
|
+
(same four-state semantics; one CTE statement so the deletion and the unattributable count come
|
|
58
|
+
from the same snapshot). The observable-absence channel above is what exposed it as missing.
|
|
59
|
+
- **Deny-refusal disclosure cites the view the pattern actually matched** (`ReadDenyHit.matchedView`):
|
|
60
|
+
under a symlink/case alias the canonical target and the requested spelling differ, and naming
|
|
61
|
+
the unmatched one sent the reader chasing a path the pattern does not match.
|
|
62
|
+
- Search results keep the honesty caveat and the deny-withholding note as SEPARATE disclosures
|
|
63
|
+
(a tail-anchored bracket merge used to splice read-loop facts into the deny note and drop the
|
|
64
|
+
"results may be incomplete" marker whenever pruning fired).
|
|
65
|
+
- Checkpoint face section: a compile-time closed-set guard forces a deliberate carry-or-drop
|
|
66
|
+
ruling when a new section key is added (the hands-less seed-carry whitelist can no longer
|
|
67
|
+
silently lag the persisted shape).
|
|
68
|
+
|
|
69
|
+
## 5.30.0 — 2026-08-13
|
|
70
|
+
|
|
71
|
+
No API-BREAKING changes (exports grow only: `resolveReadFace`, `ReadFace`, `ReadFaceInputs`,
|
|
72
|
+
`READ_FACE_DEFAULT_DENY_ENTRIES`, `compileReadDeny`, `ReadDenyEntry`, `ReadDenyMatcher`,
|
|
73
|
+
`NormalizedReadDenyEntry`; `TaskSpec`/`RunnerDeps`/`HandsToolkitOptions` gain `readFace`/
|
|
74
|
+
`readDenyPatterns`). Narrowings are tighten-direction and named below.
|
|
75
|
+
|
|
76
|
+
### Added
|
|
77
|
+
|
|
78
|
+
- **A read-face containment knob (`readFace: "open" | "roots"`, default `"roots"` — byte-compatible
|
|
79
|
+
with prior behavior).** `TaskSpec`/`RunnerDeps`/`HandsToolkitOptions` each gain the seat; task-layer
|
|
80
|
+
may only tighten under governance, deployment-layer sets the default. Beside a read-only (verifier)
|
|
81
|
+
mount, a task-seat `"open"` is a genuine contradiction and refuses loudly; a deployment-seat
|
|
82
|
+
`"open"` there silently clamps to `"roots"` (a deployment default cannot override a load-bearing
|
|
83
|
+
containment wall). Every workflow/subagent/SendMessage delegation lane inherits the resolved face
|
|
84
|
+
tighten-only (stricter-wins), including `run_workflow`'s script-spawned children, which previously
|
|
85
|
+
did not inherit it at all. Checkpoint schema bumps to v9 (`FACE_CHECKPOINT_VERSION`) to carry the
|
|
86
|
+
resolved face across suspend/resume; an absent v9 section on an older checkpoint row is the default
|
|
87
|
+
posture (roots) — no existing row's behavior changes.
|
|
88
|
+
- **Checkpoint schema v9** (erratum 2026-08-13 — this deserved its own entry, not a clause above; a
|
|
89
|
+
store schema bump is a load-bearing event for store implementors regardless of which feature rides
|
|
90
|
+
it). What v9 is: rows minted under a non-default read-face posture carry a `readFace` section —
|
|
91
|
+
`{ face: "open" | "roots", denyEntries?: Array<{pattern, caseSensitive}>, realApproval?: true }` —
|
|
92
|
+
and the resume pre-CAS ladder validates it (a malformed section refuses the row; a v9+ row claiming
|
|
93
|
+
`realApproval: true` with no well-formed gate bit refuses as `real_approval_damaged`). Absence
|
|
94
|
+
semantics: a missing section (all pre-v9 rows, and v9 rows minted under the default posture) means
|
|
95
|
+
ROOTS — zero migration, no existing row's behavior changes. Store implementors (SQL backends
|
|
96
|
+
included): the section is part of the opaque checkpoint state blob, so no schema/DDL change is
|
|
97
|
+
required — but a store that inspects or rewrites state must treat the section as tamper-guarded
|
|
98
|
+
(the pre-CAS ladder refuses a row whose section it cannot validate), and `MAX_SUPPORTED` acceptance
|
|
99
|
+
now includes v9.
|
|
100
|
+
- **A built-in sensitive-path READ deny set**, exported as `READ_FACE_DEFAULT_DENY_ENTRIES` (SSH
|
|
101
|
+
keys, cloud/VCS credential files, browser profile directories, crypto wallet files, shell history,
|
|
102
|
+
and similar). Enforced under BOTH containment modes — an `"open"` face does not exempt it — and not
|
|
103
|
+
shrinkable by any deployment configuration, including the `readFace: "roots"` compatibility seat
|
|
104
|
+
(the two axes are orthogonal). `.env`-family files are deliberately excluded (write-side guards
|
|
105
|
+
already cover them). Deployments may extend the table (`readDenyPatterns` on the same three seats,
|
|
106
|
+
additive-only; `compileReadDeny` lets a host pre-validate additions with the engine's own rules).
|
|
107
|
+
|
|
108
|
+
### Narrowed (behavior)
|
|
109
|
+
|
|
110
|
+
- **A UNC-rooted deployment's `..` can no longer walk out of its declared root.** Lexical folding now
|
|
111
|
+
normalizes `..` segments and Win32 device-namespace prefixes (`\\?\`, `\\.\`) before they reach the
|
|
112
|
+
containment check; this is a security repair with no compatibility escape hatch (unaffected by any
|
|
113
|
+
"keep old behavior" knob).
|
|
114
|
+
- **`bashReversibilityProbe` no longer auto-executes an operand with an unexpanded shell glob
|
|
115
|
+
metacharacter it can't verify against the deny/boundary check.** A command whose read target is
|
|
116
|
+
undecided at the classifier layer now routes to the ask flow instead of running unattended.
|
|
117
|
+
- **A hands-less resume no longer clones the `realApproval` governance marker onto an ungoverned
|
|
118
|
+
re-park.** The seed-carry now picks only `{face, denyEntries}`; previously, a worker resuming into a
|
|
119
|
+
plain (non-governed) ask could inherit a stale `realApproval: true` and permanently wedge the row on
|
|
120
|
+
its next resume (`real_approval_damaged`, fail-closed by design).
|
|
121
|
+
- **The auto-mode classifier can no longer resolve an ask that originated from an explicit
|
|
122
|
+
user-written permission rule or a hook-raised promotion.** Both are now excluded from classifier
|
|
123
|
+
judgment and the persisted-rule-clearing lane, closing two asymmetric gaps against the existing
|
|
124
|
+
`decisionReason === "hook"` exclusion.
|
|
125
|
+
- **`resolveAsk`'s object-arm reads share one exception boundary.** A throwing getter on `settledBy`/
|
|
126
|
+
`allow`/`updatedInput` now fails closed (deny) instead of propagating an unhandled rejection; a
|
|
127
|
+
non-boolean `allow` is refused loudly by name instead of silently rendering as a human refusal.
|
|
128
|
+
|
|
129
|
+
### Fixed
|
|
130
|
+
|
|
131
|
+
- `run_workflow`'s durable-store off switch (`spec.checkpointStore === null`) and design/148's
|
|
132
|
+
center-artifact inheritance (`centerArtifactDigest`/`centerSourceRevision`) were dead reads on the
|
|
133
|
+
tool's auto-mounted execute context (which never carries them) — neither ever reached a
|
|
134
|
+
script-spawned child. Both now fall back to a mount-time dependency, matching the tool's other two
|
|
135
|
+
ctx-or-deps fields.
|
|
136
|
+
- Under `readFace: "open"`, compaction's attachment-reader legs (`readTaskFile`/
|
|
137
|
+
`normalizeAttachmentPath`) refused files the Read tool had already legitimately served out-of-root;
|
|
138
|
+
they now thread the resolved face like the structured read tools do.
|
|
139
|
+
- `deleteBySession` joins the `ToolResultStore` contract kit as an honest optional member: when
|
|
140
|
+
implemented, the four-state semantics (same-session rows deleted, other-session rows untouched,
|
|
141
|
+
unowned rows counted not deleted, idempotent re-delete) are asserted; when absent, the contract
|
|
142
|
+
probe reports it by name instead of silently passing.
|
|
143
|
+
|
|
3
144
|
## 5.29.0 — 2026-08-13
|
|
4
145
|
|
|
5
146
|
No API-BREAKING changes (exports grow only: `AskOutcome.reason`, `AskRequest.isDelegatedChild`).
|
|
@@ -680,6 +680,8 @@ export function createSendMessageTool(opts) {
|
|
|
680
680
|
...(ctx.onQuestion !== undefined && row.sessionScoped !== true ? { currentOnQuestion: ctx.onQuestion } : {}),
|
|
681
681
|
currentClamps: {
|
|
682
682
|
...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
683
|
+
...(ctx.readFace === "roots" ? { readFaceRoots: true } : {}),
|
|
684
|
+
...(ctx.readDenyPatterns !== undefined ? { readDenyPatterns: ctx.readDenyPatterns } : {}),
|
|
683
685
|
...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
684
686
|
...(ctx.oneShot === true ? { oneShot: true } : {}),
|
|
685
687
|
...(ctx.memoryPersistenceCapable === false ? { memoryPersistenceCapable: false } : {}),
|
|
@@ -404,6 +404,8 @@ export declare function createSubagentResume(deps: {
|
|
|
404
404
|
interactiveTools?: false;
|
|
405
405
|
oneShot?: true;
|
|
406
406
|
memoryPersistenceCapable?: false;
|
|
407
|
+
readFaceRoots?: true;
|
|
408
|
+
readDenyPatterns?: readonly import("../tools/fs/read-deny.js").ReadDenyEntry[];
|
|
407
409
|
};
|
|
408
410
|
/** The RESUMING caller's own handback-review seat (its trusted `ToolExecuteContext.autoModeReview`),
|
|
409
411
|
* same turn-bound reasoning as `currentOnQuestion` above: a resumed cycle is a completion like any
|
package/dist/agents/subagent.js
CHANGED
|
@@ -520,6 +520,10 @@ export function createSubagentResume(deps) {
|
|
|
520
520
|
requireExistingSession: true,
|
|
521
521
|
...(deps.currentOnQuestion !== undefined ? { onQuestion: deps.currentOnQuestion } : {}),
|
|
522
522
|
...(entry.specSnapshot.handsReadOnly === true || deps.currentClamps?.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
523
|
+
...(entry.specSnapshot.readFace === "roots" || deps.currentClamps?.readFaceRoots === true ? { readFace: "roots" } : {}),
|
|
524
|
+
...(entry.specSnapshot.readDenyPatterns !== undefined || deps.currentClamps?.readDenyPatterns !== undefined
|
|
525
|
+
? { readDenyPatterns: [...(entry.specSnapshot.readDenyPatterns ?? []), ...(deps.currentClamps?.readDenyPatterns ?? [])] }
|
|
526
|
+
: {}),
|
|
523
527
|
...(entry.specSnapshot.interactiveTools === false || deps.currentClamps?.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
524
528
|
...(entry.specSnapshot.oneShot === true || deps.currentClamps?.oneShot === true ? { oneShot: true } : {}),
|
|
525
529
|
...(entry.specSnapshot.memoryPersistenceCapable === false || deps.currentClamps?.memoryPersistenceCapable === false
|
|
@@ -1693,6 +1697,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1693
1697
|
...(ctx.onQuestion !== undefined ? { onQuestion: ctx.onQuestion } : {}),
|
|
1694
1698
|
...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
1695
1699
|
...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
1700
|
+
...(ctx.readFace === "roots" ? { readFace: "roots" } : {}),
|
|
1701
|
+
...(ctx.readDenyPatterns !== undefined ? { readDenyPatterns: [...ctx.readDenyPatterns] } : {}),
|
|
1696
1702
|
...(childMemoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: childMemoryPersistenceCapable } : {}),
|
|
1697
1703
|
...(ctx.oneShot === true ? { oneShot: true } : {}),
|
|
1698
1704
|
...(ctx.clientContext !== undefined ? { clientContext: ctx.clientContext } : {}),
|
package/dist/agents/teacher.js
CHANGED
|
@@ -180,6 +180,8 @@ async function runTeacherCore(runner, studentSpec, teacher) {
|
|
|
180
180
|
...(studentSpec.clientContext !== undefined ? { clientContext: studentSpec.clientContext } : {}),
|
|
181
181
|
...(studentSpec.promptProfile !== undefined ? { promptProfile: studentSpec.promptProfile } : {}),
|
|
182
182
|
...(studentSpec.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
183
|
+
...(studentSpec.readFace === "roots" ? { readFace: "roots" } : {}),
|
|
184
|
+
...(studentSpec.readDenyPatterns !== undefined ? { readDenyPatterns: [...studentSpec.readDenyPatterns] } : {}),
|
|
183
185
|
...(studentSpec.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
184
186
|
...(studentSpec.excludeTools !== undefined ? { excludeTools: [...studentSpec.excludeTools] } : {}),
|
|
185
187
|
...(studentSpec.deferTools !== undefined ? { deferTools: [...studentSpec.deferTools] } : {}),
|
package/dist/agents/verify.js
CHANGED
|
@@ -99,6 +99,8 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
|
|
|
99
99
|
roles: specBase.roles,
|
|
100
100
|
tools: verifierTools,
|
|
101
101
|
handsReadOnly: config.verifierHandsReadOnly ?? true,
|
|
102
|
+
...(specBase.readFace === "roots" ? { readFace: "roots" } : {}),
|
|
103
|
+
...(specBase.readDenyPatterns !== undefined ? { readDenyPatterns: [...specBase.readDenyPatterns] } : {}),
|
|
102
104
|
interactiveTools: false,
|
|
103
105
|
outputSchema: VerdictSchema,
|
|
104
106
|
enableBlockedReport: false,
|
|
@@ -244,7 +244,11 @@ export interface MaybeCompactOptions {
|
|
|
244
244
|
*/
|
|
245
245
|
workingFileAttachments?: {
|
|
246
246
|
/** Read a task workspace file; null/throw = skip it. Wired by the Runner from the task's ExecutionEnv. */
|
|
247
|
-
readFile: (path: string) => Promise<string | null
|
|
247
|
+
readFile: (path: string) => Promise<string | null | {
|
|
248
|
+
withheld: {
|
|
249
|
+
pattern: string;
|
|
250
|
+
};
|
|
251
|
+
}>;
|
|
248
252
|
/** Max modified files to attach. Default 3. */
|
|
249
253
|
maxFiles?: number;
|
|
250
254
|
/** Per-file char cap (≈4 chars/token). Default 16_000 (~4k tokens). */
|
|
@@ -397,12 +397,21 @@ export async function maybeCompact(opts) {
|
|
|
397
397
|
if (remaining <= 0)
|
|
398
398
|
break;
|
|
399
399
|
let content = null;
|
|
400
|
+
let withheldPattern;
|
|
400
401
|
try {
|
|
401
|
-
|
|
402
|
+
const r = await att.readFile(path);
|
|
403
|
+
if (typeof r === "string")
|
|
404
|
+
content = r;
|
|
405
|
+
else if (r !== null && typeof r === "object")
|
|
406
|
+
withheldPattern = r.withheld.pattern;
|
|
402
407
|
}
|
|
403
408
|
catch {
|
|
404
409
|
content = null;
|
|
405
410
|
}
|
|
411
|
+
if (withheldPattern !== undefined) {
|
|
412
|
+
blocks.push(`<working-file path="${escapeWorkingFilePathAttr(path)}" withheld="content withheld: matches the sensitive-path read deny list (pattern ${escapeWorkingFilePathAttr(withheldPattern)})"/>`);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
406
415
|
if (content === null)
|
|
407
416
|
continue;
|
|
408
417
|
const cap = Math.min(perFileCap, remaining);
|
|
@@ -647,6 +647,37 @@ export interface CheckpointState {
|
|
|
647
647
|
/** design/44: the hand's read-file state (content hashes), serialized from the hands-toolkit closure
|
|
648
648
|
* (NOT part of Prepared — §15.2 net-add #8). Without it a resumed `edit_file` is rejected "not read". */
|
|
649
649
|
readFileState?: Array<[string, ReadEntry]>;
|
|
650
|
+
/**
|
|
651
|
+
* design/199 — the READ-face containment state at suspend (v9,
|
|
652
|
+
* {@link FACE_CHECKPOINT_VERSION}): the RESOLVED face plus the NORMALIZED deny-set ADDITIONS in
|
|
653
|
+
* force (deployment ∪ task layers; the built-in table is engine code, never frozen here — a newer
|
|
654
|
+
* engine's built-ins apply on resume as everywhere else). Written ONLY when the posture is
|
|
655
|
+
* non-default (face "open", or additions present) — an absent section IS the default posture and
|
|
656
|
+
* resumes as roots + built-ins on any worker. Resume folds STRICTER-WINS: face = roots unless the
|
|
657
|
+
* section says open AND the live resolution says open; deny additions = frozen ∪ live (add-only,
|
|
658
|
+
* monotone — a resume can never widen what the cut leg could read).
|
|
659
|
+
*
|
|
660
|
+
* The design's r3/r4 rounds carried a third key (the workspace domain at cut) to evaluate
|
|
661
|
+
* domain-scoped deny entries under their frozen domain; the D-4 ruling (2026-08-13) removed the
|
|
662
|
+
* scoped tier entirely, leaving that key without a consumer — it is deliberately NOT persisted
|
|
663
|
+
* (dead fields in a durable format are the drift surface the liveness gate exists to catch).
|
|
664
|
+
*/
|
|
665
|
+
readFace?: {
|
|
666
|
+
face: "open" | "roots";
|
|
667
|
+
denyEntries?: Array<{
|
|
668
|
+
pattern: string;
|
|
669
|
+
caseSensitive: boolean;
|
|
670
|
+
}>;
|
|
671
|
+
/** codex r2 — REDUNDANT governance marker: `true` iff the row's gate was minted WITH a
|
|
672
|
+
* {@link RealApprovalGateBit}. v8's bit-required invariant was version-keyed and v9 (face)
|
|
673
|
+
* outranks v8 in the stamp, so without this a v9 GOVERNED row whose `gate.realApproval` member
|
|
674
|
+
* was dropped in storage would pass pre-CAS and be redeemable on an unwired worker — the exact
|
|
675
|
+
* #130/#120 corruption/downgrade hole, reopened one version up. The resume pre-CAS ladder
|
|
676
|
+
* refuses a v9+ row whose section says `realApproval: true` while the gate carries no
|
|
677
|
+
* well-formed bit (same protection level as v8: a coherent rewrite of BOTH sites remains out of
|
|
678
|
+
* scope for any in-row invariant). Absent = the row was minted ungoverned. */
|
|
679
|
+
realApproval?: true;
|
|
680
|
+
};
|
|
650
681
|
/**
|
|
651
682
|
* design/78 Slice-1: the SAFE-tier self-repair loop's durable state (`failureTrace`/`diagnostics`/
|
|
652
683
|
* `rejectedHypotheses`/`attemptCount`/`oracleTier` — all JSON/`structuredClone`-safe, no fn/Date). Set ONLY
|
|
@@ -963,14 +994,29 @@ export declare const F012_CHECKPOINT_VERSION = 7;
|
|
|
963
994
|
* stays true: rows whose gate carries no realApproval bit keep their historic stamps everywhere.
|
|
964
995
|
*/
|
|
965
996
|
export declare const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
|
|
997
|
+
/**
|
|
998
|
+
* design/199 (2026-08-13 — the precedent replayed a seventh time): the schema version a suspend
|
|
999
|
+
* stamps when its state carries the READ-FACE section ({@link CheckpointState.readFace} — the
|
|
1000
|
+
* resolved open face and/or deny-set additions). The enforcement lives ENTIRELY in the resuming
|
|
1001
|
+
* worker (the stricter-wins face fold + the deny-entry union at prepare): a pre-199 worker
|
|
1002
|
+
* (MAX_SUPPORTED=8) would accept the row, ignore the section, and resume the leg under its own
|
|
1003
|
+
* defaults — silently DROPPING the frozen deny additions (a widening), and re-deriving the face
|
|
1004
|
+
* from live config alone. Stamping v9 forces it to reject PRE-CAS (`unsupported_version`, stays
|
|
1005
|
+
* `pending`, retried on an enforcing worker). A row whose task ran the default posture (face roots,
|
|
1006
|
+
* no additions) carries NO section and keeps the historic stamps — such a resume on ANY worker
|
|
1007
|
+
* folds to roots + built-ins, which is exactly the semantics it was cut under.
|
|
1008
|
+
*/
|
|
1009
|
+
export declare const FACE_CHECKPOINT_VERSION = 9;
|
|
966
1010
|
/** The highest {@link Checkpoint.version} `runner.resume` will act on; a higher one is rejected pre-CAS with
|
|
967
1011
|
* {@link CheckpointError} `unsupported_version` (the checkpoint stays `pending`, retryable on a newer worker).
|
|
968
1012
|
* Raised to 8 for realApproval-bearing gates — this worker reads v1 (legacy human), v2 (resource), v3
|
|
969
1013
|
* (binding human/irreversible_ask), v4 (a pre-164 row, refused only when it carries the retired
|
|
970
1014
|
* wall-clock allocation), v5 (token-allocation-bearing resource/approval), v6 (org-admission freeze
|
|
971
|
-
* state), v7 (F-012 constraint-chain / delegation-provenance enforcement state)
|
|
972
|
-
* non-budgetable `realApproval` gate bit with its org-origin resume semantics)
|
|
973
|
-
|
|
1015
|
+
* state), v7 (F-012 constraint-chain / delegation-provenance enforcement state), v8 (a
|
|
1016
|
+
* non-budgetable `realApproval` gate bit with its org-origin resume semantics) and v9 (the
|
|
1017
|
+
* design/199 read-face section: resolved face + deny-set additions, folded stricter-wins on
|
|
1018
|
+
* resume). */
|
|
1019
|
+
export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 9;
|
|
974
1020
|
/**
|
|
975
1021
|
* Read a checkpoint's schema version, defaulting an absent field to **legacy `0`** (a 1.67-era checkpoint
|
|
976
1022
|
* written before the field existed — it carries no `workspaceHandle`, so resuming it the v1 way is safe).
|
|
@@ -1507,7 +1553,7 @@ export declare class CheckpointError extends Error {
|
|
|
1507
1553
|
* · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
|
|
1508
1554
|
* `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
|
|
1509
1555
|
* current bytes — no worker version redeems a damaged row). */
|
|
1510
|
-
reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "constraint_chain_missing";
|
|
1556
|
+
reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
|
|
1511
1557
|
} | undefined;
|
|
1512
1558
|
constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
|
|
1513
1559
|
/** `runner.resume` was handed an {@link ResumeOutcome} whose `gate` arm does not match the
|
|
@@ -1628,7 +1674,7 @@ export declare class CheckpointError extends Error {
|
|
|
1628
1674
|
* · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
|
|
1629
1675
|
* `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
|
|
1630
1676
|
* current bytes — no worker version redeems a damaged row). */
|
|
1631
|
-
reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "constraint_chain_missing";
|
|
1677
|
+
reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
|
|
1632
1678
|
} | undefined);
|
|
1633
1679
|
}
|
|
1634
1680
|
/**
|
|
@@ -136,7 +136,8 @@ export const TOKEN_CHECKPOINT_VERSION = 5;
|
|
|
136
136
|
export const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
|
|
137
137
|
export const F012_CHECKPOINT_VERSION = 7;
|
|
138
138
|
export const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
|
|
139
|
-
export const
|
|
139
|
+
export const FACE_CHECKPOINT_VERSION = 9;
|
|
140
|
+
export const MAX_SUPPORTED_CHECKPOINT_VERSION = 9;
|
|
140
141
|
export function checkpointVersionOf(cp) {
|
|
141
142
|
return cp.version ?? 0;
|
|
142
143
|
}
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -724,13 +724,20 @@ export interface ToolGateInput {
|
|
|
724
724
|
* - `unavailable`/`parse_error` → FAIL-CLOSED: the ask continues down the ORIGINAL chain
|
|
725
725
|
* (durable suspend / onAsk / headless auto-deny) exactly as if auto mode were absent
|
|
726
726
|
* (CC `automode-unavailable`/`automode-parsing-error` — "NOT a policy decision").
|
|
727
|
-
* SCOPE (declared semantic): this covers
|
|
727
|
+
* SCOPE (declared semantic): this covers every surviving ask INCLUDING the deterministic egress/
|
|
728
728
|
* irreversibility safety tightens — auto mode is the deployment's explicit choice to let the
|
|
729
729
|
* classifier be the "explicit ask-resolution" for this session (its rule set covers exactly those
|
|
730
730
|
* action classes: outbound writes, irreversible operations, and sensitive-value handling). A
|
|
731
731
|
* deployment that wants human-only
|
|
732
732
|
* resolution for safety asks simply does not arm auto mode. The classifier can never AUTO-ALLOW by
|
|
733
733
|
* failing — only an affirmative `<block>no</block>` allows.
|
|
734
|
+
* EXCLUDED from that scope (the ask flows the ORIGINAL chain untouched, exactly as if this field
|
|
735
|
+
* were absent for that one call): an org-constrained ask (design/182 §7.4, either org arm), the
|
|
736
|
+
* reserved question tool, a call MARKED unresolvable (design/153), a hook-raised or hook-promoted
|
|
737
|
+
* ask (`decisionReason:"hook"`), and an ask carrying `matchedAskRule` (an explicit `ask` permission
|
|
738
|
+
* rule matched — #213: a person's standing "ask me each time" is not classifier hesitation). The
|
|
739
|
+
* last two are the same pair the persisted-rule lane refuses, so the gate's two ask→allow seams
|
|
740
|
+
* answer provenance questions identically.
|
|
734
741
|
*/
|
|
735
742
|
autoMode?: {
|
|
736
743
|
decider: import("./auto-mode.js").AutoModeDecider;
|
|
@@ -769,6 +776,10 @@ export interface ToolGateInput {
|
|
|
769
776
|
* tool's own egress/irreversibility marks are structural requirements, not classifier hesitation
|
|
770
777
|
* ("allow rules silence the classifier's questions, never a mandated one"). When a rule matches
|
|
771
778
|
* but cannot clear, the surviving ask discloses it (message + `persistedRuleShadowed`).
|
|
779
|
+
* · #213: an ask carrying `matchedAskRule` (an explicit `ask` permission rule matched this call)
|
|
780
|
+
* is never consumed — a person's standing "ask me each time" outranks the memory of one past
|
|
781
|
+
* yes, across lanes exactly as the DSL's own deny > ask > allow order says within one. Same
|
|
782
|
+
* disclosure as the mandated family when a rule matches anyway.
|
|
772
783
|
* What it DOES consume is the classify-DOCTRINE shell ask (the coarse tier "maybe" — the
|
|
773
784
|
* don't-ask-again main case this feature exists for) and unmarked bare asks. Consuming such an ask
|
|
774
785
|
* also skips the park it would have minted; that IS what a standing approval means, and the
|
package/dist/core/hooks.js
CHANGED
|
@@ -354,7 +354,7 @@ export async function runToolGate(input) {
|
|
|
354
354
|
req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
355
355
|
input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
|
|
356
356
|
const hit = await input.persistedRules.admits(req).catch(() => undefined);
|
|
357
|
-
if (hit !== undefined && persistedRuleMandate === undefined) {
|
|
357
|
+
if (hit !== undefined && persistedRuleMandate === undefined && decision.matchedAskRule === undefined) {
|
|
358
358
|
decision = {
|
|
359
359
|
action: "allow",
|
|
360
360
|
message: `a persisted allow rule (${hit}) covers this call`,
|
|
@@ -365,7 +365,11 @@ export async function runToolGate(input) {
|
|
|
365
365
|
}
|
|
366
366
|
else if (hit !== undefined) {
|
|
367
367
|
const shownRule = inlineUntrusted(hit, 200);
|
|
368
|
-
const mandateNoun = persistedRuleMandate === "operator_always"
|
|
368
|
+
const mandateNoun = persistedRuleMandate === "operator_always"
|
|
369
|
+
? "this deployment mandates per-call confirmation for shell commands (shellGate: always)"
|
|
370
|
+
: persistedRuleMandate !== undefined
|
|
371
|
+
? "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)"
|
|
372
|
+
: "an explicit ask rule matched this call (a person's ask-me-each-time outranks a standing allow rule)";
|
|
369
373
|
decision = {
|
|
370
374
|
...decision,
|
|
371
375
|
persistedRuleShadowed: shownRule,
|
|
@@ -376,6 +380,8 @@ export async function runToolGate(input) {
|
|
|
376
380
|
if (input.autoMode &&
|
|
377
381
|
!orgRealApprovalRequired &&
|
|
378
382
|
decision.action === "ask" &&
|
|
383
|
+
decision.decisionReason !== "hook" &&
|
|
384
|
+
decision.matchedAskRule === undefined &&
|
|
379
385
|
req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
380
386
|
input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
|
|
381
387
|
const verdict = await input.autoMode.decider
|
|
@@ -287,11 +287,11 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
287
287
|
return { action: "deny", message: ruleMessage("denied", paramDeny.ruleText, paramDeny.source) };
|
|
288
288
|
}
|
|
289
289
|
if (entry.bare.ask) {
|
|
290
|
-
return { action: "ask", message: ruleMessage("flagged", entry.bare.ask.ruleText, entry.bare.ask.source) };
|
|
290
|
+
return { action: "ask", message: ruleMessage("flagged", entry.bare.ask.ruleText, entry.bare.ask.source), matchedAskRule: entry.bare.ask.ruleText };
|
|
291
291
|
}
|
|
292
292
|
const paramAsk = matchParamRules(entry.param.ask, req.args, caps.maxScalarValueChars);
|
|
293
293
|
if (paramAsk) {
|
|
294
|
-
return { action: "ask", message: ruleMessage("flagged", paramAsk.ruleText, paramAsk.source) };
|
|
294
|
+
return { action: "ask", message: ruleMessage("flagged", paramAsk.ruleText, paramAsk.source), matchedAskRule: paramAsk.ruleText };
|
|
295
295
|
}
|
|
296
296
|
if (entry.bare.allow) {
|
|
297
297
|
return { action: "allow" };
|
|
@@ -307,14 +307,14 @@ export interface Prepared {
|
|
|
307
307
|
* threaded to every compaction-boundary epoch selection so boundary re-pins hash the same
|
|
308
308
|
* artifact identity as the prepare-time pin. Empty on declaration-free runs. */
|
|
309
309
|
epochDeclaredSections: import("../../prompt-assembly/epoch.js").EpochDeclaredSections;
|
|
310
|
+
/** design/148 S3 — the LIVE nine-element identity face (refreshed by deferred materialization and
|
|
311
|
+
* the RB-31 adoption swap); the manifest holds the prepare-time initial copy. */
|
|
312
|
+
turnSnapshot?: import("../../prompt-assembly/turn-snapshot.js").TurnPromptSnapshot;
|
|
310
313
|
/** RB-31 (design/148 S2) — the compaction-boundary center-adoption seam (undefined = no source
|
|
311
314
|
* wired or session not center-pinned). Returns the maybeCompact `centerAdoption` fragment or
|
|
312
315
|
* undefined; `apply` runs post-CAS only (auto-compaction owns the ordering). Both
|
|
313
316
|
* `epochDeclaredSections` and `promptOverheadTokens` on THIS object are mutated by a successful
|
|
314
317
|
* adoption (read them at call time, never capture early). */
|
|
315
|
-
/** design/148 S3 — the LIVE nine-element identity face (refreshed by deferred materialization and
|
|
316
|
-
* the RB-31 adoption swap); the manifest holds the prepare-time initial copy. */
|
|
317
|
-
turnSnapshot?: import("../../prompt-assembly/turn-snapshot.js").TurnPromptSnapshot;
|
|
318
318
|
centerCompactionCandidate?: () => {
|
|
319
319
|
centerArtifactDigest?: string;
|
|
320
320
|
sourceRevision?: string;
|
|
@@ -573,8 +573,15 @@ export interface Prepared {
|
|
|
573
573
|
/** Narrow workspace reader for compaction working-file attachments (LONGRUN-2): reads a task file
|
|
574
574
|
* via the SAME env the hands ran against (so remote/k8s/E2B tasks read the container's tree, not
|
|
575
575
|
* the control plane's). Present only when the hands are enabled — without an env there is no
|
|
576
|
-
* workspace to re-read. null = unreadable (deleted/binary/transport error); callers skip it.
|
|
577
|
-
|
|
576
|
+
* workspace to re-read. null = unreadable (deleted/binary/transport error); callers skip it.
|
|
577
|
+
* design/199 件B: `{ withheld }` = the target matches the sensitive-path read deny list — the
|
|
578
|
+
* attachment is deliberately withheld and the consumer must SAY so (an annotation, never a silent
|
|
579
|
+
* skip: a silently missing attachment reads as "file gone", which is a different fact). */
|
|
580
|
+
readTaskFile?: (path: string) => Promise<string | null | {
|
|
581
|
+
withheld: {
|
|
582
|
+
pattern: string;
|
|
583
|
+
};
|
|
584
|
+
}>;
|
|
578
585
|
/** CC post-compact restore parity (2026-07-03): the task's READ files, most recent
|
|
579
586
|
* first (from the hands' readFileState `lastReadAt` stamps). The compaction working-file
|
|
580
587
|
* attachment prefers this over the modified set — CC restores what the model RECENTLY READ,
|
|
@@ -1483,6 +1490,15 @@ export declare function rebaseWorkspacePath(p: string, fromRaw: string, toRaw: s
|
|
|
1483
1490
|
* which case the earlier — more specific — spelling is the intended one. Exported for direct unit pinning.
|
|
1484
1491
|
*/
|
|
1485
1492
|
export declare function rebaseWorkspacePathAcross(p: string, froms: readonly string[], to: string): string;
|
|
1493
|
+
/**
|
|
1494
|
+
* Build everything a task run needs (council design/34 ②: a free function with EXPLICIT deps, not a
|
|
1495
|
+
* Runner method — testable and decoupled). Resolves the model/role/thinking, acquires + reconciles the
|
|
1496
|
+
* session (with the conflict-detecting Proxy + bounded re-wake on a lost CAS), assembles the tool set
|
|
1497
|
+
* (caller tools + offload wrap + report_blocked + submit_output + MCP + memory + read_tool_result),
|
|
1498
|
+
* builds the system prompt, constructs the harness, wires the tool-policy gate + context-edit/guard
|
|
1499
|
+
* hooks + external-signal cancellation, and sets up the cache-break detector. **No run-loop control
|
|
1500
|
+
* flow** lives here — that stays in the Runner loop.
|
|
1501
|
+
*/
|
|
1486
1502
|
export declare function prepareTask(spec: TaskSpec, deps: RunnerDeps, sessions: SessionStore, resume?: PrepareResume, internals?: RunInternals,
|
|
1487
1503
|
/** design/98 §3.1 (S8c): a TRUSTED self-reference to the Runner, passed by the Runner itself (never a
|
|
1488
1504
|
* TaskSpec field) so the `run_workflow` tool can execute child tasks via `runner.runTask`. Undefined when
|