@lannguyensi/harness 0.64.0 → 1.0.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 +64 -0
- package/README.md +46 -42
- package/dist/cli/add/index.js +17 -21
- package/dist/cli/add/index.js.map +1 -1
- package/dist/cli/adopt/derive.d.ts +2 -2
- package/dist/cli/adopt/derive.js +2 -19
- package/dist/cli/adopt/derive.js.map +1 -1
- package/dist/cli/adopt/index.d.ts +1 -1
- package/dist/cli/adopt/index.js +1 -12
- package/dist/cli/adopt/index.js.map +1 -1
- package/dist/cli/apply/apply.d.ts +20 -35
- package/dist/cli/apply/apply.js +61 -136
- package/dist/cli/apply/apply.js.map +1 -1
- package/dist/cli/apply/generate-codex-config.js +10 -58
- package/dist/cli/apply/generate-codex-config.js.map +1 -1
- package/dist/cli/apply/generate-memory-index.js +1 -2
- package/dist/cli/apply/generate-memory-index.js.map +1 -1
- package/dist/cli/apply/generate-opencode-config.d.ts +2 -10
- package/dist/cli/apply/generate-opencode-config.js +74 -113
- package/dist/cli/apply/generate-opencode-config.js.map +1 -1
- package/dist/cli/apply/generate-settings.d.ts +11 -64
- package/dist/cli/apply/generate-settings.js +27 -132
- package/dist/cli/apply/generate-settings.js.map +1 -1
- package/dist/cli/apply/index.d.ts +1 -1
- package/dist/cli/apply/index.js +1 -1
- package/dist/cli/apply/index.js.map +1 -1
- package/dist/cli/apply/install-codex-config.d.ts +153 -1
- package/dist/cli/apply/install-codex-config.js +445 -70
- package/dist/cli/apply/install-codex-config.js.map +1 -1
- package/dist/cli/apply/next-steps.d.ts +7 -0
- package/dist/cli/apply/next-steps.js +13 -1
- package/dist/cli/apply/next-steps.js.map +1 -1
- package/dist/cli/bounded-stdin.d.ts +0 -23
- package/dist/cli/bounded-stdin.js +2 -49
- package/dist/cli/bounded-stdin.js.map +1 -1
- package/dist/cli/describe.d.ts +1 -1
- package/dist/cli/describe.js +0 -3
- package/dist/cli/describe.js.map +1 -1
- package/dist/cli/diff/engine.d.ts +0 -11
- package/dist/cli/diff/engine.js +1 -17
- package/dist/cli/diff/engine.js.map +1 -1
- package/dist/cli/diff/index.js +14 -25
- package/dist/cli/diff/index.js.map +1 -1
- package/dist/cli/doctor/claude-mcp.d.ts +4 -19
- package/dist/cli/doctor/claude-mcp.js +11 -22
- package/dist/cli/doctor/claude-mcp.js.map +1 -1
- package/dist/cli/doctor/codex.d.ts +12 -14
- package/dist/cli/doctor/codex.js +31 -144
- package/dist/cli/doctor/codex.js.map +1 -1
- package/dist/cli/doctor/format.js +23 -345
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/hook-liveness.d.ts +56 -0
- package/dist/cli/doctor/hook-liveness.js +455 -0
- package/dist/cli/doctor/hook-liveness.js.map +1 -0
- package/dist/cli/doctor/index.d.ts +13 -87
- package/dist/cli/doctor/index.js +57 -713
- package/dist/cli/doctor/index.js.map +1 -1
- package/dist/cli/doctor/opencode.d.ts +2 -1
- package/dist/cli/doctor/opencode.js.map +1 -1
- package/dist/cli/doctor/types.d.ts +21 -387
- package/dist/cli/doctor/types.js.map +1 -1
- package/dist/cli/drain-stdio.d.ts +31 -0
- package/dist/cli/drain-stdio.js +76 -0
- package/dist/cli/drain-stdio.js.map +1 -0
- package/dist/cli/export.d.ts +3 -1
- package/dist/cli/export.js +7 -16
- package/dist/cli/export.js.map +1 -1
- package/dist/cli/gate/codex-disable.d.ts +29 -0
- package/dist/cli/gate/codex-disable.js +195 -0
- package/dist/cli/gate/codex-disable.js.map +1 -0
- package/dist/cli/gate/codex-enable.d.ts +41 -0
- package/dist/cli/gate/codex-enable.js +127 -0
- package/dist/cli/gate/codex-enable.js.map +1 -0
- package/dist/cli/gate/codex-snapshot.d.ts +208 -0
- package/dist/cli/gate/codex-snapshot.js +402 -0
- package/dist/cli/gate/codex-snapshot.js.map +1 -0
- package/dist/cli/gate/disable.d.ts +9 -0
- package/dist/cli/gate/disable.js +263 -28
- package/dist/cli/gate/disable.js.map +1 -1
- package/dist/cli/gate/enable.d.ts +26 -6
- package/dist/cli/gate/enable.js +152 -80
- package/dist/cli/gate/enable.js.map +1 -1
- package/dist/cli/gate/link-chain.d.ts +53 -0
- package/dist/cli/gate/link-chain.js +122 -0
- package/dist/cli/gate/link-chain.js.map +1 -0
- package/dist/cli/gate/snapshot.d.ts +289 -12
- package/dist/cli/gate/snapshot.js +481 -22
- package/dist/cli/gate/snapshot.js.map +1 -1
- package/dist/cli/index.d.ts +0 -9
- package/dist/cli/index.js +9 -2131
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/composer.d.ts +3 -6
- package/dist/cli/init/composer.js +6 -370
- package/dist/cli/init/composer.js.map +1 -1
- package/dist/cli/init/dependencies.d.ts +1 -2
- package/dist/cli/init/dependencies.js +4 -80
- package/dist/cli/init/dependencies.js.map +1 -1
- package/dist/cli/init/detect.js +5 -4
- package/dist/cli/init/detect.js.map +1 -1
- package/dist/cli/init/interactive.d.ts +1 -23
- package/dist/cli/init/interactive.js +33 -247
- package/dist/cli/init/interactive.js.map +1 -1
- package/dist/cli/init/profiles.d.ts +2 -2
- package/dist/cli/init/profiles.js +51 -245
- package/dist/cli/init/profiles.js.map +1 -1
- package/dist/cli/init/templates.d.ts +2 -37
- package/dist/cli/init/templates.js +25 -1237
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/list.d.ts +1 -1
- package/dist/cli/list.js +1 -38
- package/dist/cli/list.js.map +1 -1
- package/dist/cli/loader.d.ts +15 -1
- package/dist/cli/loader.js +56 -39
- package/dist/cli/loader.js.map +1 -1
- package/dist/cli/main.js +7 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/migrate-home/index.js +2 -2
- package/dist/cli/migrate-home/index.js.map +1 -1
- package/dist/cli/pack/add.js +1 -1
- package/dist/cli/pack/add.js.map +1 -1
- package/dist/cli/pack/hook-bootstrap.d.ts +5 -164
- package/dist/cli/pack/hook-bootstrap.js +15 -291
- package/dist/cli/pack/hook-bootstrap.js.map +1 -1
- package/dist/cli/pack/hook-branch-protection.d.ts +28 -21
- package/dist/cli/pack/hook-branch-protection.js +365 -340
- package/dist/cli/pack/hook-branch-protection.js.map +1 -1
- package/dist/cli/pack/index.d.ts +0 -1
- package/dist/cli/pack/index.js +0 -1
- package/dist/cli/pack/index.js.map +1 -1
- package/dist/cli/pack/list.js +1 -6
- package/dist/cli/pack/list.js.map +1 -1
- package/dist/cli/pack/mutate.d.ts +7 -9
- package/dist/cli/pack/mutate.js +6 -9
- package/dist/cli/pack/mutate.js.map +1 -1
- package/dist/cli/pack/reseed.d.ts +2 -2
- package/dist/cli/pack/reseed.js +16 -22
- package/dist/cli/pack/reseed.js.map +1 -1
- package/dist/cli/{delegate/register.d.ts → register-gate-group.d.ts} +1 -1
- package/dist/cli/register-gate-group.js +348 -0
- package/dist/cli/register-gate-group.js.map +1 -0
- package/dist/cli/register-inspect-group.d.ts +6 -0
- package/dist/cli/register-inspect-group.js +189 -0
- package/dist/cli/register-inspect-group.js.map +1 -0
- package/dist/cli/register-operator-lifecycle.d.ts +1 -1
- package/dist/cli/register-operator-lifecycle.js +3 -100
- package/dist/cli/register-operator-lifecycle.js.map +1 -1
- package/dist/cli/register-pack-group.d.ts +5 -0
- package/dist/cli/register-pack-group.js +134 -0
- package/dist/cli/register-pack-group.js.map +1 -0
- package/dist/cli/{register-audit-group.d.ts → register-setup-group.d.ts} +1 -1
- package/dist/cli/register-setup-group.js +495 -0
- package/dist/cli/register-setup-group.js.map +1 -0
- package/dist/cli/remove/index.d.ts +0 -14
- package/dist/cli/remove/index.js +2 -115
- package/dist/cli/remove/index.js.map +1 -1
- package/dist/cli/remove/mutate.d.ts +0 -2
- package/dist/cli/remove/mutate.js +1 -19
- package/dist/cli/remove/mutate.js.map +1 -1
- package/dist/cli/remove/register.js +2 -19
- package/dist/cli/remove/register.js.map +1 -1
- package/dist/cli/shell-quote.d.ts +7 -0
- package/dist/cli/shell-quote.js +17 -0
- package/dist/cli/shell-quote.js.map +1 -0
- package/dist/cli/uninstall/index.d.ts +4 -2
- package/dist/cli/uninstall/index.js +15 -7
- package/dist/cli/uninstall/index.js.map +1 -1
- package/dist/cli/uninstall/register.js +6 -4
- package/dist/cli/uninstall/register.js.map +1 -1
- package/dist/cli/uninstall/snapshot.d.ts +15 -0
- package/dist/cli/uninstall/snapshot.js +38 -0
- package/dist/cli/uninstall/snapshot.js.map +1 -1
- package/dist/cli/validate/checks.d.ts +0 -77
- package/dist/cli/validate/checks.js +3 -1049
- package/dist/cli/validate/checks.js.map +1 -1
- package/dist/cli/validate/index.js +19 -15
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/io/claude-mcp.d.ts +1 -1
- package/dist/io/claude-mcp.js +27 -14
- package/dist/io/claude-mcp.js.map +1 -1
- package/dist/io/harness-lock.js +2 -2
- package/dist/io/harness-lock.js.map +1 -1
- package/dist/io/lock.d.ts +0 -26
- package/dist/io/lock.js +10 -27
- package/dist/io/lock.js.map +1 -1
- package/dist/io/merge-settings.js +6 -4
- package/dist/io/merge-settings.js.map +1 -1
- package/dist/io/project-name.d.ts +22 -39
- package/dist/io/project-name.js +25 -42
- package/dist/io/project-name.js.map +1 -1
- package/dist/io/read-regular-file.d.ts +103 -40
- package/dist/io/read-regular-file.js +196 -35
- package/dist/io/read-regular-file.js.map +1 -1
- package/dist/io/restart-hints.js +3 -6
- package/dist/io/restart-hints.js.map +1 -1
- package/dist/io/version-compare.d.ts +1 -3
- package/dist/io/version-compare.js +1 -3
- package/dist/io/version-compare.js.map +1 -1
- package/dist/io/write-regular-file.d.ts +30 -0
- package/dist/io/write-regular-file.js +85 -0
- package/dist/io/write-regular-file.js.map +1 -0
- package/dist/overrides/index.d.ts +1 -0
- package/dist/overrides/index.js +1 -0
- package/dist/overrides/index.js.map +1 -1
- package/dist/overrides/merge-layers.d.ts +43 -0
- package/dist/overrides/merge-layers.js +76 -0
- package/dist/overrides/merge-layers.js.map +1 -0
- package/dist/overrides/merge.d.ts +16 -0
- package/dist/overrides/merge.js +21 -8
- package/dist/overrides/merge.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +4 -69
- package/dist/policy-packs/builtin/branch-protection-runtime.js +12 -84
- package/dist/policy-packs/builtin/branch-protection-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection.d.ts +7 -30
- package/dist/policy-packs/builtin/branch-protection.js +56 -89
- package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
- package/dist/policy-packs/config-check.d.ts +7 -5
- package/dist/policy-packs/config-check.js +2 -2
- package/dist/policy-packs/config-check.js.map +1 -1
- package/dist/policy-packs/expand.d.ts +1 -3
- package/dist/policy-packs/expand.js +5 -42
- package/dist/policy-packs/expand.js.map +1 -1
- package/dist/policy-packs/index.d.ts +3 -3
- package/dist/policy-packs/index.js +2 -2
- package/dist/policy-packs/index.js.map +1 -1
- package/dist/policy-packs/registry.d.ts +9 -38
- package/dist/policy-packs/registry.js +5 -65
- package/dist/policy-packs/registry.js.map +1 -1
- package/dist/policy-packs/runtime.d.ts +0 -4
- package/dist/policy-packs/runtime.js +9 -22
- package/dist/policy-packs/runtime.js.map +1 -1
- package/dist/policy-packs/types.d.ts +0 -19
- package/dist/policy-packs/ux-compare.d.ts +1 -11
- package/dist/policy-packs/ux-compare.js +6 -42
- package/dist/policy-packs/ux-compare.js.map +1 -1
- package/dist/policy-packs/ux-drift-check.d.ts +5 -5
- package/dist/policy-packs/ux-drift-check.js +17 -22
- package/dist/policy-packs/ux-drift-check.js.map +1 -1
- package/dist/policy-packs/version-check.d.ts +5 -26
- package/dist/policy-packs/version-check.js +19 -74
- package/dist/policy-packs/version-check.js.map +1 -1
- package/dist/probes/memory.d.ts +1 -1
- package/dist/probes/memory.js +1 -1
- package/dist/probes/memory.js.map +1 -1
- package/dist/runtime/agent-facing.d.ts +9 -0
- package/dist/runtime/agent-facing.js +28 -14
- package/dist/runtime/agent-facing.js.map +1 -1
- package/dist/runtime/bounded-text.d.ts +15 -0
- package/dist/runtime/bounded-text.js +27 -0
- package/dist/runtime/bounded-text.js.map +1 -0
- package/dist/runtime/git-branch.d.ts +125 -0
- package/dist/runtime/git-branch.js +381 -0
- package/dist/runtime/git-branch.js.map +1 -0
- package/dist/runtime/hermetic-spawn-guard.d.ts +2 -2
- package/dist/runtime/hermetic-spawn-guard.js +24 -34
- package/dist/runtime/hermetic-spawn-guard.js.map +1 -1
- package/dist/runtime/home-dir.js +3 -2
- package/dist/runtime/home-dir.js.map +1 -1
- package/dist/runtime/tool-name-aliases.d.ts +0 -6
- package/dist/runtime/tool-name-aliases.js +5 -18
- package/dist/runtime/tool-name-aliases.js.map +1 -1
- package/dist/schema/grounding.d.ts +7 -54
- package/dist/schema/grounding.js +10 -12
- package/dist/schema/grounding.js.map +1 -1
- package/dist/schema/hooks.d.ts +39 -136
- package/dist/schema/hooks.js +3 -6
- package/dist/schema/hooks.js.map +1 -1
- package/dist/schema/index.d.ts +114 -3828
- package/dist/schema/index.js +41 -80
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/memory.d.ts +54 -131
- package/dist/schema/memory.js +2 -2
- package/dist/schema/memory.js.map +1 -1
- package/dist/schema/policy-packs.d.ts +3 -45
- package/dist/schema/policy-packs.js +7 -13
- package/dist/schema/policy-packs.js.map +1 -1
- package/dist/schema/policy-ux.d.ts +7 -0
- package/dist/schema/policy-ux.js +18 -0
- package/dist/schema/policy-ux.js.map +1 -0
- package/dist/schema/removed-keys.d.ts +185 -0
- package/dist/schema/removed-keys.js +531 -0
- package/dist/schema/removed-keys.js.map +1 -0
- package/dist/schema/tools.d.ts +30 -268
- package/dist/schema/tools.js +4 -4
- package/dist/schema/tools.js.map +1 -1
- package/package.json +5 -16
- package/dist/cli/approve/branch-protection.d.ts +0 -69
- package/dist/cli/approve/branch-protection.js +0 -144
- package/dist/cli/approve/branch-protection.js.map +0 -1
- package/dist/cli/approve/risk.d.ts +0 -112
- package/dist/cli/approve/risk.js +0 -197
- package/dist/cli/approve/risk.js.map +0 -1
- package/dist/cli/approve/stdin-report.d.ts +0 -57
- package/dist/cli/approve/stdin-report.js +0 -145
- package/dist/cli/approve/stdin-report.js.map +0 -1
- package/dist/cli/approve/understanding.d.ts +0 -410
- package/dist/cli/approve/understanding.js +0 -996
- package/dist/cli/approve/understanding.js.map +0 -1
- package/dist/cli/audit.d.ts +0 -80
- package/dist/cli/audit.js +0 -252
- package/dist/cli/audit.js.map +0 -1
- package/dist/cli/delegate/index.d.ts +0 -76
- package/dist/cli/delegate/index.js +0 -444
- package/dist/cli/delegate/index.js.map +0 -1
- package/dist/cli/delegate/register.js +0 -90
- package/dist/cli/delegate/register.js.map +0 -1
- package/dist/cli/doctor/auto-approve-mode.d.ts +0 -37
- package/dist/cli/doctor/auto-approve-mode.js +0 -80
- package/dist/cli/doctor/auto-approve-mode.js.map +0 -1
- package/dist/cli/doctor/bypass-without-auto-approve.d.ts +0 -38
- package/dist/cli/doctor/bypass-without-auto-approve.js +0 -95
- package/dist/cli/doctor/bypass-without-auto-approve.js.map +0 -1
- package/dist/cli/doctor/codex-config-drift.d.ts +0 -28
- package/dist/cli/doctor/codex-config-drift.js +0 -240
- package/dist/cli/doctor/codex-config-drift.js.map +0 -1
- package/dist/cli/doctor/expire-on-tool-match.d.ts +0 -15
- package/dist/cli/doctor/expire-on-tool-match.js +0 -58
- package/dist/cli/doctor/expire-on-tool-match.js.map +0 -1
- package/dist/cli/doctor/rogue-ledger.d.ts +0 -53
- package/dist/cli/doctor/rogue-ledger.js +0 -153
- package/dist/cli/doctor/rogue-ledger.js.map +0 -1
- package/dist/cli/doctor/session-start-preflight-setup-version.d.ts +0 -83
- package/dist/cli/doctor/session-start-preflight-setup-version.js +0 -114
- package/dist/cli/doctor/session-start-preflight-setup-version.js.map +0 -1
- package/dist/cli/doctor/settings-drift.d.ts +0 -25
- package/dist/cli/doctor/settings-drift.js +0 -213
- package/dist/cli/doctor/settings-drift.js.map +0 -1
- package/dist/cli/doctor/toolchain-parity.d.ts +0 -63
- package/dist/cli/doctor/toolchain-parity.js +0 -154
- package/dist/cli/doctor/toolchain-parity.js.map +0 -1
- package/dist/cli/doctor/ug-auto-approvals.d.ts +0 -40
- package/dist/cli/doctor/ug-auto-approvals.js +0 -91
- package/dist/cli/doctor/ug-auto-approvals.js.map +0 -1
- package/dist/cli/doctor/ug-delegations.d.ts +0 -26
- package/dist/cli/doctor/ug-delegations.js +0 -121
- package/dist/cli/doctor/ug-delegations.js.map +0 -1
- package/dist/cli/doctor/ug-inflight.d.ts +0 -35
- package/dist/cli/doctor/ug-inflight.js +0 -57
- package/dist/cli/doctor/ug-inflight.js.map +0 -1
- package/dist/cli/doctor/understanding-mode-env.d.ts +0 -51
- package/dist/cli/doctor/understanding-mode-env.js +0 -98
- package/dist/cli/doctor/understanding-mode-env.js.map +0 -1
- package/dist/cli/dry-run.d.ts +0 -58
- package/dist/cli/dry-run.js +0 -283
- package/dist/cli/dry-run.js.map +0 -1
- package/dist/cli/enriched-event.d.ts +0 -35
- package/dist/cli/enriched-event.js +0 -60
- package/dist/cli/enriched-event.js.map +0 -1
- package/dist/cli/event-input.d.ts +0 -28
- package/dist/cli/event-input.js +0 -73
- package/dist/cli/event-input.js.map +0 -1
- package/dist/cli/explain-action.d.ts +0 -25
- package/dist/cli/explain-action.js +0 -57
- package/dist/cli/explain-action.js.map +0 -1
- package/dist/cli/explain-policy.d.ts +0 -147
- package/dist/cli/explain-policy.js +0 -248
- package/dist/cli/explain-policy.js.map +0 -1
- package/dist/cli/explain.d.ts +0 -31
- package/dist/cli/explain.js +0 -159
- package/dist/cli/explain.js.map +0 -1
- package/dist/cli/gc/index.d.ts +0 -63
- package/dist/cli/gc/index.js +0 -566
- package/dist/cli/gc/index.js.map +0 -1
- package/dist/cli/pack/approve-escape.d.ts +0 -19
- package/dist/cli/pack/approve-escape.js +0 -190
- package/dist/cli/pack/approve-escape.js.map +0 -1
- package/dist/cli/pack/auto-approve-path.d.ts +0 -146
- package/dist/cli/pack/auto-approve-path.js +0 -462
- package/dist/cli/pack/auto-approve-path.js.map +0 -1
- package/dist/cli/pack/hook-codex-post-tool-use.d.ts +0 -25
- package/dist/cli/pack/hook-codex-post-tool-use.js +0 -160
- package/dist/cli/pack/hook-codex-post-tool-use.js.map +0 -1
- package/dist/cli/pack/hook-codex-pre-tool-use.d.ts +0 -48
- package/dist/cli/pack/hook-codex-pre-tool-use.js +0 -474
- package/dist/cli/pack/hook-codex-pre-tool-use.js.map +0 -1
- package/dist/cli/pack/hook-codex-stop.d.ts +0 -35
- package/dist/cli/pack/hook-codex-stop.js +0 -329
- package/dist/cli/pack/hook-codex-stop.js.map +0 -1
- package/dist/cli/pack/hook-codex-user-prompt-submit.d.ts +0 -24
- package/dist/cli/pack/hook-codex-user-prompt-submit.js +0 -93
- package/dist/cli/pack/hook-codex-user-prompt-submit.js.map +0 -1
- package/dist/cli/pack/hook-post-merge-gate-record.d.ts +0 -37
- package/dist/cli/pack/hook-post-merge-gate-record.js +0 -190
- package/dist/cli/pack/hook-post-merge-gate-record.js.map +0 -1
- package/dist/cli/pack/hook-post-merge-gate.d.ts +0 -34
- package/dist/cli/pack/hook-post-merge-gate.js +0 -277
- package/dist/cli/pack/hook-post-merge-gate.js.map +0 -1
- package/dist/cli/pack/hook-post-tool-use.d.ts +0 -45
- package/dist/cli/pack/hook-post-tool-use.js +0 -137
- package/dist/cli/pack/hook-post-tool-use.js.map +0 -1
- package/dist/cli/pack/hook-pre-tool-use.d.ts +0 -92
- package/dist/cli/pack/hook-pre-tool-use.js +0 -1328
- package/dist/cli/pack/hook-pre-tool-use.js.map +0 -1
- package/dist/cli/pack/hook-runtime-reality.d.ts +0 -63
- package/dist/cli/pack/hook-runtime-reality.js +0 -210
- package/dist/cli/pack/hook-runtime-reality.js.map +0 -1
- package/dist/cli/pack/hook-solution-acceptance-writeguard.d.ts +0 -32
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js +0 -438
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +0 -1
- package/dist/cli/pack/hook-solution-acceptance.d.ts +0 -157
- package/dist/cli/pack/hook-solution-acceptance.js +0 -530
- package/dist/cli/pack/hook-solution-acceptance.js.map +0 -1
- package/dist/cli/pack/hook-stay-in-scope.d.ts +0 -37
- package/dist/cli/pack/hook-stay-in-scope.js +0 -128
- package/dist/cli/pack/hook-stay-in-scope.js.map +0 -1
- package/dist/cli/pack/hook-subagent-start.d.ts +0 -23
- package/dist/cli/pack/hook-subagent-start.js +0 -88
- package/dist/cli/pack/hook-subagent-start.js.map +0 -1
- package/dist/cli/pack/hook-subagent-stop.d.ts +0 -19
- package/dist/cli/pack/hook-subagent-stop.js +0 -41
- package/dist/cli/pack/hook-subagent-stop.js.map +0 -1
- package/dist/cli/pack/hook-track-active-claim.d.ts +0 -27
- package/dist/cli/pack/hook-track-active-claim.js +0 -275
- package/dist/cli/pack/hook-track-active-claim.js.map +0 -1
- package/dist/cli/pack/transcript-report-scan.d.ts +0 -71
- package/dist/cli/pack/transcript-report-scan.js +0 -359
- package/dist/cli/pack/transcript-report-scan.js.map +0 -1
- package/dist/cli/pack/understanding-report-schema-hint.d.ts +0 -26
- package/dist/cli/pack/understanding-report-schema-hint.js +0 -120
- package/dist/cli/pack/understanding-report-schema-hint.js.map +0 -1
- package/dist/cli/pack/upgrade.d.ts +0 -32
- package/dist/cli/pack/upgrade.js +0 -273
- package/dist/cli/pack/upgrade.js.map +0 -1
- package/dist/cli/pause/index.d.ts +0 -113
- package/dist/cli/pause/index.js +0 -251
- package/dist/cli/pause/index.js.map +0 -1
- package/dist/cli/pause-check.d.ts +0 -22
- package/dist/cli/pause-check.js +0 -54
- package/dist/cli/pause-check.js.map +0 -1
- package/dist/cli/policy/intercept.d.ts +0 -233
- package/dist/cli/policy/intercept.js +0 -1022
- package/dist/cli/policy/intercept.js.map +0 -1
- package/dist/cli/policy/risk-envelope-enrichment.d.ts +0 -40
- package/dist/cli/policy/risk-envelope-enrichment.js +0 -110
- package/dist/cli/policy/risk-envelope-enrichment.js.map +0 -1
- package/dist/cli/record/index.d.ts +0 -81
- package/dist/cli/record/index.js +0 -348
- package/dist/cli/record/index.js.map +0 -1
- package/dist/cli/register-audit-group.js +0 -91
- package/dist/cli/register-audit-group.js.map +0 -1
- package/dist/cli/resolve-env.d.ts +0 -32
- package/dist/cli/resolve-env.js +0 -46
- package/dist/cli/resolve-env.js.map +0 -1
- package/dist/cli/session-export/index.d.ts +0 -46
- package/dist/cli/session-export/index.js +0 -169
- package/dist/cli/session-export/index.js.map +0 -1
- package/dist/cli/session-export/redact.d.ts +0 -22
- package/dist/cli/session-export/redact.js +0 -47
- package/dist/cli/session-export/redact.js.map +0 -1
- package/dist/cli/session-export/transcript.d.ts +0 -24
- package/dist/cli/session-export/transcript.js +0 -164
- package/dist/cli/session-export/transcript.js.map +0 -1
- package/dist/cli/session-start/branch-check.d.ts +0 -18
- package/dist/cli/session-start/branch-check.js +0 -140
- package/dist/cli/session-start/branch-check.js.map +0 -1
- package/dist/cli/session-start/index.d.ts +0 -121
- package/dist/cli/session-start/index.js +0 -591
- package/dist/cli/session-start/index.js.map +0 -1
- package/dist/cli/session-start/shared-options.d.ts +0 -103
- package/dist/cli/session-start/shared-options.js +0 -107
- package/dist/cli/session-start/shared-options.js.map +0 -1
- package/dist/cli/session-start/stale-base-check.d.ts +0 -46
- package/dist/cli/session-start/stale-base-check.js +0 -352
- package/dist/cli/session-start/stale-base-check.js.map +0 -1
- package/dist/cli/session-start/toolchain-parity.d.ts +0 -267
- package/dist/cli/session-start/toolchain-parity.js +0 -797
- package/dist/cli/session-start/toolchain-parity.js.map +0 -1
- package/dist/cli/smoke/assertions.d.ts +0 -46
- package/dist/cli/smoke/assertions.js +0 -170
- package/dist/cli/smoke/assertions.js.map +0 -1
- package/dist/cli/smoke/index.d.ts +0 -76
- package/dist/cli/smoke/index.js +0 -245
- package/dist/cli/smoke/index.js.map +0 -1
- package/dist/cli/smoke/runner.d.ts +0 -55
- package/dist/cli/smoke/runner.js +0 -134
- package/dist/cli/smoke/runner.js.map +0 -1
- package/dist/cli/smoke/stream-parser.d.ts +0 -65
- package/dist/cli/smoke/stream-parser.js +0 -115
- package/dist/cli/smoke/stream-parser.js.map +0 -1
- package/dist/cli/test-risk.d.ts +0 -27
- package/dist/cli/test-risk.js +0 -38
- package/dist/cli/test-risk.js.map +0 -1
- package/dist/io/display-path.d.ts +0 -11
- package/dist/io/display-path.js +0 -29
- package/dist/io/display-path.js.map +0 -1
- package/dist/io/duration.d.ts +0 -5
- package/dist/io/duration.js +0 -50
- package/dist/io/duration.js.map +0 -1
- package/dist/io/extract.d.ts +0 -79
- package/dist/io/extract.js +0 -226
- package/dist/io/extract.js.map +0 -1
- package/dist/io/ledger-record.d.ts +0 -83
- package/dist/io/ledger-record.js +0 -301
- package/dist/io/ledger-record.js.map +0 -1
- package/dist/io/read-json-dir-entries.d.ts +0 -36
- package/dist/io/read-json-dir-entries.js +0 -48
- package/dist/io/read-json-dir-entries.js.map +0 -1
- package/dist/io/safe-json-parse.d.ts +0 -5
- package/dist/io/safe-json-parse.js +0 -21
- package/dist/io/safe-json-parse.js.map +0 -1
- package/dist/policies/index.d.ts +0 -5
- package/dist/policies/index.js +0 -6
- package/dist/policies/index.js.map +0 -1
- package/dist/policies/ledger-client.d.ts +0 -113
- package/dist/policies/ledger-client.js +0 -500
- package/dist/policies/ledger-client.js.map +0 -1
- package/dist/policies/producers.d.ts +0 -12
- package/dist/policies/producers.js +0 -61
- package/dist/policies/producers.js.map +0 -1
- package/dist/policies/requires.d.ts +0 -75
- package/dist/policies/requires.js +0 -240
- package/dist/policies/requires.js.map +0 -1
- package/dist/policies/timestamp.d.ts +0 -14
- package/dist/policies/timestamp.js +0 -36
- package/dist/policies/timestamp.js.map +0 -1
- package/dist/policy-packs/builtin/permission-profiles.d.ts +0 -11
- package/dist/policy-packs/builtin/permission-profiles.js +0 -74
- package/dist/policy-packs/builtin/permission-profiles.js.map +0 -1
- package/dist/policy-packs/builtin/post-merge-gate-runtime.d.ts +0 -286
- package/dist/policy-packs/builtin/post-merge-gate-runtime.js +0 -397
- package/dist/policy-packs/builtin/post-merge-gate-runtime.js.map +0 -1
- package/dist/policy-packs/builtin/post-merge-gate.d.ts +0 -56
- package/dist/policy-packs/builtin/post-merge-gate.js +0 -347
- package/dist/policy-packs/builtin/post-merge-gate.js.map +0 -1
- package/dist/policy-packs/builtin/solution-acceptance-reconnect.d.ts +0 -50
- package/dist/policy-packs/builtin/solution-acceptance-reconnect.js +0 -151
- package/dist/policy-packs/builtin/solution-acceptance-reconnect.js.map +0 -1
- package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +0 -323
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js +0 -672
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +0 -1
- package/dist/policy-packs/builtin/solution-acceptance.d.ts +0 -45
- package/dist/policy-packs/builtin/solution-acceptance.js +0 -226
- package/dist/policy-packs/builtin/solution-acceptance.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/active-claim.d.ts +0 -25
- package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js +0 -94
- package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/auto-approve-default.d.ts +0 -35
- package/dist/policy-packs/builtin/understanding-before-execution/auto-approve-default.js +0 -86
- package/dist/policy-packs/builtin/understanding-before-execution/auto-approve-default.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.d.ts +0 -210
- package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.js +0 -361
- package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.d.ts +0 -304
- package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js +0 -734
- package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.d.ts +0 -36
- package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.js +0 -73
- package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +0 -19
- package/dist/policy-packs/builtin/understanding-before-execution/index.js +0 -83
- package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.d.ts +0 -135
- package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.js +0 -643
- package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/ledger.d.ts +0 -25
- package/dist/policy-packs/builtin/understanding-before-execution/ledger.js +0 -56
- package/dist/policy-packs/builtin/understanding-before-execution/ledger.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.d.ts +0 -20
- package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js +0 -106
- package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/markers.d.ts +0 -126
- package/dist/policy-packs/builtin/understanding-before-execution/markers.js +0 -245
- package/dist/policy-packs/builtin/understanding-before-execution/markers.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.d.ts +0 -29
- package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.js +0 -82
- package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.d.ts +0 -71
- package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.js +0 -175
- package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.d.ts +0 -625
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js +0 -1076
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.d.ts +0 -128
- package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js +0 -213
- package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.d.ts +0 -129
- package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.js +0 -166
- package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution/task-markers.d.ts +0 -164
- package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js +0 -275
- package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +0 -10
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +0 -1
- package/dist/policy-packs/builtin/understanding-before-execution.d.ts +0 -526
- package/dist/policy-packs/builtin/understanding-before-execution.js +0 -1078
- package/dist/policy-packs/builtin/understanding-before-execution.js.map +0 -1
- package/dist/policy-packs/permission-translator.d.ts +0 -9
- package/dist/policy-packs/permission-translator.js +0 -76
- package/dist/policy-packs/permission-translator.js.map +0 -1
- package/dist/runtime/action-envelope.d.ts +0 -64
- package/dist/runtime/action-envelope.js +0 -46
- package/dist/runtime/action-envelope.js.map +0 -1
- package/dist/runtime/approval-signing.d.ts +0 -111
- package/dist/runtime/approval-signing.js +0 -384
- package/dist/runtime/approval-signing.js.map +0 -1
- package/dist/runtime/bash-match-facts.d.ts +0 -60
- package/dist/runtime/bash-match-facts.js +0 -196
- package/dist/runtime/bash-match-facts.js.map +0 -1
- package/dist/runtime/bash-match-registry.d.ts +0 -112
- package/dist/runtime/bash-match-registry.js +0 -406
- package/dist/runtime/bash-match-registry.js.map +0 -1
- package/dist/runtime/bash-prefix-parse.d.ts +0 -31
- package/dist/runtime/bash-prefix-parse.js +0 -576
- package/dist/runtime/bash-prefix-parse.js.map +0 -1
- package/dist/runtime/command-normalize.d.ts +0 -658
- package/dist/runtime/command-normalize.js +0 -2410
- package/dist/runtime/command-normalize.js.map +0 -1
- package/dist/runtime/deletion-target-resolve.d.ts +0 -28
- package/dist/runtime/deletion-target-resolve.js +0 -1078
- package/dist/runtime/deletion-target-resolve.js.map +0 -1
- package/dist/runtime/destructive-shell-floor.d.ts +0 -19
- package/dist/runtime/destructive-shell-floor.js +0 -611
- package/dist/runtime/destructive-shell-floor.js.map +0 -1
- package/dist/runtime/environment-resolver.d.ts +0 -36
- package/dist/runtime/environment-resolver.js +0 -138
- package/dist/runtime/environment-resolver.js.map +0 -1
- package/dist/runtime/git-context.d.ts +0 -201
- package/dist/runtime/git-context.js +0 -422
- package/dist/runtime/git-context.js.map +0 -1
- package/dist/runtime/index.d.ts +0 -12
- package/dist/runtime/index.js +0 -13
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/intercept.d.ts +0 -623
- package/dist/runtime/intercept.js +0 -1384
- package/dist/runtime/intercept.js.map +0 -1
- package/dist/runtime/kube-context.d.ts +0 -16
- package/dist/runtime/kube-context.js +0 -63
- package/dist/runtime/kube-context.js.map +0 -1
- package/dist/runtime/kubectl-target-parse.d.ts +0 -31
- package/dist/runtime/kubectl-target-parse.js +0 -249
- package/dist/runtime/kubectl-target-parse.js.map +0 -1
- package/dist/runtime/ledger-add.d.ts +0 -16
- package/dist/runtime/ledger-add.js +0 -150
- package/dist/runtime/ledger-add.js.map +0 -1
- package/dist/runtime/ledger-writer.d.ts +0 -27
- package/dist/runtime/ledger-writer.js +0 -46
- package/dist/runtime/ledger-writer.js.map +0 -1
- package/dist/runtime/pause-sentinel.d.ts +0 -60
- package/dist/runtime/pause-sentinel.js +0 -156
- package/dist/runtime/pause-sentinel.js.map +0 -1
- package/dist/runtime/pending-approval.d.ts +0 -23
- package/dist/runtime/pending-approval.js +0 -67
- package/dist/runtime/pending-approval.js.map +0 -1
- package/dist/runtime/read-only-bash.d.ts +0 -141
- package/dist/runtime/read-only-bash.js +0 -2204
- package/dist/runtime/read-only-bash.js.map +0 -1
- package/dist/runtime/recovery-git-commit.d.ts +0 -22
- package/dist/runtime/recovery-git-commit.js +0 -284
- package/dist/runtime/recovery-git-commit.js.map +0 -1
- package/dist/runtime/reject-malformed-session-id.d.ts +0 -8
- package/dist/runtime/reject-malformed-session-id.js +0 -16
- package/dist/runtime/reject-malformed-session-id.js.map +0 -1
- package/dist/runtime/risk-classifier.d.ts +0 -38
- package/dist/runtime/risk-classifier.js +0 -311
- package/dist/runtime/risk-classifier.js.map +0 -1
- package/dist/runtime/session-id.d.ts +0 -113
- package/dist/runtime/session-id.js +0 -193
- package/dist/runtime/session-id.js.map +0 -1
- package/dist/runtime/shell-word.d.ts +0 -62
- package/dist/runtime/shell-word.js +0 -410
- package/dist/runtime/shell-word.js.map +0 -1
- package/dist/runtime/task-providers/agent-tasks.d.ts +0 -105
- package/dist/runtime/task-providers/agent-tasks.js +0 -247
- package/dist/runtime/task-providers/agent-tasks.js.map +0 -1
- package/dist/runtime/when-eval.d.ts +0 -62
- package/dist/runtime/when-eval.js +0 -212
- package/dist/runtime/when-eval.js.map +0 -1
- package/dist/runtime/workflow-policies.d.ts +0 -207
- package/dist/runtime/workflow-policies.js +0 -596
- package/dist/runtime/workflow-policies.js.map +0 -1
- package/dist/schema/audit.d.ts +0 -71
- package/dist/schema/audit.js +0 -32
- package/dist/schema/audit.js.map +0 -1
- package/dist/schema/doctor.d.ts +0 -25
- package/dist/schema/doctor.js +0 -23
- package/dist/schema/doctor.js.map +0 -1
- package/dist/schema/environments.d.ts +0 -215
- package/dist/schema/environments.js +0 -101
- package/dist/schema/environments.js.map +0 -1
- package/dist/schema/extract.d.ts +0 -9
- package/dist/schema/extract.js +0 -86
- package/dist/schema/extract.js.map +0 -1
- package/dist/schema/permission-profiles.d.ts +0 -2292
- package/dist/schema/permission-profiles.js +0 -60
- package/dist/schema/permission-profiles.js.map +0 -1
- package/dist/schema/policies.d.ts +0 -974
- package/dist/schema/policies.js +0 -298
- package/dist/schema/policies.js.map +0 -1
- package/dist/schema/requires.d.ts +0 -131
- package/dist/schema/requires.js +0 -88
- package/dist/schema/requires.js.map +0 -1
- package/dist/schema/risk.d.ts +0 -144
- package/dist/schema/risk.js +0 -194
- package/dist/schema/risk.js.map +0 -1
- package/dist/schema/session-start-preflight.d.ts +0 -10
- package/dist/schema/session-start-preflight.js +0 -106
- package/dist/schema/session-start-preflight.js.map +0 -1
- package/dist/schema/stale-base-check.d.ts +0 -18
- package/dist/schema/stale-base-check.js +0 -37
- package/dist/schema/stale-base-check.js.map +0 -1
- package/dist/schema/toolchain-parity.d.ts +0 -21
- package/dist/schema/toolchain-parity.js +0 -48
- package/dist/schema/toolchain-parity.js.map +0 -1
- package/dist/schema/workflows.d.ts +0 -519
- package/dist/schema/workflows.js +0 -81
- package/dist/schema/workflows.js.map +0 -1
- package/scripts/runtime-reality-docker-probe.mjs +0 -71
|
@@ -1,1328 +0,0 @@
|
|
|
1
|
-
// Phase 6 #4 — `harness pack hook pre-tool-use [--pack <name>]` runtime verb.
|
|
2
|
-
//
|
|
3
|
-
// PreToolUse blocker for pack-driven gates. Wired by the
|
|
4
|
-
// understanding-before-execution pack's hook contribution; receives the
|
|
5
|
-
// Claude Code event JSON on stdin, consults the signed operator approval
|
|
6
|
-
// marker (the only approval authority; the persisted JSON report and the
|
|
7
|
-
// evidence ledger are audit evidence, task 7402301d), emits a
|
|
8
|
-
// `{decision: "block"}` JSON to stdout when no valid marker approves.
|
|
9
|
-
//
|
|
10
|
-
// Why a new CLI verb (vs reusing `harness policy intercept`): the
|
|
11
|
-
// existing intercept layer evaluates `policies[]` against `requires`,
|
|
12
|
-
// which is purely ledger-based, while the gate has a pack-specific
|
|
13
|
-
// signed-marker source the generic requires evaluator does not model
|
|
14
|
-
// (task 7402301d); bolting it in would leak pack-specific semantics
|
|
15
|
-
// into the generic policy layer. This verb lives next to the pack instead.
|
|
16
|
-
//
|
|
17
|
-
// Failure mode: any error in load / parse / ledger / report scan
|
|
18
|
-
// resolves to ALLOW (exit 0, silent). The Understanding Gate is opt-in;
|
|
19
|
-
// turning a bug in this code into a session-wide tool block would be
|
|
20
|
-
// hostile. The npm package's own standalone blocker still runs as a
|
|
21
|
-
// secondary safety net for solo users, and `harness explain --trace`
|
|
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
|
-
//
|
|
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`.
|
|
54
|
-
import { appendFileSync, existsSync, lstatSync, mkdirSync } from "node:fs";
|
|
55
|
-
import * as path from "node:path";
|
|
56
|
-
import { readRegularFileRejectingSymlink } from "../../io/read-regular-file.js";
|
|
57
|
-
import { queryLedgerByTag, } from "../../policies/index.js";
|
|
58
|
-
import { renderProducers } from "../../policies/producers.js";
|
|
59
|
-
import { ADOPTION_LEDGER_DIRNAME, CLAUDE_CODE_HARNESS, approvalExpiryNotice, checkOperatorApprovalMarkers, checkPersistedReport, defaultReportsDir, delegationMarkerPathFor, delegationReportPathFor, describeMarkerTtlExpiry, harnessAllowed, listPersistedReportsBoundedWithSkips, MAX_HOOK_LISTING_ENTRIES, matchLedgerEntries, noApprovalMarkerReason, parseAutoApprove, readActiveClaim, recordPermissionModeObservation, sanitizeForDisplay, selectNewestStrictSessionReport, verifyDelegation, verifyInflightRecord, verifyMatchedMarkerReport, } from "../../policy-packs/builtin/understanding-before-execution-runtime.js";
|
|
60
|
-
import { resolveMode, toPackageMode, } from "../../policy-packs/builtin/understanding-before-execution.js";
|
|
61
|
-
import { sha256Hex, signingKeyExists } from "../../runtime/approval-signing.js";
|
|
62
|
-
import { findLatestParseError, renderMalformedSectionsNotice } from "../approve/understanding.js";
|
|
63
|
-
import { persistStdinReport } from "../approve/stdin-report.js";
|
|
64
|
-
import { attemptAutoApproval, AUTO_APPROVE_LEDGER_SOURCE } from "./auto-approve-path.js";
|
|
65
|
-
import { scanTranscriptForReport } from "./transcript-report-scan.js";
|
|
66
|
-
import { resolveGeneratedDir, writePendingApproval, } from "../../runtime/pending-approval.js";
|
|
67
|
-
import { resolveManifestLedgerWriter } from "../../runtime/ledger-writer.js";
|
|
68
|
-
import { isReadOnlyBashPipeline } from "../../runtime/read-only-bash.js";
|
|
69
|
-
import { isRecoveryGitCommit } from "../../runtime/recovery-git-commit.js";
|
|
70
|
-
import { ProducerSchema, } from "../../schema/index.js";
|
|
71
|
-
import { renderAgentFacing } from "../../runtime/agent-facing.js";
|
|
72
|
-
import { z } from "zod";
|
|
73
|
-
import { isEscapeCommand } from "./approve-escape.js";
|
|
74
|
-
import { checkHookPause, loadManifestOrInjected, parseConfigUx, readStdin, runGateWithStdinRefusal, } from "./hook-bootstrap.js";
|
|
75
|
-
import { stdinTimeoutBlockJson } from "../bounded-stdin.js";
|
|
76
|
-
import { renderReportSchemaHint } from "./understanding-report-schema-hint.js";
|
|
77
|
-
const PACK_NAME = "understanding-before-execution";
|
|
78
|
-
/**
|
|
79
|
-
* The sentence appended to the standard block reason when a valid
|
|
80
|
-
* delegation was present but the child's own report had not reached the
|
|
81
|
-
* transcript within `auto_approve.report_scan.max_wait` (ADR "Report
|
|
82
|
-
* capture under `-p`": past the bound the hook blocks with an instruction
|
|
83
|
-
* that asks for REPEATED retries, because a text asking for a single
|
|
84
|
-
* retry was measured to leave the outcome to run-to-run variation).
|
|
85
|
-
*
|
|
86
|
-
* Deliberately does NOT pin a heading level. The same block envelope
|
|
87
|
-
* already carries `renderReportSchemaHint`'s "any heading level (#, ##,
|
|
88
|
-
* ###)" rule, and naming one level here would give the agent two
|
|
89
|
-
* different heading rules in one deny text.
|
|
90
|
-
*/
|
|
91
|
-
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.";
|
|
92
|
-
function findGroundingMcp(manifest) {
|
|
93
|
-
return manifest.tools.mcp.find((m) => m.name === "grounding-mcp") ?? null;
|
|
94
|
-
}
|
|
95
|
-
// `ADOPTION_LEDGER_DIRNAME` (the once-per-session ADOPTED-ENTRY ledger
|
|
96
|
-
// dirname, `.delegation-adoptions/`, a SIBLING of `.delegations/`, never
|
|
97
|
-
// inside it or `.approvals/`) now lives in delegation-markers.ts, next to
|
|
98
|
-
// `DELEGATION_MARKER_DIRNAME`: `harness gc`'s delegation sweep
|
|
99
|
-
// (task 3ece079d) needs the same constant to find orphaned ledgers, and a
|
|
100
|
-
// reader that never writes one has no business importing this CLI verb
|
|
101
|
-
// module for it. One id per line; ids come from the scan (`entryId`),
|
|
102
|
-
// which guarantees they carry no line break.
|
|
103
|
-
function adoptedEntriesPathFor(generatedDir, childSessionId) {
|
|
104
|
-
return path.join(generatedDir, ADOPTION_LEDGER_DIRNAME, childSessionId);
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* The entry ids this session has already spent. An absent file is the
|
|
108
|
-
* ordinary first-call case and reads as the empty set; every OTHER read
|
|
109
|
-
* failure fails closed with a result the caller declines on, because a
|
|
110
|
-
* ledger we cannot read cannot prove that the next transcript hit has not
|
|
111
|
-
* already been adopted. Goes through the same symlink-rejecting reader
|
|
112
|
-
* every other gate marker uses (`readRegularFileRejectingSymlink`):
|
|
113
|
-
* without it a symlink planted at this path would have the ledger read
|
|
114
|
-
* follow it to an arbitrary target the agent controls, and an empty or
|
|
115
|
-
* missing target there would read as "nothing adopted yet", the same
|
|
116
|
-
* open the delegation and approval marker reads already close.
|
|
117
|
-
*/
|
|
118
|
-
function readAdoptedEntries(generatedDir, childSessionId) {
|
|
119
|
-
const ledgerDir = path.join(generatedDir, ADOPTION_LEDGER_DIRNAME);
|
|
120
|
-
let ledgerDirStat;
|
|
121
|
-
try {
|
|
122
|
-
ledgerDirStat = lstatSync(ledgerDir);
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
return { ok: true, ids: new Set() };
|
|
126
|
-
}
|
|
127
|
-
if (!ledgerDirStat.isDirectory()) {
|
|
128
|
-
return { ok: false, detail: `containment refusal: ${ledgerDir} is not a plain directory` };
|
|
129
|
-
}
|
|
130
|
-
const filePath = adoptedEntriesPathFor(generatedDir, childSessionId);
|
|
131
|
-
const read = readRegularFileRejectingSymlink(filePath);
|
|
132
|
-
if (read.kind === "missing")
|
|
133
|
-
return { ok: true, ids: new Set() };
|
|
134
|
-
if (read.kind === "symlink") {
|
|
135
|
-
return { ok: false, detail: `${filePath} is a symlink, refusing for safety` };
|
|
136
|
-
}
|
|
137
|
-
if (read.kind === "not-regular") {
|
|
138
|
-
return { ok: false, detail: `${filePath} is not a regular file` };
|
|
139
|
-
}
|
|
140
|
-
if (read.kind !== "ok") {
|
|
141
|
-
return { ok: false, detail: `${filePath} exists but could not be read (I/O error)` };
|
|
142
|
-
}
|
|
143
|
-
const ids = new Set();
|
|
144
|
-
for (const line of read.content.split("\n")) {
|
|
145
|
-
const id = line.trim();
|
|
146
|
-
if (id.length > 0)
|
|
147
|
-
ids.add(id);
|
|
148
|
-
}
|
|
149
|
-
return { ok: true, ids };
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Append one adopted entry id, creating `.delegation-adoptions/` on first
|
|
153
|
-
* use with the same default directory mode `atomicWriteFile` gives
|
|
154
|
-
* `.delegations/` itself (the ledger FILE is 0600, like every marker).
|
|
155
|
-
* `appendFileSync` opens with `O_APPEND`, so a single short write lands
|
|
156
|
-
* whole even if two hooks race on the same session; no read-modify-write,
|
|
157
|
-
* therefore nothing to lose. `lstatSync` (NOT `existsSync`/`statSync`)
|
|
158
|
-
* gates the append the same way `readRegularFileRejectingSymlink` gates
|
|
159
|
-
* the read: a symlink planted at this path would otherwise have
|
|
160
|
-
* `appendFileSync` follow it and write the adoption record through to an
|
|
161
|
-
* arbitrary target, same class of defense as the read side above.
|
|
162
|
-
*/
|
|
163
|
-
function recordAdoptedEntry(generatedDir, childSessionId, entryId) {
|
|
164
|
-
try {
|
|
165
|
-
const ledgerDir = path.join(generatedDir, ADOPTION_LEDGER_DIRNAME);
|
|
166
|
-
let ledgerDirStat;
|
|
167
|
-
try {
|
|
168
|
-
ledgerDirStat = lstatSync(ledgerDir);
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
mkdirSync(ledgerDir, { recursive: true });
|
|
172
|
-
ledgerDirStat = lstatSync(ledgerDir);
|
|
173
|
-
}
|
|
174
|
-
if (!ledgerDirStat.isDirectory()) {
|
|
175
|
-
return {
|
|
176
|
-
ok: false,
|
|
177
|
-
detail: `containment refusal: ${ledgerDir} is not a plain directory`,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
const filePath = adoptedEntriesPathFor(generatedDir, childSessionId);
|
|
181
|
-
let priorStat;
|
|
182
|
-
try {
|
|
183
|
-
priorStat = lstatSync(filePath);
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
priorStat = undefined;
|
|
187
|
-
}
|
|
188
|
-
if (priorStat !== undefined && !priorStat.isFile()) {
|
|
189
|
-
return {
|
|
190
|
-
ok: false,
|
|
191
|
-
detail: `${filePath} exists and is not a regular file, refusing to append through it`,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
appendFileSync(filePath, `${entryId}\n`, { mode: 0o600 });
|
|
195
|
-
return { ok: true };
|
|
196
|
-
}
|
|
197
|
-
catch (err) {
|
|
198
|
-
return { ok: false, detail: err.message };
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// The Claude Code "block" envelope. Mirrors the runtime/intercept.ts
|
|
202
|
-
// shape (PR #81): `decision: "block"` keeps legacy 2.0.x CLIs blocking,
|
|
203
|
-
// `hookSpecificOutput.permissionDecision: "deny"` is the Claude Code
|
|
204
|
-
// 2.1+ documented contract for PreToolUse. This hook is always wired to
|
|
205
|
-
// PreToolUse (the pack contributes only a PreToolUse hook), so the
|
|
206
|
-
// envelope is unconditional here — no event-kind branch like
|
|
207
|
-
// runtime/intercept.ts needs.
|
|
208
|
-
// Producers list from the pack's config (agent-tasks/25bced52). Same
|
|
209
|
-
// shape as the policy engine's `producers:` field, surfaced through the
|
|
210
|
-
// understanding-gate's separate deny path. The constraint differs from
|
|
211
|
-
// the policy engine: here we require at-least-one `ask` (the canonical
|
|
212
|
-
// unblock surface) rather than at-least-one `mcp`, because post-v0.14.0
|
|
213
|
-
// the gate signal is a filesystem marker and the mcp ledger_add path no
|
|
214
|
-
// longer satisfies the gate. Only the operator-approval (`ask`) or a
|
|
215
|
-
// shell from an un-hooked terminal can write the marker.
|
|
216
|
-
const ProducersConfigSchema = z
|
|
217
|
-
.array(ProducerSchema)
|
|
218
|
-
.min(1)
|
|
219
|
-
.refine((arr) => arr.some((p) => p.kind === "ask"), "understanding-gate config.producers must include at least one kind:ask entry (the canonical unblock surface)");
|
|
220
|
-
function parseConfigProducers(raw, stderr) {
|
|
221
|
-
if (raw === undefined)
|
|
222
|
-
return undefined;
|
|
223
|
-
const result = ProducersConfigSchema.safeParse(raw);
|
|
224
|
-
if (!result.success) {
|
|
225
|
-
stderr.write(`harness pack hook: config.producers ignored (${result.error.issues
|
|
226
|
-
.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
227
|
-
.join("; ")})\n`);
|
|
228
|
-
return undefined;
|
|
229
|
-
}
|
|
230
|
-
return result.data;
|
|
231
|
-
}
|
|
232
|
-
// Agent-facing `ux:` block on the pack config (agent-tasks/e48e3b45):
|
|
233
|
-
// parsed via the shared `parseConfigUx` (hook-bootstrap.ts). When present,
|
|
234
|
-
// the deny envelope the agent sees becomes the plain-language
|
|
235
|
-
// `{ cannot, required, run }` shape and the legacy "Understanding Gate:
|
|
236
|
-
// no approved..." + schemaHint + producers vocabulary is suppressed.
|
|
237
|
-
function blockJson(toolName, reason, producers, ux, sessionId, escapeHint, malformedSections, retryInstruction, expiryNotice) {
|
|
238
|
-
// When the pack config declares `ux:`, the agent-facing surface
|
|
239
|
-
// becomes the plain-language `{ cannot, required, run }` shape, and
|
|
240
|
-
// the legacy schemaHint + producers block is suppressed (the ux
|
|
241
|
-
// entries are now the canonical surface; mixing both would split
|
|
242
|
-
// the agent's attention). Internal stuff (the `reason` argument's
|
|
243
|
-
// engine vocabulary) still lands in stderr via the BLOCK diagnostic
|
|
244
|
-
// for operator audit.
|
|
245
|
-
let reasonText;
|
|
246
|
-
if (ux) {
|
|
247
|
-
reasonText = renderAgentFacing(ux, {
|
|
248
|
-
SESSION_ID: sessionId,
|
|
249
|
-
TOOL_NAME: toolName,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
// Legacy suffix kept unchanged so existing operators / docs that quote
|
|
254
|
-
// the old surface still find the recognizable string. The producers
|
|
255
|
-
// block (when configured) appends AFTER, so a reader's eye lands on
|
|
256
|
-
// the structured recipe last. The schema-hint paragraph sits between
|
|
257
|
-
// them: the agent reads the call-to-action first, then learns what
|
|
258
|
-
// shape the report needs to take (without this, freeform prose
|
|
259
|
-
// satisfies the marker write but silently fails the parser).
|
|
260
|
-
const suffix = `Run \`harness approve understanding\` once you have produced and confirmed an Understanding Report.`;
|
|
261
|
-
const schemaHint = renderReportSchemaHint();
|
|
262
|
-
const producersBlock = renderProducers(producers, { SESSION_ID: sessionId });
|
|
263
|
-
reasonText = `Understanding Gate: ${reason}. Tool: ${toolName}. ${suffix}\n${schemaHint}${producersBlock}`;
|
|
264
|
-
}
|
|
265
|
-
// Say why the approval lapsed (max_age elapsed, or the PostToolUse event
|
|
266
|
-
// that expired it), after the ux/legacy envelope for the same reason the
|
|
267
|
-
// malformed-sections notice below is appended there: both envelopes omit
|
|
268
|
-
// the engine vocabulary, so without this the agent would not see it.
|
|
269
|
-
if (expiryNotice) {
|
|
270
|
-
reasonText = `${reasonText}\n\n${expiryNotice}`;
|
|
271
|
-
}
|
|
272
|
-
// Name the malformed sections from the session's own latest parse-error
|
|
273
|
-
// log, when it carries any (task 823837fd, follow-up to 7e29e5d7): a
|
|
274
|
-
// (list) heading that WAS present but whose body was prose instead of
|
|
275
|
-
// markdown list items. Without this an agent whose report almost
|
|
276
|
-
// parsed sees the same generic "no approved report" block as one that
|
|
277
|
-
// never attempted a report at all. Appended after the ux/legacy
|
|
278
|
-
// envelope (both are the "what to do" recipe; this is "what went wrong
|
|
279
|
-
// last time") and before the escape hint (the most immediate
|
|
280
|
-
// remediation, kept last). Sentence itself is shared with the Codex
|
|
281
|
-
// hook via `renderMalformedSectionsNotice` (approve/understanding.ts)
|
|
282
|
-
// so the two runtimes cannot drift apart byte-for-byte.
|
|
283
|
-
const malformedNotice = renderMalformedSectionsNotice(malformedSections ?? []);
|
|
284
|
-
if (malformedNotice) {
|
|
285
|
-
reasonText = `${reasonText}\n\n${malformedNotice}`;
|
|
286
|
-
}
|
|
287
|
-
// A targeted remediation hint for an approve-like command that tripped the
|
|
288
|
-
// escape matcher's metachar guard. Appended last so it reads after the
|
|
289
|
-
// structured recipe regardless of which envelope (ux vs legacy) rendered.
|
|
290
|
-
if (escapeHint) {
|
|
291
|
-
reasonText = `${reasonText}\n\n${escapeHint}`;
|
|
292
|
-
}
|
|
293
|
-
// Slice 3's repeated-retry instruction, appended only when a valid
|
|
294
|
-
// delegation was present and the child's report simply had not landed
|
|
295
|
-
// yet. It is the most immediate remediation for that specific case (and
|
|
296
|
-
// the only one the child can act on alone), so it reads last. The
|
|
297
|
-
// standard reason above is kept intact rather than replaced: an
|
|
298
|
-
// operator reading the audit trail still sees the same block cause.
|
|
299
|
-
if (retryInstruction) {
|
|
300
|
-
reasonText = `${reasonText}\n\n${retryInstruction}`;
|
|
301
|
-
}
|
|
302
|
-
return JSON.stringify({
|
|
303
|
-
decision: "block",
|
|
304
|
-
reason: reasonText,
|
|
305
|
-
hookSpecificOutput: {
|
|
306
|
-
hookEventName: "PreToolUse",
|
|
307
|
-
permissionDecision: "deny",
|
|
308
|
-
permissionDecisionReason: reasonText,
|
|
309
|
-
},
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
// Codepoints in the STRUCTURALLY relevant part of the command (the whole
|
|
313
|
-
// command when there is no heredoc, otherwise only the intro line up to
|
|
314
|
-
// the first newline) that JS's generic `\s` class matches but bash does
|
|
315
|
-
// NOT treat as a blank separator here (task 623640a5 review, anti-lockout
|
|
316
|
-
// finding): every one of them glues onto the adjacent token instead of
|
|
317
|
-
// being stripped as insignificant whitespace, which is exactly what let a
|
|
318
|
-
// report heredoc's real delimiter (as bash reads it) diverge from the
|
|
319
|
-
// word isEscapeCommand extracts. Deliberately scoped to the intro line,
|
|
320
|
-
// not the heredoc BODY (review 2026-08-08 round 2): the body is free-form
|
|
321
|
-
// Understanding Report markdown, where an NBSP or em-space is ordinary,
|
|
322
|
-
// legitimate prose whitespace, not a matcher-defeating character, so
|
|
323
|
-
// naming one there would misattribute an independent block's cause to
|
|
324
|
-
// inert body text an agent never needs to touch. Scans the RAW intro line
|
|
325
|
-
// (not a `.trim()`'d copy, since trimming would silently eat a leading or
|
|
326
|
-
// trailing occurrence before it could be named). Returns the codepoints
|
|
327
|
-
// in `U+XXXX` form, ascending, deduplicated; empty when the intro line
|
|
328
|
-
// carries none.
|
|
329
|
-
function findNonBashBlankWhitespace(command) {
|
|
330
|
-
const nl = command.indexOf("\n");
|
|
331
|
-
const introLine = nl === -1 ? command : command.slice(0, nl);
|
|
332
|
-
const codepoints = new Set();
|
|
333
|
-
for (const ch of introLine) {
|
|
334
|
-
if (ch === " " || ch === "\t")
|
|
335
|
-
continue;
|
|
336
|
-
if (/^\s$/.test(ch))
|
|
337
|
-
codepoints.add(ch.codePointAt(0));
|
|
338
|
-
}
|
|
339
|
-
return [...codepoints]
|
|
340
|
-
.sort((a, b) => a - b)
|
|
341
|
-
.map((cp) => `U+${cp.toString(16).toUpperCase().padStart(4, "0")}`);
|
|
342
|
-
}
|
|
343
|
-
// When a blocked Bash command clearly INTENDS to be the operator-approval
|
|
344
|
-
// escape (`harness approve ...`) but trips the deliberately strict
|
|
345
|
-
// isEscapeCommand matcher because it carries shell metacharacters (a pipe,
|
|
346
|
-
// chaining, redirection, or substitution) or a malformed report heredoc,
|
|
347
|
-
// it lands in the generic hard block with no clue that the command itself
|
|
348
|
-
// was almost the way out. The strictness is intentional (chaining could
|
|
349
|
-
// smuggle other work past the gate), so the fix is discoverability, not
|
|
350
|
-
// relaxation: surface a targeted hint naming the two accepted shapes.
|
|
351
|
-
// Returns null when the command is not approve-like or already qualifies
|
|
352
|
-
// as a clean escape.
|
|
353
|
-
function approveEscapeHint(toolName, command) {
|
|
354
|
-
if (toolName !== "Bash")
|
|
355
|
-
return null;
|
|
356
|
-
const trimmed = command.trim();
|
|
357
|
-
// This `\s` is deliberately broader than isEscapeCommand's `[ \t]`-only
|
|
358
|
-
// bash-blank matcher (task 623640a5 review): it only gates whether to
|
|
359
|
-
// SHOW the discoverability hint, not whether to accept the command as
|
|
360
|
-
// the escape, so being permissive here is safe. Do not narrow it to
|
|
361
|
-
// `[ \t]` to "match" the matcher.
|
|
362
|
-
if (!/^harness\s+approve\b/.test(trimmed))
|
|
363
|
-
return null;
|
|
364
|
-
if (isEscapeCommand(trimmed))
|
|
365
|
-
return null;
|
|
366
|
-
let hint = "This looks like a `harness approve` command, but it was blocked because it carries shell " +
|
|
367
|
-
"metacharacters (a pipe, `;`/`&&`/`||` chaining, `<`/`>` redirection, or command substitution) " +
|
|
368
|
-
"in the executable part, or a malformed report heredoc. Two shapes are accepted: " +
|
|
369
|
-
"(1) bare: `harness approve understanding` with nothing else; " +
|
|
370
|
-
"(2) with the Understanding Report attached for capture: " +
|
|
371
|
-
"`harness approve understanding <<'UNDERSTANDING_REPORT'` followed by the report markdown and a " +
|
|
372
|
-
"final line containing exactly `UNDERSTANDING_REPORT`, with nothing after it. " +
|
|
373
|
-
"Re-run in one of those shapes, then approve the prompt.";
|
|
374
|
-
// Anti-lockout (task 623640a5 review): an agent that emitted an
|
|
375
|
-
// invisible or non-ASCII whitespace codepoint (e.g. a non-breaking
|
|
376
|
-
// space) sees a command that reads as clean, so the generic hint above
|
|
377
|
-
// gives it no signal that whitespace is the actual cause — it just
|
|
378
|
-
// resends the same bytes and stays blocked. Name the offending
|
|
379
|
-
// codepoint(s) explicitly whenever the command carries one.
|
|
380
|
-
const invisible = findNonBashBlankWhitespace(command);
|
|
381
|
-
if (invisible.length > 0) {
|
|
382
|
-
const plural = invisible.length > 1;
|
|
383
|
-
hint +=
|
|
384
|
-
` It also contains ${plural ? "invisible or non-ASCII whitespace characters" : "an invisible or non-ASCII whitespace character"} ` +
|
|
385
|
-
`(${invisible.join(", ")}) where an ordinary space or tab was expected; bash does not treat ` +
|
|
386
|
-
`${plural ? "them" : "it"} as a separator here. Replace ${plural ? "them" : "it"} with a real space or tab and re-run.`;
|
|
387
|
-
}
|
|
388
|
-
return hint;
|
|
389
|
-
}
|
|
390
|
-
// The Claude Code PreToolUse "ask" envelope: surface the normal interactive
|
|
391
|
-
// permission prompt. Per the hooks contract `permissionDecision: "ask"` is
|
|
392
|
-
// PreToolUse-only, and the legacy top-level `decision` field is omitted on
|
|
393
|
-
// purpose: a `decision: "block"` would hard-block legacy 2.0.x CLIs and
|
|
394
|
-
// defeat the ask.
|
|
395
|
-
function askJson() {
|
|
396
|
-
const reason = "Understanding Gate: no approved Understanding Report yet. This is a " +
|
|
397
|
-
"`harness approve` command (the operator-approval path). Approve this " +
|
|
398
|
-
"prompt to record your go.";
|
|
399
|
-
return JSON.stringify({
|
|
400
|
-
hookSpecificOutput: {
|
|
401
|
-
hookEventName: "PreToolUse",
|
|
402
|
-
permissionDecision: "ask",
|
|
403
|
-
permissionDecisionReason: reason,
|
|
404
|
-
},
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
async function checkLedger(manifest, sessionId, opts) {
|
|
408
|
-
if (opts.ledgerQuery) {
|
|
409
|
-
const result = await opts.ledgerQuery(sessionId);
|
|
410
|
-
if ("degraded" in result) {
|
|
411
|
-
return { matched: false, detail: `ledger degraded (${result.degraded})` };
|
|
412
|
-
}
|
|
413
|
-
return matchLedgerEntries(result, sessionId);
|
|
414
|
-
}
|
|
415
|
-
const server = findGroundingMcp(manifest);
|
|
416
|
-
if (!server) {
|
|
417
|
-
return { matched: false, detail: "grounding-mcp not declared in manifest" };
|
|
418
|
-
}
|
|
419
|
-
const command = Array.isArray(server.command)
|
|
420
|
-
? server.command
|
|
421
|
-
: server.command.trim().split(/\s+/);
|
|
422
|
-
const env = server.env ?? undefined;
|
|
423
|
-
const timeoutMs = opts.ledgerTimeoutMs ?? server.health?.timeout_ms ?? 5_000;
|
|
424
|
-
const result = await queryLedgerByTag({
|
|
425
|
-
mcpCommand: command,
|
|
426
|
-
...(env && { mcpEnv: env }),
|
|
427
|
-
sessionId,
|
|
428
|
-
timeoutMs,
|
|
429
|
-
});
|
|
430
|
-
if (result.kind === "degraded") {
|
|
431
|
-
return { matched: false, detail: `ledger degraded (${result.reason})` };
|
|
432
|
-
}
|
|
433
|
-
return matchLedgerEntries(result.entries, sessionId);
|
|
434
|
-
}
|
|
435
|
-
async function runPackHookPreToolUseCliInner(opts = {}) {
|
|
436
|
-
const stdin = opts.stdin ?? process.stdin;
|
|
437
|
-
const stdout = opts.stdout ?? process.stdout;
|
|
438
|
-
const stderr = opts.stderr ?? process.stderr;
|
|
439
|
-
const packName = opts.pack ?? PACK_NAME;
|
|
440
|
-
// Read stdin defensively. Bad JSON falls through to allow (matches
|
|
441
|
-
// policy intercept's failure mode) but emits a stderr diagnostic so
|
|
442
|
-
// the degradation is loud — a silently-allowing gate manufactures
|
|
443
|
-
// false confidence, which is the worst direction for a governance
|
|
444
|
-
// hook to fail in.
|
|
445
|
-
const raw = await readStdin(stdin);
|
|
446
|
-
let event = {};
|
|
447
|
-
try {
|
|
448
|
-
event = JSON.parse(raw.trim() || "{}");
|
|
449
|
-
}
|
|
450
|
-
catch (err) {
|
|
451
|
-
stderr.write(`harness pack hook: malformed event JSON on stdin (${err.message}), allowing.\n`);
|
|
452
|
-
}
|
|
453
|
-
const sessionId = (typeof event.session_id === "string" ? event.session_id : undefined) ??
|
|
454
|
-
process.env.CLAUDE_CODE_SESSION_ID ??
|
|
455
|
-
process.env.CLAUDE_SESSION_ID ??
|
|
456
|
-
"";
|
|
457
|
-
const toolName = typeof event.tool_name === "string" ? event.tool_name : "(unknown)";
|
|
458
|
-
const rawCommand = event.tool_input && typeof event.tool_input === "object"
|
|
459
|
-
? event.tool_input.command
|
|
460
|
-
: undefined;
|
|
461
|
-
const commandStr = typeof rawCommand === "string" ? rawCommand : "";
|
|
462
|
-
// Pause sentinel — operator-only kill switch. Honoured BEFORE manifest
|
|
463
|
-
// load so the lockout-recovery flow (where the manifest is exactly
|
|
464
|
-
// what's broken) still respects an active pause.
|
|
465
|
-
if (checkHookPause("pre-tool-use", stderr, opts, opts.generatedDir, opts.now).paused) {
|
|
466
|
-
const diagnostic = "harness paused; pre-tool-use allowing without evaluating.";
|
|
467
|
-
return {
|
|
468
|
-
exitCode: 0,
|
|
469
|
-
blocked: false,
|
|
470
|
-
approvalCheck: { approved: true, source: "none", detail: diagnostic },
|
|
471
|
-
diagnostic,
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
// Load manifest (or use injection). Bail to allow on any failure so a
|
|
475
|
-
// missing harness install never bricks the session. The resolved
|
|
476
|
-
// manifest path feeds the harness.generated/ lookup below; an injected
|
|
477
|
-
// manifest has no path, so the staging write is skipped in that case
|
|
478
|
-
// (tests inject `generatedDir` directly instead).
|
|
479
|
-
let manifest;
|
|
480
|
-
let manifestPath;
|
|
481
|
-
try {
|
|
482
|
-
({ manifest, manifestPath } = loadManifestOrInjected(opts, opts.manifest));
|
|
483
|
-
}
|
|
484
|
-
catch (err) {
|
|
485
|
-
const diagnostic = `harness pack hook: manifest load failed (${err.message}), allowing.`;
|
|
486
|
-
stderr.write(`${diagnostic}\n`);
|
|
487
|
-
return {
|
|
488
|
-
exitCode: 0,
|
|
489
|
-
blocked: false,
|
|
490
|
-
approvalCheck: { approved: true, source: "none", detail: diagnostic },
|
|
491
|
-
diagnostic,
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
// Confirm the pack is enabled. A pack that isn't even declared in the
|
|
495
|
-
// manifest means the operator wired this hook directly into
|
|
496
|
-
// settings.json without `harness apply` — odd but harmless; allow.
|
|
497
|
-
const declared = manifest.policy_packs.find((p) => p.name === packName);
|
|
498
|
-
if (!declared) {
|
|
499
|
-
const diagnostic = `harness pack hook: pack "${packName}" not declared in manifest, allowing.`;
|
|
500
|
-
stderr.write(`${diagnostic}\n`);
|
|
501
|
-
return {
|
|
502
|
-
exitCode: 0,
|
|
503
|
-
blocked: false,
|
|
504
|
-
approvalCheck: { approved: true, source: "none", detail: diagnostic },
|
|
505
|
-
diagnostic,
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
if (!declared.enabled) {
|
|
509
|
-
const diagnostic = `harness pack hook: pack "${packName}" is enabled:false, allowing.`;
|
|
510
|
-
stderr.write(`${diagnostic}\n`);
|
|
511
|
-
return {
|
|
512
|
-
exitCode: 0,
|
|
513
|
-
blocked: false,
|
|
514
|
-
approvalCheck: { approved: true, source: "none", detail: diagnostic },
|
|
515
|
-
diagnostic,
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
if (sessionId === "") {
|
|
519
|
-
const diagnostic = 'harness pack hook: no session_id resolvable from input or $CLAUDE_CODE_SESSION_ID/$CLAUDE_SESSION_ID, allowing.';
|
|
520
|
-
stderr.write(`${diagnostic}\n`);
|
|
521
|
-
return {
|
|
522
|
-
exitCode: 0,
|
|
523
|
-
blocked: false,
|
|
524
|
-
approvalCheck: { approved: true, source: "none", detail: diagnostic },
|
|
525
|
-
diagnostic,
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
// Resolve generatedDir up-front: marker check and pending-approval
|
|
529
|
-
// staging both depend on it.
|
|
530
|
-
const generatedDir = opts.generatedDir ??
|
|
531
|
-
(manifestPath !== undefined
|
|
532
|
-
? resolveGeneratedDir({
|
|
533
|
-
...(opts.homeDir !== undefined ? { homeDir: opts.homeDir } : {}),
|
|
534
|
-
manifestPath,
|
|
535
|
-
})
|
|
536
|
-
: undefined);
|
|
537
|
-
// Source 1: filesystem marker (agent-tasks/88ca4bb3). Canonical for
|
|
538
|
-
// harnessed sessions. The ledger check is no longer authoritative
|
|
539
|
-
// because the agent has direct MCP access to the same ledger and
|
|
540
|
-
// could self-approve; the marker file lives in harness.generated/
|
|
541
|
-
// which Edit / Write / Bash are all gated from writing to. Bail to
|
|
542
|
-
// ledger-as-audit only when generatedDir is unresolvable (injected
|
|
543
|
-
// manifest without a resolved path: only happens in tests).
|
|
544
|
-
//
|
|
545
|
-
// `markerExpired` is hoisted out of the block below so the
|
|
546
|
-
// recovery-git-commit exception (task 6e888423) further down can see
|
|
547
|
-
// it: it is true only when a REAL marker existed for this
|
|
548
|
-
// session/task and aged past `approval_lifecycle.max_age` — as
|
|
549
|
-
// opposed to no marker ever existing, or one cleared by a
|
|
550
|
-
// task-completion boundary tool. See understanding-before-execution-
|
|
551
|
-
// runtime.ts's `OperatorMarkerApproval.expired` doc for the full
|
|
552
|
-
// distinction.
|
|
553
|
-
let markerExpired = false;
|
|
554
|
-
// The max_age sentence for the block reason (wording only, never read by a
|
|
555
|
-
// gate decision); set when the marker check reported a TTL expiry.
|
|
556
|
-
let markerTtlExpiry;
|
|
557
|
-
// True when checkOperatorApprovalMarkers found a marker FILE that failed
|
|
558
|
-
// signature verification (harness/f9485cc7) — missing/invalid signature,
|
|
559
|
-
// wrong alg, or tampered payload — for either the task-scoped or
|
|
560
|
-
// session-scoped check. Surfaced in the block reason below with a
|
|
561
|
-
// distinct phrase from "no approval marker" so an operator/auditor can
|
|
562
|
-
// tell a forgery attempt apart from the routine "never approved" case.
|
|
563
|
-
let markerForged = false;
|
|
564
|
-
// `markers.detail` itself, hoisted alongside `markerForged` so the
|
|
565
|
-
// in-flight consult below can still surface it: a forged SESSION/TASK
|
|
566
|
-
// marker sitting next to a genuinely valid in-flight record (ADR "TTL,
|
|
567
|
-
// cwd, and subagents") is a forgery attempt the operator should see
|
|
568
|
-
// even though the in-flight record is what ends up opening the gate.
|
|
569
|
-
let markerForgedDetail;
|
|
570
|
-
// Set when the session marker was refused only because of its task
|
|
571
|
-
// binding (task 5018c0c4): it was granted for another task, or predates
|
|
572
|
-
// the binding. Blocks exactly like a missing marker; only the reason
|
|
573
|
-
// text below differs, so the operator sees why an approval that exists
|
|
574
|
-
// on disk does not open the gate.
|
|
575
|
-
let sessionBindingRefusedDetail;
|
|
576
|
-
// Set when a marker matched but no persisted report file hashes to the
|
|
577
|
-
// content that marker was signed for any more (task fa423e9b).
|
|
578
|
-
// The marker is then treated as not matching: the call falls through to the
|
|
579
|
-
// ordinary block path (read-only Bash and the operator-approval command
|
|
580
|
-
// keep their exemptions) with this as the reason, and the in-flight
|
|
581
|
-
// subagent record below is not consulted, because it would otherwise
|
|
582
|
-
// re-open the gate for a subagent under the very approval just refused.
|
|
583
|
-
let reportHashMismatchDetail;
|
|
584
|
-
const reportsDir = opts.reportsDir ?? defaultReportsDir();
|
|
585
|
-
if (generatedDir !== undefined) {
|
|
586
|
-
// Source 1a/1b: task-scoped marker for the currently-claimed task
|
|
587
|
-
// (harness/1ee26e77 + PR #198 correctness fix), then the
|
|
588
|
-
// session-scoped marker (legacy / fallback), both under the
|
|
589
|
-
// `approval_lifecycle` TTL. The resolution is shared with the Codex
|
|
590
|
-
// hook (`checkOperatorApprovalMarkers`, task e7c2ec3c) so the two
|
|
591
|
-
// runtimes cannot drift on lifecycle semantics again.
|
|
592
|
-
const markers = checkOperatorApprovalMarkers(generatedDir, sessionId, declared.config, stderr);
|
|
593
|
-
markerExpired = markers.expired;
|
|
594
|
-
markerTtlExpiry = describeMarkerTtlExpiry(markers);
|
|
595
|
-
markerForged = markers.forged;
|
|
596
|
-
if (markers.sessionBindingRefused)
|
|
597
|
-
sessionBindingRefusedDetail = markers.detail;
|
|
598
|
-
if (markerForged) {
|
|
599
|
-
// `markers.detail` is only ever the SESSION marker's detail on the
|
|
600
|
-
// unmatched path (see OperatorMarkerApproval.detail); when it is
|
|
601
|
-
// the TASK marker that failed verification, that fact is buried
|
|
602
|
-
// inside `taskCheckDetail`'s wrapping instead. Detect which one
|
|
603
|
-
// actually carries the forgery phrase so this line names the
|
|
604
|
-
// marker that was actually tampered with, not always the session
|
|
605
|
-
// one (review T-003 R3 L1: a forged task marker next to an intact
|
|
606
|
-
// session marker used to report the session's unrelated "no
|
|
607
|
-
// approval marker" detail here).
|
|
608
|
-
const taskMarkerIsForged = /forged\/unsigned marker rejected/.test(markers.taskCheckDetail);
|
|
609
|
-
const forgedMarkerDetail = taskMarkerIsForged ? markers.taskCheckDetail : markers.detail;
|
|
610
|
-
markerForgedDetail = `harness pack hook: forged/unsigned marker rejected for session ${sessionId}; ${forgedMarkerDetail}`;
|
|
611
|
-
}
|
|
612
|
-
if (markers.source !== "task") {
|
|
613
|
-
// Trace the task-marker miss to stderr so an operator chasing
|
|
614
|
-
// "why isn't my approval working?" sees the active-claim vs marker
|
|
615
|
-
// mismatch, not just the eventual generic session-marker miss.
|
|
616
|
-
stderr.write(`harness pack hook: task-scoped check: ${markers.taskCheckDetail}\n`);
|
|
617
|
-
}
|
|
618
|
-
if (markers.matched) {
|
|
619
|
-
// Cross-check the matched marker's signed report hash against the
|
|
620
|
-
// persisted reports (task fa423e9b): the task marker first, the
|
|
621
|
-
// session marker as fallback, so the verdict never depends on which
|
|
622
|
-
// marker the check order picked.
|
|
623
|
-
const reportHash = verifyMatchedMarkerReport(reportsDir, markers);
|
|
624
|
-
if (reportHash.ok) {
|
|
625
|
-
const diagnostic = `harness pack hook: ${reportHash.detail}, allowing.`;
|
|
626
|
-
stderr.write(`${diagnostic}\n`);
|
|
627
|
-
return {
|
|
628
|
-
exitCode: 0,
|
|
629
|
-
blocked: false,
|
|
630
|
-
approvalCheck: { approved: true, source: "marker", detail: reportHash.detail },
|
|
631
|
-
diagnostic,
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
reportHashMismatchDetail = reportHash.detail;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
// Source 1c: in-flight subagent record (ADR "TTL, cwd, and subagents").
|
|
638
|
-
// An Agent-tool subagent shares its parent's
|
|
639
|
-
// `session_id`, so the marker check above cannot tell a subagent's call
|
|
640
|
-
// apart from the parent's own — the payload's `agent_id` is what does.
|
|
641
|
-
// Consulted ONLY here, ONLY when the marker check just missed AND the
|
|
642
|
-
// payload names a non-empty `agent_id`: a main-line call (no
|
|
643
|
-
// `agent_id`) never reaches `verifyInflightRecord` at all, so no record
|
|
644
|
-
// on disk for any agent can ever open the gate for a main-line call.
|
|
645
|
-
const agentId = typeof event.agent_id === "string" && event.agent_id.length > 0
|
|
646
|
-
? event.agent_id
|
|
647
|
-
: undefined;
|
|
648
|
-
// Sanitized copy for every surface a human or the agent itself reads
|
|
649
|
-
// (the block suffix, the forged phrase, this allow diagnostic): the raw
|
|
650
|
-
// `agentId` above is what verification and path-building use, but an
|
|
651
|
-
// attacker-controlled `agent_id` is otherwise interpolated verbatim
|
|
652
|
-
// into stderr and the agent-facing deny text (review finding: newline
|
|
653
|
-
// injection, unbounded length).
|
|
654
|
-
const displayAgentId = agentId !== undefined ? sanitizeForDisplay(agentId) : undefined;
|
|
655
|
-
// True SPECIFICALLY when a record FILE existed but failed verification
|
|
656
|
-
// (bad signature, or its body disagreed with its own path) — the same
|
|
657
|
-
// distinction `markerForged` draws for the operator marker. Folded into
|
|
658
|
-
// the auto-approval gating below so a forged record is declined the
|
|
659
|
-
// same way a forged marker is, without duplicating that enforcement.
|
|
660
|
-
let inflightForged = false;
|
|
661
|
-
// True SPECIFICALLY when the record verified and agreed with its own
|
|
662
|
-
// path but aged past the staleness window — distinct from `forged` and
|
|
663
|
-
// from a record simply never existing, so the subagent sentence below
|
|
664
|
-
// can name the actual cause instead of always saying "no record".
|
|
665
|
-
let inflightStale = false;
|
|
666
|
-
if (generatedDir !== undefined &&
|
|
667
|
-
agentId !== undefined &&
|
|
668
|
-
reportHashMismatchDetail === undefined) {
|
|
669
|
-
const record = verifyInflightRecord(generatedDir, sessionId, agentId, opts.now !== undefined ? { now: opts.now } : {});
|
|
670
|
-
inflightForged = record.forged;
|
|
671
|
-
inflightStale = record.stale;
|
|
672
|
-
if (record.matched) {
|
|
673
|
-
// A forged SESSION/TASK marker sitting next to this valid record is
|
|
674
|
-
// a distinct forgery attempt: surfaced to stderr BEFORE the allow
|
|
675
|
-
// diagnostic, but never gating it — the in-flight record is its
|
|
676
|
-
// own, independently-verified approval authority (ADR "TTL, cwd,
|
|
677
|
-
// and subagents"), so a forged copy of a DIFFERENT authority must
|
|
678
|
-
// not turn this allow into a block.
|
|
679
|
-
if (markerForgedDetail !== undefined) {
|
|
680
|
-
stderr.write(`${markerForgedDetail}\n`);
|
|
681
|
-
}
|
|
682
|
-
const diagnostic = `harness pack hook: in-flight subagent record for agent ${displayAgentId} (${record.detail}), allowing.`;
|
|
683
|
-
stderr.write(`${diagnostic}\n`);
|
|
684
|
-
return {
|
|
685
|
-
exitCode: 0,
|
|
686
|
-
blocked: false,
|
|
687
|
-
approvalCheck: { approved: true, source: "inflight", detail: record.detail },
|
|
688
|
-
diagnostic,
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
// Persisted-report EVIDENCE probe (task 7402301d). This used to be
|
|
693
|
-
// "Source 2", an equal approval authority consulted right after a forged
|
|
694
|
-
// marker was rejected, and it was unsigned: under the threat model that
|
|
695
|
-
// motivated marker signing (harness/f9485cc7: a write primitive the
|
|
696
|
-
// `Edit|Write|Bash` matcher does not cover) one unsigned JSON write
|
|
697
|
-
// into the reports directory forged an approval, no session id or key
|
|
698
|
-
// read needed. The report now contributes only to the block diagnostic
|
|
699
|
-
// (`report.detail`, which carries the distinct `unsigned
|
|
700
|
-
// persisted-report approval rejected` phrase when the on-disk status
|
|
701
|
-
// says approved) and to the parse-error lookup gate below
|
|
702
|
-
// (`report.report === null`). `PersistedReportEvidence` has no
|
|
703
|
-
// `approved` field on purpose; the signed marker is the only APPROVAL
|
|
704
|
-
// source that opens the gate. The carve-outs below (read-only Bash,
|
|
705
|
-
// recovery git-commit, escape-ask) are separate, independently-argued
|
|
706
|
-
// exemptions, not a second approval source.
|
|
707
|
-
const report = checkPersistedReport(reportsDir, sessionId);
|
|
708
|
-
// Audit-only ledger probe: the ledger row is still recorded by
|
|
709
|
-
// `harness approve understanding`, and we surface its presence in
|
|
710
|
-
// the diagnostic so an operator chasing a flapping gate can see the
|
|
711
|
-
// historic trail. The result intentionally does NOT influence the
|
|
712
|
-
// allow/block decision.
|
|
713
|
-
const ledger = await checkLedger(manifest, sessionId, opts);
|
|
714
|
-
// A subagent call (non-empty `agent_id`) that reached this point had no
|
|
715
|
-
// matching in-flight record — missing, for a different agent, forged,
|
|
716
|
-
// stale, or naming a malformed id (`verifyInflightRecord` never throws
|
|
717
|
-
// on any of those; it just reports `matched: false`). Branched on the
|
|
718
|
-
// actual verification outcome rather than always naming "no record": a
|
|
719
|
-
// forged or stale record is a materially different signal than one
|
|
720
|
-
// that never existed — the parent DID hold a valid approval at some
|
|
721
|
-
// point — and conflating them misdirects the agent's next action.
|
|
722
|
-
// Gated on `generatedDir !== undefined`: when it
|
|
723
|
-
// is not resolvable (test/injection path), `verifyInflightRecord` was
|
|
724
|
-
// never consulted at all (see `inflightForged`/`inflightStale` staying
|
|
725
|
-
// at their `false` initializers above), so there is no verification
|
|
726
|
-
// outcome to report. Named explicitly so an agent reading the deny
|
|
727
|
-
// text knows to stop rather than retry: unlike the operator-approval
|
|
728
|
-
// case, nothing this session can do mints a new record — only the
|
|
729
|
-
// parent's NEXT `subagent-start` call can.
|
|
730
|
-
const subagentRecordSentence = generatedDir !== undefined && displayAgentId !== undefined
|
|
731
|
-
? inflightForged
|
|
732
|
-
? ` subagent ${displayAgentId}: the in-flight record for this subagent failed verification; stop cleanly and report to the orchestrator instead of retrying.`
|
|
733
|
-
: inflightStale
|
|
734
|
-
? ` 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.`
|
|
735
|
-
: ` 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.`
|
|
736
|
-
: "";
|
|
737
|
-
// Neither operator source approved. When a marker FILE existed but
|
|
738
|
-
// failed signature verification, use a distinct reason phrase
|
|
739
|
-
// ("forged/unsigned marker rejected") instead of the routine "no
|
|
740
|
-
// approval marker" — audit/operator surfaces can then tell an active
|
|
741
|
-
// forgery attempt (or a pre-signing legacy marker) apart from a session
|
|
742
|
-
// that simply never approved (harness/f9485cc7). A forged in-flight
|
|
743
|
-
// record gets its own distinct phrase for the same reason, checked
|
|
744
|
-
// second so a forged SESSION/TASK marker (the higher-authority forgery)
|
|
745
|
-
// is never masked by a merely-forged copy of it.
|
|
746
|
-
const unapprovedReason = generatedDir !== undefined
|
|
747
|
-
? markerForged
|
|
748
|
-
? `forged/unsigned marker rejected for session ${sessionId}; ${report.detail}; ${ledger.detail}`
|
|
749
|
-
: inflightForged
|
|
750
|
-
? `forged/unsigned in-flight record for agent ${displayAgentId} rejected for session ${sessionId}; ${report.detail}; ${ledger.detail}`
|
|
751
|
-
: sessionBindingRefusedDetail !== undefined
|
|
752
|
-
? `${sessionBindingRefusedDetail}; ${report.detail}; ${ledger.detail}${subagentRecordSentence}`
|
|
753
|
-
: noApprovalMarkerReason(sessionId, markerTtlExpiry, report.detail, ledger.detail, subagentRecordSentence)
|
|
754
|
-
: `generatedDir not resolvable (test/injection path); ${report.detail}; ${ledger.detail}`;
|
|
755
|
-
// A marker that matched but whose signed report content is gone (the
|
|
756
|
-
// approved report was edited or removed after approval, task fa423e9b)
|
|
757
|
-
// gets its own reason, naming the marker kind and the one-command fix; no
|
|
758
|
-
// "no approval marker" wording applies to it.
|
|
759
|
-
const reason = reportHashMismatchDetail !== undefined
|
|
760
|
-
? `${reportHashMismatchDetail}; ${ledger.detail}`
|
|
761
|
-
: unapprovedReason;
|
|
762
|
-
// Stage the session id so `harness approve`, run from the operator's
|
|
763
|
-
// shell where $CLAUDE_CODE_SESSION_ID / $CLAUDE_SESSION_ID is unset, can resolve it without
|
|
764
|
-
// guessing from transcript filenames. Covers both the ask and the
|
|
765
|
-
// block branches below. Best-effort: a staging-write failure must not
|
|
766
|
-
// escalate a gate block into a hook error.
|
|
767
|
-
if (generatedDir !== undefined) {
|
|
768
|
-
try {
|
|
769
|
-
writePendingApproval(generatedDir, sessionId);
|
|
770
|
-
}
|
|
771
|
-
catch {
|
|
772
|
-
/* best-effort; the ask / block below proceeds regardless */
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
// Exception: read-only Bash commands. The pack hook matcher
|
|
776
|
-
// necessarily covers `Bash` as a whole, but commands like
|
|
777
|
-
// `git status`, `gh pr view`, `ls`, `cat` mutate nothing. Blocking
|
|
778
|
-
// them behind a full Understanding Report cycle trains the agent
|
|
779
|
-
// and operator to experience the gate as noise, which erodes its
|
|
780
|
-
// credibility on the writes that actually matter. Pass the
|
|
781
|
-
// classifier on Bash commands only; Edit and Write stay
|
|
782
|
-
// hard-blocked regardless (the matcher's other arms reach the
|
|
783
|
-
// same final block path below). Unclassifiable Bash falls through
|
|
784
|
-
// to the block (fail-closed).
|
|
785
|
-
if (toolName === "Bash" && isReadOnlyBashPipeline(commandStr)) {
|
|
786
|
-
const diagnostic = `harness pack hook: read-only Bash command, allowing without an approved report (\`${commandStr.trim()}\`)`;
|
|
787
|
-
stderr.write(`${diagnostic}\n`);
|
|
788
|
-
return {
|
|
789
|
-
exitCode: 0,
|
|
790
|
-
blocked: false,
|
|
791
|
-
approvalCheck: { approved: true, source: "none", detail: diagnostic },
|
|
792
|
-
diagnostic,
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
// Exception: the narrow recovery-git-commit shape (task 6e888423,
|
|
796
|
-
// agent-grounding frictions #2/#9/#58/#71). `approval_lifecycle.max_age`
|
|
797
|
-
// can age out the marker mid-task — e.g. during a long reviewer-
|
|
798
|
-
// amendment loop — leaving the recovery `git commit` that consolidates
|
|
799
|
-
// ALREADY-approved Edit/Write output into a new HEAD hard-blocked
|
|
800
|
-
// behind an operator. Gated on BOTH conditions so this cannot become a
|
|
801
|
-
// generic bypass: `markerExpired` proves a real operator approval
|
|
802
|
-
// existed for this exact session/task and merely aged out (a marker
|
|
803
|
-
// that is simply absent, or one a task-completion boundary tool just
|
|
804
|
-
// cleared for a NEW task, leaves this false and the block below still
|
|
805
|
-
// applies), and `isRecoveryGitCommit` proves the command is a bare,
|
|
806
|
-
// unchained `git commit` that cannot smuggle other work or introduce
|
|
807
|
-
// new file content — see src/runtime/recovery-git-commit.ts for the
|
|
808
|
-
// full safety argument. Edit / Write / every other Bash shape remain
|
|
809
|
-
// hard-gated regardless.
|
|
810
|
-
if (toolName === "Bash" && markerExpired && isRecoveryGitCommit(commandStr)) {
|
|
811
|
-
const diagnostic = `harness pack hook: recovery-commit exemption — approval for session ${sessionId} had expired, but this session/task WAS previously approved; allowing the bare \`git commit\` to record already-approved work (\`${commandStr.trim()}\`). A fresh Understanding Report is still required for any new Edit/Write/Bash.`;
|
|
812
|
-
stderr.write(`${diagnostic}\n`);
|
|
813
|
-
return {
|
|
814
|
-
exitCode: 0,
|
|
815
|
-
blocked: false,
|
|
816
|
-
approvalCheck: { approved: true, source: "recovery-commit", detail: diagnostic },
|
|
817
|
-
diagnostic,
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
// Exception: the operator-approval command itself. Hard-denying
|
|
821
|
-
// `harness approve understanding` is a catch-22: it is the very command
|
|
822
|
-
// that records the operator's go, and a Bash `deny` gives no prompt to
|
|
823
|
-
// approve. Defer it to the interactive permission prompt instead, so the
|
|
824
|
-
// operator's go on that prompt IS the approval, and `harness approve
|
|
825
|
-
// understanding` then writes the ledger tag that unblocks the session.
|
|
826
|
-
if (toolName === "Bash" && isEscapeCommand(commandStr)) {
|
|
827
|
-
const diagnostic = `harness pack hook: ASK: operator-approval command, deferring to the interactive permission prompt`;
|
|
828
|
-
stderr.write(`${diagnostic}\n`);
|
|
829
|
-
stdout.write(`${askJson()}\n`);
|
|
830
|
-
return {
|
|
831
|
-
exitCode: 0,
|
|
832
|
-
blocked: false,
|
|
833
|
-
asked: true,
|
|
834
|
-
approvalCheck: { approved: false, source: "none", detail: reason },
|
|
835
|
-
diagnostic,
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
// Step 9: the operator-opt-in auto-approval attempt (agent-tasks/
|
|
839
|
-
// 74b4b17d, ADR docs/decisions/2026-08-27-ug-auto-mode-approval.md,
|
|
840
|
-
// Option A). Deliberately LAST, on a call that the marker check and
|
|
841
|
-
// every exemption above has already declined — i.e. exactly a call
|
|
842
|
-
// that would otherwise reach the final block. It writes no marker for
|
|
843
|
-
// anything the gate was already going to open, and it never returns an
|
|
844
|
-
// allow of its own: the allow below is produced by the auto path's own
|
|
845
|
-
// re-run of `checkOperatorApprovalMarkers`, the same authority step 3
|
|
846
|
-
// consults. Any failure falls through to the block with step 3's
|
|
847
|
-
// `markerExpired` / `markerForged` intact.
|
|
848
|
-
//
|
|
849
|
-
// The ledger writer is resolved lazily: `attemptAutoApproval` calls
|
|
850
|
-
// this thunk only after its own opt-in check and `when` allowlist
|
|
851
|
-
// check both pass, so a call that never opted in (or opted in but
|
|
852
|
-
// runs in an unlisted `permission_mode`, the ordinary case for every
|
|
853
|
-
// other gated call reaching step 9) never resolves a manifest-level
|
|
854
|
-
// ledger writer at all. A CLI-injected `opts.writeLedger` (test
|
|
855
|
-
// harness path) is used as-is when present, still only once the
|
|
856
|
-
// thunk actually runs; otherwise `resolveManifestLedgerWriter` looks
|
|
857
|
-
// up `grounding-mcp` in the manifest on demand. Stays audit-only
|
|
858
|
-
// either way: a missing `grounding-mcp` entry costs one stderr line
|
|
859
|
-
// inside the auto path, never an approval.
|
|
860
|
-
// Step 9's key-one-by-delegation branch (slice 3, agent-tasks/37ad0b05,
|
|
861
|
-
// ADR "Decision: two-key design"). Placed immediately before the
|
|
862
|
-
// `attemptAutoApproval` call and therefore, like it, only on a call
|
|
863
|
-
// every branch above has already declined. What happens here is
|
|
864
|
-
// narrow: verify the delegation, and if it holds, obtain the child's
|
|
865
|
-
// OWN report through the bounded transcript poll and persist it
|
|
866
|
-
// `pending`. It writes no marker and returns no allow of its own; the
|
|
867
|
-
// auto path below still applies every condition it applies to a
|
|
868
|
-
// `when`-matched call, the `pending`-only report rule included, and the
|
|
869
|
-
// marker re-check inside it is still the only thing that allows.
|
|
870
|
-
//
|
|
871
|
-
// Fail-closed at every step, each with its own stderr line: no opt-in,
|
|
872
|
-
// an unlisted harness, no payload `session_id`, no delegation file, an
|
|
873
|
-
// absent signing key, any refusal from `verifyDelegation`, an
|
|
874
|
-
// unparseable or absent transcript report. Each one falls through with
|
|
875
|
-
// `delegation` unset, and the block below is what the call then
|
|
876
|
-
// reaches.
|
|
877
|
-
let delegation;
|
|
878
|
-
// Set only by the bound elapsing with no report in the transcript: the
|
|
879
|
-
// measured case where the child DID what it was asked and the write is
|
|
880
|
-
// simply still in flight. That is the one case where the deny text asks
|
|
881
|
-
// for a retry, so it is the one case that sets this flag.
|
|
882
|
-
let reportScanTimedOut = false;
|
|
883
|
-
// `!inflightForged` added alongside `!markerForged`: a forged in-flight
|
|
884
|
-
// record is the same posture as a forged marker (ADR condition 6, "a
|
|
885
|
-
// forgery detected is never laundered into an approval") — no point
|
|
886
|
-
// spending a delegation resolution on a call already known to end in a
|
|
887
|
-
// decline.
|
|
888
|
-
if (generatedDir !== undefined && !markerForged && !inflightForged) {
|
|
889
|
-
// `null` stderr on purpose: `attemptAutoApproval` parses the same
|
|
890
|
-
// block a few lines below and writes the malformed-config line
|
|
891
|
-
// itself, so passing this hook's stderr here would double it.
|
|
892
|
-
const autoCfg = parseAutoApprove(declared.config["auto_approve"], null);
|
|
893
|
-
const payloadSid = typeof event.session_id === "string" && event.session_id.length > 0
|
|
894
|
-
? event.session_id
|
|
895
|
-
: null;
|
|
896
|
-
let delegationPath = null;
|
|
897
|
-
if (autoCfg !== null && harnessAllowed(autoCfg, CLAUDE_CODE_HARNESS) && payloadSid !== null) {
|
|
898
|
-
try {
|
|
899
|
-
delegationPath = delegationMarkerPathFor(generatedDir, payloadSid);
|
|
900
|
-
}
|
|
901
|
-
catch {
|
|
902
|
-
// A session id that cannot name a file has no delegation, by
|
|
903
|
-
// construction (`verifyDelegation` draws the same conclusion).
|
|
904
|
-
delegationPath = null;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
if (autoCfg !== null && delegationPath !== null && existsSync(delegationPath)) {
|
|
908
|
-
const childSessionId = payloadSid;
|
|
909
|
-
if (!signingKeyExists(generatedDir)) {
|
|
910
|
-
// Never verify through a path that would MINT the key
|
|
911
|
-
// (`verifyMarkerSignature` -> `getOrCreateSigningKey`). The
|
|
912
|
-
// verifier prechecks this too; this line names the condition at
|
|
913
|
-
// the hook, where the operator is reading.
|
|
914
|
-
stderr.write("harness pack hook: delegation present but signing key absent; not verified\n");
|
|
915
|
-
}
|
|
916
|
-
else {
|
|
917
|
-
// The conventional launcher-report path (a plain function of
|
|
918
|
-
// `childSessionId`, never a value read from the payload or from
|
|
919
|
-
// the delegation file itself): offered unconditionally, exactly
|
|
920
|
-
// as `verifyDelegation` expects. It only affects the outcome when
|
|
921
|
-
// the delegation actually binds a `report=` segment; for every
|
|
922
|
-
// other delegation it is read and then never consulted (see the
|
|
923
|
-
// `reportPathHash !== undefined` guard inside `verifyDelegation`).
|
|
924
|
-
const launcherReportPath = delegationReportPathFor(generatedDir, childSessionId);
|
|
925
|
-
const verified = verifyDelegation({
|
|
926
|
-
generatedDir,
|
|
927
|
-
childSessionId,
|
|
928
|
-
// The payload's own cwd, held against the hashed binding the
|
|
929
|
-
// parent signed. Absent means the delegation's cwd binding
|
|
930
|
-
// cannot be satisfied, which the verifier reports as a
|
|
931
|
-
// `cwd_mismatch` rather than waving through.
|
|
932
|
-
cwd: typeof event.cwd === "string" && event.cwd.length > 0 ? event.cwd : null,
|
|
933
|
-
// The same active-claim id the task-scoped marker check resolves
|
|
934
|
-
// above, so a delegation issued for one task cannot authorize a
|
|
935
|
-
// child that has claimed another. Read a SECOND time here on
|
|
936
|
-
// purpose: `checkOperatorApprovalMarkers` resolves the claim
|
|
937
|
-
// inside itself and returns only a detail string, never the id,
|
|
938
|
-
// so there is no resolved value to reuse without widening that
|
|
939
|
-
// shared runtime's return shape for one caller.
|
|
940
|
-
taskId: readActiveClaim(generatedDir),
|
|
941
|
-
launcherReportPath,
|
|
942
|
-
});
|
|
943
|
-
if (!verified.ok) {
|
|
944
|
-
// An absent, moved, or content-modified launcher report surfaces
|
|
945
|
-
// here through the verifier's own distinct reasons
|
|
946
|
-
// (`report_missing` / `report_path_mismatch` /
|
|
947
|
-
// `report_content_mismatch`), same as every other refusal: no
|
|
948
|
-
// special-casing needed now that this hook actually checks the
|
|
949
|
-
// fallback shape against a file. `verified.detail` names the
|
|
950
|
-
// expected path for the three file-state cases this hook can
|
|
951
|
-
// actually reach (absent / wrong path / wrong bytes). Two of
|
|
952
|
-
// `verifyDelegation`'s binding-shape branches are unreachable
|
|
953
|
-
// from here for different reasons, so their details are worth
|
|
954
|
-
// spelling out separately rather than lumping them together:
|
|
955
|
-
// "no report path offered" never fires from this call site,
|
|
956
|
-
// since this hook always offers the conventional path
|
|
957
|
-
// (`launcherReportPath` above is unconditional); "no
|
|
958
|
-
// reportContentHash to check against" IS reachable here, just
|
|
959
|
-
// not from a delegation this hook itself minted, since
|
|
960
|
-
// `writeDelegationMarker` refuses to produce that half-binding
|
|
961
|
-
// shape (a hand-signed marker can still carry it, as the
|
|
962
|
-
// half-binding test in
|
|
963
|
-
// understanding-before-execution-delegation.test.ts shows), and
|
|
964
|
-
// its detail names the delegation marker's own path, not the
|
|
965
|
-
// report path, since there is no report path yet to name.
|
|
966
|
-
stderr.write(`harness pack hook: delegation for ${childSessionId} refused: ${verified.reason}: ${verified.detail}\n`);
|
|
967
|
-
}
|
|
968
|
-
else {
|
|
969
|
-
delegation = { parentSessionId: verified.parentSessionId };
|
|
970
|
-
// Key two: the child's own report. Already on disk and pending
|
|
971
|
-
// (the child was denied once and its report has since been
|
|
972
|
-
// captured) means there is nothing to scan for, so neither
|
|
973
|
-
// channel below is read at all.
|
|
974
|
-
// `childSessionId`, not the outer `sessionId`: this whole block
|
|
975
|
-
// decides the PAYLOAD's session, and the two only coincide
|
|
976
|
-
// because the env fallback never wins on a path that carries a
|
|
977
|
-
// payload session_id. Spelling it out keeps the report this
|
|
978
|
-
// branch looks for bound to the same session the delegation,
|
|
979
|
-
// the scan and the adoption ledger are all keyed by.
|
|
980
|
-
// Bounded by entry count and bytes too (planted entries would run
|
|
981
|
-
// this hook past its budget, which the runtime treats as an allow):
|
|
982
|
-
// past either bound no report is returned and the capture below is
|
|
983
|
-
// skipped, since a listing that cannot see every entry cannot tell
|
|
984
|
-
// whether the child already has a report.
|
|
985
|
-
const delegationListing = listPersistedReportsBoundedWithSkips(reportsDir, {
|
|
986
|
-
maxEntries: MAX_HOOK_LISTING_ENTRIES,
|
|
987
|
-
});
|
|
988
|
-
const existing = selectNewestStrictSessionReport(delegationListing.reports, // bounded: agent-writable dir; a skipped entry is no report
|
|
989
|
-
childSessionId);
|
|
990
|
-
// A report-bound delegation (the `--report` fallback shape,
|
|
991
|
-
// `verified.reportPathHash !== undefined`) never falls through
|
|
992
|
-
// to the transcript scan: the launcher already supplied and
|
|
993
|
-
// proved (via the verify call above) the report the child would
|
|
994
|
-
// otherwise have to emit into its own transcript, so scanning
|
|
995
|
-
// for a second copy would be pointless and would let a
|
|
996
|
-
// report-bound delegation ALSO be satisfied by whatever the
|
|
997
|
-
// child happens to write, which defeats the point of the
|
|
998
|
-
// launcher fixing the content.
|
|
999
|
-
if (delegationListing.truncated) {
|
|
1000
|
-
stderr.write(`harness pack hook: the reports directory ${delegationListing.truncatedDetail}; the report for session ${childSessionId} was not captured\n`);
|
|
1001
|
-
}
|
|
1002
|
-
else if (existing === null || existing.approvalStatus !== "pending") {
|
|
1003
|
-
if (verified.reportPathHash !== undefined) {
|
|
1004
|
-
// The bytes `verifyDelegation` already proved present,
|
|
1005
|
-
// path-matched, and content-matched against the bound
|
|
1006
|
-
// hash, returned verbatim on its ok shape. There is NO
|
|
1007
|
-
// second read of `launcherReportPath` here on purpose: a
|
|
1008
|
-
// second read would be a second trust decision, reopening
|
|
1009
|
-
// the exact race (a rewrite between verification and
|
|
1010
|
-
// persist) that returning the verified bytes closes
|
|
1011
|
-
// structurally instead of by re-checking a hash.
|
|
1012
|
-
const reportContent = verified.reportContent;
|
|
1013
|
-
if (reportContent === undefined) {
|
|
1014
|
-
// Cannot happen given the invariant `reportContent` is
|
|
1015
|
-
// set whenever `reportPathHash` is (see the type's own
|
|
1016
|
-
// doc comment); fail closed rather than persist nothing
|
|
1017
|
-
// we can vouch for.
|
|
1018
|
-
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`);
|
|
1019
|
-
}
|
|
1020
|
-
else {
|
|
1021
|
-
const contentHash = sha256Hex(reportContent);
|
|
1022
|
-
// Adoption keyed by the report's CONTENT hash, in the
|
|
1023
|
-
// SAME once-per-session ledger the transcript scan uses,
|
|
1024
|
-
// namespaced with a `report:` prefix so the two id shapes
|
|
1025
|
-
// (`uuid:...` / `digest:...` for a transcript entry,
|
|
1026
|
-
// `report:...` here) can never collide. Without this a
|
|
1027
|
-
// launcher report could mint a fresh marker every time an
|
|
1028
|
-
// earlier one expired, for as long as the delegation
|
|
1029
|
-
// itself stayed valid: the delegation's TTL would
|
|
1030
|
-
// silently become the report's own re-mint budget instead
|
|
1031
|
-
// of a one-shot authorization.
|
|
1032
|
-
const adoptedPath = adoptedEntriesPathFor(generatedDir, childSessionId);
|
|
1033
|
-
const adopted = readAdoptedEntries(generatedDir, childSessionId);
|
|
1034
|
-
const entryId = `report:${contentHash}`;
|
|
1035
|
-
if (!adopted.ok) {
|
|
1036
|
-
// Fail closed: without the ledger we cannot tell a
|
|
1037
|
-
// fresh report from one this session already spent.
|
|
1038
|
-
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`);
|
|
1039
|
-
}
|
|
1040
|
-
else if (adopted.ids.has(entryId)) {
|
|
1041
|
-
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`);
|
|
1042
|
-
}
|
|
1043
|
-
else {
|
|
1044
|
-
// RECORD THE ADOPTION FIRST, then persist, same ordering
|
|
1045
|
-
// and same reasoning as the transcript-scan path below:
|
|
1046
|
-
// a failed ledger write costs one blocked call and
|
|
1047
|
-
// leaves the report re-adoptable, while the reverse
|
|
1048
|
-
// order would leave a persisted, mintable report behind
|
|
1049
|
-
// an unrecorded adoption.
|
|
1050
|
-
const recorded = recordAdoptedEntry(generatedDir, childSessionId, entryId);
|
|
1051
|
-
if (!recorded.ok) {
|
|
1052
|
-
stderr.write(`harness pack hook: could not record the launcher-supplied report as adopted for session ${childSessionId} (${recorded.detail}); nothing was persisted\n`);
|
|
1053
|
-
}
|
|
1054
|
-
else {
|
|
1055
|
-
const persisted = persistStdinReport({
|
|
1056
|
-
markdown: reportContent,
|
|
1057
|
-
reportsDir,
|
|
1058
|
-
sessionId: childSessionId,
|
|
1059
|
-
now: new Date(),
|
|
1060
|
-
mode: toPackageMode(resolveMode(declared).mode),
|
|
1061
|
-
});
|
|
1062
|
-
if (persisted.ok) {
|
|
1063
|
-
stderr.write(`harness pack hook: adopted the launcher-supplied report for session ${childSessionId} from ${launcherReportPath} and persisted it pending at ${persisted.filePath}\n`);
|
|
1064
|
-
}
|
|
1065
|
-
else {
|
|
1066
|
-
// The entry stays adopted: the launcher's report is
|
|
1067
|
-
// fixed content, so re-reading it would fail to
|
|
1068
|
-
// parse identically forever. Unlike the transcript
|
|
1069
|
-
// path this is not recoverable by a child retry
|
|
1070
|
-
// (the child does not control the launcher's file),
|
|
1071
|
-
// so no retry instruction is set here.
|
|
1072
|
-
stderr.write(`harness pack hook: the launcher-supplied report for session ${childSessionId} did not parse (${persisted.reason}); nothing was persisted\n`);
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
// The transcript scan below never runs for a report-bound
|
|
1078
|
-
// delegation: this `if` branch is its own complete
|
|
1079
|
-
// report-capture attempt.
|
|
1080
|
-
}
|
|
1081
|
-
else {
|
|
1082
|
-
const transcriptPath = typeof event.transcript_path === "string" && event.transcript_path.length > 0
|
|
1083
|
-
? event.transcript_path
|
|
1084
|
-
: null;
|
|
1085
|
-
// The once-per-session adoption ledger, read only on the path
|
|
1086
|
-
// that can actually capture something.
|
|
1087
|
-
const adoptedPath = adoptedEntriesPathFor(generatedDir, childSessionId);
|
|
1088
|
-
const adopted = transcriptPath === null
|
|
1089
|
-
? { ok: true, ids: new Set() }
|
|
1090
|
-
: readAdoptedEntries(generatedDir, childSessionId);
|
|
1091
|
-
if (transcriptPath === null) {
|
|
1092
|
-
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`);
|
|
1093
|
-
}
|
|
1094
|
-
else if (!adopted.ok) {
|
|
1095
|
-
// Fail closed: without the ledger we cannot tell a fresh
|
|
1096
|
-
// report from one this session already spent.
|
|
1097
|
-
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`);
|
|
1098
|
-
}
|
|
1099
|
-
else {
|
|
1100
|
-
const scan = await scanTranscriptForReport({
|
|
1101
|
-
transcriptPath,
|
|
1102
|
-
sessionId: childSessionId,
|
|
1103
|
-
maxWaitMs: autoCfg.reportScan.maxWaitMs,
|
|
1104
|
-
adopted: adopted.ids,
|
|
1105
|
-
...(opts.reportScanClock?.now !== undefined
|
|
1106
|
-
? { now: opts.reportScanClock.now }
|
|
1107
|
-
: {}),
|
|
1108
|
-
...(opts.reportScanClock?.sleep !== undefined
|
|
1109
|
-
? { sleep: opts.reportScanClock.sleep }
|
|
1110
|
-
: {}),
|
|
1111
|
-
...(opts.reportScanClock?.pollMs !== undefined
|
|
1112
|
-
? { pollMs: opts.reportScanClock.pollMs }
|
|
1113
|
-
: {}),
|
|
1114
|
-
});
|
|
1115
|
-
if (scan.found) {
|
|
1116
|
-
// RECORD THE ADOPTION FIRST, then persist. In this order a
|
|
1117
|
-
// failed ledger write costs one blocked call and leaves the
|
|
1118
|
-
// entry re-scannable, while the reverse order would leave a
|
|
1119
|
-
// persisted, mintable report behind an unrecorded adoption:
|
|
1120
|
-
// exactly the replay this ledger exists to stop.
|
|
1121
|
-
const recorded = recordAdoptedEntry(generatedDir, childSessionId, scan.entryId);
|
|
1122
|
-
if (!recorded.ok) {
|
|
1123
|
-
stderr.write(`harness pack hook: could not record transcript entry ${scan.entryId} as adopted for session ${childSessionId} (${recorded.detail}); nothing was persisted\n`);
|
|
1124
|
-
}
|
|
1125
|
-
else {
|
|
1126
|
-
// Reuses the SAME persister `harness approve
|
|
1127
|
-
// understanding` uses for a heredoc-attached report, so
|
|
1128
|
-
// the capture is session-bound, `pending`, and validated
|
|
1129
|
-
// by one parser rather than a second one written here.
|
|
1130
|
-
// A real `new Date()` on purpose: the persisted report
|
|
1131
|
-
// must sort as the newest strict-session report against
|
|
1132
|
-
// whatever is already on disk, which a caller-injected
|
|
1133
|
-
// clock could not guarantee.
|
|
1134
|
-
const persisted = persistStdinReport({
|
|
1135
|
-
markdown: scan.markdown,
|
|
1136
|
-
reportsDir,
|
|
1137
|
-
sessionId: childSessionId,
|
|
1138
|
-
now: new Date(),
|
|
1139
|
-
mode: toPackageMode(resolveMode(declared).mode),
|
|
1140
|
-
});
|
|
1141
|
-
if (persisted.ok) {
|
|
1142
|
-
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`);
|
|
1143
|
-
}
|
|
1144
|
-
else {
|
|
1145
|
-
// The entry stays adopted: re-reading a report that
|
|
1146
|
-
// does not parse would fail identically forever, so
|
|
1147
|
-
// the child needs a NEW one, which is what the block's
|
|
1148
|
-
// retry instruction asks for. Set the same flag the
|
|
1149
|
-
// timeout branch sets, for that reason: the retry
|
|
1150
|
-
// sentence reads correctly for this case too, and
|
|
1151
|
-
// without it the block below would ask for a retry
|
|
1152
|
-
// implicitly while never saying so.
|
|
1153
|
-
reportScanTimedOut = true;
|
|
1154
|
-
stderr.write(`harness pack hook: the transcript report for session ${childSessionId} did not parse (${persisted.reason}); nothing was persisted\n`);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
else if (scan.reason === "timeout") {
|
|
1159
|
-
reportScanTimedOut = true;
|
|
1160
|
-
stderr.write(scan.adoptedOnly === true
|
|
1161
|
-
? `harness pack hook: the newest Understanding Report entry in the transcript for session ${childSessionId} was already adopted; emit a fresh report\n`
|
|
1162
|
-
: `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`);
|
|
1163
|
-
}
|
|
1164
|
-
else {
|
|
1165
|
-
stderr.write(`harness pack hook: the transcript at ${transcriptPath} exists but could not be read; the report for session ${childSessionId} cannot be captured\n`);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
// Closes the `if (verified.reportPathHash !== undefined) { ... }
|
|
1170
|
-
// else { <transcript scan> }` branch opened above.
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
// Task 8f637efd: best-effort observation of this call's
|
|
1177
|
-
// `permission_mode`, independent of whether `auto_approve` is even
|
|
1178
|
-
// configured: this call site is deliberately the SAME one
|
|
1179
|
-
// `attemptAutoApproval` runs at just below (see
|
|
1180
|
-
// permission-mode-observations.ts's module header for why every call
|
|
1181
|
-
// that reaches this point already needed approval, and for the
|
|
1182
|
-
// coverage gap this accepts). Feeds `harness doctor`'s
|
|
1183
|
-
// missing-`auto_approve` finding; never a gate input.
|
|
1184
|
-
if (generatedDir !== undefined) {
|
|
1185
|
-
recordPermissionModeObservation(generatedDir, sessionId, event.permission_mode, stderr);
|
|
1186
|
-
}
|
|
1187
|
-
const resolveAutoLedger = () => {
|
|
1188
|
-
if (opts.writeLedger)
|
|
1189
|
-
return { write: opts.writeLedger };
|
|
1190
|
-
const resolved = resolveManifestLedgerWriter(manifest, {
|
|
1191
|
-
...(opts.ledgerTimeoutMs !== undefined ? { ledgerTimeoutMs: opts.ledgerTimeoutMs } : {}),
|
|
1192
|
-
});
|
|
1193
|
-
return resolved.ok ? { write: resolved.write } : { write: null, reason: resolved.reason };
|
|
1194
|
-
};
|
|
1195
|
-
const auto = await attemptAutoApproval({
|
|
1196
|
-
generatedDir,
|
|
1197
|
-
sessionId,
|
|
1198
|
-
payloadSessionId: event.session_id,
|
|
1199
|
-
permissionMode: event.permission_mode,
|
|
1200
|
-
// Slice 2 (agent-tasks/57058364) moved these two out of the shared
|
|
1201
|
-
// body and onto the call sites: the Codex hook calls the SAME
|
|
1202
|
-
// `attemptAutoApproval` with `CODEX_HARNESS` and its own
|
|
1203
|
-
// session-consistency evidence. Claude Code's evidence is the hook
|
|
1204
|
-
// process's `$CLAUDE_CODE_SESSION_ID`, exactly what the body used
|
|
1205
|
-
// to read directly, so the wording of every diagnostic on this path
|
|
1206
|
-
// is unchanged.
|
|
1207
|
-
harness: CLAUDE_CODE_HARNESS,
|
|
1208
|
-
// This hook's own verb on the audit-only ledger fact, and its own
|
|
1209
|
-
// stderr prefix by omission (`label` defaults to "harness pack
|
|
1210
|
-
// hook"): both keep this path's output byte-identical to slice 1's.
|
|
1211
|
-
ledgerSource: AUTO_APPROVE_LEDGER_SOURCE,
|
|
1212
|
-
sessionConsistency: { kind: "env", variable: "CLAUDE_CODE_SESSION_ID" },
|
|
1213
|
-
packConfig: declared.config,
|
|
1214
|
-
reportsDir,
|
|
1215
|
-
// Folded with `inflightForged`: `attemptAutoApproval`'s own guard on
|
|
1216
|
-
// this flag is the ACTUAL enforcement point for "a forgery is never
|
|
1217
|
-
// laundered into an approval" (the `!inflightForged` added to the
|
|
1218
|
-
// block above only skips a doomed delegation resolution; it does not
|
|
1219
|
-
// by itself stop this call).
|
|
1220
|
-
markerForged: markerForged || inflightForged,
|
|
1221
|
-
// Names which of the two the flag above actually folds together, so
|
|
1222
|
-
// the decline diagnostic says the right artifact. A real marker
|
|
1223
|
-
// forgery keeps the plain "marker" wording even when both happen to
|
|
1224
|
-
// be true, matching the higher-authority precedence `reason` above
|
|
1225
|
-
// already gives it.
|
|
1226
|
-
forgedKind: markerForged ? "marker" : inflightForged ? "inflight" : undefined,
|
|
1227
|
-
// Slice 3: present only when the branch above verified a delegation
|
|
1228
|
-
// for this child session. It supplies key ONE in place of a
|
|
1229
|
-
// `when`-listed `permission_mode`; every other condition, key two
|
|
1230
|
-
// included, is unchanged.
|
|
1231
|
-
...(delegation !== undefined ? { delegation } : {}),
|
|
1232
|
-
stderr,
|
|
1233
|
-
resolveLedger: resolveAutoLedger,
|
|
1234
|
-
});
|
|
1235
|
-
if (auto.approved) {
|
|
1236
|
-
const autoDiagnostic = `harness pack hook: ${auto.detail}, allowing.`;
|
|
1237
|
-
stderr.write(`${autoDiagnostic}\n`);
|
|
1238
|
-
return {
|
|
1239
|
-
exitCode: 0,
|
|
1240
|
-
blocked: false,
|
|
1241
|
-
approvalCheck: { approved: true, source: "marker", detail: auto.detail },
|
|
1242
|
-
diagnostic: autoDiagnostic,
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
const diagnostic = `harness pack hook: BLOCK — ${reason}`;
|
|
1246
|
-
stderr.write(`${diagnostic}\n`);
|
|
1247
|
-
const configProducers = parseConfigProducers(declared.config["producers"], stderr);
|
|
1248
|
-
const configUx = parseConfigUx(declared.config["ux"], stderr, "harness pack hook");
|
|
1249
|
-
const escapeHint = approveEscapeHint(toolName, commandStr);
|
|
1250
|
-
// Best-effort lookup of the session's latest parse-error log (task
|
|
1251
|
-
// 823837fd, follow-up gate task 823837fd review): the standalone Stop
|
|
1252
|
-
// hook / `harness approve understanding` stdin-report path writes one
|
|
1253
|
-
// to `<reports-parent>/parse-errors/` when the agent's report failed to
|
|
1254
|
-
// parse. Reused here (findLatestParseError already swallows I/O
|
|
1255
|
-
// errors, same contract as the approve-time lookup) so a blocked agent
|
|
1256
|
-
// whose report almost parsed can be told WHICH sections were
|
|
1257
|
-
// malformed, not just "no approved report". Gated on `report.report
|
|
1258
|
-
// === null` (no persisted report at all for this session): a report
|
|
1259
|
-
// that WAS persisted but is merely pending approval must not surface a
|
|
1260
|
-
// stale parse-error from an earlier, already-fixed attempt; mirrors
|
|
1261
|
-
// the CLI's own gate (`if (!latest)`, approve/understanding.ts). Moved
|
|
1262
|
-
// here, right before the block render and after every exemption
|
|
1263
|
-
// early-return above, so the lookup only runs on the path that
|
|
1264
|
-
// actually renders it (pure code motion from its previous location
|
|
1265
|
-
// right after `checkPersistedReport`).
|
|
1266
|
-
const latestParseError = report.report === null
|
|
1267
|
-
? findLatestParseError(path.join(path.dirname(reportsDir), "parse-errors"), sessionId, {
|
|
1268
|
-
maxEntries: MAX_HOOK_LISTING_ENTRIES,
|
|
1269
|
-
})
|
|
1270
|
-
: null;
|
|
1271
|
-
// The expiry notice belongs to the routine "no approval marker" reason
|
|
1272
|
-
// only: a forged marker, a forged in-flight record or a refused task
|
|
1273
|
-
// binding keep their own reasons and never carry it.
|
|
1274
|
-
const expiryNotice = generatedDir !== undefined &&
|
|
1275
|
-
!markerForged &&
|
|
1276
|
-
!inflightForged &&
|
|
1277
|
-
sessionBindingRefusedDetail === undefined
|
|
1278
|
-
? approvalExpiryNotice(markerTtlExpiry, report.report)
|
|
1279
|
-
: undefined;
|
|
1280
|
-
// The subagent record sentence, delivered to the AGENT through the
|
|
1281
|
-
// same channel the slice-3 retry instruction uses (appended after both
|
|
1282
|
-
// the `ux:` and legacy envelopes, so it survives either one) rather
|
|
1283
|
-
// than staying stderr/audit-only, reaching only `reason` /
|
|
1284
|
-
// `approvalCheck.detail` and never the agent-facing stdout JSON. A
|
|
1285
|
-
// main-line block (`displayAgentId` undefined) never carries it.
|
|
1286
|
-
//
|
|
1287
|
-
// The retry instruction takes PRECEDENCE when both would apply: nothing
|
|
1288
|
-
// stops a delegated session (ADR "Report capture under `-p`") from also
|
|
1289
|
-
// being the parent of an in-flight subagent, but the two instructions
|
|
1290
|
-
// disagree on what the agent should do next — the subagent sentence
|
|
1291
|
-
// says stop, the retry instruction says retry — and the delegation
|
|
1292
|
-
// path can still succeed on the very next retry regardless of the
|
|
1293
|
-
// subagent record's own state, so telling the agent to stop here would
|
|
1294
|
-
// be wrong.
|
|
1295
|
-
const agentInstruction = reportScanTimedOut
|
|
1296
|
-
? DELEGATION_REPORT_RETRY_INSTRUCTION
|
|
1297
|
-
: displayAgentId !== undefined && subagentRecordSentence.length > 0
|
|
1298
|
-
? subagentRecordSentence.trim()
|
|
1299
|
-
: null;
|
|
1300
|
-
stdout.write(`${blockJson(toolName, "no approved Understanding Report for this session", configProducers, configUx, sessionId, escapeHint, latestParseError?.malformedSections, agentInstruction, expiryNotice)}\n`);
|
|
1301
|
-
return {
|
|
1302
|
-
exitCode: 0,
|
|
1303
|
-
blocked: true,
|
|
1304
|
-
approvalCheck: { approved: false, source: "none", detail: reason },
|
|
1305
|
-
diagnostic,
|
|
1306
|
-
};
|
|
1307
|
-
}
|
|
1308
|
-
/**
|
|
1309
|
-
* `harness pack hook pre-tool-use`. A stdin read that times out (the idle
|
|
1310
|
-
* bound fired before stdin closed) is refused here instead of being handed to
|
|
1311
|
-
* the gate as an empty or truncated event it would allow: the event never
|
|
1312
|
-
* finished arriving, so the gate cannot judge the tool call (task 7dfdcaaf).
|
|
1313
|
-
* The operator pause still wins; every other input runs the gate unchanged.
|
|
1314
|
-
*/
|
|
1315
|
-
export function runPackHookPreToolUseCli(opts = {}) {
|
|
1316
|
-
return runGateWithStdinRefusal(opts, (stderr) => checkHookPause("pre-tool-use", stderr, opts, opts.generatedDir, opts.now).paused, (reason, stdout, stderr) => {
|
|
1317
|
-
const diagnostic = `harness pack hook: BLOCK: ${reason}`;
|
|
1318
|
-
stderr.write(`${diagnostic}\n`);
|
|
1319
|
-
stdout.write(`${stdinTimeoutBlockJson(reason)}\n`);
|
|
1320
|
-
return {
|
|
1321
|
-
exitCode: 0,
|
|
1322
|
-
blocked: true,
|
|
1323
|
-
approvalCheck: { approved: false, source: "none", detail: reason },
|
|
1324
|
-
diagnostic,
|
|
1325
|
-
};
|
|
1326
|
-
}, runPackHookPreToolUseCliInner);
|
|
1327
|
-
}
|
|
1328
|
-
//# sourceMappingURL=hook-pre-tool-use.js.map
|