@webpieces/ai-hook-rules 0.4.579 → 0.4.581
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/ai-hook-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.581",
|
|
4
4
|
"description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "packages/tooling/ai-hook-rules"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@webpieces/rules-config": "0.4.
|
|
28
|
+
"@webpieces/rules-config": "0.4.581"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|
|
@@ -79,6 +79,38 @@ export declare class EffectiveTreeResolver {
|
|
|
79
79
|
* so the quoted `cd` is never picked up and cannot be weaponised into a scope escape.
|
|
80
80
|
*/
|
|
81
81
|
effectiveCwd(command: string, shellCwd: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* WHY a `cd` the agent wrote did not move the effective cwd — or null when there is nothing to
|
|
84
|
+
* explain. Pure diagnosis for the block MESSAGE; it changes no verdict and relaxes nothing.
|
|
85
|
+
*
|
|
86
|
+
* The two shapes below both degrade to "judge against the governed root" (the safe direction) and
|
|
87
|
+
* both look, from the agent's side, exactly like a `cd` that was ignored. A guard that then says
|
|
88
|
+
* "cd into it first" is describing a remedy the agent has ALREADY performed, so it cannot discover
|
|
89
|
+
* the real cause: in the session that motivated this, the agent concluded the exemption was broken
|
|
90
|
+
* and wrote a persistent memory entry asserting `cd` must be its own Bash call. Neither is true.
|
|
91
|
+
* Naming the precondition here is what makes the hint self-correcting.
|
|
92
|
+
*
|
|
93
|
+
* `cd "$DIR" && git push` — the target is not a literal path. `path.resolve(cwd, '$DIR')` is
|
|
94
|
+
* a directory that does not exist, not the tree that was meant.
|
|
95
|
+
* `D=/x; cd "$D"; git push` — a bare `VAR=value` segment tokenizes to NO words (CommandScanner
|
|
96
|
+
* strips assignments as command prefixes), so the leading run ends
|
|
97
|
+
* before the `cd` is ever reached.
|
|
98
|
+
* `git fetch && cd /x && git push` — the leading run ended at `git fetch`. Bash really does run
|
|
99
|
+
* the push inside /x; the guard judges it from the shell cwd. Same
|
|
100
|
+
* false positive as the two above, just wearing a different shape.
|
|
101
|
+
*
|
|
102
|
+
* ONE thing is silent: a `cd` with NO work after it (`git push && cd /x`). Nothing ran in the new
|
|
103
|
+
* directory, so the verdict on what DID run is simply correct, and "put the cd in front" would
|
|
104
|
+
* change what the command does rather than where it is judged.
|
|
105
|
+
*
|
|
106
|
+
* The rule the message must land, in every one of these: only `cd <literal path> && <work>`
|
|
107
|
+
* relocates the verdict. Everything else fails CLOSED — judged from the shell cwd — which is why
|
|
108
|
+
* an unresolvable `cd` is a message defect and never a hole. Expanding `$VAR` here, or resuming the
|
|
109
|
+
* scan after a real command, are both the fix NOT taken: the resolver decides ONE location for the
|
|
110
|
+
* whole line, so a `cd` that is allowed to count retroactively is exactly the `… && cd <exempt-tree>`
|
|
111
|
+
* scope escape.
|
|
112
|
+
*/
|
|
113
|
+
unresolvedCd(command: string): string | null;
|
|
82
114
|
private classify;
|
|
83
115
|
private owningWorktree;
|
|
84
116
|
/** Is `dir` the directory `root` itself, or somewhere beneath it? Pure path math, no filesystem. */
|
|
@@ -66,6 +66,64 @@ class EffectiveTreeResolver {
|
|
|
66
66
|
}
|
|
67
67
|
return effective;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* WHY a `cd` the agent wrote did not move the effective cwd — or null when there is nothing to
|
|
71
|
+
* explain. Pure diagnosis for the block MESSAGE; it changes no verdict and relaxes nothing.
|
|
72
|
+
*
|
|
73
|
+
* The two shapes below both degrade to "judge against the governed root" (the safe direction) and
|
|
74
|
+
* both look, from the agent's side, exactly like a `cd` that was ignored. A guard that then says
|
|
75
|
+
* "cd into it first" is describing a remedy the agent has ALREADY performed, so it cannot discover
|
|
76
|
+
* the real cause: in the session that motivated this, the agent concluded the exemption was broken
|
|
77
|
+
* and wrote a persistent memory entry asserting `cd` must be its own Bash call. Neither is true.
|
|
78
|
+
* Naming the precondition here is what makes the hint self-correcting.
|
|
79
|
+
*
|
|
80
|
+
* `cd "$DIR" && git push` — the target is not a literal path. `path.resolve(cwd, '$DIR')` is
|
|
81
|
+
* a directory that does not exist, not the tree that was meant.
|
|
82
|
+
* `D=/x; cd "$D"; git push` — a bare `VAR=value` segment tokenizes to NO words (CommandScanner
|
|
83
|
+
* strips assignments as command prefixes), so the leading run ends
|
|
84
|
+
* before the `cd` is ever reached.
|
|
85
|
+
* `git fetch && cd /x && git push` — the leading run ended at `git fetch`. Bash really does run
|
|
86
|
+
* the push inside /x; the guard judges it from the shell cwd. Same
|
|
87
|
+
* false positive as the two above, just wearing a different shape.
|
|
88
|
+
*
|
|
89
|
+
* ONE thing is silent: a `cd` with NO work after it (`git push && cd /x`). Nothing ran in the new
|
|
90
|
+
* directory, so the verdict on what DID run is simply correct, and "put the cd in front" would
|
|
91
|
+
* change what the command does rather than where it is judged.
|
|
92
|
+
*
|
|
93
|
+
* The rule the message must land, in every one of these: only `cd <literal path> && <work>`
|
|
94
|
+
* relocates the verdict. Everything else fails CLOSED — judged from the shell cwd — which is why
|
|
95
|
+
* an unresolvable `cd` is a message defect and never a hole. Expanding `$VAR` here, or resuming the
|
|
96
|
+
* scan after a real command, are both the fix NOT taken: the resolver decides ONE location for the
|
|
97
|
+
* whole line, so a `cd` that is allowed to count retroactively is exactly the `… && cd <exempt-tree>`
|
|
98
|
+
* scope escape.
|
|
99
|
+
*/
|
|
100
|
+
unresolvedCd(command) {
|
|
101
|
+
const segments = this.scanner.commandSegments(command).map((segment) => this.shell.effectiveWords(segment));
|
|
102
|
+
// The leading run effectiveCwd() actually consumed — a `cd` at or after this index did not count.
|
|
103
|
+
let consumed = 0;
|
|
104
|
+
while (consumed < segments.length && isCd(segments[consumed]))
|
|
105
|
+
consumed++;
|
|
106
|
+
const reasons = [];
|
|
107
|
+
const add = (reason) => { if (!reasons.includes(reason))
|
|
108
|
+
reasons.push(reason); };
|
|
109
|
+
for (let i = 0; i < segments.length; i++) {
|
|
110
|
+
if (!isCd(segments[i]))
|
|
111
|
+
continue;
|
|
112
|
+
// A `cd` with nothing left to run after it changed nothing — see the header.
|
|
113
|
+
if (!segments.slice(i + 1).some(isRealCommand))
|
|
114
|
+
continue;
|
|
115
|
+
if (i >= consumed) {
|
|
116
|
+
add(segments.slice(0, i).some(isRealCommand)
|
|
117
|
+
? 'another command precedes it, and only a `cd` at the FRONT of the line counts'
|
|
118
|
+
: 'a `VAR=…` assignment precedes it, which ends the scan');
|
|
119
|
+
}
|
|
120
|
+
const target = segments[i][1];
|
|
121
|
+
if (target !== undefined && VARIABLE_TARGET.test(target)) {
|
|
122
|
+
add('its target is not a literal path (a `$VAR`, `~` or `$(…)` the guard cannot expand)');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return reasons.length === 0 ? null : reasons.join(', and ');
|
|
126
|
+
}
|
|
69
127
|
classify(effectiveCwd, governedRoot) {
|
|
70
128
|
// Fast path — no `cd`, or a `cd` within the governed tree. No worktree enumeration needed, and
|
|
71
129
|
// the nested-clone check keeps its exact previous behaviour.
|
|
@@ -107,6 +165,20 @@ class EffectiveTreeResolver {
|
|
|
107
165
|
}
|
|
108
166
|
}
|
|
109
167
|
exports.EffectiveTreeResolver = EffectiveTreeResolver;
|
|
168
|
+
// The two segment shapes unresolvedCd() sorts by. A segment with NO words at all is a bare `VAR=value`
|
|
169
|
+
// assignment (CommandScanner strips assignments as command prefixes), which is neither.
|
|
170
|
+
// webpieces-disable no-function-outside-class -- pure predicates over one segment's words, siblings of the module-scope helpers below
|
|
171
|
+
function isCd(words) {
|
|
172
|
+
return words[0] === 'cd' || words[0] === 'pushd';
|
|
173
|
+
}
|
|
174
|
+
// webpieces-disable no-function-outside-class -- sibling of isCd()
|
|
175
|
+
function isRealCommand(words) {
|
|
176
|
+
return words.length > 0 && !isCd(words);
|
|
177
|
+
}
|
|
178
|
+
// A `cd` target that is not a literal path: `$DIR`, `${DIR}`, `~`, or a `$(…)`/backtick substitution.
|
|
179
|
+
// `~` is here because path.resolve() does not expand it either — the shell does, and the hook never
|
|
180
|
+
// sees a shell.
|
|
181
|
+
const VARIABLE_TARGET = /[$`]|^~/;
|
|
110
182
|
/** Data-only carrier for the two values classify() decides together. */
|
|
111
183
|
class TreeClassification {
|
|
112
184
|
kind;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"effective-tree.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/effective-tree.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport { spawnSync } from 'child_process';\n\nimport { WorktreeService, Worktree } from '@webpieces/rules-config';\n\nimport { CommandScanner } from './command-scan';\nimport { ShellSegmentScan } from './rules/shell-segment-scan';\n\n/**\n * WHICH TREE does a Bash command actually act on? The ONE resolver every bash guard and the\n * force-to-root check share.\n *\n * WHY it has to exist at all: the shell cwd a PreToolUse hook is handed does not tell you which tree\n * the command acts on. `cd` behaves TWO different ways, and both break a cwd-based guard:\n *\n * - A `cd` that stays INSIDE the session's working directory PERSISTS to later calls. So the cwd\n * can be a subdirectory of the governed root, left there by an unrelated command several turns\n * earlier — a relative path then resolves somewhere other than the root while still being in the\n * governed tree.\n * - A `cd` that LEAVES it is reset by the harness, which says so (`Shell cwd was reset to <root>`).\n * So an agent working in a linked worktree is back in the primary clone by the next call and must\n * write self-contained `cd <worktree> && …` commands — and the cwd the hook sees is the primary\n * clone, not the worktree the command targets.\n *\n * (Measured on 2026-08-02: `cd backlog && pwd` → `…/backlog`, then a bare `pwd` in a FRESH call →\n * still `…/backlog`. But `cd ../<linked-worktree> && pwd` → the worktree, then a bare `pwd` → back at\n * the primary clone. An earlier version of this comment asserted `cd` never persists; that was the\n * worktree case generalized. The conclusion below is unchanged — only the reason was wrong.)\n *\n * Either way a guard that reasons from the raw cwd judges the wrong tree. Three field sightings in\n * one session:\n * an `ls` of a path outside every repo blocked as \"this branch is merged\"; a version-drift cure\n * (`pnpm install`) that could not be typed from the directory that needed it; and a command aimed at\n * `/private/tmp` blocked because the PRIMARY clone's main was behind — with a remedy (`git pull` in\n * the primary clone) the agent had been explicitly forbidden to run.\n *\n * Two separate copies of the cwd logic used to exist (the runner's foreign-repo/excludePaths check\n * and force-to-root). They are both this class now: two resolvers WILL disagree about which tree you\n * are in, and a guard that disagrees with the guard beside it is worse than either being wrong.\n *\n * Resolution, in order:\n * 1. `effectiveCwd` — the leading run of `cd`/`pushd` in the command itself, resolved left to right.\n * 2. If that sits under the governed root, the tree is the governed root unless git says the\n * directory belongs to a DIFFERENT repo (a nested clone under `repositories/**`) → foreign.\n * 3. Otherwise ask git for the worktree list. A LINKED WORKTREE of the governed repo is MANAGED —\n * it is the same project, just another checkout — so guards run, keyed on THAT tree's branch and\n * its own `.webpieces/` cache. Before this, a linked worktree read as a different git toplevel\n * and so as FOREIGN, which silently disabled every guard for `cd <worktree> && …` commands.\n * 4. Anything else that is a git repo → foreign (out of scope, hands-off, as before).\n * 5. Not a git repo at all (`cd /tmp && …`) → OUTSIDE. The guards still run — an absolute path back\n * into the repo must still be judged — but nothing the command names relative to `/tmp` is\n * workspace content, which is what ContentReadScan uses `effectiveCwd` for.\n */\n// L1's K dimension. 'primary' and 'worktree' are the same PROJECT, so every rule-scoped guard treats\n// them alike — guards/L1-location.md writes them as one value, `pw`. Exactly ONE guard separates them,\n// and on a dimension that is not the tree at all: CoordinatorWorktreeGuard blocks the COORDINATOR\n// (never a subagent) from working inside a linked worktree, because the coordinator's governance is\n// anchored at session start and does not follow a `cd`.\n//\n// 'outside' is produced below (gitRoot === null) and consumed NOWHERE, so a command in no git repo is\n// judged against governedRoot — a repo it is not in. guards/L1-location.md's \"Not done\" section explains why\n// exempting it must ship together with target-based jurisdiction, never alone.\nexport type TreeKind = 'primary' | 'worktree' | 'foreign' | 'outside';\n\n/** Data-only (per CLAUDE.md, classes for data). */\nexport class EffectiveTree {\n /** The pre-`cd` cwd the hook was handed. For an agent in a worktree this is the primary clone. */\n readonly shellCwd: string;\n /** The directory the command really runs in, after its own leading `cd`/`pushd` run. */\n readonly effectiveCwd: string;\n /** The tree root to JUDGE: the owning worktree root, the foreign repo root, or the governed root. */\n readonly root: string;\n /** The root that owns webpieces.config.json — where config and excludePaths come from. */\n readonly governedRoot: string;\n readonly kind: TreeKind;\n /** The command acts on a tree other than the shell's own — messages must steer with `cd <root> &&`. */\n readonly redirected: boolean;\n\n constructor(shellCwd: string, effectiveCwd: string, root: string, governedRoot: string, kind: TreeKind) {\n this.shellCwd = shellCwd;\n this.effectiveCwd = effectiveCwd;\n this.root = root;\n this.governedRoot = governedRoot;\n this.kind = kind;\n this.redirected = path.resolve(root) !== path.resolve(shellCwd);\n }\n}\n\nexport class EffectiveTreeResolver {\n private readonly worktrees = new WorktreeService();\n private readonly shell: ShellSegmentScan;\n\n constructor(private readonly scanner: CommandScanner = new CommandScanner()) {\n this.shell = new ShellSegmentScan(scanner);\n }\n\n resolve(command: string, shellCwd: string, governedRoot: string): EffectiveTree {\n const effectiveCwd = this.effectiveCwd(command, shellCwd);\n const kindAndRoot = this.classify(effectiveCwd, governedRoot);\n return new EffectiveTree(shellCwd, effectiveCwd, kindAndRoot.root, governedRoot, kindAndRoot.kind);\n }\n\n /**\n * The cwd a command actually runs from, resolving a LEADING run of `cd`/`pushd` in the command.\n *\n * ONLY a leading run counts. Once a non-cd command appears it has ALREADY run in the current dir,\n * so a later `cd` must not retroactively pull it out of scope — otherwise a trailing\n * `… && cd <exempt-tree>` would exempt the WHOLE line, smuggling a root-level `git push` past the\n * guards. `cd a && cd b && git …` resolves left to right, matching the shell.\n *\n * Segmentation is ShellSegmentScan's (over CommandScanner), so quoting is handled exactly as the\n * guards handle it: `echo \"cd sub && git push\"` is ONE opaque segment whose first word is `echo`,\n * so the quoted `cd` is never picked up and cannot be weaponised into a scope escape.\n */\n effectiveCwd(command: string, shellCwd: string): string {\n let effective = shellCwd;\n for (const segment of this.scanner.commandSegments(command)) {\n const words = this.shell.effectiveWords(segment);\n if (words[0] !== 'cd' && words[0] !== 'pushd') break;\n if (words[1] !== undefined) effective = path.resolve(effective, words[1]);\n }\n return effective;\n }\n\n private classify(effectiveCwd: string, governedRoot: string): TreeClassification {\n // Fast path — no `cd`, or a `cd` within the governed tree. No worktree enumeration needed, and\n // the nested-clone check keeps its exact previous behaviour.\n if (this.isInside(effectiveCwd, governedRoot)) {\n const gitRoot = gitToplevel(effectiveCwd);\n if (gitRoot === null) return new TreeClassification('primary', governedRoot);\n if (path.resolve(gitRoot) === path.resolve(governedRoot)) return new TreeClassification('primary', governedRoot);\n return new TreeClassification('foreign', gitRoot);\n }\n\n // Outside the governed tree: is it a linked worktree of the SAME repo? Longest match wins, so a\n // worktree nested under another resolves to the innermost one.\n const owner = this.owningWorktree(effectiveCwd, governedRoot);\n if (owner !== null) {\n return new TreeClassification(owner.isMain ? 'primary' : 'worktree', owner.path);\n }\n\n const gitRoot = gitToplevel(effectiveCwd);\n if (gitRoot === null) return new TreeClassification('outside', governedRoot);\n if (path.resolve(gitRoot) === path.resolve(governedRoot)) return new TreeClassification('primary', governedRoot);\n return new TreeClassification('foreign', gitRoot);\n }\n\n private owningWorktree(dir: string, governedRoot: string): Worktree | null {\n let best: Worktree | null = null;\n for (const tree of this.worktrees.listWorktrees(governedRoot)) {\n if (!this.isInside(dir, tree.path)) continue;\n if (best === null || tree.path.length > best.path.length) best = tree;\n }\n return best;\n }\n\n /** Is `dir` the directory `root` itself, or somewhere beneath it? Pure path math, no filesystem. */\n private isInside(dir: string, root: string): boolean {\n const relative = path.relative(path.resolve(root), path.resolve(dir));\n return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));\n }\n}\n\n/** Data-only carrier for the two values classify() decides together. */\nclass TreeClassification {\n readonly kind: TreeKind;\n readonly root: string;\n\n constructor(kind: TreeKind, root: string) {\n this.kind = kind;\n this.root = root;\n }\n}\n\n/**\n * The steering prefix every remedy needs, re-exported from @webpieces/rules-config so the guards, the\n * message builders and pr-gate's worktree notices all emit the IDENTICAL string — including the single\n * quotes that keep it runnable when the repo path contains a space. See atRoot's own header for why the\n * quotes are single and never double.\n */\nexport { atRoot } from '@webpieces/rules-config';\n\n// The git repo root of `dir`, or null when it is not in a git repo / git is unavailable. `status !== 0`\n// IS the expected \"not a repo\" answer (spawnSync does not throw on a non-zero exit), so no try/catch.\n// webpieces-disable no-function-outside-class -- sibling of atRoot(); this module is the resolver plus its two pure helpers\nfunction gitToplevel(dir: string): string | null {\n const r = spawnSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (r.status !== 0) return null;\n const root = (r.stdout ?? '').trim();\n return root !== '' ? root : null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"effective-tree.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/effective-tree.ts"],"names":[],"mappings":";;;;AAAA,mDAA6B;AAC7B,iDAA0C;AAE1C,0DAAoE;AAEpE,iDAAgD;AAChD,mEAA8D;AA0D9D,mDAAmD;AACnD,MAAa,aAAa;IACtB,kGAAkG;IACzF,QAAQ,CAAS;IAC1B,wFAAwF;IAC/E,YAAY,CAAS;IAC9B,qGAAqG;IAC5F,IAAI,CAAS;IACtB,0FAA0F;IACjF,YAAY,CAAS;IACrB,IAAI,CAAW;IACxB,uGAAuG;IAC9F,UAAU,CAAU;IAE7B,YAAY,QAAgB,EAAE,YAAoB,EAAE,IAAY,EAAE,YAAoB,EAAE,IAAc;QAClG,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpE,CAAC;CACJ;AArBD,sCAqBC;AAED,MAAa,qBAAqB;IAID;IAHZ,SAAS,GAAG,IAAI,8BAAe,EAAE,CAAC;IAClC,KAAK,CAAmB;IAEzC,YAA6B,UAA0B,IAAI,6BAAc,EAAE;QAA9C,YAAO,GAAP,OAAO,CAAuC;QACvE,IAAI,CAAC,KAAK,GAAG,IAAI,qCAAgB,CAAC,OAAO,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,CAAC,OAAe,EAAE,QAAgB,EAAE,YAAoB;QAC3D,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QAC9D,OAAO,IAAI,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC;IACvG,CAAC;IAED;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,OAAe,EAAE,QAAgB;QAC1C,IAAI,SAAS,GAAG,QAAQ,CAAC;QACzB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;YACjD,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO;gBAAE,MAAM;YACrD,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS;gBAAE,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9E,CAAC;QACD,OAAO,SAAS,CAAC;IACrB,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,YAAY,CAAC,OAAe;QACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,GAAG,CACtD,CAAC,OAAe,EAAqB,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;QAEhF,kGAAkG;QAClG,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,OAAO,QAAQ,GAAG,QAAQ,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAAE,QAAQ,EAAE,CAAC;QAE1E,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,CAAC,MAAc,EAAQ,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE/F,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAAE,SAAS;YACjC,6EAA6E;YAC7E,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC;gBAAE,SAAS;YAEzD,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;gBAChB,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC;oBACxC,CAAC,CAAC,8EAA8E;oBAChF,CAAC,CAAC,uDAAuD,CAAC,CAAC;YACnE,CAAC;YACD,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9B,IAAI,MAAM,KAAK,SAAS,IAAI,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBACvD,GAAG,CAAC,oFAAoF,CAAC,CAAC;YAC9F,CAAC;QACL,CAAC;QACD,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChE,CAAC;IAEO,QAAQ,CAAC,YAAoB,EAAE,YAAoB;QACvD,+FAA+F;QAC/F,6DAA6D;QAC7D,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC,EAAE,CAAC;YAC5C,MAAM,OAAO,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC;YAC1C,IAAI,OAAO,KAAK,IAAI;gBAAE,OAAO,IAAI,kBAAkB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;YAC7E,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;gBAAE,OAAO,IAAI,kBAAkB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;YACjH,OAAO,IAAI,kBAAkB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACtD,CAAC;QAED,gGAAgG;QAChG,+DAA+D;QAC/D,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QAC9D,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACjB,OAAO,IAAI,kBAAkB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACrF,CAAC;QAED,MAAM,OAAO,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC;QAC1C,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,IAAI,kBAAkB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;QAC7E,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;YAAE,OAAO,IAAI,kBAAkB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;QACjH,OAAO,IAAI,kBAAkB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IAEO,cAAc,CAAC,GAAW,EAAE,YAAoB;QACpD,IAAI,IAAI,GAAoB,IAAI,CAAC;QACjC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,YAAY,CAAC,EAAE,CAAC;YAC5D,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC;gBAAE,SAAS;YAC7C,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM;gBAAE,IAAI,GAAG,IAAI,CAAC;QAC1E,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,oGAAoG;IAC5F,QAAQ,CAAC,GAAW,EAAE,IAAY;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QACtE,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzF,CAAC;CACJ;AArID,sDAqIC;AAED,uGAAuG;AACvG,wFAAwF;AACxF,sIAAsI;AACtI,SAAS,IAAI,CAAC,KAAwB;IAClC,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC;AACrD,CAAC;AAED,mEAAmE;AACnE,SAAS,aAAa,CAAC,KAAwB;IAC3C,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5C,CAAC;AAED,sGAAsG;AACtG,oGAAoG;AACpG,gBAAgB;AAChB,MAAM,eAAe,GAAG,SAAS,CAAC;AAElC,wEAAwE;AACxE,MAAM,kBAAkB;IACX,IAAI,CAAW;IACf,IAAI,CAAS;IAEtB,YAAY,IAAc,EAAE,IAAY;QACpC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;CACJ;AAED;;;;;GAKG;AACH,wDAAiD;AAAxC,sGAAA,MAAM,OAAA;AAEf,wGAAwG;AACxG,sGAAsG;AACtG,4HAA4H;AAC5H,SAAS,WAAW,CAAC,GAAW;IAC5B,MAAM,CAAC,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,iBAAiB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9F,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACrC,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACrC,CAAC","sourcesContent":["import * as path from 'path';\nimport { spawnSync } from 'child_process';\n\nimport { WorktreeService, Worktree } from '@webpieces/rules-config';\n\nimport { CommandScanner } from './command-scan';\nimport { ShellSegmentScan } from './rules/shell-segment-scan';\n\n/**\n * WHICH TREE does a Bash command actually act on? The ONE resolver every bash guard and the\n * force-to-root check share.\n *\n * WHY it has to exist at all: the shell cwd a PreToolUse hook is handed does not tell you which tree\n * the command acts on. `cd` behaves TWO different ways, and both break a cwd-based guard:\n *\n * - A `cd` that stays INSIDE the session's working directory PERSISTS to later calls. So the cwd\n * can be a subdirectory of the governed root, left there by an unrelated command several turns\n * earlier — a relative path then resolves somewhere other than the root while still being in the\n * governed tree.\n * - A `cd` that LEAVES it is reset by the harness, which says so (`Shell cwd was reset to <root>`).\n * So an agent working in a linked worktree is back in the primary clone by the next call and must\n * write self-contained `cd <worktree> && …` commands — and the cwd the hook sees is the primary\n * clone, not the worktree the command targets.\n *\n * (Measured on 2026-08-02: `cd backlog && pwd` → `…/backlog`, then a bare `pwd` in a FRESH call →\n * still `…/backlog`. But `cd ../<linked-worktree> && pwd` → the worktree, then a bare `pwd` → back at\n * the primary clone. An earlier version of this comment asserted `cd` never persists; that was the\n * worktree case generalized. The conclusion below is unchanged — only the reason was wrong.)\n *\n * Either way a guard that reasons from the raw cwd judges the wrong tree. Three field sightings in\n * one session:\n * an `ls` of a path outside every repo blocked as \"this branch is merged\"; a version-drift cure\n * (`pnpm install`) that could not be typed from the directory that needed it; and a command aimed at\n * `/private/tmp` blocked because the PRIMARY clone's main was behind — with a remedy (`git pull` in\n * the primary clone) the agent had been explicitly forbidden to run.\n *\n * Two separate copies of the cwd logic used to exist (the runner's foreign-repo/excludePaths check\n * and force-to-root). They are both this class now: two resolvers WILL disagree about which tree you\n * are in, and a guard that disagrees with the guard beside it is worse than either being wrong.\n *\n * Resolution, in order:\n * 1. `effectiveCwd` — the leading run of `cd`/`pushd` in the command itself, resolved left to right.\n * 2. If that sits under the governed root, the tree is the governed root unless git says the\n * directory belongs to a DIFFERENT repo (a nested clone under `repositories/**`) → foreign.\n * 3. Otherwise ask git for the worktree list. A LINKED WORKTREE of the governed repo is MANAGED —\n * it is the same project, just another checkout — so guards run, keyed on THAT tree's branch and\n * its own `.webpieces/` cache. Before this, a linked worktree read as a different git toplevel\n * and so as FOREIGN, which silently disabled every guard for `cd <worktree> && …` commands.\n * 4. Anything else that is a git repo → foreign (out of scope, hands-off, as before).\n * 5. Not a git repo at all (`cd /tmp && …`) → OUTSIDE. The guards still run — an absolute path back\n * into the repo must still be judged — but nothing the command names relative to `/tmp` is\n * workspace content, which is what ContentReadScan uses `effectiveCwd` for.\n */\n// L1's K dimension. 'primary' and 'worktree' are the same PROJECT, so every rule-scoped guard treats\n// them alike — guards/L1-location.md writes them as one value, `pw`. Exactly ONE guard separates them,\n// and on a dimension that is not the tree at all: CoordinatorWorktreeGuard blocks the COORDINATOR\n// (never a subagent) from working inside a linked worktree, because the coordinator's governance is\n// anchored at session start and does not follow a `cd`.\n//\n// 'outside' is produced below (gitRoot === null) and consumed NOWHERE, so a command in no git repo is\n// judged against governedRoot — a repo it is not in. guards/L1-location.md's \"Not done\" section explains why\n// exempting it must ship together with target-based jurisdiction, never alone.\nexport type TreeKind = 'primary' | 'worktree' | 'foreign' | 'outside';\n\n/** Data-only (per CLAUDE.md, classes for data). */\nexport class EffectiveTree {\n /** The pre-`cd` cwd the hook was handed. For an agent in a worktree this is the primary clone. */\n readonly shellCwd: string;\n /** The directory the command really runs in, after its own leading `cd`/`pushd` run. */\n readonly effectiveCwd: string;\n /** The tree root to JUDGE: the owning worktree root, the foreign repo root, or the governed root. */\n readonly root: string;\n /** The root that owns webpieces.config.json — where config and excludePaths come from. */\n readonly governedRoot: string;\n readonly kind: TreeKind;\n /** The command acts on a tree other than the shell's own — messages must steer with `cd <root> &&`. */\n readonly redirected: boolean;\n\n constructor(shellCwd: string, effectiveCwd: string, root: string, governedRoot: string, kind: TreeKind) {\n this.shellCwd = shellCwd;\n this.effectiveCwd = effectiveCwd;\n this.root = root;\n this.governedRoot = governedRoot;\n this.kind = kind;\n this.redirected = path.resolve(root) !== path.resolve(shellCwd);\n }\n}\n\nexport class EffectiveTreeResolver {\n private readonly worktrees = new WorktreeService();\n private readonly shell: ShellSegmentScan;\n\n constructor(private readonly scanner: CommandScanner = new CommandScanner()) {\n this.shell = new ShellSegmentScan(scanner);\n }\n\n resolve(command: string, shellCwd: string, governedRoot: string): EffectiveTree {\n const effectiveCwd = this.effectiveCwd(command, shellCwd);\n const kindAndRoot = this.classify(effectiveCwd, governedRoot);\n return new EffectiveTree(shellCwd, effectiveCwd, kindAndRoot.root, governedRoot, kindAndRoot.kind);\n }\n\n /**\n * The cwd a command actually runs from, resolving a LEADING run of `cd`/`pushd` in the command.\n *\n * ONLY a leading run counts. Once a non-cd command appears it has ALREADY run in the current dir,\n * so a later `cd` must not retroactively pull it out of scope — otherwise a trailing\n * `… && cd <exempt-tree>` would exempt the WHOLE line, smuggling a root-level `git push` past the\n * guards. `cd a && cd b && git …` resolves left to right, matching the shell.\n *\n * Segmentation is ShellSegmentScan's (over CommandScanner), so quoting is handled exactly as the\n * guards handle it: `echo \"cd sub && git push\"` is ONE opaque segment whose first word is `echo`,\n * so the quoted `cd` is never picked up and cannot be weaponised into a scope escape.\n */\n effectiveCwd(command: string, shellCwd: string): string {\n let effective = shellCwd;\n for (const segment of this.scanner.commandSegments(command)) {\n const words = this.shell.effectiveWords(segment);\n if (words[0] !== 'cd' && words[0] !== 'pushd') break;\n if (words[1] !== undefined) effective = path.resolve(effective, words[1]);\n }\n return effective;\n }\n\n /**\n * WHY a `cd` the agent wrote did not move the effective cwd — or null when there is nothing to\n * explain. Pure diagnosis for the block MESSAGE; it changes no verdict and relaxes nothing.\n *\n * The two shapes below both degrade to \"judge against the governed root\" (the safe direction) and\n * both look, from the agent's side, exactly like a `cd` that was ignored. A guard that then says\n * \"cd into it first\" is describing a remedy the agent has ALREADY performed, so it cannot discover\n * the real cause: in the session that motivated this, the agent concluded the exemption was broken\n * and wrote a persistent memory entry asserting `cd` must be its own Bash call. Neither is true.\n * Naming the precondition here is what makes the hint self-correcting.\n *\n * `cd \"$DIR\" && git push` — the target is not a literal path. `path.resolve(cwd, '$DIR')` is\n * a directory that does not exist, not the tree that was meant.\n * `D=/x; cd \"$D\"; git push` — a bare `VAR=value` segment tokenizes to NO words (CommandScanner\n * strips assignments as command prefixes), so the leading run ends\n * before the `cd` is ever reached.\n * `git fetch && cd /x && git push` — the leading run ended at `git fetch`. Bash really does run\n * the push inside /x; the guard judges it from the shell cwd. Same\n * false positive as the two above, just wearing a different shape.\n *\n * ONE thing is silent: a `cd` with NO work after it (`git push && cd /x`). Nothing ran in the new\n * directory, so the verdict on what DID run is simply correct, and \"put the cd in front\" would\n * change what the command does rather than where it is judged.\n *\n * The rule the message must land, in every one of these: only `cd <literal path> && <work>`\n * relocates the verdict. Everything else fails CLOSED — judged from the shell cwd — which is why\n * an unresolvable `cd` is a message defect and never a hole. Expanding `$VAR` here, or resuming the\n * scan after a real command, are both the fix NOT taken: the resolver decides ONE location for the\n * whole line, so a `cd` that is allowed to count retroactively is exactly the `… && cd <exempt-tree>`\n * scope escape.\n */\n unresolvedCd(command: string): string | null {\n const segments = this.scanner.commandSegments(command).map(\n (segment: string): readonly string[] => this.shell.effectiveWords(segment));\n\n // The leading run effectiveCwd() actually consumed — a `cd` at or after this index did not count.\n let consumed = 0;\n while (consumed < segments.length && isCd(segments[consumed])) consumed++;\n\n const reasons: string[] = [];\n const add = (reason: string): void => { if (!reasons.includes(reason)) reasons.push(reason); };\n\n for (let i = 0; i < segments.length; i++) {\n if (!isCd(segments[i])) continue;\n // A `cd` with nothing left to run after it changed nothing — see the header.\n if (!segments.slice(i + 1).some(isRealCommand)) continue;\n\n if (i >= consumed) {\n add(segments.slice(0, i).some(isRealCommand)\n ? 'another command precedes it, and only a `cd` at the FRONT of the line counts'\n : 'a `VAR=…` assignment precedes it, which ends the scan');\n }\n const target = segments[i][1];\n if (target !== undefined && VARIABLE_TARGET.test(target)) {\n add('its target is not a literal path (a `$VAR`, `~` or `$(…)` the guard cannot expand)');\n }\n }\n return reasons.length === 0 ? null : reasons.join(', and ');\n }\n\n private classify(effectiveCwd: string, governedRoot: string): TreeClassification {\n // Fast path — no `cd`, or a `cd` within the governed tree. No worktree enumeration needed, and\n // the nested-clone check keeps its exact previous behaviour.\n if (this.isInside(effectiveCwd, governedRoot)) {\n const gitRoot = gitToplevel(effectiveCwd);\n if (gitRoot === null) return new TreeClassification('primary', governedRoot);\n if (path.resolve(gitRoot) === path.resolve(governedRoot)) return new TreeClassification('primary', governedRoot);\n return new TreeClassification('foreign', gitRoot);\n }\n\n // Outside the governed tree: is it a linked worktree of the SAME repo? Longest match wins, so a\n // worktree nested under another resolves to the innermost one.\n const owner = this.owningWorktree(effectiveCwd, governedRoot);\n if (owner !== null) {\n return new TreeClassification(owner.isMain ? 'primary' : 'worktree', owner.path);\n }\n\n const gitRoot = gitToplevel(effectiveCwd);\n if (gitRoot === null) return new TreeClassification('outside', governedRoot);\n if (path.resolve(gitRoot) === path.resolve(governedRoot)) return new TreeClassification('primary', governedRoot);\n return new TreeClassification('foreign', gitRoot);\n }\n\n private owningWorktree(dir: string, governedRoot: string): Worktree | null {\n let best: Worktree | null = null;\n for (const tree of this.worktrees.listWorktrees(governedRoot)) {\n if (!this.isInside(dir, tree.path)) continue;\n if (best === null || tree.path.length > best.path.length) best = tree;\n }\n return best;\n }\n\n /** Is `dir` the directory `root` itself, or somewhere beneath it? Pure path math, no filesystem. */\n private isInside(dir: string, root: string): boolean {\n const relative = path.relative(path.resolve(root), path.resolve(dir));\n return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));\n }\n}\n\n// The two segment shapes unresolvedCd() sorts by. A segment with NO words at all is a bare `VAR=value`\n// assignment (CommandScanner strips assignments as command prefixes), which is neither.\n// webpieces-disable no-function-outside-class -- pure predicates over one segment's words, siblings of the module-scope helpers below\nfunction isCd(words: readonly string[]): boolean {\n return words[0] === 'cd' || words[0] === 'pushd';\n}\n\n// webpieces-disable no-function-outside-class -- sibling of isCd()\nfunction isRealCommand(words: readonly string[]): boolean {\n return words.length > 0 && !isCd(words);\n}\n\n// A `cd` target that is not a literal path: `$DIR`, `${DIR}`, `~`, or a `$(…)`/backtick substitution.\n// `~` is here because path.resolve() does not expand it either — the shell does, and the hook never\n// sees a shell.\nconst VARIABLE_TARGET = /[$`]|^~/;\n\n/** Data-only carrier for the two values classify() decides together. */\nclass TreeClassification {\n readonly kind: TreeKind;\n readonly root: string;\n\n constructor(kind: TreeKind, root: string) {\n this.kind = kind;\n this.root = root;\n }\n}\n\n/**\n * The steering prefix every remedy needs, re-exported from @webpieces/rules-config so the guards, the\n * message builders and pr-gate's worktree notices all emit the IDENTICAL string — including the single\n * quotes that keep it runnable when the repo path contains a space. See atRoot's own header for why the\n * quotes are single and never double.\n */\nexport { atRoot } from '@webpieces/rules-config';\n\n// The git repo root of `dir`, or null when it is not in a git repo / git is unavailable. `status !== 0`\n// IS the expected \"not a repo\" answer (spawnSync does not throw on a non-zero exit), so no try/catch.\n// webpieces-disable no-function-outside-class -- sibling of atRoot(); this module is the resolver plus its two pure helpers\nfunction gitToplevel(dir: string): string | null {\n const r = spawnSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (r.status !== 0) return null;\n const root = (r.stdout ?? '').trim();\n return root !== '' ? root : null;\n}\n"]}
|
package/src/core/runner.js
CHANGED
|
@@ -349,7 +349,7 @@ function runBashInternal(command, cwd, mode, agent) {
|
|
|
349
349
|
}
|
|
350
350
|
const ruleNames = groups.map((g) => g.ruleName).join(',');
|
|
351
351
|
(0, decision_log_1.logGuardDecision)(tree.root, new decision_log_1.GuardDecision(ruleNames, 'Bash', command, (0, decision_log_1.branchForLog)(tree.root), 'BLOCK', 'bash-guard block'));
|
|
352
|
-
const report = (0, report_1.formatReport)(commandLabel(command), groups, report_1.BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.paths);
|
|
352
|
+
const report = (0, report_1.formatReport)(commandLabel(command), groups, report_1.BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.paths, command);
|
|
353
353
|
return new types_1.BlockedResult(report);
|
|
354
354
|
}
|
|
355
355
|
// The bash report's subject line. It used to be the literal string `<bash>`, which told the agent
|
|
@@ -366,14 +366,32 @@ function commandLabel(command) {
|
|
|
366
366
|
// governed by its own repo, not this one). Scoped to pr-creation-or-push-guard — for the other guards
|
|
367
367
|
// "cd into an exempt tree" is not the remedy — and emitted only when such trees are actually configured,
|
|
368
368
|
// so a repo without exemptions never sees the noise.
|
|
369
|
+
//
|
|
370
|
+
// The hint MUST state the precondition, not just the remedy. "cd into it first" is true only for a
|
|
371
|
+
// LITERAL cd at the FRONT of the same command; `D=…; cd "$D"; git push` lands in the identical
|
|
372
|
+
// directory and still blocks (see EffectiveTreeResolver.unresolvedCd for why, and why the resolver is
|
|
373
|
+
// deliberately not taught to expand `$D`). An agent that had already cd'd read the old wording as
|
|
374
|
+
// evidence the exemption was broken and reasoned itself into two wrong conclusions it later had to
|
|
375
|
+
// retract. Where the near-miss is detectable we say so outright, which turns the misdirect into a
|
|
376
|
+
// self-correcting hint.
|
|
369
377
|
// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape
|
|
370
|
-
function exemptTreesHint(groups, exemptGuards) {
|
|
378
|
+
function exemptTreesHint(groups, exemptGuards, command) {
|
|
371
379
|
if (exemptGuards.length === 0)
|
|
372
380
|
return '';
|
|
373
381
|
if (!groups.some((g) => g.ruleName === 'pr-creation-or-push-guard'))
|
|
374
382
|
return '';
|
|
375
|
-
|
|
376
|
-
|
|
383
|
+
const unresolved = new effective_tree_1.EffectiveTreeResolver().unresolvedCd(command);
|
|
384
|
+
const nearMiss = unresolved === null ? ''
|
|
385
|
+
: `\n⚠️ This command DOES contain a \`cd\`, but the guard could not resolve it: ${unresolved}. `
|
|
386
|
+
+ `It was therefore judged against this repo. Re-run it as one command with a literal path in front.`;
|
|
387
|
+
return `\n\nℹ️ Working in a nested repo under one of these exempt trees (${exemptGuards.join(', ')})? `
|
|
388
|
+
+ `Put a LITERAL \`cd\` at the FRONT of the SAME command — \`cd /abs/path/to/repo && git push\` — and git/gh `
|
|
389
|
+
+ `run normally there: the webpieces guards do NOT govern them (each is its own repo).`
|
|
390
|
+
+ `\n The guard resolves only a leading run of literal \`cd\`/\`pushd\`, so \`cd <literal path> && <work>\` `
|
|
391
|
+
+ `is the ONE shape that moves where the command is judged. Anything ahead of the \`cd\` ends the scan — a `
|
|
392
|
+
+ `\`VAR=…\` assignment, or another command as in \`git fetch && cd /x && git push\` — and so does a `
|
|
393
|
+
+ `non-literal target like \`cd "$DIR"\`. In every one of those the command is judged against this repo instead.`
|
|
394
|
+
+ nearMiss;
|
|
377
395
|
}
|
|
378
396
|
// The set of rule names explicitly present in webpieces.config.json (every key except rulesDir).
|
|
379
397
|
function configuredRuleNames(config) {
|
package/src/core/runner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/runner.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { loadAndValidate, LoadedConfig, WebpiecesRulesConfig, ExcludePaths, isHookGuard, DEFAULT_HANG_TIMEOUT_MINUTES, RepoRootFinder, seedEntryForRule, CONFIG_FILENAME } from '@webpieces/rules-config';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { AgentIdentity, CoordinatorWorktreeGuard, UNKNOWN_AGENT } from './coordinator-worktree';\nimport { EffectiveTree, EffectiveTreeResolver, atRoot } from './effective-tree';\nimport { loadRules, loadMatchRules, globMatches } from './load-rules';\nimport { MatchRule } from './rules/match-rule';\nimport { triggerMainSyncRefresh } from './main-sync-refresh';\nimport { logGuardDecision, GuardDecision, branchForLog } from './decision-log';\nimport { toError } from './to-error';\nimport { formatReport, READ_SUBJECT, BASH_SUBJECT } from './report';\nimport { ReadOnlyInspectionScan } from './read-only-inspection';\nimport { L0_ALLOW_JS, L0_CURE_ALLOW_JS } from '../bin/shim';\nimport { CONFIG_MISSING_REPORT, CONFIG_OUT_OF_SYNC_HEADER, writeGuardMatrixDoc, guardMatrixPointer } from './l0-matrix';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult, HookMode,\n Rule, Violation, RuleGroup, RuleFailError, InformAiError,\n EditContext, FileContext, BashContext,\n} from './types';\n\n// Restrict loaded rules to the category this hook invocation runs. The two split hooks each pass a\n// disjoint category ('rules' = code-style, 'guards' = the hookGuards section); 'all' runs both (the\n// openclaw plugin adapter, a single before_tool_call hook). isHookGuard is the shared classifier in\n// @webpieces/rules-config.\nfunction filterByMode(rules: readonly Rule[], mode: HookMode): readonly Rule[] {\n if (mode === 'all') return rules;\n if (mode === 'guards') return rules.filter((r: Rule): boolean => isHookGuard(r.name));\n return rules.filter((r: Rule): boolean => !isHookGuard(r.name));\n}\n\n// Drop every rule excluded for this path (webpieces.config.json → excludePaths). ONE glob list: a path\n// listed there is hands-off for code-style rules and file-scoped guards alike, because webpieces either\n// governs a path or it does not. Per-rule carve-outs live in the rule's own `excludePaths`.\n// This is L1's FILTER (not a table row) — see guards/L1-location.md.\nexport function filterByExcludedPaths(rules: readonly Rule[], relativePath: string, ex: ExcludePaths): readonly Rule[] {\n if (ex.paths.some((p: string): boolean => globMatches(p, relativePath))) return [];\n return rules;\n}\n\n// The cwd a command actually runs from, after its own leading `cd`/`pushd` run. Thin delegate kept\n// for the callers (and specs) that only need the directory; the full tree classification — primary\n// clone vs linked worktree vs nested clone vs outside any repo — is EffectiveTreeResolver.resolve().\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nexport function effectiveBashCwd(command: string, cwd: string): string {\n return new EffectiveTreeResolver().effectiveCwd(command, cwd);\n}\n\n// A git or gh invocation anywhere in the command (start, or after a ;/&&/|| separator or pipe).\nconst GIT_OR_GH_RE = /(?:^|[;&|]\\s*)(?:git|gh)\\b/;\nexport function isGitOrGhCommand(command: string): boolean {\n return GIT_OR_GH_RE.test(command);\n}\n\n// Fire-and-forget the detached refresher when feature-branch-guard is loaded and active, so the\n// cache (.webpieces/main-sync-status.json) stays fresh as the AI works. The guard rule itself also\n// triggers this on Write/Edit; this covers the Bash path so the cache is warm on every command.\nfunction maybeRefreshMainSync(rules: readonly Rule[], workspaceRoot: string): void {\n const guard = rules.find((r: Rule): boolean => r.name === 'feature-branch-guard');\n if (guard && guard.shouldRun()) {\n triggerMainSyncRefresh(workspaceRoot, DEFAULT_HANG_TIMEOUT_MINUTES);\n }\n}\n\n// Fault C (webpieces.config.json missing) — the deny text lives in ./l0-matrix beside the rest of the\n// L0 fault table, so the message and the allowlist can never prescribe different cures. `cwd` is used\n// only to drop the matrix doc where the AI can read it (the config root does not exist yet here).\n//\n// `command` is '' on the file-tool path (there is no command to judge) and the Bash command otherwise:\n// a call on the L0 allowlist survives this block, which is how read-only orientation stays available\n// under C. You have to be able to see which tree you are standing in before you can decide what to\n// write into the config. Returns null when the call is allowed through.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction configMissingBlock(cwd: string, command: string = ''): BlockedResult | null {\n if (command !== '' && l0FaultAllows(command)) return null;\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n return new BlockedResult(CONFIG_MISSING_REPORT + guardMatrixPointer(writeGuardMatrixDoc(root)));\n}\n\nexport function run(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode = 'all',\n): BlockedResult | null {\n return runInternal(toolKind, input, cwd, mode);\n}\n\nfunction runInternal(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode,\n): BlockedResult | null {\n const loaded = loadAndValidate(cwd);\n if (loaded.configPath === null) return configMissingBlock(cwd);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Always allow edits to webpieces.config.json — it's the fix target when out of sync\n if (path.resolve(input.filePath) === path.resolve(loaded.configPath)) {\n return null;\n }\n\n // Built-in/custom rules PLUS the client-authored match-rules (content guards). Match-rules run only\n // in the file-edit path (they are code-style, so filterByMode keeps them out of the bash/guards path).\n const allRules = [...loadRules(loaded.rulesConfig, workspaceRoot), ...loadMatchRules(loaded.matchRules)];\n const modeRules = filterByMode(allRules, mode);\n if (modeRules.length === 0) return null;\n\n // Suppress enforcement for files under this category's excludePaths (e.g. vendored repos under\n // repositories/**). Exclusion is all-or-nothing per category, so an excluded file drops the whole\n // rule set and is fully hands-off — no violations AND no config-sync nag on those files.\n const relativePath = path.relative(workspaceRoot, input.filePath);\n const rules = filterByExcludedPaths(modeRules, relativePath, loaded.excludePaths);\n if (rules.length === 0) return null;\n\n // Config-sync applies only to built-in/custom rules; match-rules have their own validated section\n // (loadAndValidate already rejected an invalid `match-rules`), so they must not trip the sync nag.\n const outOfSync = checkConfigSync(rules.filter((r: Rule) => !(r instanceof MatchRule)), loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const contexts = buildContexts(toolKind, input, workspaceRoot);\n\n const editGroups = runEditRules(rules, contexts.editContexts);\n const fileGroups = runFileRules(rules, contexts.fileContext);\n const allGroups = [...editGroups, ...fileGroups];\n\n if (allGroups.length === 0) return null;\n\n const report = formatReport(relativePath, allGroups);\n return new BlockedResult(report);\n}\n\n// `agent` defaults to UNKNOWN_AGENT — NOT the coordinator. Only the Claude Code adapter can read\n// agent_id/agent_type off the payload; every other caller (the openclaw adapter, library consumers,\n// the existing specs) genuinely does not know, and a caller who does not know must not be guessed\n// into a coordinator-only block. See UNKNOWN_AGENT.\n// webpieces-disable no-function-outside-class -- sibling of run()/runRead() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runBash(command: string, cwd: string, mode: HookMode = 'all', agent: AgentIdentity = UNKNOWN_AGENT): BlockedResult | null {\n return runBashInternal(command, cwd, mode, agent);\n}\n\n// The name of the ONLY rule permitted to block a Read. Reads are the highest-blast-radius tool\n// there is, so this path is an explicit single-rule allowlist rather than the general rule loop.\nconst READ_SCOPED_GUARDS: ReadonlySet<string> = new Set(['read-stale-guard']);\n\n/**\n * The Read path. Deliberately NOT `run()`:\n *\n * - NO config-sync check. A rule present in code but missing from webpieces.config.json blocks\n * every Write/Edit/Bash by design — but applying that to Read would mean an upgrade that adds\n * any new rule instantly blocks the agent from reading the very config file it must edit to fix\n * it. Reads must never carry that failure mode.\n * - NO general rule loop. Only READ_SCOPED_GUARDS run, so no code-style rule can ever see a Read.\n * - Fails OPEN everywhere, including on a thrown rule (the caller catches and allows).\n *\n * Returns null (allow) unless the one guard fires.\n */\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runRead(filePath: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n // Code-style mode has nothing to say about a read.\n if (mode === 'rules') return null;\n\n const loaded = loadAndValidate(cwd);\n // No config → nothing to enforce. Unlike the edit path we do NOT block: an unconfigured repo\n // must still be readable.\n if (loaded.configPath === null) return null;\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Same git-repo-boundary governance as bash, through the SAME resolver: a read inside a different\n // clone is out of scope. (No command to parse here, so the shell cwd IS the effective cwd.)\n if (new EffectiveTreeResolver().resolve('', cwd, workspaceRoot).kind === 'foreign') return null;\n\n const relativePath = path.relative(workspaceRoot, filePath);\n const all = loadRules(loaded.rulesConfig, workspaceRoot);\n const rules = filterByExcludedPaths(\n all.filter((r: Rule): boolean => READ_SCOPED_GUARDS.has(r.name)),\n relativePath,\n loaded.excludePaths,\n );\n if (rules.length === 0) return null;\n\n const ctx = new FileContext('Read', filePath, relativePath, workspaceRoot, 0, 0, 0, 0);\n const groups = runFileRules(rules, ctx);\n if (groups.length === 0) return null;\n\n return new BlockedResult(formatReport(relativePath, groups, READ_SUBJECT));\n}\n\n// L0 cure bypass — every command on THE L0 allowlist passes here, ahead of any config load. A\n// webpieces.config.json that is ahead of the installed validator (new rule tokens the published\n// binary doesn't know yet) makes loadAndValidate() throw and would deny `pnpm install` — the very\n// command that updates the validator (deadlock).\n//\n// This used to test INSTALLER_ALLOW_JS alone, which made the config faults (C = config missing,\n// Y = config out of sync) accept a bare `pnpm install` while denying `rm -rf node_modules && pnpm\n// install` — the one cure that works when node_modules is CORRUPT rather than merely stale. Same\n// intent, opposite verdict, for no reason anyone recorded. Each alternative is still anchored at both\n// ends, so `pnpm install && rm -rf /` still falls to the guards.\n//\n// It tests the CURE subset (L0_CURE_ALLOW_JS), not the whole list, and the distinction matters here\n// and nowhere else: this call site runs BEFORE any fault has been established, so whatever it accepts\n// is waved past the L1 guards on a perfectly healthy repo too. A repair command has to be waved past —\n// it runs before the config can be loaded at all. Read-only ORIENTATION does not: it fixes nothing, and\n// letting `git status` through here would delete force-to-root as a side effect of adding `pwd`. Under\n// an actual fault, l0FaultAllows() below consults the WHOLE list.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction isL0CureCommand(command: string): boolean {\n return L0_CURE_ALLOW_JS.test(command.trim());\n}\n\n// The FULL L0 list, asked only where this file has actually detected an L0 fault (C = config missing,\n// Y = config out of sync). Those two are the JS-side faults; D/X/K are decided in the shim, which greps\n// the same union. Without this the orientation entry would be honoured under four faults and silently\n// dropped under the other two — precisely the per-fault carve-out the L0 rewrite removed.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l0FaultAllows(command: string): boolean {\n return L0_ALLOW_JS.test(command.trim());\n}\n\n// Force-to-root: git/gh commands must run from the repo root of the tree they act on, where the guards\n// can reason about git state coherently. L1 row 4 — see guards/L1-location.md for the table\n// and the use cases; change this predicate and that file is stale until you update it.\n//\n// ONE variable decides it: `tree.effectiveCwd` — the directory the command actually runs in, which is\n// the shell's cwd unless the command leads with `cd <dir> &&`. Root or not-root, nothing else.\n//\n// It used to be `shellAtRoot || cdsToRoot`, two variables OR'd, and that produced opposite verdicts for\n// the same destination: `git status` with the shell in packages/http/ was BLOCKED, while\n// `cd packages/http && git status` from the root was ALLOWED, because shellAtRoot short-circuited\n// before the destination was ever considered. The point of this guard is to keep the agent's git work\n// at the root — an agent that cd's INTO a subdir has the same broken mental model as one that is\n// stranded there, so it gets the same answer now.\n//\n// The remedy is emitted as ONE runnable line, `cd <root> && <the original command>`, rather than as\n// \"cd first, then re-run\". That advice is what made this guard print the very command it had just\n// rejected, and it is unreliable in both directions: a `cd` INTO this repo sticks (so the next call\n// may start somewhere unexpected), while a `cd` OUT of it is reset by the harness (so a separate\n// `cd <worktree>` call buys nothing). One self-contained line is correct either way.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction gitFromSubdirBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const targetAtRoot = path.resolve(tree.effectiveCwd) === path.resolve(tree.root);\n if (!isGitOrGhCommand(command) || targetAtRoot) return null;\n const report =\n `❌ Run git/gh commands from the repo root, not a subdirectory.\\n` +\n ` Command runs in: ${tree.effectiveCwd}\\n` +\n ` Judged against: ${tree.root}\\n` +\n ` Run EXACTLY this instead, as ONE line (a bare \\`cd\\` in a separate call is not equivalent —\\n` +\n ` a \\`cd\\` inside this repo STICKS for later calls, and a \\`cd\\` out of it is reset by the harness):\\n` +\n ` ${atRoot(tree.root, command)}\\n` +\n ` A leading \\`cd <path> &&\\` is ACCEPTED by the guards — it cannot change what the command\\n` +\n ` does to the repo. (The webpieces guards evaluate the repo's git state at its root.)`;\n logGuardDecision(tree.root, new GuardDecision('force-to-root', 'Bash', command, branchForLog(tree.root), 'BLOCK', 'git/gh from subdir'));\n return new BlockedResult(report);\n}\n\n// The L0 cure bypass's audit line. Anchored at the repo root that owns `.webpieces` — RepoRootFinder\n// (config-walk-up first, then git toplevel) is the authority for that, and it is correct in a linked\n// worktree because each worktree checks out its own webpieces.config.json. This runs BEFORE\n// loadAndValidate, which is why it resolves the root itself rather than using workspaceRoot.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction logL0CureBypass(command: string, cwd: string): void {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n logGuardDecision(root, new GuardDecision('-', 'Bash', command, branchForLog(root), 'ALLOW', 'L0 cure bypass (always allowed)'));\n}\n\n/**\n * Load the config for the bash path — but do NOT let an unloadable config trap the tools needed to\n * repair it.\n *\n * loadAndValidate throws an InformAiError when webpieces.config.json is unparseable (a real syntax\n * error, or leftover `<<<<<<< HEAD` markers mid-merge) or fails validation. That throw propagates to\n * the hook adapter, which fails CLOSED and denies the command — correct for work, since a config that\n * did not load means no guards ran. But it denied `cat`/`grep`/`sed -n` on webpieces.config.json too,\n * i.e. it blocked the only way to see the problem it was reporting. Observed live, twice.\n *\n * So: on a load failure, a provably-inert INSPECTION command is allowed through (returns null, \"no\n * block\"), matching the escape hatch every other layer already grants this file. Everything else —\n * every write, every git/gh command, every build — still hits the same hard failure as before. The\n * bypass cannot be widened by accident; see ReadOnlyInspectionScan for how narrow \"inert\" is.\n *\n * Returns the loaded config, or null meaning \"allow this command without guards\".\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction loadConfigOrAllowInspection(command: string, cwd: string): LoadedConfig | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrown unchanged unless the command is provably inert\n try {\n return loadAndValidate(cwd);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof InformAiError && new ReadOnlyInspectionScan().isReadOnlyInspection(command)) {\n return null;\n }\n throw error;\n }\n}\n\n// Coordinator-in-worktree: the coordinator's governance is anchored at session start and does NOT\n// follow a `cd`, so a coordinator working inside a linked worktree has its filesystem in one tree and\n// its guards in another. L1 row 3 — guards/L1-location.md carries the table and the incident; the\n// predicate and the message are CoordinatorWorktreeGuard's.\n// webpieces-disable no-function-outside-class -- sibling of gitFromSubdirBlock() and the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction coordinatorInWorktreeBlock(command: string, tree: EffectiveTree, agent: AgentIdentity): BlockedResult | null {\n const report = new CoordinatorWorktreeGuard().block(command, tree, agent);\n if (report === null) return null;\n logGuardDecision(tree.root, new GuardDecision('coordinator-in-worktree', 'Bash', command, branchForLog(tree.root), 'BLOCK', 'coordinator working inside a linked worktree'));\n return new BlockedResult(report);\n}\n\n// The structural L1 blocks, in order: coordinator-in-worktree (row 3), then force-to-root (row 5).\n// Neither is a configurable rule — they are decided from the resolved tree and the caller, so they run\n// as one step here rather than as two near-identical stanzas in runBashInternal.\n// webpieces-disable no-function-outside-class -- sibling of gitFromSubdirBlock() and the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l1LocationBlock(command: string, tree: EffectiveTree, agent: AgentIdentity): BlockedResult | null {\n return coordinatorInWorktreeBlock(command, tree, agent) ?? gitFromSubdirBlock(command, tree);\n}\n\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nfunction runBashInternal(command: string, cwd: string, mode: HookMode, agent: AgentIdentity): BlockedResult | null {\n if (isL0CureCommand(command)) {\n logL0CureBypass(command, cwd);\n return null;\n }\n\n const loaded = loadConfigOrAllowInspection(command, cwd);\n // null = the config would not load AND this command only inspects → allow, see the helper.\n if (loaded === null) return null;\n if (loaded.configPath === null) return configMissingBlock(cwd, command); // fault C\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // WHICH TREE does this command act on? Not the shell's cwd: a `cd` OUT of the workspace is reset\n // by the harness and one INSIDE it persists, so neither can be assumed (see EffectiveTree).\n // ONE resolver answers this for the guards AND for force-to-root below, so the two can never\n // disagree about which tree you are in.\n const tree = new EffectiveTreeResolver().resolve(command, cwd, workspaceRoot);\n\n // Git-repo-boundary governance: the command runs inside a DIFFERENT git repo than this\n // webpieces.config governs (e.g. a clone under repositories/). Out of scope → allow, hands-off.\n // Intentional, not a silent hole. A LINKED WORKTREE of this repo is deliberately NOT foreign — it\n // is the same project, so the guards run against THAT tree's branch and cache.\n if (tree.kind === 'foreign') {\n logGuardDecision(workspaceRoot, new GuardDecision('-', 'Bash', command, branchForLog(workspaceRoot), 'ALLOW', 'foreign git repo (out of scope)'));\n return null;\n }\n\n // Honour excludePaths on the bash path too (not just Read/Edit): a command whose effective\n // cwd sits under an excluded tree (e.g. repositories/**) drops the whole guard set — matching how\n // runInternal/runRead treat file paths. The relative path is '' when there is no `cd` (root), which\n // matches no exclusion glob, so a plain command at the repo root is unaffected.\n const rules = filterByExcludedPaths(\n filterByMode(loadRules(loaded.rulesConfig, workspaceRoot), mode),\n path.relative(workspaceRoot, tree.effectiveCwd),\n loaded.excludePaths,\n );\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig); // fault Y — L0 list wins, as under C\n if (outOfSync) return l0FaultAllows(command) ? null : outOfSync;\n\n const locationBlock = l1LocationBlock(command, tree, agent);\n if (locationBlock) return locationBlock;\n\n // Keep the feature-branch-guard cache warm on EVERY command (not just Write/Edit): the AI runs\n // far more bash than edits, so refreshing here means the guard's next file-edit check reads a\n // fresh status. Detached + fire-and-forget — never blocks the command. Only when the guard is\n // loaded (guards/all mode) and enabled, so a project that opted out never triggers git fetches.\n // Keyed on the JUDGED tree, so a worktree's cache is refreshed rather than the primary clone's.\n maybeRefreshMainSync(rules, tree.root);\n\n const ctx = buildBashContext(command, tree);\n const groups = runBashRules(rules, ctx);\n if (groups.length === 0) {\n // Record the ALLOW only for git/gh commands — the operations the bash guards actually reason\n // about (branch create, commit, push, merge, PR). Skipping ls/cat/grep keeps the audit log\n // focused (the whole point of the log is \"why did/didn't a guard fire?\"). Blocks are always\n // logged below.\n if (/\\b(?:git|gh)\\b/.test(command)) {\n logGuardDecision(tree.root, new GuardDecision('-', 'Bash', command, branchForLog(tree.root), 'ALLOW', 'no bash-guard block'));\n }\n return null;\n }\n\n const ruleNames = groups.map((g: RuleGroup): string => g.ruleName).join(',');\n logGuardDecision(tree.root, new GuardDecision(ruleNames, 'Bash', command, branchForLog(tree.root), 'BLOCK', 'bash-guard block'));\n const report = formatReport(commandLabel(command), groups, BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.paths);\n return new BlockedResult(report);\n}\n\n// The bash report's subject line. It used to be the literal string `<bash>`, which told the agent\n// nothing; the command itself is what was blocked, so name it — truncated, because a heredoc-bearing\n// command can run to thousands of characters and the violation lines already carry the detail.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction commandLabel(command: string): string {\n const oneLine = command.replace(/\\s+/g, ' ').trim();\n const MAX = 100;\n return oneLine.length <= MAX ? oneLine : oneLine.slice(0, MAX) + '…';\n}\n\n// When a push/PR block fires AND the config exempts vendored/nested trees, surface the escape hatch the\n// AI cannot otherwise discover: git/gh run UNGUARDED inside those trees if it cd's there first (each is\n// governed by its own repo, not this one). Scoped to pr-creation-or-push-guard — for the other guards\n// \"cd into an exempt tree\" is not the remedy — and emitted only when such trees are actually configured,\n// so a repo without exemptions never sees the noise.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction exemptTreesHint(groups: readonly RuleGroup[], exemptGuards: readonly string[]): string {\n if (exemptGuards.length === 0) return '';\n if (!groups.some((g: RuleGroup): boolean => g.ruleName === 'pr-creation-or-push-guard')) return '';\n return `\\n\\nℹ️ Working in a nested repo under one of these exempt trees? cd into it first and run git/gh `\n + `normally there — the webpieces guards do NOT govern them (each is its own repo): ${exemptGuards.join(', ')}.`;\n}\n\n// The set of rule names explicitly present in webpieces.config.json (every key except rulesDir).\nfunction configuredRuleNames(config: WebpiecesRulesConfig): ReadonlySet<string> {\n return new Set(Object.keys(config).filter((k: string) => k !== 'rulesDir'));\n}\n\nfunction checkConfigSync(rules: readonly Rule[], config: WebpiecesRulesConfig): BlockedResult | null {\n const configured = configuredRuleNames(config);\n const unconfiguredRules = rules.filter((r: Rule) => !configured.has(r.name));\n if (unconfiguredRules.length === 0) return null;\n\n // ONE action, no menu, no escalation — the config-validation invariant (guards/L0-tooling.md): every\n // config problem cures to \"make the file right\", and editing it is never denied. This message used\n // to tell the agent to interview the human about each rule; agents did not do it, so the block just\n // stalled. Each rule now ships a paste-ready entry at its recommended mode.\n //\n // Note this is the CONFIG-BEHIND-CODE direction. The opposite one — the config names a rule the\n // installed validator has no schema for — is unknownRuleError() in rules-config/validate-config.ts\n // and surfaces in the validation banner, not here.\n const lines = [\n // Fault Y's header lives in ./l0-matrix beside the rest of the L0 fault table (same reason as\n // CONFIG_MISSING_REPORT: one place states what this fault is and what cures it).\n CONFIG_OUT_OF_SYNC_HEADER,\n '',\n `Add an entry for each rule below to ${CONFIG_FILENAME}. Editing that file is ALWAYS allowed through`,\n 'the guard — including right now, while this block is up — so paste the entries and retry.',\n '',\n 'Each entry below is ready to paste at its recommended mode; adjust the option values if your',\n 'project needs different ones.',\n '',\n `Do NOT delete a rule from ${CONFIG_FILENAME} to silence it — an entry is REQUIRED for every rule,`,\n 'and \"mode\": \"OFF\" is how a rule is turned off.',\n '',\n ];\n\n for (const rule of unconfiguredRules) {\n lines.push(`--- ${rule.name} ---`);\n lines.push(`Description: ${rule.description}`);\n const opts = rule.defaultOptions;\n const optKeys = Object.keys(opts);\n if (optKeys.length > 0) {\n lines.push(`Available options (suggested defaults shown):`);\n for (const key of optKeys) {\n lines.push(` ${key}: ${JSON.stringify(opts[key])}`);\n }\n } else {\n lines.push('Available options: none beyond mode');\n }\n // The SAME entry the installer would seed: recommended mode, both hatches, and every other\n // schema-required field — so pasting it satisfies the loader in one pass.\n lines.push(`Entry to add to ${CONFIG_FILENAME}:`);\n lines.push(` \"${rule.name}\": ${JSON.stringify(seedEntryForRule(rule.name))}`);\n lines.push('');\n }\n\n return new BlockedResult(lines.join('\\n'));\n}\n\n// N-legs pattern: each rule runs independently so one rule can never abort the others. A rule may\n// EITHER return Violation[] OR throw — both accumulate here into visible violations the AI sees:\n// - a thrown RuleFailError → an expected, well-formed violation (its line/snippet/fixHints kept);\n// - a thrown plain Error → a \"crashed\" violation (a bug, surfaced not swallowed).\nexport function runRuleCheck(rule: Rule, ctx: EditContext | FileContext | BashContext): readonly Violation[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return rule.check(ctx);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof RuleFailError) {\n return [violationFromRuleFail(error)];\n }\n return [new Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];\n }\n}\n\n// A thrown RuleFailError carries its own AI-facing message + optional location and fix hints. Fold the\n// fix hints into the message because Violation has no fixHint field (RuleGroup's fixHint comes from the\n// rule definition, not a per-throw value).\nfunction violationFromRuleFail(error: RuleFailError): Violation {\n const hints = error.fixHints.length > 0 ? `\\n Fix: ${error.fixHints.join('\\n Fix: ')}` : '';\n return new Violation(error.line ?? 0, error.snippet ?? '', error.aiMessage + hints);\n}\n\nfunction ruleMatchesFile(rule: Rule, relativePath: string): boolean {\n for (const pattern of rule.files) {\n if (globMatches(pattern, relativePath)) return true;\n }\n return false;\n}\n\nfunction runBashRules(rules: readonly Rule[], bashContext: BashContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'bash') continue;\n if (!rule.shouldRun()) continue;\n const vs = runRuleCheck(rule, bashContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, [...vs],\n ));\n }\n }\n return groups;\n}\n\nfunction runEditRules(rules: readonly Rule[], editContexts: readonly EditContext[]): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'edit') continue;\n if (!rule.shouldRun()) continue;\n const allViolations: Violation[] = [];\n for (const ctx of editContexts) {\n if (!ruleMatchesFile(rule, ctx.relativePath)) continue;\n const vs = runRuleCheck(rule, ctx);\n for (const v of vs) {\n const copy = new Violation(v.line, v.snippet, v.message);\n copy.editIndex = ctx.editIndex;\n copy.editCount = ctx.editCount;\n allViolations.push(copy);\n }\n }\n if (allViolations.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, allViolations,\n ));\n }\n }\n return groups;\n}\n\nfunction runFileRules(rules: readonly Rule[], fileContext: FileContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'file') continue;\n if (!rule.shouldRun()) continue;\n if (!ruleMatchesFile(rule, fileContext.relativePath)) continue;\n const vs = runRuleCheck(rule, fileContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, [...vs],\n ));\n }\n }\n return groups;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/runner.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { loadAndValidate, LoadedConfig, WebpiecesRulesConfig, ExcludePaths, isHookGuard, DEFAULT_HANG_TIMEOUT_MINUTES, RepoRootFinder, seedEntryForRule, CONFIG_FILENAME } from '@webpieces/rules-config';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { AgentIdentity, CoordinatorWorktreeGuard, UNKNOWN_AGENT } from './coordinator-worktree';\nimport { EffectiveTree, EffectiveTreeResolver, atRoot } from './effective-tree';\nimport { loadRules, loadMatchRules, globMatches } from './load-rules';\nimport { MatchRule } from './rules/match-rule';\nimport { triggerMainSyncRefresh } from './main-sync-refresh';\nimport { logGuardDecision, GuardDecision, branchForLog } from './decision-log';\nimport { toError } from './to-error';\nimport { formatReport, READ_SUBJECT, BASH_SUBJECT } from './report';\nimport { ReadOnlyInspectionScan } from './read-only-inspection';\nimport { L0_ALLOW_JS, L0_CURE_ALLOW_JS } from '../bin/shim';\nimport { CONFIG_MISSING_REPORT, CONFIG_OUT_OF_SYNC_HEADER, writeGuardMatrixDoc, guardMatrixPointer } from './l0-matrix';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult, HookMode,\n Rule, Violation, RuleGroup, RuleFailError, InformAiError,\n EditContext, FileContext, BashContext,\n} from './types';\n\n// Restrict loaded rules to the category this hook invocation runs. The two split hooks each pass a\n// disjoint category ('rules' = code-style, 'guards' = the hookGuards section); 'all' runs both (the\n// openclaw plugin adapter, a single before_tool_call hook). isHookGuard is the shared classifier in\n// @webpieces/rules-config.\nfunction filterByMode(rules: readonly Rule[], mode: HookMode): readonly Rule[] {\n if (mode === 'all') return rules;\n if (mode === 'guards') return rules.filter((r: Rule): boolean => isHookGuard(r.name));\n return rules.filter((r: Rule): boolean => !isHookGuard(r.name));\n}\n\n// Drop every rule excluded for this path (webpieces.config.json → excludePaths). ONE glob list: a path\n// listed there is hands-off for code-style rules and file-scoped guards alike, because webpieces either\n// governs a path or it does not. Per-rule carve-outs live in the rule's own `excludePaths`.\n// This is L1's FILTER (not a table row) — see guards/L1-location.md.\nexport function filterByExcludedPaths(rules: readonly Rule[], relativePath: string, ex: ExcludePaths): readonly Rule[] {\n if (ex.paths.some((p: string): boolean => globMatches(p, relativePath))) return [];\n return rules;\n}\n\n// The cwd a command actually runs from, after its own leading `cd`/`pushd` run. Thin delegate kept\n// for the callers (and specs) that only need the directory; the full tree classification — primary\n// clone vs linked worktree vs nested clone vs outside any repo — is EffectiveTreeResolver.resolve().\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nexport function effectiveBashCwd(command: string, cwd: string): string {\n return new EffectiveTreeResolver().effectiveCwd(command, cwd);\n}\n\n// A git or gh invocation anywhere in the command (start, or after a ;/&&/|| separator or pipe).\nconst GIT_OR_GH_RE = /(?:^|[;&|]\\s*)(?:git|gh)\\b/;\nexport function isGitOrGhCommand(command: string): boolean {\n return GIT_OR_GH_RE.test(command);\n}\n\n// Fire-and-forget the detached refresher when feature-branch-guard is loaded and active, so the\n// cache (.webpieces/main-sync-status.json) stays fresh as the AI works. The guard rule itself also\n// triggers this on Write/Edit; this covers the Bash path so the cache is warm on every command.\nfunction maybeRefreshMainSync(rules: readonly Rule[], workspaceRoot: string): void {\n const guard = rules.find((r: Rule): boolean => r.name === 'feature-branch-guard');\n if (guard && guard.shouldRun()) {\n triggerMainSyncRefresh(workspaceRoot, DEFAULT_HANG_TIMEOUT_MINUTES);\n }\n}\n\n// Fault C (webpieces.config.json missing) — the deny text lives in ./l0-matrix beside the rest of the\n// L0 fault table, so the message and the allowlist can never prescribe different cures. `cwd` is used\n// only to drop the matrix doc where the AI can read it (the config root does not exist yet here).\n//\n// `command` is '' on the file-tool path (there is no command to judge) and the Bash command otherwise:\n// a call on the L0 allowlist survives this block, which is how read-only orientation stays available\n// under C. You have to be able to see which tree you are standing in before you can decide what to\n// write into the config. Returns null when the call is allowed through.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction configMissingBlock(cwd: string, command: string = ''): BlockedResult | null {\n if (command !== '' && l0FaultAllows(command)) return null;\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n return new BlockedResult(CONFIG_MISSING_REPORT + guardMatrixPointer(writeGuardMatrixDoc(root)));\n}\n\nexport function run(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode = 'all',\n): BlockedResult | null {\n return runInternal(toolKind, input, cwd, mode);\n}\n\nfunction runInternal(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode,\n): BlockedResult | null {\n const loaded = loadAndValidate(cwd);\n if (loaded.configPath === null) return configMissingBlock(cwd);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Always allow edits to webpieces.config.json — it's the fix target when out of sync\n if (path.resolve(input.filePath) === path.resolve(loaded.configPath)) {\n return null;\n }\n\n // Built-in/custom rules PLUS the client-authored match-rules (content guards). Match-rules run only\n // in the file-edit path (they are code-style, so filterByMode keeps them out of the bash/guards path).\n const allRules = [...loadRules(loaded.rulesConfig, workspaceRoot), ...loadMatchRules(loaded.matchRules)];\n const modeRules = filterByMode(allRules, mode);\n if (modeRules.length === 0) return null;\n\n // Suppress enforcement for files under this category's excludePaths (e.g. vendored repos under\n // repositories/**). Exclusion is all-or-nothing per category, so an excluded file drops the whole\n // rule set and is fully hands-off — no violations AND no config-sync nag on those files.\n const relativePath = path.relative(workspaceRoot, input.filePath);\n const rules = filterByExcludedPaths(modeRules, relativePath, loaded.excludePaths);\n if (rules.length === 0) return null;\n\n // Config-sync applies only to built-in/custom rules; match-rules have their own validated section\n // (loadAndValidate already rejected an invalid `match-rules`), so they must not trip the sync nag.\n const outOfSync = checkConfigSync(rules.filter((r: Rule) => !(r instanceof MatchRule)), loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const contexts = buildContexts(toolKind, input, workspaceRoot);\n\n const editGroups = runEditRules(rules, contexts.editContexts);\n const fileGroups = runFileRules(rules, contexts.fileContext);\n const allGroups = [...editGroups, ...fileGroups];\n\n if (allGroups.length === 0) return null;\n\n const report = formatReport(relativePath, allGroups);\n return new BlockedResult(report);\n}\n\n// `agent` defaults to UNKNOWN_AGENT — NOT the coordinator. Only the Claude Code adapter can read\n// agent_id/agent_type off the payload; every other caller (the openclaw adapter, library consumers,\n// the existing specs) genuinely does not know, and a caller who does not know must not be guessed\n// into a coordinator-only block. See UNKNOWN_AGENT.\n// webpieces-disable no-function-outside-class -- sibling of run()/runRead() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runBash(command: string, cwd: string, mode: HookMode = 'all', agent: AgentIdentity = UNKNOWN_AGENT): BlockedResult | null {\n return runBashInternal(command, cwd, mode, agent);\n}\n\n// The name of the ONLY rule permitted to block a Read. Reads are the highest-blast-radius tool\n// there is, so this path is an explicit single-rule allowlist rather than the general rule loop.\nconst READ_SCOPED_GUARDS: ReadonlySet<string> = new Set(['read-stale-guard']);\n\n/**\n * The Read path. Deliberately NOT `run()`:\n *\n * - NO config-sync check. A rule present in code but missing from webpieces.config.json blocks\n * every Write/Edit/Bash by design — but applying that to Read would mean an upgrade that adds\n * any new rule instantly blocks the agent from reading the very config file it must edit to fix\n * it. Reads must never carry that failure mode.\n * - NO general rule loop. Only READ_SCOPED_GUARDS run, so no code-style rule can ever see a Read.\n * - Fails OPEN everywhere, including on a thrown rule (the caller catches and allows).\n *\n * Returns null (allow) unless the one guard fires.\n */\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runRead(filePath: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n // Code-style mode has nothing to say about a read.\n if (mode === 'rules') return null;\n\n const loaded = loadAndValidate(cwd);\n // No config → nothing to enforce. Unlike the edit path we do NOT block: an unconfigured repo\n // must still be readable.\n if (loaded.configPath === null) return null;\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Same git-repo-boundary governance as bash, through the SAME resolver: a read inside a different\n // clone is out of scope. (No command to parse here, so the shell cwd IS the effective cwd.)\n if (new EffectiveTreeResolver().resolve('', cwd, workspaceRoot).kind === 'foreign') return null;\n\n const relativePath = path.relative(workspaceRoot, filePath);\n const all = loadRules(loaded.rulesConfig, workspaceRoot);\n const rules = filterByExcludedPaths(\n all.filter((r: Rule): boolean => READ_SCOPED_GUARDS.has(r.name)),\n relativePath,\n loaded.excludePaths,\n );\n if (rules.length === 0) return null;\n\n const ctx = new FileContext('Read', filePath, relativePath, workspaceRoot, 0, 0, 0, 0);\n const groups = runFileRules(rules, ctx);\n if (groups.length === 0) return null;\n\n return new BlockedResult(formatReport(relativePath, groups, READ_SUBJECT));\n}\n\n// L0 cure bypass — every command on THE L0 allowlist passes here, ahead of any config load. A\n// webpieces.config.json that is ahead of the installed validator (new rule tokens the published\n// binary doesn't know yet) makes loadAndValidate() throw and would deny `pnpm install` — the very\n// command that updates the validator (deadlock).\n//\n// This used to test INSTALLER_ALLOW_JS alone, which made the config faults (C = config missing,\n// Y = config out of sync) accept a bare `pnpm install` while denying `rm -rf node_modules && pnpm\n// install` — the one cure that works when node_modules is CORRUPT rather than merely stale. Same\n// intent, opposite verdict, for no reason anyone recorded. Each alternative is still anchored at both\n// ends, so `pnpm install && rm -rf /` still falls to the guards.\n//\n// It tests the CURE subset (L0_CURE_ALLOW_JS), not the whole list, and the distinction matters here\n// and nowhere else: this call site runs BEFORE any fault has been established, so whatever it accepts\n// is waved past the L1 guards on a perfectly healthy repo too. A repair command has to be waved past —\n// it runs before the config can be loaded at all. Read-only ORIENTATION does not: it fixes nothing, and\n// letting `git status` through here would delete force-to-root as a side effect of adding `pwd`. Under\n// an actual fault, l0FaultAllows() below consults the WHOLE list.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction isL0CureCommand(command: string): boolean {\n return L0_CURE_ALLOW_JS.test(command.trim());\n}\n\n// The FULL L0 list, asked only where this file has actually detected an L0 fault (C = config missing,\n// Y = config out of sync). Those two are the JS-side faults; D/X/K are decided in the shim, which greps\n// the same union. Without this the orientation entry would be honoured under four faults and silently\n// dropped under the other two — precisely the per-fault carve-out the L0 rewrite removed.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l0FaultAllows(command: string): boolean {\n return L0_ALLOW_JS.test(command.trim());\n}\n\n// Force-to-root: git/gh commands must run from the repo root of the tree they act on, where the guards\n// can reason about git state coherently. L1 row 4 — see guards/L1-location.md for the table\n// and the use cases; change this predicate and that file is stale until you update it.\n//\n// ONE variable decides it: `tree.effectiveCwd` — the directory the command actually runs in, which is\n// the shell's cwd unless the command leads with `cd <dir> &&`. Root or not-root, nothing else.\n//\n// It used to be `shellAtRoot || cdsToRoot`, two variables OR'd, and that produced opposite verdicts for\n// the same destination: `git status` with the shell in packages/http/ was BLOCKED, while\n// `cd packages/http && git status` from the root was ALLOWED, because shellAtRoot short-circuited\n// before the destination was ever considered. The point of this guard is to keep the agent's git work\n// at the root — an agent that cd's INTO a subdir has the same broken mental model as one that is\n// stranded there, so it gets the same answer now.\n//\n// The remedy is emitted as ONE runnable line, `cd <root> && <the original command>`, rather than as\n// \"cd first, then re-run\". That advice is what made this guard print the very command it had just\n// rejected, and it is unreliable in both directions: a `cd` INTO this repo sticks (so the next call\n// may start somewhere unexpected), while a `cd` OUT of it is reset by the harness (so a separate\n// `cd <worktree>` call buys nothing). One self-contained line is correct either way.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction gitFromSubdirBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const targetAtRoot = path.resolve(tree.effectiveCwd) === path.resolve(tree.root);\n if (!isGitOrGhCommand(command) || targetAtRoot) return null;\n const report =\n `❌ Run git/gh commands from the repo root, not a subdirectory.\\n` +\n ` Command runs in: ${tree.effectiveCwd}\\n` +\n ` Judged against: ${tree.root}\\n` +\n ` Run EXACTLY this instead, as ONE line (a bare \\`cd\\` in a separate call is not equivalent —\\n` +\n ` a \\`cd\\` inside this repo STICKS for later calls, and a \\`cd\\` out of it is reset by the harness):\\n` +\n ` ${atRoot(tree.root, command)}\\n` +\n ` A leading \\`cd <path> &&\\` is ACCEPTED by the guards — it cannot change what the command\\n` +\n ` does to the repo. (The webpieces guards evaluate the repo's git state at its root.)`;\n logGuardDecision(tree.root, new GuardDecision('force-to-root', 'Bash', command, branchForLog(tree.root), 'BLOCK', 'git/gh from subdir'));\n return new BlockedResult(report);\n}\n\n// The L0 cure bypass's audit line. Anchored at the repo root that owns `.webpieces` — RepoRootFinder\n// (config-walk-up first, then git toplevel) is the authority for that, and it is correct in a linked\n// worktree because each worktree checks out its own webpieces.config.json. This runs BEFORE\n// loadAndValidate, which is why it resolves the root itself rather than using workspaceRoot.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction logL0CureBypass(command: string, cwd: string): void {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n logGuardDecision(root, new GuardDecision('-', 'Bash', command, branchForLog(root), 'ALLOW', 'L0 cure bypass (always allowed)'));\n}\n\n/**\n * Load the config for the bash path — but do NOT let an unloadable config trap the tools needed to\n * repair it.\n *\n * loadAndValidate throws an InformAiError when webpieces.config.json is unparseable (a real syntax\n * error, or leftover `<<<<<<< HEAD` markers mid-merge) or fails validation. That throw propagates to\n * the hook adapter, which fails CLOSED and denies the command — correct for work, since a config that\n * did not load means no guards ran. But it denied `cat`/`grep`/`sed -n` on webpieces.config.json too,\n * i.e. it blocked the only way to see the problem it was reporting. Observed live, twice.\n *\n * So: on a load failure, a provably-inert INSPECTION command is allowed through (returns null, \"no\n * block\"), matching the escape hatch every other layer already grants this file. Everything else —\n * every write, every git/gh command, every build — still hits the same hard failure as before. The\n * bypass cannot be widened by accident; see ReadOnlyInspectionScan for how narrow \"inert\" is.\n *\n * Returns the loaded config, or null meaning \"allow this command without guards\".\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction loadConfigOrAllowInspection(command: string, cwd: string): LoadedConfig | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrown unchanged unless the command is provably inert\n try {\n return loadAndValidate(cwd);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof InformAiError && new ReadOnlyInspectionScan().isReadOnlyInspection(command)) {\n return null;\n }\n throw error;\n }\n}\n\n// Coordinator-in-worktree: the coordinator's governance is anchored at session start and does NOT\n// follow a `cd`, so a coordinator working inside a linked worktree has its filesystem in one tree and\n// its guards in another. L1 row 3 — guards/L1-location.md carries the table and the incident; the\n// predicate and the message are CoordinatorWorktreeGuard's.\n// webpieces-disable no-function-outside-class -- sibling of gitFromSubdirBlock() and the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction coordinatorInWorktreeBlock(command: string, tree: EffectiveTree, agent: AgentIdentity): BlockedResult | null {\n const report = new CoordinatorWorktreeGuard().block(command, tree, agent);\n if (report === null) return null;\n logGuardDecision(tree.root, new GuardDecision('coordinator-in-worktree', 'Bash', command, branchForLog(tree.root), 'BLOCK', 'coordinator working inside a linked worktree'));\n return new BlockedResult(report);\n}\n\n// The structural L1 blocks, in order: coordinator-in-worktree (row 3), then force-to-root (row 5).\n// Neither is a configurable rule — they are decided from the resolved tree and the caller, so they run\n// as one step here rather than as two near-identical stanzas in runBashInternal.\n// webpieces-disable no-function-outside-class -- sibling of gitFromSubdirBlock() and the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l1LocationBlock(command: string, tree: EffectiveTree, agent: AgentIdentity): BlockedResult | null {\n return coordinatorInWorktreeBlock(command, tree, agent) ?? gitFromSubdirBlock(command, tree);\n}\n\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nfunction runBashInternal(command: string, cwd: string, mode: HookMode, agent: AgentIdentity): BlockedResult | null {\n if (isL0CureCommand(command)) {\n logL0CureBypass(command, cwd);\n return null;\n }\n\n const loaded = loadConfigOrAllowInspection(command, cwd);\n // null = the config would not load AND this command only inspects → allow, see the helper.\n if (loaded === null) return null;\n if (loaded.configPath === null) return configMissingBlock(cwd, command); // fault C\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // WHICH TREE does this command act on? Not the shell's cwd: a `cd` OUT of the workspace is reset\n // by the harness and one INSIDE it persists, so neither can be assumed (see EffectiveTree).\n // ONE resolver answers this for the guards AND for force-to-root below, so the two can never\n // disagree about which tree you are in.\n const tree = new EffectiveTreeResolver().resolve(command, cwd, workspaceRoot);\n\n // Git-repo-boundary governance: the command runs inside a DIFFERENT git repo than this\n // webpieces.config governs (e.g. a clone under repositories/). Out of scope → allow, hands-off.\n // Intentional, not a silent hole. A LINKED WORKTREE of this repo is deliberately NOT foreign — it\n // is the same project, so the guards run against THAT tree's branch and cache.\n if (tree.kind === 'foreign') {\n logGuardDecision(workspaceRoot, new GuardDecision('-', 'Bash', command, branchForLog(workspaceRoot), 'ALLOW', 'foreign git repo (out of scope)'));\n return null;\n }\n\n // Honour excludePaths on the bash path too (not just Read/Edit): a command whose effective\n // cwd sits under an excluded tree (e.g. repositories/**) drops the whole guard set — matching how\n // runInternal/runRead treat file paths. The relative path is '' when there is no `cd` (root), which\n // matches no exclusion glob, so a plain command at the repo root is unaffected.\n const rules = filterByExcludedPaths(\n filterByMode(loadRules(loaded.rulesConfig, workspaceRoot), mode),\n path.relative(workspaceRoot, tree.effectiveCwd),\n loaded.excludePaths,\n );\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig); // fault Y — L0 list wins, as under C\n if (outOfSync) return l0FaultAllows(command) ? null : outOfSync;\n\n const locationBlock = l1LocationBlock(command, tree, agent);\n if (locationBlock) return locationBlock;\n\n // Keep the feature-branch-guard cache warm on EVERY command (not just Write/Edit): the AI runs\n // far more bash than edits, so refreshing here means the guard's next file-edit check reads a\n // fresh status. Detached + fire-and-forget — never blocks the command. Only when the guard is\n // loaded (guards/all mode) and enabled, so a project that opted out never triggers git fetches.\n // Keyed on the JUDGED tree, so a worktree's cache is refreshed rather than the primary clone's.\n maybeRefreshMainSync(rules, tree.root);\n\n const ctx = buildBashContext(command, tree);\n const groups = runBashRules(rules, ctx);\n if (groups.length === 0) {\n // Record the ALLOW only for git/gh commands — the operations the bash guards actually reason\n // about (branch create, commit, push, merge, PR). Skipping ls/cat/grep keeps the audit log\n // focused (the whole point of the log is \"why did/didn't a guard fire?\"). Blocks are always\n // logged below.\n if (/\\b(?:git|gh)\\b/.test(command)) {\n logGuardDecision(tree.root, new GuardDecision('-', 'Bash', command, branchForLog(tree.root), 'ALLOW', 'no bash-guard block'));\n }\n return null;\n }\n\n const ruleNames = groups.map((g: RuleGroup): string => g.ruleName).join(',');\n logGuardDecision(tree.root, new GuardDecision(ruleNames, 'Bash', command, branchForLog(tree.root), 'BLOCK', 'bash-guard block'));\n const report = formatReport(commandLabel(command), groups, BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.paths, command);\n return new BlockedResult(report);\n}\n\n// The bash report's subject line. It used to be the literal string `<bash>`, which told the agent\n// nothing; the command itself is what was blocked, so name it — truncated, because a heredoc-bearing\n// command can run to thousands of characters and the violation lines already carry the detail.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction commandLabel(command: string): string {\n const oneLine = command.replace(/\\s+/g, ' ').trim();\n const MAX = 100;\n return oneLine.length <= MAX ? oneLine : oneLine.slice(0, MAX) + '…';\n}\n\n// When a push/PR block fires AND the config exempts vendored/nested trees, surface the escape hatch the\n// AI cannot otherwise discover: git/gh run UNGUARDED inside those trees if it cd's there first (each is\n// governed by its own repo, not this one). Scoped to pr-creation-or-push-guard — for the other guards\n// \"cd into an exempt tree\" is not the remedy — and emitted only when such trees are actually configured,\n// so a repo without exemptions never sees the noise.\n//\n// The hint MUST state the precondition, not just the remedy. \"cd into it first\" is true only for a\n// LITERAL cd at the FRONT of the same command; `D=…; cd \"$D\"; git push` lands in the identical\n// directory and still blocks (see EffectiveTreeResolver.unresolvedCd for why, and why the resolver is\n// deliberately not taught to expand `$D`). An agent that had already cd'd read the old wording as\n// evidence the exemption was broken and reasoned itself into two wrong conclusions it later had to\n// retract. Where the near-miss is detectable we say so outright, which turns the misdirect into a\n// self-correcting hint.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction exemptTreesHint(groups: readonly RuleGroup[], exemptGuards: readonly string[], command: string): string {\n if (exemptGuards.length === 0) return '';\n if (!groups.some((g: RuleGroup): boolean => g.ruleName === 'pr-creation-or-push-guard')) return '';\n\n const unresolved = new EffectiveTreeResolver().unresolvedCd(command);\n const nearMiss = unresolved === null ? ''\n : `\\n⚠️ This command DOES contain a \\`cd\\`, but the guard could not resolve it: ${unresolved}. `\n + `It was therefore judged against this repo. Re-run it as one command with a literal path in front.`;\n\n return `\\n\\nℹ️ Working in a nested repo under one of these exempt trees (${exemptGuards.join(', ')})? `\n + `Put a LITERAL \\`cd\\` at the FRONT of the SAME command — \\`cd /abs/path/to/repo && git push\\` — and git/gh `\n + `run normally there: the webpieces guards do NOT govern them (each is its own repo).`\n + `\\n The guard resolves only a leading run of literal \\`cd\\`/\\`pushd\\`, so \\`cd <literal path> && <work>\\` `\n + `is the ONE shape that moves where the command is judged. Anything ahead of the \\`cd\\` ends the scan — a `\n + `\\`VAR=…\\` assignment, or another command as in \\`git fetch && cd /x && git push\\` — and so does a `\n + `non-literal target like \\`cd \"$DIR\"\\`. In every one of those the command is judged against this repo instead.`\n + nearMiss;\n}\n\n// The set of rule names explicitly present in webpieces.config.json (every key except rulesDir).\nfunction configuredRuleNames(config: WebpiecesRulesConfig): ReadonlySet<string> {\n return new Set(Object.keys(config).filter((k: string) => k !== 'rulesDir'));\n}\n\nfunction checkConfigSync(rules: readonly Rule[], config: WebpiecesRulesConfig): BlockedResult | null {\n const configured = configuredRuleNames(config);\n const unconfiguredRules = rules.filter((r: Rule) => !configured.has(r.name));\n if (unconfiguredRules.length === 0) return null;\n\n // ONE action, no menu, no escalation — the config-validation invariant (guards/L0-tooling.md): every\n // config problem cures to \"make the file right\", and editing it is never denied. This message used\n // to tell the agent to interview the human about each rule; agents did not do it, so the block just\n // stalled. Each rule now ships a paste-ready entry at its recommended mode.\n //\n // Note this is the CONFIG-BEHIND-CODE direction. The opposite one — the config names a rule the\n // installed validator has no schema for — is unknownRuleError() in rules-config/validate-config.ts\n // and surfaces in the validation banner, not here.\n const lines = [\n // Fault Y's header lives in ./l0-matrix beside the rest of the L0 fault table (same reason as\n // CONFIG_MISSING_REPORT: one place states what this fault is and what cures it).\n CONFIG_OUT_OF_SYNC_HEADER,\n '',\n `Add an entry for each rule below to ${CONFIG_FILENAME}. Editing that file is ALWAYS allowed through`,\n 'the guard — including right now, while this block is up — so paste the entries and retry.',\n '',\n 'Each entry below is ready to paste at its recommended mode; adjust the option values if your',\n 'project needs different ones.',\n '',\n `Do NOT delete a rule from ${CONFIG_FILENAME} to silence it — an entry is REQUIRED for every rule,`,\n 'and \"mode\": \"OFF\" is how a rule is turned off.',\n '',\n ];\n\n for (const rule of unconfiguredRules) {\n lines.push(`--- ${rule.name} ---`);\n lines.push(`Description: ${rule.description}`);\n const opts = rule.defaultOptions;\n const optKeys = Object.keys(opts);\n if (optKeys.length > 0) {\n lines.push(`Available options (suggested defaults shown):`);\n for (const key of optKeys) {\n lines.push(` ${key}: ${JSON.stringify(opts[key])}`);\n }\n } else {\n lines.push('Available options: none beyond mode');\n }\n // The SAME entry the installer would seed: recommended mode, both hatches, and every other\n // schema-required field — so pasting it satisfies the loader in one pass.\n lines.push(`Entry to add to ${CONFIG_FILENAME}:`);\n lines.push(` \"${rule.name}\": ${JSON.stringify(seedEntryForRule(rule.name))}`);\n lines.push('');\n }\n\n return new BlockedResult(lines.join('\\n'));\n}\n\n// N-legs pattern: each rule runs independently so one rule can never abort the others. A rule may\n// EITHER return Violation[] OR throw — both accumulate here into visible violations the AI sees:\n// - a thrown RuleFailError → an expected, well-formed violation (its line/snippet/fixHints kept);\n// - a thrown plain Error → a \"crashed\" violation (a bug, surfaced not swallowed).\nexport function runRuleCheck(rule: Rule, ctx: EditContext | FileContext | BashContext): readonly Violation[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return rule.check(ctx);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof RuleFailError) {\n return [violationFromRuleFail(error)];\n }\n return [new Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];\n }\n}\n\n// A thrown RuleFailError carries its own AI-facing message + optional location and fix hints. Fold the\n// fix hints into the message because Violation has no fixHint field (RuleGroup's fixHint comes from the\n// rule definition, not a per-throw value).\nfunction violationFromRuleFail(error: RuleFailError): Violation {\n const hints = error.fixHints.length > 0 ? `\\n Fix: ${error.fixHints.join('\\n Fix: ')}` : '';\n return new Violation(error.line ?? 0, error.snippet ?? '', error.aiMessage + hints);\n}\n\nfunction ruleMatchesFile(rule: Rule, relativePath: string): boolean {\n for (const pattern of rule.files) {\n if (globMatches(pattern, relativePath)) return true;\n }\n return false;\n}\n\nfunction runBashRules(rules: readonly Rule[], bashContext: BashContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'bash') continue;\n if (!rule.shouldRun()) continue;\n const vs = runRuleCheck(rule, bashContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, [...vs],\n ));\n }\n }\n return groups;\n}\n\nfunction runEditRules(rules: readonly Rule[], editContexts: readonly EditContext[]): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'edit') continue;\n if (!rule.shouldRun()) continue;\n const allViolations: Violation[] = [];\n for (const ctx of editContexts) {\n if (!ruleMatchesFile(rule, ctx.relativePath)) continue;\n const vs = runRuleCheck(rule, ctx);\n for (const v of vs) {\n const copy = new Violation(v.line, v.snippet, v.message);\n copy.editIndex = ctx.editIndex;\n copy.editCount = ctx.editCount;\n allViolations.push(copy);\n }\n }\n if (allViolations.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, allViolations,\n ));\n }\n }\n return groups;\n}\n\nfunction runFileRules(rules: readonly Rule[], fileContext: FileContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'file') continue;\n if (!rule.shouldRun()) continue;\n if (!ruleMatchesFile(rule, fileContext.relativePath)) continue;\n const vs = runRuleCheck(rule, fileContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, [...vs],\n ));\n }\n }\n return groups;\n}\n"]}
|