@lannguyensi/harness 0.28.1 → 0.30.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 +28 -0
- package/dist/cli/apply/generate-codex-config.js +24 -4
- package/dist/cli/apply/generate-codex-config.js.map +1 -1
- package/dist/cli/approve/risk.d.ts +49 -6
- package/dist/cli/approve/risk.js +87 -8
- package/dist/cli/approve/risk.js.map +1 -1
- package/dist/cli/approve/understanding.d.ts +38 -2
- package/dist/cli/approve/understanding.js +145 -28
- package/dist/cli/approve/understanding.js.map +1 -1
- package/dist/cli/index.js +94 -21
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/templates.d.ts +1 -1
- package/dist/cli/init/templates.js +2 -1
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/pack/hook-codex-pre-tool-use.js +38 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
- package/dist/cli/policy/intercept.d.ts +18 -0
- package/dist/cli/policy/intercept.js +66 -10
- package/dist/cli/policy/intercept.js.map +1 -1
- package/dist/runtime/git-context.js +7 -0
- package/dist/runtime/git-context.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.30.0] - 2026-05-25
|
|
11
|
+
|
|
12
|
+
**Headline: `harness approve risk` grows a `--force <reason>` flag so the operator can clear a deny-tier Risk Gate block without ever touching the ledger directly.** Closes the operator-UX leak surfaced during the v0.29.0 release-cut: the built-in `gate-prod-destructive` policy is `deny`-tier and requires the `risk-override:${SESSION_ID}` ledger tag, but no CLI verb wrote that tag. Its `ux.run` instruction told operators to "record a risk-override entry in the evidence ledger", leaking the ledger as an implementation detail. The new flag mirrors the `approve understanding --force` pattern PR #253 shipped: a one-line operator command with an audit-trail suffix. The built-in policy template's `ux.run` now names the verb, not the tag. **Operator action**: none required; back-compat. The default `harness approve risk` (no flag) is byte-for-byte unchanged. Re-run `npm i -g @lannguyensi/harness` to upgrade.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`harness approve risk --force <reason>`** (#258, task a48ccd55). Writes `risk-override:${SESSION_ID}:forced:<sanitised-reason>` to the evidence ledger so the deny-tier `gate-prod-destructive` policy's requires clears. The `:forced:<reason>` suffix is additive metadata so `harness audit` can grep `:forced:` to surface every operator-deliberate override. The TTY guard mirrors `harness pause`: non-TTY stdin refuses with `EX_USAGE` unless `--i-am-the-operator` is passed. Agent-shell (`$CLAUDE_SESSION_ID` set) is intentionally NOT refused; the verb is designed to be runnable from `!`-prefixed Claude Code shells and reads `$CLAUDE_CODE_SESSION_ID` as a session-id resolution tier. Reason sanitisation: lowercase, keeps `[a-z0-9._-]`, collapses other runs to `-`, trims edges, caps at 64 chars; an empty-after-sanitisation result falls back to `operator-override` so the tag is never malformed. The built-in `gate-prod-destructive.ux.run` instruction (and the worked example in `docs/examples/full-manifest.yaml` plus its byte-equivalent golden) is rewritten: no more "record a risk-override entry in the evidence ledger", it now names `harness approve risk --force <reason>` for the per-block override and `harness pause --for <duration>` for the session-wide kill switch. `harness approve risk` without `--force` is byte-for-byte unchanged. Covered by 10 new tests in `tests/cli/approve-risk.test.ts`.
|
|
17
|
+
|
|
18
|
+
## [0.29.0] - 2026-05-25
|
|
19
|
+
|
|
20
|
+
**Headline: Codex hook diagnostics get sharp, `harness approve understanding` actually enforces the Prior Art rule, and read-only Codex shell commands stop tripping the Understanding Gate.** Three feats + three fixes tighten the operator surface around the Codex adapter and the approve verb. The Codex `harness policy intercept` projection now floors at a 2s timeout (PR #255), self-identifies via `--hook <name>` in the projected command + `[hook=<name>]`-prefixed stderr (PR #256), and resolves the per-call workdir for git-context builtins (PR #254). The Understanding Gate now lets read-only Codex shell commands through (PR #252) and `harness approve understanding` validates the persisted report before writing the marker, refusing reports with missing / empty / literal-`- None` `Prior Art` sections (PR #253). The `harness approve` env-var resolution chain is also fixed to read `$CLAUDE_CODE_SESSION_ID` (PR #251). **Operator action**: none required; all changes are back-compat. After upgrade, regenerate Codex config via `harness apply --runtime codex` to pick up the new `--hook` flags and 2s timeout floor in the projected `~/.codex/config.toml`. Re-run `npm i -g @lannguyensi/harness` to upgrade.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`harness policy intercept` self-identifies in Codex via `--hook <name>`** (#256, task 16683705). The Codex generator (`generate-codex-config.ts`) now appends ` --hook <name>` to every projected `harness policy intercept` command so each spawned process is identifiable from `ps`, audit logs, and any error string that echoes the command line. The intercept entrypoint (`policy/intercept.ts`) accepts the new flag via a commander option and prefixes every stderr emission with `[hook=<name>]`: no-match hint, verbose decision diagnostic, malformed-event JSON error, manifest-load failure, audit-write failure. Unsafe hook names (whitespace, shell metachars) silently skip the flag (`SAFE_HOOK_NAME_RE` is `[A-Za-z0-9._:-]+`, which excludes every POSIX shell active token) and fall back to the un-tagged emission. The Claude Code projection (`generate-settings.ts`) is deliberately NOT changed: that generator dedupes by `(command, timeout)` within each matcher group, so per-hook flag injection would diverge the dedupe key and N-multiply ledger queries plus audit writes per tool event. Codex has no such dedupe so the change is safe there. Back-compat: invocations without `--hook` (operator probes, smoke scripts, pre-0.29.0 installed configs) keep the un-tagged stderr format. Covered by 3 new tests in `tests/cli/apply/generate-codex-config.test.ts` (positive injection, non-policy bypass, unsafe-name skip) and 6 new tests in `tests/runtime/intercept-cli.test.ts` (tag in no-match / verbose / malformed-JSON / manifest-load / audit-write paths, plus back-compat untagged path).
|
|
25
|
+
|
|
26
|
+
- **`harness approve understanding` validates the persisted Understanding Report before writing the marker** (#253, task 2947c2a9). A `grill_me` report with missing, empty, or literal-`- None` `priorArt` is now refused before the canonical `.approvals/<sessionId>` marker is written, the ledger entry is appended, or the report is flipped from `pending` to `approved`. `fast_confirm` reports skip the check (the relaxed schema variant intentionally drops `priorArt` from `required`); legacy reports without a `mode` field pass through unchanged so the v0.4.0 schema bump doesn't retroactively invalidate historical reports. A new `--force` flag bypasses the check for emergency unblock; the ledger tag is stamped `understanding-approved:<session>:forced:<field>` so audit can distinguish forced approvals from clean ones, and the CLI prints a `validation:` line on stdout so a forced approval is visible at the call site. Closes a gap found via dogfood on 2026-05-24: the prompt declared Section 10 (Prior Art) required since `@lannguyensi/understanding-gate@0.4.0` BREAKING, but no operator-side path enforced it, so an agent could skip the section and still get the gate open. Covered by 11 new tests in `tests/cli/approve-understanding.test.ts`.
|
|
27
|
+
|
|
28
|
+
- **Read-only Codex shell commands skip the Understanding Gate pre-tool-use blocker** (#252, task c0e67c14). The Codex variant of the `understanding-before-execution` pack pre-tool-use hook (`src/cli/pack/hook-codex-pre-tool-use.ts`) now reuses the existing read-only Bash classifier (introduced for the Claude side in PR #242) to allow read-only shell commands (`git status`, `git log`, `git diff`, `ls`, `pwd`, `cat`, `grep`, `find`, etc.) through without an approved report. Mutating commands, shell-chained commands, and `apply_patch` still hard-block. Codex shell extraction reads `raw_input.command`, `raw_input.cmd`, and raw string payloads, failing closed on conflicting aliases. Closes the Codex-side gap left by PR #242, which only treated the Claude pre-tool-use blocker. Covered by `tests/cli/pack-hook-codex-pre-tool-use.test.ts` and `tests/cli/pack-read-only-bash.test.ts`.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **Codex `harness policy intercept` hooks float at a 2s timeout floor** (#255, task 25dec529). The Codex generator (`generate-codex-config.ts`) now projects every `harness policy intercept` hook with `Math.max(2, ceil(budget_ms / 1000))` instead of `Math.max(1, ceil(...))`, so the Full-template policy-intercept hooks with `budget_ms: 1000` (`require-preflight-evidence`, `require-preflight-push-evidence`) get `timeout = 2` instead of `timeout = 1` in the emitted `~/.codex/config.toml`. The 1s floor was too tight under Codex's cold-start path (manifest load, git-context resolution, ledger query, risk/env evaluation, process startup), so the gates surfaced spurious `PreToolUse hook (failed) error: hook timed out after 1s` errors on routine Bash. Non-policy hooks with `budget_ms: 1000` still floor at `timeout = 1` (the bump is scoped via exact-match `h.command.trim() === "harness policy intercept"`). Each generated `[[hooks.*]]` block now also carries a `# harness hook: <name> (budget_ms=N)` comment so an operator opening `~/.codex/config.toml` can identify the source hook. Operators must re-run `harness apply --runtime codex --install` to pick up the floor for an existing install. Covered by regressions in `tests/cli/apply/generate-codex-config.test.ts`.
|
|
33
|
+
|
|
34
|
+
- **Codex `harness policy intercept` resolves the per-call shell workdir for git-context builtins** (#254). When a Codex `exec_command` / `shell` event arrives without a top-level `event.cwd` (Codex's default for non-Bash-aliased shell tools), `harness policy intercept` now resolves the policy cwd from `event.raw_input.workdir`, then `event.tool_input.workdir`, then `event.input.workdir`, then the Codex sandbox `--command-cwd` from `/proc/1/cmdline`, before falling back to `process.cwd()`. This gives the `REPO` / `BRANCH` builtins a meaningful work tree to derive from, so per-repo and per-branch ledger tags (`preflight:${REPO}`, `preflight:${BRANCH}`) actually namespace under Codex rather than collapsing to the harness process's cwd. The git-context resolver also now rejects directory-form `.git` entries without a readable `HEAD`, so an empty parent `.git/` directory no longer becomes a fake repo with a blank `BRANCH`. Covered by new Codex workdir-extraction tests in `tests/runtime/intercept-cli.test.ts` and a directory-`.git`-fallback test in `tests/runtime/git-context.test.ts`.
|
|
35
|
+
|
|
36
|
+
- **`harness approve` reads `$CLAUDE_CODE_SESSION_ID` before legacy env vars** (#251, task 058b31a3). The env-var fallback chain on both `harness approve risk` and `harness approve understanding` previously only checked `$CLAUDE_SESSION_ID` and `$CODEX_SESSION_ID`. Claude Code exports its session id as `$CLAUDE_CODE_SESSION_ID` (not `$CLAUDE_SESSION_ID`), so an arg-less `harness approve` invoked from inside a Claude Code session never resolved via the env tier: it only worked via `--session` or the hook-staged `.pending-approval` marker. The resolver now reads `$CLAUDE_CODE_SESSION_ID` first (canonical, runtime-exported), then `$CLAUDE_SESSION_ID` (legacy / docs-name peer, back-compat), then `$CODEX_SESSION_ID`, before falling through to `.pending-approval` and (for understanding) the newest pending Understanding Report. The no-session-id error message and the `--session` option help text are rewritten to name the full chain. A new `sessionSource: "env-claude-code"` variant lets the CLI annotate `(from $CLAUDE_CODE_SESSION_ID)` in stdout so a wrong env pick is visible before it lands. Tests in `tests/cli/approve-risk.test.ts` and `tests/cli/approve-understanding.test.ts` cover each env-var path independently and assert documented precedence; hermetic env hygiene additions clear all three vars in beforeEach so an external shell's exports cannot leak into a test run.
|
|
37
|
+
|
|
10
38
|
## [0.28.1] - 2026-05-24
|
|
11
39
|
|
|
12
40
|
**Hotfix: arg-less `harness approve risk` now actually works after a Risk Gate block, and `harness doctor` warns on the misconfiguration that caused the original lockout.** Surfaced during the 0.28.0 release-cut session: a Risk Gate block fired against a routine read-only Bash probe (`--version`), and recovery via `harness approve risk` failed arg-less because the session id was never staged where the operator could read it. Two complementary fixes ship together.
|
|
@@ -111,19 +111,39 @@ function eventKey(event) {
|
|
|
111
111
|
return event;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
function codexTimeoutSeconds(
|
|
115
|
-
|
|
114
|
+
function codexTimeoutSeconds(h) {
|
|
115
|
+
const minimumSeconds = h.command.trim() === "harness policy intercept" ? 2 : 1;
|
|
116
|
+
return Math.max(minimumSeconds, Math.ceil(h.budget_ms / 1000));
|
|
117
|
+
}
|
|
118
|
+
// Hook names safe to splice into the emitted command literal as
|
|
119
|
+
// `--hook <name>`. Restricted to a quote/space/metachar-free charset so
|
|
120
|
+
// the projection never produces a shell-broken command, even though the
|
|
121
|
+
// underlying `Hook.name` schema (z.string().min(1)) accepts anything.
|
|
122
|
+
// All policy-pack hook names ship within this charset; an exotic name
|
|
123
|
+
// silently skips the flag and falls back to the un-tagged emission, with
|
|
124
|
+
// the preceding `# harness hook: <name>` TOML comment still naming it.
|
|
125
|
+
const SAFE_HOOK_NAME_RE = /^[A-Za-z0-9._:-]+$/;
|
|
126
|
+
function commandWithHookTag(h) {
|
|
127
|
+
if (h.command.trim() !== "harness policy intercept")
|
|
128
|
+
return h.command;
|
|
129
|
+
if (!SAFE_HOOK_NAME_RE.test(h.name))
|
|
130
|
+
return h.command;
|
|
131
|
+
return `${h.command} --hook ${h.name}`;
|
|
116
132
|
}
|
|
117
133
|
function emitCommandHook(h) {
|
|
118
134
|
const fields = [
|
|
119
135
|
`type = "command"`,
|
|
120
|
-
`command = ${tomlString(h
|
|
121
|
-
`timeout = ${codexTimeoutSeconds(h
|
|
136
|
+
`command = ${tomlString(commandWithHookTag(h))}`,
|
|
137
|
+
`timeout = ${codexTimeoutSeconds(h)}`,
|
|
122
138
|
];
|
|
123
139
|
return `{ ${fields.join(", ")} }`;
|
|
124
140
|
}
|
|
141
|
+
function tomlCommentText(s) {
|
|
142
|
+
return s.replace(CONTROL_CHAR_RE, " ").replace(/\s+/g, " ").trim();
|
|
143
|
+
}
|
|
125
144
|
function emitHook(h) {
|
|
126
145
|
const lines = [];
|
|
146
|
+
lines.push(`# harness hook: ${tomlCommentText(h.name)} (budget_ms=${h.budget_ms})`);
|
|
127
147
|
lines.push(`[[hooks.${eventKey(h.event)}]]`);
|
|
128
148
|
if (h.match !== undefined) {
|
|
129
149
|
lines.push(`matcher = ${tomlString(expandCodexHookMatchPattern(h.match))}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-codex-config.js","sourceRoot":"","sources":["../../../src/cli/apply/generate-codex-config.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"generate-codex-config.js","sourceRoot":"","sources":["../../../src/cli/apply/generate-codex-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import type { Manifest } from "../../schema/index.js";
|
|
2
2
|
import { type LoaderOptions } from "../loader.js";
|
|
3
3
|
export interface ApproveRiskOptions extends LoaderOptions {
|
|
4
|
-
/** Explicit session id (overrides $CLAUDE_SESSION_ID / $CODEX_SESSION_ID). */
|
|
4
|
+
/** Explicit session id (overrides $CLAUDE_CODE_SESSION_ID / $CLAUDE_SESSION_ID / $CODEX_SESSION_ID). */
|
|
5
5
|
session?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Operator-deliberate override of a Risk Gate `deny` decision. Writes
|
|
8
|
+
* `risk-override:${SESSION_ID}:forced:<reason-slug>` instead of the
|
|
9
|
+
* default `risk-approved:${SESSION_ID}` tag, so the built-in
|
|
10
|
+
* `gate-prod-destructive` policy (which gates on `risk-override:`,
|
|
11
|
+
* see `src/cli/init/templates.ts`) clears. `deny` is by design not
|
|
12
|
+
* approvable, so this path is hard-gated behind operator-only checks:
|
|
13
|
+
* a TTY stdin OR explicit `iAmTheOperator` acknowledgement is
|
|
14
|
+
* required, mirroring `harness pause`. The reason becomes part of the
|
|
15
|
+
* audit trail and is sanitised to a tag-safe slug.
|
|
16
|
+
*/
|
|
17
|
+
force?: {
|
|
18
|
+
reason: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Acknowledge a non-TTY / scripted invocation of `--force`. Without
|
|
22
|
+
* this flag a non-TTY `--force` refuses with a usage error.
|
|
23
|
+
*/
|
|
24
|
+
iAmTheOperator?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Override `process.stdin.isTTY` for tests; mirrors the pause/resume
|
|
27
|
+
* seam so the non-TTY refusal can be exercised hermetically.
|
|
28
|
+
*/
|
|
29
|
+
stdinIsTTY?: boolean;
|
|
6
30
|
/** Override the harness.generated/ directory (test injection). */
|
|
7
31
|
generatedDir?: string;
|
|
8
32
|
/** Inject a manifest (test); bypasses `loadManifest`. */
|
|
@@ -18,7 +42,14 @@ export interface ApproveRiskOptions extends LoaderOptions {
|
|
|
18
42
|
export interface ApproveRiskResult {
|
|
19
43
|
sessionId: string;
|
|
20
44
|
/** Where `sessionId` came from — surfaced so the operator can sanity-check it. */
|
|
21
|
-
sessionSource: "flag" | "env-claude" | "env-codex" | "pending-approval";
|
|
45
|
+
sessionSource: "flag" | "env-claude-code" | "env-claude" | "env-codex" | "pending-approval";
|
|
46
|
+
/**
|
|
47
|
+
* True when the verb wrote a `risk-override:` tag via `--force`
|
|
48
|
+
* instead of the default `risk-approved:`. The CLI surfaces this so
|
|
49
|
+
* the operator can see at a glance that they exercised the deny-tier
|
|
50
|
+
* override, not a clean require_approval approval.
|
|
51
|
+
*/
|
|
52
|
+
forced: boolean;
|
|
22
53
|
ledger: {
|
|
23
54
|
ok: boolean;
|
|
24
55
|
tag: string;
|
|
@@ -27,13 +58,25 @@ export interface ApproveRiskResult {
|
|
|
27
58
|
}
|
|
28
59
|
/** The evidence-ledger tag a Risk Gate `require_approval` policy consults. */
|
|
29
60
|
export declare function riskApprovedTagFor(sessionId: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* The evidence-ledger tag a Risk Gate `deny`-tier policy with
|
|
63
|
+
* `requires.ledger_tag: risk-override:${SESSION_ID}` consults. The
|
|
64
|
+
* `:forced:<reason>` suffix is additive metadata: the built-in
|
|
65
|
+
* policy's matcher pins the `risk-override:<session>` substring, so the
|
|
66
|
+
* suffix never affects whether the gate clears. It exists for the
|
|
67
|
+
* audit trail (`harness audit` can grep `:forced:` to surface every
|
|
68
|
+
* operator-deliberate override).
|
|
69
|
+
*/
|
|
70
|
+
export declare function riskOverrideTagFor(sessionId: string, reason: string): string;
|
|
30
71
|
/**
|
|
31
72
|
* Resolve the target session id and write its `risk-approved:` ledger
|
|
32
73
|
* tag. Session id precedence mirrors `harness approve understanding`
|
|
33
|
-
* tiers 1-4: explicit `--session`, then `$
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* the
|
|
74
|
+
* tiers 1-4: explicit `--session`, then `$CLAUDE_CODE_SESSION_ID`
|
|
75
|
+
* (the var Claude Code itself sets), then `$CLAUDE_SESSION_ID` (legacy
|
|
76
|
+
* / docs name), then `$CODEX_SESSION_ID`, then the `.pending-approval`
|
|
77
|
+
* file the gate hook staged on its last block. There is no
|
|
78
|
+
* persisted-report tier-5 guess: the Risk Gate produces no persisted
|
|
79
|
+
* reports.
|
|
37
80
|
*
|
|
38
81
|
* Throws `HarnessExitError(EX_FAIL)` when no session id can be resolved.
|
|
39
82
|
* A degraded ledger (grounding-mcp absent / unreachable) is surfaced in
|
package/dist/cli/approve/risk.js
CHANGED
|
@@ -15,13 +15,64 @@
|
|
|
15
15
|
// needs no change: it already stores arbitrary tags through `ledger_add`.
|
|
16
16
|
import { addLedgerFact } from "../../runtime/ledger-add.js";
|
|
17
17
|
import { readPendingApproval, resolveGeneratedDir, } from "../../runtime/pending-approval.js";
|
|
18
|
-
import { EX_FAIL, HarnessExitError } from "../exit-codes.js";
|
|
18
|
+
import { EX_FAIL, EX_USAGE, HarnessExitError } from "../exit-codes.js";
|
|
19
19
|
import { loadManifest, resolvePaths } from "../loader.js";
|
|
20
20
|
const RISK_APPROVED_PREFIX = "risk-approved";
|
|
21
|
+
const RISK_OVERRIDE_PREFIX = "risk-override";
|
|
21
22
|
/** The evidence-ledger tag a Risk Gate `require_approval` policy consults. */
|
|
22
23
|
export function riskApprovedTagFor(sessionId) {
|
|
23
24
|
return `${RISK_APPROVED_PREFIX}:${sessionId}`;
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The evidence-ledger tag a Risk Gate `deny`-tier policy with
|
|
28
|
+
* `requires.ledger_tag: risk-override:${SESSION_ID}` consults. The
|
|
29
|
+
* `:forced:<reason>` suffix is additive metadata: the built-in
|
|
30
|
+
* policy's matcher pins the `risk-override:<session>` substring, so the
|
|
31
|
+
* suffix never affects whether the gate clears. It exists for the
|
|
32
|
+
* audit trail (`harness audit` can grep `:forced:` to surface every
|
|
33
|
+
* operator-deliberate override).
|
|
34
|
+
*/
|
|
35
|
+
export function riskOverrideTagFor(sessionId, reason) {
|
|
36
|
+
return `${RISK_OVERRIDE_PREFIX}:${sessionId}:forced:${sanitiseReasonSlug(reason)}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Reduce an operator-supplied free-form reason to a tag-safe slug. Keeps
|
|
40
|
+
* `[A-Za-z0-9._-]`, collapses any other run to a single `-`, trims
|
|
41
|
+
* leading / trailing `-`, lowercases, caps at 64 chars. An empty result
|
|
42
|
+
* (e.g. the operator passed only punctuation) is rejected upstream.
|
|
43
|
+
*/
|
|
44
|
+
function sanitiseReasonSlug(reason) {
|
|
45
|
+
const slug = reason
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
48
|
+
.replace(/^-+|-+$/g, "")
|
|
49
|
+
.slice(0, 64);
|
|
50
|
+
return slug.length > 0 ? slug : "operator-override";
|
|
51
|
+
}
|
|
52
|
+
function refuseForcedIfNonTTY(opts) {
|
|
53
|
+
if (!opts.force)
|
|
54
|
+
return;
|
|
55
|
+
if (opts.iAmTheOperator === true)
|
|
56
|
+
return;
|
|
57
|
+
// Deliberate divergence from `harness pause`'s twin guard
|
|
58
|
+
// (`src/cli/pause/index.ts`'s `refuseIfAgentShell`): this verb is
|
|
59
|
+
// designed to be runnable from inside `!`-prefixed Claude Code shells
|
|
60
|
+
// where `$CLAUDE_SESSION_ID` is set (the session-id resolver below
|
|
61
|
+
// reads it as a fallback tier). Refusing on that env var would gut
|
|
62
|
+
// the `! ` UX. Operator intent is gated by the non-empty `<reason>`
|
|
63
|
+
// and the TTY / `--i-am-the-operator` check; that is sufficient.
|
|
64
|
+
const tty = opts.stdinIsTTY !== undefined ? opts.stdinIsTTY : process.stdin.isTTY === true;
|
|
65
|
+
if (tty)
|
|
66
|
+
return;
|
|
67
|
+
throw new HarnessExitError([
|
|
68
|
+
"harness approve risk --force refuses to run with non-TTY stdin (looks scripted).",
|
|
69
|
+
"",
|
|
70
|
+
"This is the load-bearing guardrail against `--force` becoming an agent-driven",
|
|
71
|
+
"bypass of a deny-tier Risk Gate decision. Run the verb from your own operator",
|
|
72
|
+
"shell (in Claude Code: prefix the command with `! `), or pass --i-am-the-operator",
|
|
73
|
+
"to acknowledge a one-off recovery script.",
|
|
74
|
+
].join("\n"), EX_USAGE);
|
|
75
|
+
}
|
|
25
76
|
function findGroundingMcp(manifest) {
|
|
26
77
|
return manifest.tools.mcp.find((m) => m.name === "grounding-mcp") ?? null;
|
|
27
78
|
}
|
|
@@ -49,10 +100,12 @@ async function writeLedgerTag(manifest, sessionId, content, opts) {
|
|
|
49
100
|
/**
|
|
50
101
|
* Resolve the target session id and write its `risk-approved:` ledger
|
|
51
102
|
* tag. Session id precedence mirrors `harness approve understanding`
|
|
52
|
-
* tiers 1-4: explicit `--session`, then `$
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* the
|
|
103
|
+
* tiers 1-4: explicit `--session`, then `$CLAUDE_CODE_SESSION_ID`
|
|
104
|
+
* (the var Claude Code itself sets), then `$CLAUDE_SESSION_ID` (legacy
|
|
105
|
+
* / docs name), then `$CODEX_SESSION_ID`, then the `.pending-approval`
|
|
106
|
+
* file the gate hook staged on its last block. There is no
|
|
107
|
+
* persisted-report tier-5 guess: the Risk Gate produces no persisted
|
|
108
|
+
* reports.
|
|
56
109
|
*
|
|
57
110
|
* Throws `HarnessExitError(EX_FAIL)` when no session id can be resolved.
|
|
58
111
|
* A degraded ledger (grounding-mcp absent / unreachable) is surfaced in
|
|
@@ -60,6 +113,12 @@ async function writeLedgerTag(manifest, sessionId, content, opts) {
|
|
|
60
113
|
* understanding`'s ledger write.
|
|
61
114
|
*/
|
|
62
115
|
export async function approveRisk(opts = {}) {
|
|
116
|
+
// --force is operator-deliberate; guard before any side effect. The
|
|
117
|
+
// session-id resolution below intentionally still runs the agent-shell
|
|
118
|
+
// env tier (CLAUDE_CODE_SESSION_ID etc.), since the operator may be
|
|
119
|
+
// running from `!` with --i-am-the-operator and the session id has to
|
|
120
|
+
// come from somewhere.
|
|
121
|
+
refuseForcedIfNonTTY(opts);
|
|
63
122
|
const generatedDir = opts.generatedDir ??
|
|
64
123
|
resolveGeneratedDir({
|
|
65
124
|
...(opts.homeDir !== undefined ? { homeDir: opts.homeDir } : {}),
|
|
@@ -71,6 +130,15 @@ export async function approveRisk(opts = {}) {
|
|
|
71
130
|
sessionId = opts.session;
|
|
72
131
|
sessionSource = "flag";
|
|
73
132
|
}
|
|
133
|
+
else if (typeof process.env.CLAUDE_CODE_SESSION_ID === "string" &&
|
|
134
|
+
process.env.CLAUDE_CODE_SESSION_ID.length > 0) {
|
|
135
|
+
// Canonical: the var Claude Code actually exports into the agent
|
|
136
|
+
// shell. Read before the legacy $CLAUDE_SESSION_ID so an operator
|
|
137
|
+
// who has both set (e.g. a manual export plus the runtime export)
|
|
138
|
+
// gets the runtime's id, not whatever they typed by hand.
|
|
139
|
+
sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
140
|
+
sessionSource = "env-claude-code";
|
|
141
|
+
}
|
|
74
142
|
else if (typeof process.env.CLAUDE_SESSION_ID === "string" &&
|
|
75
143
|
process.env.CLAUDE_SESSION_ID.length > 0) {
|
|
76
144
|
sessionId = process.env.CLAUDE_SESSION_ID;
|
|
@@ -90,7 +158,9 @@ export async function approveRisk(opts = {}) {
|
|
|
90
158
|
}
|
|
91
159
|
if (sessionId === "") {
|
|
92
160
|
throw new HarnessExitError([
|
|
93
|
-
"no session id available. Pass --session <id>, or set
|
|
161
|
+
"no session id available. Pass --session <id>, or set one of",
|
|
162
|
+
"$CLAUDE_CODE_SESSION_ID (Claude Code) / $CLAUDE_SESSION_ID (legacy) /",
|
|
163
|
+
"$CODEX_SESSION_ID (Codex).",
|
|
94
164
|
"",
|
|
95
165
|
"The understanding-gate PreToolUse hook and `harness preflight` both stage the",
|
|
96
166
|
`session id in ${generatedDir}/.pending-approval, so an arg-less`,
|
|
@@ -98,7 +168,7 @@ export async function approveRisk(opts = {}) {
|
|
|
98
168
|
"neither has run for the current session yet.",
|
|
99
169
|
"",
|
|
100
170
|
"From inside the running agent you can also read the id directly:",
|
|
101
|
-
"Claude Code exposes $
|
|
171
|
+
"Claude Code exposes $CLAUDE_CODE_SESSION_ID; Codex exposes $CODEX_SESSION_ID.",
|
|
102
172
|
].join("\n"), EX_FAIL);
|
|
103
173
|
}
|
|
104
174
|
// The manifest is needed only for the grounding-mcp command. If it
|
|
@@ -111,13 +181,22 @@ export async function approveRisk(opts = {}) {
|
|
|
111
181
|
catch {
|
|
112
182
|
/* swallow; ledger write becomes a degraded-ok */
|
|
113
183
|
}
|
|
114
|
-
|
|
184
|
+
// Forced override writes a different tag prefix (`risk-override:`)
|
|
185
|
+
// that the deny-tier `gate-prod-destructive` policy template consults.
|
|
186
|
+
// The audit-only `:forced:<reason>` suffix lets `harness audit` and
|
|
187
|
+
// `harness explain --trace` distinguish a clean require_approval
|
|
188
|
+
// approval from a deliberate deny override.
|
|
189
|
+
const forced = opts.force !== undefined;
|
|
190
|
+
const tag = forced
|
|
191
|
+
? riskOverrideTagFor(sessionId, opts.force.reason)
|
|
192
|
+
: riskApprovedTagFor(sessionId);
|
|
115
193
|
const ledgerResult = manifest
|
|
116
194
|
? await writeLedgerTag(manifest, sessionId, tag, opts)
|
|
117
195
|
: { ok: false, reason: "manifest unreadable; skipped ledger write" };
|
|
118
196
|
return {
|
|
119
197
|
sessionId,
|
|
120
198
|
sessionSource,
|
|
199
|
+
forced,
|
|
121
200
|
ledger: ledgerResult.ok
|
|
122
201
|
? { ok: true, tag }
|
|
123
202
|
: { ok: false, tag, reason: ledgerResult.reason },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"risk.js","sourceRoot":"","sources":["../../../src/cli/approve/risk.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,oEAAoE;AACpE,qEAAqE;AACrE,uEAAuE;AACvE,mEAAmE;AACnE,wEAAwE;AACxE,EAAE;AACF,sEAAsE;AACtE,sEAAsE;AACtE,yEAAyE;AACzE,mEAAmE;AACnE,wEAAwE;AACxE,oEAAoE;AACpE,0EAA0E;AAE1E,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EACL,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"risk.js","sourceRoot":"","sources":["../../../src/cli/approve/risk.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { Manifest } from "../../schema/index.js";
|
|
2
2
|
import { type LoaderOptions } from "../loader.js";
|
|
3
3
|
export interface ApproveUnderstandingOptions extends LoaderOptions {
|
|
4
|
-
/** Explicit session id (overrides $CLAUDE_SESSION_ID). */
|
|
4
|
+
/** Explicit session id (overrides $CLAUDE_CODE_SESSION_ID / $CLAUDE_SESSION_ID / $CODEX_SESSION_ID). */
|
|
5
5
|
session?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Override the approve-time report validation (priorArt enforcement for
|
|
8
|
+
* `grill_me` reports). Writes the marker / ledger / report-flip anyway
|
|
9
|
+
* and stamps the ledger tag content with a `:forced:<field>-<reason>`
|
|
10
|
+
* suffix so the audit trail records the bypass. Emergency-unblock path:
|
|
11
|
+
* the default refuses the marker when validation fails so an agent
|
|
12
|
+
* cannot ship a hollow Understanding Report and still get the gate open.
|
|
13
|
+
*/
|
|
14
|
+
force?: boolean;
|
|
6
15
|
/**
|
|
7
16
|
* Optional agent-tasks task id (harness/1ee26e77). When set, an
|
|
8
17
|
* additional task-scoped marker file is written at
|
|
@@ -73,7 +82,7 @@ export interface ApproveUnderstandingResult {
|
|
|
73
82
|
* operator when the id was not explicit (`pending-approval` / `env`),
|
|
74
83
|
* which is the moment to sanity-check it against the live session.
|
|
75
84
|
*/
|
|
76
|
-
sessionSource: "flag" | "env-claude" | "env-codex" | "pending-approval" | "newest-report";
|
|
85
|
+
sessionSource: "flag" | "env-claude-code" | "env-claude" | "env-codex" | "pending-approval" | "newest-report";
|
|
77
86
|
/**
|
|
78
87
|
* When `sessionSource` is `"newest-report"`, the absolute path of the
|
|
79
88
|
* persisted report the session id was guessed from. Undefined for
|
|
@@ -125,6 +134,33 @@ export interface ApproveUnderstandingResult {
|
|
|
125
134
|
ok: false;
|
|
126
135
|
reason: string;
|
|
127
136
|
};
|
|
137
|
+
/**
|
|
138
|
+
* Approve-time content validation of the persisted report. `mode` is
|
|
139
|
+
* the report's stamped mode field (the schema variant the report is
|
|
140
|
+
* judged against). `ok: false` means a structural rule the prompt
|
|
141
|
+
* already declared was violated (today: `grill_me` reports must have
|
|
142
|
+
* a non-empty priorArt list with no literal `- None`).
|
|
143
|
+
*
|
|
144
|
+
* `enforced: false` means the failure was bypassed via `--force`; the
|
|
145
|
+
* marker / ledger / report-flip still ran, and `ledger.tag` carries a
|
|
146
|
+
* `:forced:<field>-<reason>` suffix so audit can distinguish forced
|
|
147
|
+
* approvals from clean ones.
|
|
148
|
+
*
|
|
149
|
+
* `skipped: true` means no report was loaded to validate (no `latest`
|
|
150
|
+
* matched the session, ledger-only path); validation is silently
|
|
151
|
+
* waived because there is nothing to enforce.
|
|
152
|
+
*/
|
|
153
|
+
validation: {
|
|
154
|
+
ok: true;
|
|
155
|
+
mode: string | null;
|
|
156
|
+
} | {
|
|
157
|
+
ok: false;
|
|
158
|
+
field: string;
|
|
159
|
+
reason: string;
|
|
160
|
+
enforced: boolean;
|
|
161
|
+
} | {
|
|
162
|
+
skipped: true;
|
|
163
|
+
};
|
|
128
164
|
}
|
|
129
165
|
/**
|
|
130
166
|
* Normalise a list of task ids supplied via `opts.tasks` (or the CLI's
|