@lannguyensi/harness 0.41.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +9 -0
- package/dist/cli/adopt/derive.d.ts +15 -0
- package/dist/cli/adopt/derive.js +34 -7
- package/dist/cli/adopt/derive.js.map +1 -1
- package/dist/cli/adopt/index.d.ts +30 -0
- package/dist/cli/adopt/index.js +23 -3
- package/dist/cli/adopt/index.js.map +1 -1
- package/dist/cli/apply/generate-settings.d.ts +14 -1
- package/dist/cli/apply/generate-settings.js +7 -3
- package/dist/cli/apply/generate-settings.js.map +1 -1
- package/dist/cli/approve/branch-protection.js +1 -1
- package/dist/cli/approve/branch-protection.js.map +1 -1
- package/dist/cli/approve/stdin-report.js +2 -0
- package/dist/cli/approve/stdin-report.js.map +1 -1
- package/dist/cli/doctor/claude-mcp.d.ts +50 -0
- package/dist/cli/doctor/claude-mcp.js +170 -0
- package/dist/cli/doctor/claude-mcp.js.map +1 -0
- package/dist/cli/doctor/format.js +37 -0
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/index.d.ts +10 -0
- package/dist/cli/doctor/index.js +24 -0
- package/dist/cli/doctor/index.js.map +1 -1
- package/dist/cli/doctor/npm-bin-path.js +42 -0
- package/dist/cli/doctor/npm-bin-path.js.map +1 -1
- package/dist/cli/doctor/types.d.ts +11 -0
- package/dist/cli/doctor/types.js.map +1 -1
- package/dist/cli/dry-run.js +13 -1
- package/dist/cli/dry-run.js.map +1 -1
- package/dist/cli/index.js +253 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/agent-tasks-auth.js +32 -0
- package/dist/cli/init/agent-tasks-auth.js.map +1 -1
- package/dist/cli/init/composer.js +9 -15
- package/dist/cli/init/composer.js.map +1 -1
- package/dist/cli/init/dependencies.js +21 -0
- package/dist/cli/init/dependencies.js.map +1 -1
- package/dist/cli/init/detect.d.ts +20 -3
- package/dist/cli/init/detect.js +34 -36
- package/dist/cli/init/detect.js.map +1 -1
- package/dist/cli/init/interactive.d.ts +33 -0
- package/dist/cli/init/interactive.js +399 -5
- package/dist/cli/init/interactive.js.map +1 -1
- package/dist/cli/init/profiles.d.ts +1 -1
- package/dist/cli/init/profiles.js +2 -2
- package/dist/cli/init/templates.d.ts +1 -1
- package/dist/cli/init/templates.js +111 -32
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/main.js +10 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/pack/hook-branch-protection.js +1 -1
- package/dist/cli/pack/hook-branch-protection.js.map +1 -1
- package/dist/cli/pack/hook-post-merge-gate-record.d.ts +30 -0
- package/dist/cli/pack/hook-post-merge-gate-record.js +153 -0
- package/dist/cli/pack/hook-post-merge-gate-record.js.map +1 -0
- package/dist/cli/pack/hook-post-merge-gate.d.ts +28 -0
- package/dist/cli/pack/hook-post-merge-gate.js +244 -0
- package/dist/cli/pack/hook-post-merge-gate.js.map +1 -0
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js +149 -3
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance.js +1 -1
- package/dist/cli/pack/hook-solution-acceptance.js.map +1 -1
- package/dist/cli/pack/understanding-report-schema-hint.js +1 -1
- package/dist/cli/pack/understanding-report-schema-hint.js.map +1 -1
- package/dist/cli/policy/intercept.js +77 -10
- package/dist/cli/policy/intercept.js.map +1 -1
- package/dist/cli/record/index.d.ts +64 -0
- package/dist/cli/record/index.js +211 -0
- package/dist/cli/record/index.js.map +1 -0
- package/dist/cli/session-start/index.d.ts +7 -0
- package/dist/cli/session-start/index.js +133 -38
- package/dist/cli/session-start/index.js.map +1 -1
- package/dist/cli/session-start/toolchain-parity.d.ts +149 -0
- package/dist/cli/session-start/toolchain-parity.js +618 -0
- package/dist/cli/session-start/toolchain-parity.js.map +1 -0
- package/dist/cli/uninstall/index.d.ts +73 -1
- package/dist/cli/uninstall/index.js +134 -2
- package/dist/cli/uninstall/index.js.map +1 -1
- package/dist/cli/validate/checks.js +2 -2
- package/dist/cli/validate/checks.js.map +1 -1
- package/dist/io/claude-mcp.d.ts +258 -0
- package/dist/io/claude-mcp.js +544 -0
- package/dist/io/claude-mcp.js.map +1 -0
- package/dist/policies/ledger-client.d.ts +34 -1
- package/dist/policies/ledger-client.js +21 -16
- package/dist/policies/ledger-client.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +1 -1
- package/dist/policy-packs/builtin/branch-protection-runtime.js +1 -1
- package/dist/policy-packs/builtin/branch-protection.js +2 -2
- package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
- package/dist/policy-packs/builtin/post-merge-gate-runtime.d.ts +195 -0
- package/dist/policy-packs/builtin/post-merge-gate-runtime.js +290 -0
- package/dist/policy-packs/builtin/post-merge-gate-runtime.js.map +1 -0
- package/dist/policy-packs/builtin/post-merge-gate.d.ts +56 -0
- package/dist/policy-packs/builtin/post-merge-gate.js +291 -0
- package/dist/policy-packs/builtin/post-merge-gate.js.map +1 -0
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js +29 -14
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/solution-acceptance.js +1 -1
- package/dist/policy-packs/registry.d.ts +1 -1
- package/dist/policy-packs/registry.js +12 -0
- package/dist/policy-packs/registry.js.map +1 -1
- package/dist/runtime/bash-match-facts.d.ts +58 -0
- package/dist/runtime/bash-match-facts.js +194 -0
- package/dist/runtime/bash-match-facts.js.map +1 -0
- package/dist/runtime/bash-match-registry.d.ts +75 -0
- package/dist/runtime/bash-match-registry.js +359 -0
- package/dist/runtime/bash-match-registry.js.map +1 -0
- package/dist/runtime/bash-prefix-parse.js +23 -0
- package/dist/runtime/bash-prefix-parse.js.map +1 -1
- package/dist/runtime/command-normalize.d.ts +92 -0
- package/dist/runtime/command-normalize.js +1182 -0
- package/dist/runtime/command-normalize.js.map +1 -0
- package/dist/runtime/git-context.d.ts +50 -0
- package/dist/runtime/git-context.js +108 -1
- package/dist/runtime/git-context.js.map +1 -1
- package/dist/runtime/hermetic-spawn-guard.d.ts +25 -0
- package/dist/runtime/hermetic-spawn-guard.js +136 -0
- package/dist/runtime/hermetic-spawn-guard.js.map +1 -0
- package/dist/runtime/intercept.d.ts +39 -1
- package/dist/runtime/intercept.js +33 -4
- package/dist/runtime/intercept.js.map +1 -1
- package/dist/runtime/ledger-writer.d.ts +27 -0
- package/dist/runtime/ledger-writer.js +46 -0
- package/dist/runtime/ledger-writer.js.map +1 -0
- package/dist/runtime/read-only-bash.d.ts +43 -0
- package/dist/runtime/read-only-bash.js +133 -3
- package/dist/runtime/read-only-bash.js.map +1 -1
- package/dist/schema/index.d.ts +41 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/policies.js +1 -1
- package/dist/schema/policies.js.map +1 -1
- package/dist/schema/toolchain-parity.d.ts +18 -0
- package/dist/schema/toolchain-parity.js +35 -0
- package/dist/schema/toolchain-parity.js.map +1 -0
- package/package.json +5 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// `harness pack hook post-merge-gate-record` — PostToolUse producer for
|
|
2
|
+
// the `post-merge-gate` policy pack.
|
|
3
|
+
//
|
|
4
|
+
// Receives Claude Code's PostToolUse event JSON on stdin. Fires only when
|
|
5
|
+
// the just-completed tool was Bash AND the command matched `gh pr merge`
|
|
6
|
+
// AND the merge is CONFIRMED by one of two contracts (see
|
|
7
|
+
// `resolveMergeConfirmation` in post-merge-gate-runtime.ts, "PAYLOAD
|
|
8
|
+
// REALITY" follow-up):
|
|
9
|
+
//
|
|
10
|
+
// Contract A (original, unchanged): `tool_output.exit_code === 0`.
|
|
11
|
+
// Contract B (2.1.218 payload reality): `tool_response` present with
|
|
12
|
+
// `interrupted === false` AND `stdout`+`stderr` containing `gh pr
|
|
13
|
+
// merge`'s own past-tense success sentence (all three merge methods).
|
|
14
|
+
//
|
|
15
|
+
// Contract A wins whenever it resolves to any definite verdict (success
|
|
16
|
+
// OR failure); Contract B is consulted only when Contract A's exit_code
|
|
17
|
+
// is entirely unresolvable. On a confirmed merge, records a
|
|
18
|
+
// `post-merge-gate:merged:<repo>:<branch>:<sha>` fact (plus PR number and
|
|
19
|
+
// timestamp, audit-only) to the evidence ledger via the same
|
|
20
|
+
// Trusted-Writer path `harness session-start branch-check` /
|
|
21
|
+
// `harness record *` use (`resolveManifestLedgerWriter` /
|
|
22
|
+
// `addLedgerFact`) — never an agent-issued `ledger_add`.
|
|
23
|
+
//
|
|
24
|
+
// `<sha>` is the LOCAL branch tip observed right after the tool ran:
|
|
25
|
+
// `gh pr merge` merges the PR on the remote side and does not itself move
|
|
26
|
+
// the local branch pointer, so this IS the exact commit that got merged
|
|
27
|
+
// (see post-merge-gate-runtime.ts's header for the full squash-fest
|
|
28
|
+
// rationale — no ancestry walk, no `git` subprocess).
|
|
29
|
+
//
|
|
30
|
+
// Every non-match / failure path is a no-op: wrong tool, non-matching
|
|
31
|
+
// command, neither contract confirms, an unresolvable git context, no
|
|
32
|
+
// session id, a manifest/ledger failure. `PostToolUse` is `blocking:false`
|
|
33
|
+
// by contract (see the pack's Hook entry) so none of these ever break the
|
|
34
|
+
// session; the only observable effect of a miss is that the blocker has
|
|
35
|
+
// no merged-tip fact to compare against for this particular merge.
|
|
36
|
+
import { buildMergedTagContent, GH_PR_MERGE_BASH_RE, PACK_NAME, resolveMergeConfirmation, } from "../../policy-packs/builtin/post-merge-gate-runtime.js";
|
|
37
|
+
import { resolveGitContext } from "../../runtime/git-context.js";
|
|
38
|
+
import { resolveManifestLedgerWriter } from "../../runtime/ledger-writer.js";
|
|
39
|
+
import { checkHookPause, loadManifestOrInjected, readStdin } from "./hook-bootstrap.js";
|
|
40
|
+
const LEDGER_SOURCE = "harness-pack-hook-post-merge-gate-record";
|
|
41
|
+
function bashCommandOf(toolInput) {
|
|
42
|
+
if (typeof toolInput !== "object" || toolInput === null)
|
|
43
|
+
return "";
|
|
44
|
+
const cmd = toolInput["command"];
|
|
45
|
+
return typeof cmd === "string" ? cmd : "";
|
|
46
|
+
}
|
|
47
|
+
export async function runPackHookPostMergeGateRecordCli(opts = {}) {
|
|
48
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
49
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
50
|
+
const note = (msg) => {
|
|
51
|
+
stderr.write(`harness pack hook post-merge-gate-record: ${msg}\n`);
|
|
52
|
+
};
|
|
53
|
+
const raw = await readStdin(stdin);
|
|
54
|
+
let event = {};
|
|
55
|
+
try {
|
|
56
|
+
event = JSON.parse(raw.trim() || "{}");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
const diagnostic = "malformed event JSON, skipping";
|
|
60
|
+
note(diagnostic);
|
|
61
|
+
return { exitCode: 0, wrote: false, diagnostic };
|
|
62
|
+
}
|
|
63
|
+
// Pause sentinel — mirrors every other pack hook's operator kill-switch.
|
|
64
|
+
if (checkHookPause(PACK_NAME, stderr, opts, opts.generatedDir).paused) {
|
|
65
|
+
const diagnostic = "harness paused; post-merge-gate-record skipping without evaluating.";
|
|
66
|
+
note(diagnostic);
|
|
67
|
+
return { exitCode: 0, wrote: false, diagnostic };
|
|
68
|
+
}
|
|
69
|
+
const toolName = typeof event.tool_name === "string" ? event.tool_name : "";
|
|
70
|
+
if (toolName !== "Bash") {
|
|
71
|
+
const diagnostic = `tool ${toolName || "(unknown)"} is not Bash, skipping`;
|
|
72
|
+
note(diagnostic);
|
|
73
|
+
return { exitCode: 0, wrote: false, diagnostic };
|
|
74
|
+
}
|
|
75
|
+
const command = bashCommandOf(event.tool_input);
|
|
76
|
+
if (!command || !GH_PR_MERGE_BASH_RE.test(command)) {
|
|
77
|
+
const diagnostic = "command did not match gh pr merge, skipping";
|
|
78
|
+
note(diagnostic);
|
|
79
|
+
return { exitCode: 0, wrote: false, diagnostic };
|
|
80
|
+
}
|
|
81
|
+
// The confirmed-success gate: neither contract confirming — including
|
|
82
|
+
// any unresolvable / unexpected payload shape on either — writes NO
|
|
83
|
+
// fact. Fail-safe against a false "merged" record, which would be a
|
|
84
|
+
// self-lock in the wrong direction (03-decisions.md). See
|
|
85
|
+
// resolveMergeConfirmation's own doc comment for the dual-contract /
|
|
86
|
+
// "Contract A wins" ordering decision.
|
|
87
|
+
const confirmation = resolveMergeConfirmation(event.tool_output, event.tool_response, command);
|
|
88
|
+
if (!confirmation.confirmed) {
|
|
89
|
+
const diagnostic = `not a confirmed merge success (${confirmation.reason}); skipping (no fact written)`;
|
|
90
|
+
note(diagnostic);
|
|
91
|
+
return { exitCode: 0, wrote: false, diagnostic };
|
|
92
|
+
}
|
|
93
|
+
const cwd = typeof opts.cwd === "string" && opts.cwd.length > 0
|
|
94
|
+
? opts.cwd
|
|
95
|
+
: typeof event.cwd === "string" && event.cwd.length > 0
|
|
96
|
+
? event.cwd
|
|
97
|
+
: process.cwd();
|
|
98
|
+
const { repo, branch, sha } = resolveGitContext(cwd);
|
|
99
|
+
if (repo === "" || branch === "" || sha === "") {
|
|
100
|
+
const diagnostic = `cannot resolve git context for ${cwd} ` +
|
|
101
|
+
`(repo=${JSON.stringify(repo)} branch=${JSON.stringify(branch)} sha=${JSON.stringify(sha)}); ` +
|
|
102
|
+
`skipping (no fact written)`;
|
|
103
|
+
note(diagnostic);
|
|
104
|
+
return { exitCode: 0, wrote: false, diagnostic };
|
|
105
|
+
}
|
|
106
|
+
const sessionId = (typeof event.session_id === "string" && event.session_id.length > 0
|
|
107
|
+
? event.session_id
|
|
108
|
+
: undefined) ??
|
|
109
|
+
process.env.CLAUDE_CODE_SESSION_ID ??
|
|
110
|
+
process.env.CLAUDE_SESSION_ID ??
|
|
111
|
+
"";
|
|
112
|
+
if (sessionId === "") {
|
|
113
|
+
const diagnostic = "no session_id resolvable from stdin or $CLAUDE_CODE_SESSION_ID/$CLAUDE_SESSION_ID; skipping (no fact written)";
|
|
114
|
+
note(diagnostic);
|
|
115
|
+
return { exitCode: 0, wrote: false, diagnostic };
|
|
116
|
+
}
|
|
117
|
+
// PR number: resolveMergeConfirmation already applied the binding
|
|
118
|
+
// resolution order (command-first, gh-success-sentence fallback only
|
|
119
|
+
// for Contract B; Contract A stays command-only, unchanged).
|
|
120
|
+
const whenIso = (opts.now ?? new Date()).toISOString();
|
|
121
|
+
const content = buildMergedTagContent({ repo, branch, sha, pr: confirmation.pr, whenIso });
|
|
122
|
+
let writeLedger = opts.writeLedger;
|
|
123
|
+
if (!writeLedger) {
|
|
124
|
+
let manifest;
|
|
125
|
+
try {
|
|
126
|
+
({ manifest } = loadManifestOrInjected(opts, opts.manifest));
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const diagnostic = `manifest load failed (${err.message}); skipping (no fact written)`;
|
|
130
|
+
note(diagnostic);
|
|
131
|
+
return { exitCode: 0, wrote: false, diagnostic };
|
|
132
|
+
}
|
|
133
|
+
const resolved = resolveManifestLedgerWriter(manifest, {
|
|
134
|
+
...(opts.ledgerTimeoutMs !== undefined ? { ledgerTimeoutMs: opts.ledgerTimeoutMs } : {}),
|
|
135
|
+
});
|
|
136
|
+
if (!resolved.ok) {
|
|
137
|
+
const diagnostic = `${resolved.reason}; cannot record ${content}`;
|
|
138
|
+
note(diagnostic);
|
|
139
|
+
return { exitCode: 0, wrote: false, diagnostic };
|
|
140
|
+
}
|
|
141
|
+
writeLedger = resolved.write;
|
|
142
|
+
}
|
|
143
|
+
const result = await writeLedger({ sessionId, content, source: LEDGER_SOURCE });
|
|
144
|
+
if (!result.ok) {
|
|
145
|
+
const diagnostic = `ledger write failed: ${result.reason ?? "unknown error"}`;
|
|
146
|
+
note(diagnostic);
|
|
147
|
+
return { exitCode: 0, wrote: false, diagnostic };
|
|
148
|
+
}
|
|
149
|
+
const diagnostic = `recorded ${content} for session ${sessionId}`;
|
|
150
|
+
note(diagnostic);
|
|
151
|
+
return { exitCode: 0, wrote: true, diagnostic };
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=hook-post-merge-gate-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-post-merge-gate-record.js","sourceRoot":"","sources":["../../../src/cli/pack/hook-post-merge-gate-record.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type LedgerEntry } from "../../policies/index.js";
|
|
2
|
+
import { type Manifest } from "../../schema/index.js";
|
|
3
|
+
import { type LoaderOptions } from "../loader.js";
|
|
4
|
+
export interface PackHookPostMergeGateOptions extends LoaderOptions {
|
|
5
|
+
/** Defaults to process.stdin. */
|
|
6
|
+
stdin?: NodeJS.ReadableStream;
|
|
7
|
+
/** Defaults to process.stdout. */
|
|
8
|
+
stdout?: NodeJS.WritableStream;
|
|
9
|
+
/** Defaults to process.stderr. */
|
|
10
|
+
stderr?: NodeJS.WritableStream;
|
|
11
|
+
/** Override cwd resolution (test injection). Falls back to event.cwd then process.cwd(). */
|
|
12
|
+
cwd?: string;
|
|
13
|
+
/** Per-call ledger timeout in ms. */
|
|
14
|
+
ledgerTimeoutMs?: number;
|
|
15
|
+
/** Inject a manifest (test). */
|
|
16
|
+
manifest?: Manifest;
|
|
17
|
+
/** Inject a fake ledger query (test). */
|
|
18
|
+
ledgerQuery?: (sessionId: string) => Promise<LedgerEntry[] | {
|
|
19
|
+
degraded: string;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export interface PackHookPostMergeGateResult {
|
|
23
|
+
exitCode: number;
|
|
24
|
+
blocked: boolean;
|
|
25
|
+
/** Diagnostic line emitted to stderr (always, even on allow). */
|
|
26
|
+
diagnostic: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function runPackHookPostMergeGateCli(opts?: PackHookPostMergeGateOptions): Promise<PackHookPostMergeGateResult>;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// `harness pack hook post-merge-gate` — PreToolUse blocker for the
|
|
2
|
+
// `post-merge-gate` policy pack.
|
|
3
|
+
//
|
|
4
|
+
// Receives Claude Code's PreToolUse event JSON on stdin and emits a
|
|
5
|
+
// `{ decision: "block" }` envelope when a CURATED history-mutating Bash
|
|
6
|
+
// command is about to run on a branch whose current tip exactly matches
|
|
7
|
+
// a merged-tip fact the producer recorded (see hook-post-merge-gate-record.ts).
|
|
8
|
+
//
|
|
9
|
+
// Evaluation order — deliberate, and pinned by the self-lock test suite:
|
|
10
|
+
//
|
|
11
|
+
// 1. Parse stdin. Malformed JSON, a non-Bash tool, or an empty command
|
|
12
|
+
// all ALLOW immediately (nothing to classify).
|
|
13
|
+
// 2. Pause sentinel (operator kill-switch) — same convention as every
|
|
14
|
+
// other pack hook.
|
|
15
|
+
// 3. ESCAPE ALLOWLIST — checked BEFORE manifest load, pack lookup, or
|
|
16
|
+
// any ledger query. `git switch`/`checkout`/`pull`/`fetch`,
|
|
17
|
+
// `git branch -d`/`-D`, `git stash list`/`show`, and any
|
|
18
|
+
// `harness ...` command ALWAYS pass, unconditionally, with NO
|
|
19
|
+
// dependency on whether the manifest or the ledger are reachable.
|
|
20
|
+
// This is the recovery path the deny message itself recommends; it
|
|
21
|
+
// must never be starved by an unrelated failure (the incident class
|
|
22
|
+
// this guards against: a different gate once got blocked alongside
|
|
23
|
+
// its own recovery command, and only the operator could unstick it).
|
|
24
|
+
// 4. CURATED MUTATION MATCH — commands outside the curated list
|
|
25
|
+
// (git commit/add/push/merge/rebase/cherry-pick/revert/reset/
|
|
26
|
+
// stash pop|apply, gh pr create/merge) pass through untouched.
|
|
27
|
+
// 5. Manifest load, pack-enabled check, git-context resolution
|
|
28
|
+
// (detached HEAD / no-repo ALLOWS — nothing to compare), then the
|
|
29
|
+
// ledger query.
|
|
30
|
+
// 6. DENY only when the current branch tip exactly matches a recorded
|
|
31
|
+
// `post-merge-gate:merged:<repo>:<branch>:<sha>` fact.
|
|
32
|
+
//
|
|
33
|
+
// Fail posture: OPEN. A manifest-load failure or a degraded ledger both
|
|
34
|
+
// ALLOW (with a stderr warning) — the inverse of branch-protection's
|
|
35
|
+
// fail-closed contract. Without the ledger, "merged" and "not merged" are
|
|
36
|
+
// indistinguishable, and fail-closed here would block ordinary git
|
|
37
|
+
// history work on every branch whenever grounding-mcp hiccups. See
|
|
38
|
+
// post-merge-gate-runtime.ts's header for the full rationale.
|
|
39
|
+
import { isCuratedMutationCommand, isEscapeCommand, MERGED_TAG_PREFIX, mergedTagMatchKey, PACK_NAME, resolveDefaultBranchName, } from "../../policy-packs/builtin/post-merge-gate-runtime.js";
|
|
40
|
+
import { queryLedgerByTag } from "../../policies/index.js";
|
|
41
|
+
import { resolveGitContext } from "../../runtime/git-context.js";
|
|
42
|
+
import { renderAgentFacing } from "../../runtime/agent-facing.js";
|
|
43
|
+
import { POLICY_DECISION_TYPE } from "../../runtime/ledger-record.js";
|
|
44
|
+
import { checkHookPause, loadManifestOrInjected, parseConfigUx, readStdin } from "./hook-bootstrap.js";
|
|
45
|
+
const DEFAULT_BRANCH_PLACEHOLDER = "<default-branch>";
|
|
46
|
+
function bashCommandOf(toolInput) {
|
|
47
|
+
if (typeof toolInput !== "object" || toolInput === null)
|
|
48
|
+
return "";
|
|
49
|
+
const cmd = toolInput["command"];
|
|
50
|
+
return typeof cmd === "string" ? cmd : "";
|
|
51
|
+
}
|
|
52
|
+
function findGroundingMcp(manifest) {
|
|
53
|
+
return manifest.tools.mcp.find((m) => m.name === "grounding-mcp") ?? null;
|
|
54
|
+
}
|
|
55
|
+
function evaluateEntries(entries, matchKey) {
|
|
56
|
+
for (const e of entries) {
|
|
57
|
+
// Skip policy_decision audit rows — a past DENIED decision this very
|
|
58
|
+
// pack recorded would otherwise incidentally carry the match key in
|
|
59
|
+
// its serialized payload, falsely satisfying itself. Same two-tier
|
|
60
|
+
// filter branch-protection's blocker uses (type first, legacy
|
|
61
|
+
// content-prefix backstop second).
|
|
62
|
+
if (e.type === POLICY_DECISION_TYPE)
|
|
63
|
+
continue;
|
|
64
|
+
if (e.content.startsWith(`${POLICY_DECISION_TYPE}:`))
|
|
65
|
+
continue;
|
|
66
|
+
if (e.content.includes(matchKey)) {
|
|
67
|
+
return { merged: true, matchedContent: e.content, degraded: null };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { merged: false, matchedContent: null, degraded: null };
|
|
71
|
+
}
|
|
72
|
+
async function probeLedger(manifest, sessionId, matchKey, opts) {
|
|
73
|
+
if (opts.ledgerQuery) {
|
|
74
|
+
const r = await opts.ledgerQuery(sessionId);
|
|
75
|
+
if ("degraded" in r) {
|
|
76
|
+
return { merged: false, matchedContent: null, degraded: r.degraded };
|
|
77
|
+
}
|
|
78
|
+
return evaluateEntries(r, matchKey);
|
|
79
|
+
}
|
|
80
|
+
if (!manifest) {
|
|
81
|
+
return { merged: false, matchedContent: null, degraded: "manifest unavailable" };
|
|
82
|
+
}
|
|
83
|
+
const server = findGroundingMcp(manifest);
|
|
84
|
+
if (!server) {
|
|
85
|
+
return { merged: false, matchedContent: null, degraded: "grounding-mcp not declared in manifest" };
|
|
86
|
+
}
|
|
87
|
+
const command = Array.isArray(server.command)
|
|
88
|
+
? server.command
|
|
89
|
+
: server.command.trim().split(/\s+/);
|
|
90
|
+
const env = server.env ?? undefined;
|
|
91
|
+
const timeoutMs = opts.ledgerTimeoutMs ?? server.health?.timeout_ms ?? 5_000;
|
|
92
|
+
const result = await queryLedgerByTag({
|
|
93
|
+
mcpCommand: command,
|
|
94
|
+
...(env && { mcpEnv: env }),
|
|
95
|
+
sessionId,
|
|
96
|
+
timeoutMs,
|
|
97
|
+
});
|
|
98
|
+
if (result.kind === "degraded") {
|
|
99
|
+
return { merged: false, matchedContent: null, degraded: result.reason };
|
|
100
|
+
}
|
|
101
|
+
return evaluateEntries(result.entries, matchKey);
|
|
102
|
+
}
|
|
103
|
+
function blockJson(toolName, command, branch, defaultBranch, matchedContent, ux, sessionId) {
|
|
104
|
+
let reasonText;
|
|
105
|
+
if (ux) {
|
|
106
|
+
reasonText = renderAgentFacing(ux, {
|
|
107
|
+
BRANCH: branch,
|
|
108
|
+
DEFAULT_BRANCH: defaultBranch,
|
|
109
|
+
TOOL_NAME: toolName,
|
|
110
|
+
SESSION_ID: sessionId,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
reasonText =
|
|
115
|
+
`post-merge-gate: refusing ${toolName} ("${command}") on branch "${branch}" — ` +
|
|
116
|
+
`its current tip was already merged (recorded: ${matchedContent}).\n` +
|
|
117
|
+
`Further history-mutating commands on an already-merged branch usually mean stale local state.\n` +
|
|
118
|
+
`\n` +
|
|
119
|
+
`To proceed:\n` +
|
|
120
|
+
` git switch ${defaultBranch}\n` +
|
|
121
|
+
` git pull --ff-only\n` +
|
|
122
|
+
` git branch -d ${branch} # optional cleanup\n` +
|
|
123
|
+
`\n` +
|
|
124
|
+
`If you have NEW work to continue on this branch (not leftover local state): make a commit first —` +
|
|
125
|
+
` this gate only fires while the branch tip still exactly matches the merged commit, so a new commit` +
|
|
126
|
+
` moves the tip and this check no longer applies.\n` +
|
|
127
|
+
`\n` +
|
|
128
|
+
`Escape hatches (always allowed, independent of this gate): git switch/checkout, git pull/fetch,` +
|
|
129
|
+
` git branch -d/-D, git stash list/show, and any \`harness ...\` command.`;
|
|
130
|
+
}
|
|
131
|
+
return JSON.stringify({
|
|
132
|
+
decision: "block",
|
|
133
|
+
reason: reasonText,
|
|
134
|
+
hookSpecificOutput: {
|
|
135
|
+
hookEventName: "PreToolUse",
|
|
136
|
+
permissionDecision: "deny",
|
|
137
|
+
permissionDecisionReason: reasonText,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
export async function runPackHookPostMergeGateCli(opts = {}) {
|
|
142
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
143
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
144
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
145
|
+
const note = (msg) => {
|
|
146
|
+
stderr.write(`harness pack hook post-merge-gate: ${msg}\n`);
|
|
147
|
+
};
|
|
148
|
+
const raw = await readStdin(stdin);
|
|
149
|
+
let event = {};
|
|
150
|
+
try {
|
|
151
|
+
event = JSON.parse(raw.trim() || "{}");
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
const diagnostic = "malformed event JSON, cannot classify; allowing";
|
|
155
|
+
note(diagnostic);
|
|
156
|
+
return { exitCode: 0, blocked: false, diagnostic };
|
|
157
|
+
}
|
|
158
|
+
// Pause sentinel — even this gate yields to an operator pause.
|
|
159
|
+
if (checkHookPause(PACK_NAME, stderr, opts).paused) {
|
|
160
|
+
const diagnostic = "harness paused; post-merge-gate allowing without evaluating.";
|
|
161
|
+
return { exitCode: 0, blocked: false, diagnostic };
|
|
162
|
+
}
|
|
163
|
+
const sessionId = (typeof event.session_id === "string" ? event.session_id : undefined) ??
|
|
164
|
+
process.env.CLAUDE_CODE_SESSION_ID ??
|
|
165
|
+
process.env.CLAUDE_SESSION_ID ??
|
|
166
|
+
"";
|
|
167
|
+
const toolName = typeof event.tool_name === "string" ? event.tool_name : "(unknown)";
|
|
168
|
+
const cwd = typeof opts.cwd === "string" && opts.cwd.length > 0
|
|
169
|
+
? opts.cwd
|
|
170
|
+
: typeof event.cwd === "string" && event.cwd.length > 0
|
|
171
|
+
? event.cwd
|
|
172
|
+
: process.cwd();
|
|
173
|
+
if (toolName !== "Bash") {
|
|
174
|
+
const diagnostic = `${toolName} is not Bash; allowing`;
|
|
175
|
+
note(diagnostic);
|
|
176
|
+
return { exitCode: 0, blocked: false, diagnostic };
|
|
177
|
+
}
|
|
178
|
+
const command = bashCommandOf(event.tool_input);
|
|
179
|
+
if (command === "") {
|
|
180
|
+
const diagnostic = "no resolvable command text; allowing";
|
|
181
|
+
note(diagnostic);
|
|
182
|
+
return { exitCode: 0, blocked: false, diagnostic };
|
|
183
|
+
}
|
|
184
|
+
// ESCAPE ALLOWLIST — checked FIRST, unconditionally, before manifest
|
|
185
|
+
// load, pack lookup, or any ledger query. See module header.
|
|
186
|
+
if (isEscapeCommand(command)) {
|
|
187
|
+
const diagnostic = `escape command matched; allowing regardless of ledger/manifest state`;
|
|
188
|
+
note(diagnostic);
|
|
189
|
+
return { exitCode: 0, blocked: false, diagnostic };
|
|
190
|
+
}
|
|
191
|
+
if (!isCuratedMutationCommand(command)) {
|
|
192
|
+
const diagnostic = `command is not in the curated v1 deny-scope; allowing`;
|
|
193
|
+
note(diagnostic);
|
|
194
|
+
return { exitCode: 0, blocked: false, diagnostic };
|
|
195
|
+
}
|
|
196
|
+
// Fail-open manifest load (opposite of branch-protection's fail-closed):
|
|
197
|
+
// without the manifest we cannot tell if the pack is even enabled, and
|
|
198
|
+
// this pack's whole posture is advisory, not a hard security boundary.
|
|
199
|
+
let manifest;
|
|
200
|
+
try {
|
|
201
|
+
({ manifest } = loadManifestOrInjected(opts, opts.manifest));
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
const diagnostic = `manifest load failed (${err.message}); post-merge-gate fails open, allowing`;
|
|
205
|
+
note(diagnostic);
|
|
206
|
+
return { exitCode: 0, blocked: false, diagnostic };
|
|
207
|
+
}
|
|
208
|
+
const pack = manifest.policy_packs.find((p) => p.name === PACK_NAME);
|
|
209
|
+
if (!pack) {
|
|
210
|
+
const diagnostic = `pack "${PACK_NAME}" not declared in manifest, allowing`;
|
|
211
|
+
note(diagnostic);
|
|
212
|
+
return { exitCode: 0, blocked: false, diagnostic };
|
|
213
|
+
}
|
|
214
|
+
if (!pack.enabled) {
|
|
215
|
+
const diagnostic = `pack "${PACK_NAME}" is enabled:false, allowing`;
|
|
216
|
+
note(diagnostic);
|
|
217
|
+
return { exitCode: 0, blocked: false, diagnostic };
|
|
218
|
+
}
|
|
219
|
+
const { repo, branch, sha } = resolveGitContext(cwd);
|
|
220
|
+
if (branch === "" || sha === "" || repo === "") {
|
|
221
|
+
const diagnostic = `cannot resolve git context for ${cwd} (detached HEAD, outside a git work tree, or unresolvable sha); allowing`;
|
|
222
|
+
note(diagnostic);
|
|
223
|
+
return { exitCode: 0, blocked: false, diagnostic };
|
|
224
|
+
}
|
|
225
|
+
const configUx = parseConfigUx(pack.config["ux"], stderr, "harness pack hook post-merge-gate");
|
|
226
|
+
const matchKey = mergedTagMatchKey(repo, branch, sha);
|
|
227
|
+
const check = await probeLedger(manifest, sessionId, matchKey, opts);
|
|
228
|
+
if (check.degraded !== null) {
|
|
229
|
+
const diagnostic = `ledger degraded (${check.degraded}); post-merge-gate fails open, allowing`;
|
|
230
|
+
note(diagnostic);
|
|
231
|
+
return { exitCode: 0, blocked: false, diagnostic };
|
|
232
|
+
}
|
|
233
|
+
if (!check.merged) {
|
|
234
|
+
const diagnostic = `no recorded ${MERGED_TAG_PREFIX} fact for ${repo}:${branch} at tip ${sha.slice(0, 7)}; allowing`;
|
|
235
|
+
note(diagnostic);
|
|
236
|
+
return { exitCode: 0, blocked: false, diagnostic };
|
|
237
|
+
}
|
|
238
|
+
const defaultBranch = resolveDefaultBranchName(cwd) ?? DEFAULT_BRANCH_PLACEHOLDER;
|
|
239
|
+
const diagnostic = `BLOCK — branch tip ${sha.slice(0, 7)} matches a recorded merged tip (${check.matchedContent ?? matchKey})`;
|
|
240
|
+
note(diagnostic);
|
|
241
|
+
stdout.write(`${blockJson(toolName, command, branch, defaultBranch, check.matchedContent ?? matchKey, configUx, sessionId)}\n`);
|
|
242
|
+
return { exitCode: 0, blocked: true, diagnostic };
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=hook-post-merge-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-post-merge-gate.js","sourceRoot":"","sources":["../../../src/cli/pack/hook-post-merge-gate.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,6 +17,40 @@
|
|
|
17
17
|
// (covers `echo >`, `$SOLUTION_VERDICT_DIR` spellings, `tee`, `mv`/`cp`/
|
|
18
18
|
// `ln`/`install`, `python3 -c '...path...'`, and `chmod`/`chattr` that
|
|
19
19
|
// would loosen perms) — or whose shell cwd is inside the dir.
|
|
20
|
+
// - Bash `cd` whose RESOLVED target is inside the verdict dir. `cd` itself
|
|
21
|
+
// is provably read-only (task fb67b402 added it to the shared
|
|
22
|
+
// read-only-bash floor: it mutates only the invoking shell's own cwd,
|
|
23
|
+
// never the filesystem), so it no longer reaches the "not read-only"
|
|
24
|
+
// checks below. But stepping into the dir is the setup half of a
|
|
25
|
+
// two-call forge (`cd <dir>` then, in a LATER un-chained Bash call,
|
|
26
|
+
// a bare relative write): this pre-check keeps that first step blocked
|
|
27
|
+
// as defense-in-depth, ahead of (not instead of) the cwd-inside check
|
|
28
|
+
// below, which is what actually closes the later relative-write call
|
|
29
|
+
// once cwd has genuinely moved. A `cd` target that `path.resolve`
|
|
30
|
+
// cannot literally evaluate does NOT get `cd`'s read-only fast path
|
|
31
|
+
// either: it falls through to the same reference-based checks as any
|
|
32
|
+
// other non-read-only Bash. `CD_TARGET_UNRESOLVABLE_CHARS` documents
|
|
33
|
+
// exactly WHICH expansions this enumerates (quoting, `$`/env-var,
|
|
34
|
+
// `~`, glob, and `{...}` brace expansion) — that list is what this
|
|
35
|
+
// check catches, not a claim that every shell-expansion class is
|
|
36
|
+
// closed. Known-open residual, pre-existing (not introduced by this
|
|
37
|
+
// branch, since it applies to `bashReferencesVerdictDir`'s textual
|
|
38
|
+
// match on ANY Bash command, not just `cd`): a backslash-escaped
|
|
39
|
+
// spelling of the leaf, e.g. `cd <parent>/solution\-verdicts`, still
|
|
40
|
+
// navigates into the dir under bash (verified against bash 3.2.57)
|
|
41
|
+
// while `bashReferencesVerdictDir` returns false — the backslash
|
|
42
|
+
// breaks the literal-leaf substring match and no metacharacter here
|
|
43
|
+
// triggers the glob/brace fallback either. Closing it needs
|
|
44
|
+
// shell-unescaping in the textual check itself, which is more than
|
|
45
|
+
// this task absorbs; do not assume the class is closed.
|
|
46
|
+
// A SECOND residual of the same class, and on a macOS operator
|
|
47
|
+
// machine the more reachable of the two: case variance.
|
|
48
|
+
// `cd <parent>/SOLUTION-VERDICTS` navigates into the dir on a
|
|
49
|
+
// case-insensitive APFS volume, but `isInsideDir` compares with a
|
|
50
|
+
// case-sensitive `path.relative` and the textual check uses a
|
|
51
|
+
// case-sensitive `includes`, so both return false. Also allowed:
|
|
52
|
+
// a trailing backslash, `cd <DIR>\`. Both pre-existing; closing
|
|
53
|
+
// them needs case-folding in the textual check.
|
|
20
54
|
//
|
|
21
55
|
// Pure reads (`cat <dir>/x.json`) are allowed so the guard is not over-broad.
|
|
22
56
|
//
|
|
@@ -30,7 +64,10 @@
|
|
|
30
64
|
// write paths above. A path constructed at runtime inside an interpreter
|
|
31
65
|
// with no textual reference is NOT caught; marker signing (a cross-repo
|
|
32
66
|
// follow-up) closes content-authenticity against an unguarded write
|
|
33
|
-
// primitive.
|
|
67
|
+
// primitive. The `cd`-target check added later (task fb67b402) has its own
|
|
68
|
+
// enumerated, not exhaustive, expansion list — see the bullet above and
|
|
69
|
+
// `CD_TARGET_UNRESOLVABLE_CHARS`'s docstring for the backslash-escaping
|
|
70
|
+
// residual specifically.
|
|
34
71
|
import { bashReferencesVerdictDir, isInsideDir, PACK_NAME, verdictDir as resolveVerdictDir, } from "../../policy-packs/builtin/solution-acceptance-runtime.js";
|
|
35
72
|
import { isReadOnlyBashCommand } from "../../runtime/read-only-bash.js";
|
|
36
73
|
import { checkHookPause, readStdin } from "./hook-bootstrap.js";
|
|
@@ -60,6 +97,94 @@ function bashCommandOf(toolInput) {
|
|
|
60
97
|
const cmd = toolInput["command"];
|
|
61
98
|
return typeof cmd === "string" ? cmd : "";
|
|
62
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Extract `cd`'s destination argument from a Bash command string, or null
|
|
102
|
+
* when the command is not a `cd` invocation or has no statically resolvable
|
|
103
|
+
* destination. Whitespace-tokenized, same as `read-only-bash.ts`'s own
|
|
104
|
+
* classifier (this hook does not shell-parse either).
|
|
105
|
+
*
|
|
106
|
+
* Deliberately narrow:
|
|
107
|
+
* - Only fires when the FIRST token is exactly `cd`. `env cd x` or a
|
|
108
|
+
* chained `foo && cd x` do not match here (tokens[0] would be `env` /
|
|
109
|
+
* `foo`); those are unaffected by this pre-check and are still governed
|
|
110
|
+
* by the existing not-read-only + `bashReferencesVerdictDir` checks
|
|
111
|
+
* below, which already close the `cd <parent> && write <relative>`
|
|
112
|
+
* descent via the leaf-segment match.
|
|
113
|
+
* - `cd`'s own flags (`-L`, `-P`, `-e`, `-@`, and combinations) are
|
|
114
|
+
* skipped to find the first positional path argument.
|
|
115
|
+
* - `cd -` (switch to `$OLDPWD`) and a bare `cd` (goes to `$HOME`) have no
|
|
116
|
+
* destination this function can resolve without ambient shell state;
|
|
117
|
+
* both return null (not flagged), the same conservative stance as the
|
|
118
|
+
* rest of this module's enumerated-path checks.
|
|
119
|
+
* - A single layer of surrounding quotes (`"..."` or `'...'`) is stripped
|
|
120
|
+
* from the returned token before the caller does anything with it, so
|
|
121
|
+
* `cd "<path>"` and `cd <path>` are treated identically (both the
|
|
122
|
+
* resolved-path inside-dir check AND the unresolvable-chars check see
|
|
123
|
+
* the same unquoted value). Without this, a quoted literal that merely
|
|
124
|
+
* shares a text prefix with the verdict dir (`cd "<dir>-decoy"`) would
|
|
125
|
+
* route to the textual leaf match and be wrongly blocked, while the
|
|
126
|
+
* unquoted `cd <dir>-decoy` correctly passes via the resolved-path
|
|
127
|
+
* check — spelling-dependent behavior for what is the same destination.
|
|
128
|
+
*/
|
|
129
|
+
function cdTargetArgument(command) {
|
|
130
|
+
const tokens = command.trim().split(/\s+/);
|
|
131
|
+
if (tokens[0] !== "cd")
|
|
132
|
+
return null;
|
|
133
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
134
|
+
const t = tokens[i];
|
|
135
|
+
if (t === undefined)
|
|
136
|
+
break;
|
|
137
|
+
if (t === "-")
|
|
138
|
+
return null;
|
|
139
|
+
if (t.startsWith("-"))
|
|
140
|
+
continue;
|
|
141
|
+
return stripSurroundingQuotes(t);
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Strip one layer of surrounding double or single quotes from `token`, or
|
|
147
|
+
* return it unchanged if it is not fully wrapped in a matching pair. Does
|
|
148
|
+
* NOT attempt any other shell-quoting semantics (no backslash-escape
|
|
149
|
+
* handling, no nested-quote handling) — this is a narrow normalization so a
|
|
150
|
+
* simple quoted literal reads the same as its unquoted form, not a shell
|
|
151
|
+
* parser.
|
|
152
|
+
*/
|
|
153
|
+
function stripSurroundingQuotes(token) {
|
|
154
|
+
if (token.length >= 2 &&
|
|
155
|
+
((token.startsWith('"') && token.endsWith('"')) ||
|
|
156
|
+
(token.startsWith("'") && token.endsWith("'")))) {
|
|
157
|
+
return token.slice(1, -1);
|
|
158
|
+
}
|
|
159
|
+
return token;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Characters/sequences that make a `cd` destination token something
|
|
163
|
+
* `path.resolve` cannot literally evaluate: quoting (`"`, `'` — the residual
|
|
164
|
+
* case after `stripSurroundingQuotes` has already removed a clean single
|
|
165
|
+
* layer, e.g. an unbalanced or embedded quote), env-var expansion (`$`),
|
|
166
|
+
* home-directory expansion (`~`), shell globbing (`*`, `?`, `[`), and brace
|
|
167
|
+
* expansion (`{`, `}`, `,`). `path.resolve` treats the raw token as a
|
|
168
|
+
* literal path segment, so `cd "$SOLUTION_VERDICT_DIR"`, `cd
|
|
169
|
+
* ~/.../solution-verdicts`, or `cd <parent>/{solution-verdicts,x}` all
|
|
170
|
+
* resolve to a nonsense path that is never "inside" the dir even though a
|
|
171
|
+
* real shell would expand each of them TO (or, for the brace form, INTO)
|
|
172
|
+
* the dir. A `cd` target containing any of these must therefore NOT take
|
|
173
|
+
* the read-only fast path in `evaluateWriteGuard`: control must fall
|
|
174
|
+
* through to the same `bashReferencesVerdictDir` text-reference check any
|
|
175
|
+
* other non-read-only Bash command goes through, which DOES recognize
|
|
176
|
+
* these spellings (the env-var token, the `agent-grounding/solution-verdicts`
|
|
177
|
+
* tail, and — after the matching widening in
|
|
178
|
+
* `solution-acceptance-runtime.ts`'s glob-metacharacter test — glob- and
|
|
179
|
+
* brace-obscured leaf words).
|
|
180
|
+
*
|
|
181
|
+
* This list is enumerated, not exhaustive: it is exactly the expansion
|
|
182
|
+
* classes this check has been reasoned about, not a claim that every way a
|
|
183
|
+
* shell can rewrite a `cd` argument is covered. A known-open residual
|
|
184
|
+
* (pre-existing, not introduced by this check) is backslash escaping — see
|
|
185
|
+
* the module header's "Known-open residual" note.
|
|
186
|
+
*/
|
|
187
|
+
const CD_TARGET_UNRESOLVABLE_CHARS = /[$~*?[{},"']/;
|
|
63
188
|
/**
|
|
64
189
|
* Pure write-guard decision for a tool event. Exported for direct unit
|
|
65
190
|
* testing of the full forge-attempt matrix without spinning up the CLI.
|
|
@@ -100,7 +225,28 @@ export function evaluateWriteGuard(toolName, toolInput, dir, cwd) {
|
|
|
100
225
|
const command = bashCommandOf(toolInput);
|
|
101
226
|
if (command === "")
|
|
102
227
|
return { blocked: false, reason: "empty Bash command" };
|
|
103
|
-
|
|
228
|
+
// Defense-in-depth, ahead of the read-only fast path below: `cd` itself
|
|
229
|
+
// cannot write, but `cd`ing into the verdict dir is the setup half of a
|
|
230
|
+
// two-call forge (see the module header). Only a RESOLVED-path check
|
|
231
|
+
// (via the shared `isInsideDir`, not a substring match) so a sibling
|
|
232
|
+
// that merely shares a text prefix with the dir (`solution-verdicts-decoy`,
|
|
233
|
+
// or a parent directory of it) is not wrongly flagged.
|
|
234
|
+
const cdTarget = cdTargetArgument(command);
|
|
235
|
+
if (cdTarget !== null && isInsideDir(cdTarget, dir, cwd)) {
|
|
236
|
+
return {
|
|
237
|
+
blocked: true,
|
|
238
|
+
reason: `cd targets the harness-protected solution-verdict dir (${dir}); stepping into it would set up a later un-chained relative write to forge the verdict marker`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// A `cd` target containing one of the enumerated unresolvable
|
|
242
|
+
// expansions (see `CD_TARGET_UNRESOLVABLE_CHARS`'s docstring for the
|
|
243
|
+
// exact list and its non-exhaustive scope) must not take `cd`'s
|
|
244
|
+
// read-only fast path below: without this, `cd "$SOLUTION_VERDICT_DIR"`
|
|
245
|
+
// etc. would be waved through by `isReadOnlyBashCommand` (true for ANY
|
|
246
|
+
// `cd`, per the shared read-only-bash floor) before ever reaching the
|
|
247
|
+
// reference check that recognizes these exact spellings.
|
|
248
|
+
const cdTargetUnresolvable = cdTarget !== null && CD_TARGET_UNRESOLVABLE_CHARS.test(cdTarget);
|
|
249
|
+
if (!cdTargetUnresolvable && isReadOnlyBashCommand(command)) {
|
|
104
250
|
return { blocked: false, reason: "read-only Bash command" };
|
|
105
251
|
}
|
|
106
252
|
if (isInsideDir(".", dir, cwd)) {
|
|
@@ -123,7 +269,7 @@ function blockJson(toolName, reason) {
|
|
|
123
269
|
const text = `solution-acceptance write-guard: refusing ${toolName}. ${reason}.\n` +
|
|
124
270
|
`The solution-acceptance verdict marker is derived by the producer from a ` +
|
|
125
271
|
`real preflight run; hand-writing it would forge a green "done". ` +
|
|
126
|
-
`Run \`
|
|
272
|
+
`Run \`mcp__grounding-mcp__solution_evaluate({ id: "<task-id>" })\` instead, ` +
|
|
127
273
|
`which writes the marker for you.\n` +
|
|
128
274
|
`Operator override: \`harness pause\`.`;
|
|
129
275
|
return JSON.stringify({
|