@lannguyensi/harness 0.63.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 +90 -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 -338
- 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 -705
- 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 -378
- 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 +15 -2607
- 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 -243
- 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/register-gate-group.d.ts +5 -0
- 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 +5 -0
- package/dist/cli/register-operator-lifecycle.js +20 -0
- package/dist/cli/register-operator-lifecycle.js.map +1 -0
- 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-setup-group.d.ts +5 -0
- 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.d.ts +5 -0
- package/dist/cli/remove/register.js +25 -0
- package/dist/cli/remove/register.js.map +1 -0
- 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.d.ts +5 -0
- package/dist/cli/uninstall/register.js +171 -0
- package/dist/cli/uninstall/register.js.map +1 -0
- 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 -443
- package/dist/cli/delegate/index.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/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 -46
- package/dist/cli/dry-run.js +0 -250
- package/dist/cli/dry-run.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 -20
- package/dist/cli/explain-action.js +0 -27
- package/dist/cli/explain-action.js.map +0 -1
- package/dist/cli/explain-policy.d.ts +0 -135
- package/dist/cli/explain-policy.js +0 -266
- 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 -337
- 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 -1020
- 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 -108
- 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/resolve-env.d.ts +0 -32
- package/dist/cli/resolve-env.js +0 -47
- 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 -26
- package/dist/cli/test-risk.js +0 -34
- 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 -618
- 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 -93
- 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 -620
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js +0 -1047
- 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 -1074
- 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 -494
- package/dist/runtime/intercept.js +0 -1313
- 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 -103
- package/dist/runtime/task-providers/agent-tasks.js +0 -245
- 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,1047 +0,0 @@
|
|
|
1
|
-
// Persisted JSON report handling (`.understanding-gate/reports/`), split
|
|
2
|
-
// out of the former monolithic understanding-before-execution-runtime.ts
|
|
3
|
-
// (structural concentration slice 2, agent-tasks 348a4d42). Pure move: see
|
|
4
|
-
// src/policy-packs/builtin/understanding-before-execution/index.ts for
|
|
5
|
-
// the re-exported public surface.
|
|
6
|
-
//
|
|
7
|
-
// `safeJsonParse` used to be defined and exported here (module-private in
|
|
8
|
-
// the monolith, then exported so markers.ts could reuse it for
|
|
9
|
-
// marker-body parsing). It has since moved to src/io/safe-json-parse.ts
|
|
10
|
-
// (task 9bc0d546) so both call sites import a shared helper instead of one
|
|
11
|
-
// importing it from the other -- import/export mechanics only, no
|
|
12
|
-
// behavior change.
|
|
13
|
-
import * as fs from "node:fs";
|
|
14
|
-
import * as path from "node:path";
|
|
15
|
-
import { atomicWriteFile } from "../../../io/atomic-write.js";
|
|
16
|
-
import { safeJsonParse } from "../../../io/safe-json-parse.js";
|
|
17
|
-
import { sha256Hex } from "../../../runtime/approval-signing.js";
|
|
18
|
-
const DEFAULT_REPORTS_DIRNAME = ".understanding-gate";
|
|
19
|
-
const REPORTS_SUBDIR = "reports";
|
|
20
|
-
/**
|
|
21
|
-
* Env var the persisted-report directory can be set from. Honored by
|
|
22
|
-
* harness (`defaultReportsDir` below + emitted by `harness apply` onto
|
|
23
|
-
* the pack-contributed hook commands) AND by `@lannguyensi/understanding-gate`
|
|
24
|
-
* (its `core/persistence.js:resolveReportDir` reads the same name), so
|
|
25
|
-
* the three actors that touch the directory — Stop hook (package),
|
|
26
|
-
* PreToolUse blocker (harness), `harness approve understanding` — can
|
|
27
|
-
* agree on the path regardless of each process's cwd.
|
|
28
|
-
*/
|
|
29
|
-
export const REPORTS_DIR_ENV = "UNDERSTANDING_GATE_REPORT_DIR";
|
|
30
|
-
/**
|
|
31
|
-
* Resolve the persisted-report directory. Precedence:
|
|
32
|
-
* 1. `UNDERSTANDING_GATE_REPORT_DIR` (taken verbatim — apply emits an
|
|
33
|
-
* absolute path, operator-exported values are shell-expanded before
|
|
34
|
-
* we see them).
|
|
35
|
-
* 2. `<cwd>/.understanding-gate/reports` — backward-compat fallback.
|
|
36
|
-
* Callers that have a stable anchor (the manifest directory) pass
|
|
37
|
-
* it as `cwd` so the fallback agrees with whatever path apply
|
|
38
|
-
* baked into the hook commands.
|
|
39
|
-
*/
|
|
40
|
-
export function defaultReportsDir(cwd = process.cwd()) {
|
|
41
|
-
const fromEnv = process.env[REPORTS_DIR_ENV];
|
|
42
|
-
if (typeof fromEnv === "string" && fromEnv.length > 0)
|
|
43
|
-
return fromEnv;
|
|
44
|
-
return path.join(cwd, DEFAULT_REPORTS_DIRNAME, REPORTS_SUBDIR);
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Project root anchor for the reports directory: `<dir-of-manifest>/.understanding-gate/reports`.
|
|
48
|
-
* Used by `harness apply` to bake an absolute, manifest-anchored value into
|
|
49
|
-
* the pack-contributed hook commands' env, and by `harness approve` as the
|
|
50
|
-
* fallback when `UNDERSTANDING_GATE_REPORT_DIR` is unset.
|
|
51
|
-
*/
|
|
52
|
-
export function reportsDirForManifest(manifestPath) {
|
|
53
|
-
return path.join(path.dirname(manifestPath), DEFAULT_REPORTS_DIRNAME, REPORTS_SUBDIR);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Parse the ISO prefix of a producer filename
|
|
57
|
-
* (`2026-05-24T06-16-39-409Z-<slug>-<hash>.json`) into epoch ms. The
|
|
58
|
-
* producer flattens `:` and `.` to `-` for filesystem safety; undo that
|
|
59
|
-
* before `Date.parse`. null when the name does not carry the prefix.
|
|
60
|
-
*/
|
|
61
|
-
function parseFilenameIsoMs(name) {
|
|
62
|
-
const m = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/.exec(name);
|
|
63
|
-
if (!m)
|
|
64
|
-
return null;
|
|
65
|
-
const ms = Date.parse(`${m[1]}T${m[2]}:${m[3]}:${m[4]}.${m[5]}Z`);
|
|
66
|
-
return Number.isNaN(ms) ? null : ms;
|
|
67
|
-
}
|
|
68
|
-
function readPersistedReport(filePath, mtimeMs, boundedRaw) {
|
|
69
|
-
let raw;
|
|
70
|
-
try {
|
|
71
|
-
raw = boundedRaw ?? fs.readFileSync(filePath, "utf8");
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
const parsed = safeJsonParse(raw);
|
|
77
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
78
|
-
return null;
|
|
79
|
-
const obj = parsed;
|
|
80
|
-
const createdAt = typeof obj["createdAt"] === "string" ? obj["createdAt"] : null;
|
|
81
|
-
const createdAtJsonMs = createdAt !== null ? Date.parse(createdAt) : Number.NaN;
|
|
82
|
-
const createdAtMs = !Number.isNaN(createdAtJsonMs)
|
|
83
|
-
? createdAtJsonMs
|
|
84
|
-
: (parseFilenameIsoMs(path.basename(filePath)) ?? mtimeMs);
|
|
85
|
-
return {
|
|
86
|
-
filePath,
|
|
87
|
-
sessionId: typeof obj["sessionId"] === "string" ? obj["sessionId"] : null,
|
|
88
|
-
approvalStatus: typeof obj["approvalStatus"] === "string" ? obj["approvalStatus"] : null,
|
|
89
|
-
approvedAt: typeof obj["approvedAt"] === "string" ? obj["approvedAt"] : null,
|
|
90
|
-
expiredBy: typeof obj["expiredBy"] === "string" ? obj["expiredBy"] : null,
|
|
91
|
-
expiredAt: typeof obj["expiredAt"] === "string" ? obj["expiredAt"] : null,
|
|
92
|
-
createdAt,
|
|
93
|
-
createdAtMs,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* List persisted reports under `dir`, newest-first by creation time
|
|
98
|
-
* (JSON `createdAt`, then the filename ISO prefix, then mtime). Missing
|
|
99
|
-
* directory returns []; an I/O error on a single file skips it. Reads each
|
|
100
|
-
* regular file IN FULL, so it must not list the agent-writable reports
|
|
101
|
-
* directory (use {@link listPersistedReportsBoundedWithSkips}); no `src`
|
|
102
|
-
* caller remains, it stays for its tests and the pack's export surface.
|
|
103
|
-
*
|
|
104
|
-
* Creation time, NOT mtime, is the sort key: `harness approve
|
|
105
|
-
* understanding` rewrites the report it flips, which bumps mtime and
|
|
106
|
-
* made an old just-approved report sort as the freshest
|
|
107
|
-
* (harness-discovery C1). mtime is the last-resort fallback.
|
|
108
|
-
*/
|
|
109
|
-
export function listPersistedReports(dir) {
|
|
110
|
-
let names;
|
|
111
|
-
try {
|
|
112
|
-
names = fs.readdirSync(dir);
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
return [];
|
|
116
|
-
}
|
|
117
|
-
const reports = [];
|
|
118
|
-
for (const name of names) {
|
|
119
|
-
if (!name.endsWith(".json"))
|
|
120
|
-
continue;
|
|
121
|
-
const full = path.join(dir, name);
|
|
122
|
-
let stat;
|
|
123
|
-
try {
|
|
124
|
-
stat = fs.statSync(full);
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (!stat.isFile())
|
|
130
|
-
continue;
|
|
131
|
-
const report = readPersistedReport(full, stat.mtimeMs);
|
|
132
|
-
if (!report)
|
|
133
|
-
continue;
|
|
134
|
-
reports.push(report);
|
|
135
|
-
}
|
|
136
|
-
reports.sort((a, b) => b.createdAtMs - a.createdAtMs);
|
|
137
|
-
return reports;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Maximum age a sessionId-null report may have for the tolerant
|
|
141
|
-
* fallback to adopt it on the `harness approve understanding` path.
|
|
142
|
-
* Sized for the real flow (Stop hook persists at turn end, operator
|
|
143
|
-
* approves within minutes) with slack for a slow read-through. Live
|
|
144
|
-
* repro that motivated it: a 17-day-old pending report got adopted,
|
|
145
|
-
* validated, and stamped for a fresh session because the producer had
|
|
146
|
-
* silently failed to persist the fresh report (harness-discovery C1,
|
|
147
|
-
* friction-log #67).
|
|
148
|
-
*/
|
|
149
|
-
export const TOLERANT_FALLBACK_MAX_AGE_MS = 15 * 60_000;
|
|
150
|
-
/**
|
|
151
|
-
* Tolerance for a sessionId-less candidate whose `createdAt` lies in
|
|
152
|
-
* the FUTURE relative to the approve-time clock. A future creation
|
|
153
|
-
* time is suspect either way: a forged `createdAt` (the producer's
|
|
154
|
-
* Metadata block lets the agent author it) or serious clock skew.
|
|
155
|
-
* Beyond this skew the fallback rejects the candidate just like a
|
|
156
|
-
* stale one rather than trusting a timestamp that cannot be right.
|
|
157
|
-
*/
|
|
158
|
-
export const TOLERANT_FALLBACK_FUTURE_SKEW_MS = 5 * 60_000;
|
|
159
|
-
/**
|
|
160
|
-
* Select the freshest report for a given session_id, or the freshest
|
|
161
|
-
* applicable report when the persisted file lacks a sessionId field
|
|
162
|
-
* (older package versions). `report: null` when nothing matches.
|
|
163
|
-
*
|
|
164
|
-
* The strict (sessionId-equals) match always wins. The tolerant
|
|
165
|
-
* fallback's appetite is controlled by `opts.tolerantFallback` and
|
|
166
|
-
* `opts.maxFallbackAgeMs` — see `FindReportOptions`. The selection
|
|
167
|
-
* result carries enough context (`fallbackAdopted`, `staleRejected`)
|
|
168
|
-
* for the caller to be loud about non-strict adoptions.
|
|
169
|
-
*/
|
|
170
|
-
export function selectReportForSession(reports, sessionId, opts = {}) {
|
|
171
|
-
// Strict match first.
|
|
172
|
-
for (const r of reports) {
|
|
173
|
-
if (r.sessionId === sessionId) {
|
|
174
|
-
return { report: r, fallbackAdopted: false, staleRejected: [] };
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
// Tolerant fallback: a report without sessionId is treated as
|
|
178
|
-
// applicable to whichever session is asking. Only kicks in when no
|
|
179
|
-
// matching sessionId-tagged report exists — which includes the case
|
|
180
|
-
// where the producer Stop hook silently failed to persist the live
|
|
181
|
-
// session's report, so the candidates here may be entirely unrelated
|
|
182
|
-
// leftovers. `maxFallbackAgeMs` is the guard against adopting those.
|
|
183
|
-
const mode = opts.tolerantFallback ?? "any";
|
|
184
|
-
const nowMs = (opts.now ?? new Date()).getTime();
|
|
185
|
-
const staleRejected = [];
|
|
186
|
-
for (const r of reports) {
|
|
187
|
-
if (r.sessionId !== null)
|
|
188
|
-
continue;
|
|
189
|
-
if (mode === "uncompleted" &&
|
|
190
|
-
(r.approvalStatus === "approved" || r.approvalStatus === "expired")) {
|
|
191
|
-
// A completed-cycle report from another session/task; skipping it
|
|
192
|
-
// here is what stops `harness approve understanding` from binding
|
|
193
|
-
// the live session to a stale, unrelated report.
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (opts.maxFallbackAgeMs !== undefined) {
|
|
197
|
-
const ageMs = nowMs - r.createdAtMs;
|
|
198
|
-
if (ageMs > opts.maxFallbackAgeMs || ageMs < -TOLERANT_FALLBACK_FUTURE_SKEW_MS) {
|
|
199
|
-
staleRejected.push(r);
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return { report: r, fallbackAdopted: true, staleRejected };
|
|
204
|
-
}
|
|
205
|
-
return { report: null, fallbackAdopted: false, staleRejected };
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* The NEWEST report whose `sessionId` strictly equals `sessionId`, or
|
|
209
|
-
* `null` when the newest-first list has none. Deliberately NOT a thin
|
|
210
|
-
* wrapper over {@link selectReportForSession}: it stops at the first
|
|
211
|
-
* strict match and never consults the sessionId-null tolerant fallback.
|
|
212
|
-
*
|
|
213
|
-
* Written for the PreToolUse hook's auto-approval path
|
|
214
|
-
* (agent-tasks/74b4b17d, ADR
|
|
215
|
-
* docs/decisions/2026-08-27-ug-auto-mode-approval.md, Option A condition
|
|
216
|
-
* 3), where both of the fallback's properties are unacceptable:
|
|
217
|
-
*
|
|
218
|
-
* - A sessionId-null report is adopted by whichever session asks, so
|
|
219
|
-
* the fallback would let a report bound to no session at all mint a
|
|
220
|
-
* marker for THIS one. The auto path has no operator to confirm that
|
|
221
|
-
* adoption the way `harness approve understanding` does.
|
|
222
|
-
* - Eligibility is evaluated on the newest report ONLY. The caller
|
|
223
|
-
* checks `approvalStatus === "pending"` on whatever this returns; if
|
|
224
|
-
* the newest strict-session report was already consumed, the session
|
|
225
|
-
* is ineligible even when an older `pending` report is still on disk.
|
|
226
|
-
* Scanning further down the list for some other pending report is
|
|
227
|
-
* exactly the re-mint this design forbids.
|
|
228
|
-
*/
|
|
229
|
-
export function selectNewestStrictSessionReport(reports, sessionId) {
|
|
230
|
-
for (const r of reports) {
|
|
231
|
-
if (r.sessionId === sessionId)
|
|
232
|
-
return r;
|
|
233
|
-
}
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Back-compat wrapper around `selectReportForSession` for callers that
|
|
238
|
-
* only need the report (the gate read and expiry paths).
|
|
239
|
-
*/
|
|
240
|
-
export function findLatestReportForSession(reports, sessionId, opts = {}) {
|
|
241
|
-
return selectReportForSession(reports, sessionId, opts).report;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Distinct, greppable audit phrase for the case where the persisted report
|
|
245
|
-
* on disk says `approvalStatus: "approved"` but no validly-signed approval
|
|
246
|
-
* marker backs it (task 7402301d). The counterpart of the marker path's
|
|
247
|
-
* `forged/unsigned marker rejected` phrase: both PreToolUse hooks embed
|
|
248
|
-
* `PersistedReportEvidence.detail` in their block reason, so this phrase
|
|
249
|
-
* is what an operator or auditor greps for to tell "someone dropped an
|
|
250
|
-
* approved-looking report into the reports directory" (or: a report was
|
|
251
|
-
* approved through a path that never wrote the signed marker, e.g. the
|
|
252
|
-
* standalone `understanding-gate approve` CLI) apart from the routine
|
|
253
|
-
* "never approved" case. Module-private on purpose: the tests that pin
|
|
254
|
-
* the phrase spell it out literally, so a change here turns them red
|
|
255
|
-
* instead of silently following the constant.
|
|
256
|
-
*/
|
|
257
|
-
const UNSIGNED_REPORT_APPROVAL_REJECTED = "unsigned persisted-report approval rejected";
|
|
258
|
-
/** Cap applied to each interpolated value in `checkPersistedReport`'s `detail`. */
|
|
259
|
-
const DETAIL_VALUE_MAX_LENGTH = 120;
|
|
260
|
-
/**
|
|
261
|
-
* `detail` is not an inert log line: both PreToolUse hooks embed it
|
|
262
|
-
* verbatim into their block `reason`, and the Codex hook additionally
|
|
263
|
-
* writes it to stderr, the block-reason surface an operator reads
|
|
264
|
-
* directly. The report file's basename and its `approvedAt` field both
|
|
265
|
-
* come from a JSON file whose content the gated agent controls (it is
|
|
266
|
-
* only unsigned evidence, task 7402301d), so neither is trusted
|
|
267
|
-
* verbatim here: control characters (including newlines, which could
|
|
268
|
-
* forge extra `reason:`-looking lines) are replaced with a space and
|
|
269
|
-
* the result is capped so one field cannot blow out the surface.
|
|
270
|
-
*/
|
|
271
|
-
export function sanitizeDetailValue(value) {
|
|
272
|
-
// Deliberately strips C0/DEL control characters (including newline,
|
|
273
|
-
// which could otherwise forge an extra `reason:`-looking stderr line).
|
|
274
|
-
const flattened = value.replace(/[\x00-\x1f\x7f]/g, " ");
|
|
275
|
-
return flattened.length > DETAIL_VALUE_MAX_LENGTH
|
|
276
|
-
? `${flattened.slice(0, DETAIL_VALUE_MAX_LENGTH)}...`
|
|
277
|
-
: flattened;
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Flip the latest matching persisted report's approvalStatus to
|
|
281
|
-
* `expired` so the audit record agrees with the cleared marker
|
|
282
|
-
* (harness/1ee26e77 follow-up: post-tool-use expiry was marker-only; the
|
|
283
|
-
* persisted report at .understanding-gate/reports/ silently kept
|
|
284
|
-
* satisfying the gate even after task_finish deleted the marker. Since
|
|
285
|
-
* task 7402301d the report carries no gate authority at all, so this flip
|
|
286
|
-
* is audit hygiene rather than a second gate closure).
|
|
287
|
-
*
|
|
288
|
-
* Atomic rewrite. Preserves the rest of the report body so the audit
|
|
289
|
-
* trail (the operator's actual Understanding text + previous approval
|
|
290
|
-
* timestamps) stays intact; only the status fields change.
|
|
291
|
-
*
|
|
292
|
-
* Returns `{ ok: true, filePath, previousStatus }` on success,
|
|
293
|
-
* `{ ok: false, reason }` when no matching report exists or rewrite
|
|
294
|
-
* failed. Non-throwing: caller (post-tool-use hook) uses this as a
|
|
295
|
-
* best-effort cleanup so a missing report dir or unrelated I/O issue
|
|
296
|
-
* does not escalate into a session-breaking hook failure.
|
|
297
|
-
*/
|
|
298
|
-
export function expirePersistedReport(reportsDir, sessionId, now = new Date(), trigger) {
|
|
299
|
-
const reports = listPersistedReportsBounded(reportsDir);
|
|
300
|
-
if (reports.length === 0) {
|
|
301
|
-
return { ok: false, reason: `no reports under ${reportsDir}` };
|
|
302
|
-
}
|
|
303
|
-
const latest = findLatestReportForSession(reports, sessionId);
|
|
304
|
-
if (!latest) {
|
|
305
|
-
return {
|
|
306
|
-
ok: false,
|
|
307
|
-
reason: `no report matched session_id=${sessionId} (${reports.length} report(s) for other sessions)`,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
if (latest.approvalStatus !== "approved") {
|
|
311
|
-
return {
|
|
312
|
-
ok: false,
|
|
313
|
-
reason: `latest report ${sanitizeDetailValue(path.basename(latest.filePath))} already has approvalStatus=${sanitizeDetailValue(latest.approvalStatus ?? "<missing>")}, nothing to expire`,
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
// Bounded like the listing: the file may have been swapped since.
|
|
317
|
-
const read = readReportFileBounded(latest.filePath);
|
|
318
|
-
if (!read.ok) {
|
|
319
|
-
return { ok: false, reason: `failed to read ${latest.filePath}: ${read.detail}` };
|
|
320
|
-
}
|
|
321
|
-
const raw = read.raw;
|
|
322
|
-
let parsed;
|
|
323
|
-
try {
|
|
324
|
-
parsed = JSON.parse(raw);
|
|
325
|
-
}
|
|
326
|
-
catch (err) {
|
|
327
|
-
return { ok: false, reason: `failed to parse ${latest.filePath}: ${err.message}` };
|
|
328
|
-
}
|
|
329
|
-
const previousStatus = typeof parsed["approvalStatus"] === "string" ? parsed["approvalStatus"] : null;
|
|
330
|
-
parsed["approvalStatus"] = "expired";
|
|
331
|
-
parsed["expiredAt"] = now.toISOString();
|
|
332
|
-
if (trigger !== undefined && trigger !== "") {
|
|
333
|
-
parsed["expiredBy"] = trigger;
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
// No event to record: drop a stale one from an earlier expiry.
|
|
337
|
-
delete parsed["expiredBy"];
|
|
338
|
-
}
|
|
339
|
-
try {
|
|
340
|
-
atomicWriteFile(latest.filePath, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
341
|
-
}
|
|
342
|
-
catch (err) {
|
|
343
|
-
return {
|
|
344
|
-
ok: false,
|
|
345
|
-
reason: `failed to rewrite ${latest.filePath}: ${err.message}`,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
return { ok: true, filePath: latest.filePath, previousStatus };
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Gate-side EVIDENCE probe of the persisted report (task 7402301d). Both
|
|
352
|
-
* PreToolUse hooks call this after the signed-marker check has NOT
|
|
353
|
-
* matched, purely to (a) put a precise reason into the block diagnostic
|
|
354
|
-
* and (b) tell "no report at all" (`report: null`, which gates the
|
|
355
|
-
* parse-error lookup) apart from "a report exists but is pending".
|
|
356
|
-
*
|
|
357
|
-
* It never returns an approval. The selection still uses the tolerant
|
|
358
|
-
* sessionId-null fallback (`"any"`, no age limit) so the diagnostic keeps
|
|
359
|
-
* naming a legacy session's own sessionId-less report; that leniency is
|
|
360
|
-
* harmless now because nothing here can open the gate. A report whose
|
|
361
|
-
* on-disk status is `approved` yields `claimsApproved: true` with the
|
|
362
|
-
* `UNSIGNED_REPORT_APPROVAL_REJECTED` phrase in `detail`, the distinct
|
|
363
|
-
* audit signal for a report-side forgery attempt (or an approval that
|
|
364
|
-
* bypassed `harness approve understanding`, e.g. the standalone
|
|
365
|
-
* `understanding-gate approve` CLI, which writes no signed marker).
|
|
366
|
-
*/
|
|
367
|
-
export function checkPersistedReport(reportsDir, sessionId) {
|
|
368
|
-
// Bounded by entry count: the directory is agent-writable, and a read of
|
|
369
|
-
// hundreds of thousands of planted entries runs the hook past its budget,
|
|
370
|
-
// which the runtime treats as an allow. Past the bound nothing is opened and
|
|
371
|
-
// the answer is no evidence (never an approval, so the block stands).
|
|
372
|
-
const listing = listPersistedReportsBoundedWithSkips(reportsDir, {
|
|
373
|
-
maxEntries: MAX_HOOK_LISTING_ENTRIES,
|
|
374
|
-
});
|
|
375
|
-
if (listing.truncated) {
|
|
376
|
-
return {
|
|
377
|
-
claimsApproved: false,
|
|
378
|
-
detail: `no report evidence read: ${reportsDir} ${listing.truncatedDetail}`,
|
|
379
|
-
report: null,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
const reports = listing.reports;
|
|
383
|
-
if (reports.length === 0) {
|
|
384
|
-
return {
|
|
385
|
-
claimsApproved: false,
|
|
386
|
-
detail: `no reports found at ${reportsDir}`,
|
|
387
|
-
report: null,
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
const latest = findLatestReportForSession(reports, sessionId);
|
|
391
|
-
if (!latest) {
|
|
392
|
-
return {
|
|
393
|
-
claimsApproved: false,
|
|
394
|
-
detail: `no report matched session_id=${sessionId} (${reports.length} report(s) for other sessions)`,
|
|
395
|
-
report: null,
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
const safeFileName = sanitizeDetailValue(path.basename(latest.filePath));
|
|
399
|
-
if (latest.approvalStatus !== "approved") {
|
|
400
|
-
const boundaryExpiry = describeBoundaryExpiry(latest);
|
|
401
|
-
const expirySuffix = boundaryExpiry !== undefined ? `; ${boundaryExpiry}` : "";
|
|
402
|
-
return {
|
|
403
|
-
claimsApproved: false,
|
|
404
|
-
detail: `latest report ${safeFileName} has approvalStatus=${sanitizeDetailValue(latest.approvalStatus ?? "<missing>")}${expirySuffix}`,
|
|
405
|
-
report: latest,
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
const safeApprovedAt = latest.approvedAt ? sanitizeDetailValue(latest.approvedAt) : null;
|
|
409
|
-
return {
|
|
410
|
-
claimsApproved: true,
|
|
411
|
-
detail: `${UNSIGNED_REPORT_APPROVAL_REJECTED}: report ${safeFileName} has ` +
|
|
412
|
-
`approvalStatus=approved${safeApprovedAt ? ` (approved at ${safeApprovedAt})` : ""} ` +
|
|
413
|
-
`but the persisted report is evidence, not authority; the gate opens only on a ` +
|
|
414
|
-
`validly-signed approval marker written by \`harness approve understanding\``,
|
|
415
|
-
report: latest,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* `approval expired because <event> at <time>` for a report a PostToolUse
|
|
420
|
-
* boundary expired (`expirePersistedReport` stamps `expiredBy` and
|
|
421
|
-
* `expiredAt`), `undefined` for any other report, including an older one or
|
|
422
|
-
* one a package producer wrote that carries no `expiredBy`. Both values come
|
|
423
|
-
* from a JSON file the gated agent can write, so both pass
|
|
424
|
-
* `sanitizeDetailValue`.
|
|
425
|
-
*/
|
|
426
|
-
export function describeBoundaryExpiry(report) {
|
|
427
|
-
if (report.approvalStatus !== "expired")
|
|
428
|
-
return undefined;
|
|
429
|
-
if (report.expiredBy === null || report.expiredBy === "")
|
|
430
|
-
return undefined;
|
|
431
|
-
const when = report.expiredAt !== null && report.expiredAt !== ""
|
|
432
|
-
? ` at ${sanitizeDetailValue(report.expiredAt)}`
|
|
433
|
-
: "";
|
|
434
|
-
return `approval expired because ${sanitizeDetailValue(report.expiredBy)}${when}`;
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Report fields the approval and expiry lifecycles rewrite. They are left
|
|
438
|
-
* out of {@link canonicalReportHash}: `rewriteReportApproved` flips
|
|
439
|
-
* `approvalStatus`/`approvedAt`/`approvedBy`, may stamp `sessionId`, and
|
|
440
|
-
* drops `expiredAt`/`expiredBy`; `expirePersistedReport` later sets
|
|
441
|
-
* `approvalStatus`/`expiredAt`/`expiredBy` again. The hash must survive all
|
|
442
|
-
* of that for an untouched report, so it covers only the content the
|
|
443
|
-
* operator actually reviewed.
|
|
444
|
-
*/
|
|
445
|
-
const LIFECYCLE_REPORT_FIELDS = new Set([
|
|
446
|
-
"approvalStatus",
|
|
447
|
-
"approvedAt",
|
|
448
|
-
"approvedBy",
|
|
449
|
-
"expiredAt",
|
|
450
|
-
"expiredBy",
|
|
451
|
-
"sessionId",
|
|
452
|
-
]);
|
|
453
|
-
/**
|
|
454
|
-
* Deepest array/object nesting {@link canonicalReportHash} serialises (the
|
|
455
|
-
* report object itself is level 1). `sortKeysDeep` and `JSON.stringify`
|
|
456
|
-
* recurse once per level, and any agent-written `*.json` file in the reports
|
|
457
|
-
* directory reaches them through the gate-read scan, so without a bound a
|
|
458
|
-
* file nested a few thousand levels deep overflows the stack and the hook
|
|
459
|
-
* process dies instead of deciding. A real report nests a handful of levels.
|
|
460
|
-
*/
|
|
461
|
-
const MAX_CANONICAL_REPORT_DEPTH = 64;
|
|
462
|
-
/**
|
|
463
|
-
* Largest report file, in bytes, the canonical hash reads from disk (1 MiB).
|
|
464
|
-
* Every `*.json` entry of the reports directory reaches the hash through the
|
|
465
|
-
* gate-read scan, and the agent can write that directory, so without a bound
|
|
466
|
-
* a planted file of a few hundred megabytes made the hook run out of heap or
|
|
467
|
-
* past its time budget instead of deciding (a hook that dies or overruns is a
|
|
468
|
-
* non-blocking error for the runtime). A real report is a few kilobytes, far
|
|
469
|
-
* below the cap. A file over the cap matches nothing at gate read, and both
|
|
470
|
-
* producers refuse to approve such a report.
|
|
471
|
-
*/
|
|
472
|
-
export const MAX_HASHED_REPORT_BYTES = 1024 * 1024;
|
|
473
|
-
/**
|
|
474
|
-
* Budget the gate-read hash scan spends before it gives up (32 MiB), charged
|
|
475
|
-
* per `*.json` entry as the larger of the bytes read and
|
|
476
|
-
* {@link MIN_SCAN_ENTRY_COST_BYTES}. The per-file cap bounds one file, not
|
|
477
|
-
* their number: 160 planted 1 MiB key-heavy files took 15.9 to 17.6 s of scan
|
|
478
|
-
* time, past the 15 s PreToolUse budget, and a hook past its budget is a
|
|
479
|
-
* non-blocking error for the runtime (an allow). Charging bytes alone left the
|
|
480
|
-
* entry count open: entries that cost no bytes (tiny files, directories named
|
|
481
|
-
* `*.json`, files over the cap, unreadable ones) still cost an open each, and
|
|
482
|
-
* 400k tiny files took about 22 s of CPU. A scan that spent the budget without
|
|
483
|
-
* finding every wanted hash stops and the check fails closed with the mismatch
|
|
484
|
-
* reason. With the floor the scan opens at most 8192 entries and reads about
|
|
485
|
-
* 32 MiB (the last file read may add up to the 1 MiB per-file cap), whichever
|
|
486
|
-
* the entries are. Real reports are a few kilobytes, so the budget covers
|
|
487
|
-
* thousands of them. The scan reads the newest file first, which is where the
|
|
488
|
-
* approved report usually is.
|
|
489
|
-
*
|
|
490
|
-
* The other full-directory reads on the PreToolUse path spend the same
|
|
491
|
-
* budget with the same floor, each its own (a fresh {@link ReadBudget} per
|
|
492
|
-
* listing, not one shared across the hook call): the evidence read, the
|
|
493
|
-
* auto-approval precondition listing, the subagent-delegation lookup and the
|
|
494
|
-
* parse-error log lookup each read at most about 32 MiB, and none opens an
|
|
495
|
-
* entry once the directory holds more than {@link MAX_HOOK_LISTING_ENTRIES}.
|
|
496
|
-
*/
|
|
497
|
-
export const MAX_HASH_SCAN_BYTES = 32 * 1024 * 1024;
|
|
498
|
-
/**
|
|
499
|
-
* Least a `*.json` entry is charged against {@link MAX_HASH_SCAN_BYTES}
|
|
500
|
-
* whatever the read returned (4 KiB, about one real report), so entries that
|
|
501
|
-
* read nothing or little still spend the budget at a rate that bounds their
|
|
502
|
-
* count: 32 MiB / 4 KiB = 8192 entries.
|
|
503
|
-
*/
|
|
504
|
-
export const MIN_SCAN_ENTRY_COST_BYTES = 4096;
|
|
505
|
-
/**
|
|
506
|
-
* Most entries (8192, the hash scan's budget divided by its per-entry floor)
|
|
507
|
-
* a PreToolUse-path reader accepts in one agent-writable directory. The hash
|
|
508
|
-
* scan stops at its budget, but four more readers on the same hook path used
|
|
509
|
-
* to open every entry (the evidence read, the auto-approval precondition
|
|
510
|
-
* listing, the subagent-delegation lookup, the parse-error log lookup), so a
|
|
511
|
-
* directory of hundreds of thousands of planted entries took the hook past the
|
|
512
|
-
* 15 s PreToolUse budget, which the runtime treats as an allow (measured at
|
|
513
|
-
* 400k planted entries: 408195 opens and 28.9 s of CPU in the Claude Code
|
|
514
|
-
* hook). The count alone left each entry readable up to the 1 MiB per-file
|
|
515
|
-
* cap, so every one of those readers also charges the entries it reads against
|
|
516
|
-
* {@link MAX_HASH_SCAN_BYTES} (see {@link ReadBudget}).
|
|
517
|
-
*
|
|
518
|
-
* A reader does not take a newest window of such a directory: a partial view
|
|
519
|
-
* would let a hidden newer entry change a selection (an auto-approval would
|
|
520
|
-
* adopt an older pending report while a newer, hidden one is already
|
|
521
|
-
* approved). Past either bound (more entries, or more report data than the
|
|
522
|
-
* byte budget) it returns nothing and reports the directory as too large to
|
|
523
|
-
* read, and each reader treats that as no evidence: the evidence read
|
|
524
|
-
* reports no report, the auto-approval path declines, the delegation lookup
|
|
525
|
-
* captures nothing and the parse-error lookup finds nothing. None of them
|
|
526
|
-
* allows. The directory listing stops as soon as the bound is crossed. See
|
|
527
|
-
* {@link listDirNamesBounded}.
|
|
528
|
-
*/
|
|
529
|
-
export const MAX_HOOK_LISTING_ENTRIES = MAX_HASH_SCAN_BYTES / MIN_SCAN_ENTRY_COST_BYTES;
|
|
530
|
-
/**
|
|
531
|
-
* Most entries (matching or not) {@link listDirNamesBounded} iterates in one
|
|
532
|
-
* directory, as a multiple of `maxEntries`. The reports and parse-errors
|
|
533
|
-
* directories hold nothing but their `*.json` and `*.log` entries, so a
|
|
534
|
-
* directory with many more names than that is planted, not real.
|
|
535
|
-
*/
|
|
536
|
-
const HOOK_LISTING_SCAN_FACTOR = 2;
|
|
537
|
-
const TRUNCATED_REMEDY = "remove non-report or stale *.json entries from it by hand (`harness gc` removes only aged approved or expired reports)";
|
|
538
|
-
/** Clause a past-the-entry-bound listing reports; the caller supplies the subject. */
|
|
539
|
-
const ENTRIES_TRUNCATED_DETAIL = `holds more than ${MAX_HOOK_LISTING_ENTRIES} *.json entries, or more than ${MAX_HOOK_LISTING_ENTRIES * HOOK_LISTING_SCAN_FACTOR} entries of any name, more than the gate reads; ${TRUNCATED_REMEDY}`;
|
|
540
|
-
/** Clause a past-the-byte-budget listing reports; the caller supplies the subject. */
|
|
541
|
-
const BYTES_TRUNCATED_DETAIL = `holds more than ${MAX_HASH_SCAN_BYTES / (1024 * 1024)} MiB of report data, more than the gate reads; ${TRUNCATED_REMEDY}`;
|
|
542
|
-
/**
|
|
543
|
-
* Names under `dir` ending in `suffix`, or none and `truncated: true` when
|
|
544
|
-
* there are more than `maxEntries` of them. The one listing the bounded
|
|
545
|
-
* PreToolUse-path readers share, so the bound lives in one place; a missing or
|
|
546
|
-
* unreadable directory lists nothing and is not `truncated`.
|
|
547
|
-
*
|
|
548
|
-
* The listing reads the directory one entry at a time and stops as soon as
|
|
549
|
-
* `maxEntries + 1` matching names have been seen, or `maxScanned` entries of
|
|
550
|
-
* any name (default: {@link HOOK_LISTING_SCAN_FACTOR} times `maxEntries`),
|
|
551
|
-
* so millions of planted names, matching or not, cost a bounded number of
|
|
552
|
-
* directory reads and no allocation of their names. The directory handle is
|
|
553
|
-
* always closed. An unbounded `maxEntries` reads the whole directory. The
|
|
554
|
-
* names come back in ascending byte order (the `fs.readdirSync` order), not
|
|
555
|
-
* in the filesystem's iteration order.
|
|
556
|
-
*/
|
|
557
|
-
export function listDirNamesBounded(dir, suffix, maxEntries, maxScanned = maxEntries * HOOK_LISTING_SCAN_FACTOR) {
|
|
558
|
-
let handle;
|
|
559
|
-
try {
|
|
560
|
-
handle = fs.opendirSync(dir);
|
|
561
|
-
}
|
|
562
|
-
catch {
|
|
563
|
-
return { names: [], truncated: false };
|
|
564
|
-
}
|
|
565
|
-
const names = [];
|
|
566
|
-
let scanned = 0;
|
|
567
|
-
try {
|
|
568
|
-
for (let entry = handle.readSync(); entry !== null; entry = handle.readSync()) {
|
|
569
|
-
scanned += 1;
|
|
570
|
-
if (scanned > maxScanned)
|
|
571
|
-
return { names: [], truncated: true };
|
|
572
|
-
if (!entry.name.endsWith(suffix))
|
|
573
|
-
continue;
|
|
574
|
-
names.push(entry.name);
|
|
575
|
-
if (names.length > maxEntries)
|
|
576
|
-
return { names: [], truncated: true };
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
catch {
|
|
580
|
-
return { names: [], truncated: false };
|
|
581
|
-
}
|
|
582
|
-
finally {
|
|
583
|
-
try {
|
|
584
|
-
handle.closeSync();
|
|
585
|
-
}
|
|
586
|
-
catch {
|
|
587
|
-
// Already gone; nothing left to release.
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
return { names: sortNamesByBytes(names), truncated: false };
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* `names` in ascending byte order of their UTF-8 encoding: the order
|
|
594
|
-
* `fs.readdirSync` returns (libuv sorts its scandir with `strcmp`), so the
|
|
595
|
-
* readers that took names from it before {@link listDirNamesBounded} read
|
|
596
|
-
* the directory one entry at a time keep the same order, and ties in their
|
|
597
|
-
* own newest-first sort stay deterministic instead of following the
|
|
598
|
-
* filesystem's iteration order.
|
|
599
|
-
*/
|
|
600
|
-
function sortNamesByBytes(names) {
|
|
601
|
-
return names
|
|
602
|
-
.map((name) => ({ name, key: Buffer.from(name, "utf8") }))
|
|
603
|
-
.sort((a, b) => Buffer.compare(a.key, b.key))
|
|
604
|
-
.map((entry) => entry.name);
|
|
605
|
-
}
|
|
606
|
-
/**
|
|
607
|
-
* Iterative walk over the arrays and objects of `value` (the value itself is
|
|
608
|
-
* level 1) that stops at the first one nested deeper than `max` levels.
|
|
609
|
-
* Iterative, so the walk itself cannot overflow the stack. Only arrays and
|
|
610
|
-
* objects are pushed: a scalar child (string, number, boolean, null) cannot
|
|
611
|
-
* nest, so a wide array of scalars costs no stack slot per element, and
|
|
612
|
-
* `visited` counts the root plus the containers below it. Exported for tests.
|
|
613
|
-
*/
|
|
614
|
-
export function walkContainerDepth(value, max) {
|
|
615
|
-
const nodes = [value];
|
|
616
|
-
const depths = [1];
|
|
617
|
-
let visited = 0;
|
|
618
|
-
while (nodes.length > 0) {
|
|
619
|
-
const node = nodes.pop();
|
|
620
|
-
const depth = depths.pop() ?? 1;
|
|
621
|
-
visited += 1;
|
|
622
|
-
if (node === null || typeof node !== "object")
|
|
623
|
-
continue;
|
|
624
|
-
if (depth > max)
|
|
625
|
-
return { tooDeep: true, visited };
|
|
626
|
-
for (const child of Array.isArray(node) ? node : Object.values(node)) {
|
|
627
|
-
if (child !== null && typeof child === "object") {
|
|
628
|
-
nodes.push(child);
|
|
629
|
-
depths.push(depth + 1);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
return { tooDeep: false, visited };
|
|
634
|
-
}
|
|
635
|
-
/** True when `value` nests arrays/objects deeper than `max` levels. */
|
|
636
|
-
function nestsDeeperThan(value, max) {
|
|
637
|
-
return walkContainerDepth(value, max).tooDeep;
|
|
638
|
-
}
|
|
639
|
-
function sortKeysDeep(value) {
|
|
640
|
-
if (Array.isArray(value))
|
|
641
|
-
return value.map(sortKeysDeep);
|
|
642
|
-
if (value !== null && typeof value === "object") {
|
|
643
|
-
const entries = Object.entries(value)
|
|
644
|
-
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
645
|
-
.map(([k, v]) => [k, sortKeysDeep(v)]);
|
|
646
|
-
// `Object.fromEntries` defines own properties, so a report key named
|
|
647
|
-
// `__proto__` stays data instead of re-parenting the copy.
|
|
648
|
-
return Object.fromEntries(entries);
|
|
649
|
-
}
|
|
650
|
-
return value;
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* The hash an approval marker signs as its `reportContentHash`, and the one
|
|
654
|
-
* the gate recomputes at read time (task fa423e9b): sha256 over a key-sorted
|
|
655
|
-
* JSON serialisation of the parsed report with the lifecycle fields in
|
|
656
|
-
* `LIFECYCLE_REPORT_FIELDS` removed. A raw-bytes hash cannot do this job: the
|
|
657
|
-
* producers hash before `rewriteReportApproved` re-serialises the file and
|
|
658
|
-
* `expirePersistedReport` rewrites it again, so the raw bytes of an untouched
|
|
659
|
-
* report never match what the marker signed. One function on purpose, shared
|
|
660
|
-
* by both producers (`harness approve understanding`, the auto-approval path)
|
|
661
|
-
* and the verifier.
|
|
662
|
-
*
|
|
663
|
-
* Total: returns null instead of throwing for a report nested deeper than
|
|
664
|
-
* `MAX_CANONICAL_REPORT_DEPTH`. The verifier counts such a file as one that
|
|
665
|
-
* matches nothing; both producers refuse to approve such a report, so no
|
|
666
|
-
* marker is ever signed without a content binding because of it. Report
|
|
667
|
-
* files reach it only after {@link readReportFileBounded} bounded their type
|
|
668
|
-
* and size (the gate-read scan and `harness approve understanding` through
|
|
669
|
-
* {@link hashReportFile}, the auto-approval path directly).
|
|
670
|
-
*/
|
|
671
|
-
export function canonicalReportHash(report) {
|
|
672
|
-
// `Object.fromEntries` (not `kept[key] = value`): a report key named
|
|
673
|
-
// `__proto__` must stay hashed data, not re-parent the copy and vanish.
|
|
674
|
-
const kept = Object.fromEntries(Object.entries(report).filter(([key]) => !LIFECYCLE_REPORT_FIELDS.has(key)));
|
|
675
|
-
if (nestsDeeperThan(kept, MAX_CANONICAL_REPORT_DEPTH))
|
|
676
|
-
return null;
|
|
677
|
-
return sha256Hex(JSON.stringify(sortKeysDeep(kept)));
|
|
678
|
-
}
|
|
679
|
-
/** What `readReportFileBounded` returns for a symbolic link when `noFollow` is set. */
|
|
680
|
-
const SYMLINK_REFUSED = {
|
|
681
|
-
ok: false,
|
|
682
|
-
reason: "not-regular",
|
|
683
|
-
detail: "a symbolic link, not a regular file",
|
|
684
|
-
};
|
|
685
|
-
/** What `readReportFileBounded` returns once its `budget` is spent; nothing is opened. */
|
|
686
|
-
const OVER_BUDGET = {
|
|
687
|
-
ok: false,
|
|
688
|
-
reason: "over-budget",
|
|
689
|
-
detail: "not read: the reader's byte budget is spent",
|
|
690
|
-
};
|
|
691
|
-
function errorCode(err) {
|
|
692
|
-
const code = err?.code;
|
|
693
|
-
return typeof code === "string" ? code : String(err);
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* Read a report file for hashing, bounded by type and size. This is the one
|
|
697
|
-
* read every canonical report hash goes through: the gate-read scan (via
|
|
698
|
-
* {@link hashReportFile}) and both producers. The path is opened once,
|
|
699
|
-
* read-only and non-blocking (`O_NONBLOCK`: opening a FIFO with no writer
|
|
700
|
-
* returns at once instead of waiting for one); its type and size come from
|
|
701
|
-
* `fstat` on that descriptor, and the content is read through the same
|
|
702
|
-
* descriptor, so nothing can be swapped in between (no separate stat of the
|
|
703
|
-
* path, which a symlink flipped to a FIFO used to race). Refused without
|
|
704
|
-
* reading: anything that is not a regular file, and a file larger than
|
|
705
|
-
* `MAX_HASHED_REPORT_BYTES`. The read stops one byte past the fstat size
|
|
706
|
-
* (never more than the cap plus one byte); a file that grows into that byte
|
|
707
|
-
* while being read is refused too. The descriptor is always closed. Never
|
|
708
|
-
* throws.
|
|
709
|
-
*
|
|
710
|
-
* `opts.noFollow` adds `O_NOFOLLOW` to the open: a path that is a symbolic
|
|
711
|
-
* link (whatever it points at, a dangling link included) is refused as
|
|
712
|
-
* not-regular by the open itself (`ELOOP`), with no earlier `lstat` for a
|
|
713
|
-
* swap to race. Off by default, so every hook read keeps following links
|
|
714
|
-
* exactly as before; `harness approve understanding` turns it on.
|
|
715
|
-
*
|
|
716
|
-
* `opts.budget` charges the read against {@link MAX_HASH_SCAN_BYTES} the way
|
|
717
|
-
* the hash scan does (the larger of the `fstat` size and
|
|
718
|
-
* {@link MIN_SCAN_ENTRY_COST_BYTES} per entry, taken before the read); once
|
|
719
|
-
* the budget is spent the call opens nothing and returns `over-budget`.
|
|
720
|
-
*/
|
|
721
|
-
export function readReportFileBounded(filePath, opts = {}) {
|
|
722
|
-
const budget = opts.budget;
|
|
723
|
-
if (budget !== undefined && budget.spent >= MAX_HASH_SCAN_BYTES)
|
|
724
|
-
return OVER_BUDGET;
|
|
725
|
-
let fd;
|
|
726
|
-
try {
|
|
727
|
-
fd = fs.openSync(filePath, fs.constants.O_RDONLY |
|
|
728
|
-
fs.constants.O_NONBLOCK |
|
|
729
|
-
(opts.noFollow === true ? fs.constants.O_NOFOLLOW : 0));
|
|
730
|
-
}
|
|
731
|
-
catch (err) {
|
|
732
|
-
if (budget !== undefined)
|
|
733
|
-
budget.spent += MIN_SCAN_ENTRY_COST_BYTES;
|
|
734
|
-
if (opts.noFollow === true && errorCode(err) === "ELOOP")
|
|
735
|
-
return SYMLINK_REFUSED;
|
|
736
|
-
return { ok: false, reason: "unreadable", detail: `could not be opened (${errorCode(err)})` };
|
|
737
|
-
}
|
|
738
|
-
try {
|
|
739
|
-
const stat = fs.fstatSync(fd);
|
|
740
|
-
// Charged from the descriptor's size BEFORE the read, so the bytes read
|
|
741
|
-
// never run past the budget by more than the one file in flight: the
|
|
742
|
-
// larger of the size and the per-entry floor for a file about to be read,
|
|
743
|
-
// the floor for one that is refused unread.
|
|
744
|
-
if (budget !== undefined) {
|
|
745
|
-
budget.spent +=
|
|
746
|
-
stat.isFile() && stat.size <= MAX_HASHED_REPORT_BYTES
|
|
747
|
-
? Math.max(stat.size, MIN_SCAN_ENTRY_COST_BYTES)
|
|
748
|
-
: MIN_SCAN_ENTRY_COST_BYTES;
|
|
749
|
-
}
|
|
750
|
-
if (!stat.isFile())
|
|
751
|
-
return { ok: false, reason: "not-regular", detail: "not a regular file" };
|
|
752
|
-
if (stat.size > MAX_HASHED_REPORT_BYTES) {
|
|
753
|
-
return {
|
|
754
|
-
ok: false,
|
|
755
|
-
reason: "too-large",
|
|
756
|
-
detail: `${stat.size} bytes, over the ${MAX_HASHED_REPORT_BYTES}-byte cap for hashing its content`,
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
const buf = Buffer.allocUnsafe(stat.size + 1);
|
|
760
|
-
let total = 0;
|
|
761
|
-
while (total < buf.length) {
|
|
762
|
-
const n = fs.readSync(fd, buf, total, buf.length - total, null);
|
|
763
|
-
if (n === 0)
|
|
764
|
-
break;
|
|
765
|
-
total += n;
|
|
766
|
-
}
|
|
767
|
-
if (total > stat.size)
|
|
768
|
-
return { ok: false, reason: "grew", detail: "grew while being read" };
|
|
769
|
-
return { ok: true, raw: buf.toString("utf8", 0, total), mtimeMs: stat.mtimeMs };
|
|
770
|
-
}
|
|
771
|
-
catch (err) {
|
|
772
|
-
return { ok: false, reason: "unreadable", detail: `could not be read (${errorCode(err)})` };
|
|
773
|
-
}
|
|
774
|
-
finally {
|
|
775
|
-
try {
|
|
776
|
-
fs.closeSync(fd);
|
|
777
|
-
}
|
|
778
|
-
catch {
|
|
779
|
-
// Already gone; nothing left to release.
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Canonical hash of the report file at `filePath`, read through
|
|
785
|
-
* {@link readReportFileBounded}, or why it has none: not a regular file, over
|
|
786
|
-
* `MAX_HASHED_REPORT_BYTES`, unreadable, grown while read, not a JSON object,
|
|
787
|
-
* or nested too deeply. `harness approve understanding` refuses a report on
|
|
788
|
-
* any of these (the `detail` is its message); the gate-read scan counts such
|
|
789
|
-
* a file as one that matches nothing. Never throws.
|
|
790
|
-
*/
|
|
791
|
-
export function hashReportFile(filePath, opts = {}) {
|
|
792
|
-
const read = readReportFileBounded(filePath, opts);
|
|
793
|
-
if (!read.ok)
|
|
794
|
-
return read;
|
|
795
|
-
return hashReportText(read.raw);
|
|
796
|
-
}
|
|
797
|
-
/** {@link hashReportFile} for text already read through {@link readReportFileBounded}. */
|
|
798
|
-
function hashReportText(raw) {
|
|
799
|
-
const parsed = safeJsonParse(raw);
|
|
800
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
801
|
-
return { ok: false, reason: "not-json-object", detail: "not a JSON object" };
|
|
802
|
-
}
|
|
803
|
-
const hash = canonicalReportHash(parsed);
|
|
804
|
-
if (hash === null) {
|
|
805
|
-
return { ok: false, reason: "too-deep", detail: "nested too deeply to hash its content" };
|
|
806
|
-
}
|
|
807
|
-
return { ok: true, hash };
|
|
808
|
-
}
|
|
809
|
-
/** Canonical hash of the report file at `filePath` ({@link hashReportFile}); null when it has none. */
|
|
810
|
-
export function canonicalReportHashOfFile(filePath, opts = {}) {
|
|
811
|
-
const hashed = hashReportFile(filePath, opts);
|
|
812
|
-
return hashed.ok ? hashed.hash : null;
|
|
813
|
-
}
|
|
814
|
-
/**
|
|
815
|
-
* Hash every `*.json` entry of `dir` (any session, any `approvalStatus`)
|
|
816
|
-
* through {@link readReportFileBounded} and report which of the `wanted`
|
|
817
|
-
* hashes occur. Stops as soon as every wanted hash was found. Every entry
|
|
818
|
-
* counts as a report file; one that is not a regular file, is larger than
|
|
819
|
-
* `MAX_HASHED_REPORT_BYTES`, cannot be opened or read, grows while read, is
|
|
820
|
-
* not a JSON object, or nests too deeply matches nothing. Entries are taken
|
|
821
|
-
* in descending name order, newest first (report names start with a
|
|
822
|
-
* timestamp), so the approved report is usually the first file read. Every
|
|
823
|
-
* entry, whatever its read returned, is charged the larger of the bytes read
|
|
824
|
-
* and `MIN_SCAN_ENTRY_COST_BYTES`; once the charges reach `MAX_HASH_SCAN_BYTES`
|
|
825
|
-
* with a wanted hash still unfound the scan stops and sets `budgetExhausted`,
|
|
826
|
-
* so the number of entries opened and the bytes read (about 32 MiB, the last
|
|
827
|
-
* file read may add up to the per-file cap) are both bounded by the budget,
|
|
828
|
-
* however many entries the directory holds. The `readdir` and the name sort
|
|
829
|
-
* of the directory itself are outside the budget. The evidence read a refused
|
|
830
|
-
* marker falls through to has its own entry bound
|
|
831
|
-
* ({@link MAX_HOOK_LISTING_ENTRIES}).
|
|
832
|
-
*/
|
|
833
|
-
function scanReportHashes(dir, wanted) {
|
|
834
|
-
const scan = { files: 0, matched: new Set(), budgetExhausted: false };
|
|
835
|
-
let names;
|
|
836
|
-
try {
|
|
837
|
-
names = fs.readdirSync(dir);
|
|
838
|
-
}
|
|
839
|
-
catch {
|
|
840
|
-
return scan;
|
|
841
|
-
}
|
|
842
|
-
names = names.filter((name) => name.endsWith(".json")).sort().reverse();
|
|
843
|
-
let hashedBytes = 0; // budget charged so far, see the charge below
|
|
844
|
-
for (const name of names) {
|
|
845
|
-
scan.files += 1;
|
|
846
|
-
if (hashedBytes >= MAX_HASH_SCAN_BYTES) {
|
|
847
|
-
scan.budgetExhausted = true;
|
|
848
|
-
break;
|
|
849
|
-
}
|
|
850
|
-
const read = readReportFileBounded(path.join(dir, name));
|
|
851
|
-
hashedBytes += Math.max(read.ok ? Buffer.byteLength(read.raw) : 0, MIN_SCAN_ENTRY_COST_BYTES);
|
|
852
|
-
if (!read.ok)
|
|
853
|
-
continue;
|
|
854
|
-
const hashed = hashReportText(read.raw);
|
|
855
|
-
if (hashed.ok && wanted.has(hashed.hash)) {
|
|
856
|
-
scan.matched.add(hashed.hash);
|
|
857
|
-
if (scan.matched.size === wanted.size)
|
|
858
|
-
break;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
return scan;
|
|
862
|
-
}
|
|
863
|
-
/**
|
|
864
|
-
* Gate-read cross-check of a matched approval marker against the persisted
|
|
865
|
-
* reports (task fa423e9b). Both PreToolUse hooks call it after
|
|
866
|
-
* `checkOperatorApprovalMarkers` matched, through `verifyMatchedMarkerReport`.
|
|
867
|
-
*
|
|
868
|
-
* Rule: a marker with a non-null signed `reportContentHash` is valid iff at
|
|
869
|
-
* least one parseable report file in `reportsDir` (any session, any
|
|
870
|
-
* `approvalStatus`) has a canonical hash equal to it. The hash already
|
|
871
|
-
* leaves the lifecycle fields out, so approving, expiring or re-approving
|
|
872
|
-
* the report never changes it. The signed hash therefore proves one thing:
|
|
873
|
-
* the content the operator approved is still on disk unmodified. Editing
|
|
874
|
-
* that file (in place, by swap, by symlink, by an unparseable rewrite)
|
|
875
|
-
* removes the only match and the marker stops opening the gate. Comparing
|
|
876
|
-
* against the session's newest approved report instead would deny
|
|
877
|
-
* legitimate approvals (a newer report approved for the same claim, two
|
|
878
|
-
* sessions on one claim) and would let a swap pass by moving the swapped
|
|
879
|
-
* report out of the selection.
|
|
880
|
-
*
|
|
881
|
-
* Bounded input: only a regular file of at most `MAX_HASHED_REPORT_BYTES`
|
|
882
|
-
* (1 MiB) that parses as a JSON object nested at most 64 levels deep has a
|
|
883
|
-
* canonical hash, read through {@link readReportFileBounded} (opened
|
|
884
|
-
* non-blocking, typed and sized by `fstat` on the open descriptor, read
|
|
885
|
-
* through it). Every other `*.json` entry (a FIFO, a directory, a device, a
|
|
886
|
-
* symlink to one of them, a file over the cap, an unreadable, unparseable or
|
|
887
|
-
* too deeply nested file) still counts as a report file but matches nothing,
|
|
888
|
-
* so it can neither open the gate nor make the scan block, run out of
|
|
889
|
-
* memory or die instead of deciding.
|
|
890
|
-
*
|
|
891
|
-
* Residuals: (1) the check proves only that some file carries the approved
|
|
892
|
-
* content. Any regular `*.json` file within the size cap whose content
|
|
893
|
-
* equals it keeps a match, whether it was kept before the edit (a copy) or
|
|
894
|
-
* re-created after it (the agent wrote that content, so it can write it
|
|
895
|
-
* again), and the marker still opens the gate. (2) A reports directory that
|
|
896
|
-
* is empty, unreadable or holds no `*.json` entry at all (every report
|
|
897
|
-
* deleted, the directory made unreadable, the reports renamed) falls back to
|
|
898
|
-
* the "no report file" allow, the same as before this check existed. The
|
|
899
|
-
* marker and the operator's approval itself are untouched in both cases;
|
|
900
|
-
* only the audit trail loses its claim to be the approved content. (3) The
|
|
901
|
-
* cap bounds each file, not their number, so the scan charges every `*.json`
|
|
902
|
-
* entry at least `MIN_SCAN_ENTRY_COST_BYTES` against `MAX_HASH_SCAN_BYTES`
|
|
903
|
-
* (bytes read or the floor, whichever is larger, whatever the read returned):
|
|
904
|
-
* past the budget the check denies with the mismatch reason (fail closed).
|
|
905
|
-
* That bounds the entries opened (8192) and the bytes read (about 32 MiB, the
|
|
906
|
-
* last file read may add up to the 1 MiB per-file cap). Three costs remain.
|
|
907
|
-
* A legitimate approval whose report sits behind that much newer report data
|
|
908
|
-
* (by name, descending) is denied; re-approving writes a new, newest report
|
|
909
|
-
* and recovers only when the newer entries are real reports: planted `*.json`
|
|
910
|
-
* names that sort after the timestamped report names (for example names
|
|
911
|
-
* starting with a letter) stay newer than any new report and keep denying
|
|
912
|
-
* until removed. The `readdir` plus name sort of the directory itself are not
|
|
913
|
-
* charged, so a directory of millions of entries still costs that listing
|
|
914
|
-
* before this scan starts (the scan runs when a signed marker is present). The
|
|
915
|
-
* hash scan is not the whole hook: the other full-directory reads on it (the
|
|
916
|
-
* evidence read, the auto-approval listing, the delegation lookup and the
|
|
917
|
-
* parse-error lookup) list the directory entry by entry and stop once
|
|
918
|
-
* {@link MAX_HOOK_LISTING_ENTRIES} entries are crossed, and charge the entries
|
|
919
|
-
* they read against the same byte budget; past either bound they open or read
|
|
920
|
-
* nothing more and fail closed.
|
|
921
|
-
*
|
|
922
|
-
* Removing the approved report while other report files remain is a deny:
|
|
923
|
-
* nothing on disk carries the signed content any more. `harness gc --apply`
|
|
924
|
-
* can do that legitimately: it ages approved reports out by their createdAt
|
|
925
|
-
* but markers by their file mtime, so a report written long before its
|
|
926
|
-
* approval can go while the marker stays. Re-approving recovers.
|
|
927
|
-
*
|
|
928
|
-
* `primary` is the marker that satisfied the gate (task-scoped first),
|
|
929
|
-
* `fallback` a session marker that matched behind a task marker. The
|
|
930
|
-
* fallback is consulted only when the primary fails the check, so a task
|
|
931
|
-
* marker signed for another report does not hide a session marker that
|
|
932
|
-
* verifies. Unchanged behaviour: a null hash on the accepted marker (no
|
|
933
|
-
* report resolved, or `--force` over a report nested too deeply to hash,
|
|
934
|
-
* never one over the size cap) and a reports directory with no
|
|
935
|
-
* `*.json` entry at all both allow. A forced approval of a hashable report
|
|
936
|
-
* signs its hash and is checked like any other.
|
|
937
|
-
*
|
|
938
|
-
* The `detail` is the block reason. It names the failing marker kind and the
|
|
939
|
-
* one-command fix, which is also the single migration step for a marker
|
|
940
|
-
* written before the canonical hash existed. It carries no report-derived
|
|
941
|
-
* value, so nothing in it needs sanitising.
|
|
942
|
-
*/
|
|
943
|
-
export function verifyApprovedReportHash(reportsDir, primary, fallback = null) {
|
|
944
|
-
if (primary.reportContentHash === null)
|
|
945
|
-
return { ok: true, kind: primary.kind };
|
|
946
|
-
if (fallback !== null && fallback.reportContentHash === null) {
|
|
947
|
-
return { ok: true, kind: fallback.kind };
|
|
948
|
-
}
|
|
949
|
-
const wanted = new Set([primary.reportContentHash]);
|
|
950
|
-
if (fallback !== null && fallback.reportContentHash !== null) {
|
|
951
|
-
wanted.add(fallback.reportContentHash);
|
|
952
|
-
}
|
|
953
|
-
const scan = scanReportHashes(reportsDir, wanted);
|
|
954
|
-
if (scan.files === 0 || scan.matched.has(primary.reportContentHash)) {
|
|
955
|
-
return { ok: true, kind: primary.kind };
|
|
956
|
-
}
|
|
957
|
-
if (fallback !== null &&
|
|
958
|
-
fallback.reportContentHash !== null &&
|
|
959
|
-
scan.matched.has(fallback.reportContentHash)) {
|
|
960
|
-
return { ok: true, kind: fallback.kind };
|
|
961
|
-
}
|
|
962
|
-
const kinds = fallback !== null
|
|
963
|
-
? `${primary.kind} and ${fallback.kind} approval markers were`
|
|
964
|
-
: `${primary.kind} approval marker was`;
|
|
965
|
-
return {
|
|
966
|
-
ok: false,
|
|
967
|
-
detail: `no report in the reports directory matches the content the ${kinds} signed for ` +
|
|
968
|
-
`(the approved report was changed or removed after approval); re-run \`harness approve understanding\`` +
|
|
969
|
-
(scan.budgetExhausted
|
|
970
|
-
? " (the reports directory holds more report data than the gate-read scan budget covers)"
|
|
971
|
-
: ""),
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* {@link listPersistedReports} for every read a PreToolUse hook (or the
|
|
976
|
-
* PostToolUse boundary) makes of the reports directory. The directory holds
|
|
977
|
-
* files the gated agent can write, so every entry is opened once through
|
|
978
|
-
* {@link readReportFileBounded}: non-blocking, typed and sized by `fstat` on
|
|
979
|
-
* the open descriptor, read through it. A `*.json` entry that is not a
|
|
980
|
-
* regular file (a FIFO, a directory, a device, a symlink to one of them), is
|
|
981
|
-
* over `MAX_HASHED_REPORT_BYTES`, cannot be opened or read, or grows while it
|
|
982
|
-
* is read is left out of `reports`, as if it were not there, and recorded in
|
|
983
|
-
* `skipped`: it can neither block the hook nor run it out of memory. There is
|
|
984
|
-
* no separate stat of the path (a symlink flipped to a FIFO between that stat
|
|
985
|
-
* and the read used to hang the read), and the modification time comes from
|
|
986
|
-
* the same descriptor.
|
|
987
|
-
*
|
|
988
|
-
* A caller that must not let the selection fall back to an older report when
|
|
989
|
-
* a newer one is unreadable (the auto-approval precondition) declines when
|
|
990
|
-
* `skipped` is not empty. The operator commands read through this listing as
|
|
991
|
-
* well, so a planted oversized or non-regular entry cannot crash or hang
|
|
992
|
-
* them, and they still see it: `harness approve understanding` refuses
|
|
993
|
-
* while `skipped` is not empty (it passes `refuseSymlinks`, which also
|
|
994
|
-
* refuses a symbolic link at the open, whatever it points at), and
|
|
995
|
-
* `harness gc` reports each skipped entry and leaves it in place.
|
|
996
|
-
*
|
|
997
|
-
* Placement: this block sits at the end of the file, and
|
|
998
|
-
* {@link readPersistedReport} takes the already-read text (`boundedRaw`), so
|
|
999
|
-
* the line numbers the decision record
|
|
1000
|
-
* docs/decisions/2026-08-27-ug-auto-mode-approval.md cites in this file stay
|
|
1001
|
-
* put (tests/decisions-citations-resolve.test.ts pins them).
|
|
1002
|
-
*/
|
|
1003
|
-
export function listPersistedReportsBoundedWithSkips(dir, opts = {}) {
|
|
1004
|
-
// `maxEntries` is set by the PreToolUse-path readers (see
|
|
1005
|
-
// `MAX_HOOK_LISTING_ENTRIES`); the operator commands leave it unset. Setting
|
|
1006
|
-
// it applies both bounds of one hook read: the entry count, and the byte
|
|
1007
|
-
// budget every entry read is charged against (see `MAX_HASH_SCAN_BYTES`).
|
|
1008
|
-
const bounded = opts.maxEntries !== undefined;
|
|
1009
|
-
const listed = listDirNamesBounded(dir, ".json", opts.maxEntries ?? Number.POSITIVE_INFINITY);
|
|
1010
|
-
if (listed.truncated) {
|
|
1011
|
-
return { reports: [], skipped: [], truncated: true, truncatedDetail: ENTRIES_TRUNCATED_DETAIL };
|
|
1012
|
-
}
|
|
1013
|
-
const budget = bounded ? { spent: 0 } : undefined;
|
|
1014
|
-
const reports = [];
|
|
1015
|
-
const skipped = [];
|
|
1016
|
-
for (const name of listed.names) {
|
|
1017
|
-
const full = path.join(dir, name);
|
|
1018
|
-
// `refuseSymlinks` opens with O_NOFOLLOW, so a link is recorded as skipped
|
|
1019
|
-
// by the open itself and no earlier lstat can race a swap.
|
|
1020
|
-
const read = readReportFileBounded(full, { noFollow: opts.refuseSymlinks === true, budget });
|
|
1021
|
-
// All or nothing: a listing that ran out of budget cannot say which
|
|
1022
|
-
// report is the newest, so it returns no report at all.
|
|
1023
|
-
if (!read.ok && read.reason === "over-budget") {
|
|
1024
|
-
return { reports: [], skipped: [], truncated: true, truncatedDetail: BYTES_TRUNCATED_DETAIL };
|
|
1025
|
-
}
|
|
1026
|
-
if (!read.ok) {
|
|
1027
|
-
skipped.push({ filePath: full, reason: read.reason, detail: read.detail });
|
|
1028
|
-
continue;
|
|
1029
|
-
}
|
|
1030
|
-
const report = readPersistedReport(full, read.mtimeMs, read.raw);
|
|
1031
|
-
if (report !== null)
|
|
1032
|
-
reports.push(report);
|
|
1033
|
-
}
|
|
1034
|
-
reports.sort((a, b) => b.createdAtMs - a.createdAtMs);
|
|
1035
|
-
return { reports, skipped, truncated: false, truncatedDetail: "" };
|
|
1036
|
-
}
|
|
1037
|
-
/**
|
|
1038
|
-
* {@link listPersistedReportsBoundedWithSkips} without the skipped entries,
|
|
1039
|
-
* for the callers that treat a skipped entry as no evidence
|
|
1040
|
-
* ({@link checkPersistedReport}, {@link expirePersistedReport}). Expiry acts
|
|
1041
|
-
* on the newest report that was within the cap, so an oversized newest
|
|
1042
|
-
* approved report is left as it is and the next in-cap one is expired.
|
|
1043
|
-
*/
|
|
1044
|
-
export function listPersistedReportsBounded(dir) {
|
|
1045
|
-
return listPersistedReportsBoundedWithSkips(dir).reports;
|
|
1046
|
-
}
|
|
1047
|
-
//# sourceMappingURL=persisted-reports.js.map
|