@lannguyensi/harness 0.55.0 → 0.56.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cli/adopt/derive.js +1 -0
  3. package/dist/cli/adopt/derive.js.map +1 -1
  4. package/dist/cli/doctor/format.js +8 -0
  5. package/dist/cli/doctor/format.js.map +1 -1
  6. package/dist/cli/doctor/index.js +16 -0
  7. package/dist/cli/doctor/index.js.map +1 -1
  8. package/dist/cli/doctor/types.d.ts +16 -0
  9. package/dist/cli/doctor/types.js.map +1 -1
  10. package/dist/cli/doctor/ug-inflight.d.ts +35 -0
  11. package/dist/cli/doctor/ug-inflight.js +57 -0
  12. package/dist/cli/doctor/ug-inflight.js.map +1 -0
  13. package/dist/cli/gc/index.d.ts +2 -1
  14. package/dist/cli/gc/index.js +196 -9
  15. package/dist/cli/gc/index.js.map +1 -1
  16. package/dist/cli/index.js +42 -5
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/pack/auto-approve-path.d.ts +11 -0
  19. package/dist/cli/pack/auto-approve-path.js +4 -2
  20. package/dist/cli/pack/auto-approve-path.js.map +1 -1
  21. package/dist/cli/pack/hook-bootstrap.d.ts +90 -0
  22. package/dist/cli/pack/hook-bootstrap.js +162 -0
  23. package/dist/cli/pack/hook-bootstrap.js.map +1 -1
  24. package/dist/cli/pack/hook-pre-tool-use.js +143 -6
  25. package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
  26. package/dist/cli/pack/hook-subagent-start.d.ts +21 -0
  27. package/dist/cli/pack/hook-subagent-start.js +71 -0
  28. package/dist/cli/pack/hook-subagent-start.js.map +1 -0
  29. package/dist/cli/pack/hook-subagent-stop.d.ts +19 -0
  30. package/dist/cli/pack/hook-subagent-stop.js +41 -0
  31. package/dist/cli/pack/hook-subagent-stop.js.map +1 -0
  32. package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +2 -1
  33. package/dist/policy-packs/builtin/understanding-before-execution/index.js +9 -0
  34. package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +1 -1
  35. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.d.ts +135 -0
  36. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.js +619 -0
  37. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.js.map +1 -0
  38. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js +25 -11
  39. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js.map +1 -1
  40. package/dist/policy-packs/builtin/understanding-before-execution.d.ts +4 -4
  41. package/dist/policy-packs/builtin/understanding-before-execution.js +42 -1
  42. package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
  43. package/dist/schema/hooks.d.ts +13 -13
  44. package/dist/schema/hooks.js +1 -0
  45. package/dist/schema/hooks.js.map +1 -1
  46. package/dist/schema/index.d.ts +24 -24
  47. package/dist/schema/policies.d.ts +19 -19
  48. package/package.json +1 -1
@@ -82,3 +82,93 @@ export declare function resolveToolInput(event: {
82
82
  * hook then falls back to its legacy message shape.
83
83
  */
84
84
  export declare function parseConfigUx(raw: unknown, stderr: NodeJS.WritableStream, hookLabel: string): PolicyUx | undefined;
85
+ export interface ResolveHookPackContextOptions extends LoaderOptions {
86
+ pack?: string;
87
+ generatedDir?: string;
88
+ manifest?: Manifest;
89
+ }
90
+ export interface ResolvedHookPackContext {
91
+ manifest: Manifest;
92
+ declared: Manifest["policy_packs"][number];
93
+ generatedDir: string;
94
+ }
95
+ export type ResolveHookPackContextResult = {
96
+ ok: true;
97
+ context: ResolvedHookPackContext;
98
+ } | {
99
+ ok: false;
100
+ diagnostic: string;
101
+ };
102
+ /**
103
+ * Load the manifest (or use injection), confirm `packName` is declared and
104
+ * enabled, and resolve `generatedDir` — the fixed sequence that follows
105
+ * `loadManifestOrInjected` in nearly every pack hook. Returns a single
106
+ * failure shape with a ready-to-emit `${hookLabel}: ...` diagnostic instead
107
+ * of three separate early-return blocks, so a caller wires it as:
108
+ *
109
+ * const ctx = resolveHookPackContext(hookLabel, packName, opts);
110
+ * if (!ctx.ok) return noop(ctx.diagnostic, stderr, ...);
111
+ */
112
+ export declare function resolveHookPackContext(hookLabel: string, packName: string, opts: ResolveHookPackContextOptions): ResolveHookPackContextResult;
113
+ export interface ResolvedSessionAndAgentIds {
114
+ sessionId: string;
115
+ agentId: string;
116
+ }
117
+ export type ResolveSessionAndAgentIdsResult = {
118
+ ok: true;
119
+ ids: ResolvedSessionAndAgentIds;
120
+ } | {
121
+ ok: false;
122
+ diagnostic: string;
123
+ sessionId: string | null;
124
+ };
125
+ /**
126
+ * Parse and validate `session_id` + `agent_id` off an event body (the
127
+ * subagent-start/subagent-stop shared shape). `validateAgentId` is
128
+ * injected rather than imported here — the agent-id allowlist lives with
129
+ * the understanding-before-execution pack's in-flight records
130
+ * (`rejectMalformedAgentId`), and this module stays pack-agnostic on
131
+ * purpose (see the module header).
132
+ */
133
+ export declare function resolveSessionAndAgentIds(hookLabel: string, event: {
134
+ session_id?: unknown;
135
+ agent_id?: unknown;
136
+ }, validateAgentId: (agentId: string) => void): ResolveSessionAndAgentIdsResult;
137
+ export interface ResolveSubagentHookContextOptions extends ResolveHookPackContextOptions {
138
+ stderr: NodeJS.WritableStream;
139
+ }
140
+ export interface ResolvedSubagentHookContext {
141
+ sessionId: string;
142
+ agentId: string;
143
+ declared: Manifest["policy_packs"][number];
144
+ generatedDir: string;
145
+ }
146
+ export type ResolveSubagentHookContextResult = {
147
+ ok: true;
148
+ context: ResolvedSubagentHookContext;
149
+ } | {
150
+ ok: false;
151
+ diagnostic: string;
152
+ sessionId: string | null;
153
+ agentId: string | null;
154
+ };
155
+ /**
156
+ * The three-step preamble subagent-start and subagent-stop both ran
157
+ * verbatim before the point where their bodies diverge (write vs. clear):
158
+ * pause-sentinel check, session_id/agent_id resolution, then pack-context
159
+ * resolution. Extracted to close the residual clone between the two hook
160
+ * files (review finding, subagent-gate). `verb` names the hook for both
161
+ * the pause check's own stderr label and the caller-facing "paused,
162
+ * skipping" diagnostic (e.g. "subagent-start"); `hookLabel` is the fuller
163
+ * `harness pack hook: <verb>` prefix used in every other diagnostic.
164
+ *
165
+ * Each ok:false case reports a `sessionId`/`agentId` pair matching exactly
166
+ * what the pre-extraction call sites passed to their own `noop()`: both
167
+ * null on a pause, `sessionId` non-null (once past validation) on a
168
+ * missing/malformed agent_id, and both non-null once ids resolved but the
169
+ * pack context failed.
170
+ */
171
+ export declare function resolveSubagentHookContext(hookLabel: string, verb: string, packName: string, event: {
172
+ session_id?: unknown;
173
+ agent_id?: unknown;
174
+ }, validateAgentId: (agentId: string) => void, opts: ResolveSubagentHookContextOptions): ResolveSubagentHookContextResult;
@@ -20,6 +20,24 @@
20
20
  // Codex shim that sends `raw_input` instead — the exact shape
21
21
  // `hook-codex-post-tool-use.ts`'s own private `resolveToolInput`
22
22
  // already handles).
23
+ // 7. `resolveHookPackContext` — the declared-pack-lookup / enabled-check /
24
+ // generatedDir-resolution trio that follows manifest load in nearly
25
+ // every hook. Its shape is closely mirrored by hook-post-tool-use.ts,
26
+ // hook-track-active-claim.ts, hook-pre-tool-use.ts and the Codex
27
+ // siblings, which were deliberately left calling their own inline
28
+ // copies rather than switched over here (out of scope for the task
29
+ // that added this helper); it was extracted for the
30
+ // subagent-start/subagent-stop pair, which would otherwise have
31
+ // re-duplicated it a further two times.
32
+ // 8. `resolveSessionAndAgentIds` — session_id + agent_id parse and
33
+ // validation shared by subagent-start/subagent-stop. Pack-agnostic
34
+ // by construction: the agent-id validator is injected by the caller
35
+ // rather than imported here, so this module stays free of any
36
+ // understanding-before-execution-specific dependency.
37
+ // 9. `resolveSubagentHookContext` — the pause-check / id-resolution /
38
+ // pack-context preamble shared verbatim by subagent-start and
39
+ // subagent-stop, up to the point where their bodies diverge (write
40
+ // vs. clear). Composes 2, 7, and 8 above.
23
41
  //
24
42
  // Not used by:
25
43
  // - hook-runtime-reality.ts: its stdin reader uses async iteration + an
@@ -37,6 +55,8 @@
37
55
  // to each hook. This module covers structural boilerplate only, not semantics.
38
56
  import { checkPauseFromLoader } from "../pause-check.js";
39
57
  import { loadManifest } from "../loader.js";
58
+ import { resolveGeneratedDir } from "../../runtime/pending-approval.js";
59
+ import { rejectMalformedSessionId } from "../../runtime/reject-malformed-session-id.js";
40
60
  import { PolicyUxSchema } from "../../schema/index.js";
41
61
  // ---------------------------------------------------------------------------
42
62
  // 1. Standard stdin reader
@@ -164,4 +184,146 @@ export function parseConfigUx(raw, stderr, hookLabel) {
164
184
  }
165
185
  return result.data;
166
186
  }
187
+ /**
188
+ * Load the manifest (or use injection), confirm `packName` is declared and
189
+ * enabled, and resolve `generatedDir` — the fixed sequence that follows
190
+ * `loadManifestOrInjected` in nearly every pack hook. Returns a single
191
+ * failure shape with a ready-to-emit `${hookLabel}: ...` diagnostic instead
192
+ * of three separate early-return blocks, so a caller wires it as:
193
+ *
194
+ * const ctx = resolveHookPackContext(hookLabel, packName, opts);
195
+ * if (!ctx.ok) return noop(ctx.diagnostic, stderr, ...);
196
+ */
197
+ export function resolveHookPackContext(hookLabel, packName, opts) {
198
+ let manifest;
199
+ let manifestPath;
200
+ try {
201
+ ({ manifest, manifestPath } = loadManifestOrInjected(opts, opts.manifest));
202
+ }
203
+ catch (err) {
204
+ return {
205
+ ok: false,
206
+ diagnostic: `${hookLabel}: manifest load failed (${err.message}), skipping`,
207
+ };
208
+ }
209
+ const declared = manifest.policy_packs.find((p) => p.name === packName);
210
+ if (!declared) {
211
+ return {
212
+ ok: false,
213
+ diagnostic: `${hookLabel}: pack "${packName}" not declared in manifest, skipping`,
214
+ };
215
+ }
216
+ if (!declared.enabled) {
217
+ return {
218
+ ok: false,
219
+ diagnostic: `${hookLabel}: pack "${packName}" is enabled:false, skipping`,
220
+ };
221
+ }
222
+ const generatedDir = opts.generatedDir ??
223
+ (manifestPath !== undefined
224
+ ? resolveGeneratedDir({
225
+ ...(opts.homeDir !== undefined ? { homeDir: opts.homeDir } : {}),
226
+ manifestPath,
227
+ })
228
+ : undefined);
229
+ if (generatedDir === undefined) {
230
+ return {
231
+ ok: false,
232
+ diagnostic: `${hookLabel}: generatedDir unresolvable, skipping`,
233
+ };
234
+ }
235
+ return { ok: true, context: { manifest, declared, generatedDir } };
236
+ }
237
+ /**
238
+ * Parse and validate `session_id` + `agent_id` off an event body (the
239
+ * subagent-start/subagent-stop shared shape). `validateAgentId` is
240
+ * injected rather than imported here — the agent-id allowlist lives with
241
+ * the understanding-before-execution pack's in-flight records
242
+ * (`rejectMalformedAgentId`), and this module stays pack-agnostic on
243
+ * purpose (see the module header).
244
+ */
245
+ export function resolveSessionAndAgentIds(hookLabel, event, validateAgentId) {
246
+ const sessionId = pickString(event.session_id) ?? "";
247
+ const agentId = pickString(event.agent_id) ?? "";
248
+ if (sessionId === "") {
249
+ return { ok: false, diagnostic: `${hookLabel}: missing session_id, skipping`, sessionId: null };
250
+ }
251
+ // Validate sessionId right after the emptiness check, before any other
252
+ // early return, so every ok:false path below carries an already-rejected
253
+ // (never a raw, unvalidated) sessionId — a caller that echoes `sessionId`
254
+ // into a diagnostic or a path.join can never see an unvalidated value.
255
+ try {
256
+ rejectMalformedSessionId(sessionId);
257
+ }
258
+ catch (err) {
259
+ return {
260
+ ok: false,
261
+ diagnostic: `${hookLabel}: malformed session_id (${err.message}), skipping`,
262
+ sessionId: null,
263
+ };
264
+ }
265
+ if (agentId === "") {
266
+ return { ok: false, diagnostic: `${hookLabel}: missing agent_id, skipping`, sessionId };
267
+ }
268
+ try {
269
+ validateAgentId(agentId);
270
+ }
271
+ catch (err) {
272
+ return {
273
+ ok: false,
274
+ diagnostic: `${hookLabel}: malformed agent_id (${err.message}), skipping`,
275
+ sessionId,
276
+ };
277
+ }
278
+ return { ok: true, ids: { sessionId, agentId } };
279
+ }
280
+ /**
281
+ * The three-step preamble subagent-start and subagent-stop both ran
282
+ * verbatim before the point where their bodies diverge (write vs. clear):
283
+ * pause-sentinel check, session_id/agent_id resolution, then pack-context
284
+ * resolution. Extracted to close the residual clone between the two hook
285
+ * files (review finding, subagent-gate). `verb` names the hook for both
286
+ * the pause check's own stderr label and the caller-facing "paused,
287
+ * skipping" diagnostic (e.g. "subagent-start"); `hookLabel` is the fuller
288
+ * `harness pack hook: <verb>` prefix used in every other diagnostic.
289
+ *
290
+ * Each ok:false case reports a `sessionId`/`agentId` pair matching exactly
291
+ * what the pre-extraction call sites passed to their own `noop()`: both
292
+ * null on a pause, `sessionId` non-null (once past validation) on a
293
+ * missing/malformed agent_id, and both non-null once ids resolved but the
294
+ * pack context failed.
295
+ */
296
+ export function resolveSubagentHookContext(hookLabel, verb, packName, event, validateAgentId, opts) {
297
+ if (checkHookPause(verb, opts.stderr, opts, opts.generatedDir).paused) {
298
+ return {
299
+ ok: false,
300
+ diagnostic: `harness paused; ${verb} skipping without evaluating.`,
301
+ sessionId: null,
302
+ agentId: null,
303
+ };
304
+ }
305
+ const idsResult = resolveSessionAndAgentIds(hookLabel, event, validateAgentId);
306
+ if (!idsResult.ok) {
307
+ return {
308
+ ok: false,
309
+ diagnostic: idsResult.diagnostic,
310
+ sessionId: idsResult.sessionId,
311
+ agentId: null,
312
+ };
313
+ }
314
+ const { sessionId, agentId } = idsResult.ids;
315
+ const ctx = resolveHookPackContext(hookLabel, packName, opts);
316
+ if (!ctx.ok) {
317
+ return { ok: false, diagnostic: ctx.diagnostic, sessionId, agentId };
318
+ }
319
+ return {
320
+ ok: true,
321
+ context: {
322
+ sessionId,
323
+ agentId,
324
+ declared: ctx.context.declared,
325
+ generatedDir: ctx.context.generatedDir,
326
+ },
327
+ };
328
+ }
167
329
  //# sourceMappingURL=hook-bootstrap.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hook-bootstrap.js","sourceRoot":"","sources":["../../../src/cli/pack/hook-bootstrap.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,gEAAgE;AAChE,+BAA+B;AAC/B,EAAE;AACF,8DAA8D;AAC9D,6EAA6E;AAC7E,6DAA6D;AAC7D,2EAA2E;AAC3E,0EAA0E;AAC1E,iDAAiD;AACjD,oEAAoE;AACpE,8CAA8C;AAC9C,4EAA4E;AAC5E,wEAAwE;AACxE,sEAAsE;AACtE,wEAAwE;AACxE,6DAA6D;AAC7D,qEAAqE;AACrE,mEAAmE;AACnE,sEAAsE;AACtE,yBAAyB;AACzB,EAAE;AACF,eAAe;AACf,0EAA0E;AAC1E,+DAA+D;AAC/D,iEAAiE;AACjE,gDAAgD;AAChD,EAAE;AACF,wEAAwE;AACxE,4EAA4E;AAC5E,yEAAyE;AACzE,2EAA2E;AAC3E,wDAAwD;AACxD,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAE/E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAsB,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,cAAc,EAAgC,MAAM,uBAAuB,CAAC;AAErF,8EAA8E;AAC9E,2BAA2B;AAC3B,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,MAA6B;IAC3D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAC3B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YAClC,IAAI,IAAI,KAAK,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC;AAED,8EAA8E;AAC9E,iCAAiC;AACjC,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAC5B,SAAiB,EACjB,MAA6B,EAC7B,UAA0B,EAC1B,YAAqB,EACrB,GAAU;IAEV,OAAO,oBAAoB,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;AACpF,CAAC;AAeD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CACpC,UAAyB,EACzB,QAA8B;IAE9B,4EAA4E;IAC5E,8EAA8E;IAC9E,sDAAsD;IACtD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;IACzD,CAAC;IACD,MAAM,MAAM,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;AAC3E,CAAC;AAED,8EAA8E;AAC9E,gDAAgD;AAChD,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,GAAG,UAAqB;IACjD,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,8EAA8E;AAC9E,iDAAiD;AACjD,8EAA8E;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAGhC;IACC,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,UAAU,CAAC;IAC5D,OAAO,KAAK,CAAC,SAAS,CAAC;AACzB,CAAC;AAED,8EAA8E;AAC9E,6BAA6B;AAC7B,8EAA8E;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAC3B,GAAY,EACZ,MAA6B,EAC7B,SAAiB;IAEjB,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACxC,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAC7C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,CAAC,KAAK,CACV,GAAG,SAAS,wBAAwB,MAAM,CAAC,KAAK,CAAC,MAAM;aACpD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;aAC3D,IAAI,CAAC,IAAI,CAAC,KAAK,CACnB,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC"}
1
+ {"version":3,"file":"hook-bootstrap.js","sourceRoot":"","sources":["../../../src/cli/pack/hook-bootstrap.ts"],"names":[],"mappings":"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"}
@@ -56,7 +56,7 @@ import * as path from "node:path";
56
56
  import { readRegularFileRejectingSymlink } from "../../io/read-regular-file.js";
57
57
  import { queryLedgerByTag, } from "../../policies/index.js";
58
58
  import { renderProducers } from "../../policies/producers.js";
59
- import { ADOPTION_LEDGER_DIRNAME, CLAUDE_CODE_HARNESS, checkOperatorApprovalMarkers, checkPersistedReport, defaultReportsDir, delegationMarkerPathFor, delegationReportPathFor, harnessAllowed, listPersistedReports, matchLedgerEntries, parseAutoApprove, readActiveClaim, recordPermissionModeObservation, selectNewestStrictSessionReport, verifyDelegation, } from "../../policy-packs/builtin/understanding-before-execution-runtime.js";
59
+ import { ADOPTION_LEDGER_DIRNAME, CLAUDE_CODE_HARNESS, checkOperatorApprovalMarkers, checkPersistedReport, defaultReportsDir, delegationMarkerPathFor, delegationReportPathFor, harnessAllowed, listPersistedReports, matchLedgerEntries, parseAutoApprove, readActiveClaim, recordPermissionModeObservation, sanitizeForDisplay, selectNewestStrictSessionReport, verifyDelegation, verifyInflightRecord, } from "../../policy-packs/builtin/understanding-before-execution-runtime.js";
60
60
  import { resolveMode, toPackageMode, } from "../../policy-packs/builtin/understanding-before-execution.js";
61
61
  import { sha256Hex, signingKeyExists } from "../../runtime/approval-signing.js";
62
62
  import { findLatestParseError, renderMalformedSectionsNotice } from "../approve/understanding.js";
@@ -523,6 +523,12 @@ export async function runPackHookPreToolUseCli(opts = {}) {
523
523
  // distinct phrase from "no approval marker" so an operator/auditor can
524
524
  // tell a forgery attempt apart from the routine "never approved" case.
525
525
  let markerForged = false;
526
+ // `markers.detail` itself, hoisted alongside `markerForged` so the
527
+ // in-flight consult below can still surface it: a forged SESSION/TASK
528
+ // marker sitting next to a genuinely valid in-flight record (ADR "TTL,
529
+ // cwd, and subagents") is a forgery attempt the operator should see
530
+ // even though the in-flight record is what ends up opening the gate.
531
+ let markerForgedDetail;
526
532
  if (generatedDir !== undefined) {
527
533
  // Source 1a/1b: task-scoped marker for the currently-claimed task
528
534
  // (harness/1ee26e77 + PR #198 correctness fix), then the
@@ -533,6 +539,20 @@ export async function runPackHookPreToolUseCli(opts = {}) {
533
539
  const markers = checkOperatorApprovalMarkers(generatedDir, sessionId, declared.config, stderr);
534
540
  markerExpired = markers.expired;
535
541
  markerForged = markers.forged;
542
+ if (markerForged) {
543
+ // `markers.detail` is only ever the SESSION marker's detail on the
544
+ // unmatched path (see OperatorMarkerApproval.detail); when it is
545
+ // the TASK marker that failed verification, that fact is buried
546
+ // inside `taskCheckDetail`'s wrapping instead. Detect which one
547
+ // actually carries the forgery phrase so this line names the
548
+ // marker that was actually tampered with, not always the session
549
+ // one (review T-003 R3 L1: a forged task marker next to an intact
550
+ // session marker used to report the session's unrelated "no
551
+ // approval marker" detail here).
552
+ const taskMarkerIsForged = /forged\/unsigned marker rejected/.test(markers.taskCheckDetail);
553
+ const forgedMarkerDetail = taskMarkerIsForged ? markers.taskCheckDetail : markers.detail;
554
+ markerForgedDetail = `harness pack hook: forged/unsigned marker rejected for session ${sessionId}; ${forgedMarkerDetail}`;
555
+ }
536
556
  if (markers.source !== "task") {
537
557
  // Trace the task-marker miss to stderr so an operator chasing
538
558
  // "why isn't my approval working?" sees the active-claim vs marker
@@ -550,6 +570,59 @@ export async function runPackHookPreToolUseCli(opts = {}) {
550
570
  };
551
571
  }
552
572
  }
573
+ // Source 1c: in-flight subagent record (ADR "TTL, cwd, and subagents").
574
+ // An Agent-tool subagent shares its parent's
575
+ // `session_id`, so the marker check above cannot tell a subagent's call
576
+ // apart from the parent's own — the payload's `agent_id` is what does.
577
+ // Consulted ONLY here, ONLY when the marker check just missed AND the
578
+ // payload names a non-empty `agent_id`: a main-line call (no
579
+ // `agent_id`) never reaches `verifyInflightRecord` at all, so no record
580
+ // on disk for any agent can ever open the gate for a main-line call.
581
+ const agentId = typeof event.agent_id === "string" && event.agent_id.length > 0
582
+ ? event.agent_id
583
+ : undefined;
584
+ // Sanitized copy for every surface a human or the agent itself reads
585
+ // (the block suffix, the forged phrase, this allow diagnostic): the raw
586
+ // `agentId` above is what verification and path-building use, but an
587
+ // attacker-controlled `agent_id` is otherwise interpolated verbatim
588
+ // into stderr and the agent-facing deny text (review finding: newline
589
+ // injection, unbounded length).
590
+ const displayAgentId = agentId !== undefined ? sanitizeForDisplay(agentId) : undefined;
591
+ // True SPECIFICALLY when a record FILE existed but failed verification
592
+ // (bad signature, or its body disagreed with its own path) — the same
593
+ // distinction `markerForged` draws for the operator marker. Folded into
594
+ // the auto-approval gating below so a forged record is declined the
595
+ // same way a forged marker is, without duplicating that enforcement.
596
+ let inflightForged = false;
597
+ // True SPECIFICALLY when the record verified and agreed with its own
598
+ // path but aged past the staleness window — distinct from `forged` and
599
+ // from a record simply never existing, so the subagent sentence below
600
+ // can name the actual cause instead of always saying "no record".
601
+ let inflightStale = false;
602
+ if (generatedDir !== undefined && agentId !== undefined) {
603
+ const record = verifyInflightRecord(generatedDir, sessionId, agentId, opts.now !== undefined ? { now: opts.now } : {});
604
+ inflightForged = record.forged;
605
+ inflightStale = record.stale;
606
+ if (record.matched) {
607
+ // A forged SESSION/TASK marker sitting next to this valid record is
608
+ // a distinct forgery attempt: surfaced to stderr BEFORE the allow
609
+ // diagnostic, but never gating it — the in-flight record is its
610
+ // own, independently-verified approval authority (ADR "TTL, cwd,
611
+ // and subagents"), so a forged copy of a DIFFERENT authority must
612
+ // not turn this allow into a block.
613
+ if (markerForgedDetail !== undefined) {
614
+ stderr.write(`${markerForgedDetail}\n`);
615
+ }
616
+ const diagnostic = `harness pack hook: in-flight subagent record for agent ${displayAgentId} (${record.detail}), allowing.`;
617
+ stderr.write(`${diagnostic}\n`);
618
+ return {
619
+ exitCode: 0,
620
+ blocked: false,
621
+ approvalCheck: { approved: true, source: "inflight", detail: record.detail },
622
+ diagnostic,
623
+ };
624
+ }
625
+ }
553
626
  // Persisted-report EVIDENCE probe (task 7402301d). This used to be
554
627
  // "Source 2", an equal approval authority consulted right after a forged
555
628
  // marker was rejected, and it was unsigned: under the threat model that
@@ -573,16 +646,44 @@ export async function runPackHookPreToolUseCli(opts = {}) {
573
646
  // historic trail. The result intentionally does NOT influence the
574
647
  // allow/block decision.
575
648
  const ledger = await checkLedger(manifest, sessionId, opts);
649
+ // A subagent call (non-empty `agent_id`) that reached this point had no
650
+ // matching in-flight record — missing, for a different agent, forged,
651
+ // stale, or naming a malformed id (`verifyInflightRecord` never throws
652
+ // on any of those; it just reports `matched: false`). Branched on the
653
+ // actual verification outcome rather than always naming "no record": a
654
+ // forged or stale record is a materially different signal than one
655
+ // that never existed — the parent DID hold a valid approval at some
656
+ // point — and conflating them misdirects the agent's next action.
657
+ // Gated on `generatedDir !== undefined`: when it
658
+ // is not resolvable (test/injection path), `verifyInflightRecord` was
659
+ // never consulted at all (see `inflightForged`/`inflightStale` staying
660
+ // at their `false` initializers above), so there is no verification
661
+ // outcome to report. Named explicitly so an agent reading the deny
662
+ // text knows to stop rather than retry: unlike the operator-approval
663
+ // case, nothing this session can do mints a new record — only the
664
+ // parent's NEXT `subagent-start` call can.
665
+ const subagentRecordSentence = generatedDir !== undefined && displayAgentId !== undefined
666
+ ? inflightForged
667
+ ? ` subagent ${displayAgentId}: the in-flight record for this subagent failed verification; stop cleanly and report to the orchestrator instead of retrying.`
668
+ : inflightStale
669
+ ? ` subagent ${displayAgentId}: the in-flight record for this subagent is older than the staleness window; stop cleanly and report to the orchestrator instead of retrying.`
670
+ : ` subagent ${displayAgentId}: no in-flight approval record (the parent session held no valid approval when this subagent started, or the record was removed); stop cleanly and report to the orchestrator instead of retrying.`
671
+ : "";
576
672
  // Neither operator source approved. When a marker FILE existed but
577
673
  // failed signature verification, use a distinct reason phrase
578
674
  // ("forged/unsigned marker rejected") instead of the routine "no
579
675
  // approval marker" — audit/operator surfaces can then tell an active
580
676
  // forgery attempt (or a pre-signing legacy marker) apart from a session
581
- // that simply never approved (harness/f9485cc7).
677
+ // that simply never approved (harness/f9485cc7). A forged in-flight
678
+ // record gets its own distinct phrase for the same reason, checked
679
+ // second so a forged SESSION/TASK marker (the higher-authority forgery)
680
+ // is never masked by a merely-forged copy of it.
582
681
  const reason = generatedDir !== undefined
583
682
  ? markerForged
584
683
  ? `forged/unsigned marker rejected for session ${sessionId}; ${report.detail}; ${ledger.detail}`
585
- : `no approval marker for session ${sessionId}; ${report.detail}; ${ledger.detail}`
684
+ : inflightForged
685
+ ? `forged/unsigned in-flight record for agent ${displayAgentId} rejected for session ${sessionId}; ${report.detail}; ${ledger.detail}`
686
+ : `no approval marker for session ${sessionId}; ${report.detail}; ${ledger.detail}${subagentRecordSentence}`
586
687
  : `generatedDir not resolvable (test/injection path); ${report.detail}; ${ledger.detail}`;
587
688
  // Stage the session id so `harness approve`, run from the operator's
588
689
  // shell where $CLAUDE_CODE_SESSION_ID / $CLAUDE_SESSION_ID is unset, can resolve it without
@@ -705,7 +806,12 @@ export async function runPackHookPreToolUseCli(opts = {}) {
705
806
  // simply still in flight. That is the one case where the deny text asks
706
807
  // for a retry, so it is the one case that sets this flag.
707
808
  let reportScanTimedOut = false;
708
- if (generatedDir !== undefined && !markerForged) {
809
+ // `!inflightForged` added alongside `!markerForged`: a forged in-flight
810
+ // record is the same posture as a forged marker (ADR condition 6, "a
811
+ // forgery detected is never laundered into an approval") — no point
812
+ // spending a delegation resolution on a call already known to end in a
813
+ // decline.
814
+ if (generatedDir !== undefined && !markerForged && !inflightForged) {
709
815
  // `null` stderr on purpose: `attemptAutoApproval` parses the same
710
816
  // block a few lines below and writes the malformed-config line
711
817
  // itself, so passing this hook's stderr here would double it.
@@ -1020,7 +1126,18 @@ export async function runPackHookPreToolUseCli(opts = {}) {
1020
1126
  sessionConsistency: { kind: "env", variable: "CLAUDE_CODE_SESSION_ID" },
1021
1127
  packConfig: declared.config,
1022
1128
  reportsDir,
1023
- markerForged,
1129
+ // Folded with `inflightForged`: `attemptAutoApproval`'s own guard on
1130
+ // this flag is the ACTUAL enforcement point for "a forgery is never
1131
+ // laundered into an approval" (the `!inflightForged` added to the
1132
+ // block above only skips a doomed delegation resolution; it does not
1133
+ // by itself stop this call).
1134
+ markerForged: markerForged || inflightForged,
1135
+ // Names which of the two the flag above actually folds together, so
1136
+ // the decline diagnostic says the right artifact. A real marker
1137
+ // forgery keeps the plain "marker" wording even when both happen to
1138
+ // be true, matching the higher-authority precedence `reason` above
1139
+ // already gives it.
1140
+ forgedKind: markerForged ? "marker" : inflightForged ? "inflight" : undefined,
1024
1141
  // Slice 3: present only when the branch above verified a delegation
1025
1142
  // for this child session. It supplies key ONE in place of a
1026
1143
  // `when`-listed `permission_mode`; every other condition, key two
@@ -1063,7 +1180,27 @@ export async function runPackHookPreToolUseCli(opts = {}) {
1063
1180
  const latestParseError = report.report === null
1064
1181
  ? findLatestParseError(path.join(path.dirname(reportsDir), "parse-errors"), sessionId)
1065
1182
  : null;
1066
- stdout.write(`${blockJson(toolName, "no approved Understanding Report for this session", configProducers, configUx, sessionId, escapeHint, latestParseError?.malformedSections, reportScanTimedOut ? DELEGATION_REPORT_RETRY_INSTRUCTION : null)}\n`);
1183
+ // The subagent record sentence, delivered to the AGENT through the
1184
+ // same channel the slice-3 retry instruction uses (appended after both
1185
+ // the `ux:` and legacy envelopes, so it survives either one) rather
1186
+ // than staying stderr/audit-only, reaching only `reason` /
1187
+ // `approvalCheck.detail` and never the agent-facing stdout JSON. A
1188
+ // main-line block (`displayAgentId` undefined) never carries it.
1189
+ //
1190
+ // The retry instruction takes PRECEDENCE when both would apply: nothing
1191
+ // stops a delegated session (ADR "Report capture under `-p`") from also
1192
+ // being the parent of an in-flight subagent, but the two instructions
1193
+ // disagree on what the agent should do next — the subagent sentence
1194
+ // says stop, the retry instruction says retry — and the delegation
1195
+ // path can still succeed on the very next retry regardless of the
1196
+ // subagent record's own state, so telling the agent to stop here would
1197
+ // be wrong.
1198
+ const agentInstruction = reportScanTimedOut
1199
+ ? DELEGATION_REPORT_RETRY_INSTRUCTION
1200
+ : displayAgentId !== undefined && subagentRecordSentence.length > 0
1201
+ ? subagentRecordSentence.trim()
1202
+ : null;
1203
+ stdout.write(`${blockJson(toolName, "no approved Understanding Report for this session", configProducers, configUx, sessionId, escapeHint, latestParseError?.malformedSections, agentInstruction)}\n`);
1067
1204
  return {
1068
1205
  exitCode: 0,
1069
1206
  blocked: true,