@lannguyensi/harness 0.53.0 → 0.55.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 (63) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/dist/cli/delegate/index.d.ts +15 -2
  3. package/dist/cli/delegate/index.js +128 -4
  4. package/dist/cli/delegate/index.js.map +1 -1
  5. package/dist/cli/doctor/bypass-without-auto-approve.js +10 -4
  6. package/dist/cli/doctor/bypass-without-auto-approve.js.map +1 -1
  7. package/dist/cli/dry-run.js +32 -1
  8. package/dist/cli/dry-run.js.map +1 -1
  9. package/dist/cli/index.js +13 -5
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/cli/init/profiles.js +89 -8
  12. package/dist/cli/init/profiles.js.map +1 -1
  13. package/dist/cli/init/templates.js +163 -3
  14. package/dist/cli/init/templates.js.map +1 -1
  15. package/dist/cli/pack/hook-pre-tool-use.js +217 -95
  16. package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
  17. package/dist/cli/record/index.d.ts +8 -0
  18. package/dist/cli/record/index.js +55 -7
  19. package/dist/cli/record/index.js.map +1 -1
  20. package/dist/cli/validate/checks.js +61 -9
  21. package/dist/cli/validate/checks.js.map +1 -1
  22. package/dist/io/extract.d.ts +29 -0
  23. package/dist/io/extract.js +36 -0
  24. package/dist/io/extract.js.map +1 -1
  25. package/dist/io/read-regular-file.d.ts +33 -2
  26. package/dist/io/read-regular-file.js +41 -8
  27. package/dist/io/read-regular-file.js.map +1 -1
  28. package/dist/policies/index.d.ts +1 -1
  29. package/dist/policies/index.js +1 -1
  30. package/dist/policies/index.js.map +1 -1
  31. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.d.ts +71 -4
  32. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js +75 -4
  33. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js.map +1 -1
  34. package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +1 -1
  35. package/dist/policy-packs/builtin/understanding-before-execution/index.js +1 -1
  36. package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +1 -1
  37. package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.js +9 -3
  38. package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.js.map +1 -1
  39. package/dist/runtime/destructive-shell-floor.d.ts +19 -0
  40. package/dist/runtime/destructive-shell-floor.js +597 -0
  41. package/dist/runtime/destructive-shell-floor.js.map +1 -0
  42. package/dist/runtime/intercept.d.ts +10 -7
  43. package/dist/runtime/intercept.js +108 -9
  44. package/dist/runtime/intercept.js.map +1 -1
  45. package/dist/runtime/read-only-bash.d.ts +54 -0
  46. package/dist/runtime/read-only-bash.js +775 -7
  47. package/dist/runtime/read-only-bash.js.map +1 -1
  48. package/dist/runtime/risk-classifier.js +72 -13
  49. package/dist/runtime/risk-classifier.js.map +1 -1
  50. package/dist/runtime/when-eval.d.ts +18 -5
  51. package/dist/runtime/when-eval.js +64 -11
  52. package/dist/runtime/when-eval.js.map +1 -1
  53. package/dist/runtime/workflow-policies.d.ts +43 -1
  54. package/dist/runtime/workflow-policies.js +158 -1
  55. package/dist/runtime/workflow-policies.js.map +1 -1
  56. package/dist/schema/extract.d.ts +4 -0
  57. package/dist/schema/extract.js +64 -1
  58. package/dist/schema/extract.js.map +1 -1
  59. package/dist/schema/index.d.ts +13 -0
  60. package/dist/schema/policies.d.ts +19 -0
  61. package/dist/schema/policies.js +17 -1
  62. package/dist/schema/policies.js.map +1 -1
  63. package/package.json +1 -1
@@ -213,8 +213,8 @@ hooks:
213
213
  # f2d2a29): every \`harness policy intercept\` hook below down through
214
214
  # \`risk-gate\` carries \`budget_ms: 15000\`, i.e. a Claude Code outer
215
215
  # kill-timeout of \`ceil(15000/1000) = 15\` seconds (generate-settings.ts's
216
- # \`hookTimeoutSeconds\`). This is deliberately UNIFORM across all eleven of
217
- # them, for two independent reasons:
216
+ # \`hookTimeoutSeconds\`). This is deliberately UNIFORM across all thirteen
217
+ # of them, for two independent reasons:
218
218
  #
219
219
  # 1. FAIL-CLOSED MARGIN. \`harness policy intercept\` evaluates its FULL
220
220
  # \`policies:\` list against the incoming event (src/runtime/intercept.ts,
@@ -257,7 +257,7 @@ hooks:
257
257
  # and reintroduce exactly the redundant-invocation cost the dedup
258
258
  # exists to avoid — on top of leaving whichever entry keeps a low
259
259
  # timeout still exposed to the fail-open risk above. Keeping all
260
- # eleven at the identical 15000ms budget_ms preserves the existing
260
+ # thirteen at the identical 15000ms budget_ms preserves the existing
261
261
  # one-invocation-per-matcher-group collapse (previously they all
262
262
  # collapsed onto the shared 2s floor; now they collapse onto 15s).
263
263
  - name: require-review-evidence
@@ -281,6 +281,31 @@ hooks:
281
281
  blocking: hard
282
282
  budget_ms: 15000
283
283
 
284
+ # The two OTHER agent-tasks verbs that land a PR (task 2699b476).
285
+ # \`pull_requests_merge\` is not the only merge surface the MCP server
286
+ # exposes: \`task_merge\` merges the PR attached to a task, and
287
+ # \`task_finish\` with \`autoMerge: true\` merges as part of finishing
288
+ # (both of its auto-merge modes, the soloMode work claim and the review
289
+ # claim + approve). Each needs its OWN hook entry because a hook's
290
+ # \`match\` is what \`harness apply\` projects into settings.json's
291
+ # tool-name matcher, and a matcher for \`pull_requests_merge\` never
292
+ # spawns \`harness policy intercept\` for either of these. Same
293
+ # 15000ms budget_ms as every other blocking intercept hook, for the
294
+ # same two reasons the note above gives.
295
+ - name: require-review-evidence-task-merge
296
+ event: PreToolUse
297
+ match: "mcp__agent-tasks__task_merge"
298
+ command: harness policy intercept
299
+ blocking: hard
300
+ budget_ms: 15000
301
+
302
+ - name: require-review-evidence-task-finish
303
+ event: PreToolUse
304
+ match: "mcp__agent-tasks__task_finish"
305
+ command: harness policy intercept
306
+ blocking: hard
307
+ budget_ms: 15000
308
+
284
309
  - name: require-dogfood-evidence
285
310
  event: PreToolUse
286
311
  match: "Bash"
@@ -472,6 +497,74 @@ policies:
472
497
  run:
473
498
  - 'harness record review --pr <pr> "<summary>"'
474
499
 
500
+ # The two agent-tasks verbs that ALSO land a PR (task 2699b476, closing
501
+ # the residual 99f47307 Slice 1 named twice in the CHANGELOG). Same
502
+ # shape as review-before-merge, three scope notes:
503
+ # 1. Tag shape: \`review:\${TASK_ID}\` rather than \`review:\${PR_NUMBER}\`.
504
+ # Both verbs are task-scoped: they derive owner/repo/PR number from
505
+ # the task, so \`taskId\` is the only identifier in the tool payload.
506
+ # \`harness record review --pr <pr> --task <id> "<summary>"\` writes
507
+ # the PR, branch, base AND task tags in ONE ledger fact, so a single
508
+ # recorded review satisfies all four merge gates at once.
509
+ # 2. task_finish is gated ONLY in its auto-merge mode. The verb is
510
+ # polymorphic: a plain \`task_finish\` advances the task and merges
511
+ # nothing, while \`autoMerge: true\` merges the PR (both modes do,
512
+ # the soloMode work claim and the review claim + approve). Gating
513
+ # the whole verb would block the ordinary finish call for no reason;
514
+ # \`input_match\` narrows the trigger to the merging mode alone.
515
+ # A missing \`autoMerge\` never matches, so an omitted argument
516
+ # leaves the gate out of the way rather than arming it.
517
+ # 3. These sit ALONGSIDE review-before-merge / -bash, same rationale
518
+ # as the bash variant above: an operator may use any of the four
519
+ # merge surfaces, and each carries its own tag shape.
520
+ - name: review-before-task-merge
521
+ description: Block agent-tasks task_merge unless a ledger entry tagged review:<task-id> exists for this session.
522
+ trigger:
523
+ event: PreToolUse
524
+ match: "mcp__agent-tasks__task_merge"
525
+ extract:
526
+ TASK_ID: "toolArgs.taskId"
527
+ requires:
528
+ ledger_tag: "review:\${TASK_ID}"
529
+ hook: require-review-evidence-task-merge
530
+ enforcement: block
531
+ producers:
532
+ - kind: mcp
533
+ verb: mcp__grounding-mcp__ledger_add
534
+ example: '{sessionId:"\${SESSION_ID}", type:"fact", content:"review:\${TASK_ID}: <verdict + key findings + nits>", source:"Agent(general-purpose) review"}'
535
+ description: Spawn a review subagent against the PR diff, capture its verdict, then persist a ledger entry tagged with the task id. Mirror of the review-before-merge producer for the task-scoped merge surface.
536
+ ux:
537
+ cannot: "You cannot merge the PR for task \${TASK_ID} yet."
538
+ required:
539
+ - "a recorded review of task \${TASK_ID}"
540
+ run:
541
+ - 'harness record review --pr <pr> --task \${TASK_ID} "<summary>"'
542
+
543
+ - name: review-before-task-finish-automerge
544
+ description: 'Block agent-tasks task_finish with autoMerge: true unless a ledger entry tagged review:<task-id> exists for this session.'
545
+ trigger:
546
+ event: PreToolUse
547
+ match: "mcp__agent-tasks__task_finish"
548
+ input_match:
549
+ toolArgs.autoMerge: true
550
+ extract:
551
+ TASK_ID: "toolArgs.taskId"
552
+ requires:
553
+ ledger_tag: "review:\${TASK_ID}"
554
+ hook: require-review-evidence-task-finish
555
+ enforcement: block
556
+ producers:
557
+ - kind: mcp
558
+ verb: mcp__grounding-mcp__ledger_add
559
+ example: '{sessionId:"\${SESSION_ID}", type:"fact", content:"review:\${TASK_ID}: <verdict + key findings + nits>", source:"Agent(general-purpose) review"}'
560
+ description: Spawn a review subagent against the PR diff, capture its verdict, then persist a ledger entry tagged with the task id. Same evidence the task_merge gate reads, so one recorded review opens both.
561
+ ux:
562
+ cannot: "You cannot finish task \${TASK_ID} with autoMerge yet."
563
+ required:
564
+ - "a recorded review of task \${TASK_ID}"
565
+ run:
566
+ - 'harness record review --pr <pr> --task \${TASK_ID} "<summary>"'
567
+
475
568
  - name: dogfood-before-release
476
569
  description: Block npm publish / git tag v* without a recent dogfood ledger entry.
477
570
  trigger:
@@ -1080,6 +1173,73 @@ risk:
1080
1173
  - pattern: 'terraform(?:\\s+-\\S+(?:\\s+(?!destroy\\b)(?!-)\\S+)?)*\\s+destroy'
1081
1174
  categories: [destructive, infrastructure_change]
1082
1175
  severity: critical
1176
+ # Task 2929c5b7: unclassified commands no longer trivially
1177
+ # satisfy risk.severity_at_least: critical (see when-eval.ts and
1178
+ # docs/risk-gate.md's "Unclassified actions and the fail-close
1179
+ # rule") — kept in lockstep with docs/examples/full-manifest.yaml
1180
+ # by tests/cli/init-full-template-parity.test.ts.
1181
+ #
1182
+ # These patterns are the OPERATOR-EDITABLE MIRROR of the built-in
1183
+ # destructive floor (src/runtime/destructive-shell-floor.ts),
1184
+ # not the only line of defence: the floor ships in the binary and
1185
+ # already classifies these heads for an EXISTING manifest that
1186
+ # never adopts the patterns below. Edit, narrow, or raise these
1187
+ # freely: an operator pattern composes with the floor under
1188
+ # highest-severity-wins, so it can only add. The floor is
1189
+ # argv-aware where a regex cannot be (path-qualified and wrapped
1190
+ # spellings: /bin/dd, sudo dd, sh -c "dd ...", git -C <dir> push
1191
+ # -f), so a few spellings are caught by the floor alone; the
1192
+ # parity test in tests/runtime/destructive-shell-floor.test.ts
1193
+ # pins that everything caught HERE is also caught THERE, at the
1194
+ # same severity or higher.
1195
+ - pattern: '\\bdd\\s[^\\n]*\\bof='
1196
+ categories: [destructive, data_loss]
1197
+ severity: critical
1198
+ - pattern: '\\btruncate\\b[^\\n]*(\\s-[a-zA-Z]*s|--size)'
1199
+ categories: [destructive, data_loss]
1200
+ severity: critical
1201
+ - pattern: '\\bshred\\b'
1202
+ categories: [destructive, data_loss, irreversible_action]
1203
+ severity: critical
1204
+ - pattern: '\\bmkfs(\\.\\w+)?\\b'
1205
+ categories: [destructive, data_loss, infrastructure_change]
1206
+ severity: critical
1207
+ - pattern: '\\bfind\\b[^\\n]*-delete\\b'
1208
+ categories: [destructive, data_loss]
1209
+ severity: critical
1210
+ - pattern: '\\bfind\\b[^\\n]*-exec(dir)?\\s+rm\\b'
1211
+ categories: [destructive, data_loss]
1212
+ severity: critical
1213
+ - pattern: '\\bgit\\s+reset\\b[^\\n]*--hard\\b'
1214
+ categories: [destructive, data_loss]
1215
+ severity: high
1216
+ - pattern: '\\bgit\\s+push\\b[^\\n]*(--force(-with-lease)?\\b|\\s-f\\b)'
1217
+ categories: [destructive, production_mutation, deployment_change]
1218
+ severity: high
1219
+ - pattern: '\\bgit\\s+clean\\b[^\\n]*(--force\\b|\\s-[a-zA-Z]*f[a-zA-Z]*\\b)'
1220
+ categories: [destructive, data_loss]
1221
+ severity: high
1222
+ - pattern: '\\bgit\\s+checkout\\s+--\\s+\\.'
1223
+ categories: [destructive, data_loss]
1224
+ severity: high
1225
+ - pattern: '\\bgit\\s+restore\\s+\\.(\\s|$)'
1226
+ categories: [destructive, data_loss]
1227
+ severity: high
1228
+ - pattern: '\\b(chmod|chown)\\b[^\\n]*(\\s-[a-zA-Z]*R|--recursive\\b)'
1229
+ categories: [mass_update]
1230
+ severity: high
1231
+ - pattern: '\\bcurl\\b[^\\n]*(-X\\s*|--request[\\s=])(?![Gg][Ee][Tt]\\b)(?![Hh][Ee][Aa][Dd]\\b)[A-Za-z]'
1232
+ categories: [production_mutation, network_exfiltration]
1233
+ severity: high
1234
+ - pattern: '\\bcurl\\b[^\\n]*(\\s-[a-zA-Z]*[dFT]|--data\\b|--json\\b|--form(-string)?\\b|--upload-file\\b)'
1235
+ categories: [production_mutation, network_exfiltration]
1236
+ severity: high
1237
+ - pattern: '\\bcurl\\b[^\\n]*(\\s-[a-zA-Z]*[oODcK]|--output(-dir)?\\b|--remote-name\\b|--remote-header-name\\b|--dump-header\\b|--cookie-jar\\b|--config\\b|--create-dirs\\b|--etag-save\\b|--trace(-ascii)?\\b|--stderr\\b|(\\s-[a-zA-Z]*w\\b|--write-out\\b)[^\\n]*%output)'
1238
+ categories: [destructive, data_loss]
1239
+ severity: high
1240
+ - pattern: '\\bsed\\b[^\\n]*(\\s-[a-zA-Z]*i[a-zA-Z]*\\b|--in-place\\b)'
1241
+ categories: [destructive, data_loss]
1242
+ severity: high
1083
1243
 
1084
1244
  environments:
1085
1245
  resolvers:
@@ -1 +1 @@
1
- {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../../src/cli/init/templates.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../../src/cli/init/templates.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,wEAAwE;AACxE,mEAAmE;AACnE,kEAAkE;AAClE,sEAAsE;AACtE,0DAA0D;AAC1D,OAAO,EAAE,wBAAwB,EAAE,MAAM,sEAAsE,CAAC;AAEhH,uEAAuE;AACvE,kEAAkE;AAClE,MAAM,oBAAoB,GAAG,wBAAwB,CAAC,CAAC,CAAC,CAAC;AAEzD,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;CAqB/B,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAy9B3B,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6OrB,CAAC;AAEF,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAI7D,MAAM,UAAU,WAAW,CAAC,IAAkB;IAC5C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,MAAM;YACT,OAAO,aAAa,CAAC;QACvB,KAAK,MAAM;YACT,OAAO,aAAa,CAAC;QACvB,KAAK,MAAM;YACT,OAAO,aAAa,CAAC;QACvB,KAAK,SAAS;YACZ,OAAO,gBAAgB,CAAC;IAC5B,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,IAAI,wBAAuD,CAAC;AAC5D,MAAM,UAAU,8BAA8B;IAC5C,IAAI,wBAAwB,KAAK,SAAS,EAAE,CAAC;QAC3C,MAAM,QAAQ,GAAG,aAAa,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC;QACzD,wBAAwB,GAAG,QAAQ,CAAC,QAAQ;aACzC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,KAAK,IAAI,CAAC;aACvC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,wBAAwB,CAAC;AAClC,CAAC;AAsBD,yEAAyE;AACzE,yEAAyE;AACzE,uEAAuE;AACvE,uEAAuE;AACvE,gCAAgC;AAChC,MAAM,iBAAiB,GAAG,cAAc,CAAC;AAEzC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CAAC,SAAiB;IACxD,OAAO,iBAAiB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,IAAI,+BAA8E,CAAC;AACnF,MAAM,UAAU,0BAA0B;IACxC,IAAI,+BAA+B,KAAK,SAAS,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG,aAAa,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC;QACzD,MAAM,OAAO,GAA6B,EAAE,CAAC;QAC7C,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;YAClC,IAAI,CAAC,IAAI,CAAC,UAAU;gBAAE,SAAS;YAC/B,MAAM,QAAQ,GAAG,wBAAwB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC3D,IAAI,QAAQ,KAAK,SAAS;gBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QACzF,CAAC;QACD,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACvC,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC;YAC5C,IAAI,CAAC,SAAS;gBAAE,SAAS;YACzB,MAAM,QAAQ,GAAG,wBAAwB,CAAC,SAAS,CAAC,CAAC;YACrD,IAAI,QAAQ,KAAK,SAAS;gBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC7F,CAAC;QACD,+BAA+B,GAAG,OAAO,CAAC;IAC5C,CAAC;IACD,OAAO,+BAA+B,CAAC;AACzC,CAAC"}
@@ -36,14 +36,29 @@
36
36
  // failed capture, through the same `persistStdinReport` the operator
37
37
  // path uses. That log is what the block envelope's malformed-sections
38
38
  // notice is rendered from, so the per-capture write is deliberate.
39
+ //
40
+ // FOLLOW-UP (agent-tasks 49d1ee41) adds the second report channel the
41
+ // ADR's "Report capture under `-p`" fallback describes: a report-bound
42
+ // delegation (one `harness delegate --report <path>` minted) is verified
43
+ // against the CONVENTIONAL launcher-report path
44
+ // (`delegationReportPathFor`, a plain function of the child session id,
45
+ // never a value read from the payload), and on success that file's
46
+ // content is persisted the same way a transcript hit is: adopted once
47
+ // (this time keyed by the report's CONTENT hash, in the same
48
+ // `.delegation-adoptions/` ledger, so the same launcher report cannot
49
+ // mint a second marker once the first one expires), persisted `pending`
50
+ // via `persistStdinReport`, and left for the auto path below to consume.
51
+ // The transcript scan never runs for a report-bound delegation: the two
52
+ // channels are mutually exclusive per delegation, decided by whether
53
+ // `verifyDelegation` returns a `reportPathHash`.
39
54
  import { appendFileSync, existsSync, lstatSync, mkdirSync } from "node:fs";
40
55
  import * as path from "node:path";
41
56
  import { readRegularFileRejectingSymlink } from "../../io/read-regular-file.js";
42
57
  import { queryLedgerByTag, } from "../../policies/index.js";
43
58
  import { renderProducers } from "../../policies/producers.js";
44
- import { ADOPTION_LEDGER_DIRNAME, CLAUDE_CODE_HARNESS, checkOperatorApprovalMarkers, checkPersistedReport, defaultReportsDir, delegationMarkerPathFor, 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, selectNewestStrictSessionReport, verifyDelegation, } from "../../policy-packs/builtin/understanding-before-execution-runtime.js";
45
60
  import { resolveMode, toPackageMode, } from "../../policy-packs/builtin/understanding-before-execution.js";
46
- import { signingKeyExists } from "../../runtime/approval-signing.js";
61
+ import { sha256Hex, signingKeyExists } from "../../runtime/approval-signing.js";
47
62
  import { findLatestParseError, renderMalformedSectionsNotice } from "../approve/understanding.js";
48
63
  import { persistStdinReport } from "../approve/stdin-report.js";
49
64
  import { attemptAutoApproval, AUTO_APPROVE_LEDGER_SOURCE } from "./auto-approve-path.js";
@@ -719,6 +734,14 @@ export async function runPackHookPreToolUseCli(opts = {}) {
719
734
  stderr.write("harness pack hook: delegation present but signing key absent; not verified\n");
720
735
  }
721
736
  else {
737
+ // The conventional launcher-report path (a plain function of
738
+ // `childSessionId`, never a value read from the payload or from
739
+ // the delegation file itself): offered unconditionally, exactly
740
+ // as `verifyDelegation` expects. It only affects the outcome when
741
+ // the delegation actually binds a `report=` segment; for every
742
+ // other delegation it is read and then never consulted (see the
743
+ // `reportPathHash !== undefined` guard inside `verifyDelegation`).
744
+ const launcherReportPath = delegationReportPathFor(generatedDir, childSessionId);
722
745
  const verified = verifyDelegation({
723
746
  generatedDir,
724
747
  childSessionId,
@@ -735,31 +758,39 @@ export async function runPackHookPreToolUseCli(opts = {}) {
735
758
  // so there is no resolved value to reuse without widening that
736
759
  // shared runtime's return shape for one caller.
737
760
  taskId: readActiveClaim(generatedDir),
761
+ launcherReportPath,
738
762
  });
739
763
  if (!verified.ok) {
740
- if (verified.reason === "report_path_mismatch") {
741
- // This hook never passes `launcherReportPath` to
742
- // `verifyDelegation`, so a delegation that binds one (the
743
- // `--report` fallback shape) is refused here every time,
744
- // not merely on a path/content mismatch. Name the actual
745
- // limitation instead of the generic `report_path_mismatch`
746
- // wording `verified.detail` carries: the fallback shape is
747
- // issued and signature-verifiable by `harness delegate`, it
748
- // is just not yet consumed by THIS hook (a named
749
- // follow-up); the transcript scan below is the only report
750
- // channel this hook acts on today.
751
- 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`);
752
- }
753
- else {
754
- stderr.write(`harness pack hook: delegation for ${childSessionId} refused: ${verified.reason}: ${verified.detail}\n`);
755
- }
764
+ // An absent, moved, or content-modified launcher report surfaces
765
+ // here through the verifier's own distinct reasons
766
+ // (`report_missing` / `report_path_mismatch` /
767
+ // `report_content_mismatch`), same as every other refusal: no
768
+ // special-casing needed now that this hook actually checks the
769
+ // fallback shape against a file. `verified.detail` names the
770
+ // expected path for the three file-state cases this hook can
771
+ // actually reach (absent / wrong path / wrong bytes). Two of
772
+ // `verifyDelegation`'s binding-shape branches are unreachable
773
+ // from here for different reasons, so their details are worth
774
+ // spelling out separately rather than lumping them together:
775
+ // "no report path offered" never fires from this call site,
776
+ // since this hook always offers the conventional path
777
+ // (`launcherReportPath` above is unconditional); "no
778
+ // reportContentHash to check against" IS reachable here, just
779
+ // not from a delegation this hook itself minted, since
780
+ // `writeDelegationMarker` refuses to produce that half-binding
781
+ // shape (a hand-signed marker can still carry it, as the
782
+ // half-binding test in
783
+ // understanding-before-execution-delegation.test.ts shows), and
784
+ // its detail names the delegation marker's own path, not the
785
+ // report path, since there is no report path yet to name.
786
+ stderr.write(`harness pack hook: delegation for ${childSessionId} refused: ${verified.reason}: ${verified.detail}\n`);
756
787
  }
757
788
  else {
758
789
  delegation = { parentSessionId: verified.parentSessionId };
759
790
  // Key two: the child's own report. Already on disk and pending
760
791
  // (the child was denied once and its report has since been
761
- // captured) means there is nothing to scan for, so the
762
- // transcript is not read at all.
792
+ // captured) means there is nothing to scan for, so neither
793
+ // channel below is read at all.
763
794
  // `childSessionId`, not the outer `sessionId`: this whole block
764
795
  // decides the PAYLOAD's session, and the two only coincide
765
796
  // because the env fallback never wins on a path that carries a
@@ -767,93 +798,184 @@ export async function runPackHookPreToolUseCli(opts = {}) {
767
798
  // branch looks for bound to the same session the delegation,
768
799
  // the scan and the adoption ledger are all keyed by.
769
800
  const existing = selectNewestStrictSessionReport(listPersistedReports(reportsDir), childSessionId);
801
+ // A report-bound delegation (the `--report` fallback shape,
802
+ // `verified.reportPathHash !== undefined`) never falls through
803
+ // to the transcript scan: the launcher already supplied and
804
+ // proved (via the verify call above) the report the child would
805
+ // otherwise have to emit into its own transcript, so scanning
806
+ // for a second copy would be pointless and would let a
807
+ // report-bound delegation ALSO be satisfied by whatever the
808
+ // child happens to write, which defeats the point of the
809
+ // launcher fixing the content.
770
810
  if (existing === null || existing.approvalStatus !== "pending") {
771
- const transcriptPath = typeof event.transcript_path === "string" && event.transcript_path.length > 0
772
- ? event.transcript_path
773
- : null;
774
- // The once-per-session adoption ledger, read only on the path
775
- // that can actually capture something.
776
- const adoptedPath = adoptedEntriesPathFor(generatedDir, childSessionId);
777
- const adopted = transcriptPath === null
778
- ? { ok: true, ids: new Set() }
779
- : readAdoptedEntries(adoptedPath);
780
- if (transcriptPath === null) {
781
- 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`);
782
- }
783
- else if (!adopted.ok) {
784
- // Fail closed: without the ledger we cannot tell a fresh
785
- // report from one this session already spent.
786
- 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`);
787
- }
788
- else {
789
- const scan = await scanTranscriptForReport({
790
- transcriptPath,
791
- sessionId: childSessionId,
792
- maxWaitMs: autoCfg.reportScan.maxWaitMs,
793
- adopted: adopted.ids,
794
- ...(opts.reportScanClock?.now !== undefined
795
- ? { now: opts.reportScanClock.now }
796
- : {}),
797
- ...(opts.reportScanClock?.sleep !== undefined
798
- ? { sleep: opts.reportScanClock.sleep }
799
- : {}),
800
- ...(opts.reportScanClock?.pollMs !== undefined
801
- ? { pollMs: opts.reportScanClock.pollMs }
802
- : {}),
803
- });
804
- if (scan.found) {
805
- // RECORD THE ADOPTION FIRST, then persist. In this order a
806
- // failed ledger write costs one blocked call and leaves the
807
- // entry re-scannable, while the reverse order would leave a
808
- // persisted, mintable report behind an unrecorded adoption:
809
- // exactly the replay this ledger exists to stop.
810
- const recorded = recordAdoptedEntry(adoptedPath, scan.entryId);
811
- if (!recorded.ok) {
812
- stderr.write(`harness pack hook: could not record transcript entry ${scan.entryId} as adopted for session ${childSessionId} (${recorded.detail}); nothing was persisted\n`);
811
+ if (verified.reportPathHash !== undefined) {
812
+ // The bytes `verifyDelegation` already proved present,
813
+ // path-matched, and content-matched against the bound
814
+ // hash, returned verbatim on its ok shape. There is NO
815
+ // second read of `launcherReportPath` here on purpose: a
816
+ // second read would be a second trust decision, reopening
817
+ // the exact race (a rewrite between verification and
818
+ // persist) that returning the verified bytes closes
819
+ // structurally instead of by re-checking a hash.
820
+ const reportContent = verified.reportContent;
821
+ if (reportContent === undefined) {
822
+ // Cannot happen given the invariant `reportContent` is
823
+ // set whenever `reportPathHash` is (see the type's own
824
+ // doc comment); fail closed rather than persist nothing
825
+ // we can vouch for.
826
+ stderr.write(`harness pack hook: the launcher-supplied report for session ${childSessionId} at ${launcherReportPath} was verified but no content was returned; nothing was persisted\n`);
827
+ }
828
+ else {
829
+ const contentHash = sha256Hex(reportContent);
830
+ // Adoption keyed by the report's CONTENT hash, in the
831
+ // SAME once-per-session ledger the transcript scan uses,
832
+ // namespaced with a `report:` prefix so the two id shapes
833
+ // (`uuid:...` / `digest:...` for a transcript entry,
834
+ // `report:...` here) can never collide. Without this a
835
+ // launcher report could mint a fresh marker every time an
836
+ // earlier one expired, for as long as the delegation
837
+ // itself stayed valid: the delegation's TTL would
838
+ // silently become the report's own re-mint budget instead
839
+ // of a one-shot authorization.
840
+ const adoptedPath = adoptedEntriesPathFor(generatedDir, childSessionId);
841
+ const adopted = readAdoptedEntries(adoptedPath);
842
+ const entryId = `report:${contentHash}`;
843
+ if (!adopted.ok) {
844
+ // Fail closed: without the ledger we cannot tell a
845
+ // fresh report from one this session already spent.
846
+ stderr.write(`harness pack hook: the adopted-entry ledger at ${adoptedPath} could not be read (${adopted.detail}); refusing to adopt a launcher-supplied report that may already have been adopted for session ${childSessionId}\n`);
847
+ }
848
+ else if (adopted.ids.has(entryId)) {
849
+ stderr.write(`harness pack hook: the launcher-supplied report for session ${childSessionId} was already adopted by an earlier mint; refusing to re-mint from the same report (issue a fresh delegation with a new report to authorize another mint)\n`);
813
850
  }
814
851
  else {
815
- // Reuses the SAME persister `harness approve
816
- // understanding` uses for a heredoc-attached report, so
817
- // the capture is session-bound, `pending`, and validated
818
- // by one parser rather than a second one written here.
819
- // A real `new Date()` on purpose: the persisted report
820
- // must sort as the newest strict-session report against
821
- // whatever is already on disk, which a caller-injected
822
- // clock could not guarantee.
823
- const persisted = persistStdinReport({
824
- markdown: scan.markdown,
825
- reportsDir,
826
- sessionId: childSessionId,
827
- now: new Date(),
828
- mode: toPackageMode(resolveMode(declared).mode),
829
- });
830
- if (persisted.ok) {
831
- 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`);
852
+ // RECORD THE ADOPTION FIRST, then persist, same ordering
853
+ // and same reasoning as the transcript-scan path below:
854
+ // a failed ledger write costs one blocked call and
855
+ // leaves the report re-adoptable, while the reverse
856
+ // order would leave a persisted, mintable report behind
857
+ // an unrecorded adoption.
858
+ const recorded = recordAdoptedEntry(adoptedPath, entryId);
859
+ if (!recorded.ok) {
860
+ stderr.write(`harness pack hook: could not record the launcher-supplied report as adopted for session ${childSessionId} (${recorded.detail}); nothing was persisted\n`);
832
861
  }
833
862
  else {
834
- // The entry stays adopted: re-reading a report that
835
- // does not parse would fail identically forever, so
836
- // the child needs a NEW one, which is what the block's
837
- // retry instruction asks for. Set the same flag the
838
- // timeout branch sets, for that reason: the retry
839
- // sentence reads correctly for this case too, and
840
- // without it the block below would ask for a retry
841
- // implicitly while never saying so.
842
- reportScanTimedOut = true;
843
- stderr.write(`harness pack hook: the transcript report for session ${childSessionId} did not parse (${persisted.reason}); nothing was persisted\n`);
863
+ const persisted = persistStdinReport({
864
+ markdown: reportContent,
865
+ reportsDir,
866
+ sessionId: childSessionId,
867
+ now: new Date(),
868
+ mode: toPackageMode(resolveMode(declared).mode),
869
+ });
870
+ if (persisted.ok) {
871
+ stderr.write(`harness pack hook: adopted the launcher-supplied report for session ${childSessionId} from ${launcherReportPath} and persisted it pending at ${persisted.filePath}\n`);
872
+ }
873
+ else {
874
+ // The entry stays adopted: the launcher's report is
875
+ // fixed content, so re-reading it would fail to
876
+ // parse identically forever. Unlike the transcript
877
+ // path this is not recoverable by a child retry
878
+ // (the child does not control the launcher's file),
879
+ // so no retry instruction is set here.
880
+ stderr.write(`harness pack hook: the launcher-supplied report for session ${childSessionId} did not parse (${persisted.reason}); nothing was persisted\n`);
881
+ }
844
882
  }
845
883
  }
846
884
  }
847
- else if (scan.reason === "timeout") {
848
- reportScanTimedOut = true;
849
- stderr.write(scan.adoptedOnly === true
850
- ? `harness pack hook: the newest Understanding Report entry in the transcript for session ${childSessionId} was already adopted; emit a fresh report\n`
851
- : `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`);
885
+ // The transcript scan below never runs for a report-bound
886
+ // delegation: this `if` branch is its own complete
887
+ // report-capture attempt.
888
+ }
889
+ else {
890
+ const transcriptPath = typeof event.transcript_path === "string" && event.transcript_path.length > 0
891
+ ? event.transcript_path
892
+ : null;
893
+ // The once-per-session adoption ledger, read only on the path
894
+ // that can actually capture something.
895
+ const adoptedPath = adoptedEntriesPathFor(generatedDir, childSessionId);
896
+ const adopted = transcriptPath === null
897
+ ? { ok: true, ids: new Set() }
898
+ : readAdoptedEntries(adoptedPath);
899
+ if (transcriptPath === null) {
900
+ 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`);
901
+ }
902
+ else if (!adopted.ok) {
903
+ // Fail closed: without the ledger we cannot tell a fresh
904
+ // report from one this session already spent.
905
+ 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`);
852
906
  }
853
907
  else {
854
- stderr.write(`harness pack hook: the transcript at ${transcriptPath} exists but could not be read; the report for session ${childSessionId} cannot be captured\n`);
908
+ const scan = await scanTranscriptForReport({
909
+ transcriptPath,
910
+ sessionId: childSessionId,
911
+ maxWaitMs: autoCfg.reportScan.maxWaitMs,
912
+ adopted: adopted.ids,
913
+ ...(opts.reportScanClock?.now !== undefined
914
+ ? { now: opts.reportScanClock.now }
915
+ : {}),
916
+ ...(opts.reportScanClock?.sleep !== undefined
917
+ ? { sleep: opts.reportScanClock.sleep }
918
+ : {}),
919
+ ...(opts.reportScanClock?.pollMs !== undefined
920
+ ? { pollMs: opts.reportScanClock.pollMs }
921
+ : {}),
922
+ });
923
+ if (scan.found) {
924
+ // RECORD THE ADOPTION FIRST, then persist. In this order a
925
+ // failed ledger write costs one blocked call and leaves the
926
+ // entry re-scannable, while the reverse order would leave a
927
+ // persisted, mintable report behind an unrecorded adoption:
928
+ // exactly the replay this ledger exists to stop.
929
+ const recorded = recordAdoptedEntry(adoptedPath, scan.entryId);
930
+ if (!recorded.ok) {
931
+ stderr.write(`harness pack hook: could not record transcript entry ${scan.entryId} as adopted for session ${childSessionId} (${recorded.detail}); nothing was persisted\n`);
932
+ }
933
+ else {
934
+ // Reuses the SAME persister `harness approve
935
+ // understanding` uses for a heredoc-attached report, so
936
+ // the capture is session-bound, `pending`, and validated
937
+ // by one parser rather than a second one written here.
938
+ // A real `new Date()` on purpose: the persisted report
939
+ // must sort as the newest strict-session report against
940
+ // whatever is already on disk, which a caller-injected
941
+ // clock could not guarantee.
942
+ const persisted = persistStdinReport({
943
+ markdown: scan.markdown,
944
+ reportsDir,
945
+ sessionId: childSessionId,
946
+ now: new Date(),
947
+ mode: toPackageMode(resolveMode(declared).mode),
948
+ });
949
+ if (persisted.ok) {
950
+ 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`);
951
+ }
952
+ else {
953
+ // The entry stays adopted: re-reading a report that
954
+ // does not parse would fail identically forever, so
955
+ // the child needs a NEW one, which is what the block's
956
+ // retry instruction asks for. Set the same flag the
957
+ // timeout branch sets, for that reason: the retry
958
+ // sentence reads correctly for this case too, and
959
+ // without it the block below would ask for a retry
960
+ // implicitly while never saying so.
961
+ reportScanTimedOut = true;
962
+ stderr.write(`harness pack hook: the transcript report for session ${childSessionId} did not parse (${persisted.reason}); nothing was persisted\n`);
963
+ }
964
+ }
965
+ }
966
+ else if (scan.reason === "timeout") {
967
+ reportScanTimedOut = true;
968
+ stderr.write(scan.adoptedOnly === true
969
+ ? `harness pack hook: the newest Understanding Report entry in the transcript for session ${childSessionId} was already adopted; emit a fresh report\n`
970
+ : `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`);
971
+ }
972
+ else {
973
+ stderr.write(`harness pack hook: the transcript at ${transcriptPath} exists but could not be read; the report for session ${childSessionId} cannot be captured\n`);
974
+ }
855
975
  }
856
976
  }
977
+ // Closes the `if (verified.reportPathHash !== undefined) { ... }
978
+ // else { <transcript scan> }` branch opened above.
857
979
  }
858
980
  }
859
981
  }