@sema-agent/core 7.49.0 → 7.49.2
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 +85 -0
- package/dist/core/arg-summary.d.ts +4 -4
- package/dist/core/arg-summary.js +106 -42
- package/dist/core/credential-vocabulary.js +19 -2
- package/dist/core/engine-notice.d.ts +11 -4
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/hooks.d.ts +17 -0
- package/dist/core/runner/prepare-gate-stations.js +2 -2
- package/dist/core/runner/prepare-lsp.d.ts +4 -4
- package/dist/core/runner/prepare-lsp.js +2 -2
- package/dist/core/runner/prepare-safety-scan.js +10 -6
- package/dist/core/runner/run-harness-handlers.js +2 -2
- package/dist/core/runner/tool-turn-repair-seat.d.ts +28 -4
- package/dist/core/runner/tool-turn-repair-seat.js +36 -13
- package/dist/core/runner-deps.d.ts +2 -2
- package/dist/core/shell-wrapper-projection.js +1 -1
- package/dist/core/task-spec.d.ts +2 -2
- package/dist/core/tool-catalog-entries.js +1 -1
- package/dist/core/tool-conformance.js +2 -1
- package/dist/core/tool-roster.d.ts +11 -6
- package/dist/core/tool-roster.js +8 -4
- package/dist/core/write-protect.d.ts +3 -1
- package/dist/core/write-protect.js +4 -5
- package/dist/tools/fs/bash-program-position.d.ts +2 -1
- package/dist/tools/fs/bash-program-position.js +15 -4
- package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -2
- package/dist/tools/fs/bash-readonly-classifier.js +5 -3
- package/dist/tools/fs/program-read-model.d.ts +7 -2
- package/dist/tools/fs/program-read-model.js +21 -1
- package/dist/tools/fs/program-read-model.rows.js +11 -10
- package/dist/tools/fs/read-deny-home.d.ts +5 -1
- package/dist/tools/fs/read-deny-home.js +40 -19
- package/dist/tools/fs/shell-read-check.d.ts +1 -1
- package/dist/tools/fs/shell-read-check.js +43 -14
- package/dist/tools/fs/shell-read-environment.d.ts +74 -0
- package/dist/tools/fs/shell-read-environment.js +71 -0
- package/dist/tools/fs/shell-read-establish.d.ts +8 -4
- package/dist/tools/fs/shell-read-establish.js +34 -36
- package/dist/tools/fs/shell-read-identity.d.ts +3 -2
- package/dist/tools/fs/shell-read-identity.js +5 -3
- package/dist/tools/fs/shell-read-judge.d.ts +69 -15
- package/dist/tools/fs/shell-read-judge.js +30 -10
- package/package.json +1 -1
- package/wire-changes/core@7.49.1.json +57 -0
- package/wire-changes/core@7.49.2.json +138 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,88 @@
|
|
|
1
|
+
## 7.49.2 — 2026-10-13
|
|
2
|
+
|
|
3
|
+
**Hard-breaking changes (#1674):** one, open to veto — #1674 (see its entry). **Closed sets:** none. **Package exports:** +0 / −0 names, no member changes (the exported data `describeToolCatalog()` / `TOOL_CONFORMANCE_VECTORS` change values, below). **Behavior:** #1674 mounts `LSP` only on a leg that has an execution env, and seeds the pre-mount effect table for a caller's own tool that shares a hand-band name with the more conservative of its declared effect and the hand's. **Known limits added** (`docs/KNOWN-LIMITS.md`): KL-1674-a.
|
|
4
|
+
|
|
5
|
+
### A leg without hands has no LSP tool (#1674; fix; security; hard-breaking: one, open to veto) (@server @cli @client-core @test)
|
|
6
|
+
|
|
7
|
+
**Security face first — a leg with no execution env no longer mounts `LSP`.** Before, a runner built without an execution env that still carried `lspManager` mounted the tool over the engine's stub environment, whose `fileInfo` answers `not_supported` (`src/core/stub-env.ts:62`) — a precheck outcome the tool lets through (`src/core/lsp.ts:488-491` blocks only not-found / invalid / directory) — and then asked the manager for a session (`lsp.ts:506`); the in-process manager's default reader opens the path on the host's own disk (`src/engine/lsp/node-lsp-manager.ts:520`). So the one leg with no read face at all had an LSP reader outside every fence. @server: the hands-less runner is built with `withoutExecutionEnv` (`src/boot/stage-06-runners.ts:303`) and the hands lane drops only `executionEnvFactory` (`src/capabilities/hands-lane.ts:29`) — both coordinates as reported to this repository, not re-read here (inferred); a deployment-side drop of `lspManager` on that runner is the matching shape on your side.
|
|
8
|
+
|
|
9
|
+
- **Before (v7.49.1).** The catalog row said `mountedBy: ["optional"]` and the mount read `if (lspManager)`: a manager on the task seat or the deployment seat mounted the tool on every leg, hands or not (measured on a hands-less leg that also carried a caller's own tool named `LSP`: the model was offered the name twice).
|
|
10
|
+
- **Now.** One rule: LSP mounts where the hands are AND a manager seat resolved (`spec.lspManager ?? deps.lspManager`). The catalog row says `mountedBy: ["hands"]` and the mount site carries the same tag. A hands-less leg with a manager wired has no engine-mounted `LSP` in its roster (a caller's own tool of that name still mounts), calls it as an unknown tool when none is mounted (`Tool LSP not found.` with the live roster, `details.code` `tool.not_found`, `absence` `unknown`), never reaches the manager, and its wiring manifest still discloses the seat: `lsp: { mounted: false }`. The diagnostics lane already required hands and is unchanged. Exported data: `describeToolCatalog()`'s LSP row `mountedBy` `["optional"]` → `["hands"]`; `TOOL_CONFORMANCE_VECTORS`: `hands-full` and `hands-readonly` gain the absent row `{ LSP, "needs a wired LSP manager" }` and `plan-mode` drops it — every vector's `expected` set is unchanged; `predictMountedToolNames` of a hands-only set now names `LSP`, and `handBandEffects()` / `HANDS_BAND_TAGS` consumers see one more hand-band name (`LSP`, effect `read`). The JSDoc of `RunnerDeps.lspManager` / `TaskSpec.lspManager` and the SDK page say "legs with hands". The hand band's effect table is filled by name up front on every leg with an execution env; with `LSP` in the band, a caller's own tool of that name on a leg with no manager wired would have read `read` there whatever it declared — a declared-`write` one would pass `allowDirs` confinement, which it was refused by before. So a caller tool's own name now claims the entry the way an alias already did: the table is seeded with the less repeat-safe of its declared effect (a write when it declares none) and the hand's, for every hand-band name, not only `LSP`, and read off the instance the roster keeps when several caller tools share a name (the later one — one selection, `survivorsByName`, used by the roster, by this table and by the post-assembly sync; an earlier same-named tool's declaration leaves no entry, none declared on the kept one ⇒ write). A caller tool is written into the table by that scan alone: the roster's scenario mount no longer writes it (it used to write an absent name from whichever same-named tool arrived first, over a kept tool that declared nothing — for any tool name, not only the hand band's), and the post-assembly sync fills only from each name's kept instance. The alias mirror (effect, tier, probe, egress mark onto each alias) walks the same kept instances in the roster's order too: a dropped duplicate's alias used to be registered with the dropped instance's effect (`Scratch(read, alias a)` then `Scratch(write, alias a)`: the kept write instance runs a call by `a`, the table said `read`, and a wake-time reconcile called an interrupted such call a safe retry). The seed is what the pre-mount readers see (the wake-time reconcile, the confinement policy) until a mount sets the entry, and an engine tool that does mount (`LSP` with a manager wired) replaces it with its own effect. The table keyed by the mounted tool instead of the name is the planned root fix (#1680, 7.50.0).
|
|
11
|
+
- **Hard-breaking (open to veto).** (1) **What changed:** a leg without an execution env and with a manager wired no longer has the `LSP` tool (it used to). (2) **Who is hurt:** a deployment whose hands-less scenarios relied on `LSP` — server `hands: none` runs with LSP enabled lose the tool (user-visible: the model is no longer offered code-intelligence lookups there); a host that derives its hand band from the catalog (`HANDS_BAND_TAGS`, `handBandEffects`) or checks its factory against `TOOL_CONFORMANCE_VECTORS` sees the row move as above (server's memory-notice filter keys on effect ≠ read — inferred unaffected); a probe that pinned `lsp.mounted: true` on a runner with no execution env. (3) **How to migrate:** give the scenario that needs LSP an execution env (hands); a library host that really wants LSP without hands mounts `createLspTool` itself over a manager and an env it owns (that mount is the host's own: the engine fences nothing in it, KL-1674-a).
|
|
12
|
+
- **Known limits.** The tool on a leg WITH hands is unchanged: its `filePath` is still not a declared read target and its session reads through the manager's own reader (KL-1674-a); the read-face wiring is a later release's.
|
|
13
|
+
- **Pins.** `test/backlog1674-lsp-needs-hands.test.ts` (① hands-less × deps seat / task seat: not in the roster, `{ mounted: false }`, no session lookup; ② the call is the unknown-tool miss in sentence and record, manager unreached; ③ hands × both seats: mounted, `{ mounted: true, lane: "tool" }`, a call reaches the manager once; ④ catalog and roster row carry `hands`; ⑤ plan-mode / bare mount none under a wired manager, hands-full / hands-readonly flip exactly `LSP`; ⑥ hands from an execution-env factory count, and a caller's own `LSP` tool on a hands-less leg is offered once and runs); `test/backlog1674-hand-band-same-name.test.ts` (a caller tool named `LSP` declaring write / declaring none / declaring read on a leg with hands and no manager: table and `allowDirs` verdict; the engine's own LSP unclaimed; another hand-band name, `TaskOutput`, under read-only hands; ⑦ two caller tools under one name — `[read, none]`, `[none, read]`, `[read, read]`, `[write, write]` × full / read-only hands × a hand-band name (`LSP`) and an ordinary name: table, roster row and `allowDirs` verdict follow the kept instance; ⑧ the same with a shared alias — the alias entry, the instance a call by the alias runs, the `allowDirs` verdict and the wake-time reconcile's wording follow the kept instance, with the claim order of the roster); `test/tool-roster.test.ts` unchanged and green; re-pinned: `test/prepare-lsp.test.ts`, `test/prepare-lsp-transcript.test.ts`, `test/backlog789-wiring-manifest-hooks-lsp.test.ts`, `test/public-knob-liveness.test.ts`, `test/tool-catalog.test.ts` (hand-band snapshots), `test/backlog1432-deny-universe.test.ts` (hand list), `test/design388-write-fences.test.ts` (a scenario mount writes nothing), `test/backlog1167-roster-freeze.test.ts` (R28: a non-array `tools` value is refused by the roster door's code).
|
|
14
|
+
|
|
15
|
+
**Hard-breaking changes (#1672):** one exported value's shape, and one exported ordering. `SHELL_READ_FINDING_RANK` is renumbered (`unanswered` sits between `deny_hit` and `outside_roots`: 4 / 3 / 2 / 1 / 0 for `unanswered` / `outside_roots` / `unestablishable` / `declined` / `none`; `identity_changed` 6 and `deny_hit` 5 keep their numbers). `SHELL_READ_DECISION.unestablishable.full_shell` is the object `{ narrowed: "mandated", open: "ordinary" }` where it was the string `"mandated"` (every other cell stays a string), and `SHELL_READ_FINDINGS` / `SHELL_READ_DECISION` gain the member / row `unanswered`. A consumer that compares that cell to a disposition word, or switches exhaustively over the findings, reads the new shape — the readers in the family are sema-cli's dist verify scripts (`verify/_bash-gate-danger-matrix.mjs`, dev-time, they compare cells and the six-word finding set) and sema-server's `test/read-face-e2e.test.ts` (prose only); both read `typeof cell === "string" ? cell : cell.narrowed` for the engine default. **Closed sets:** `ShellReadFinding` +1 (`unanswered`). **Package exports:** +0 / −0 names. **Behavior:** #1672 loosens the mandated probe 7.49.0 made of every read the station cannot place — on an `open` read face and for a written target alone, both toward the reference implementation's plain permission ask (CC 2.1.296), open to veto; the engine-default `roots` face keeps the mandate. **Known limits added** (`docs/KNOWN-LIMITS.md`): KL-1644-c rewritten (the rewritten redirection target is the ordinary question now), KL-1672-a, KL-1672-b, KL-1672-c (a followed sed backup the text places nowhere is still read open, as in 7.49.1).
|
|
16
|
+
|
|
17
|
+
### A read the station cannot place is the read face's question: the plain permission ask on an `open` read face and for a written target, the mandated probe on a `roots` read face (#1672; fix; loosening of 7.49.0 toward the reference; hard-breaking: one value shape; closed sets: `ShellReadFinding` +1; exports: +0 / −0) (@web-admin @server @cli @test)
|
|
18
|
+
|
|
19
|
+
- **Before (7.49.1).** Every word the read station could not place — `ls -1 $ANDROID_HOME/platforms`, `cat ~root/x`, `cat $(echo …)`, a program the shell re-enters (`sh -c '…'`), a directory change the text does not settle, a redirection or `tee` operand the shell rewrites (`> "$DEST"`) — drew the write/read face's MANDATED probe: no allow of any kind, a spec pre-approval (`allowedTools: ["Bash"]`) included, released it, and under `auto` the classifier was never asked. The reference implementation asks the same word as a plain permission ask (CC 2.1.296 `sR`, cli296.js @13116941, reason `other`: "Shell expansion syntax in paths requires manual approval"; the tilde variants likewise) that a whole-tool allow (`$pn` @18717070), bypass (`$pn` @18716980) and the auto-mode classifier release and a prefix allow does not (`vX`'s ask returns before the allow rules, @18567915). A headless team run (`-p`, `auto`) whose first command read `$ANDROID_HOME/…` hung on the card.
|
|
20
|
+
- **Now.** One decision-table cell, keyed by the READ FACE the question was raised under (`SHELL_READ_DECISION.unestablishable.full_shell`, read by `decideShellRead`): on an **`open`** read face — where the station asks only because a deny list has rows — the unplaced read is the ORDINARY ask: `default` / `acceptEdits` ask it, a spec pre-approval and `bypassPermissions` release it, the auto-mode classifier is consulted and its yes releases it, `dontAsk` refuses it by the mode; a stored whole-tool rule and a prefix allow keep asking it. On a **`roots`** read face (the engine default) it stays the mandated probe, as in 7.49.1: a deployment that narrowed the fence asked to be asked. A **written target** the station cannot place and nothing else (`echo x > "$DEST"`, `printf x | tee $X`) is the ordinary ask on either read face — a narrowed write face fences what is written and is no request to be asked about every word the floor cannot place; a command that also has an unplaced READ keeps the read face's reading. The read-only face and the post-approval re-judgement still refuse; the deny row, the out-of-roots mandate, the removal guards and the judge that gave no answer rank above or beside the cell and do not move. The unplaced-read card (`read_unestablished`) rides the finding on either question. A deployment that wants the plain ask for such commands declares the read face `open` (`RunnerDeps.readFace: "open"`); a lane on the engine default keeps the card.
|
|
21
|
+
- **A judge that did not finish is its own finding, `unanswered`**: the gate's fallback when the station timed out, threw or a lookup it stands on was absent — and, recorded at ONE door, anything the station could not finish while establishing the command: an environment lookup that gave no answer (`canonicalPath`, `listDir`, `fileInfo`, `readLink` returning an error that is no conclusive "not there"; a conclusive absence and an adapter that declares no filesystem surface are not failures, and a resolver that declared nothing and answered NOTHING in the whole command keeps the lexical reading it always had — one answer anywhere in the command, the declared roots and a write guard's probe included, makes every silence beside it a failure, whichever word it falls on; the decision is taken once, after the write guards have asked too, so a read and a write on the same resolver are read alike), a bound the station stopped at (more directories or names than it enumerates, a link chain past its hop bound), a home view that could not be read (an identity lookup that errored on the path, on a level of the declared home's chain or on an auxiliary home's, or a home whose canonical spelling did not answer), and the write guards' own probes (they ask the environment through the command's one door; what their silence means is decided with the establishment's, and folded into the verdict BEFORE the plan's `writesPlaced` is read off it). Any one makes the WHOLE command `unanswered`, whatever the words it did reach found: mandated on the full shell on every read face — a standing allow was given on the premise that the judge works — and refused on the read-only face; ranked directly under `deny_hit` (`identity_changed > deny_hit > unanswered > outside_roots > unestablishable > declined > none`), so the redirection-only ordinary cell, a grant offer, a pre-approval or the classifier cannot answer for the part it did not check. It was the `unestablishable` row in 7.49.1, and earlier cuts of this ticket let a lookup's failure fall into that row one producer at a time — the ordinary question on an open read face with a deny list, so a pre-approval or the classifier could release a read the station had not checked against the deny table (`cat link` whose resolution errored while the link led to a denied file; a glob write whose listing errored; a path spelled through a bind-mount alias of the home whose identity lookup errored). The establisher now asks the environment ONLY through the observed door (`shell-read-environment.ts`: it is handed the door, not the environment), which records each failure and bound on the establishment's `incomplete`; the fold reads that list in one rule. No producer has to remember to mark anything. The door covers every lookup the command makes — the establishment, the declared roots, the home views, the write guards AND the leg's write judgment — and the station decides what a silence means once, after all of them have asked. `ShellReadBoundary.writeTargetJudge` therefore takes the command's door in its options (`opts.fs`, required) and resolves nowhere else (type-level change on a public member: a host-supplied judge keeps compiling and should resolve in `opts.fs`; no consumer in the sibling checkouts supplies one — **inferred**, grep of the local sema-server / sema-cli trees). What stays `unestablishable` is the TEXT's own unplaced word: a word the shell rewrites, a lost working directory, a pattern the matcher does not model, a spelling it cannot place.
|
|
22
|
+
- **The allow lane's floor reads `~user` / `~+` / `~-` as it reads `$`** (b): a tilde word that is neither the bare `~` nor `~/…` — also after `=` / `:` in an assignment-shaped word — is an expansion word (the account database or the shell's own variables decide where it points), so a standing prefix allow no longer matches a command carrying one, the classify faces read it as outside their residual space (no longer a read-only command), and a rule body naming one is `invalid.not_simple_command` (as with `$`). The bare `~`, `~/…`, quoted and mid-word tildes are read as before. Consequence: an `ask` / `deny` rule text naming a `~user` word can no longer be minted (an org snapshot carrying one fails loudly, as one naming `$X` does).
|
|
23
|
+
- **Who needs it.** Any session on an `open` read face with a deny list (a single-user product) whose shell reads go through a variable, a substitution, a re-entered shell text or an unknown program: the pre-approval and the classifier answer them now. **Who is hurt.** A deployment that relied on the mandate to stop an `allowedTools: ["Bash"]` run from executing an unplaced read on an open face no longer gets it (the deny rows still refuse every spelling the station can place; an unplaceable spelling is the ordinary question). A lane on the engine-default `roots` face is unchanged for reads — it is released only by `bypassPermissions`, a person's yes, or the deployment declaring the face `open`.
|
|
24
|
+
- **Divergence from the reference (open to veto).** A stored whole-tool `Bash` rule does not clear this ask (the reference's `N9` does): the shape tier's irreversibility mark still asks (the rule-silencing principle, as for the redirection ask in 7.49.1). The `roots` face's mandate is sema's (C-R261b), not the reference's.
|
|
25
|
+
- **Consumers.** web-admin / server: a sandbox lane that mounts the engine-default read face keeps the card for `$VAR` reads; declare the lane's read face `open` to release it (this ticket does not change a deployment's face). cli: `SHELL_READ_DECISION.unestablishable.full_shell` and the finding set changed shape (above). test: re-pinned readings are the open-face / written-target cells of the pins listed below.
|
|
26
|
+
- Pins: `test/backlog1672-variable-read-ordinary-ask.test.ts` (shapes × read face × nine arms on the real Runner, the tilde floor's four consumers, the table's face-keyed cell, the seats, `unanswered` on both faces, the floors above the cell on both faces); re-pinned where an OPEN-face cell or a written-target cell was pinned mandated: `backlog1100`, `backlog1102-program-facts`, `backlog1102-read-station-semantics`, `backlog1421-launcher-reread`, `backlog1425-readonly-binding`, `backlog1427-write-targets-roots`, `backlog1599-delegate-auto`, `backlog1644-device-targets`, `backlog769`, `backlog791`, `backlog854`, `backlog860`; the table readers `backlog924`, `backlog1004`, `backlog1010`; the equivalence allowed lists (class `1672-tilde-variant-floor` in both) and the #994 S1 attribution rows `tilde_variant_floor`, `write_target_ordinary`.
|
|
27
|
+
|
|
28
|
+
**Hard-breaking changes (#1675 / #1676):** none. **Closed sets:** +0 / −0. **Package exports:** +0 / −0 names, no member changes (`npm run handoff:diff v7.49.1 HEAD`: 2 609 → 2 609). **Behavior:** #1675 removes a value that was left in clear after a bearer token, and #1676 stops one marker from being written cut — redaction only, nothing a run could do before is refused. **Known limits added** (`docs/KNOWN-LIMITS.md`): KL-958-c, KL-958-d, KL-958-e (and KL-958-a restated).
|
|
29
|
+
|
|
30
|
+
### A bearer credential is the whole run to whitespace — a name=value glued to the token goes with it (#1675; fix) (@server @cli @client-core @test)
|
|
31
|
+
|
|
32
|
+
- **Before (7.49.1, and 7.49.0).** `Authorization: Bearer <vendor token><connector><name>=<value>` left `<value>` in clear when the name was no credential keyword or sat glued to the token. The token pass takes the token (its body takes `_` `-`), the bearer row stopped at `=`, and the keyword pass found no name for the value. 7.48.0 hid it, in the `Authorization:` spelling, only by accident: its bearer row took the token, and the `Authorization:` keyword row's value run then ate the glued remainder. On a 19 000-text constructed probe (seeded; eleven token families × connectors `_` `-` `.` / none × ten keyword and ten plain names × seven wrappers; values and tokens built at run time) 7.49.1 left at least 6 characters of the value visible in 383 of 6 000 bearer-wrapped texts where 7.48.0 did not (3 488 of 6 000 in all on 7.49.1, most of them texts where 7.48.0 showed it too — a token row that takes the name before the bearer row reads the text; 4 386 on 7.48.0).
|
|
33
|
+
- **Now.** The bearer row's wide form reads the credential the way the header does: the token68 run and, when `=` follows it, whatever is glued after the `=` up to the same delimiters the keyword row's value run ends at (whitespace, a quote, `;`, `|`, `&` — one constant, read by both rows). The tail starts at `=` only: `Bearer <token>,next=1` still ends at the comma. A quote right after the `=` is read by the character that follows it. A boundary (`,` `}` `]` `:`, white space, another quote, the end) = the quote that closes a string around the text: the `=` is the token's padding and no tail starts. Anything else = an opening quote: the value up to the next quote of the same spelling goes whole, quotes included (bounded to 1 024 characters, one line; a backslash escape closes with the same word behind exactly the same number of backslashes — a quote of another depth inside is value content). An opening quote with no closing quote is read as an unquoted value, to its usual end and no further: it never lets the value through. Whether a string around the text closes inside that run is not something a local reading can tell, so it does not try: when the text sits inside a string whose own quotes are written as escapes (a backslash and a quote, a Unicode escape, `%22`), the run takes that string's encoded closing quote and the members after it in the same segment with the value, over-redaction being this face's cheap direction (known-limits row). The spellings: `"` and `'`; the backslash escapes `\"` `\'` `\x22` `\x27`, the Unicode escapes (a backslash, then `u0022` or `u0027`) and the octal `\042` `\047`, behind up to 63 backslashes; `"` `'` `"` `'`; `%22` `%27`. Lower case only (`\X22` and `\U0022` are no quotes), which is why the header word is spelled out (`[Bb][Ee]…`) instead of carrying the `i` flag. Any other backslash after the `=` is value content, and the tail starts. The run gives back the backslashes that escape the quote closing it (up to 63 — six layers of JSON inside JSON; the check looks a bounded 63 characters ahead, so a backslash run of any length costs a bounded amount per hit), so `\"Bearer <token>=\"` inside a string keeps its escape. A hit that ran over the next credential's keyword (`…,Bearer` at the end of a glued value) no longer hides that credential: the engine scans every vocabulary row on the original text from one character after each hit's start instead of at its end — bounded to eight characters of matches per character of input; a scan that runs out of that work stops, and everything from its last hit's start to the end of the text goes as one hit (over-redaction, never a leak; known-limits row) — and the union with the sequential run removes what the run left. No vendor row, character class or word list changed; the strict (refuse-write) form is untouched. The pass engine's existing union carries the enclosing hit, so the vendor token's own finding is still reported beside the bearer finding, whose span is now the whole credential. Same probe: 0 of 6 000 bearer-wrapped texts show a value on any of the three faces (the activity summary is pinned separately, A1), 0 show a token, and no text shows a value or a token that 7.48.0 hid.
|
|
34
|
+
- **Who is hurt.** Nobody who relied on a value surviving: text after `Bearer <token>=` up to whitespace is part of the credential now (over-redaction is this face's cheap direction). A host that counts findings sees the bearer finding's span reach the end of the glued tail.
|
|
35
|
+
- **Consumers.** None needs to act.
|
|
36
|
+
- Pins: `test/backlog1675-bearer-tail-and-url-marker.test.ts` A (two token families × four wrappers × four connectors × six names on four faces; the delimiters; the header word's case; JSON inside JSON up to six layers; every quote spelling × bare / spaced / comma-and-brace / inside a JSON string, opening and closing; the escape-depth boundary; a second credential whose keyword the first hit's run took in; a scan that runs out of work, fail-closed; a two-million-character run; the finding's span) and D (the seeded probe against the frozen v7.48.0 chain: ≥ 2 000 texts, no value or token the old reading hid is shown, every marker whole).
|
|
37
|
+
|
|
38
|
+
### A replacement marker is never cut: a URL userinfo that holds a token and a keyword-named tail (#1676; fix) (@server @cli @client-core @test)
|
|
39
|
+
|
|
40
|
+
- **Before (7.49.0 and 7.49.1).** `https://user:<vendor token>_password=<value>@host/path` (the `curl -H` spelling too) read `https://user:-credentials]` on the same-principal tier: the marker lost its head and the host went with the value. The userinfo replacement is a function, so the engine inferred what it keeps from the characters it shares with the match; the match had run to the vendor token's marker `[redacted`, which `[redacted-credentials]` starts with, so the kept head reached into the marker and the union wrote the rest. The value was not shown; the output was broken. 933 of 4 000 constructed URL texts on 7.49.1 (0 on 7.48.0).
|
|
41
|
+
- **Now.** What a function replacement keeps at each end never reaches into the pass's own marker literal (the last occurrence in what it wrote); a replacement that writes no marker literal is read as before. Same probe: 0 broken markers, and no value or token shown, on the same-principal and human tiers.
|
|
42
|
+
- **Not changed (KL-958-c).** When the name is a credential keyword, the keyword hit read on the original runs over the `@` (a value run ends at whitespace), so the host tail still goes with it: 1 727 of 2 046 such texts on 7.49.1 and now, 887 on 7.48.0 (only for the families whose token body does not take the name). The host is no secret; the loss is the over-redacting direction. Closing it needs one more engine rule (text a pass explicitly keeps bounds the hits read on the original) and is left to 7.50.0.
|
|
43
|
+
- **Who is hurt.** Nobody; the broken marker was never meaningful. A host that parsed `[redacted-credentials]` out of these outputs now finds it.
|
|
44
|
+
- **Consumers.** None needs to act.
|
|
45
|
+
- Pins: `test/backlog1675-bearer-tail-and-url-marker.test.ts` B (both families, both name classes, the `curl` spelling, bare / userinfo tokens) and C (the engine law on synthetic passes — head side and tail side).
|
|
46
|
+
|
|
47
|
+
## 7.49.1 — 2026-10-12
|
|
48
|
+
|
|
49
|
+
**Hard-breaking changes:** none. **Closed sets:** `OptionMode` +1 (`queries`; internal to the program read model, not exported). **Package exports:** +0 / −0 names, no member changes (`SHELL_WRAPPER_TABLE` is byte-equal: a lookup switch is no peel flag; the two types #1583 / #1640 add live in a runner-internal module). **Behavior:** #1583 removes a value that was left in clear next to a credential-shaped name and #1640 makes a line say what already happened — nothing a run could do before is refused; #1643 and #1644 restore two readings 7.49.0 made too strict to the shell's and the reference's own — both are loosenings of 7.49.0 toward 7.48.0 and the reference implementation (CC 2.1.296), each open to veto. **Known limits added** (`docs/KNOWN-LIMITS.md`): KL-1643-a / -b, KL-1644-a / -b / -c (and the rows #1583 / #1640 carry).
|
|
50
|
+
|
|
51
|
+
### A name an earlier cut swallowed no longer frees its value (#1583; fix) (@server @cli @client-core @test)
|
|
52
|
+
|
|
53
|
+
- **Before (7.49.0).** The vendor-prefixed pass runs before the keyword pass, and a prefixed token's body takes `_` and `-` (a GitLab token does by construction): `glpat-<22>_password=<value>` read `[redacted]=<value>` — the cut took the name `password` with the token, and the keyword pass, reading the text the cut left, found no name for the value. 7.48.0 had no `glpat-` row, so the keyword pass saw the name and removed the value (its `ghp_` and `sk-` rows swallowed the name the same way). On a 2 700-case constructed probe (seeded: five prefixes × nine names × five value separators × six wrappers) 7.49.0 left at least 6 characters of the value visible in 416 cases where 7.48.0 did not — all of them `glpat-`; the token itself was cut better (7.48.0 left it visible in 1 080 cases, 7.49.0 in none).
|
|
54
|
+
- **Now.** The pass engine (`runRedactionPasses`, behind `scrubSecrets`, `redactSecrets`, `redactHostLeaks` and the activity summary) reads every pass's hits on the original text as well, and adds every hit the sequential run did not remove whole to what it removes: a value that holds another pass's hit (a bearer value, a URL, a host path — the former "strictly encloses another pass's hit" rule) and an assignment whose name an earlier cut took are one reading, not two. The name of a hit is read on the original text, never on what a cut left. No token row, character class or vendor case changed. The added hit is reported beside the token's (kind `keyword-value`, span in the original text). The union is rendered from the replacements' templates — a label a replacement keeps is the template's group, never characters it happens to share with the match — so a marker is never cut or doubled; the format-character path uses the same disjoint rendering for the view and for every boundary scan (an overlapping scan is no longer dropped, so a token a format character hid from the view is still found; a format character a matched value ran over goes with it instead of being left after the marker) and merges the scans' spans in one pass. On the probe the value axis goes 1 192 (7.48.0) / 1 608 (7.49.0) → 360 and the token axis stays at 0; the 360 left are one shape — a name behind an escaped quote inside a JSON string (`\"password\":\"…\"`) — which 7.48.0 reads the same way and which prefixes that never take a name (`AKIA`, `ASIA`) leak on identically (KL-958-a).
|
|
55
|
+
- **Who is hurt.** Nobody who relied on a value surviving: text that holds a prefixed token glued to a credential name by `_` or `-` and then `=` or `:` loses the value too — over-redaction is this face's cheap direction. A host that counts redaction findings sees one more `keyword-value` finding on those texts.
|
|
56
|
+
- **Consumers.** None needs to act.
|
|
57
|
+
- Pins: `test/backlog1583-prefix-adjacent-value.test.ts` (four prefixes × name separators `_` `-` `.` × value separators `=` `: ` JSON key × bare / nested, on four faces, each row beside its recorded v7.48.0 reading; the engine rule on synthetic passes, with the three shapes the union path made reachable — a kept label read from the template not from shared characters, format-character boundary scans that keep what they found, a linear merge of their spans; the seeded probe against 7.48.0's recorded counts); `test/backlog958-credential-vocabulary.test.ts` D5 unchanged and green.
|
|
58
|
+
|
|
59
|
+
### The dropped-call line says what became of the turn (#1640; fix; +1 optional detail key) (@cli @server @client-core @test)
|
|
60
|
+
|
|
61
|
+
- **Before (7.49.0).** `task.tool_turn_incomplete` ended every dropped-call sentence (`truncated`, `unnamed`) with "the model was asked to re-issue them". On a turn whose only content was the dropped call (a call cut at the output cap, no text, no valid call) the lane fails the turn and the run ends with `terminal.detail.kind` `model_output_invalid` after one request; a provider refusal / content filter ends the run the same way, and a turn that completed with text and nothing to run ends it with the text — in none of them was anything asked.
|
|
62
|
+
- **Now.** The line says what became of the run only where the turn's own message states it, and is silent everywhere else. (1) Where the lane's dropped-calls account is on the failed turn (`modelOutputInvalid`, built once by `modelOutputInvalidOf` and read back by `modelOutputInvalidOfMessage`, the admission the terminal's `detail` is minted from) the run did end: "…so they did not run; the run ended with this turn (model_output_invalid)", `outcome: "terminal"`. (2) Where the turn goes on by the loop's own turn rules — it did not fail, and it carries a tool call (a tool-calling turn guarantees a next request), or stopped to call a tool (the malformed-tool-use retry asks again), or stopped at the output cap (the truncated-output continue asks again) — the sentence is unchanged, "…so they did not run; the model was asked to re-issue them", `outcome: "reissued"`. (3) Any other turn — a provider refusal / content filter, an abort, a turn that completed with text and nothing to run, a torn stream the loop may resume — says only "…so they did not run" and carries no `outcome`: whether a request follows is the loop's decision, not the message's. New optional key `detail.outcome` (`"terminal"` | `"reissued"`): new optional key, readers need no action; present only when `cause` is `truncated` or `unnamed` and the message states it — its absence is a fact, not missing data; absent on `bad_frame` and `missing_result_synthesized`. Once per run per cause stands — the first turn that showed the cause decides. A `reissued` turn is the turn, not the run: a recovery budget already spent, a turn limit or an interrupt that follows it is not read (KL-1598-b; the exact reading, announced after the loop decides, is #1666).
|
|
63
|
+
- **Who is hurt.** A consumer that matches the sentence text for the cases above (a refusal, an abort, a text-only completion and the failed turn no longer say "re-issue"); everything else is read from `detail`, which only gained a key.
|
|
64
|
+
- **Consumers.** cli may show `outcome`; nothing needs to change. The harness's own continuation gate (`lastTurnHadToolCalls`) now reads the same exported predicate (`turnCarriesToolCall`), one spelling.
|
|
65
|
+
- Pins: `test/backlog1640-incomplete-outcome.test.ts` (three lanes × two causes × the dropped call alone / beside a valid call / beside text only / a provider refusal, each against the run's own terminal and request count; a torn stream the loop resumes, said nothing about; the seat on hostile and mixed messages; the one shared predicate); `test/backlog1230-tool-turn-incomplete.test.ts` ③ and `test/backlog1598-truncated-args.test.ts` ③ ⑧ re-pinned for the new key.
|
|
66
|
+
|
|
67
|
+
### A launcher's bare `--` and `command -v` / `-V` are read as the launcher reads them: neither is text the engine could not read (#1643; fix; loosening of 7.49.0 toward 7.48.0; hard-breaking: none; closed sets: `OptionMode` +1; exports: +0 / −0) (@test @cli @server @client-core)
|
|
68
|
+
|
|
69
|
+
- **Before (v7.49.0).** The launcher rows (`command`, `env`, `nice`, `nohup`, `builtin`, `exec`, `stdbuf`, `timeout`, `setsid`, `flock`) carried neither a bare `--` (end of options: the next word is the program) nor `command -v` / `-V` (a lookup: the words after it are names, nothing is run). The program-position walk refused both as "an option no row explains", and 7.49.0's removal guard asks a refused program position as text the engine did not read — `unverifiable_shell_text`, a release row no stored allow and no `bypassPermissions` clears, which `dontAsk` refuses. So `command -v git`, `if command -v node >/dev/null; then …`, `test -x "$(command -v git)"`, `command -- ls`, `env -- ls`, `nice -- ls`, `nohup -- ls` and `timeout -- 5 ls` drew an uncleared question whose card said "`command` runs shell text this guard cannot read (command -v git)" about a command whose text was plain; `command -- rm -rf "$X"` was asked as unreadable text instead of the removal guard's own unverifiable-delete question. 7.48.0 asked these in `default` (an ordinary "the read boundary could not establish what is read") and ran them under a whole-tool `Bash` allow and under bypass.
|
|
70
|
+
- **Now.** One walk, rows carry the fact. A bare `--` is stepped by every launcher row the binary gate witnesses against its real program (`command`, `builtin`, `exec`, `env`, `nice`, `nohup`, `stdbuf`, `timeout`, `setsid`, `flock`; `time`, `sudo`, `doas`, `su`, `runuser`, `watch` already did) and it ENDS the options: the next word is the program (or the launcher's required operand — `timeout -- 5 ls`), whatever it spells, and a word spelled like an option after it is not stepped (`env -- -i ls` stays unread: bash runs a program named `-i`). `command -v` / `-V` (also `-pv`, `-p -v`, behind another launcher: `env command -V x`) is a new option mode, `queries`: the chain names no program, so nothing is read as run — `command -v rm -rf "$X"` looks up three names and removes nothing, and asks no removal question. `command -- rm -rf "$X"` reads as `rm -rf "$X"` (`unverifiable_delete`, as without the launcher). An option no row explains (`command -x ls`, `env --frobnicate ls`, `nice -z ls`) is still unread text. The card says "cannot read" only for a text that was not read: the finding and its sentence are one source.
|
|
71
|
+
- **Who needs it.** Any session whose shell checks for a tool before using it (`command -v`) or ends a launcher's options with `--`: both were uncleared questions under a whole-tool allow and under bypass. **Who is hurt.** Nobody is newly asked; this is looser than 7.49.0 by exactly the two spellings, and each word after `--` is judged as the program it is (a removal behind `command --` is the removal guard's). `default` / `acceptEdits` still ask a lookup the ordinary question (KL-1643-a: the lookup is no read-only program, as in 7.48.0 and the reference).
|
|
72
|
+
- **Divergence from the reference (open to veto).** CC 2.1.296's wrapper strip steps `--` for `command`, `builtin`, `nice`, `nohup`, `time` and `timeout` (`bL` @18513380, `R3e` @18541453) — aligned — but not for `env` (`cvr` @18513144 returns −1 on it) or `stdbuf` (`uvr`), which stay the program there; sema reads them by the real grammar (GNU and BSD `env`, `stdbuf`: getopt ends options at `--`). `exec`, `setsid`, `flock` are not in the reference's wrapper set at all.
|
|
73
|
+
- **Compensation.** None needed. **If unchanged.** A plain presence check or `--` form keeps drawing an uncleared card that says it cannot read text it read.
|
|
74
|
+
- Pins: `test/backlog1643-launcher-options.test.ts` (① lookups × five arms × two read faces, ② `--` per launcher, ③ the word after `--` is the program, `-- -i` stays unread, ④ unexplained options stay unread, ⑤ the card's sentence, ⑥ controls; 7.48.0 / 7.49.0 / this tree columns in the receipt); `test/program-read-model.binary-gate.cells.ts` (a `-- <program>` cell for every row that steps `--`, a `-v <names>` cell for the lookup — run against the real programs); re-pinned where `env --` / `command --` stood for "an option no row explains": `backlog780-program-position-reader`, `backlog891-seat-keyed-declines`, `backlog945-plan`, `backlog769-sed-script-slot`, `backlog1498-retire`, `backlog1505-streamfed-shell`, the two program-read-model equivalence allowed-lists (classes `1643-launcher-end-of-options`, `1643-launcher-lookup`) and the #994 S1 dual-run attribution `launcher_end_of_options`.
|
|
75
|
+
- Consumers: test — the G10 forms (`command -v`, `command --`, `env --`, `nice --`) answer as the table above; cli / server / client-core — wording only (the card no longer says "cannot read" for these), no new field.
|
|
76
|
+
|
|
77
|
+
### An output redirection outside the write roots is the reference's ordinary redirection ask again — `> /dev/stdout`, `2>/dev/stderr`, `> /dev/tty`, `> /dev/fd/N` ask the ordinary question, bypass and a pre-approval release them; `> /dev/null` stays exempt (#1644; fix; loosening of 7.49.0 toward 7.48.0 and the reference; hard-breaking: none; closed sets: +0; exports: +0 / −0) (@test @cli @server @client-core)
|
|
78
|
+
|
|
79
|
+
- **Before (v7.49.0).** #1427 judged every write target a shell command names against the write roots and made an outside finding the write face's MANDATED probe — the same row a read outside the roots has. The reference's redirection check does not: CC 2.1.296 `lvr` (cli296.js @18502854, called @18508831) exempts `/dev/null` alone and asks every other target outside the working directories as a plain permission ask (`> /dev/stdout` too: `sR` @13116941 has no device case), which bypass (`$pn` @18716980) and a whole-tool allow rule (`@18717070`) release and a prefix allow does not (the ask is not `bashAllowRuleOverridable`; `vX`'s caller returns it before the allow rules, @18567915). So 7.49.0 was stricter than the reference on exactly this cell, disclosed with #1427: `echo x > /dev/stdout`, `echo x 2>/dev/stderr`, `> /dev/tty`, `> /dev/fd/N` and `> <outside file>` drew a probe card that nothing but bypass released — a spec pre-approval (`allowedTools: ["Bash"]`) included, which 7.48.0's ordinary question had accepted.
|
|
80
|
+
- **Now.** One cell, read where the reference's check reads it. A command whose every outside-the-roots finding is an OUTPUT REDIRECTION (`>` `>>` `>|` `&>` `N>`, `exec >`, a here-document into a file) reads the redirection cell of the decision table — `ordinary` on the full shell, `refuse` on the read-only face — instead of the table's `mandated`: `default` and `acceptEdits` ask the question (the card still says "writes outside the directories allowed for this session"), `bypassPermissions` and a spec pre-approval release it, `dontAsk` refuses it by the mode; a stored whole-tool `Bash` rule and a prefix allow keep asking it (the rule-silencing principle, unchanged: they did in 7.48.0, and the reference's prefix allow does not release it either). `/dev/null` stays exempt. A program OPERAND written outside the roots (`tee`, `cp`, `mv`, `rm`, `touch`, `sed -i`, a `sed w` file), a redirection beside a read outside the roots, beside such an operand or beside any word the station could not place (`echo x > <outside>; cat "$UNKNOWN"`), and a redirection target the shell rewrites (`> "$OUT"`) keep the mandated probe; the table's row for `outside_roots` is unchanged and still the read face's.
|
|
81
|
+
- **Who needs it.** Any session whose commands print to a standard stream by name or redirect outside the workspace under bypass or a pre-approval. **Who is hurt.** Nobody is asked more than in 7.49.0; a session that relied on the 7.49.0 mandate to stop a redirected write under `allowedTools: ["Bash"]` or bypass no longer gets it (7.48.0 and the reference never did) — the roots still ask under `default` / `acceptEdits`. The 7.49.0 divergence row (stricter than the reference on this cell) is retired.
|
|
82
|
+
- **Compensation.** None needed. A deployment that wants the mandate back for redirections declares a write deny row or runs `default` without a pre-approval. **If unchanged.** A command that only prints to its own output stays unrunnable under a pre-approval.
|
|
83
|
+
- **For the record (not done, awaiting the owner's ruling).** The device spellings the reference's predicate `wwe` (cli296.js @18511591: `/dev/null`, `/dev/stdin`, `/dev/stdout`, `/dev/stderr`, `/dev/tty`, `/dev/fd/<digits>`, `/proc/self/fd/{0,1,2}`) drops from its write list, git-diff operands, read-path list and input-redirection check could be exempt from the containment question too — `> /dev/stdout` would then run unasked in `default` (7.48.0's behavior), one step looser than the reference's redirection check. Not implemented: it departs from the reference and needs an explicit ruling; its cost is that a descriptor spelling is exempt whatever the same command backs it with (`exec 3< <file in a read-only grant>; echo x > /dev/fd/3` writes the file on Linux), and it would also settle KL-1644-a (`/dev/fd/9`).
|
|
84
|
+
- Pins: `test/backlog1644-device-targets.test.ts` (① the real Runner × seven arms × two read faces over outside files and seven device spellings, ② `/dev/null`, ③ what keeps the mandate — operands, a read beside, a rewritten target, ④ the judge's cell and the unchanged table row); re-pinned: `backlog1427-write-targets-roots` (① split into redirections and operands, ⑦ lanes, ⑨).
|
|
85
|
+
|
|
1
86
|
## 7.49.0 — 2026-10-12
|
|
2
87
|
|
|
3
88
|
**Retirement and unification wave — BREAKING.** One reading per fact across the path tables, the tool selector, the removal guard, the checkpoint row, the terminal detail and the decide record; the orchestration families with no reference counterpart leave the package. Every entry below states Before / Now / who is hurt / compensation; this head lists only the hard-breaking items, each open to veto (rulings in parentheses).
|
|
@@ -62,10 +62,10 @@ export interface RedactionPass {
|
|
|
62
62
|
* only the first (or neither) matches. The OUTPUT keeps every format character that sits OUTSIDE a
|
|
63
63
|
* replaced span — a joiner in an emoji sequence, a non-joiner in Persian text, a soft hyphen in a word
|
|
64
64
|
* — and drops only those INSIDE the span the marker replaces (they were part of the secret). A
|
|
65
|
-
* format-free input takes the exact historical path (byte-identical output).
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
65
|
+
* format-free input takes the exact historical path (byte-identical output). The view's replacements and
|
|
66
|
+
* every boundary scan's are taken as disjoint spans of their input (a later pass that matched across an
|
|
67
|
+
* earlier pass's inserted text is rendered from the removals instead), so the spans always map back
|
|
68
|
+
* exactly and no scan is skipped.
|
|
69
69
|
*/
|
|
70
70
|
export declare function runRedactionPasses(input: string, passes: readonly RedactionPass[], report?: RedactionReport): string;
|
|
71
71
|
/**
|
package/dist/core/arg-summary.js
CHANGED
|
@@ -53,43 +53,95 @@ function insertedFor(pass, match, groups) {
|
|
|
53
53
|
? pass.replace.replace(/\$(\d)/g, (_m, d) => groups[Number(d) - 1] ?? "")
|
|
54
54
|
: pass.replace(match, ...groups);
|
|
55
55
|
}
|
|
56
|
-
function removedRegion(match, inserted) {
|
|
56
|
+
function removedRegion(pass, match, groups, inserted) {
|
|
57
|
+
if (typeof pass.replace === "string") {
|
|
58
|
+
const parts = pass.replace.split(/(\$\d)/).filter((p) => p !== "");
|
|
59
|
+
const isRef = (p) => /^\$\d$/.test(p);
|
|
60
|
+
const refText = (p) => groups[Number(p[1]) - 1] ?? "";
|
|
61
|
+
let head = "";
|
|
62
|
+
for (const p of parts) {
|
|
63
|
+
if (!isRef(p))
|
|
64
|
+
break;
|
|
65
|
+
head += refText(p);
|
|
66
|
+
}
|
|
67
|
+
let tail = "";
|
|
68
|
+
for (let i = parts.length - 1; i >= 0 && isRef(parts[i]); i--)
|
|
69
|
+
tail = refText(parts[i]) + tail;
|
|
70
|
+
if (parts.every(isRef))
|
|
71
|
+
return [0, match.length];
|
|
72
|
+
if (!match.startsWith(head))
|
|
73
|
+
head = "";
|
|
74
|
+
if (!match.endsWith(tail) || head.length + tail.length > match.length)
|
|
75
|
+
tail = "";
|
|
76
|
+
return [head.length, match.length - tail.length];
|
|
77
|
+
}
|
|
78
|
+
const markerAt = pass.marker === "" ? -1 : inserted.lastIndexOf(pass.marker);
|
|
79
|
+
const headStop = markerAt < 0 ? inserted.length : markerAt;
|
|
80
|
+
const tailStop = markerAt < 0 ? inserted.length : inserted.length - markerAt - pass.marker.length;
|
|
57
81
|
let head = 0;
|
|
58
|
-
while (head < match.length && head <
|
|
82
|
+
while (head < match.length && head < headStop && match[head] === inserted[head])
|
|
59
83
|
head++;
|
|
60
84
|
let tail = 0;
|
|
61
|
-
while (tail < match.length - head && tail < inserted.length - head && match[match.length - 1 - tail] === inserted[inserted.length - 1 - tail])
|
|
85
|
+
while (tail < match.length - head && tail < inserted.length - head && tail < tailStop && match[match.length - 1 - tail] === inserted[inserted.length - 1 - tail])
|
|
62
86
|
tail++;
|
|
63
87
|
return [head, match.length - tail];
|
|
64
88
|
}
|
|
89
|
+
const SOLO_SCAN_WORK_PER_CHAR = 8;
|
|
65
90
|
function soloHits(input, passes) {
|
|
66
91
|
const hits = [];
|
|
67
92
|
passes.forEach((pass, i) => {
|
|
68
|
-
|
|
93
|
+
const re = new RegExp(pass.re.source, pass.re.flags.includes("g") ? pass.re.flags : `${pass.re.flags}g`);
|
|
94
|
+
const rescans = pass.family !== undefined;
|
|
95
|
+
let work = SOLO_SCAN_WORK_PER_CHAR * input.length;
|
|
96
|
+
for (let m = re.exec(input); m !== null; m = re.exec(input)) {
|
|
97
|
+
const at = m.index;
|
|
98
|
+
const familyAt = () => (typeof pass.family === "function" ? pass.family(input, at) : pass.family);
|
|
99
|
+
if (rescans) {
|
|
100
|
+
work -= m[0].length;
|
|
101
|
+
if (work < 0) {
|
|
102
|
+
hits.push({ start: at, end: input.length, marker: pass.marker === "" ? REDACTED_WHOLE : pass.marker, pass: i, family: familyAt(), matchStart: at, matchEnd: input.length });
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
re.lastIndex = at + 1;
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
re.lastIndex = at + Math.max(m[0].length, 1);
|
|
69
109
|
const inserted = insertedFor(pass, m[0], m.slice(1));
|
|
70
110
|
if (inserted === m[0])
|
|
71
111
|
continue;
|
|
72
|
-
const [a, b] = removedRegion(m[0], inserted);
|
|
112
|
+
const [a, b] = removedRegion(pass, m[0], m.slice(1), inserted);
|
|
73
113
|
if (b <= a)
|
|
74
114
|
continue;
|
|
75
|
-
const
|
|
76
|
-
const family = typeof pass.family === "function" ? pass.family(input, at) : pass.family;
|
|
115
|
+
const family = familyAt();
|
|
77
116
|
hits.push({ start: at + a, end: at + b, marker: inserted.slice(a, inserted.length - (m[0].length - b)), pass: i, family, matchStart: at, matchEnd: at + m[0].length });
|
|
78
117
|
}
|
|
79
118
|
});
|
|
80
119
|
return hits;
|
|
81
120
|
}
|
|
82
|
-
function
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
reach = r;
|
|
121
|
+
function unremovedHits(hits, removals) {
|
|
122
|
+
const gone = [];
|
|
123
|
+
for (const r of [...removals].filter((x) => x.end > x.start).sort((x, y) => x.start - y.start || y.end - x.end)) {
|
|
124
|
+
const last = gone[gone.length - 1];
|
|
125
|
+
if (last !== undefined && r.start <= last[1])
|
|
126
|
+
last[1] = Math.max(last[1], r.end);
|
|
127
|
+
else
|
|
128
|
+
gone.push([r.start, r.end]);
|
|
91
129
|
}
|
|
92
|
-
return
|
|
130
|
+
return hits.filter((h) => {
|
|
131
|
+
let lo = 0;
|
|
132
|
+
let hi = gone.length - 1;
|
|
133
|
+
let at = -1;
|
|
134
|
+
while (lo <= hi) {
|
|
135
|
+
const mid = (lo + hi) >> 1;
|
|
136
|
+
if (gone[mid][0] <= h.start) {
|
|
137
|
+
at = mid;
|
|
138
|
+
lo = mid + 1;
|
|
139
|
+
}
|
|
140
|
+
else
|
|
141
|
+
hi = mid - 1;
|
|
142
|
+
}
|
|
143
|
+
return at === -1 || gone[at][1] < h.end;
|
|
144
|
+
});
|
|
93
145
|
}
|
|
94
146
|
function sequentialRemovals(batches) {
|
|
95
147
|
const removals = [];
|
|
@@ -143,7 +195,7 @@ function runPasses(input, passes, findings, toOriginal) {
|
|
|
143
195
|
const family = typeof pass.family === "function" ? pass.family(passInput, offset) : pass.family;
|
|
144
196
|
if (findings !== undefined)
|
|
145
197
|
findings.push({ pass: passIndex, f: finding(pass, [toOriginal(toPassZero(offset)), toOriginal(toPassZero(offset + match.length))], family) });
|
|
146
|
-
const [a, b] = removedRegion(match, inserted);
|
|
198
|
+
const [a, b] = removedRegion(pass, match, groups, inserted);
|
|
147
199
|
edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length, inserted, family, head: a, tail: match.length - b, pass: passIndex });
|
|
148
200
|
return inserted;
|
|
149
201
|
});
|
|
@@ -151,12 +203,15 @@ function runPasses(input, passes, findings, toOriginal) {
|
|
|
151
203
|
});
|
|
152
204
|
return { out: cur, batches };
|
|
153
205
|
}
|
|
154
|
-
function applyPasses(input, passes, report, toOriginal) {
|
|
206
|
+
function applyPasses(input, passes, report, toOriginal, disjoint = false) {
|
|
155
207
|
const found = report === undefined ? undefined : [];
|
|
156
208
|
const plain = runPasses(input, passes, found, toOriginal);
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
209
|
+
const solo = soloHits(input, passes);
|
|
210
|
+
const sequential = solo.length === 0 ? [] : sequentialRemovals(plain.batches);
|
|
211
|
+
const outer = unremovedHits(solo, sequential);
|
|
212
|
+
const mappable = !disjoint || replacedSpans(plain.batches) !== undefined;
|
|
213
|
+
const whole = outer.length === 0 && mappable ? undefined : renderRemovals(input, [...sequential, ...outer]);
|
|
214
|
+
if (whole === undefined || (whole.out === plain.out && mappable)) {
|
|
160
215
|
if (report !== undefined)
|
|
161
216
|
report.findings.push(...found.map((x) => x.f));
|
|
162
217
|
return plain;
|
|
@@ -233,10 +288,10 @@ export function runRedactionPasses(input, passes, report) {
|
|
|
233
288
|
const idx = offsetToCharIndex.get(pos);
|
|
234
289
|
return idx === undefined ? input.length : viewToOriginal[idx];
|
|
235
290
|
};
|
|
236
|
-
const {
|
|
291
|
+
const { batches } = applyPasses(view, passes, report, toOriginal, true);
|
|
237
292
|
const viewSpans = replacedSpans(batches);
|
|
238
293
|
if (viewSpans === undefined)
|
|
239
|
-
return
|
|
294
|
+
return REDACTED_WHOLE;
|
|
240
295
|
const spans = viewSpans.map((sp) => {
|
|
241
296
|
const startIdx = offsetToCharIndex.get(sp.start) ?? viewChars.length;
|
|
242
297
|
const endIdx = offsetToCharIndex.get(sp.end) ?? viewChars.length;
|
|
@@ -258,11 +313,11 @@ export function runRedactionPasses(input, passes, report) {
|
|
|
258
313
|
if (cutOffsets.length > MAX_FORMAT_SLOT_SCANS)
|
|
259
314
|
return REDACTED_WHOLE;
|
|
260
315
|
const extra = [];
|
|
261
|
-
const rawSpans = replacedSpans(applyPasses(input, passes, undefined, (pos) => pos).batches);
|
|
316
|
+
const rawSpans = replacedSpans(applyPasses(input, passes, undefined, (pos) => pos, true).batches);
|
|
262
317
|
if (rawSpans !== undefined)
|
|
263
318
|
extra.push(...rawSpans);
|
|
264
319
|
for (const cut of cutOffsets) {
|
|
265
|
-
const cutSpans = replacedSpans(applyPasses(view.slice(cut), passes, undefined, (pos) => pos).batches);
|
|
320
|
+
const cutSpans = replacedSpans(applyPasses(view.slice(cut), passes, undefined, (pos) => pos, true).batches);
|
|
266
321
|
if (cutSpans === undefined)
|
|
267
322
|
continue;
|
|
268
323
|
for (const c of cutSpans) {
|
|
@@ -271,27 +326,36 @@ export function runRedactionPasses(input, passes, report) {
|
|
|
271
326
|
}
|
|
272
327
|
}
|
|
273
328
|
extra.sort((a, b) => a.start - b.start);
|
|
329
|
+
const firstEndingAfter = (pos) => {
|
|
330
|
+
let lo = 0;
|
|
331
|
+
let hi = spans.length;
|
|
332
|
+
while (lo < hi) {
|
|
333
|
+
const mid = (lo + hi) >> 1;
|
|
334
|
+
if (spans[mid].end > pos)
|
|
335
|
+
hi = mid;
|
|
336
|
+
else
|
|
337
|
+
lo = mid + 1;
|
|
338
|
+
}
|
|
339
|
+
return lo;
|
|
340
|
+
};
|
|
274
341
|
for (const r of extra) {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
342
|
+
const at = firstEndingAfter(r.start);
|
|
343
|
+
const hit = spans[at];
|
|
344
|
+
if (hit !== undefined && hit.start < r.end) {
|
|
345
|
+
if (r.start < hit.start)
|
|
346
|
+
hit.start = r.start;
|
|
347
|
+
if (r.end > hit.end)
|
|
348
|
+
hit.end = r.end;
|
|
349
|
+
let next = at + 1;
|
|
350
|
+
while (next < spans.length && spans[next].start < hit.end)
|
|
351
|
+
hit.end = Math.max(hit.end, spans[next++].end);
|
|
352
|
+
if (next > at + 1)
|
|
353
|
+
spans.splice(at + 1, next - at - 1);
|
|
281
354
|
continue;
|
|
282
355
|
}
|
|
283
|
-
spans.
|
|
356
|
+
spans.splice(at, 0, { start: r.start, end: r.end, inserted: r.inserted });
|
|
284
357
|
if (report !== undefined)
|
|
285
358
|
report.findings.push(finding(passes[r.pass], [r.start, r.end], r.family));
|
|
286
|
-
spans.sort((a, b) => a.start - b.start);
|
|
287
|
-
}
|
|
288
|
-
for (let i = 1; i < spans.length;) {
|
|
289
|
-
if (spans[i].start < spans[i - 1].end) {
|
|
290
|
-
spans[i - 1].end = Math.max(spans[i - 1].end, spans[i].end);
|
|
291
|
-
spans.splice(i, 1);
|
|
292
|
-
}
|
|
293
|
-
else
|
|
294
|
-
i++;
|
|
295
359
|
}
|
|
296
360
|
let result = "";
|
|
297
361
|
let cursor = 0;
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
export const URI_SCHEME_SOURCE = String.raw `[A-Za-z][A-Za-z0-9+.\-_]{0,63}`;
|
|
2
2
|
const VENDOR_BODY = "[A-Za-z0-9_-]{8,}";
|
|
3
|
+
const VALUE_END = String.raw `\s"';|&`;
|
|
4
|
+
const VALUE_CHAR = `[^${VALUE_END}]`;
|
|
5
|
+
const ESCAPE_DEPTH = 63;
|
|
6
|
+
const QUOTED_VALUE_MAX = 1024;
|
|
7
|
+
const PLAIN_QUOTES = [`"`, `'`, `"`, `'`, `"`, `'`, `%22`, `%27`];
|
|
8
|
+
const ESCAPED_QUOTES = [`"`, `'`, `x22`, `x27`, `u0022`, `u0027`, `042`, `047`];
|
|
9
|
+
const BACKSLASHES = String.raw `\\{1,${ESCAPE_DEPTH}}`;
|
|
10
|
+
const QUOTE = `(?:${PLAIN_QUOTES.join("|")}|${BACKSLASHES}(?:${ESCAPED_QUOTES.join("|")}))`;
|
|
11
|
+
const AFTER_CLOSING_QUOTE = String.raw `(?:[\s,}\]:]|["']|$)`;
|
|
12
|
+
const GLUED_RUN = `${VALUE_CHAR}*(?:(?<!\\\\)|(?!\\\\{0,${ESCAPE_DEPTH}}["']))`;
|
|
13
|
+
const pairedValue = (open, close) => `${open}(?!${AFTER_CLOSING_QUOTE})[^\\n]{0,${QUOTED_VALUE_MAX}}?${close}`;
|
|
14
|
+
const QUOTED_VALUE = [
|
|
15
|
+
...PLAIN_QUOTES.map((q) => pairedValue(q, q)),
|
|
16
|
+
...ESCAPED_QUOTES.map((q, i) => pairedValue(`(${BACKSLASHES})${q}`, `(?<!\\\\)(?:\\${i + 2})${q}`)),
|
|
17
|
+
`${QUOTE}(?!${AFTER_CLOSING_QUOTE})${GLUED_RUN}`,
|
|
18
|
+
].join("|");
|
|
19
|
+
const BEARER_TAIL = `(?<==)(?:(?!${QUOTE})${GLUED_RUN}|${QUOTED_VALUE})`;
|
|
3
20
|
export const CREDENTIAL_VOCABULARY = {
|
|
4
21
|
values: [
|
|
5
22
|
{ id: "github-token", kind: "prefixed-token", source: "arg-summary", confidence: "high", pass: "vendor-prefixed", match: new RegExp(`(?<![A-Za-z0-9])gh[opsur][-_]${VENDOR_BODY}`, "g"), strict: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/ },
|
|
@@ -14,9 +31,9 @@ export const CREDENTIAL_VOCABULARY = {
|
|
|
14
31
|
{ id: "huggingface-token", kind: "prefixed-token", source: "arg-summary", confidence: "high", match: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{20,}/g, strict: /\bhf_(?=[A-Za-z0-9]*[0-9])[A-Za-z0-9]{20,}\b/ },
|
|
15
32
|
{ id: "jwt", kind: "jwt", source: "arg-summary", confidence: "high", match: /(?<![A-Za-z0-9])eyJ[A-Za-z0-9._-]{20,}/g, strict: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{16,}\b/ },
|
|
16
33
|
{ id: "private-key", kind: "private-key-block", source: "arg-summary", confidence: "high", match: /-----BEGIN[ A-Z0-9]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END[ A-Z0-9]*PRIVATE KEY(?: BLOCK)?-----|$)/g, strict: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/ },
|
|
17
|
-
{ id: "bearer-token", kind: "bearer-token", source: "arg-summary", confidence: "high", match:
|
|
34
|
+
{ id: "bearer-token", kind: "bearer-token", source: "arg-summary", confidence: "high", match: new RegExp(String.raw `(?<![A-Za-z0-9])()[Bb][Ee][Aa][Rr][Ee][Rr]\s+[A-Za-z0-9._~+/-]{8,}=*(?:${BEARER_TAIL})?`, "g"), strict: /\bAuthorization:\s*Bearer\s+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{20,}=*/ },
|
|
18
35
|
{ id: "azure-storage-account-key", kind: "prefixed-token", source: "arg-summary", confidence: "high", match: /(AccountKey=)[A-Za-z0-9+/]{40,}={0,2}/g, strict: /AccountKey=[A-Za-z0-9+/]{40,}={0,2}/ },
|
|
19
|
-
{ id: "keyword-value", kind: "keyword-value", source: "arg-summary", confidence: "medium", match:
|
|
36
|
+
{ id: "keyword-value", kind: "keyword-value", source: "arg-summary", confidence: "medium", match: new RegExp(String.raw `(?<![A-Za-z0-9])((?:token|secret|key|password|passwd|credential|authorization)["']?\s*[=:]\s*["']?)${VALUE_CHAR}{4,}`, "gi") },
|
|
20
37
|
{ id: "url-userinfo", kind: "url-credentials", source: "untrusted-egress", confidence: "medium", match: new RegExp(String.raw `(${URI_SCHEME_SOURCE}:\/\/)((?:[^/\s"'@]*@)+)`, "gi") },
|
|
21
38
|
{ id: "signed-url-param", kind: "signed-url-param", source: "untrusted-egress", confidence: "high", match: /([?&#](?:sig|signature|sas|code)=)[^&\s"'#]+/gi },
|
|
22
39
|
],
|
|
@@ -309,13 +309,20 @@ export interface EngineNotice {
|
|
|
309
309
|
* the run and announced on the first. Absent ⇒ every request of the run already had the structure.
|
|
310
310
|
* - `"task.tool_turn_incomplete"` (#1230) — the person's line for a tool turn the model was handed incomplete, beside
|
|
311
311
|
* the note the model itself reads. `detail.cause` (closed `TOOL_TURN_INCOMPLETE_CAUSES`): `truncated` — calls whose
|
|
312
|
-
* arguments were cut before they parsed, dropped (they did not run;
|
|
313
|
-
*
|
|
312
|
+
* arguments were cut before they parsed, dropped (they did not run); `unnamed` — calls that arrived with no tool
|
|
313
|
+
* name, dropped the same way; `bad_frame` — streamed response frames
|
|
314
314
|
* that could not be read, dropped (the response may be missing content); `missing_result_synthesized` — a
|
|
315
315
|
* call of this run with no recorded result, which the request view closed as interrupted, outcome unknown (the
|
|
316
316
|
* `missing_result` repair above; a call that went unanswered before the run began is the transcript's history and
|
|
317
|
-
* is on that operator line only). Audience `"user"`; `detail: { cause, count, toolCallIds?, sessionId, runId }` —
|
|
318
|
-
* `count` the calls (or frames) of that cause in the turn or request that first showed it; `
|
|
317
|
+
* is on that operator line only). Audience `"user"`; `detail: { cause, count, outcome?, toolCallIds?, sessionId, runId }` —
|
|
318
|
+
* `count` the calls (or frames) of that cause in the turn or request that first showed it; `outcome` (#1640) on
|
|
319
|
+
* `truncated` / `unnamed` only, and only where the turn's own message states it: `"terminal"` — the lane failed
|
|
320
|
+
* the run on those calls (its dropped-calls account is on the failed turn; the terminal's `detail.kind` is
|
|
321
|
+
* `model_output_invalid`), the sentence says the run ended; `"reissued"` — the turn goes on by the loop's turn
|
|
322
|
+
* rules (it carries a tool call, or stopped to call a tool, or stopped at the output cap) and the next request
|
|
323
|
+
* asks the model to re-issue them. ABSENT is a fact, not missing data: the message does not say what became of the
|
|
324
|
+
* run (a refusal, an abort, a completed turn with nothing to run, a torn stream the loop may resume), and the
|
|
325
|
+
* sentence then stops at "so they did not run". Once for the cause that first showed it; `toolCallIds` on
|
|
319
326
|
* `missing_result_synthesized` only, each id in the provider echo shape (a stored id outside it is not shown;
|
|
320
327
|
* `count` still counts it). Closed-set words, a count and call ids only: never a tool name, an argument or the
|
|
321
328
|
* note's text. ONCE PER RUN PER CAUSE. Absent ⇒ nothing of that cause reached the model.
|
package/dist/core/gate-fold.js
CHANGED
|
@@ -194,7 +194,7 @@ function applyShellReadVerdict(pass, verdict, toolName, shellGated) {
|
|
|
194
194
|
pass.targetUnresolved = true;
|
|
195
195
|
if (verdict.unplacedOptions !== undefined)
|
|
196
196
|
pass.shellUnplacedOptions = verdict.unplacedOptions;
|
|
197
|
-
if (verdict.finding === "unestablishable" || verdict.finding === "declined" || verdict.unestablished === true)
|
|
197
|
+
if (verdict.finding === "unestablishable" || verdict.finding === "declined" || verdict.finding === "unanswered" || verdict.unestablished === true)
|
|
198
198
|
pass.shellReadUnestablished = true;
|
|
199
199
|
switch (verdict.disposition) {
|
|
200
200
|
case "vouch":
|
|
@@ -244,7 +244,7 @@ function applyShellReadVerdict(pass, verdict, toolName, shellGated) {
|
|
|
244
244
|
pass.shellReadOffer = true;
|
|
245
245
|
pass.tightenedBy = shellGated ? "shell_gate" : "safety";
|
|
246
246
|
}
|
|
247
|
-
if (verdict.finding === "declined")
|
|
247
|
+
if (verdict.finding === "declined" || verdict.finding === "unestablishable")
|
|
248
248
|
pass.probeBoundaryDeclined = true;
|
|
249
249
|
return;
|
|
250
250
|
}
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -856,6 +856,23 @@ export interface HookToolOutput {
|
|
|
856
856
|
* formerly APPEARED on the promoted ask object as an undeclared property, because that fold spread the
|
|
857
857
|
* whole hook result; the concurrent arm never carried it, so the two arms disagreed on a member neither
|
|
858
858
|
* of them reads. Nothing in the engine reads it off a decision.
|
|
859
|
+
*
|
|
860
|
+
* PreToolUse lane × settlement table (the gate's phase 1 in {@link runToolGate}; the delegated twin in
|
|
861
|
+
* {@link createPreToolUseConstraintPolicy} speaks the same words as a policy layer). "Held" = the ask survives to a person:
|
|
862
|
+
* neither an allow rule (`decisionReason: "hook"` is outside the allow layer) nor the classifier (hook asks are
|
|
863
|
+
* never classifier-answerable) clears it.
|
|
864
|
+
* · returns `allow` (± `updatedInput` / `additionalContext`) → the face's own word (`hookAllowed`); the policy
|
|
865
|
+
* still adjudicates the FINAL args; the allow layer's hook member may clear an unmandated ask. Not held.
|
|
866
|
+
* · returns `ask` (± `updatedInput`, ± `requiresRealApproval`) → folded as `hookAsk`: a policy deny still
|
|
867
|
+
* outranks it, a policy allow is PROMOTED to this ask with `"hook"` stamped by the engine; the rewrite rides
|
|
868
|
+
* into execution and the real-approval bit crosses delegation. Held.
|
|
869
|
+
* · returns `deny` → {@link hookDenyExit}: the hook's OWN refusal — the deny observer does NOT fire, the model
|
|
870
|
+
* reads the hook's sentence. Nothing executes.
|
|
871
|
+
* · returns `undefined` ("no opinion") or no screening face is installed → no word at all; the policy decides.
|
|
872
|
+
* · throws / times out / is aborted / returns an out-of-contract object (refused by {@link receiveHookResult}) →
|
|
873
|
+
* {@link engineFailClosedExit}: an ENGINE deny — the deny observer fires, a crash and a timeout also reach
|
|
874
|
+
* `onHookError`→`onError(phase:"hook")` (an abort does not), the model reads the engine's envelope. Nothing
|
|
875
|
+
* executes, nothing is held.
|
|
859
876
|
*/
|
|
860
877
|
export type PreToolUseResult = PermissionResult & {
|
|
861
878
|
additionalContext?: string;
|
|
@@ -10,7 +10,7 @@ import { askUnresolvableNotice } from "../ask-unresolvable-notice.js";
|
|
|
10
10
|
import { askReachesPerson } from "../wiring-manifest.js";
|
|
11
11
|
import { classifierUnavailableSeat } from "./classifier-ladder.js";
|
|
12
12
|
import { PATH_CONFINABLE_WRITE_TOOLS } from "./session-rule-policy.js";
|
|
13
|
-
import { callFaceSeat, rosterToolName, toolCallFaceOf } from "../tool-roster.js";
|
|
13
|
+
import { callFaceSeat, rosterToolName, survivorsByName, toolCallFaceOf } from "../tool-roster.js";
|
|
14
14
|
import { heldBy, heldValues, seatOf, wireOf } from "../tool-name-readings.js";
|
|
15
15
|
import { createApprovedCallScreen, createToolResultStation } from "./call-hook-stations.js";
|
|
16
16
|
const PEER_MESSAGE_NAMES = new Set([SEND_MESSAGE_TOOL_NAME]);
|
|
@@ -73,7 +73,7 @@ export function prepareGateStations(input) {
|
|
|
73
73
|
if (askLane !== undefined && parkAsk !== undefined) {
|
|
74
74
|
const { adjudicate, resolveAskBound, notifyPermissionDenied, notifyHookError } = askLane;
|
|
75
75
|
const { resolveContentAsk, suspendAsk } = parkAsk;
|
|
76
|
-
for (const t of tools) {
|
|
76
|
+
for (const t of survivorsByName(tools).values()) {
|
|
77
77
|
const eff = t.effect;
|
|
78
78
|
if (eff !== undefined && !toolEffects.has(t.name))
|
|
79
79
|
toolEffects.set(t.name, eff);
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* LSP tool mount over the per-task env with its git-ignore filter, the diagnostics lane behind its four
|
|
4
4
|
* preconditions (a manager whose sessions feed a registry, write-capable hands, not opted out), the run-scoped
|
|
5
5
|
* registry key, and the edit nudge (raw path → the SAME resolution the tool leg uses → registry reset +
|
|
6
|
-
* session didChange). No manager
|
|
7
|
-
* dependency list the segment had implicitly.
|
|
6
|
+
* session didChange). No manager, or no hands (the manifest still reports the seat, `mounted: false`), ⇒ nothing mounts and
|
|
7
|
+
* both lane seats are undefined. The interface is the dependency list the segment had implicitly.
|
|
8
8
|
*
|
|
9
9
|
* SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await, so the phase adds no yield between the
|
|
10
10
|
* fork-governance note before it and the memory trio's occupancy read after it (a stretch
|
|
@@ -28,7 +28,7 @@ export interface PrepareLspInput {
|
|
|
28
28
|
/** borrowed-readonly — the SETTLED task root: the tool's git-ignore root, and the nudge's resolution base when
|
|
29
29
|
* no cwd is tracked. */
|
|
30
30
|
taskRootFinal: string;
|
|
31
|
-
/** borrowed-readonly — whether a real fs env is present (the lane's hands precondition). */
|
|
31
|
+
/** borrowed-readonly — whether a real fs env is present (the tool mount's and the lane's hands precondition). */
|
|
32
32
|
handsEnabled: boolean;
|
|
33
33
|
/** borrowed-readonly — the acquired session id: the run's key into the deployment-shared registry. */
|
|
34
34
|
sessionId: string;
|
|
@@ -36,7 +36,7 @@ export interface PrepareLspInput {
|
|
|
36
36
|
* resolution base (identity is the contract — the band's `cd` handling writes this exact cell). */
|
|
37
37
|
handsCwdRef: Prepared["cwdRef"];
|
|
38
38
|
/** borrowed-mutable — the run's roster builder (identity is the contract). Writer here: ONE mount (the LSP tool)
|
|
39
|
-
* when a manager exists. */
|
|
39
|
+
* when a manager exists and the leg has hands. */
|
|
40
40
|
roster: RosterBuilder;
|
|
41
41
|
/** borrowed-mutable — the env-hand membership set the read-posture observer projects; writer here: `add("LSP")`
|
|
42
42
|
* on the mount. */
|
|
@@ -4,9 +4,9 @@ import { RosterBuilder } from "../tool-roster.js";
|
|
|
4
4
|
export function prepareLsp(input) {
|
|
5
5
|
const { spec, deps, executionEnv, taskRootFinal, handsEnabled, sessionId, handsCwdRef, roster, envHandToolNames } = input;
|
|
6
6
|
const lspManager = spec.lspManager ?? deps.lspManager;
|
|
7
|
-
if (lspManager) {
|
|
7
|
+
if (lspManager && handsEnabled) {
|
|
8
8
|
const lspRoot = taskRootFinal;
|
|
9
|
-
roster.mount(createLspTool(lspManager, { isPathIgnored: gitCheckIgnoreFilter(executionEnv, lspRoot), env: executionEnv }), { source: "builtin", mountedBy: "
|
|
9
|
+
roster.mount(createLspTool(lspManager, { isPathIgnored: gitCheckIgnoreFilter(executionEnv, lspRoot), env: executionEnv }), { source: "builtin", mountedBy: "hands" });
|
|
10
10
|
envHandToolNames.add("LSP");
|
|
11
11
|
}
|
|
12
12
|
const lspDiagnostics = spec.lspDiagnostics !== false && lspManager?.diagnostics !== undefined && handsEnabled && spec.handsReadOnly !== true
|