@lannguyensi/harness 0.42.0 → 0.44.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 +62 -0
- package/dist/cli/approve/branch-protection.js +1 -1
- package/dist/cli/approve/branch-protection.js.map +1 -1
- package/dist/cli/dry-run.js +19 -1
- package/dist/cli/dry-run.js.map +1 -1
- package/dist/cli/index.js +4 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/composer.js +6 -6
- package/dist/cli/init/composer.js.map +1 -1
- package/dist/cli/init/profiles.d.ts +1 -1
- package/dist/cli/init/profiles.js +1 -1
- package/dist/cli/init/templates.d.ts +1 -1
- package/dist/cli/init/templates.js +53 -31
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/pack/hook-branch-protection.js +1 -1
- package/dist/cli/pack/hook-branch-protection.js.map +1 -1
- package/dist/cli/pack/hook-post-merge-gate.js +43 -25
- package/dist/cli/pack/hook-post-merge-gate.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js +1 -1
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance.js +1 -1
- package/dist/cli/pack/hook-solution-acceptance.js.map +1 -1
- package/dist/cli/policy/intercept.js +209 -14
- package/dist/cli/policy/intercept.js.map +1 -1
- package/dist/cli/validate/checks.js +2 -2
- package/dist/cli/validate/checks.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +1 -1
- package/dist/policy-packs/builtin/branch-protection-runtime.js +1 -1
- package/dist/policy-packs/builtin/branch-protection.js +2 -2
- package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
- package/dist/policy-packs/builtin/post-merge-gate-runtime.d.ts +90 -10
- package/dist/policy-packs/builtin/post-merge-gate-runtime.js +112 -16
- package/dist/policy-packs/builtin/post-merge-gate-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/post-merge-gate.js +40 -23
- package/dist/policy-packs/builtin/post-merge-gate.js.map +1 -1
- package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +13 -0
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js +15 -2
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/solution-acceptance.js +1 -1
- package/dist/runtime/bash-match-facts.d.ts +58 -0
- package/dist/runtime/bash-match-facts.js +194 -0
- package/dist/runtime/bash-match-facts.js.map +1 -0
- package/dist/runtime/bash-match-registry.d.ts +75 -0
- package/dist/runtime/bash-match-registry.js +359 -0
- package/dist/runtime/bash-match-registry.js.map +1 -0
- package/dist/runtime/bash-prefix-parse.js +23 -0
- package/dist/runtime/bash-prefix-parse.js.map +1 -1
- package/dist/runtime/command-normalize.d.ts +405 -0
- package/dist/runtime/command-normalize.js +1888 -0
- package/dist/runtime/command-normalize.js.map +1 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/intercept.d.ts +208 -2
- package/dist/runtime/intercept.js +437 -28
- package/dist/runtime/intercept.js.map +1 -1
- package/dist/runtime/read-only-bash.d.ts +43 -0
- package/dist/runtime/read-only-bash.js +73 -10
- package/dist/runtime/read-only-bash.js.map +1 -1
- package/dist/runtime/shell-word.d.ts +21 -0
- package/dist/runtime/shell-word.js +242 -0
- package/dist/runtime/shell-word.js.map +1 -0
- package/dist/schema/policies.js +1 -1
- package/dist/schema/policies.js.map +1 -1
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.44.0] - 2026-08-03
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
|
|
14
|
+
- **SECURITY: `${REPO}`/`${BRANCH}`/`at_head` now resolve PER POLICY to the repository a gated command actually touches, closing the cross-repo defect where a preflight fact recorded for repository A authorised an investigative git read (or, chained, a mutation) of a DIFFERENT repository B** (task `98ad072f`). Measured and closed: `preflight-before-investigation`'s cwd-only resolution (unchanged since 0.42.0, explicitly named as future work — follow-up `98ad072f` — in the 0.43.0 entry below) let `git -C <B> status` from cwd A be authorised by A's own `preflight:${REPO}` fact even though the read genuinely runs against B; `preflight-before-push` had the mirror gap for `${BRANCH}`/`at_head`. `src/runtime/command-normalize.ts` gains a per-segment view (`segmentViewOf`/`CommandSegment`: own explicit target + composed effective target); `src/runtime/intercept.ts`'s `resolveAttributedContexts` uses it to evaluate a `${REPO}`/`${BRANCH}`-bearing policy once per DISTINCT repository context a trigger-satisfying segment names, not once per event.
|
|
15
|
+
|
|
16
|
+
**ACCEPTED SEMANTIC CHANGE (operator decision D-021, after a four-pass halt — see below):** attribution is UNIVERSAL-ADDITIVE, never a replacement. The cwd context is demanded UNCONDITIONALLY on every event a per-repo-builtins policy matches; each distinct attributed target (a segment's own `-C`/`env -C`/`--git-dir`, or a target inherited from a preceding `cd`) ADDS its own demand alongside cwd's, never in place of it. Concretely: **`git -C <B> push` from cwd A now demands BOTH A's and B's preflight evidence — holding only B's tag no longer authorises reading or mutating B from a different cwd.** This is more restrictive than the shipped, cwd-only engine (never weaker, by construction — see the matrix below), but it is a real behaviour change an operator upgrading should know about: a workflow that legitimately does `git -C <B> ...` from a different repository's checkout now needs a preflight recorded in EACH repository it touches, not just its own cwd.
|
|
17
|
+
|
|
18
|
+
**Why additive, not "attribute to the segment's own target and stop" (the design this task shipped and then reverted four times):** an own-target REPLACE design — trust a segment's own `-C`/`--git-dir` as proof the invocation runs there, and evaluate ONLY that context — was built, reviewed, and found to reopen the exact class of bug this task exists to close, four separate times across four independent adversarial review passes, each measured against the real shipped binary and (where relevant) real git: (1) an inherited `cd` basis carried into a segment bash does not actually run in (a subshell close, a bare pipe stage, `cd -`, `pushd`) let a forged `.git/HEAD` directory redirect the tag; (2) `--work-tree=<B>` was folded into the same own-target bucket as `-C`/`--git-dir` though it does not relocate the git-dir, so `git --work-tree=<B> push` still runs in the CWD repo but was attributed to B; (3) more than one repo-relocating option (`git -C <forge> -C <realA> push`) was attributed to the FIRST one seen while real git composes cumulatively and runs in the LAST; (4) a tilde-valued second `-C` (`git -C <decoy> -C ~/sub push`) was invisible to (3)'s own ambiguity counter, so the ambiguity lock never engaged. Each was fixed individually and confirmed sound by every later pass; the RECURRENCE — four distinct instances of one class — is what this repo's structural-regression-halt convention exists to catch, and the fourth instance triggered exactly that halt (nothing merged at that point; the operator was handed the full four-pass history rather than a fifth "surely now complete" patch). The chosen fix makes the property true BY CONSTRUCTION instead of by enumerating git's composition knobs: since the cwd demand is never dropped, no gap in the static, string-only shell/git model — known or not yet found — can make a gate weaker than the shipped cwd-only engine. `03-decisions.md` D-011/D-017/D-018/D-019/D-020/D-021 in `.ai/runs/2026-08-02-per-repo-gate-scoping-redesign/` carries the full per-pass history.
|
|
19
|
+
|
|
20
|
+
**Measured, not asserted, that this holds:** `scripts/measure-additive-attribution-matrix.mjs` (`npm run measure:additive-attribution-matrix -- --control <dir containing an installed 0.43.0 build>`) runs a differential matrix of 6 separators (`&&`, `;`, newline, `|`, `||`, subshell) x 8 command forms (spanning every one of the four historical bypass classes above, plus a legitimate single-`-C` case and two no-target baselines) = 48 cells, each with a ledger holding ONLY a forged/decoy repository's evidence (the cwd's own is always absent), run against BOTH this engine and the real, installed 0.43.0 binary as the shipped oracle. Result: **0/48 cells weaker than 0.43.0** — every cell's cwd-tagged decision is identical to what 0.43.0 itself computes for that same command, and 21/48 cells additionally exercise the additive branch (a genuine second, foreign context demanded alongside cwd's). The instrument's own self-test sabotages those 21 cells to pre-fix REPLACE semantics post-hoc and confirms the audit catches all 21/21 — proof the "0 weaker" result is a real measurement, not a referee that cannot fail.
|
|
21
|
+
|
|
22
|
+
**The bound (D-013):** distinct attributed contexts for one policy on one event are capped at 4 (`MAX_ATTRIBUTED_CONTEXTS`); a command naming more fails CLOSED — one synthetic decision naming the ambiguity, ZERO ledger queries for that policy — respecting the policy's own `enforcement:` (a `block` policy denies, a `warn` policy warns, never a hardcoded outcome), rather than silently evaluating all of them (the measured amplification this bound closes: K distinct targets against N per-repo policies previously cost N×K ledger queries and audit writes per event, unbounded).
|
|
23
|
+
|
|
24
|
+
**What this does NOT close, verbatim, so no doc or commit overclaims it:**
|
|
25
|
+
- A same-invocation composition this module cannot resolve to a single, unambiguous target (more than one repo-relocating option, or `--work-tree` alone) still falls back to cwd-only — precisely the shipped 0.43.0 behaviour for that shape, not a new gap. D-017/D-018 stay in place as PRECISION (avoiding a spurious extra demand), no longer as the security mechanism — D-021's additive backstop is what carries safety now.
|
|
26
|
+
- The tilde-valued relocating-option residual named above (bypass 4) is NOT fixed at the counting layer — a tilde-valued second `-C` still attributes an own target the way it did pre-fix. Under D-021 this is now SAFE (the cwd demand is added, never dropped) but SPURIOUS: the command also picks up an extra, over-restrictive demand it should arguably not owe. Fail-closed, spurious-but-safe over-demand, not a bypass — a precision follow-up, not a blocker.
|
|
27
|
+
- `${BRANCH}` is still a bare branch name with no repository qualifier, so a same-named branch in two different repositories can still satisfy each other's tag — unchanged, independent of attribution.
|
|
28
|
+
- The static-analysis ceiling every prior entry in this file already names is unchanged and applies here too: a heredoc body, `sh -c '...'`, `eval`, or a base64-decoded payload that constructs a targeted git invocation at runtime is invisible to this (or any) string-level normaliser; such a command still evaluates against cwd only, same as 0.43.0.
|
|
29
|
+
- The `resolveGitContext` primitive this whole mechanism (and 0.42.0's cwd-only resolution before it) rests on is a filesystem-shape check, not a git-validated one: any directory containing a `.git/HEAD` file is a taggable, agent-forgeable repository identity. Additive attribution neutralises the SPECIFIC exploit path this task closes (an agent redirecting which tag is demanded away from the repo a verb truly targets); it does not change this underlying primitive.
|
|
30
|
+
|
|
31
|
+
New tests: `tests/runtime/intercept-cli.test.ts` gains the T-001 regression pins (the three 07-27 defects, mandatory), the T-002/T-003 per-segment/per-policy attribution suite, the D-011..D-021 hardening suite (inherited-`cd` additivity, `--work-tree` exclusion, multiple-`-C` ambiguity, the tilde residual, the context bound, dedup, operator-override-in-an-attributed-context, symlink realpath identity), and — this documentation round — a `findGitEntry` upward-walk collapse pin (a relative `-C <subdir-of-cwd-repo>` with no `.git` of its own is ONE cwd decision, not two) and a bound-under-`warn` pin (more than 4 distinct targets on a `warn`-enforcement policy resolves to `warn`, never `deny`, respecting `outcomeForFailedRequires`). `resolverGit`/`riskContext` (the Risk Gate path) is byte-identical across every commit on this task — the 07-27 Risk Gate declassification regression cannot recur here. Docs: `docs/writing-custom-policies.md` (a new section on the per-policy `${REPO}`/`${BRANCH}`/`at_head` resolution and its fallback list), `docs/okf/gate-fail-posture-matrix.md` (the additive fallback posture).
|
|
32
|
+
|
|
33
|
+
- **SECURITY: post-merge-gate blocker precedence reversed — deny now wins over the escape allowlist, closing the chained-escape bypass where `harness preflight && git push origin master` (the documented preflight-then-push workflow) skipped the gate entirely** (task `19356be7`). `isEscapeCommand` was an OR over the WHOLE command string and `hook-post-merge-gate.ts` checked it first and unconditionally, so any chained command containing an escape verb anywhere (`git switch`, `git stash list`, any `harness ...`) fell out of the gate including its mutation half. The blocker now classifies via the new `isGateEligibleCommand` (post-merge-gate-runtime.ts): gate-eligible iff the curated mutation matcher fires after quoted-heredoc bodies attached to a harness invocation are stripped. **Measured before choosing (66-row corpus, bash ground truth via PATH stubs; `.ai` run `2026-08-02-post-merge-gate-escape-precedence`):** per-segment evaluation and deny-wins are 0-divergent — a consequence of the pinned escape/deny verb disjointness — so the simplest (deny-wins) shipped. The third candidate, escape-at-start-only, was measured and REJECTED: it diverges on 26 rows and still exempts three of the four lines this task exists to close. Against the shipped classifier: **0 false-frees, 0 recovery-chain lockouts** (`git switch <default> && git pull --ff-only` stays free structurally, allowed before any manifest/ledger access), **0 commands newly freed**, and 28 newly gate-eligible = 22 bash-confirmed real mutations + 6 over-blocks. **The 6 over-blocks are the honest cost, listed rather than summarised:** four are the existing quoted-text class (`git pull && echo 'a && git push'` and friends, no longer rescued by a chained escape verb), and two are harness report heredocs whose BODY mentions a mutation verb (`harness approve understanding <<'…'`). Gate-eligible is not blocked: a deny still needs the branch tip to equal a recorded merged tip, and the gate's own recommended recovery stays free.
|
|
34
|
+
|
|
35
|
+
**Explicitly NOT done, after two review rounds:** an earlier iteration pre-processed the command to strip quoted-heredoc bodies fed to a harness invocation, so the two over-blocks above would not occur. Round 1 found six shapes where that stripping consumed lines bash really executes; the quote-aware scanner written to fix them was found by round 2 to have twelve more of the same class. Both families share one cause — deciding where a heredoc body begins means re-deriving bash's quote and word grammar on the permissive side of a security boundary — and tasks `dbc6d303` and `5b1b24fb` had already been halted for it. The mechanism was therefore removed rather than patched a third time, and the two over-blocks are accepted and pinned. Task `5b1b24fb` owns the real fix, at the `bash_match` layer for every surface at once. Escape regexes stay exported as deny-message vocabulary and diagnostics. Pack instructions, `docs/policy-packs/post-merge-gate.md`, the blocker hook's `description`, `harness pack hook post-merge-gate --help`, the generated-manifest comment in `harness init --template full`, and `docs/examples/full-manifest.yaml` all updated to stop teaching escape-first, with drift pins on each of those operator-visible surfaces.
|
|
36
|
+
|
|
37
|
+
- **SECURITY: the ampersand-`&`-boundary gap `BOUNDARY_RE` deliberately left open (0.43.0's `d834a065` entry above) is now closed by a SECOND, independent normalisation pass instead of widening the first, which was measured and reverted for a 140/140 regression** (task `aabbad63`). `src/runtime/command-normalize.ts` gains `AMP_BOUNDARY_RE` (`BOUNDARY_RE` plus a bare `&`) driving `normalizeCommandAmpAware`, consulted by `policyMatchesEvent` (`src/runtime/intercept.ts`) as a THIRD, additional arm — tried only when a policy's regex has already missed both the raw command and the primary (`BOUNDARY_RE`) normalised form, so the fix is strictly additive on top of the two existing arms and never a replacement for either. This closes the family `BOUNDARY_RE`'s own comment named as out of that task's budget: `A=x&env -C /tmp git status` (glued, no space) and `echo hi & nice git status` (a genuine bash background job). **Measured, corrected numbers (review round 1 found the shipping build's own closure claim wrong):** over 7 wrappers (`env`/`nice`/`sudo`/`command`/`setsid`/`stdbuf`/`nohup`) × 4 gated verbs × 2 shapes (`A=x&<wrapper> <verb>`, `echo hi & <wrapper> <verb>`) = 56 realistic spellings, 28/56 were ALREADY gated before this change (raw or the primary normalised arm), 52/56 gate after — a delta of 24 attributable to this arm alone — and 4 remain ungated, all `echo hi & nohup <gated verb>` (`nohup` is not one of `canonicalizeSegment`'s recognised wrapper names, a pre-existing, already-documented module-header gap — not something either boundary alphabet can reach, pinned in `tests/runtime/command-normalize.test.ts`). The pre-change 28/56 is high because the shipped trigger regexes' own `(\w+=\S+\s+)*` leading-assignment group lets `\S+` swallow a glued `&<wrapper>` (`A=x&nice git push` already matched the RAW regex before this task ran at all), so the GLUED family was never actually what needed closing — the background-job family (`echo hi & <wrapper> <verb>`) is. `AMP_BOUNDARY_RE`'s alternation order (`&&` left of bare `&`) is load-bearing for JS's leftmost-first alternation and is pinned by execing a clone of the exported regex directly (fix round 1, finding F9 — the original pin exec'd the shared, exported `/g` object itself, leaving `lastIndex` mutated for whatever ran next), since the ordering is invisible in this module's own segment-and-rejoin output string — verified by swapping the order and diffing output, byte-identical either way. **New false-positive class, widening an already-accepted one, not introducing a new one:** on a 160-form quoted-text corpus arbitrated by real bash (PATH-shimmed verbs + a marker file), 144 spellings of `<text> & <wrapper> <gated verb>` INSIDE a quoted string now match where the base build did not, and bash ran the verb in 0 of them — but the wrapper-LESS variant of the same shape (`echo 'a & git push'`) already false-alarmed on the base build 16/16 via the RAW regex alone, since every shipped trigger already anchors on a bare `&` (task `d834a065`); this widens that already-accepted over-block class to the wrapped spellings rather than introducing a new one. The opposite direction, on the general (non-adversarial) corpus: of 276 matches this pass gains there, all 151 classifiable ones were TRUE positives, 0 false alarms. Engine-side fix: applies on package upgrade, no manifest change required. **Fix round 1 also closed three inert/misleading test and comment surfaces found by review, none of them behavioural:** (1) the CLI-side memoising thunk (`ampNormalizedCommandThunk`, `src/cli/policy/intercept.ts`) and its production wiring into `intercept()` had zero test coverage that actually drove an event through `runInterceptCli` — the existing "memoising thunk" test built its own closure and proved only that a memoising closure memoises; a new `runInterceptCli`-level test (counting real `normalizeCommandAmpAware` calls via a call-through mock) now goes red under either of two measured mutations (replacing the `??=` memoisation with a plain call; deleting the thunk's spread into `intercept()` entirely) that previously left the whole suite green. (2) That same once-per-event discipline never covered a Codex shell event (`tool_name: "shell"` / `"exec_command"` / `"functions.exec_command"`) whose command lived under `raw_input`/`input` or a `cmd` key: the CLI wrapper built the precompute and the thunk from `event.tool_name === "Bash"` + `tool_input.command` only, while the matcher itself (`extractShellCommand`) reads all three input locations and either key — so such an event ran BOTH normalisation passes once PER MATCHING POLICY instead of once per event. Now derived from `extractShellCommand(event)` directly (guarded by the existing `isCodexShellTool`), which also makes the precomputed value and the matcher's own value provably consistent for every event in `SHELL_ALIASES`. (3) `findNextBoundary`'s `lastIndex` reset was documented as preventing the two passes from clobbering each other's scan position, which measured false (0 output differences across the corpus with the reset removed) — they are two distinct regex objects with nothing to clobber; reworded to the actual reason (throw-safety for a hypothetical caught-and-resumed scan) and marked unreachable under today's control flow, since nothing in this module catches mid-scan and resumes. New tests: `tests/runtime/command-normalize.test.ts` (the still-ungated `nohup` residual pin, the alternation-order clone fix), `tests/runtime/intercept-cli.test.ts` (the `runInterceptCli`-level once-per-event pin for both a Bash and a Codex shell event). Docs: `docs/okf/policy-engine-producer-wiring.md`, `docs/okf/debug-verb-selection.md`, `docs/okf/gate-fail-posture-matrix.md` (a new row/paragraph for the amp pass's identical `MAX_NORMALIZE_LENGTH` fail-open).
|
|
38
|
+
|
|
39
|
+
- **SECURITY: the four surfaces `0.43.0`'s `d834a065` entry named as "deliberately left on the old alphabet" now recognize a bare `&` as a command boundary too, closing follow-up `76671e5a`** — `CURATED_MUTATION_BASH_RE` and `GH_PR_MERGE_BASH_RE` (`src/policy-packs/builtin/post-merge-gate-runtime.ts`), `DEFAULT_PUSH_BASH_RE` (`src/policy-packs/builtin/solution-acceptance-runtime.ts`), and this repo's own `dogfood/harness.yaml`'s `dogfood-recency` trigger. Same fix as `d834a065`: `&&` is subsumed rather than dropped (in `A=x&&git push` the second `&` itself serves as the boundary), so the change is strictly more permissive. **Loss measured with a passing positive control, so the zero means something:** over a 49,999-string corpus, 0 of the old alphabet's matches were lost on any of the four surfaces, all four gained real, bash-verified true positives, and a narrowing control (reverting the fix) reintroduced losses on the same corpus, proving the instrument can actually detect a regression rather than being saturated. No lockout of the documented recovery path either: across 5,258 two-segment and 5,880 three-segment recovery-then-deny commands, the broadened deny side never also matches as an escape command. **New, ACCEPTED false-positive class, widening the one `d834a065` already disclosed for `&&`:** a gated verb mentioned as TEXT after a literal `&` inside a quoted string now matches on all four surfaces too (e.g. `echo "a&git push"`, `grep -F 'x&git commit' notes.md`) — verified against the live PATH that bash never runs the gated verb in any of them, so this is an over-block, not an under-block; pinned in `tests/policy-packs/post-merge-gate-runtime.test.ts` rather than left unrecorded, matching the standard `d834a065` set. **Deliberately left narrow, not swept:** `ESCAPE_GIT_BASH_RE` / `ESCAPE_HARNESS_BASH_RE` (same file) stay on the old, `&&`-only alphabet — they are ALLOW-lists checked first and unconditionally, so widening their boundary alphabet would LOOSEN a gate instead of hardening it, the same reasoning `0.43.0` already gave for not sweeping them. Consequence, measured rather than assumed: 480 of the 5,880 three-segment corpus commands, all of exactly one shape (`<LEAD> & <RECOVERY> & <DENY>`, e.g. `sleep 0&git switch master&git push`), become newly blocked, because the recovery verb is reachable only via the bare `&` the narrow escape check does not recognize. Not a lockout: every such command also contains a mutation verb bash really runs, and the plain recovery alone (`git switch master`, no bare-`&` chaining) is never blocked — pinned as an accepted asymmetry so a future sweep that widens the escape list has to make that decision consciously. **Fix round 1, review findings, none of them behavioural:** a test comment claiming `dogfood/harness.yaml` is a self-inflicted release gate for this repo was false (measured: `harness apply --config dogfood/harness.yaml` exits 1 and rejects it, since the file is a `harness validate`/`harness doctor` fixture, never an applied manifest) and is corrected to describe it as that fixture instead; the source-level allow-list pin that counted `&` characters missed a real widening (`&&?` matches a single bare `&` while keeping the same character count) and is replaced with an exact assertion on the boundary group's source text; the "`&&` stays left of bare `&`" comments on the three runtime regexes overclaimed an ordering requirement that measurably does not hold for a `RegExp.test` matcher (byte-identical match sets swapping to `&|&&` — the segmenter reasoning in `command-normalize.ts` does not transfer here) and are reworded; the dogfood-recency trigger gained a test that actually compiles and fires the pattern (the prior drift guard only string-scanned for the old alternation substring, so a broken regex would have stayed green). New tests: `tests/policy-packs/post-merge-gate-runtime.test.ts` (accepted-over-block pin, escape/asymmetry pin, exact boundary-group pin), `tests/cli/init-full-template-pins.test.ts` (dogfood-recency compiled-trigger pin).
|
|
40
|
+
|
|
41
|
+
- **SECURITY: the read-only-bash write-flag guards now decode shell quoting before comparing, closing seven measured fail-opens where a quoted or escaped write flag hid from the guard while bash still honoured it** (slice 1, PR #392). `find . -"delete"`, `find . -'delete'`, `find . -\delete`, `find . -$'delete'`, `find . -de"lete"`, `sort --outp"ut"=out.txt` and `file --"compile"` all ran as "read-only" past the classifier — exact set-membership (`FIND_WRITE_FLAGS.has`) and long-flag prefix tests (`startsWith("--output=")`) were fully defeated, while short-flag cluster tests survived only by accident (the flag letter stayed a substring of the quoted token). New `src/runtime/shell-word.ts` `decodeShellWord` (quote removal + escape decoding only — ANSI-C `\xHH`/`\NNN`/`\uHHHH`, per-quote backslash rules, run concatenation; NO `$VAR`/`$()`/backtick/`~`/glob expansion) is applied before every write-flag comparison. Per its module-header direction rule, the decoded value is used only on the RESTRICTIVE side — to recognise MORE tokens as write flags, never fewer — and `decodeShellWord` returns the raw token for anything it cannot resolve, so an incomplete partial-bash model degrades to today's behaviour, never a new fail-open (the property that distinguishes it from the permissive-side heredoc-strip model removed the same day).
|
|
42
|
+
|
|
43
|
+
- **`fast-uri` bumped to a patched release via `npm audit fix`** (lockfile-only, no source change), closing GHSA-7p8r-x3mc-p8w7 (host confusion via a backslash authority introducer; high severity). Transitive build/test dependency, not on the CLI's shipped runtime path.
|
|
44
|
+
|
|
45
|
+
## [0.43.0] - 2026-08-01
|
|
46
|
+
|
|
47
|
+
> **Upgrade note:** the template-level changes in this release — the single-`&` trigger boundary (including the `operator_only` kill-switch deny) and the `mcp__grounding-mcp__` tool-prefix rename — live in the manifest templates, so an EXISTING install keeps its old `harness.yaml` triggers until the operator re-runs `harness init` (or edits the manifest by hand). The engine-side normaliser fixes below apply on package upgrade with no manifest change.
|
|
48
|
+
|
|
49
|
+
### Security
|
|
50
|
+
|
|
51
|
+
- **A single `&` was not a command boundary to any policy trigger, so a gated verb placed after one reached no gate at all, including an `operator_only` deny** (task `d834a065`). bash starts a new command after `&`, but the alternation every shipped `policies[].trigger.bash_match` shares listed only `&&`. Measured through the real prediction path against `docs/examples/full-manifest.yaml`, with each policy plain form as its own positive control and a PATH shim proving the verb really executed: `A=x&git status`, `A=x&git push`, `A=x&gh pr merge` and `A=x&harness pause` all missed their trigger, and `sleep 0 & git status` misses with no assignment prefix at all, so this is a boundary-alphabet gap rather than a quote-model one. The `harness pause` case is the sharpest: `deny-kill-switch-bypass` is `operator_only: true`, which no in-session evidence can open, and one character walked around it. Fix: the `&&` alternative becomes `&` in every policy trigger across `src/cli/init/templates.ts` and `docs/examples/full-manifest.yaml`. `&&` is subsumed rather than dropped, since in `A=x&&git status` the second `&` serves as the boundary, so the change is strictly more permissive and is pinned in both directions. **Cost, measured rather than argued** (the analysis that found this could not measure precision at all, 75 of 75 axes without a reference): a false-alarm corpus with bash deciding whether the verb really runs reports 3 pre-existing false alarms and 6 new ones, every new one of the shape "gated verb mentioned as text after a literal `&`" (`echo "a & git status"`, the same string inside a heredoc body, after a `#` comment, as a `grep` pattern, or as an assignment value). One more spelling outside that corpus behaves the same way: `sed -i 's/foo/& git status/' notes.md`, since `&` is sed's whole-match backreference. That is the already-open `dbc6d303` class, which fires on `&&` in text today, widened by one spelling; the direction is fail-closed. **Which policy a false alarm lands on matters, so it is recorded rather than averaged:** most land on `requires:`-gated policies an agent can satisfy itself, but a mention of `harness pause` after an `&` now trips `deny-kill-switch-bypass`, which is `operator_only: true` and has NO in-session recovery path. Writing prose that quotes this very fix in a Bash heredoc is enough to hit it. Measured live twice while reviewing this change. **Surfaces deliberately left on the old alphabet, named rather than swept:** three policy-pack runtime regexes (`CURATED_MUTATION_BASH_RE` and `GH_PR_MERGE_BASH_RE` in `src/policy-packs/builtin/post-merge-gate-runtime.ts`, `DEFAULT_PUSH_BASH_RE` in `src/policy-packs/builtin/solution-acceptance-runtime.ts`) and this repo's own `dogfood/harness.yaml`. They carry the identical gap on live gate surfaces, but a blanket sweep would be wrong: `ESCAPE_GIT_BASH_RE` / `ESCAPE_HARNESS_BASH_RE` in the same pack file are ALLOW-lists checked first, so widening those would LOOSEN a gate instead of hardening it. Tracked as follow-up `76671e5a` so the per-surface direction is decided rather than assumed. **What this does NOT touch:** `expire_on_bash_match` is a separate anchored family (`^gh pr (merge|close)`, `^git push origin (master|main)`) with no boundary alternation, deliberately left alone because widening it would change ledger-expiry semantics, and now pinned to stay anchored and alternation-free. Also unchanged: the quote-model bypasses (`cf3dff51`, `b093911d`) that survive this fix, and the head-spelling gaps (`"git" status`, `\git status`). A second emitter carried the same hole and is fixed here too, found by review rather than by the original sweep: `src/cli/init/composer.ts`, the `harness init --interactive` Custom profile, six patterns, so the bypass stayed live for every operator who picked Custom. Loss for the trigger layer was measured against the previous patterns with a passing positive control: **0 of 7,764 old matches lost** across all 15 shipped patterns, 900 newly matched, over a 68,040-string corpus of assignment prefixes, separators, wrappers and gated verbs. That number covers the RAW trigger layer only.
|
|
52
|
+
|
|
53
|
+
**A third change was attempted, measured, and REVERTED, recorded here because the attempt is the useful part.** `BOUNDARY_RE` in `src/runtime/command-normalize.ts` has the same gap, and leaving it means the adjacent spelling `A=x&env -C /tmp git status` stays ungated (a wrapper can only be peeled after a recognised boundary). Adding `&` there closed that and 259 similar spellings, but review measured what the first corpus never sampled: `BOUNDARY_RE` is quote-unaware, `&` inside a quoted assignment value is common, and the split lands mid-quote so the value continuation never engages. Independently reproduced with a passing positive control: **140 of 140** sampled `<wrapper> FOO='a&b' <gated verb>` spellings LOST a gate they held before, including the `operator_only` kill-switch deny. Closing one bypass while opening a wider one is not a trade worth shipping, so the edit is reverted and the module now carries the measurement so the next attempt starts from it. The remaining `A=x&env ...` gap is tracked separately; it needs a design that does not regress the quoted-value case, not another character in that alternation. New tests in `tests/cli/init-full-template-pins.test.ts` (data-driven over all three profile templates plus `composeCustom()` output, asserting that EVERY declared trigger is exercised rather than merely one, and a negative pin that no shipped `bash_match` string in the templates, the composer, the example manifests or the docs still carries the old alternative, scanning each file as one whitespace-collapsed string because a per-line scan went blind to the four prettier-wrapped composer values, which a mutation confirmed) and `tests/cli/init-full-template-kill-switch-deny.test.ts`. The docs that taught the vulnerable anchor (`docs/writing-custom-policies.md`, `docs/ARCHITECTURE.md`, `docs/examples/policies/02-clean-check-before-push.yaml`) are updated, since they were the propagation vector.
|
|
54
|
+
|
|
55
|
+
- **A git-verb `bash_match` policy trigger could be silently defeated by wrapping the command, and the bypass left NO audit trail at all — no ledger query, no decision, no `harness audit` row** (task `ea8becf5`). `preflight-before-investigation`'s trigger was a raw regex requiring the literal token `git` at a shell boundary, optionally preceded by `VAR=value` tokens; `env`/`nice`/`command`/`sudo`/`doas`/`time`/`timeout`/`stdbuf`/`setsid` wrappers, any git global option other than `-C`, a path-qualified `git` binary, or even a doubled space between `git` and its subcommand all defeated it. Reproduced live: `env -C <repo> git status` reached an un-preflighted repository while `cd <repo>; git status` was correctly blocked. Fix: a new pure normaliser, `src/runtime/command-normalize.ts`, peels the wrapper prefixes above (matching the git token by basename, e.g. `/usr/bin/git`) and canonicalises a git invocation's own global options (`-C`, `-c`, `--git-dir`, `--work-tree`, `--no-pager`, `--exec-path`, `--namespace`, …) down to `git <subcommand>`; `policyMatchesEvent` (`src/runtime/intercept.ts`) now tests each `bash_match` regex against the raw command and, only on a raw miss, against the normalised one — additive by construction, so nothing gated before this can become ungated, pinned by a superset test. This change lives entirely in the engine, so an already-installed manifest is fixed on upgrade — no `harness apply` or manifest edit required. **What this does NOT close, so no doc or commit claims it does:** only a `bash_match` whose peeled head token is literally `git` benefits at all. `review-before-merge-bash`'s `gh pr merge` trigger, `review-subagent-before-pr-create-bash`'s `gh pr create` trigger, `dogfood-before-release`'s `npm publish` half (its `git tag v` half IS covered), and all three `deny-*` operator-only kill switches (`harness pause`/`resume`/`gate`, the session-env-strip pattern, the pause-sentinel-write pattern) remain exactly as wrapper-bypassable as before this change — do not read it as having hardened those (since narrowed in this same Unreleased batch by task `432db3d3`, see below: the `gh`/`npm publish`/`harness` head tokens are now covered, while `deny-session-env-strip` and `deny-pause-sentinel-forgery` remain open there too). Also still open: `xargs git status` (deliberately excluded — `xargs` can invoke its wrapped command zero, one, or many times, unlike a transparent wrapper), a quoted subcommand (`git "status"`), backtick command substitution (`` echo `env -C /tmp git status` ``), `sh -c`, heredocs, `eval`, base64-decoded payloads, and quoted paths containing whitespace. **Correction (review round 2): `$(...)` command substitution is NOT open, unlike the backtick case right before it** — `echo $(env -C /tmp git status)` demonstrably blocks today, ACCIDENTALLY: `$(...)`'s opening paren happens to be the exact `(` character this module already treats as a shell boundary for the `(cd X && ...)` leading-subshell case, so it falls out of that, not a deliberate feature, and is now pinned by a dedicated test so an unrelated future edit can't silently drop the coincidence. Ten more measured bypasses found in the same review round (G2 finding, 2026-07-27), none of them peeled wrapper prefixes or a recognised `git` spelling: `exec`/`nohup`/`ionice`/`flock`/`script`/`chrt`/`taskset` wrapping `git status`, a backslash-escaped `git` token (`\git status`), a quoted git BINARY name (`"git" status` — the mirror image of the quoted-subcommand gap above), and `env -S "git status"` (the split-string flag hands off to a fresh, deliberately-opaque re-parse). An eleventh, `nice -10 git status` (the bare-dash form `nice(1)` documents as its PRIMARY spelling), was measured as a bypass in the same pass but is now CLOSED (G3 fix, review round 2): `nice` stays in the peeled-wrapper list, this just widens which of its own spellings are recognised. And an `env`-argument spelling like `--default-signal=INT`, which only one of the two independent env-peelers previously recognised, is ALSO now closed (G6 fix, review round 2): the earlier fix here (sharing the flag-NAME enumerations via exported constants in `src/runtime/read-only-bash.ts`) had only closed the flag-name-SET drift, not this specific bypass, which needed its own generic glued-`--long=value` catch-all mirrored into this module too. Separately (G4 finding, review round 2): above `MAX_NORMALIZE_LENGTH` (100,000 chars), normalisation used to skip SILENTLY — no stderr line, no audit row; `harness policy intercept` now emits one stderr line reporting the skip, and `docs/okf/gate-fail-posture-matrix.md` gained a matching row. **Newly widened false positives** (pre-existing class, task `dbc6d303`): quoted or heredoc text mentioning a wrapped git verb can now trigger where it previously did not — but only when the mention sits at a shell-boundary position (line start, or after `;` / `|` / `&&` / `(`), which is narrower than "anywhere in quoted text": measured, `echo "a; env git status"` blocks while `echo "env -C /tmp git status"` does not. Hit three times live during this fix's own development: an Understanding Report heredoc, a commit message containing an `&& git push` example, and a reviewer's own probe script whose case labels contained the literal verbs. `harness dry-run`'s trigger prediction now matches raw-or-normalised the same way `policy intercept` does, closing a debug-verb/runtime prediction mismatch on the identical spellings above.
|
|
56
|
+
- **Per-repository target resolution is explicitly NOT part of this task, and no doc or commit claims otherwise.** `${REPO}`/`${BRANCH}` (`src/cli/policy/intercept.ts`) still resolve from the hook's own cwd alone, exactly as in 0.42.0: a command that explicitly names a different repository (`git -C <B>`, `env -C <B>`, a leading `cd <B> &&`) does not change what `${REPO}`/`${BRANCH}` name. A `preflight:<repo>` fact recorded for repository A therefore still authorises a command that explicitly reads (or, chained, mutates) repository B — unchanged from the shipped 0.42.0 behaviour, not a new gap introduced here. A per-command target-directory resolution for these builtins was built and reviewed on this same task, but was found to regress security in three consecutive review rounds — Risk Gate risk declassification, a push-gate multi-invocation fail-open, and (found twice: first fixed via `&&`-chain coverage, then found still open via `|`/`||` chaining) a non-git gated verb inheriting a decoy repo's target from an unrelated, `-C`-scoped git call sharing the same command line (e.g. `git -C <decoy> rev-parse HEAD | gh pr merge`) — and was removed before shipping rather than patched a fourth time. Each fix was only ever partial because a single `targetDir` per event cannot express "this invocation targets repo B, but the gated verb after it runs in the caller's own cwd"; the correct fix needs to attribute a target to the SEGMENT that actually satisfies a policy's own trigger, which means the policy builtins becoming per-policy instead of per-event — an architectural change out of scope for this task, tracked as follow-up task `98ad072f`. The extraction this would have been built on (`command-normalize.ts`'s `targetDir`/`targetBase`, including its own git-vs-git and git-vs-non-git ambiguity rules) stays in the tree, fully tested but unwired, as that follow-up's foundation. New tests: `tests/runtime/command-normalize.test.ts`, new suites in `tests/runtime/intercept-cli.test.ts` and `tests/cli/dry-run.test.ts` (the G1 git-vs-non-git-verb ambiguity cases through the real `review-before-merge-bash`/`review-subagent-before-pr-create-bash`/`dogfood-before-release` triggers, the G2 ten-spelling ceiling plus the `$(...)` accidental-coverage pin, the G3 `nice` bare-form cases, the G6 `env --default-signal=INT` case, a `policy intercept` end-to-end pin on the G4 stderr line, and an explicit regression test that a command naming a foreign target repo resolves `${REPO}`/`${BRANCH}` to the CWD repo, not the named target, so a future reintroduction of target resolution cannot land unnoticed). Docs: `docs/okf/gate-fail-posture-matrix.md`, `docs/okf/debug-verb-selection.md`, `docs/okf/policy-engine-producer-wiring.md`, `docs/okf/codex-adapter-parity-gaps.md`, `docs/okf/pause-vs-gate-kill-switch.md`.
|
|
57
|
+
|
|
58
|
+
- **Non-git `bash_match` triggers no longer lose to a one-word wrapper: `command-normalize.ts`'s head-token condition widened from literally-`git` to the closed set `git`/`gh`/`npm`/`harness`** (task `432db3d3`, follow-up to the `ea8becf5` entry above). Measured closed, per spelling through the real hook entry point with the shipped 0.42.0 binary as negative control: `env` / `env -C <dir>` / `nice` wrapping the `gh pr merge` verb, `env` wrapping `gh pr create`, `env` / `nice` wrapping `npm publish`, `env` / `nice` / `command` / `env -C <dir>` wrapping `harness pause`, plus whitespace-run spellings (`gh pr merge`, `gh pr merge`, tab forms, `npm publish`) — the interior-run group via a full-segment single-space rejoin for set heads, because collapsing only the head-adjacent gap measurably left `gh pr merge` open (review finding). Matching stays raw-OR-normalised and therefore strictly additive (superset pinned), with a new ordering pin: `env -u CLAUDE_CODE_SESSION_ID npm publish` must keep blocking `deny-session-env-strip` via the RAW match — the one shipped shape where the normalised form erases what another policy keys on, so this single test goes red if raw-first matching is ever collapsed to normalised-only. **What this does NOT close, measured and pinned rather than implied: only one of the three operator-only kill switches benefits.** `deny-session-env-strip` keys on `env`/`unset` head tokens and is structurally out of the normaliser's reach (its peel loop consumes `env -u <VAR>` while hunting for a gated command behind it, erasing the very text that trigger keys on); `deny-pause-sentinel-forgery` keys on `tee`/`cp`; both remain one-wrapper-word bypassable (`nice env -u CLAUDE_CODE_SESSION_ID ls`, `nice tee <sentinel>`), pinned as explicit not-covered tests. Also uncovered and pinned: tool-specific flags between head token and subcommand (`gh -R`, `npm --loglevel`) and path-qualified `gh`/`npm` spellings (byte-identical through the normaliser; a path-qualified `harness pause` still denies via the trigger regex's own `\S*/` alternative at the raw level). New false-positive class recorded: introspection spellings (`command -v harness pause`, `sudo -l harness pause`) now deny — over-block direction, distinct from the quoted/heredoc class (task `dbc6d303`), which this change widens twice: to wrapped non-git verb mentions at shell-boundary positions, and — via the full-segment rejoin — to whitespace-run spellings of them inside quoted text (measured example: `harness explain-policy --command "( npm publish )"` now matches where only its single-spaced variant did before). A new drift guard, `tests/runtime/bash-match-head-token-drift.test.ts`, couples the engine's head-token set to `FULL_TEMPLATE`'s actual `bash_match` policies (policy inventory plus per-token spelling presence), so the next policy gating a new head token turns a test red instead of staying silently uncovered; the guard's own residual — a new alternative added ALONGSIDE a still-correct token inside an existing policy's regex escapes both checks — is stated in the test file header. Docs: the `deny-kill-switch-bypass` UPDATE comment in `src/cli/init/templates.ts` and `docs/okf/pause-vs-gate-kill-switch.md` now state the partial closure precisely instead of the pre-fix "no-op unless `git`" claim.
|
|
59
|
+
|
|
60
|
+
- **SECURITY: a quoted env-assignment value with embedded whitespace (`VAR='hello world' git push`) bypassed every `bash_match` gate on BOTH matching layers — closed by continuing the assignment's value across tokens in the normaliser** (task `13e55484`, found by a read-only explorer during `d977ad58` and verified against the shipped binary, not derived). Measured at the real hook entry point with shipped 0.42.0 as control: `VAR='hello world' git push origin master` and `VAR='hello world' gh pr merge 4242 --squash` both produced "no policy matched" while their plain spellings blocked. The two layers failed on the same character: the raw regex's `(\w+=\S+\s+)*` cannot span an embedded space, and `command-normalize.ts`'s whitespace tokenizer split the assignment into `VAR='hello` + `world'`, the second token aborting the wrapper-peel loop so the segment came back byte-identical. Fix: a `consumeAssignment` helper continues an assignment's VALUE across whitespace-split tokens while an opening quote from that assignment stays unbalanced at a token's end AND a matching close exists in a later token, used by both the top-level peel loop and `peelEnv`'s own assignment scan (`env VAR='a b' git push` was the same bypass one wrapper deep) — deliberately NOT a general quote-aware tokenizer. **Review round 1 found the first version's load-bearing claim ("additive by construction") FALSE by measurement (CRITICAL, fixed before merge):** a 30k-case seeded differential fuzz against a master worktree surfaced 482 lost matches, rooted in ANSI-C quoting — bash escapes `\'` inside `$'...'`, a plain-`'...'` scanner closes on it — so `A=$'don\'t' env harness pause # '` left a phantom-open state that swallowed the gated head token and turned three MASTER-BLOCKED commands (push gate, merge gate, and the operator-only kill-switch deny) into "no policy matched", PATH-shim-verified as real invocations. Closed with a ONE-DIRECTIONAL GUARD: the continuation abandons back to the byte-exact pre-task one-token consume the moment the token it would consume next matches `GIT_TOKEN_RE` or `NON_GIT_HEAD_TOKENS`. **Scope of the guarantee, corrected in round 2 after the first re-wording overclaimed again:** the guard closes the HEAD-SWALLOW channel specifically (a diverging quote state can never swallow or re-target a recognised head token — round-2 re-verified: the round-1-style differential fuzz lost 0 of 19,994 cases). It is NOT universal monotonicity: a second, round-2-measured channel remains where the continuation closes on a wrapper's glued flag token and the peel resumes at that wrapper's next argument (`A='x timeout --signal=INT' 5 git push` matched on master, does not here — 550 of 41,440 adversarial cases). Every found member is a MASTER FALSE POSITIVE, PATH-shim-verified (110 are `bash -n` syntax errors, the other 440 execute the next word — `5`, `-e0` — never the gated verb), so no gated invocation is lost and the branch is the more bash-accurate side; the class is pinned so it cannot silently widen. Differential regression pins: the three ANSI-C repros, a wrapper-swallow case (`env VAR='a git push origin master' foo`), the wrapper-argument-list class above, and a path-qualified-glued-quote case (`VAR='a /usr/bin/git' git push` — the guard's exact-spelling check does not abandon there, and the REAL invocation behind it now matches, a measured fail-closed gain). An UNTERMINATED quote (`VAR='a git push`) keeps its pre-task normalised match, and the engagement assurance was re-scoped after being measured not load-bearing (the whole-corpus ≥8 threshold stayed green with every quoted entry deleted): every quoted-assign corpus entry must now match ONLY via normalisation, ≥4 entries, so disabling the continuation reddens the assertion itself — the dbc6d303 lesson applied twice. Closure pinned per variant: double quotes, multiple mixed-quoting assignments, assignment+wrapper, env-internal assignment, tab-in-value, chained quote runs (`'a b'"c d"`), and the second gated verb; mutation-probed both ways (guard removed → exactly the 4 differential pins red; both call sites reverted → 11 red: the 8 closure pins, the over-block pin, the corpus property, and the family engagement). **What this does NOT close, pinned as still-open rather than implied:** a shell-boundary character INSIDE the quoted value (`VAR='a; b' git push origin master` — quote-unaware `BOUNDARY_RE` splits the segment before tokenisation ever runs; measured bypass on both master and branch, outside this task's pre-declared budget per its halt criterion); a quoted value containing the literal word `git`/`gh`/`npm`/`harness` as its own token (`VAR='a git b' git push` — the guard's honest cost; byte-identical to master's behaviour, so nothing regressed); backslash-escaped whitespace without quotes (`VAR=a\ b git push`, task `b093911d`'s class); and the RAW regex half, deliberately unchanged (the engine-level fix heals installed manifests on upgrade — note the shipped 0.42.0 binary has NO normaliser at all, so every wrapped spelling here is 0.42.0-bypassable until the next release ships this whole Unreleased batch). **New accepted over-block, decided not discovered:** a quoted assignment inside TEXT at a segment-start position now matches (`echo "a; VAR='x y' git push"` blocks where master allowed) — fail-closed direction, same class the module's KNOWN OVER-MATCHING section documents, pinned as a boundary assertion. Latency (corrected twice in review — the first entry claimed sub-microsecond, the second gave numbers without naming the input shape, which varies the cost by >2x at the same length): 0.6-2µs per `normalizeCommand` call on typical spellings; at `MAX_NORMALIZE_LENGTH` (100k chars) the cost ranges by shape from ~1.2ms (plain command, both sides identical) through ~0.85ms master / ~1.09ms branch (assignment-heavy) to ~2.0ms master / ~2.3ms branch on the worst adversarial quoting shape — linear scaling on every shape, against a 1000ms hook budget.
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- **Cross-build measurements of the Bash prefix parser's cd-target extraction now have a permanent, self-testing instrument: `scripts/measure-bash-prefix-parse.mjs` (`npm run measure:bash-prefix-parse -- --self-test`, then with `--baseline name=path` builds for a real run)** (task `47297478`). The `b093911d` run produced three consecutive measurement corpora that reported "0 lost cd targets" while being structurally incapable of reporting a loss (round 3: the separator-less arm rendered `A=xcd /tmp` and a non-writable cwd silently killed every redirect shape; round 4: exactly the one spelling per separator where the baseline is also null), each time turning into a false safety claim that cost a full review round. The tool hardens that run's `final-audit.mjs` into repo infrastructure: a candidate build of `src/runtime/bash-prefix-parse.ts` is measured against real bash (resolved once to an absolute path with execvp-faithful file/executable checks; the corpus children get a PATH containing ONLY the probe-shim directory, so no corpus command can resolve a real binary via PATH — that constrains lookups, not absolute paths, and the generator emitting no absolute binary paths is what makes "today nothing but the probe shim executes" true; the workspace also runs the shim once up front and fails with a self-naming error if the interpreter path is broken) and at least two baseline builds — duplicate baseline names are rejected instead of silently merging counter buckets. PER-ARM GATE: an arm whose shapes never ran, never entered the target, or whose baseline never hit is reported as proving nothing and is never folded into a total zero; a total that excludes such arms says so instead of printing a bare 0, and the baseline-independent candidate table (per-arm hits, phantoms, wrong targets — attribution down to the spelling) carries the same qualifier for arms that were never observed at all. A rendered legend states what a phantom is (bash did not enter the target ON THIS RUN, which includes a statically present `cd` on a short-circuited `||` branch — conservative for a risk gate, not by itself a parser defect). Both residual weaknesses the run's reviewer named in `final-audit.mjs` are fixed: every baseline keeps independent counters (a loss against two baselines counts against both, no `else if` folding), and a non-null-but-WRONG target is its own failure class for candidate and baselines alike, never a hit — so a wrong-target baseline cannot satisfy the gate either. The instrument proves it can fail before it is believed: the self-test (always run first; `--self-test` runs it alone) rebuilds the round-3 defect on purpose and requires the gate to flag the sabotaged arm as evidence-free via an entered-based delta that parser state cannot satisfy, carries a deliberately blind baseline as a positive control that the BASELINE-NEVER-HIT rung (the round-4 countermeasure) can fire, requires executed redirect shapes (writable-cwd control), and treats a candidate regression on the canonical arm as a loud warning rather than misdiagnosing it as a broken environment. The assertion set is a pure exported function (`evaluateSelfTest`) with its own failure-path unit tests, so a deleted assertion cannot vanish silently. New tests: `tests/scripts/measure-bash-prefix-parse.test.ts` (hermetic audit-core + self-test-assertion + arg-parsing suites with injected referee/parsers, plus a real-bash CLI self-test e2e via the INFRA-allowlisted `node`; the grandchild route is recorded at the spawn guard's residual-exposure note). `bash-prefix-parse.ts` and `command-normalize.ts` carry a MEASUREMENT RULE header pointer scoped to what the tool actually measures — `cdTarget` only; inline-env extraction and `command-normalize.ts`'s target extraction have NO instrument yet and say so. The parser itself is untouched.
|
|
65
|
+
|
|
66
|
+
- **The drift guard that couples engine-side constants to the shipped manifest is now general, after the same defect shipped twice one day and one file apart** (task `074acf5d`). `432db3d3`'s guard checked one set in one file; `dbc6d303` then put `harness` — itself the gated head token of `deny-kill-switch-bypass` — into a NEW module's inert-consumer allowlist, reverting that policy's quoted-spelling hardening, and the existing guard structurally could not see it. `src/runtime/bash-match-facts.ts` derives the shipped manifest's `bash_match` facts live (which policies declare one, which literal head tokens each anchors on, which verbs) and answers "is this token gated by any shipped policy?" in one call; it throws at module-evaluation time when a policy is added, removed or renamed, so the curated map cannot drift silently. `src/runtime/bash-match-registry.ts` registers each engine-side set with a declared intent (`covers-gated-head-tokens` or `must-not-contain-gated-head-token`) and checks every registration against those facts. Both historical incidents are pinned as regressions and were re-verified by an independent reviewer against the REAL registrations, not fixtures. An unregistered exported set in the runtime layer reddens the guard, and the scan carries fixture-tree positive controls so it cannot rot into a no-op unnoticed. No runtime matching path changed. **What this does NOT catch, named verbatim in the module's LIMITS section rather than implied:** three independent adversarial review passes each found a way to register a set the registry accepts and should not, and the fix rounds were stopped after the third on a pre-declared criterion rather than adding a fourth check. Open and measured end to end: a covers registration declaring only `documented-uncovered` gated tokens (`env`, `unset`, `tee`, `cp` — four of the eight) passes every check with an entirely honest, live-coupled registration, which is precisely the `dbc6d303` shape; a registration's `has()` predicate is never cross-checked against the constant its `id`/`module` names, and the `must-not-contain` arm — where that shape lives — has no predicate check at all; `members` may declare coverage the predicate rejects, and `members: []` typechecks; a registration may name a module or id no exported constant matches. The redundancy check additionally forbids legitimate defence-in-depth double coverage, and five `new Set([...])` spellings (`as const`, a comment between `(` and `[`, backtick-quoted elements, union-literal annotations, a trailing comma) escape the scan by design rather than by oversight. Treat the registry as a tripwire for the accidental case, never as a boundary against a determined one; the registration surface is being redesigned to derive from the constant instead of accepting a self-description (follow-ups `209e6dc4`, `26c1d33e`).
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- **All agent-facing references to the grounding MCP tool prefix renamed from `mcp__agent-grounding__*` to `mcp__grounding-mcp__*`, matching the canonical registration name `grounding-mcp` that 0.42.0's doctor convention checks** (task `ca9b0026`). Since 0.42.0 stopped emitting `mcpServers` into `settings.json`, the documented registration path is `claude mcp add-json --scope user grounding-mcp ...`, and a session registered that way exposes its tools as `mcp__grounding-mcp__*` — but every `verb:`/`producers[].example`/`ux.run` line in `FULL_TEMPLATE` and the profiles, plus the solution-acceptance deny/guidance strings, still named the legacy `mcp__agent-grounding__*` prefix from the pre-0.42 project-scope registration, so a canonically-registered session was told to call a tool name it does not have. All 92 references across `src/`, `tests/`, and `docs/` renamed in one sweep. This is presentational only, verified before the change: no PreToolUse matcher, hook, or engine path keys on the tool NAME (the ledger gates read ledger content, which both registrations write identically, since both names spawn the same `grounding-mcp` binary) — so a still-legacy-registered session keeps satisfying every gate after this change, and the only behavioural effect is that guidance now names the tool a doctor-green session actually has. **What this does NOT rename:** references to `agent-grounding` as the REPO/package name in comments and docs, the `VERDICT_DIR_TAIL` filesystem path (`agent-grounding/solution-verdicts` — an on-disk state path, renaming it would orphan existing verdicts), uninstall's recognition of the legacy registration name (`name === "agent-grounding"` in `src/cli/uninstall/index.ts` — needed precisely to clean up pre-0.42 installs), and historical CHANGELOG entries. Existing tests updated in place (same sweep); the FULL_TEMPLATE surface was already pinned (the parity and composer tests now assert the new spelling), and a new repo-wide guard test (`tests/no-legacy-mcp-prefix.test.ts`) asserts the literal legacy prefix does not reappear anywhere under `src/` — added after review measured that a partial revert of the non-template surfaces (profiles, hook guidance strings) would otherwise ship green. Operator action (corrected in review: `harness apply` does NOT do this — it never rewrites `harness.yaml`, and the deny-time guidance is rendered straight from the loaded manifest): the pack-hook messages update on package upgrade with no operator step, but an EXISTING manifest keeps the legacy `verb:`/`producers[].example`/`ux.run` spellings until the operator re-runs `harness init` or edits `harness.yaml` by hand; sessions still on the legacy project-scope registration should re-register per `harness doctor` and drop the legacy entry.
|
|
71
|
+
|
|
10
72
|
## [0.42.0] - 2026-07-27
|
|
11
73
|
|
|
12
74
|
### Security
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// The operator's deliberate blessing of a protected-branch edit (version
|
|
4
4
|
// bumps, CI workflow patches, hotfixes) for one session. Audit finding
|
|
5
5
|
// #39: the old override was a `branch-protection-ack:` ledger tag, but the
|
|
6
|
-
// agent has direct `
|
|
6
|
+
// agent has direct `mcp__grounding-mcp__ledger_add` access and could
|
|
7
7
|
// self-write that tag to bless its own edit. This verb instead writes the
|
|
8
8
|
// canonical operator-only approval MARKER under
|
|
9
9
|
// `harness.generated/.approvals/branch-protection-<sessionId>` (the same
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branch-protection.js","sourceRoot":"","sources":["../../../src/cli/approve/branch-protection.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,yEAAyE;AACzE,uEAAuE;AACvE,2EAA2E;AAC3E,
|
|
1
|
+
{"version":3,"file":"branch-protection.js","sourceRoot":"","sources":["../../../src/cli/approve/branch-protection.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/dry-run.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { stringify as stringifyYaml } from "yaml";
|
|
2
2
|
import { evaluateExtract, substituteTemplate, } from "../policies/index.js";
|
|
3
|
+
import { normalizeCommand, normalizeCommandAmpAware } from "../runtime/command-normalize.js";
|
|
3
4
|
import { resolveGitContext } from "../runtime/git-context.js";
|
|
4
5
|
import { EX_USAGE, HarnessExitError } from "./exit-codes.js";
|
|
5
6
|
import { loadManifest } from "./loader.js";
|
|
@@ -52,7 +53,24 @@ function policyMatchesTool(policy, tool, toolInput) {
|
|
|
52
53
|
catch {
|
|
53
54
|
return { matched: false, reason: `trigger.bash_match is not a valid regex` };
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
// Raw-OR-normalised-OR-amp-normalised, mirroring `policyMatchesEvent`'s
|
|
57
|
+
// real evaluation path exactly (third arm added task aabbad63): dry-run
|
|
58
|
+
// used to test only the RAW command, so it predicted `env -C /tmp git
|
|
59
|
+
// status` as NOT matching `preflight-before-investigation` while
|
|
60
|
+
// `policy intercept` actually blocks it — a debug verb contradicting the
|
|
61
|
+
// runtime it exists to predict (its own comment above and
|
|
62
|
+
// docs/okf/debug-verb-selection.md both assert parity). Leaving out the
|
|
63
|
+
// amp-aware third arm here would reintroduce that SAME class of
|
|
64
|
+
// contradiction for the bare-`&` family (`A=x&env -C /tmp git status`,
|
|
65
|
+
// `echo hi & nice git status`): `policy intercept` now blocks those via
|
|
66
|
+
// `normalizeCommandAmpAware`, so dry-run must try it too. The REPO/
|
|
67
|
+
// BRANCH half of this file (`builtinsFor`, cwd-only) stays in parity
|
|
68
|
+
// with the runtime, which is also cwd-only for `${REPO}`/`${BRANCH}` —
|
|
69
|
+
// see `src/cli/policy/intercept.ts`'s comment above `cwdGitContext` for
|
|
70
|
+
// why a per-command target directory is deliberately not consulted.
|
|
71
|
+
if (!re.test(args.command) &&
|
|
72
|
+
!re.test(normalizeCommand(args.command).normalized) &&
|
|
73
|
+
!re.test(normalizeCommandAmpAware(args.command).normalized)) {
|
|
56
74
|
return {
|
|
57
75
|
matched: false,
|
|
58
76
|
reason: `bash_match "${policy.trigger.bash_match}" did not match`,
|
package/dist/cli/dry-run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dry-run.js","sourceRoot":"","sources":["../../src/cli/dry-run.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"dry-run.js","sourceRoot":"","sources":["../../src/cli/dry-run.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/index.js
CHANGED
|
@@ -1076,9 +1076,10 @@ export function buildProgram(opts = {}) {
|
|
|
1076
1076
|
.description("PreToolUse blocker for the post-merge-gate pack: read tool-event JSON from stdin, and on a curated " +
|
|
1077
1077
|
"history-mutating Bash command (git commit/add/push/merge/rebase/cherry-pick/revert/reset/stash " +
|
|
1078
1078
|
"pop|apply, gh pr create/merge) emit a deny envelope when the current branch tip matches a recorded " +
|
|
1079
|
-
"merged-tip fact.
|
|
1080
|
-
"list/show, any `harness ...` command)
|
|
1081
|
-
"
|
|
1079
|
+
"merged-tip fact. The recovery vocabulary (git switch/checkout/pull/fetch, git branch -d/-D, git stash " +
|
|
1080
|
+
"list/show, any `harness ...` command) always passes as its own command, classified before any manifest " +
|
|
1081
|
+
"or ledger access; chaining one of those with a curated mutation does NOT exempt the mutation. Fails " +
|
|
1082
|
+
"open when the ledger is unreachable.")
|
|
1082
1083
|
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
1083
1084
|
.option("--project <name>", "apply per-project overrides")
|
|
1084
1085
|
.option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
|