@lannguyensi/harness 0.51.0 → 0.52.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 +139 -0
- package/dist/cli/approve/understanding.d.ts +46 -0
- package/dist/cli/approve/understanding.js +50 -22
- package/dist/cli/approve/understanding.js.map +1 -1
- package/dist/cli/audit.d.ts +29 -0
- package/dist/cli/audit.js +130 -17
- package/dist/cli/audit.js.map +1 -1
- package/dist/cli/delegate/index.d.ts +63 -0
- package/dist/cli/delegate/index.js +313 -0
- package/dist/cli/delegate/index.js.map +1 -0
- package/dist/cli/doctor/auto-approve-mode.d.ts +37 -0
- package/dist/cli/doctor/auto-approve-mode.js +80 -0
- package/dist/cli/doctor/auto-approve-mode.js.map +1 -0
- package/dist/cli/doctor/codex-config-drift.d.ts +28 -0
- package/dist/cli/doctor/codex-config-drift.js +240 -0
- package/dist/cli/doctor/codex-config-drift.js.map +1 -0
- package/dist/cli/doctor/format.js +32 -1
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/index.js +51 -7
- package/dist/cli/doctor/index.js.map +1 -1
- package/dist/cli/doctor/settings-drift.d.ts +6 -0
- package/dist/cli/doctor/settings-drift.js +20 -6
- package/dist/cli/doctor/settings-drift.js.map +1 -1
- package/dist/cli/doctor/types.d.ts +43 -0
- package/dist/cli/doctor/types.js.map +1 -1
- package/dist/cli/doctor/ug-delegations.d.ts +26 -0
- package/dist/cli/doctor/ug-delegations.js +112 -0
- package/dist/cli/doctor/ug-delegations.js.map +1 -0
- package/dist/cli/doctor/understanding-mode-env.d.ts +2 -0
- package/dist/cli/doctor/understanding-mode-env.js +5 -2
- package/dist/cli/doctor/understanding-mode-env.js.map +1 -1
- package/dist/cli/index.js +89 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/pack/approve-escape.js +16 -7
- package/dist/cli/pack/approve-escape.js.map +1 -1
- package/dist/cli/pack/auto-approve-path.d.ts +17 -0
- package/dist/cli/pack/auto-approve-path.js +56 -3
- package/dist/cli/pack/auto-approve-path.js.map +1 -1
- package/dist/cli/pack/hook-pre-tool-use.d.ts +25 -0
- package/dist/cli/pack/hook-pre-tool-use.js +336 -3
- package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/transcript-report-scan.d.ts +71 -0
- package/dist/cli/pack/transcript-report-scan.js +359 -0
- package/dist/cli/pack/transcript-report-scan.js.map +1 -0
- package/dist/cli/smoke/index.d.ts +18 -1
- package/dist/cli/smoke/index.js +54 -0
- package/dist/cli/smoke/index.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.d.ts +76 -2
- package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.js +139 -2
- package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.d.ts +223 -0
- package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js +633 -0
- package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +2 -1
- package/dist/policy-packs/builtin/understanding-before-execution/index.js +9 -1
- package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution.d.ts +19 -0
- package/dist/policy-packs/builtin/understanding-before-execution.js +29 -0
- package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
- package/dist/runtime/approval-signing.js +28 -0
- package/dist/runtime/approval-signing.js.map +1 -1
- package/package.json +1 -1
|
@@ -76,6 +76,23 @@ export interface AutoApproveAttemptArgs {
|
|
|
76
76
|
reportsDir: string;
|
|
77
77
|
/** Step 3's verdict: a marker FILE existed and failed signature verification. */
|
|
78
78
|
markerForged: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Slice 3 (agent-tasks 37ad0b05, ADR "Decision: two-key design"): a
|
|
81
|
+
* VALID, unexpired, cwd/task-matching delegation the calling hook has
|
|
82
|
+
* already verified for this child session. Present means key one is
|
|
83
|
+
* satisfied by the delegation instead of by a `when`-listed
|
|
84
|
+
* `permission_mode`, so a `-p` child whose payload carries `default`
|
|
85
|
+
* (or no mode at all) can still reach the report check.
|
|
86
|
+
*
|
|
87
|
+
* It is a PRE-AUTHORIZATION, not an approval: every other condition in
|
|
88
|
+
* this function stays exactly as it was, key two (the child's own
|
|
89
|
+
* `pending` report) very much included. The caller is responsible for
|
|
90
|
+
* the verification; this function never reads a delegation file
|
|
91
|
+
* itself, so there is no second, weaker verifier to keep in sync.
|
|
92
|
+
*/
|
|
93
|
+
delegation?: {
|
|
94
|
+
parentSessionId: string;
|
|
95
|
+
};
|
|
79
96
|
stderr: {
|
|
80
97
|
write(s: string): void;
|
|
81
98
|
};
|
|
@@ -44,6 +44,16 @@
|
|
|
44
44
|
// but does name a per-session `transcript_path` that exists on disk at
|
|
45
45
|
// PreToolUse time.
|
|
46
46
|
//
|
|
47
|
+
// TWO ALTERNATIVE KEY-ONE SIGNALS (slice 3, agent-tasks 37ad0b05). The
|
|
48
|
+
// ADR's "Decision: two-key design" makes the trusted signal a
|
|
49
|
+
// disjunction: a `when`-listed `permission_mode`, OR a valid delegation
|
|
50
|
+
// for this child session that the CALLING HOOK has already verified and
|
|
51
|
+
// passes in as `delegation`. Both are only key ONE. Key two, the child's
|
|
52
|
+
// own strict-session `pending` report, is condition (6) below and is
|
|
53
|
+
// identical on both paths; nothing about a delegation relaxes it, and no
|
|
54
|
+
// branch here reads a delegation file, so the one verifier stays in
|
|
55
|
+
// `understanding-before-execution/delegation-markers.ts`.
|
|
56
|
+
//
|
|
47
57
|
// ONE BODY IS NOT ONE OPT-IN. Sharing the body would otherwise mean an
|
|
48
58
|
// `auto_approve` block written for Claude Code opts every Codex session
|
|
49
59
|
// in too; `auto_approve.harnesses` is the explicit per-harness
|
|
@@ -71,6 +81,15 @@ export const AUTO_APPROVE_LEDGER_SOURCE = "harness-pack-hook-pre-tool-use";
|
|
|
71
81
|
export const AUTO_APPROVE_LEDGER_SOURCE_CODEX = "harness-pack-hook-codex-pre-tool-use";
|
|
72
82
|
/** Default stderr prefix: the Claude Code hook's, byte-for-byte. */
|
|
73
83
|
const DEFAULT_LABEL = "harness pack hook";
|
|
84
|
+
/**
|
|
85
|
+
* The `<mode>` segment an auto-marker carries whenever key one came from
|
|
86
|
+
* the DELEGATION rather than from a `when`-listed `permission_mode`.
|
|
87
|
+
* `auto-mode:claude-code:delegated;delegated:<parent-sid>` reads as what
|
|
88
|
+
* it is: a child whose trusted signal was the parent's pre-authorization,
|
|
89
|
+
* not a permission mode. Used unconditionally on that path, whatever the
|
|
90
|
+
* payload's own mode says, since that mode played no part in the decision.
|
|
91
|
+
*/
|
|
92
|
+
const DELEGATED_MODE_LABEL = "delegated";
|
|
74
93
|
function decline(reason) {
|
|
75
94
|
return { approved: false, reason };
|
|
76
95
|
}
|
|
@@ -119,10 +138,35 @@ export async function attemptAutoApproval(args) {
|
|
|
119
138
|
// the configured `auto_approve.when` allowlist to stderr on every
|
|
120
139
|
// ordinary prompting-mode call (reviewer round-1 finding).
|
|
121
140
|
const mode = args.permissionMode;
|
|
122
|
-
|
|
141
|
+
const modeAllowed = permissionModeAllowed(cfg, mode);
|
|
142
|
+
if (!modeAllowed && args.delegation === undefined) {
|
|
123
143
|
return decline("permission_mode not in auto_approve.when");
|
|
124
144
|
}
|
|
125
|
-
|
|
145
|
+
// Which of the ADR's two alternative key-one signals actually applied.
|
|
146
|
+
// Logged only on the delegation path: the `when` hit is the ordinary
|
|
147
|
+
// slice-1 case and stays as silent as it was, so slice 1's stderr trail
|
|
148
|
+
// is byte-identical to what it was before this argument existed.
|
|
149
|
+
if (args.delegation !== undefined) {
|
|
150
|
+
note(modeAllowed
|
|
151
|
+
? `auto-approval key one: permission_mode ${JSON.stringify(mode)} in auto_approve.when (a valid delegation from parent session ${args.delegation.parentSessionId} is also present)`
|
|
152
|
+
: `auto-approval key one: valid delegation from parent session ${args.delegation.parentSessionId}`);
|
|
153
|
+
}
|
|
154
|
+
// The `<mode>` segment of `approvedBy` records WHICH key one applied,
|
|
155
|
+
// so it carries a payload literal in exactly one case: the mode itself
|
|
156
|
+
// was `when`-listed, i.e. the operator wrote that exact string in the
|
|
157
|
+
// config and this call matched it.
|
|
158
|
+
//
|
|
159
|
+
// On the delegation path the payload's mode is UNCONSTRAINED (that is
|
|
160
|
+
// the whole point: a `-p` child may carry `default`, or nothing) and it
|
|
161
|
+
// played no part in the decision, so it is replaced by the neutral
|
|
162
|
+
// `delegated` literal wholesale rather than filtered. Filtering only the
|
|
163
|
+
// encoding-breaking characters was not enough: `approvedBy` is a signed
|
|
164
|
+
// AUDIT record, and a payload-composed mode carrying a newline or a tab
|
|
165
|
+
// survives a `;`/`:` check, lands verbatim in the signed field, and then
|
|
166
|
+
// shows up in the doctor listing as a permission mode that never
|
|
167
|
+
// existed (reviewer finding L2). Nothing is lost: the payload's mode is
|
|
168
|
+
// not evidence of anything on this path.
|
|
169
|
+
const modeStr = modeAllowed ? mode : DELEGATED_MODE_LABEL;
|
|
126
170
|
// Resolve the audit-only ledger writer NOW, not earlier: this is the
|
|
127
171
|
// first point past both the opt-in check (1) and the `when` allowlist
|
|
128
172
|
// check (2), so `args.resolveLedger` runs only on a call that has an
|
|
@@ -267,7 +311,16 @@ export async function attemptAutoApproval(args) {
|
|
|
267
311
|
// BEFORE the approval rewrite, exactly as the approve CLI computes it.
|
|
268
312
|
const reportContentHash = sha256Hex(raw);
|
|
269
313
|
const approvedAt = new Date().toISOString();
|
|
270
|
-
|
|
314
|
+
// The parent linkage rides in the same signed `approvedBy` field the
|
|
315
|
+
// source already travels in (ADR "Audit and doctor"): no new signed
|
|
316
|
+
// field, therefore no `SIGNING_ALG` bump. `parseAutoApprovedBy` strips
|
|
317
|
+
// the suffix, so the doctor listing still buckets the marker by
|
|
318
|
+
// harness/mode. The parent id needs no escaping here: it comes out of a
|
|
319
|
+
// verified delegation whose own writer already refused a `;` or `=` in
|
|
320
|
+
// it (`buildDelegationApprovedBy`'s `rejectUnsafeSegmentValue`).
|
|
321
|
+
const approvedBy = args.delegation === undefined
|
|
322
|
+
? autoApprovedByFor(args.harness, modeStr)
|
|
323
|
+
: `${autoApprovedByFor(args.harness, modeStr)};delegated:${args.delegation.parentSessionId}`;
|
|
271
324
|
// CONSUME FIRST, then sign. If the marker write fails after this, the
|
|
272
325
|
// report is spent and no marker exists: the call blocks and the same
|
|
273
326
|
// report can never mint again. The reverse order would leave a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-approve-path.js","sourceRoot":"","sources":["../../../src/cli/pack/auto-approve-path.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,0EAA0E;AAC1E,mEAAmE;AACnE,EAAE;AACF,oEAAoE;AACpE,yEAAyE;AACzE,iEAAiE;AACjE,qEAAqE;AACrE,wEAAwE;AACxE,sEAAsE;AACtE,oEAAoE;AACpE,sEAAsE;AACtE,0EAA0E;AAC1E,qEAAqE;AACrE,wEAAwE;AACxE,oEAAoE;AACpE,SAAS;AACT,EAAE;AACF,sEAAsE;AACtE,mEAAmE;AACnE,+DAA+D;AAC/D,mEAAmE;AACnE,sEAAsE;AACtE,oEAAoE;AACpE,iEAAiE;AACjE,uEAAuE;AACvE,+DAA+D;AAC/D,EAAE;AACF,8DAA8D;AAC9D,mEAAmE;AACnE,uEAAuE;AACvE,sEAAsE;AACtE,gCAAgC;AAChC,EAAE;AACF,4DAA4D;AAC5D,oEAAoE;AACpE,uEAAuE;AACvE,sEAAsE;AACtE,kEAAkE;AAClE,wEAAwE;AACxE,6DAA6D;AAC7D,sEAAsE;AACtE,wEAAwE;AACxE,uEAAuE;AACvE,mBAAmB;AACnB,EAAE;AACF,uEAAuE;AACvE,wEAAwE;AACxE,+DAA+D;AAC/D,sEAAsE;AACtE,iCAAiC;AAEjC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,4BAA4B,EAC5B,cAAc,EACd,oBAAoB,EACpB,gBAAgB,EAChB,qBAAqB,EACrB,+BAA+B,EAC/B,mBAAmB,GACpB,MAAM,sEAAsE,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAChF,OAAO,EACL,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAE7F;;;GAGG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,gCAAgC,CAAC;AAE3E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,sCAAsC,CAAC;AAEvF,oEAAoE;AACpE,MAAM,aAAa,GAAG,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"auto-approve-path.js","sourceRoot":"","sources":["../../../src/cli/pack/auto-approve-path.ts"],"names":[],"mappings":"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"}
|
|
@@ -42,6 +42,17 @@ export interface PackHookPreToolUseOptions extends LoaderOptions {
|
|
|
42
42
|
* real-time sleep.
|
|
43
43
|
*/
|
|
44
44
|
now?: Date;
|
|
45
|
+
/**
|
|
46
|
+
* Clock / sleep / interval injection for the slice-3 transcript scan
|
|
47
|
+
* (test). The scan is a bounded POLL, so without this a suite covering
|
|
48
|
+
* its timeout would have to sleep for real time. Production leaves it
|
|
49
|
+
* unset and the scan uses `Date.now` and a real `setTimeout`.
|
|
50
|
+
*/
|
|
51
|
+
reportScanClock?: {
|
|
52
|
+
now?: () => number;
|
|
53
|
+
sleep?: (ms: number) => Promise<void>;
|
|
54
|
+
pollMs?: number;
|
|
55
|
+
};
|
|
45
56
|
}
|
|
46
57
|
export interface PackHookPreToolUseResult {
|
|
47
58
|
exitCode: number;
|
|
@@ -57,4 +68,18 @@ export interface PackHookPreToolUseResult {
|
|
|
57
68
|
/** Diagnostic line emitted to stderr (always; even on allow). */
|
|
58
69
|
diagnostic: string;
|
|
59
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The sentence appended to the standard block reason when a valid
|
|
73
|
+
* delegation was present but the child's own report had not reached the
|
|
74
|
+
* transcript within `auto_approve.report_scan.max_wait` (ADR "Report
|
|
75
|
+
* capture under `-p`": past the bound the hook blocks with an instruction
|
|
76
|
+
* that asks for REPEATED retries, because a text asking for a single
|
|
77
|
+
* retry was measured to leave the outcome to run-to-run variation).
|
|
78
|
+
*
|
|
79
|
+
* Deliberately does NOT pin a heading level. The same block envelope
|
|
80
|
+
* already carries `renderReportSchemaHint`'s "any heading level (#, ##,
|
|
81
|
+
* ###)" rule, and naming one level here would give the agent two
|
|
82
|
+
* different heading rules in one deny text.
|
|
83
|
+
*/
|
|
84
|
+
export declare const DELEGATION_REPORT_RETRY_INSTRUCTION = "Emit or re-emit your Understanding Report as an `Understanding Report` section (any heading level), then retry this command; if it is denied again, retry again.";
|
|
60
85
|
export declare function runPackHookPreToolUseCli(opts?: PackHookPreToolUseOptions): Promise<PackHookPreToolUseResult>;
|
|
@@ -20,12 +20,34 @@
|
|
|
20
20
|
// hostile. The npm package's own standalone blocker still runs as a
|
|
21
21
|
// secondary safety net for solo users, and `harness explain --trace`
|
|
22
22
|
// (Phase 4 #6) surfaces the runtime audit trail when configured.
|
|
23
|
+
//
|
|
24
|
+
// SLICE 3 (agent-tasks 37ad0b05) adds the delegation-path capture of the
|
|
25
|
+
// child's own report from its session transcript. Two rules of that path
|
|
26
|
+
// live here rather than in the modules it calls:
|
|
27
|
+
//
|
|
28
|
+
// - ONE TRANSCRIPT ENTRY IS ADOPTED AT MOST ONCE PER SESSION. Every
|
|
29
|
+
// adopted entry's id is appended to
|
|
30
|
+
// `<generatedDir>/.delegation-adoptions/<sid>` BEFORE the capture is
|
|
31
|
+
// persisted, and the scan is given that set. Without it the same
|
|
32
|
+
// report would be re-captured and re-minted every time the auto-marker
|
|
33
|
+
// expired (or a task boundary cleared it), so the delegation's TTL
|
|
34
|
+
// would silently replace the marker's.
|
|
35
|
+
// - A CAPTURE THAT DOES NOT PARSE writes a parse-error log, once per
|
|
36
|
+
// failed capture, through the same `persistStdinReport` the operator
|
|
37
|
+
// path uses. That log is what the block envelope's malformed-sections
|
|
38
|
+
// notice is rendered from, so the per-capture write is deliberate.
|
|
39
|
+
import { appendFileSync, existsSync, lstatSync, mkdirSync } from "node:fs";
|
|
23
40
|
import * as path from "node:path";
|
|
41
|
+
import { readRegularFileRejectingSymlink } from "../../io/read-regular-file.js";
|
|
24
42
|
import { queryLedgerByTag, } from "../../policies/index.js";
|
|
25
43
|
import { renderProducers } from "../../policies/producers.js";
|
|
26
|
-
import { CLAUDE_CODE_HARNESS, checkOperatorApprovalMarkers, checkPersistedReport, defaultReportsDir, matchLedgerEntries, } from "../../policy-packs/builtin/understanding-before-execution-runtime.js";
|
|
44
|
+
import { CLAUDE_CODE_HARNESS, checkOperatorApprovalMarkers, checkPersistedReport, defaultReportsDir, delegationMarkerPathFor, harnessAllowed, listPersistedReports, matchLedgerEntries, parseAutoApprove, readActiveClaim, selectNewestStrictSessionReport, verifyDelegation, } from "../../policy-packs/builtin/understanding-before-execution-runtime.js";
|
|
45
|
+
import { resolveMode, toPackageMode, } from "../../policy-packs/builtin/understanding-before-execution.js";
|
|
46
|
+
import { signingKeyExists } from "../../runtime/approval-signing.js";
|
|
27
47
|
import { findLatestParseError, renderMalformedSectionsNotice } from "../approve/understanding.js";
|
|
48
|
+
import { persistStdinReport } from "../approve/stdin-report.js";
|
|
28
49
|
import { attemptAutoApproval, AUTO_APPROVE_LEDGER_SOURCE } from "./auto-approve-path.js";
|
|
50
|
+
import { scanTranscriptForReport } from "./transcript-report-scan.js";
|
|
29
51
|
import { resolveGeneratedDir, writePendingApproval, } from "../../runtime/pending-approval.js";
|
|
30
52
|
import { resolveManifestLedgerWriter } from "../../runtime/ledger-writer.js";
|
|
31
53
|
import { isReadOnlyBashPipeline } from "../../runtime/read-only-bash.js";
|
|
@@ -37,9 +59,114 @@ import { isEscapeCommand } from "./approve-escape.js";
|
|
|
37
59
|
import { checkHookPause, loadManifestOrInjected, parseConfigUx, readStdin, } from "./hook-bootstrap.js";
|
|
38
60
|
import { renderReportSchemaHint } from "./understanding-report-schema-hint.js";
|
|
39
61
|
const PACK_NAME = "understanding-before-execution";
|
|
62
|
+
/**
|
|
63
|
+
* The sentence appended to the standard block reason when a valid
|
|
64
|
+
* delegation was present but the child's own report had not reached the
|
|
65
|
+
* transcript within `auto_approve.report_scan.max_wait` (ADR "Report
|
|
66
|
+
* capture under `-p`": past the bound the hook blocks with an instruction
|
|
67
|
+
* that asks for REPEATED retries, because a text asking for a single
|
|
68
|
+
* retry was measured to leave the outcome to run-to-run variation).
|
|
69
|
+
*
|
|
70
|
+
* Deliberately does NOT pin a heading level. The same block envelope
|
|
71
|
+
* already carries `renderReportSchemaHint`'s "any heading level (#, ##,
|
|
72
|
+
* ###)" rule, and naming one level here would give the agent two
|
|
73
|
+
* different heading rules in one deny text.
|
|
74
|
+
*/
|
|
75
|
+
export const DELEGATION_REPORT_RETRY_INSTRUCTION = "Emit or re-emit your Understanding Report as an `Understanding Report` section (any heading level), then retry this command; if it is denied again, retry again.";
|
|
40
76
|
function findGroundingMcp(manifest) {
|
|
41
77
|
return manifest.tools.mcp.find((m) => m.name === "grounding-mcp") ?? null;
|
|
42
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Directory holding the per-child ADOPTED-ENTRY ledgers, a SIBLING of
|
|
81
|
+
* `.delegations/` itself rather than a subdirectory of it:
|
|
82
|
+
* `<generatedDir>/.delegation-adoptions/<sid>`.
|
|
83
|
+
*
|
|
84
|
+
* Never inside `.delegations/`, and never in `.approvals/`: these files
|
|
85
|
+
* record what was SPENT, they are not approvals and must never land where
|
|
86
|
+
* a marker scan or the doctor's `approvedBy` listing would read them as
|
|
87
|
+
* one. Kept OUT of `.delegations/` for two reasons: `harness doctor`'s
|
|
88
|
+
* delegations metric counts every regular file directly under
|
|
89
|
+
* `.delegations/` (a ledger file nested in there, flat or not, would be
|
|
90
|
+
* reported as an extra, unreadable delegation), and no reserved
|
|
91
|
+
* subdirectory NAME sits under `.delegations/` for a child session id to
|
|
92
|
+
* collide with, since the ledger lives at its own sibling path instead of
|
|
93
|
+
* inside `.delegations/` at all.
|
|
94
|
+
*
|
|
95
|
+
* One id per line. Ids come from the scan (`entryId`), which guarantees
|
|
96
|
+
* they carry no line break.
|
|
97
|
+
*/
|
|
98
|
+
const ADOPTION_LEDGER_DIRNAME = ".delegation-adoptions";
|
|
99
|
+
function adoptedEntriesPathFor(generatedDir, childSessionId) {
|
|
100
|
+
return path.join(generatedDir, ADOPTION_LEDGER_DIRNAME, childSessionId);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The entry ids this session has already spent. An absent file is the
|
|
104
|
+
* ordinary first-call case and reads as the empty set; every OTHER read
|
|
105
|
+
* failure fails closed with a result the caller declines on, because a
|
|
106
|
+
* ledger we cannot read cannot prove that the next transcript hit has not
|
|
107
|
+
* already been adopted. Goes through the same symlink-rejecting reader
|
|
108
|
+
* every other gate marker uses (`readRegularFileRejectingSymlink`):
|
|
109
|
+
* without it a symlink planted at this path would have the ledger read
|
|
110
|
+
* follow it to an arbitrary target the agent controls, and an empty or
|
|
111
|
+
* missing target there would read as "nothing adopted yet", the same
|
|
112
|
+
* open the delegation and approval marker reads already close.
|
|
113
|
+
*/
|
|
114
|
+
function readAdoptedEntries(filePath) {
|
|
115
|
+
const read = readRegularFileRejectingSymlink(filePath);
|
|
116
|
+
if (read.kind === "missing")
|
|
117
|
+
return { ok: true, ids: new Set() };
|
|
118
|
+
if (read.kind === "symlink") {
|
|
119
|
+
return { ok: false, detail: `${filePath} is a symlink, refusing for safety` };
|
|
120
|
+
}
|
|
121
|
+
if (read.kind === "not-regular") {
|
|
122
|
+
return { ok: false, detail: `${filePath} is not a regular file` };
|
|
123
|
+
}
|
|
124
|
+
if (read.kind !== "ok") {
|
|
125
|
+
return { ok: false, detail: `${filePath} exists but could not be read (I/O error)` };
|
|
126
|
+
}
|
|
127
|
+
const ids = new Set();
|
|
128
|
+
for (const line of read.content.split("\n")) {
|
|
129
|
+
const id = line.trim();
|
|
130
|
+
if (id.length > 0)
|
|
131
|
+
ids.add(id);
|
|
132
|
+
}
|
|
133
|
+
return { ok: true, ids };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Append one adopted entry id, creating `.delegation-adoptions/` on first
|
|
137
|
+
* use with the same default directory mode `atomicWriteFile` gives
|
|
138
|
+
* `.delegations/` itself (the ledger FILE is 0600, like every marker).
|
|
139
|
+
* `appendFileSync` opens with `O_APPEND`, so a single short write lands
|
|
140
|
+
* whole even if two hooks race on the same session; no read-modify-write,
|
|
141
|
+
* therefore nothing to lose. `lstatSync` (NOT `existsSync`/`statSync`)
|
|
142
|
+
* gates the append the same way `readRegularFileRejectingSymlink` gates
|
|
143
|
+
* the read: a symlink planted at this path would otherwise have
|
|
144
|
+
* `appendFileSync` follow it and write the adoption record through to an
|
|
145
|
+
* arbitrary target, same class of defense as the read side above.
|
|
146
|
+
*/
|
|
147
|
+
function recordAdoptedEntry(filePath, entryId) {
|
|
148
|
+
try {
|
|
149
|
+
let priorStat;
|
|
150
|
+
try {
|
|
151
|
+
priorStat = lstatSync(filePath);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
priorStat = undefined;
|
|
155
|
+
}
|
|
156
|
+
if (priorStat !== undefined && !priorStat.isFile()) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
detail: `${filePath} exists and is not a regular file, refusing to append through it`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
163
|
+
appendFileSync(filePath, `${entryId}\n`, { mode: 0o600 });
|
|
164
|
+
return { ok: true };
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
return { ok: false, detail: err.message };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
43
170
|
// The Claude Code "block" envelope. Mirrors the runtime/intercept.ts
|
|
44
171
|
// shape (PR #81): `decision: "block"` keeps legacy 2.0.x CLIs blocking,
|
|
45
172
|
// `hookSpecificOutput.permissionDecision: "deny"` is the Claude Code
|
|
@@ -76,7 +203,7 @@ function parseConfigProducers(raw, stderr) {
|
|
|
76
203
|
// the deny envelope the agent sees becomes the plain-language
|
|
77
204
|
// `{ cannot, required, run }` shape and the legacy "Understanding Gate:
|
|
78
205
|
// no approved..." + schemaHint + producers vocabulary is suppressed.
|
|
79
|
-
function blockJson(toolName, reason, producers, ux, sessionId, escapeHint, malformedSections) {
|
|
206
|
+
function blockJson(toolName, reason, producers, ux, sessionId, escapeHint, malformedSections, retryInstruction) {
|
|
80
207
|
// When the pack config declares `ux:`, the agent-facing surface
|
|
81
208
|
// becomes the plain-language `{ cannot, required, run }` shape, and
|
|
82
209
|
// the legacy schemaHint + producers block is suppressed (the ux
|
|
@@ -125,6 +252,15 @@ function blockJson(toolName, reason, producers, ux, sessionId, escapeHint, malfo
|
|
|
125
252
|
if (escapeHint) {
|
|
126
253
|
reasonText = `${reasonText}\n\n${escapeHint}`;
|
|
127
254
|
}
|
|
255
|
+
// Slice 3's repeated-retry instruction, appended only when a valid
|
|
256
|
+
// delegation was present and the child's report simply had not landed
|
|
257
|
+
// yet. It is the most immediate remediation for that specific case (and
|
|
258
|
+
// the only one the child can act on alone), so it reads last. The
|
|
259
|
+
// standard reason above is kept intact rather than replaced: an
|
|
260
|
+
// operator reading the audit trail still sees the same block cause.
|
|
261
|
+
if (retryInstruction) {
|
|
262
|
+
reasonText = `${reasonText}\n\n${retryInstruction}`;
|
|
263
|
+
}
|
|
128
264
|
return JSON.stringify({
|
|
129
265
|
decision: "block",
|
|
130
266
|
reason: reasonText,
|
|
@@ -543,6 +679,198 @@ export async function runPackHookPreToolUseCli(opts = {}) {
|
|
|
543
679
|
// up `grounding-mcp` in the manifest on demand. Stays audit-only
|
|
544
680
|
// either way: a missing `grounding-mcp` entry costs one stderr line
|
|
545
681
|
// inside the auto path, never an approval.
|
|
682
|
+
// Step 9's key-one-by-delegation branch (slice 3, agent-tasks/37ad0b05,
|
|
683
|
+
// ADR "Decision: two-key design"). Placed immediately before the
|
|
684
|
+
// `attemptAutoApproval` call and therefore, like it, only on a call
|
|
685
|
+
// every branch above has already declined. What happens here is
|
|
686
|
+
// narrow: verify the delegation, and if it holds, obtain the child's
|
|
687
|
+
// OWN report through the bounded transcript poll and persist it
|
|
688
|
+
// `pending`. It writes no marker and returns no allow of its own; the
|
|
689
|
+
// auto path below still applies every condition it applies to a
|
|
690
|
+
// `when`-matched call, the `pending`-only report rule included, and the
|
|
691
|
+
// marker re-check inside it is still the only thing that allows.
|
|
692
|
+
//
|
|
693
|
+
// Fail-closed at every step, each with its own stderr line: no opt-in,
|
|
694
|
+
// an unlisted harness, no payload `session_id`, no delegation file, an
|
|
695
|
+
// absent signing key, any refusal from `verifyDelegation`, an
|
|
696
|
+
// unparseable or absent transcript report. Each one falls through with
|
|
697
|
+
// `delegation` unset, and the block below is what the call then
|
|
698
|
+
// reaches.
|
|
699
|
+
let delegation;
|
|
700
|
+
// Set only by the bound elapsing with no report in the transcript: the
|
|
701
|
+
// measured case where the child DID what it was asked and the write is
|
|
702
|
+
// simply still in flight. That is the one case where the deny text asks
|
|
703
|
+
// for a retry, so it is the one case that sets this flag.
|
|
704
|
+
let reportScanTimedOut = false;
|
|
705
|
+
if (generatedDir !== undefined && !markerForged) {
|
|
706
|
+
// `null` stderr on purpose: `attemptAutoApproval` parses the same
|
|
707
|
+
// block a few lines below and writes the malformed-config line
|
|
708
|
+
// itself, so passing this hook's stderr here would double it.
|
|
709
|
+
const autoCfg = parseAutoApprove(declared.config["auto_approve"], null);
|
|
710
|
+
const payloadSid = typeof event.session_id === "string" && event.session_id.length > 0
|
|
711
|
+
? event.session_id
|
|
712
|
+
: null;
|
|
713
|
+
let delegationPath = null;
|
|
714
|
+
if (autoCfg !== null && harnessAllowed(autoCfg, CLAUDE_CODE_HARNESS) && payloadSid !== null) {
|
|
715
|
+
try {
|
|
716
|
+
delegationPath = delegationMarkerPathFor(generatedDir, payloadSid);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
// A session id that cannot name a file has no delegation, by
|
|
720
|
+
// construction (`verifyDelegation` draws the same conclusion).
|
|
721
|
+
delegationPath = null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (autoCfg !== null && delegationPath !== null && existsSync(delegationPath)) {
|
|
725
|
+
const childSessionId = payloadSid;
|
|
726
|
+
if (!signingKeyExists(generatedDir)) {
|
|
727
|
+
// Never verify through a path that would MINT the key
|
|
728
|
+
// (`verifyMarkerSignature` -> `getOrCreateSigningKey`). The
|
|
729
|
+
// verifier prechecks this too; this line names the condition at
|
|
730
|
+
// the hook, where the operator is reading.
|
|
731
|
+
stderr.write("harness pack hook: delegation present but signing key absent; not verified\n");
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
const verified = verifyDelegation({
|
|
735
|
+
generatedDir,
|
|
736
|
+
childSessionId,
|
|
737
|
+
// The payload's own cwd, held against the hashed binding the
|
|
738
|
+
// parent signed. Absent means the delegation's cwd binding
|
|
739
|
+
// cannot be satisfied, which the verifier reports as a
|
|
740
|
+
// `cwd_mismatch` rather than waving through.
|
|
741
|
+
cwd: typeof event.cwd === "string" && event.cwd.length > 0 ? event.cwd : null,
|
|
742
|
+
// The same active-claim id the task-scoped marker check resolves
|
|
743
|
+
// above, so a delegation issued for one task cannot authorize a
|
|
744
|
+
// child that has claimed another. Read a SECOND time here on
|
|
745
|
+
// purpose: `checkOperatorApprovalMarkers` resolves the claim
|
|
746
|
+
// inside itself and returns only a detail string, never the id,
|
|
747
|
+
// so there is no resolved value to reuse without widening that
|
|
748
|
+
// shared runtime's return shape for one caller.
|
|
749
|
+
taskId: readActiveClaim(generatedDir),
|
|
750
|
+
});
|
|
751
|
+
if (!verified.ok) {
|
|
752
|
+
if (verified.reason === "report_path_mismatch") {
|
|
753
|
+
// This hook never passes `launcherReportPath` to
|
|
754
|
+
// `verifyDelegation`, so a delegation that binds one (the
|
|
755
|
+
// `--report` fallback shape) is refused here every time,
|
|
756
|
+
// not merely on a path/content mismatch. Name the actual
|
|
757
|
+
// limitation instead of the generic `report_path_mismatch`
|
|
758
|
+
// wording `verified.detail` carries: the fallback shape is
|
|
759
|
+
// issued and signature-verifiable by `harness delegate`, it
|
|
760
|
+
// is just not yet consumed by THIS hook (a named
|
|
761
|
+
// follow-up); the transcript scan below is the only report
|
|
762
|
+
// channel this hook acts on today.
|
|
763
|
+
stderr.write(`harness pack hook: delegation for ${childSessionId} refused: it binds a launcher-supplied report file (the --report fallback shape), which is issued and verifiable but not yet consumed by the child hook (a named follow-up); the transcript scan is the only report channel this hook acts on\n`);
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
stderr.write(`harness pack hook: delegation for ${childSessionId} refused: ${verified.reason}: ${verified.detail}\n`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
delegation = { parentSessionId: verified.parentSessionId };
|
|
771
|
+
// Key two: the child's own report. Already on disk and pending
|
|
772
|
+
// (the child was denied once and its report has since been
|
|
773
|
+
// captured) means there is nothing to scan for, so the
|
|
774
|
+
// transcript is not read at all.
|
|
775
|
+
// `childSessionId`, not the outer `sessionId`: this whole block
|
|
776
|
+
// decides the PAYLOAD's session, and the two only coincide
|
|
777
|
+
// because the env fallback never wins on a path that carries a
|
|
778
|
+
// payload session_id. Spelling it out keeps the report this
|
|
779
|
+
// branch looks for bound to the same session the delegation,
|
|
780
|
+
// the scan and the adoption ledger are all keyed by.
|
|
781
|
+
const existing = selectNewestStrictSessionReport(listPersistedReports(reportsDir), childSessionId);
|
|
782
|
+
if (existing === null || existing.approvalStatus !== "pending") {
|
|
783
|
+
const transcriptPath = typeof event.transcript_path === "string" && event.transcript_path.length > 0
|
|
784
|
+
? event.transcript_path
|
|
785
|
+
: null;
|
|
786
|
+
// The once-per-session adoption ledger, read only on the path
|
|
787
|
+
// that can actually capture something.
|
|
788
|
+
const adoptedPath = adoptedEntriesPathFor(generatedDir, childSessionId);
|
|
789
|
+
const adopted = transcriptPath === null
|
|
790
|
+
? { ok: true, ids: new Set() }
|
|
791
|
+
: readAdoptedEntries(adoptedPath);
|
|
792
|
+
if (transcriptPath === null) {
|
|
793
|
+
stderr.write(`harness pack hook: delegation for ${childSessionId} is valid but the payload carries no transcript_path; the child's report cannot be captured\n`);
|
|
794
|
+
}
|
|
795
|
+
else if (!adopted.ok) {
|
|
796
|
+
// Fail closed: without the ledger we cannot tell a fresh
|
|
797
|
+
// report from one this session already spent.
|
|
798
|
+
stderr.write(`harness pack hook: the adopted-entry ledger at ${adoptedPath} could not be read (${adopted.detail}); refusing to capture a transcript entry that may already have been adopted for session ${childSessionId}\n`);
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
const scan = await scanTranscriptForReport({
|
|
802
|
+
transcriptPath,
|
|
803
|
+
sessionId: childSessionId,
|
|
804
|
+
maxWaitMs: autoCfg.reportScan.maxWaitMs,
|
|
805
|
+
adopted: adopted.ids,
|
|
806
|
+
...(opts.reportScanClock?.now !== undefined
|
|
807
|
+
? { now: opts.reportScanClock.now }
|
|
808
|
+
: {}),
|
|
809
|
+
...(opts.reportScanClock?.sleep !== undefined
|
|
810
|
+
? { sleep: opts.reportScanClock.sleep }
|
|
811
|
+
: {}),
|
|
812
|
+
...(opts.reportScanClock?.pollMs !== undefined
|
|
813
|
+
? { pollMs: opts.reportScanClock.pollMs }
|
|
814
|
+
: {}),
|
|
815
|
+
});
|
|
816
|
+
if (scan.found) {
|
|
817
|
+
// RECORD THE ADOPTION FIRST, then persist. In this order a
|
|
818
|
+
// failed ledger write costs one blocked call and leaves the
|
|
819
|
+
// entry re-scannable, while the reverse order would leave a
|
|
820
|
+
// persisted, mintable report behind an unrecorded adoption:
|
|
821
|
+
// exactly the replay this ledger exists to stop.
|
|
822
|
+
const recorded = recordAdoptedEntry(adoptedPath, scan.entryId);
|
|
823
|
+
if (!recorded.ok) {
|
|
824
|
+
stderr.write(`harness pack hook: could not record transcript entry ${scan.entryId} as adopted for session ${childSessionId} (${recorded.detail}); nothing was persisted\n`);
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
// Reuses the SAME persister `harness approve
|
|
828
|
+
// understanding` uses for a heredoc-attached report, so
|
|
829
|
+
// the capture is session-bound, `pending`, and validated
|
|
830
|
+
// by one parser rather than a second one written here.
|
|
831
|
+
// A real `new Date()` on purpose: the persisted report
|
|
832
|
+
// must sort as the newest strict-session report against
|
|
833
|
+
// whatever is already on disk, which a caller-injected
|
|
834
|
+
// clock could not guarantee.
|
|
835
|
+
const persisted = persistStdinReport({
|
|
836
|
+
markdown: scan.markdown,
|
|
837
|
+
reportsDir,
|
|
838
|
+
sessionId: childSessionId,
|
|
839
|
+
now: new Date(),
|
|
840
|
+
mode: toPackageMode(resolveMode(declared).mode),
|
|
841
|
+
});
|
|
842
|
+
if (persisted.ok) {
|
|
843
|
+
stderr.write(`harness pack hook: captured the Understanding Report for session ${childSessionId} from its own transcript (line ${scan.lineIndex}, after ${scan.waitedMs}ms) and persisted it pending at ${persisted.filePath}\n`);
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
// The entry stays adopted: re-reading a report that
|
|
847
|
+
// does not parse would fail identically forever, so
|
|
848
|
+
// the child needs a NEW one, which is what the block's
|
|
849
|
+
// retry instruction asks for. Set the same flag the
|
|
850
|
+
// timeout branch sets, for that reason: the retry
|
|
851
|
+
// sentence reads correctly for this case too, and
|
|
852
|
+
// without it the block below would ask for a retry
|
|
853
|
+
// implicitly while never saying so.
|
|
854
|
+
reportScanTimedOut = true;
|
|
855
|
+
stderr.write(`harness pack hook: the transcript report for session ${childSessionId} did not parse (${persisted.reason}); nothing was persisted\n`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
else if (scan.reason === "timeout") {
|
|
860
|
+
reportScanTimedOut = true;
|
|
861
|
+
stderr.write(scan.adoptedOnly === true
|
|
862
|
+
? `harness pack hook: the newest Understanding Report entry in the transcript for session ${childSessionId} was already adopted; emit a fresh report\n`
|
|
863
|
+
: `harness pack hook: no Understanding Report for session ${childSessionId} reached its transcript within ${autoCfg.reportScan.maxWaitMs}ms (waited ${scan.waitedMs}ms); blocking and asking the child to re-emit and retry\n`);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
stderr.write(`harness pack hook: the transcript at ${transcriptPath} exists but could not be read; the report for session ${childSessionId} cannot be captured\n`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
546
874
|
const resolveAutoLedger = () => {
|
|
547
875
|
if (opts.writeLedger)
|
|
548
876
|
return { write: opts.writeLedger };
|
|
@@ -572,6 +900,11 @@ export async function runPackHookPreToolUseCli(opts = {}) {
|
|
|
572
900
|
packConfig: declared.config,
|
|
573
901
|
reportsDir,
|
|
574
902
|
markerForged,
|
|
903
|
+
// Slice 3: present only when the branch above verified a delegation
|
|
904
|
+
// for this child session. It supplies key ONE in place of a
|
|
905
|
+
// `when`-listed `permission_mode`; every other condition, key two
|
|
906
|
+
// included, is unchanged.
|
|
907
|
+
...(delegation !== undefined ? { delegation } : {}),
|
|
575
908
|
stderr,
|
|
576
909
|
resolveLedger: resolveAutoLedger,
|
|
577
910
|
});
|
|
@@ -609,7 +942,7 @@ export async function runPackHookPreToolUseCli(opts = {}) {
|
|
|
609
942
|
const latestParseError = report.report === null
|
|
610
943
|
? findLatestParseError(path.join(path.dirname(reportsDir), "parse-errors"), sessionId)
|
|
611
944
|
: null;
|
|
612
|
-
stdout.write(`${blockJson(toolName, "no approved Understanding Report for this session", configProducers, configUx, sessionId, escapeHint, latestParseError?.malformedSections)}\n`);
|
|
945
|
+
stdout.write(`${blockJson(toolName, "no approved Understanding Report for this session", configProducers, configUx, sessionId, escapeHint, latestParseError?.malformedSections, reportScanTimedOut ? DELEGATION_REPORT_RETRY_INSTRUCTION : null)}\n`);
|
|
613
946
|
return {
|
|
614
947
|
exitCode: 0,
|
|
615
948
|
blocked: true,
|