@lannguyensi/harness 0.49.0 → 0.51.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 +791 -0
- package/dist/cli/add/index.js +15 -3
- package/dist/cli/add/index.js.map +1 -1
- package/dist/cli/apply/apply.js +19 -3
- package/dist/cli/apply/apply.js.map +1 -1
- package/dist/cli/approve/risk.d.ts +28 -2
- package/dist/cli/approve/risk.js +14 -4
- package/dist/cli/approve/risk.js.map +1 -1
- package/dist/cli/approve/understanding.d.ts +51 -0
- package/dist/cli/approve/understanding.js +17 -2
- package/dist/cli/approve/understanding.js.map +1 -1
- package/dist/cli/diff/index.js +22 -1
- package/dist/cli/diff/index.js.map +1 -1
- package/dist/cli/doctor/claude-mcp.d.ts +14 -0
- package/dist/cli/doctor/claude-mcp.js +7 -1
- package/dist/cli/doctor/claude-mcp.js.map +1 -1
- package/dist/cli/doctor/format.js +48 -3
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/index.d.ts +20 -0
- package/dist/cli/doctor/index.js +66 -2
- package/dist/cli/doctor/index.js.map +1 -1
- package/dist/cli/doctor/settings-drift.d.ts +19 -0
- package/dist/cli/doctor/settings-drift.js +189 -0
- package/dist/cli/doctor/settings-drift.js.map +1 -0
- package/dist/cli/doctor/types.d.ts +52 -0
- package/dist/cli/doctor/types.js.map +1 -1
- package/dist/cli/doctor/ug-auto-approvals.d.ts +40 -0
- package/dist/cli/doctor/ug-auto-approvals.js +116 -0
- package/dist/cli/doctor/ug-auto-approvals.js.map +1 -0
- package/dist/cli/doctor/understanding-mode-env.d.ts +10 -0
- package/dist/cli/doctor/understanding-mode-env.js +12 -0
- package/dist/cli/doctor/understanding-mode-env.js.map +1 -1
- package/dist/cli/explain-policy.d.ts +4 -1
- package/dist/cli/explain-policy.js +12 -2
- package/dist/cli/explain-policy.js.map +1 -1
- package/dist/cli/export.js +12 -1
- package/dist/cli/export.js.map +1 -1
- package/dist/cli/index.js +40 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/templates.d.ts +1 -1
- package/dist/cli/init/templates.js +65 -1
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/list.js +6 -0
- package/dist/cli/list.js.map +1 -1
- package/dist/cli/loader.js +15 -0
- package/dist/cli/loader.js.map +1 -1
- package/dist/cli/pack/auto-approve-path.d.ts +118 -0
- package/dist/cli/pack/auto-approve-path.js +347 -0
- package/dist/cli/pack/auto-approve-path.js.map +1 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.d.ts +9 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.js +129 -10
- package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-post-tool-use.d.ts +6 -4
- package/dist/cli/pack/hook-post-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-pre-tool-use.d.ts +11 -0
- package/dist/cli/pack/hook-pre-tool-use.js +90 -23
- package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
- package/dist/cli/remove/index.d.ts +12 -0
- package/dist/cli/remove/index.js +89 -0
- package/dist/cli/remove/index.js.map +1 -1
- package/dist/cli/validate/checks.d.ts +48 -0
- package/dist/cli/validate/checks.js +312 -0
- package/dist/cli/validate/checks.js.map +1 -1
- package/dist/cli/validate/index.js +11 -0
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.d.ts +136 -0
- package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.js +224 -0
- package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +3 -2
- package/dist/policy-packs/builtin/understanding-before-execution/index.js +35 -28
- package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/markers.js +2 -2
- package/dist/policy-packs/builtin/understanding-before-execution/markers.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.d.ts +29 -0
- package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.js +82 -0
- package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.d.ts +69 -8
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js +100 -12
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js +6 -3
- package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution.d.ts +23 -0
- package/dist/policy-packs/builtin/understanding-before-execution.js +36 -9
- package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
- package/dist/runtime/approval-signing.d.ts +22 -0
- package/dist/runtime/approval-signing.js +55 -1
- package/dist/runtime/approval-signing.js.map +1 -1
- package/dist/runtime/command-normalize.d.ts +112 -0
- package/dist/runtime/command-normalize.js +169 -55
- package/dist/runtime/command-normalize.js.map +1 -1
- package/dist/runtime/deletion-target-resolve.d.ts +28 -0
- package/dist/runtime/deletion-target-resolve.js +1055 -0
- package/dist/runtime/deletion-target-resolve.js.map +1 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/intercept.js +17 -1
- package/dist/runtime/intercept.js.map +1 -1
- package/dist/runtime/kubectl-target-parse.d.ts +16 -0
- package/dist/runtime/kubectl-target-parse.js +9 -1
- package/dist/runtime/kubectl-target-parse.js.map +1 -1
- package/dist/runtime/read-only-bash.d.ts +11 -0
- package/dist/runtime/read-only-bash.js +288 -7
- package/dist/runtime/read-only-bash.js.map +1 -1
- package/dist/runtime/risk-classifier.js +14 -1
- package/dist/runtime/risk-classifier.js.map +1 -1
- package/dist/runtime/when-eval.d.ts +11 -2
- package/dist/runtime/when-eval.js +25 -0
- package/dist/runtime/when-eval.js.map +1 -1
- package/dist/runtime/workflow-policies.d.ts +164 -0
- package/dist/runtime/workflow-policies.js +439 -0
- package/dist/runtime/workflow-policies.js.map +1 -0
- package/dist/schema/index.d.ts +24 -0
- package/dist/schema/policies.d.ts +25 -0
- package/dist/schema/policies.js +29 -0
- package/dist/schema/policies.js.map +1 -1
- package/dist/schema/risk.d.ts +6 -0
- package/dist/schema/risk.js +80 -0
- package/dist/schema/risk.js.map +1 -1
- package/package.json +1 -1
|
@@ -43,6 +43,29 @@
|
|
|
43
43
|
// back the operator's key file), and it makes ANY forgery attempt one
|
|
44
44
|
// step more expensive and forensically visible (a key read is a
|
|
45
45
|
// distinguishable event; a bare unsigned-JSON write no longer is).
|
|
46
|
+
// - SECOND MINTING PATH (agent-tasks/74b4b17d, ADR
|
|
47
|
+
// docs/decisions/2026-08-27-ug-auto-mode-approval.md): the residual
|
|
48
|
+
// above was a CONJUNCTION, read the key AND write through an
|
|
49
|
+
// uncovered primitive. Since the `auto_approve` opt-in of the
|
|
50
|
+
// `understanding-before-execution` pack, the PreToolUse hook itself
|
|
51
|
+
// can write a validly signed session marker (`approvedBy:
|
|
52
|
+
// auto-mode:<harness>:<mode>`) when the payload's `permission_mode`
|
|
53
|
+
// is allowlisted and a strict-session `pending` report exists. Any
|
|
54
|
+
// process that can invoke `harness pack hook pre-tool-use` with a
|
|
55
|
+
// stdin payload and `CLAUDE_CODE_SESSION_ID` of its choosing therefore
|
|
56
|
+
// obtains a signed marker for that session id WITHOUT reading the
|
|
57
|
+
// key: the residual is now a DISJUNCTION, read the key, OR make the
|
|
58
|
+
// hook sign for you. Reach, honestly: pre-approval the oracle is
|
|
59
|
+
// unreachable (invoking it is a Bash call, and `pack` is not on the
|
|
60
|
+
// read-only allowlist in `src/runtime/read-only-bash.ts`), so this
|
|
61
|
+
// is not an escalation over the post-approval write class, which can
|
|
62
|
+
// already forge by the key-read route; it is a cheaper and less
|
|
63
|
+
// forensically visible post-approval route (no key read to notice),
|
|
64
|
+
// and it is why the auto path never creates the key (explicit
|
|
65
|
+
// key-exists precheck before the shared write path), consumes the
|
|
66
|
+
// report it used, declines on a forged marker, and applies only
|
|
67
|
+
// after every existing exemption branch has declined. The pack's
|
|
68
|
+
// doctor listing of `auto-mode:` markers is the audit surface for it.
|
|
46
69
|
// - Given the agent and operator share one OS user account on this
|
|
47
70
|
// machine, this is deliberately pragmatic defense-in-depth, not a hard
|
|
48
71
|
// authorization boundary. It is documented here in full rather than
|
|
@@ -51,7 +74,8 @@
|
|
|
51
74
|
// KEY STORAGE: `<generatedDir>/.approval-signing.key` — a raw 32-byte
|
|
52
75
|
// secret, mode 0600, generated lazily on first use (by `harness approve
|
|
53
76
|
// understanding` / `harness approve branch-protection`, or by `harness
|
|
54
|
-
// init` — see src/cli/init/index.ts
|
|
77
|
+
// init` — see src/cli/init/index.ts; NEVER by the PreToolUse hook's auto
|
|
78
|
+
// path, which prechecks that the key exists and blocks when it does not). Colocated with `harness.generated/`
|
|
55
79
|
// (not a separate home-dir path) so:
|
|
56
80
|
// - it inherits the SAME test-isolation guarantee every marker/report
|
|
57
81
|
// path already has (every call site here already threads an explicit,
|
|
@@ -176,6 +200,36 @@ export function rotateSigningKey(generatedDir) {
|
|
|
176
200
|
}
|
|
177
201
|
return { key: fresh, filePath, created: true };
|
|
178
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Non-creating existence probe for the signing key: `true` only when
|
|
205
|
+
* `<generatedDir>/.approval-signing.key` is a regular file carrying at
|
|
206
|
+
* least `KEY_BYTES` bytes. Never creates, repairs, or rotates anything.
|
|
207
|
+
*
|
|
208
|
+
* Why this exists as a separate export instead of "just call
|
|
209
|
+
* `getOrCreateSigningKey` and see" (ADR
|
|
210
|
+
* docs/decisions/2026-08-27-ug-auto-mode-approval.md, threat model (b)
|
|
211
|
+
* item 5): key CREATION is an operator-side act (`harness init` /
|
|
212
|
+
* `harness approve`). The PreToolUse hook's auto-approval path
|
|
213
|
+
* (agent-tasks/74b4b17d) must refuse to mint a marker on a machine that
|
|
214
|
+
* has no key, but the shared write path it uses (`writeApprovalMarker`
|
|
215
|
+
* -> `signMarker` -> `getOrCreateSigningKey`) treats a missing key as a
|
|
216
|
+
* case to REPAIR: it generates one on ENOENT and signs with it. So the
|
|
217
|
+
* auto path calls this first and blocks when it returns `false`.
|
|
218
|
+
*
|
|
219
|
+
* A truncated / short key file counts as absent here for the same
|
|
220
|
+
* reason: `getOrCreateSigningKey` would overwrite it with a fresh key,
|
|
221
|
+
* which is the same operator-side act this probe exists to keep the
|
|
222
|
+
* hook out of.
|
|
223
|
+
*/
|
|
224
|
+
export function signingKeyExists(generatedDir) {
|
|
225
|
+
try {
|
|
226
|
+
const stat = fs.statSync(signingKeyPathFor(generatedDir));
|
|
227
|
+
return stat.isFile() && stat.size >= KEY_BYTES;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
179
233
|
/** sha256 hex digest of a string, used to bind a marker to a persisted report's content. */
|
|
180
234
|
export function sha256Hex(content) {
|
|
181
235
|
return crypto.createHash("sha256").update(content, "utf8").digest("hex");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-signing.js","sourceRoot":"","sources":["../../src/runtime/approval-signing.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,oBAAoB;AACpB,EAAE;AACF,uEAAuE;AACvE,4EAA4E;AAC5E,6EAA6E;AAC7E,mEAAmE;AACnE,0EAA0E;AAC1E,0EAA0E;AAC1E,wEAAwE;AACxE,wEAAwE;AACxE,4EAA4E;AAC5E,qEAAqE;AACrE,0EAA0E;AAC1E,0EAA0E;AAC1E,4EAA4E;AAC5E,2EAA2E;AAC3E,sDAAsD;AACtD,EAAE;AACF,wEAAwE;AACxE,EAAE;AACF,uEAAuE;AACvE,yEAAyE;AACzE,wEAAwE;AACxE,iDAAiD;AACjD,kEAAkE;AAClE,yEAAyE;AACzE,yEAAyE;AACzE,yEAAyE;AACzE,0EAA0E;AAC1E,yEAAyE;AACzE,0EAA0E;AAC1E,wEAAwE;AACxE,4EAA4E;AAC5E,0EAA0E;AAC1E,wEAAwE;AACxE,wEAAwE;AACxE,0EAA0E;AAC1E,2EAA2E;AAC3E,uEAAuE;AACvE,uEAAuE;AACvE,yEAAyE;AACzE,0EAA0E;AAC1E,oEAAoE;AACpE,uEAAuE;AACvE,qEAAqE;AACrE,2EAA2E;AAC3E,wEAAwE;AACxE,gBAAgB;AAChB,EAAE;AACF,sEAAsE;AACtE,wEAAwE;AACxE,uEAAuE;AACvE,
|
|
1
|
+
{"version":3,"file":"approval-signing.js","sourceRoot":"","sources":["../../src/runtime/approval-signing.ts"],"names":[],"mappings":"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"}
|
|
@@ -216,6 +216,22 @@ export interface CommandSegment {
|
|
|
216
216
|
*/
|
|
217
217
|
effectiveTarget: string | null;
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* The minimal shape `peelWrapperPrefixes` and every individual `peelX`
|
|
221
|
+
* wrapper-peeler actually need — `.text` only, never `.start`/`.end`
|
|
222
|
+
* (task d03af8f6, review round 3). Exported so
|
|
223
|
+
* `src/runtime/deletion-target-resolve.ts` can hand these SAME peelers a
|
|
224
|
+
* decoded token array of its own (built from ITS quote-aware tokenizer,
|
|
225
|
+
* not this module's `tokenizeWithOffsets`) without fabricating fake
|
|
226
|
+
* offsets: `Token` (with real offsets) is structurally assignable to
|
|
227
|
+
* `WrapperPeelToken[]` already, so `canonicalizeSegment`'s own call sites
|
|
228
|
+
* below need no change at all — this is a widening of the peelers'
|
|
229
|
+
* PARAMETER types only, not a behaviour change (none of them ever read
|
|
230
|
+
* `.start`/`.end`).
|
|
231
|
+
*/
|
|
232
|
+
export interface WrapperPeelToken {
|
|
233
|
+
text: string;
|
|
234
|
+
}
|
|
219
235
|
/**
|
|
220
236
|
* The `git` token itself, matched by BASENAME so a path-qualified
|
|
221
237
|
* invocation (`/usr/bin/git`, `./git`) is recognised too (F4 fix).
|
|
@@ -384,6 +400,29 @@ export declare function normalizeCommand(command: string): NormalizedCommand;
|
|
|
384
400
|
* of this function at all.
|
|
385
401
|
*/
|
|
386
402
|
export declare function segmentViewOf(command: string): CommandSegment[] | null;
|
|
403
|
+
/**
|
|
404
|
+
* Ampersand-aware sibling of `segmentViewOf` (task d03af8f6, review
|
|
405
|
+
* round 3): the same per-segment view, but segmented under
|
|
406
|
+
* `AMP_BOUNDARY_RE` (`BOUNDARY_RE` plus a bare `&`) instead of
|
|
407
|
+
* `BOUNDARY_RE` — closing the same bare-`&` gap `normalizeCommandAmpAware`
|
|
408
|
+
* closes for trigger matching (`echo hi & rm -rf /home/x`), but as a
|
|
409
|
+
* segment VIEW rather than a rejoined string, for
|
|
410
|
+
* `src/runtime/deletion-target-resolve.ts`'s multi-segment scan. Uses the
|
|
411
|
+
* SAME shared walk (`segmentAndCanonicalize`, parameterised on
|
|
412
|
+
* `boundaryRe` since task aabbad63) as every other pass in this module —
|
|
413
|
+
* no new segmentation logic. `CommandSegment.effectiveTarget`'s own
|
|
414
|
+
* composition rules are UNCHANGED by which alphabet drove the split; a
|
|
415
|
+
* caller that only needs `.text` per segment (as the deletion resolver
|
|
416
|
+
* does) is unaffected by the same directory-extraction reliability
|
|
417
|
+
* caveat `normalizeCommandAmpAware`'s own comment names for `targetDir`/
|
|
418
|
+
* `targetBase` — this function returns the FULL `CommandSegment` shape,
|
|
419
|
+
* `effectiveTarget` included, so a future caller relying on THAT field
|
|
420
|
+
* under this alphabet should read `normalizeCommandAmpAware`'s comment
|
|
421
|
+
* first. `null` under the same `MAX_NORMALIZE_LENGTH` condition
|
|
422
|
+
* `segmentViewOf` uses; `[]` for an empty/non-string command or the
|
|
423
|
+
* never-throws fallback.
|
|
424
|
+
*/
|
|
425
|
+
export declare function segmentViewOfAmpAware(command: string): CommandSegment[] | null;
|
|
387
426
|
/**
|
|
388
427
|
* Return type of `normalizeCommandAmpAware` — deliberately NOT
|
|
389
428
|
* `NormalizedCommand` (task aabbad63, hard constraint). It carries only
|
|
@@ -544,3 +583,76 @@ export interface QuoteAwareNormalizedCommand {
|
|
|
544
583
|
* "not a formal proof" caveat.
|
|
545
584
|
*/
|
|
546
585
|
export declare function normalizeCommandQuoteAware(command: string): QuoteAwareNormalizedCommand;
|
|
586
|
+
/** Return type of `peelWrapperPrefixes` — see that function's own comment. */
|
|
587
|
+
export interface WrapperPeelResult {
|
|
588
|
+
/** Index of the first token after every recognised wrapper prefix this
|
|
589
|
+
* loop peeled — the wrapped invocation's own head token, or (if
|
|
590
|
+
* `tokens.length`) no head token survived at all. */
|
|
591
|
+
idx: number;
|
|
592
|
+
/**
|
|
593
|
+
* The `env -C <dir>` / `--chdir <dir>` target directory, when this
|
|
594
|
+
* chain peeled an `env` wrapper naming one (excluding a `~`-prefixed
|
|
595
|
+
* value — F5, treated as if absent). `null` when no `env -C`/`--chdir`
|
|
596
|
+
* was peeled. Consumed only by `canonicalizeSegment`'s own
|
|
597
|
+
* `targetDir`/`targetBase` bookkeeping —
|
|
598
|
+
* `src/runtime/deletion-target-resolve.ts` (the other caller, task
|
|
599
|
+
* d03af8f6, review round 3) has no analogous need and ignores it.
|
|
600
|
+
*/
|
|
601
|
+
envTargetDir: string | null;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Peel a leading wrapper-command chain: `VAR=value` tokens (any number,
|
|
605
|
+
* in sequence), `env` (+ its own flags/assignments, capturing `-C`/
|
|
606
|
+
* `--chdir`), `command` (+ its own flags), `nice` (+ its own flags),
|
|
607
|
+
* `sudo` / `doas` / `time` / `timeout` / `stdbuf` / `setsid` / `exec` /
|
|
608
|
+
* `nohup` (+ each one's own flags — F4 fix; `exec`/`nohup` added task
|
|
609
|
+
* d03af8f6, review round 3). Bounded by `tokens.length` so a
|
|
610
|
+
* pathological input cannot spin. Returns the cursor position of
|
|
611
|
+
* whatever token (if any) survives the peel — the caller decides what to
|
|
612
|
+
* do with it; this function never itself inspects the survivor.
|
|
613
|
+
*
|
|
614
|
+
* SHARED (task d03af8f6, review round 3) between `canonicalizeSegment`
|
|
615
|
+
* below — this module's own `git`/`gh`/`npm`/`harness` trigger
|
|
616
|
+
* recognition — and `src/runtime/deletion-target-resolve.ts`'s deletion-
|
|
617
|
+
* verb recognition (`rm`/`find`/`git clean`), which used to hand-roll an
|
|
618
|
+
* independent, narrower copy of this exact peeling vocabulary
|
|
619
|
+
* (`peelWrapperHeads`, missing `-u`/`-i`/`-C` on `env`, the duration-
|
|
620
|
+
* consuming shape of `timeout`, `nice`'s glued/`--adjustment=` forms,
|
|
621
|
+
* and `exec`/`nohup` entirely). Reusing this ONE loop instead of
|
|
622
|
+
* maintaining two means a future wrapper added here (or a flag fix to an
|
|
623
|
+
* existing one) benefits both gates automatically, and
|
|
624
|
+
* `npm run check:duplication`'s clone-count fitness function has one
|
|
625
|
+
* fewer near-duplicate block to flag. `deletion-target-resolve.ts` hands
|
|
626
|
+
* this a decoded token array built from ITS OWN quote-aware tokenizer
|
|
627
|
+
* (`WrapperPeelToken[]`, `.text` only — see that interface's own
|
|
628
|
+
* comment for why the offset-carrying `Token[]` this module's own
|
|
629
|
+
* `tokenizeWithOffsets` produces is not required here), then layers its
|
|
630
|
+
* own `xargs` handling on top (this loop deliberately does NOT recognise
|
|
631
|
+
* `xargs` — see the module header's NOT-SUPPORTED list for why `xargs`
|
|
632
|
+
* is architecturally different from every wrapper peeled here: its own
|
|
633
|
+
* argv is not simply "the command to run").
|
|
634
|
+
*/
|
|
635
|
+
export declare function peelWrapperPrefixes(tokens: readonly WrapperPeelToken[], startIdx?: number): WrapperPeelResult;
|
|
636
|
+
/**
|
|
637
|
+
* Generic "peel this wrapper's own flags" loop shared by the small,
|
|
638
|
+
* bounded-vocabulary runners added for F4 (`sudo`, `doas`, `time`,
|
|
639
|
+
* `stdbuf`, `setsid`, `exec`, `nohup`): consume `--`, any flag in
|
|
640
|
+
* `valueFlags` (+ its following token), any glued short/long form
|
|
641
|
+
* matching `gluedRe`, then any OTHER `-`-prefixed token treated as
|
|
642
|
+
* boolean (mirrors `peelCommand` above — safe because an unrecognised
|
|
643
|
+
* flag is either genuinely boolean, in which case this is correct, or
|
|
644
|
+
* value-taking, in which case its VALUE token is left for the next
|
|
645
|
+
* iteration and — not being `git`, a known wrapper, or another flag —
|
|
646
|
+
* simply causes a clean, safe bail-out a token late rather than a false
|
|
647
|
+
* match). Does NOT capture flag VALUES: none of these wrappers have a
|
|
648
|
+
* "run in a different directory" semantic this module tracks as a
|
|
649
|
+
* target directory — only `env -C`/`--chdir` does, and that stays
|
|
650
|
+
* hand-written in `peelEnv`. Exported (task d03af8f6, review round 3) so
|
|
651
|
+
* `src/runtime/deletion-target-resolve.ts` can reuse this SAME loop for
|
|
652
|
+
* `xargs`'s own flags (`-0`, `-n1`, ...) instead of a third
|
|
653
|
+
* independently-drifting copy — that module is the reason this needs no
|
|
654
|
+
* value-flag vocabulary of its own either: `xargs`'s own value-taking
|
|
655
|
+
* flags are not modelled there, matching this function's existing
|
|
656
|
+
* effort level for the boolean-only wrappers above.
|
|
657
|
+
*/
|
|
658
|
+
export declare function peelGenericFlags(tokens: readonly WrapperPeelToken[], startIdx: number, valueFlags: ReadonlySet<string>, gluedRe: RegExp | null): number;
|
|
@@ -374,16 +374,29 @@
|
|
|
374
374
|
// - `$(...)` command substitution is NOT in this list — see the
|
|
375
375
|
// "accidentally covered" note right below. Do not read its absence
|
|
376
376
|
// here as "also open"; it genuinely blocks today, pinned by a test.
|
|
377
|
-
// -
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
377
|
+
// - CLOSED (task d03af8f6, review round 3): `exec git status` and
|
|
378
|
+
// `nohup git status` used to sit in this list. Both are now peeled
|
|
379
|
+
// the same boolean-flag-only way as `setsid` (`peelExec`/`peelNohup`,
|
|
380
|
+
// part of the shared `peelWrapperPrefixes` loop) — added while
|
|
381
|
+
// rebuilding `src/runtime/deletion-target-resolve.ts`'s own
|
|
382
|
+
// recognition on top of this module's peelers so the deletion gate
|
|
383
|
+
// could see past `exec rm -rf ...` / `nohup rm -rf ... &`, and added
|
|
384
|
+
// HERE (not only there) so this module's own `git`/`gh`/`npm`/
|
|
385
|
+
// `harness` trigger recognition benefits too, per the orchestrator
|
|
386
|
+
// decision for that task. `exec`'s own rare value-taking flag (`-a
|
|
387
|
+
// name`) is NOT modelled — treated as boolean like every other
|
|
388
|
+
// unrecognised flag this loop's generic branch consumes, the same
|
|
389
|
+
// approximation `setsid`'s three flags already use.
|
|
390
|
+
// - Eight more measured-bypass spellings, none of them peeled wrapper
|
|
391
|
+
// prefixes or a recognised `git` spelling: `ionice -c3 git status`,
|
|
392
|
+
// `flock /tmp/l git status`, `script -q /dev/null git status`, `chrt
|
|
393
|
+
// -b 0 git status`, `taskset -c 0 git status`, `\git status` (the
|
|
394
|
+
// backslash defeats `GIT_TOKEN_RE`, which requires the bare
|
|
395
|
+
// basename), `"git" status` (a QUOTED binary name — the mirror image
|
|
396
|
+
// of the quoted-subcommand gap above), and `env -S "git status"` (the
|
|
397
|
+
// split-string flag hands the rest of the argv to a fresh, opaque
|
|
398
|
+
// re-parse this module deliberately does not follow, per `peelEnv`'s
|
|
399
|
+
// own comment).
|
|
387
400
|
// - `gh`'s / `npm`'s own tool-specific flags inserted BETWEEN the head
|
|
388
401
|
// token and its subcommand: `gh -R owner/repo pr merge`, `gh --repo
|
|
389
402
|
// owner/repo pr create`, `npm --loglevel=silent publish`, `npm
|
|
@@ -913,6 +926,40 @@ export function segmentViewOf(command) {
|
|
|
913
926
|
return [];
|
|
914
927
|
}
|
|
915
928
|
}
|
|
929
|
+
/**
|
|
930
|
+
* Ampersand-aware sibling of `segmentViewOf` (task d03af8f6, review
|
|
931
|
+
* round 3): the same per-segment view, but segmented under
|
|
932
|
+
* `AMP_BOUNDARY_RE` (`BOUNDARY_RE` plus a bare `&`) instead of
|
|
933
|
+
* `BOUNDARY_RE` — closing the same bare-`&` gap `normalizeCommandAmpAware`
|
|
934
|
+
* closes for trigger matching (`echo hi & rm -rf /home/x`), but as a
|
|
935
|
+
* segment VIEW rather than a rejoined string, for
|
|
936
|
+
* `src/runtime/deletion-target-resolve.ts`'s multi-segment scan. Uses the
|
|
937
|
+
* SAME shared walk (`segmentAndCanonicalize`, parameterised on
|
|
938
|
+
* `boundaryRe` since task aabbad63) as every other pass in this module —
|
|
939
|
+
* no new segmentation logic. `CommandSegment.effectiveTarget`'s own
|
|
940
|
+
* composition rules are UNCHANGED by which alphabet drove the split; a
|
|
941
|
+
* caller that only needs `.text` per segment (as the deletion resolver
|
|
942
|
+
* does) is unaffected by the same directory-extraction reliability
|
|
943
|
+
* caveat `normalizeCommandAmpAware`'s own comment names for `targetDir`/
|
|
944
|
+
* `targetBase` — this function returns the FULL `CommandSegment` shape,
|
|
945
|
+
* `effectiveTarget` included, so a future caller relying on THAT field
|
|
946
|
+
* under this alphabet should read `normalizeCommandAmpAware`'s comment
|
|
947
|
+
* first. `null` under the same `MAX_NORMALIZE_LENGTH` condition
|
|
948
|
+
* `segmentViewOf` uses; `[]` for an empty/non-string command or the
|
|
949
|
+
* never-throws fallback.
|
|
950
|
+
*/
|
|
951
|
+
export function segmentViewOfAmpAware(command) {
|
|
952
|
+
if (typeof command !== "string" || command.length === 0)
|
|
953
|
+
return [];
|
|
954
|
+
if (command.length > MAX_NORMALIZE_LENGTH)
|
|
955
|
+
return null;
|
|
956
|
+
try {
|
|
957
|
+
return segmentAndCanonicalize(command, AMP_BOUNDARY_RE, true).segments ?? [];
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
}
|
|
916
963
|
/**
|
|
917
964
|
* Ampersand-aware SECOND normalisation pass (task aabbad63). Segments the
|
|
918
965
|
* command under `AMP_BOUNDARY_RE` (`BOUNDARY_RE` plus a bare `&`) instead
|
|
@@ -1644,41 +1691,40 @@ function tokenizeWithOffsets(s) {
|
|
|
1644
1691
|
return out;
|
|
1645
1692
|
}
|
|
1646
1693
|
/**
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
1656
|
-
*
|
|
1657
|
-
*
|
|
1658
|
-
*
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1661
|
-
*
|
|
1694
|
+
* Peel a leading wrapper-command chain: `VAR=value` tokens (any number,
|
|
1695
|
+
* in sequence), `env` (+ its own flags/assignments, capturing `-C`/
|
|
1696
|
+
* `--chdir`), `command` (+ its own flags), `nice` (+ its own flags),
|
|
1697
|
+
* `sudo` / `doas` / `time` / `timeout` / `stdbuf` / `setsid` / `exec` /
|
|
1698
|
+
* `nohup` (+ each one's own flags — F4 fix; `exec`/`nohup` added task
|
|
1699
|
+
* d03af8f6, review round 3). Bounded by `tokens.length` so a
|
|
1700
|
+
* pathological input cannot spin. Returns the cursor position of
|
|
1701
|
+
* whatever token (if any) survives the peel — the caller decides what to
|
|
1702
|
+
* do with it; this function never itself inspects the survivor.
|
|
1703
|
+
*
|
|
1704
|
+
* SHARED (task d03af8f6, review round 3) between `canonicalizeSegment`
|
|
1705
|
+
* below — this module's own `git`/`gh`/`npm`/`harness` trigger
|
|
1706
|
+
* recognition — and `src/runtime/deletion-target-resolve.ts`'s deletion-
|
|
1707
|
+
* verb recognition (`rm`/`find`/`git clean`), which used to hand-roll an
|
|
1708
|
+
* independent, narrower copy of this exact peeling vocabulary
|
|
1709
|
+
* (`peelWrapperHeads`, missing `-u`/`-i`/`-C` on `env`, the duration-
|
|
1710
|
+
* consuming shape of `timeout`, `nice`'s glued/`--adjustment=` forms,
|
|
1711
|
+
* and `exec`/`nohup` entirely). Reusing this ONE loop instead of
|
|
1712
|
+
* maintaining two means a future wrapper added here (or a flag fix to an
|
|
1713
|
+
* existing one) benefits both gates automatically, and
|
|
1714
|
+
* `npm run check:duplication`'s clone-count fitness function has one
|
|
1715
|
+
* fewer near-duplicate block to flag. `deletion-target-resolve.ts` hands
|
|
1716
|
+
* this a decoded token array built from ITS OWN quote-aware tokenizer
|
|
1717
|
+
* (`WrapperPeelToken[]`, `.text` only — see that interface's own
|
|
1718
|
+
* comment for why the offset-carrying `Token[]` this module's own
|
|
1719
|
+
* `tokenizeWithOffsets` produces is not required here), then layers its
|
|
1720
|
+
* own `xargs` handling on top (this loop deliberately does NOT recognise
|
|
1721
|
+
* `xargs` — see the module header's NOT-SUPPORTED list for why `xargs`
|
|
1722
|
+
* is architecturally different from every wrapper peeled here: its own
|
|
1723
|
+
* argv is not simply "the command to run").
|
|
1662
1724
|
*/
|
|
1663
|
-
function
|
|
1664
|
-
|
|
1665
|
-
if (tokens.length === 0) {
|
|
1666
|
-
return {
|
|
1667
|
-
text: segmentText,
|
|
1668
|
-
targetDir: null,
|
|
1669
|
-
targetBase: null,
|
|
1670
|
-
identityTargetDir: null,
|
|
1671
|
-
identityTargetBase: null,
|
|
1672
|
-
isGit: false,
|
|
1673
|
-
};
|
|
1674
|
-
}
|
|
1675
|
-
let idx = 0;
|
|
1725
|
+
export function peelWrapperPrefixes(tokens, startIdx = 0) {
|
|
1726
|
+
let idx = startIdx;
|
|
1676
1727
|
let envTargetDir = null;
|
|
1677
|
-
// Peel wrapper prefixes: VAR=value tokens, `env` (+ its own flags/
|
|
1678
|
-
// assignments), `command` (+ its own flags), `nice` (+ its own
|
|
1679
|
-
// flags), `sudo` / `doas` / `time` / `timeout` / `stdbuf` / `setsid`
|
|
1680
|
-
// (+ each one's own flags — F4 fix). Bounded by tokens.length so a
|
|
1681
|
-
// pathological input cannot spin.
|
|
1682
1728
|
for (let guard = 0; guard < tokens.length; guard++) {
|
|
1683
1729
|
const head = tokens[idx]?.text;
|
|
1684
1730
|
if (head === undefined)
|
|
@@ -1726,8 +1772,50 @@ function canonicalizeSegment(segmentText) {
|
|
|
1726
1772
|
idx = peelSetsid(tokens, idx + 1);
|
|
1727
1773
|
continue;
|
|
1728
1774
|
}
|
|
1775
|
+
if (head === "exec") {
|
|
1776
|
+
idx = peelExec(tokens, idx + 1);
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
if (head === "nohup") {
|
|
1780
|
+
idx = peelNohup(tokens, idx + 1);
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1729
1783
|
break;
|
|
1730
1784
|
}
|
|
1785
|
+
return { idx, envTargetDir };
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Rewrite one boundary-delimited segment: peel wrapper prefixes to look
|
|
1789
|
+
* for an invocation of one of the closed head-token set's four members
|
|
1790
|
+
* (`git`, `gh`, `npm`, `harness` — D-001, run
|
|
1791
|
+
* 2026-07-28-nongit-trigger-wrappers), and — ONLY if one is found —
|
|
1792
|
+
* canonicalise it. `git` is canonicalised to `git <subcommand>` with its
|
|
1793
|
+
* OWN global options dropped, same as before this run. The other three
|
|
1794
|
+
* are canonicalised by wrapper peeling plus whitespace collapsing across
|
|
1795
|
+
* the REST OF THE SEGMENT (fix round 2, finding F2 — rejoining every
|
|
1796
|
+
* token from the head onward with a single space, not just the head-to-
|
|
1797
|
+
* next-token gap), no option-dropping (see the module header). When no
|
|
1798
|
+
* recognised head token is found (wrong binary, ran out of tokens, or a
|
|
1799
|
+
* malformed git global option), the segment is returned COMPLETELY
|
|
1800
|
+
* UNCHANGED: peeling is tentative, and nothing is stripped from a
|
|
1801
|
+
* segment that turns out not to be a recognised call (so `digit=1 foo`,
|
|
1802
|
+
* `env -C X ls`, `ghx pr merge`, etc. are never touched).
|
|
1803
|
+
*/
|
|
1804
|
+
function canonicalizeSegment(segmentText) {
|
|
1805
|
+
const tokens = tokenizeWithOffsets(segmentText);
|
|
1806
|
+
if (tokens.length === 0) {
|
|
1807
|
+
return {
|
|
1808
|
+
text: segmentText,
|
|
1809
|
+
targetDir: null,
|
|
1810
|
+
targetBase: null,
|
|
1811
|
+
identityTargetDir: null,
|
|
1812
|
+
identityTargetBase: null,
|
|
1813
|
+
isGit: false,
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
const peeled = peelWrapperPrefixes(tokens);
|
|
1817
|
+
let idx = peeled.idx;
|
|
1818
|
+
const envTargetDir = peeled.envTargetDir;
|
|
1731
1819
|
const headTok = tokens[idx]?.text;
|
|
1732
1820
|
if (headTok === undefined) {
|
|
1733
1821
|
return {
|
|
@@ -2006,19 +2094,26 @@ function peelNice(tokens, startIdx) {
|
|
|
2006
2094
|
/**
|
|
2007
2095
|
* Generic "peel this wrapper's own flags" loop shared by the small,
|
|
2008
2096
|
* bounded-vocabulary runners added for F4 (`sudo`, `doas`, `time`,
|
|
2009
|
-
* `stdbuf`, `setsid`): consume `--`, any flag in
|
|
2010
|
-
* following token), any glued short/long form
|
|
2011
|
-
* any OTHER `-`-prefixed token treated as
|
|
2012
|
-
* above — safe because an unrecognised
|
|
2013
|
-
* in which case this is correct, or
|
|
2014
|
-
* VALUE token is left for the next
|
|
2015
|
-
* known wrapper, or another flag —
|
|
2016
|
-
* a token late rather than a false
|
|
2017
|
-
* VALUES: none of these
|
|
2018
|
-
* directory" semantic this module tracks as a
|
|
2019
|
-
* `env -C`/`--chdir` does, and that stays
|
|
2097
|
+
* `stdbuf`, `setsid`, `exec`, `nohup`): consume `--`, any flag in
|
|
2098
|
+
* `valueFlags` (+ its following token), any glued short/long form
|
|
2099
|
+
* matching `gluedRe`, then any OTHER `-`-prefixed token treated as
|
|
2100
|
+
* boolean (mirrors `peelCommand` above — safe because an unrecognised
|
|
2101
|
+
* flag is either genuinely boolean, in which case this is correct, or
|
|
2102
|
+
* value-taking, in which case its VALUE token is left for the next
|
|
2103
|
+
* iteration and — not being `git`, a known wrapper, or another flag —
|
|
2104
|
+
* simply causes a clean, safe bail-out a token late rather than a false
|
|
2105
|
+
* match). Does NOT capture flag VALUES: none of these wrappers have a
|
|
2106
|
+
* "run in a different directory" semantic this module tracks as a
|
|
2107
|
+
* target directory — only `env -C`/`--chdir` does, and that stays
|
|
2108
|
+
* hand-written in `peelEnv`. Exported (task d03af8f6, review round 3) so
|
|
2109
|
+
* `src/runtime/deletion-target-resolve.ts` can reuse this SAME loop for
|
|
2110
|
+
* `xargs`'s own flags (`-0`, `-n1`, ...) instead of a third
|
|
2111
|
+
* independently-drifting copy — that module is the reason this needs no
|
|
2112
|
+
* value-flag vocabulary of its own either: `xargs`'s own value-taking
|
|
2113
|
+
* flags are not modelled there, matching this function's existing
|
|
2114
|
+
* effort level for the boolean-only wrappers above.
|
|
2020
2115
|
*/
|
|
2021
|
-
function peelGenericFlags(tokens, startIdx, valueFlags, gluedRe) {
|
|
2116
|
+
export function peelGenericFlags(tokens, startIdx, valueFlags, gluedRe) {
|
|
2022
2117
|
let idx = startIdx;
|
|
2023
2118
|
while (idx < tokens.length) {
|
|
2024
2119
|
const t = tokens[idx].text;
|
|
@@ -2108,6 +2203,25 @@ function peelStdbuf(tokens, startIdx) {
|
|
|
2108
2203
|
function peelSetsid(tokens, startIdx) {
|
|
2109
2204
|
return peelGenericFlags(tokens, startIdx, new Set(), null);
|
|
2110
2205
|
}
|
|
2206
|
+
/**
|
|
2207
|
+
* `exec`'s own flags (bash builtin: `-a name`, `-c`, `-l`) are rare
|
|
2208
|
+
* enough that this module treats it as a boolean-only wrapper like
|
|
2209
|
+
* `setsid` above — an unrecognised flag (including `-a`, whose VALUE
|
|
2210
|
+
* this does not capture) simply consumes one token via
|
|
2211
|
+
* `peelGenericFlags`'s generic `-flag` branch (task d03af8f6, review
|
|
2212
|
+
* round 3 — closes the `exec git status` / `exec rm -rf ...` bypass
|
|
2213
|
+
* named in the module header's NOT-SUPPORTED list).
|
|
2214
|
+
*/
|
|
2215
|
+
function peelExec(tokens, startIdx) {
|
|
2216
|
+
return peelGenericFlags(tokens, startIdx, new Set(), null);
|
|
2217
|
+
}
|
|
2218
|
+
/** `nohup` takes no flags of its own (GNU coreutils: only `--help`/`--version`,
|
|
2219
|
+
* neither of which precedes a real wrapped command). Boolean-only, same
|
|
2220
|
+
* shape as `peelSetsid`/`peelExec` (task d03af8f6, review round 3 — closes
|
|
2221
|
+
* the `nohup git status` / `nohup rm -rf ... &` bypass). */
|
|
2222
|
+
function peelNohup(tokens, startIdx) {
|
|
2223
|
+
return peelGenericFlags(tokens, startIdx, new Set(), null);
|
|
2224
|
+
}
|
|
2111
2225
|
/** Strip a path that ends in a `.git` directory down to its parent. */
|
|
2112
2226
|
function parentIfDotGit(dir) {
|
|
2113
2227
|
const trimmed = dir.replace(/\/+$/, "");
|