@lannguyensi/harness 0.32.0 → 0.33.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 +20 -0
- package/dist/cli/approve/branch-protection.d.ts +69 -0
- package/dist/cli/approve/branch-protection.js +157 -0
- package/dist/cli/approve/branch-protection.js.map +1 -0
- package/dist/cli/index.js +55 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/composer.js +11 -5
- package/dist/cli/init/composer.js.map +1 -1
- package/dist/cli/init/profiles.d.ts +2 -2
- package/dist/cli/init/profiles.js +2 -2
- package/dist/cli/init/templates.d.ts +1 -1
- package/dist/cli/init/templates.js +8 -4
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/pack/hook-branch-protection.d.ts +8 -0
- package/dist/cli/pack/hook-branch-protection.js +59 -15
- package/dist/cli/pack/hook-branch-protection.js.map +1 -1
- package/dist/cli/pack/hook-pre-tool-use.js +31 -2
- package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance.d.ts +2 -0
- package/dist/cli/pack/hook-solution-acceptance.js +24 -10
- package/dist/cli/pack/hook-solution-acceptance.js.map +1 -1
- package/dist/cli/pack/read-only-bash.js +127 -4
- package/dist/cli/pack/read-only-bash.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +47 -6
- package/dist/policy-packs/builtin/branch-protection-runtime.js +53 -6
- package/dist/policy-packs/builtin/branch-protection-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection.js +21 -11
- package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
- package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +18 -0
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js +32 -0
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution.d.ts +11 -0
- package/dist/policy-packs/builtin/understanding-before-execution.js +15 -0
- package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.33.0] - 2026-06-09
|
|
11
|
+
|
|
12
|
+
**Headline: a security-driven release that closes two gate-bypasses.** A read-only-bash classifier hole let `command rm -rf /` and `env FOO=bar rm -rf /` run as "read-only" past the hard Understanding Gate, and the branch-protection override could be self-blessed by an agent-writable ledger ACK. Both are now closed: command runners recurse-classify their nested argv, and the branch-protection override is an operator-only canonical marker (new `harness approve branch-protection` verb), mirroring the understanding gate. Also bundled: a vitest CVE bump, clearer gate-block messages surfaced by dogfooding, and a `SOLUTION_VERDICT_ID` knob for solo sessions. **Operator action**: none required, back-compat. Re-run `npm i -g @lannguyensi/harness` to upgrade.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`solution-acceptance`: `SOLUTION_VERDICT_ID` env knob for solo / non-agent-tasks sessions** (task 01435583, PR #272): the completion-gate derived the verdict id solely from the agent-tasks `active-claim`, so a session that never calls `task_start` was permanently blocked with "no active-claim". It now falls back to a `SOLUTION_VERDICT_ID` env var when no claim is present. Resolution order is active-claim first, then `SOLUTION_VERDICT_ID`, then fail-closed, so a claimed session's id stays authoritative and cannot be redirected by the env (a sessionId fallback is still intentionally absent). The env value is validated as a safe single path segment; a malformed value fails closed. Set it to the same id you pass to `mcp__agent-grounding__solution_evaluate({ id })`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Gate-block messages are clearer about preflight ordering, mode-aware wording, and the approve escape hatch** (PR #274), surfaced by dogfooding the gates. `preflight-before-investigation` / `preflight-before-push` `ux.required` now note that `harness preflight` is itself gated by the Understanding Gate, so the "Run: harness preflight" remedy no longer dead-ends when the report is not yet approved. A new `understandingApprovalRequirement(mode)` helper derives the understanding `ux.required` phrase from the mode (only `strict` says "a human-approved Understanding Report"; `fast_confirm` / `grill_me` stay "an approved Understanding Report"); no output change for current `grill_me` profiles. `approveEscapeHint()` appends a targeted hint when a blocked Bash command starts with `harness approve` but trips the (unchanged) metachar guard, telling the agent to re-run it bare; the understanding `ux.run` line gains the same "(bare, no pipes, chaining, or redirection)" guidance.
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
|
|
24
|
+
- **Command runners `env` / `command` no longer bypass the hard gate** (HIGH audit finding, PR #271). The read-only-bash classifier listed `env` and `command` in `SIMPLE_READ_ONLY_BINS`, so the gate treated `command rm -rf /tmp/x` and `env FOO=bar rm -rf /` as read-only and allowed them without an approved Understanding Report, a hard-gate bypass across `hook-pre-tool-use`, `hook-codex-pre-tool-use`, and `hook-solution-acceptance-writeguard`. Both binaries are command runners (their argv is itself a nested command); they are removed from the simple set and given a find-style special case that strips the runner's own leading flags/assignments and recurse-classifies the residual underlying command. Bare and lookup-only forms (`env`, `env -u X`, `command -v node`) stay read-only; `env -S` / `--split-string` fails closed since it re-parses a string into a fresh argv that defeats whitespace tokenization.
|
|
25
|
+
|
|
26
|
+
- **branch-protection override is now an operator-only canonical marker** (MEDIUM audit finding #39, PR #275). The override was satisfied by any `branch-protection-ack` ledger entry, which an agent can self-write via `mcp__agent-grounding__ledger_add`, letting it bless its own protected-branch edit. The agent-writable ledger ACK is replaced with an operator-only canonical marker file under `harness.generated/.approvals/branch-protection-<sessionId>`, mirroring the understanding gate (`writeApprovalMarker` / `checkApprovalMarker`). A new operator verb `harness approve branch-protection` writes the marker and records the `branch-protection-ack` ledger row as a best-effort audit echo only. The blocker now reads the marker as the canonical override; the ledger tag alone no longer opens the gate.
|
|
27
|
+
|
|
28
|
+
- **vitest bumped to `^4.1.4`** (CVE-2026-47429 / GHSA-5xrq-8626-4rwp, PR #273). vitest < 4.1.0 lets the UI server read and execute arbitrary files. vitest is a devDependency; the lockfile is regenerated.
|
|
29
|
+
|
|
10
30
|
## [0.32.0] - 2026-05-30
|
|
11
31
|
|
|
12
32
|
**Headline: harness ships a new opt-in `solution-acceptance` policy pack that makes task completion EARNED from a real preflight run rather than self-attested.** The producer (`@lannguyensi/grounding-mcp` >= 0.3.2 `solution_evaluate`) records a HEAD-pinned verdict from a real `preflight run --json`; this pack gates the task-finishing tools (agent-tasks completion verbs + `git push` / `gh pr merge`) on a ready verdict at the current HEAD, and adds an anti-forgery write-guard so the agent cannot hand-write the verdict marker. **Operator action**: none required, back-compat. The pack is opt-in (`harness pack add solution-acceptance`, or flip the disabled exemplar in the full template); it needs `grounding-mcp` under `tools.mcp` and the `preflight` binary on PATH, and `harness validate` warns if you enable it without the producer. Re-run `npm i -g @lannguyensi/harness` to upgrade.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Manifest } from "../../schema/index.js";
|
|
2
|
+
import { type LoaderOptions } from "../loader.js";
|
|
3
|
+
export interface ApproveBranchProtectionOptions extends LoaderOptions {
|
|
4
|
+
/** Explicit session id (overrides $CLAUDE_CODE_SESSION_ID / $CLAUDE_SESSION_ID / $CODEX_SESSION_ID). */
|
|
5
|
+
session?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Free-form note recorded in the audit ledger tag
|
|
8
|
+
* (`branch-protection-ack:<reason>`) so a later `harness audit` can read
|
|
9
|
+
* WHY the override fired. Optional; defaults to a generic note.
|
|
10
|
+
*/
|
|
11
|
+
reason?: string;
|
|
12
|
+
/** Override the harness.generated/ directory (test injection). */
|
|
13
|
+
generatedDir?: string;
|
|
14
|
+
/** Override "now" for deterministic tests. */
|
|
15
|
+
now?: Date;
|
|
16
|
+
/** Override the actor recorded in the marker (default: harness-approve-cli). */
|
|
17
|
+
approvedBy?: string;
|
|
18
|
+
/** Inject a manifest (test); bypasses `loadManifest`. */
|
|
19
|
+
manifest?: Manifest;
|
|
20
|
+
/** Override the ledger writer (test). */
|
|
21
|
+
ledgerAdd?: (sessionId: string, content: string) => Promise<{
|
|
22
|
+
ok: true;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
reason: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
export interface ApproveBranchProtectionResult {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
/** Where `sessionId` came from — surfaced so the operator can sanity-check it. */
|
|
31
|
+
sessionSource: "flag" | "env-claude-code" | "env-claude" | "env-codex" | "pending-approval";
|
|
32
|
+
/**
|
|
33
|
+
* Canonical gate-satisfying signal. `ok: false` means the marker file
|
|
34
|
+
* could not be written (rare: fs permission, missing parent dir) and the
|
|
35
|
+
* gate will still block on the next tool call. The CLI surfaces this as a
|
|
36
|
+
* hard error so the operator does not think they approved when they didn't.
|
|
37
|
+
*/
|
|
38
|
+
marker: {
|
|
39
|
+
ok: true;
|
|
40
|
+
filePath: string;
|
|
41
|
+
approvedAt: string;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
reason: string;
|
|
45
|
+
};
|
|
46
|
+
/** Best-effort audit echo. Never affects the gate decision. */
|
|
47
|
+
ledger: {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
tag: string;
|
|
50
|
+
reason?: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** The audit ledger tag content this verb records (best-effort only). */
|
|
54
|
+
export declare function branchProtectionAckTag(reason: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the target session id, write its canonical override marker, and
|
|
57
|
+
* record a best-effort audit ledger tag. Session id precedence mirrors
|
|
58
|
+
* `harness approve risk` tiers 1-5: explicit `--session`, then
|
|
59
|
+
* `$CLAUDE_CODE_SESSION_ID`, then `$CLAUDE_SESSION_ID` (legacy), then
|
|
60
|
+
* `$CODEX_SESSION_ID`, then the `.pending-approval` file the gate hook
|
|
61
|
+
* staged on its last block. There is no persisted-report tier: the
|
|
62
|
+
* branch-protection gate produces no persisted reports.
|
|
63
|
+
*
|
|
64
|
+
* Throws `HarnessExitError(EX_FAIL)` when no session id can be resolved. A
|
|
65
|
+
* marker write failure is surfaced in the result (not thrown) so the
|
|
66
|
+
* operator still learns the resolved id; a degraded ledger is likewise
|
|
67
|
+
* surfaced, never thrown.
|
|
68
|
+
*/
|
|
69
|
+
export declare function approveBranchProtection(opts?: ApproveBranchProtectionOptions): Promise<ApproveBranchProtectionResult>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// `harness approve branch-protection [--session <id>]` CLI verb.
|
|
2
|
+
//
|
|
3
|
+
// The operator's deliberate blessing of a protected-branch edit (version
|
|
4
|
+
// bumps, CI workflow patches, hotfixes) for one session. Audit finding
|
|
5
|
+
// #39: the old override was a `branch-protection-ack:` ledger tag, but the
|
|
6
|
+
// agent has direct `mcp__agent-grounding__ledger_add` access and could
|
|
7
|
+
// self-write that tag to bless its own edit. This verb instead writes the
|
|
8
|
+
// canonical operator-only approval MARKER under
|
|
9
|
+
// `harness.generated/.approvals/branch-protection-<sessionId>` (the same
|
|
10
|
+
// trust boundary the understanding gate uses: Edit / Write / Bash are all
|
|
11
|
+
// gated from writing there, and no configured MCP server exposes a
|
|
12
|
+
// filesystem write). The branch-protection blocker consults that marker.
|
|
13
|
+
//
|
|
14
|
+
// The `branch-protection-ack:<reason>` ledger row is still written, as a
|
|
15
|
+
// best-effort AUDIT echo only, so `harness audit` / forensics keep a trail
|
|
16
|
+
// of WHY the override fired. A degraded / absent grounding-mcp surfaces as
|
|
17
|
+
// a warning, never a hard failure: the marker is what unblocks the gate.
|
|
18
|
+
import { ACK_TAG_PREFIX, writeBranchProtectionMarker, } from "../../policy-packs/builtin/branch-protection-runtime.js";
|
|
19
|
+
import { addLedgerFact } from "../../runtime/ledger-add.js";
|
|
20
|
+
import { readPendingApproval, resolveGeneratedDir, } from "../../runtime/pending-approval.js";
|
|
21
|
+
import { EX_FAIL, HarnessExitError } from "../exit-codes.js";
|
|
22
|
+
import { loadManifest, resolvePaths } from "../loader.js";
|
|
23
|
+
const DEFAULT_APPROVED_BY = "harness-approve-cli";
|
|
24
|
+
const DEFAULT_REASON = "operator branch-protection override";
|
|
25
|
+
function findGroundingMcp(manifest) {
|
|
26
|
+
return manifest.tools.mcp.find((m) => m.name === "grounding-mcp") ?? null;
|
|
27
|
+
}
|
|
28
|
+
/** The audit ledger tag content this verb records (best-effort only). */
|
|
29
|
+
export function branchProtectionAckTag(reason) {
|
|
30
|
+
return `${ACK_TAG_PREFIX}:${reason}`;
|
|
31
|
+
}
|
|
32
|
+
async function writeLedgerTag(manifest, sessionId, content, opts) {
|
|
33
|
+
if (opts.ledgerAdd)
|
|
34
|
+
return opts.ledgerAdd(sessionId, content);
|
|
35
|
+
const server = findGroundingMcp(manifest);
|
|
36
|
+
if (!server) {
|
|
37
|
+
return { ok: false, reason: "grounding-mcp not declared in manifest" };
|
|
38
|
+
}
|
|
39
|
+
// No `~` expansion: `addLedgerFact` expands leading `~/` in every command
|
|
40
|
+
// token itself, so a second pass would be dead work.
|
|
41
|
+
const command = Array.isArray(server.command)
|
|
42
|
+
? server.command
|
|
43
|
+
: server.command.trim().split(/\s+/);
|
|
44
|
+
return addLedgerFact({
|
|
45
|
+
mcpCommand: command,
|
|
46
|
+
...(server.env && { mcpEnv: server.env }),
|
|
47
|
+
timeoutMs: server.health?.timeout_ms ?? 5_000,
|
|
48
|
+
sessionId,
|
|
49
|
+
content,
|
|
50
|
+
source: "harness-approve-branch-protection",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the target session id, write its canonical override marker, and
|
|
55
|
+
* record a best-effort audit ledger tag. Session id precedence mirrors
|
|
56
|
+
* `harness approve risk` tiers 1-5: explicit `--session`, then
|
|
57
|
+
* `$CLAUDE_CODE_SESSION_ID`, then `$CLAUDE_SESSION_ID` (legacy), then
|
|
58
|
+
* `$CODEX_SESSION_ID`, then the `.pending-approval` file the gate hook
|
|
59
|
+
* staged on its last block. There is no persisted-report tier: the
|
|
60
|
+
* branch-protection gate produces no persisted reports.
|
|
61
|
+
*
|
|
62
|
+
* Throws `HarnessExitError(EX_FAIL)` when no session id can be resolved. A
|
|
63
|
+
* marker write failure is surfaced in the result (not thrown) so the
|
|
64
|
+
* operator still learns the resolved id; a degraded ledger is likewise
|
|
65
|
+
* surfaced, never thrown.
|
|
66
|
+
*/
|
|
67
|
+
export async function approveBranchProtection(opts = {}) {
|
|
68
|
+
const generatedDir = opts.generatedDir ??
|
|
69
|
+
resolveGeneratedDir({
|
|
70
|
+
...(opts.homeDir !== undefined ? { homeDir: opts.homeDir } : {}),
|
|
71
|
+
manifestPath: resolvePaths(opts).base,
|
|
72
|
+
});
|
|
73
|
+
let sessionId = "";
|
|
74
|
+
let sessionSource = "flag";
|
|
75
|
+
if (typeof opts.session === "string" && opts.session.length > 0) {
|
|
76
|
+
sessionId = opts.session;
|
|
77
|
+
sessionSource = "flag";
|
|
78
|
+
}
|
|
79
|
+
else if (typeof process.env.CLAUDE_CODE_SESSION_ID === "string" &&
|
|
80
|
+
process.env.CLAUDE_CODE_SESSION_ID.length > 0) {
|
|
81
|
+
sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
82
|
+
sessionSource = "env-claude-code";
|
|
83
|
+
}
|
|
84
|
+
else if (typeof process.env.CLAUDE_SESSION_ID === "string" &&
|
|
85
|
+
process.env.CLAUDE_SESSION_ID.length > 0) {
|
|
86
|
+
sessionId = process.env.CLAUDE_SESSION_ID;
|
|
87
|
+
sessionSource = "env-claude";
|
|
88
|
+
}
|
|
89
|
+
else if (typeof process.env.CODEX_SESSION_ID === "string" &&
|
|
90
|
+
process.env.CODEX_SESSION_ID.length > 0) {
|
|
91
|
+
sessionId = process.env.CODEX_SESSION_ID;
|
|
92
|
+
sessionSource = "env-codex";
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const staged = readPendingApproval(generatedDir);
|
|
96
|
+
if (staged !== null) {
|
|
97
|
+
sessionId = staged;
|
|
98
|
+
sessionSource = "pending-approval";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (sessionId === "") {
|
|
102
|
+
throw new HarnessExitError([
|
|
103
|
+
"no session id available. Pass --session <id>, or set one of",
|
|
104
|
+
"$CLAUDE_CODE_SESSION_ID (Claude Code) / $CLAUDE_SESSION_ID (legacy) /",
|
|
105
|
+
"$CODEX_SESSION_ID (Codex).",
|
|
106
|
+
"",
|
|
107
|
+
"The branch-protection PreToolUse hook and `harness preflight` both stage the",
|
|
108
|
+
`session id in ${generatedDir}/.pending-approval, so an arg-less`,
|
|
109
|
+
"`harness approve branch-protection` works after either has fired. An empty result",
|
|
110
|
+
"means neither has run for the current session yet.",
|
|
111
|
+
"",
|
|
112
|
+
"From inside the running agent you can also read the id directly:",
|
|
113
|
+
"Claude Code exposes $CLAUDE_CODE_SESSION_ID; Codex exposes $CODEX_SESSION_ID.",
|
|
114
|
+
].join("\n"), EX_FAIL);
|
|
115
|
+
}
|
|
116
|
+
// Write the canonical marker first — it is what unblocks the gate.
|
|
117
|
+
const approvedAt = (opts.now ?? new Date()).toISOString();
|
|
118
|
+
const approvedBy = opts.approvedBy ?? DEFAULT_APPROVED_BY;
|
|
119
|
+
let markerResult;
|
|
120
|
+
try {
|
|
121
|
+
const filePath = writeBranchProtectionMarker(generatedDir, sessionId, {
|
|
122
|
+
approvedAt,
|
|
123
|
+
approvedBy,
|
|
124
|
+
});
|
|
125
|
+
markerResult = { ok: true, filePath, approvedAt };
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
markerResult = {
|
|
129
|
+
ok: false,
|
|
130
|
+
reason: `failed to write approval marker: ${err.message}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Best-effort audit ledger echo. Loaded lazily so a missing / unparseable
|
|
134
|
+
// manifest degrades the audit row to a warning rather than aborting the
|
|
135
|
+
// marker-based approval.
|
|
136
|
+
let manifest = null;
|
|
137
|
+
try {
|
|
138
|
+
manifest = opts.manifest ?? loadManifest(opts).manifest;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* swallow; ledger write becomes a degraded-ok */
|
|
142
|
+
}
|
|
143
|
+
const reason = opts.reason && opts.reason.trim().length > 0 ? opts.reason.trim() : DEFAULT_REASON;
|
|
144
|
+
const tag = branchProtectionAckTag(reason);
|
|
145
|
+
const ledgerResult = manifest
|
|
146
|
+
? await writeLedgerTag(manifest, sessionId, tag, opts)
|
|
147
|
+
: { ok: false, reason: "manifest unreadable; skipped ledger write" };
|
|
148
|
+
return {
|
|
149
|
+
sessionId,
|
|
150
|
+
sessionSource,
|
|
151
|
+
marker: markerResult,
|
|
152
|
+
ledger: ledgerResult.ok
|
|
153
|
+
? { ok: true, tag }
|
|
154
|
+
: { ok: false, tag, reason: ledgerResult.reason },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=branch-protection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branch-protection.js","sourceRoot":"","sources":["../../../src/cli/approve/branch-protection.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import { runPackHookCodexPreToolUseCli } from "./pack/hook-codex-pre-tool-use.js
|
|
|
33
33
|
import { runPackHookCodexStopCli } from "./pack/hook-codex-stop.js";
|
|
34
34
|
import { runPackHookCodexUserPromptSubmitCli } from "./pack/hook-codex-user-prompt-submit.js";
|
|
35
35
|
import { isRuntime, KNOWN_RUNTIMES } from "../policy-packs/index.js";
|
|
36
|
+
import { approveBranchProtection } from "./approve/branch-protection.js";
|
|
36
37
|
import { approveRisk } from "./approve/risk.js";
|
|
37
38
|
import { approveUnderstanding } from "./approve/understanding.js";
|
|
38
39
|
import { describe, isPillar } from "./describe.js";
|
|
@@ -835,7 +836,8 @@ export function buildProgram(opts = {}) {
|
|
|
835
836
|
.command("branch-protection")
|
|
836
837
|
.description("PreToolUse blocker for the branch-protection pack: read tool-event JSON from stdin, consult the " +
|
|
837
838
|
"evidence ledger, emit a deny envelope on protected branches unless either a fresh " +
|
|
838
|
-
"`branch:non-protected` tag (within 5m) or
|
|
839
|
+
"`branch:non-protected` tag (within 5m) or the operator-only override marker " +
|
|
840
|
+
"(written by `harness approve branch-protection`) is present.")
|
|
839
841
|
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
840
842
|
.option("--project <name>", "apply per-project overrides")
|
|
841
843
|
.option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
|
|
@@ -1170,6 +1172,58 @@ export function buildProgram(opts = {}) {
|
|
|
1170
1172
|
}
|
|
1171
1173
|
stdout(`${lines.join("\n")}\n`);
|
|
1172
1174
|
});
|
|
1175
|
+
approveCmd
|
|
1176
|
+
.command("branch-protection")
|
|
1177
|
+
.description("Bless a deliberate protected-branch edit for one session. Writes the canonical " +
|
|
1178
|
+
"operator-only approval marker under harness.generated/.approvals/ that the " +
|
|
1179
|
+
"branch-protection blocker consults, plus a best-effort branch-protection-ack " +
|
|
1180
|
+
"ledger row for audit. Operator action: the marker (not the ledger tag) is the " +
|
|
1181
|
+
"trusted override, because the ledger is agent-writable.")
|
|
1182
|
+
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
1183
|
+
.option("--project <name>", "apply per-project overrides")
|
|
1184
|
+
.option("--session <id>", "explicit session id (default: $CLAUDE_CODE_SESSION_ID, then $CLAUDE_SESSION_ID, then $CODEX_SESSION_ID, then staged .pending-approval)")
|
|
1185
|
+
.option("--reason <text>", "free-form note recorded in the audit ledger tag (why the override fired)")
|
|
1186
|
+
.option("--approved-by <actor>", "actor to record on the marker (default: harness-approve-cli)")
|
|
1187
|
+
.action(async (options) => {
|
|
1188
|
+
const cliOpts = {};
|
|
1189
|
+
if (options.config)
|
|
1190
|
+
cliOpts.configPath = options.config;
|
|
1191
|
+
if (options.project)
|
|
1192
|
+
cliOpts.project = options.project;
|
|
1193
|
+
if (options.session)
|
|
1194
|
+
cliOpts.session = options.session;
|
|
1195
|
+
if (options.reason)
|
|
1196
|
+
cliOpts.reason = options.reason;
|
|
1197
|
+
if (options.approvedBy)
|
|
1198
|
+
cliOpts.approvedBy = options.approvedBy;
|
|
1199
|
+
const result = await approveBranchProtection(cliOpts);
|
|
1200
|
+
const lines = [];
|
|
1201
|
+
const sourceNote = result.sessionSource === "pending-approval"
|
|
1202
|
+
? " (resolved from .pending-approval staged by the gate hook)"
|
|
1203
|
+
: result.sessionSource === "env-claude-code"
|
|
1204
|
+
? " (from $CLAUDE_CODE_SESSION_ID)"
|
|
1205
|
+
: result.sessionSource === "env-claude"
|
|
1206
|
+
? " (from $CLAUDE_SESSION_ID)"
|
|
1207
|
+
: result.sessionSource === "env-codex"
|
|
1208
|
+
? " (from $CODEX_SESSION_ID)"
|
|
1209
|
+
: "";
|
|
1210
|
+
lines.push(`session: ${result.sessionId}${sourceNote}`);
|
|
1211
|
+
if (result.marker.ok) {
|
|
1212
|
+
lines.push(`marker: ✓ ${result.marker.filePath} (canonical gate signal)`);
|
|
1213
|
+
lines.push(" the branch-protection gate now allows protected-branch edits for this session.");
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
lines.push(`marker: ✗ FAILED (${result.marker.reason})`);
|
|
1217
|
+
lines.push(" the gate WILL keep blocking the next tool call until the marker exists.");
|
|
1218
|
+
}
|
|
1219
|
+
if (result.ledger.ok) {
|
|
1220
|
+
lines.push(`ledger: ✓ wrote ${result.ledger.tag} (audit only)`);
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
lines.push(`ledger: ⚠ skipped (${result.ledger.reason ?? "unknown"}) (audit only)`);
|
|
1224
|
+
}
|
|
1225
|
+
stdout(`${lines.join("\n")}\n`);
|
|
1226
|
+
});
|
|
1173
1227
|
const VALID_DECISION_FILTERS = [
|
|
1174
1228
|
"allow",
|
|
1175
1229
|
"warn",
|