@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,1020 +0,0 @@
|
|
|
1
|
-
// Phase 4 #5 — `harness policy intercept` CLI entrypoint.
|
|
2
|
-
//
|
|
3
|
-
// Wired by Claude Code's settings.json (regenerated by `harness apply`) as
|
|
4
|
-
// the PreToolUse hook. Reads the event JSON from stdin, runs the runtime
|
|
5
|
-
// interceptor, writes Claude Code's deny JSON to stdout on block.
|
|
6
|
-
import { openLedgerSession, } from "../../policies/index.js";
|
|
7
|
-
import * as fs from "node:fs";
|
|
8
|
-
import * as os from "node:os";
|
|
9
|
-
import { readStdinBounded, STDIN_IDLE_TIMEOUT_MS, stdinTimeoutNote, stdinTimeoutBlockJson, stdinTimeoutBlockReason } from "../bounded-stdin.js";
|
|
10
|
-
import { buildActionEnvelope, intercept, isBlockingDecision, recordPolicyDecisionOnSession, resolveEnvironment, resolveGitContext, resolveKubeContext, sanitizeEnvelopeReason, } from "../../runtime/index.js";
|
|
11
|
-
import { resolveGeneratedDir, writePendingApproval } from "../../runtime/pending-approval.js";
|
|
12
|
-
import { ENV_RANK, resolveBashPrefixEnrichment } from "./risk-envelope-enrichment.js";
|
|
13
|
-
import { parseKubectlTarget } from "../../runtime/kubectl-target-parse.js";
|
|
14
|
-
import { MAX_NORMALIZE_LENGTH, normalizeCommand, normalizeCommandAmpAware, normalizeCommandQuoteAware, segmentViewOf, } from "../../runtime/command-normalize.js";
|
|
15
|
-
import { extractShellCommand, SHELL_ALIASES } from "../../runtime/tool-name-aliases.js";
|
|
16
|
-
import { loadManifest } from "../loader.js";
|
|
17
|
-
import { checkPauseFromLoader } from "../pause-check.js";
|
|
18
|
-
function findGroundingMcp(manifest) {
|
|
19
|
-
return manifest.tools.mcp.find((m) => m.name === "grounding-mcp") ?? null;
|
|
20
|
-
}
|
|
21
|
-
/** Resolve an `os` fact, returning "" on the (rare) lookup failure. */
|
|
22
|
-
function safeOs(fn) {
|
|
23
|
-
try {
|
|
24
|
-
return fn();
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return "";
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Render the hook-identity suffix for a stderr diagnostic. Returns the
|
|
32
|
-
* empty string when no `--hook` flag was supplied, so an operator
|
|
33
|
-
* invoking `harness policy intercept` by hand (and tests that don't
|
|
34
|
-
* inject hookName) see the original prefix verbatim.
|
|
35
|
-
*/
|
|
36
|
-
function hookSuffix(hookName) {
|
|
37
|
-
return hookName !== undefined && hookName.length > 0 ? ` [hook=${hookName}]` : "";
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Phase 5 #3 — render a deny / warn-degraded decision as a stderr
|
|
41
|
-
* diagnostic block. Multiline, indented; each block is bounded by the
|
|
42
|
-
* policy name so concurrent fires (rare but possible) stay readable.
|
|
43
|
-
*/
|
|
44
|
-
function formatDecisionDiagnostic(decision, hookName) {
|
|
45
|
-
// The deny-degraded suffix names the fail_open opt-out ON PURPOSE and
|
|
46
|
-
// ONLY here: stderr is the operator's surface. The agent-facing deny
|
|
47
|
-
// envelope deliberately omits it (see the deny-degraded branch in
|
|
48
|
-
// `runtime/intercept.ts` — a deny that includes its own disable recipe
|
|
49
|
-
// is not a gate).
|
|
50
|
-
// Both degraded arms are cause-neutral: the degraded family has five
|
|
51
|
-
// causes and only one of them is the ledger; the true cause follows
|
|
52
|
-
// on the block's own `reason:` line (review 2026-08-08, rounds 3+5).
|
|
53
|
-
const header = `harness policy intercept${hookSuffix(hookName)}: ${decision.policyName}: ${decision.outcome}${decision.outcome === "warn-degraded"
|
|
54
|
-
? " (evidence could not be evaluated; non-blocking per warn tier or fail_open opt-out)"
|
|
55
|
-
: decision.outcome === "deny-degraded"
|
|
56
|
-
? " (evidence could not be evaluated; failing closed per enforcement tier; operator opt-out: risk.degraded_fail_posture: fail_open)"
|
|
57
|
-
: ""}`;
|
|
58
|
-
const lines = [header];
|
|
59
|
-
lines.push(` ledger_tag: ${decision.ledgerTag}`);
|
|
60
|
-
if (decision.requiresEval !== undefined) {
|
|
61
|
-
lines.push(` matched: ${decision.requiresEval.matchedCount}`);
|
|
62
|
-
}
|
|
63
|
-
lines.push(` reason: ${decision.reason}`);
|
|
64
|
-
const extractKeys = Object.keys(decision.extractValues);
|
|
65
|
-
if (extractKeys.length > 0) {
|
|
66
|
-
lines.push(" extract:");
|
|
67
|
-
for (const k of extractKeys.sort()) {
|
|
68
|
-
lines.push(` ${k}=${decision.extractValues[k]}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return `${lines.join("\n")}\n`;
|
|
72
|
-
}
|
|
73
|
-
function isVerboseEnabled(opts) {
|
|
74
|
-
if (opts.verbose === true)
|
|
75
|
-
return true;
|
|
76
|
-
if (opts.verbose === false)
|
|
77
|
-
return false;
|
|
78
|
-
const env = process.env.HARNESS_POLICY_VERBOSE;
|
|
79
|
-
if (typeof env !== "string")
|
|
80
|
-
return false;
|
|
81
|
-
if (env.length === 0)
|
|
82
|
-
return false;
|
|
83
|
-
// Accept anything truthy except literal disable-words (case-insensitive).
|
|
84
|
-
return !/^(0|false|no|off)$/i.test(env.trim());
|
|
85
|
-
}
|
|
86
|
-
// Derived from `SHELL_ALIASES` rather than hand-copying its members (fix
|
|
87
|
-
// round 2): `tool-name-aliases.ts`'s own header warns against the copy,
|
|
88
|
-
// and since fix round 1 the precompute-matches-matcher argument above
|
|
89
|
-
// depends on this set and the alias expansion agreeing. Deriving it means
|
|
90
|
-
// they cannot drift apart.
|
|
91
|
-
function isCodexShellTool(toolName) {
|
|
92
|
-
return typeof toolName === "string" && SHELL_ALIASES.includes(toolName);
|
|
93
|
-
}
|
|
94
|
-
function extractPerCallCwd(input) {
|
|
95
|
-
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
96
|
-
return null;
|
|
97
|
-
const args = input;
|
|
98
|
-
for (const candidate of [args.workdir, args.cwd]) {
|
|
99
|
-
if (typeof candidate === "string" && candidate.length > 0)
|
|
100
|
-
return candidate;
|
|
101
|
-
}
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
function readCodexCommandCwdFromProc() {
|
|
105
|
-
let argv;
|
|
106
|
-
try {
|
|
107
|
-
argv = fs.readFileSync("/proc/1/cmdline", "utf8").split("\0");
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
const idx = argv.indexOf("--command-cwd");
|
|
113
|
-
const cwd = idx >= 0 ? argv[idx + 1] : undefined;
|
|
114
|
-
return typeof cwd === "string" && cwd.length > 0 ? cwd : null;
|
|
115
|
-
}
|
|
116
|
-
function resolvePolicyCwd(event, codexCommandCwd) {
|
|
117
|
-
if (typeof event.cwd === "string" && event.cwd.length > 0)
|
|
118
|
-
return event.cwd;
|
|
119
|
-
if (isCodexShellTool(event.tool_name)) {
|
|
120
|
-
for (const input of [event.raw_input, event.tool_input, event.input]) {
|
|
121
|
-
const cwd = extractPerCallCwd(input);
|
|
122
|
-
if (cwd)
|
|
123
|
-
return cwd;
|
|
124
|
-
}
|
|
125
|
-
if (codexCommandCwd !== undefined && codexCommandCwd.length > 0) {
|
|
126
|
-
return codexCommandCwd;
|
|
127
|
-
}
|
|
128
|
-
const procCwd = readCodexCommandCwdFromProc();
|
|
129
|
-
if (procCwd)
|
|
130
|
-
return procCwd;
|
|
131
|
-
}
|
|
132
|
-
return process.cwd();
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Explicit kubectl target, upgrade-only merge (task a7eb1a71): an
|
|
136
|
-
* explicit --context/--namespace/-n on a kubectl command can only push
|
|
137
|
-
* the resolved environment to something MORE dangerous than the
|
|
138
|
-
* ambient kubeconfig alone already gave, never less, mirroring
|
|
139
|
-
* applyBranchSwitchUpgrade exactly (both the ambient-only base and the
|
|
140
|
-
* merged-candidate kube inputs run through the SAME resolveEnvironment
|
|
141
|
-
* call with identical git/env inputs, so ENV_RANK picks the more
|
|
142
|
-
* dangerous of the two). See the `CHANGELOG.md:#0.49.0` entry (task
|
|
143
|
-
* a7eb1a71) for the measured downgrade this fixes.
|
|
144
|
-
*/
|
|
145
|
-
function applyKubeTargetUpgrade(event, manifest, cwd, git, ambientKube, kubectlTarget, env, user, host, now) {
|
|
146
|
-
if (kubectlTarget === null)
|
|
147
|
-
return ambientKube;
|
|
148
|
-
const candidateKube = {
|
|
149
|
-
context: kubectlTarget.context ?? ambientKube.context,
|
|
150
|
-
namespace: kubectlTarget.namespace ?? ambientKube.namespace,
|
|
151
|
-
};
|
|
152
|
-
if (candidateKube.context === ambientKube.context &&
|
|
153
|
-
candidateKube.namespace === ambientKube.namespace) {
|
|
154
|
-
return ambientKube;
|
|
155
|
-
}
|
|
156
|
-
const effectiveNow = now ?? new Date();
|
|
157
|
-
const baseResolution = resolveEnvironment(buildActionEnvelope(event, { cwd, git, user, host, now: effectiveNow }), manifest.environments.resolvers, { env, kubeContext: ambientKube.context, kubeNamespace: ambientKube.namespace });
|
|
158
|
-
const candidateResolution = resolveEnvironment(buildActionEnvelope(event, { cwd, git, user, host, now: effectiveNow }), manifest.environments.resolvers, { env, kubeContext: candidateKube.context, kubeNamespace: candidateKube.namespace });
|
|
159
|
-
return ENV_RANK[candidateResolution.name] < ENV_RANK[baseResolution.name]
|
|
160
|
-
? candidateKube
|
|
161
|
-
: ambientKube;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Real grounding-mcp-backed ledger client. Exported for testing: the
|
|
165
|
-
* `record` adapter's failure-surfacing path is exercised against a
|
|
166
|
-
* bogus mcpCommand in tests/runtime/intercept-cli.test.ts.
|
|
167
|
-
*
|
|
168
|
-
* Connection discipline (2026-07-01 review, enforcement-subprocess-latency):
|
|
169
|
-
* ONE lazily-opened grounding-mcp session serves every query and audit
|
|
170
|
-
* record of the intercept invocation this client was constructed for,
|
|
171
|
-
* instead of 2 fresh subprocess spawns per matching policy. On top,
|
|
172
|
-
* `ledger_summary` returns the WHOLE session's entries (per-tag filtering
|
|
173
|
-
* happens client-side in `intercept()`), so the K per-policy queries are
|
|
174
|
-
* textually identical — they collapse into one cached round-trip per
|
|
175
|
-
* sessionId. `runInterceptCli` calls `dispose()` when the intercept
|
|
176
|
-
* completes; a caller constructing this client directly owns that call.
|
|
177
|
-
*/
|
|
178
|
-
export function realLedgerClient(server, opts) {
|
|
179
|
-
const command = Array.isArray(server.command)
|
|
180
|
-
? server.command
|
|
181
|
-
: server.command.trim().split(/\s+/);
|
|
182
|
-
const env = server.env ?? undefined;
|
|
183
|
-
const timeoutMs = opts.ledgerTimeoutMs ?? server.health?.timeout_ms ?? 5_000;
|
|
184
|
-
const stderr = opts.stderr ?? process.stderr;
|
|
185
|
-
let session = null;
|
|
186
|
-
const summaryCache = new Map();
|
|
187
|
-
// One fresh-session retry for audit writes per client instance,
|
|
188
|
-
// reserved for `deny-degraded` decisions (task f1aea826, AC4; review
|
|
189
|
-
// 2026-08-08 tightened both the reservation and the budget). The
|
|
190
|
-
// pooled session carries a session-wide timeout latch: a single slow
|
|
191
|
-
// QUERY (the very condition that produces a blocking `deny-degraded`)
|
|
192
|
-
// short-circuits every later call on the same session, including the
|
|
193
|
-
// audit write for that decision — so the one row that most needs to
|
|
194
|
-
// land was guaranteed to be lost. A fresh subprocess lands it whenever
|
|
195
|
-
// grounding-mcp itself is healthy (the measured 2026-08-06 case: only
|
|
196
|
-
// the artificial 1ms query budget latched; follow-up calls answered
|
|
197
|
-
// fine).
|
|
198
|
-
//
|
|
199
|
-
// Bounds, stated precisely because the record runs on the deny path
|
|
200
|
-
// BEFORE the envelope is emitted and any stall risks the outer
|
|
201
|
-
// hook-timeout-is-allow contract: the retry is (a) reserved for
|
|
202
|
-
// decisions whose outcome is `deny-degraded` — an allow/warn row
|
|
203
|
-
// failing first must not consume it — (b) taken at most once per
|
|
204
|
-
// client instance, and (c) run on a session whose OWN timeout is
|
|
205
|
-
// `auditRetryTimeoutMs(timeoutMs)` = max(250ms, timeoutMs/4). Its two
|
|
206
|
-
// calls (initialize + ledger_add) therefore add at most
|
|
207
|
-
// 2 * max(250ms, timeoutMs/4) on top of the query's timeoutMs — that
|
|
208
|
-
// is <=timeoutMs/2 (total <=1.5x) once timeoutMs >= 1s, while below
|
|
209
|
-
// 1s the 250ms floor dominates and the ADD is bounded at 500ms
|
|
210
|
-
// absolute (review 2026-08-08 round 2: at the audit's 1-100ms probe
|
|
211
|
-
// settings that is a multiple of the query budget, but a trivial
|
|
212
|
-
// absolute cost). The fresh session replaces the latched one, so
|
|
213
|
-
// later audit writes in the same invocation inherit the working
|
|
214
|
-
// transport (and its tighter budget). If the retry also fails, the
|
|
215
|
-
// loud-stderr contract stands (audit degradation never blocks,
|
|
216
|
-
// docs/okf matrix).
|
|
217
|
-
const retryTimeoutMs = auditRetryTimeoutMs(timeoutMs);
|
|
218
|
-
let recordRetryUsed = false;
|
|
219
|
-
const getSession = () => (session ??= openLedgerSession({
|
|
220
|
-
mcpCommand: command,
|
|
221
|
-
...(env && { mcpEnv: env }),
|
|
222
|
-
timeoutMs,
|
|
223
|
-
}));
|
|
224
|
-
return {
|
|
225
|
-
async query(_tag, sessionId) {
|
|
226
|
-
// Cache the Promise (not the value) so concurrent queries for the
|
|
227
|
-
// same session dedupe onto one in-flight round-trip. A degraded
|
|
228
|
-
// result is cached too — deliberately: a first-query timeout
|
|
229
|
-
// degrades ALL K policies off the one cached round-trip instead of
|
|
230
|
-
// retrying per policy, which would reintroduce the fan-out. (What
|
|
231
|
-
// a degraded evaluation MEANS is tier-dependent since task
|
|
232
|
-
// f1aea826: warn policies stay non-blocking `warn-degraded`,
|
|
233
|
-
// block/require_approval fail closed as `deny-degraded`.)
|
|
234
|
-
// Staleness: our own
|
|
235
|
-
// `record` rows are filtered out by the evaluator by design; an
|
|
236
|
-
// EXTERNAL writer landing evidence mid-loop would make a later
|
|
237
|
-
// policy deny where a fresh query would allow — a fail-closed,
|
|
238
|
-
// ms-wide window resolved by the agent's retry (next intercept =
|
|
239
|
-
// fresh session + fresh snapshot).
|
|
240
|
-
let cached = summaryCache.get(sessionId);
|
|
241
|
-
if (cached === undefined) {
|
|
242
|
-
cached = getSession().querySummary({ sessionId });
|
|
243
|
-
summaryCache.set(sessionId, cached);
|
|
244
|
-
}
|
|
245
|
-
return cached;
|
|
246
|
-
},
|
|
247
|
-
async record(decision, sessionId) {
|
|
248
|
-
let result = await recordPolicyDecisionOnSession(getSession(), decision, sessionId);
|
|
249
|
-
if (!result.ok &&
|
|
250
|
-
decision.outcome === "deny-degraded" &&
|
|
251
|
-
!recordRetryUsed) {
|
|
252
|
-
// Latched-session recovery (bounds documented at retryTimeoutMs
|
|
253
|
-
// above): drop the pooled session and retry ONCE over a fresh
|
|
254
|
-
// subprocess with the tighter retry budget. Queries are
|
|
255
|
-
// unaffected — their per-session summary promise is already
|
|
256
|
-
// cached by the time any record runs.
|
|
257
|
-
recordRetryUsed = true;
|
|
258
|
-
session?.dispose();
|
|
259
|
-
session = openLedgerSession({
|
|
260
|
-
mcpCommand: command,
|
|
261
|
-
...(env && { mcpEnv: env }),
|
|
262
|
-
timeoutMs: retryTimeoutMs,
|
|
263
|
-
});
|
|
264
|
-
result = await recordPolicyDecisionOnSession(session, decision, sessionId);
|
|
265
|
-
}
|
|
266
|
-
// A failed audit-write must not block the tool — the decision is
|
|
267
|
-
// still applied — but it must not be silent either: an unrecorded
|
|
268
|
-
// decision is invisible to `harness audit` / `explain --trace`.
|
|
269
|
-
// Goes to stderr so Claude Code's stdout deny-JSON contract holds.
|
|
270
|
-
if (!result.ok) {
|
|
271
|
-
stderr.write(`harness policy intercept${hookSuffix(opts.hookName)}: audit-write failed for ` +
|
|
272
|
-
`${decision.policyName}: ${result.reason ?? "unknown error"}\n`);
|
|
273
|
-
}
|
|
274
|
-
},
|
|
275
|
-
dispose() {
|
|
276
|
-
session?.dispose();
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Per-call budget of the audit-retry session (task f1aea826, review
|
|
282
|
-
* round 2): a quarter of the ledger timeout with a 250ms floor (a fresh
|
|
283
|
-
* node subprocess must cold-start + initialize within it). Exported as a
|
|
284
|
-
* pure function so the FORMULA itself is pinned by a unit test — the
|
|
285
|
-
* round-1 finding this bounds was precisely that an unbounded retry
|
|
286
|
-
* reused the full budget, and nothing would go red if that regressed.
|
|
287
|
-
*/
|
|
288
|
-
export function auditRetryTimeoutMs(timeoutMs) {
|
|
289
|
-
return Math.max(250, Math.floor(timeoutMs / 4));
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Recognise a hook `command` string as an invocation of `harness policy
|
|
293
|
-
* intercept`'s subcommand, regardless of how the leading interpreter /
|
|
294
|
-
* binary token is spelled: a bare `harness policy intercept`, a node-path
|
|
295
|
-
* form (`node /abs/path/dist/cli/index.js policy intercept`, e.g. a local
|
|
296
|
-
* dev build), an `npx` form (`npx harness policy intercept`), a leading
|
|
297
|
-
* inline env-var assignment (`FOO=bar harness policy intercept` — the
|
|
298
|
-
* same shape `understanding-before-execution.ts`'s `wrap()` helper
|
|
299
|
-
* prefixes pack-contributed commands with), or a trailing flag
|
|
300
|
-
* (`harness policy intercept --hook <name>`, the tag
|
|
301
|
-
* `generate-codex-config.ts#commandWithHookTag` appends).
|
|
302
|
-
*
|
|
303
|
-
* Every OTHER call site that needs to recognise this subcommand in this
|
|
304
|
-
* codebase today (`generate-settings.ts#hookTimeoutSeconds`,
|
|
305
|
-
* `generate-codex-config.ts#commandWithHookTag`) still compares
|
|
306
|
-
* `command.trim() === "harness policy intercept"` VERBATIM — deliberately
|
|
307
|
-
* left unchanged (task d20a7e0c scope: the hook-timeout projection
|
|
308
|
-
* contract those two own is out of scope). For THEM a missed match only
|
|
309
|
-
* changes a floor/flag on a command harness itself emits byte-for-byte,
|
|
310
|
-
* so the single canonical spelling is enough. This matcher exists for a
|
|
311
|
-
* caller (the validate/doctor hook-budget-vs-ledger-timeout guard, task
|
|
312
|
-
* d20a7e0c) where under-recognition is a false NEGATIVE on a security
|
|
313
|
-
* check against an OPERATOR-authored `manifest.hooks[]` entry, which is
|
|
314
|
-
* under no obligation to spell the command exactly the way harness's own
|
|
315
|
-
* generators do.
|
|
316
|
-
*
|
|
317
|
-
* Deliberately narrow, not a shell parser: matches the two literal words
|
|
318
|
-
* `policy` and `intercept` as adjacent, whitespace-separated tokens,
|
|
319
|
-
* preceded by start-of-string / whitespace / a path separator (so
|
|
320
|
-
* `mypolicy intercept` inside one longer token cannot match) and followed
|
|
321
|
-
* by whitespace, end-of-string, or a semicolon/quote character (`;`, `"`,
|
|
322
|
-
* `'`) immediately abutting the word — the shape a chained shell command
|
|
323
|
-
* (`harness policy intercept; echo done`) or an embedded `sh -c
|
|
324
|
-
* "harness policy intercept"` wrapper produces, where nothing whitespace
|
|
325
|
-
* separates `intercept` from the next token (review 2026-08-09, fix round
|
|
326
|
-
* 1, finding 3). The quote arm accepts more than it strictly should: a
|
|
327
|
-
* quote glued directly onto the word (`intercept'or'`) also matches, even
|
|
328
|
-
* though a shell concatenates that into a different, longer identifier
|
|
329
|
-
* (`interceptor`). This is a deliberate, harmless OVER-flag — at worst a
|
|
330
|
-
* spurious validate/doctor diagnostic on an exotic command, never a missed
|
|
331
|
-
* real hook — accepted because the same quote boundary is load-bearing for
|
|
332
|
-
* the legitimate `sh -c "...intercept"` wrapper above. Still not
|
|
333
|
-
* exhaustive: a trailing `)`, `|`, `&`, or
|
|
334
|
-
* backtick immediately abutting the word (e.g. `$(harness policy
|
|
335
|
-
* intercept)`) is a KNOWN, deliberately-unclosed gap — widening further
|
|
336
|
-
* risks matching inside an unrelated longer token with no whitespace
|
|
337
|
-
* boundary at all, and the shapes above already cover the realistic
|
|
338
|
-
* manifest-authoring patterns this matcher exists for. A false negative
|
|
339
|
-
* here degrades to the pre-existing verbatim behaviour (the hook is
|
|
340
|
-
* simply not classified as ledger-consulting, so the margin check has
|
|
341
|
-
* nothing to say about it) — not a runtime bypass; the dispatch this
|
|
342
|
-
* check advises about is unaffected either way.
|
|
343
|
-
*/
|
|
344
|
-
export function isPolicyInterceptCommand(command) {
|
|
345
|
-
return /(?:^|[\s/\\])policy\s+intercept(?:\s|$|[;"'])/.test(command);
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Minimum `budget_ms` a blocking, ledger-consulting hook needs to clear
|
|
349
|
-
* the worst-case wall time `realLedgerClient` (below) can spend before it
|
|
350
|
-
* writes its own fail-closed decision — derived from that function's OWN
|
|
351
|
-
* two round-trip shapes, not a measured-then-eyeballed constant. Both
|
|
352
|
-
* halves are pinned against the real client in
|
|
353
|
-
* tests/e2e/policy-intercept.test.ts ("hook-budget-ledger-margin
|
|
354
|
-
* derivation (task d20a7e0c)").
|
|
355
|
-
*
|
|
356
|
-
* SCOPE, stated precisely (review 2026-08-09, fix round 1): this is the
|
|
357
|
-
* worst case on the PURE-TIMEOUT HANG SHAPE — the shape where the
|
|
358
|
-
* `query()` call that degrades does so by tripping `LedgerSession`'s own
|
|
359
|
-
* session-wide `timedOut` latch (src/policies/ledger-client.ts), which is
|
|
360
|
-
* what lets the retry sequence below start from an INSTANT first
|
|
361
|
-
* `record()` attempt. A query() that instead degrades via a NON-timeout
|
|
362
|
-
* path — `ledger_summary` coming back a genuine JSON-RPC error, an
|
|
363
|
-
* unparseable payload, or a payload missing the `entries` shape
|
|
364
|
-
* (`querySummary`'s non-timeout `degraded` branches in
|
|
365
|
-
* src/policies/ledger-client.ts) — never sets that latch, so it falls
|
|
366
|
-
* OUTSIDE this bound. See the KNOWN RESIDUAL paragraph at the end of this
|
|
367
|
-
* comment; do not read this function, or the `checkHookBudgetLedgerMargin`
|
|
368
|
-
* diagnostic it backs, as a blanket worst-case guarantee.
|
|
369
|
-
*
|
|
370
|
-
* `T` = the ledger's own per-call budget (`health.timeout_ms`, defaulted
|
|
371
|
-
* to 5000 exactly like `realLedgerClient` itself). `R` =
|
|
372
|
-
* `auditRetryTimeoutMs(T)`, the audit-retry session's own per-call budget.
|
|
373
|
-
*
|
|
374
|
-
* 1. query() — up to 2×T. `LedgerSession.querySummary`
|
|
375
|
-
* (src/policies/ledger-client.ts) performs `ensureInitialized()`
|
|
376
|
-
* (the `initialize` handshake, its own T-bounded wait) and, only
|
|
377
|
-
* once THAT resolves, the actual `ledger_summary` `tools/call` (a
|
|
378
|
-
* SEPARATE T-bounded wait). A ledger that is merely SLOW rather than
|
|
379
|
-
* hard-down (the threat class docs/risk-gate.md and this file's own
|
|
380
|
-
* `deny-degraded` diagnostics already name, distinct from an
|
|
381
|
-
* instantly-refusing one) can answer `initialize` just under T and
|
|
382
|
-
* still time out on the following `ledger_summary` — the case a
|
|
383
|
-
* naive "1×T" assumption undercounts.
|
|
384
|
-
*
|
|
385
|
-
* 2. the deny-degraded audit retry — up to 3×R. On the pure-timeout
|
|
386
|
-
* hang shape, the pooled session's `timedOut` latch (tripped by the
|
|
387
|
-
* query()-side timeout itself) makes the FIRST `record()` attempt on
|
|
388
|
-
* it instant (see `realLedgerClient`'s own doc comment above). The
|
|
389
|
-
* ONE reserved retry then opens a FRESH session budgeted at R and
|
|
390
|
-
* calls `recordPolicyDecisionOnSession` (src/runtime/ledger-
|
|
391
|
-
* record.ts), which can spend up to three independent R-bounded
|
|
392
|
-
* round-trips before it can report success or give up: the fresh
|
|
393
|
-
* session's own `initialize` handshake, the first `ledger_add`
|
|
394
|
-
* attempt at `type: "policy_decision"`, and — only reached when that
|
|
395
|
-
* attempt comes back a genuine JSON-RPC error rather than a
|
|
396
|
-
* transport timeout (an older/incompatible grounding-mcp rejecting
|
|
397
|
-
* the newer type) — the legacy `type: "fact"` fallback `ledger_add`.
|
|
398
|
-
*
|
|
399
|
-
* query() must complete (or degrade) before any policy decision — and so
|
|
400
|
-
* any record() — is computed (`intercept()`'s per-policy loop in
|
|
401
|
-
* src/runtime/intercept.ts awaits `evaluateOnePolicy`, which calls
|
|
402
|
-
* `ledger.query`, before it calls `ledger.record`), and the retry only
|
|
403
|
-
* starts after the first record() attempt on the pooled session already
|
|
404
|
-
* failed, so the two stages are SEQUENTIAL within one hook invocation:
|
|
405
|
-
* worst case ON THE PURE-TIMEOUT HANG SHAPE = 2T + 3R.
|
|
406
|
-
*
|
|
407
|
-
* KNOWN RESIDUAL (review 2026-08-09, fix round 1): when `query()` instead
|
|
408
|
-
* degrades via one of the NON-timeout branches named in the SCOPE
|
|
409
|
-
* paragraph above, the pooled session's `timedOut` latch is never set, so
|
|
410
|
-
* `record()`'s own first attempt is a REAL round-trip too — up to T for
|
|
411
|
-
* the `type: "policy_decision"` `ledger_add` attempt, plus another up to
|
|
412
|
-
* T for the legacy `type: "fact"` fallback if that attempt is itself
|
|
413
|
-
* rejected non-timeout — BEFORE the 3R retry sequence even starts. Worst
|
|
414
|
-
* case on this "error-then-hang" shape is therefore up to 4T + 3R, not
|
|
415
|
-
* 2T + 3R: 23750ms at the shipped T=5000 default, against this
|
|
416
|
-
* function's 13750ms. This formula is DELIBERATELY NOT raised to
|
|
417
|
-
* 4T + 3R: doing so would fail `checkHookBudgetLedgerMargin` against the
|
|
418
|
-
* shipped, reviewer-confirmed-clean fresh-init budget of 15000ms
|
|
419
|
-
* (src/cli/init/templates.ts, task 7bf47554/#414 — out of scope here) at
|
|
420
|
-
* the same T=5000 default, since 15000 < 23750. The guard this function
|
|
421
|
-
* backs GUARANTEES delivery of the fail-closed verdict on the
|
|
422
|
-
* pure-timeout hang shape, but NOT on this error-then-hang shape.
|
|
423
|
-
* Unlike an earlier draft of this note, this residual is NOT confined to
|
|
424
|
-
* one runtime projection: Claude Code and Codex both convert `budget_ms`
|
|
425
|
-
* into the outer hook-kill seconds via the identical `hookTimeoutSeconds`
|
|
426
|
-
* helper (src/cli/apply/generate-settings.ts, shared by
|
|
427
|
-
* generate-codex-config.ts) since task 7bf47554/#414, so a manifest at
|
|
428
|
-
* the shipped defaults is equally exposed on both — this was checked
|
|
429
|
-
* against both generators' current source, not assumed.
|
|
430
|
-
*/
|
|
431
|
-
export function requiredHookBudgetMs(ledgerTimeoutMs) {
|
|
432
|
-
return 2 * ledgerTimeoutMs + 3 * auditRetryTimeoutMs(ledgerTimeoutMs);
|
|
433
|
-
}
|
|
434
|
-
function degradedLedgerClient(reason, stderr, hookName) {
|
|
435
|
-
return {
|
|
436
|
-
async query() {
|
|
437
|
-
return { kind: "degraded", reason };
|
|
438
|
-
},
|
|
439
|
-
async record(decision) {
|
|
440
|
-
// No transport exists, so no audit row can ever land. Stay silent
|
|
441
|
-
// for non-blocking rows (a manifest without grounding-mcp would
|
|
442
|
-
// otherwise emit one line per policy per tool event), but a
|
|
443
|
-
// BLOCKING decision with no audit row must not also be invisible
|
|
444
|
-
// (review 2026-08-08: previously a deny-degraded produced neither
|
|
445
|
-
// a row nor an audit-write-failed line on this path). Blocking-ness
|
|
446
|
-
// is the runtime's own isBlockingDecision — round 2 caught a third
|
|
447
|
-
// hand-rolled copy of that predicate here.
|
|
448
|
-
if (isBlockingDecision(decision)) {
|
|
449
|
-
stderr.write(`harness policy intercept${hookSuffix(hookName)}: blocking decision for ` +
|
|
450
|
-
`${decision.policyName} has NO audit row (${reason})\n`);
|
|
451
|
-
}
|
|
452
|
-
},
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* The hook event name a timed-out read carries when it names a hook event other
|
|
457
|
-
* than PreToolUse, else `undefined`: the text is empty, truncated, not a JSON
|
|
458
|
-
* object, names no event, or names PreToolUse. The caller keeps its pre-existing
|
|
459
|
-
* continue behaviour only for an event this returns a name for.
|
|
460
|
-
*/
|
|
461
|
-
function otherHookEventName(text) {
|
|
462
|
-
let parsed;
|
|
463
|
-
try {
|
|
464
|
-
parsed = JSON.parse(text.trim());
|
|
465
|
-
}
|
|
466
|
-
catch {
|
|
467
|
-
return undefined;
|
|
468
|
-
}
|
|
469
|
-
if (typeof parsed !== "object" || parsed === null)
|
|
470
|
-
return undefined;
|
|
471
|
-
const name = parsed.hook_event_name;
|
|
472
|
-
return typeof name === "string" && name.length > 0 && name !== "PreToolUse" ? name : undefined;
|
|
473
|
-
}
|
|
474
|
-
export async function runInterceptCli(opts = {}) {
|
|
475
|
-
const stdin = opts.stdin ?? process.stdin;
|
|
476
|
-
const stdout = opts.stdout ?? process.stdout;
|
|
477
|
-
const stderr = opts.stderr ?? process.stderr;
|
|
478
|
-
const verbose = isVerboseEnabled(opts);
|
|
479
|
-
const idleTimeoutMs = opts.stdinIdleTimeoutMs ?? STDIN_IDLE_TIMEOUT_MS;
|
|
480
|
-
// Single pause-check options, shared by both checkPauseFromLoader calls.
|
|
481
|
-
const pauseOpts = {
|
|
482
|
-
loaderOpts: opts,
|
|
483
|
-
hookLabel: "policy intercept",
|
|
484
|
-
stderr,
|
|
485
|
-
};
|
|
486
|
-
if (opts.generatedDir !== undefined)
|
|
487
|
-
pauseOpts.generatedDir = opts.generatedDir;
|
|
488
|
-
if (opts.now !== undefined)
|
|
489
|
-
pauseOpts.now = opts.now;
|
|
490
|
-
const read = await readStdinBounded(stdin, idleTimeoutMs);
|
|
491
|
-
if (read.timedOut && otherHookEventName(read.text) === undefined) {
|
|
492
|
-
// A timed-out read leaves no event to judge, and this verb is the
|
|
493
|
-
// PreToolUse gate entrypoint: continuing as an empty event turned a block
|
|
494
|
-
// into an allow whenever the host stalled past the bound and then wrote a
|
|
495
|
-
// complete gated event. The rendered hook command carries no event name, so
|
|
496
|
-
// the event the read itself declares is the only signal there is: an event
|
|
497
|
-
// naming another hook event keeps the continue path below, anything else
|
|
498
|
-
// (nothing read, a truncated or unparseable prefix, a PreToolUse event, or
|
|
499
|
-
// an object with no event name) is treated as the PreToolUse call this
|
|
500
|
-
// verb is registered for. The operator pause still wins, and a paused run
|
|
501
|
-
// allows exactly as it does after a read that closed.
|
|
502
|
-
if (checkPauseFromLoader(pauseOpts).paused) {
|
|
503
|
-
return { exitCode: 0, decisions: [], blocked: false };
|
|
504
|
-
}
|
|
505
|
-
const reason = stdinTimeoutBlockReason(idleTimeoutMs);
|
|
506
|
-
stderr.write(`harness policy intercept${hookSuffix(opts.hookName)}: BLOCK: ${reason}\n`);
|
|
507
|
-
stdout.write(`${stdinTimeoutBlockJson(reason)}\n`);
|
|
508
|
-
return { exitCode: 0, decisions: [], blocked: true };
|
|
509
|
-
}
|
|
510
|
-
if (read.timedOut) {
|
|
511
|
-
// Only an event that names another hook event gets here (see above), so the
|
|
512
|
-
// text read is non-empty and parsed as that event below.
|
|
513
|
-
stderr.write(`harness policy intercept${hookSuffix(opts.hookName)}: ${stdinTimeoutNote(read, idleTimeoutMs)}\n`);
|
|
514
|
-
}
|
|
515
|
-
const raw = read.text;
|
|
516
|
-
let event;
|
|
517
|
-
try {
|
|
518
|
-
event = JSON.parse(raw.trim() || "{}");
|
|
519
|
-
}
|
|
520
|
-
catch (err) {
|
|
521
|
-
stderr.write(`harness policy intercept${hookSuffix(opts.hookName)}: malformed event JSON: ${err.message}\n`);
|
|
522
|
-
return { exitCode: 0, decisions: [], blocked: false };
|
|
523
|
-
}
|
|
524
|
-
// Pause sentinel — operator-only kill switch. Honoured BEFORE manifest
|
|
525
|
-
// load so the lockout-recovery flow (where the manifest is exactly what
|
|
526
|
-
// is broken) still respects an active pause. Mirrors the pack PreToolUse
|
|
527
|
-
// hook: without this the policy gates kept firing while paused, i.e. the
|
|
528
|
-
// very gates the kill switch most needs to silence (a wedged preflight /
|
|
529
|
-
// grounding gate is the canonical reason an operator pauses at all).
|
|
530
|
-
{
|
|
531
|
-
if (checkPauseFromLoader(pauseOpts).paused) {
|
|
532
|
-
return { exitCode: 0, decisions: [], blocked: false };
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
let manifest;
|
|
536
|
-
let manifestPath;
|
|
537
|
-
try {
|
|
538
|
-
if (opts.manifest) {
|
|
539
|
-
manifest = opts.manifest;
|
|
540
|
-
}
|
|
541
|
-
else {
|
|
542
|
-
const loaded = loadManifest(opts);
|
|
543
|
-
manifest = loaded.manifest;
|
|
544
|
-
manifestPath = loaded.resolved.base;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
catch (err) {
|
|
548
|
-
stderr.write(`harness policy intercept${hookSuffix(opts.hookName)}: manifest load failed: ${err.message}\n`);
|
|
549
|
-
return { exitCode: 0, decisions: [], blocked: false };
|
|
550
|
-
}
|
|
551
|
-
let ledger;
|
|
552
|
-
if (opts.ledger) {
|
|
553
|
-
ledger = opts.ledger;
|
|
554
|
-
}
|
|
555
|
-
else {
|
|
556
|
-
const server = findGroundingMcp(manifest);
|
|
557
|
-
ledger = server
|
|
558
|
-
? realLedgerClient(server, opts)
|
|
559
|
-
: degradedLedgerClient("grounding-mcp not declared in manifest", stderr, opts.hookName);
|
|
560
|
-
}
|
|
561
|
-
// For the SESSION_ID builtin we keep the empty-string fallback rather
|
|
562
|
-
// than routing through resolveSessionId (which lands on "default").
|
|
563
|
-
// Routing through "default" would silently bake the literal string
|
|
564
|
-
// into ledger_tag templates like `dogfood:${SESSION_ID}` →
|
|
565
|
-
// `dogfood:default`, which would substring-match anything in the
|
|
566
|
-
// shared "default" session — a worse failure mode than the current
|
|
567
|
-
// empty substitution. The empty path produces tags like `dogfood:`
|
|
568
|
-
// (substituteTemplate treats the registered "" value as a successful
|
|
569
|
-
// substitution since the key is hasOwnProperty-present, so this is
|
|
570
|
-
// NOT marked warn-degraded by the requires evaluator); the ledger
|
|
571
|
-
// query for that fire also lands on the resolveSessionId-derived
|
|
572
|
-
// session, so the asymmetry is acceptable in practice. Do NOT
|
|
573
|
-
// "simplify" by routing this through resolveSessionId.
|
|
574
|
-
// The env fallback is still applied so a Claude-Code-driven hook with
|
|
575
|
-
// a stripped event can pick up the active session.
|
|
576
|
-
const eventSessionId = typeof event.session_id === "string" ? event.session_id : undefined;
|
|
577
|
-
// Env-var fallback chain: $CLAUDE_CODE_SESSION_ID first (the var Claude
|
|
578
|
-
// Code actually exports), then legacy $CLAUDE_SESSION_ID. Matches the
|
|
579
|
-
// resolver precedence in src/runtime/session-id.ts.
|
|
580
|
-
const builtinSessionId = eventSessionId ??
|
|
581
|
-
process.env.CLAUDE_CODE_SESSION_ID ??
|
|
582
|
-
process.env.CLAUDE_SESSION_ID ??
|
|
583
|
-
"";
|
|
584
|
-
// REPO / BRANCH are derived from the tool event's cwd so that per-repo
|
|
585
|
-
// and per-branch ledger tags (`preflight:${REPO}`, `preflight:${BRANCH}`)
|
|
586
|
-
// actually namespace — they were previously read from HARNESS_REPO /
|
|
587
|
-
// HARNESS_BRANCH env vars that nothing sets, collapsing every tag to the
|
|
588
|
-
// literal `preflight:`. An explicit env var still wins: it is the
|
|
589
|
-
// operator's deliberate override of the derived value.
|
|
590
|
-
//
|
|
591
|
-
// `cwdGitContext` below is the DEFAULT / FALLBACK builtins basis, not
|
|
592
|
-
// the only one used anymore (task `98ad072f`, T-003). A command that
|
|
593
|
-
// explicitly names a different repository (`git -C <B>`, `env -C <B>`,
|
|
594
|
-
// a leading `cd <B> &&`) still resolves `builtins.REPO`/`.BRANCH` (and
|
|
595
|
-
// `currentHeadSha`, below) from THIS cwd for the majority of policies —
|
|
596
|
-
// any policy whose `requires:` doesn't reference `${REPO}`/`${BRANCH}`/
|
|
597
|
-
// `at_head` stays exactly on this cwd-only path, as does any policy
|
|
598
|
-
// whose own trigger-satisfying segment names no foreign target. Only a
|
|
599
|
-
// MATCHED policy that (a) uses `${REPO}`/`${BRANCH}`/`at_head` and (b)
|
|
600
|
-
// whose own `bash_match`-satisfying segment names a different,
|
|
601
|
-
// resolvable git repository gets a per-policy override — resolved
|
|
602
|
-
// lazily, inside `intercept()`, from the `commandSegments` view threaded
|
|
603
|
-
// below, never from this `cwdGitContext`. A prior attempt (run
|
|
604
|
-
// `2026-07-27-gate-target-repo-resolution`) tried a single global
|
|
605
|
-
// `targetDir` per event instead and was removed before shipping: three
|
|
606
|
-
// consecutive review rounds each found a different way one event-wide
|
|
607
|
-
// target leaked into the wrong evaluation (Risk Gate risk
|
|
608
|
-
// declassification; a push-gate multi-invocation fail-open; a non-git
|
|
609
|
-
// gated verb sharing a chain with a targeted git call). This
|
|
610
|
-
// `cwdGitContext` variable, and the `builtins` object built from it
|
|
611
|
-
// below, remain exactly what they were before this task — the resolved
|
|
612
|
-
// fallback every policy without a foreign attribution still uses,
|
|
613
|
-
// including the Risk Gate's own `resolverGit` further down, which
|
|
614
|
-
// this task does not touch at all.
|
|
615
|
-
const cwd = resolvePolicyCwd(event, opts.codexCommandCwd);
|
|
616
|
-
const cwdGitContext = resolveGitContext(cwd);
|
|
617
|
-
// Computed once here (not per-policy) so `intercept()` below can thread
|
|
618
|
-
// the SAME `NormalizedCommand` into every `bash_match` trigger check
|
|
619
|
-
// instead of each one recomputing it.
|
|
620
|
-
//
|
|
621
|
-
// Fix round 1, finding F4: derive this from `extractShellCommand(event)`
|
|
622
|
-
// — the SAME function `policyMatchesEvent`'s `bash_match` branch itself
|
|
623
|
-
// calls (`src/runtime/tool-name-aliases.ts`) — instead of the narrower
|
|
624
|
-
// `event.tool_name === "Bash"` + `readBashCommand(event.tool_input)` pair
|
|
625
|
-
// this used before, which only ever looked at `tool_input.command`. A
|
|
626
|
-
// Codex shell event (`tool_name: "shell"` / `"exec_command"` /
|
|
627
|
-
// `"functions.exec_command"`, which `expandToolNameAliases` maps onto
|
|
628
|
-
// `match: "Bash"` policies) commonly carries its command under
|
|
629
|
-
// `raw_input`/`input` rather than `tool_input`, or under a `cmd` key —
|
|
630
|
-
// `extractShellCommand` reads all of those, `readBashCommand` read none
|
|
631
|
-
// of them. Before this fix such an event still reached the `bash_match`
|
|
632
|
-
// branch (via alias expansion) but with no precomputed value, so
|
|
633
|
-
// `policyMatchesEvent`'s per-policy fallback ran BOTH normalisation
|
|
634
|
-
// passes once PER MATCHING POLICY instead of once per event.
|
|
635
|
-
// `isCodexShellTool` scopes the precompute to exactly the tool-name set
|
|
636
|
-
// `SHELL_ALIASES` expands (Bash plus the three Codex shell aliases), so
|
|
637
|
-
// this does not start normalising unrelated tool calls that merely
|
|
638
|
-
// happen to carry a `command`/`cmd`-named argument for some other
|
|
639
|
-
// purpose.
|
|
640
|
-
//
|
|
641
|
-
// Fix round 2: that set is NOT, as this comment previously claimed, the
|
|
642
|
-
// only way a `match: "Bash"` policy's `bash_match` branch can be
|
|
643
|
-
// reached. `trigger.match` is a SUBSTRING test over the expanded alias
|
|
644
|
-
// list, so `BashOutput` and `KillBash` — real Claude Code tool names —
|
|
645
|
-
// satisfy `match: "Bash"` too and do reach that branch (measured: both
|
|
646
|
-
// return `policyMatchesEvent === true`). They simply fall through to
|
|
647
|
-
// the per-policy fallback here, exactly as they did before this change,
|
|
648
|
-
// so nothing is gated differently; the precompute just does not
|
|
649
|
-
// de-duplicate for them.
|
|
650
|
-
//
|
|
651
|
-
// This also makes the precomputed value and the matcher's own value
|
|
652
|
-
// PROVABLY consistent for the common case: both are now the same
|
|
653
|
-
// `extractShellCommand(event)` call, so the latent
|
|
654
|
-
// `raw_input`/`input`/`cmd` divergence `InterceptOptions.normalizedCommand`'s
|
|
655
|
-
// own JSDoc warns about (nothing verifies the threaded value came from
|
|
656
|
-
// the event's real command) can no longer actually diverge for a
|
|
657
|
-
// `tool_name` in `SHELL_ALIASES` — the one production call site
|
|
658
|
-
// (`runInterceptCli`) derives both from the identical event via the
|
|
659
|
-
// identical function. It remains an unenforced INVARIANT, not a checked
|
|
660
|
-
// one: nothing stops a future second caller from threading a mismatched
|
|
661
|
-
// value in, and `event.tool_input`/`raw_input`/`input` could still
|
|
662
|
-
// change between the moment this is read and any later read of the same
|
|
663
|
-
// event object, however unlikely in practice.
|
|
664
|
-
//
|
|
665
|
-
// TWO behavioural differences, both checked (fix round 2 corrects the
|
|
666
|
-
// prior wording, which named only the first):
|
|
667
|
-
//
|
|
668
|
-
// 1. `readBashCommand` required a NON-EMPTY string; `extractShellCommand`
|
|
669
|
-
// accepts an empty one. An empty command now becomes
|
|
670
|
-
// `bashCommand = ""` instead of `null`, so `normalizedCommand`
|
|
671
|
-
// becomes a defined `normalizeCommand("") = {normalized: "",
|
|
672
|
-
// truncated: false, ...}` instead of staying `undefined` —
|
|
673
|
-
// behaviourally identical either way (a pure function of the empty
|
|
674
|
-
// string, now computed once instead of falling back per policy), and
|
|
675
|
-
// `truncated` can never be `true` for an empty string, so the
|
|
676
|
-
// `MAX_NORMALIZE_LENGTH` stderr line just below cannot fire from it.
|
|
677
|
-
//
|
|
678
|
-
// 2. That stderr line's SCOPE widened. Under the old ternary
|
|
679
|
-
// `bashCommand` was unconditionally `null` for any non-`Bash`
|
|
680
|
-
// `tool_name`, so `normalizedCommand` stayed `undefined` and the
|
|
681
|
-
// guard could never fire for a Codex shell event, nor for a command
|
|
682
|
-
// carried under `raw_input`/`input` or a `cmd` key. Measured at
|
|
683
|
-
// HEAD: a `tool_name: "shell"` event with a 100,001-char
|
|
684
|
-
// `raw_input.cmd` now emits the line. The direction is
|
|
685
|
-
// safety-positive — a previously SILENT loss of normalised-form
|
|
686
|
-
// coverage on those events is now reported — but it is a real change
|
|
687
|
-
// in observable output, so it is named here rather than implied.
|
|
688
|
-
const bashCommand = isCodexShellTool(event.tool_name) ? extractShellCommand(event) : null;
|
|
689
|
-
const normalizedCommand = bashCommand === null ? undefined : normalizeCommand(bashCommand);
|
|
690
|
-
// Per-segment view for `intercept()`'s per-policy attribution (task
|
|
691
|
-
// `98ad072f`, T-003) — deferred behind a memoised thunk, mirroring
|
|
692
|
-
// `ampNormalizedCommandThunk` below (D-015 fix round, run
|
|
693
|
-
// 2026-08-02-per-repo-gate-scoping-redesign, correcting this comment's
|
|
694
|
-
// own prior claim). CORRECTED: this is NOT "the same class as
|
|
695
|
-
// `normalizeCommand`'s own eager call just above" — `segmentViewOf` runs
|
|
696
|
-
// its OWN full segmentation walk over `bashCommand` (`command-
|
|
697
|
-
// normalize.ts`'s `segmentAndCanonicalize`, a SEPARATE call from the one
|
|
698
|
-
// `normalizeCommand` just made on the SAME string), and computing it
|
|
699
|
-
// unconditionally, for every Bash event, regardless of whether any
|
|
700
|
-
// policy in the manifest even uses `${REPO}`/`${BRANCH}`/`at_head`, was
|
|
701
|
-
// measured as a real, avoidable second pass — +206% at
|
|
702
|
-
// `MAX_NORMALIZE_LENGTH` (2.73 → 8.36 ms; see `segmentViewOf`'s own doc
|
|
703
|
-
// comment in `command-normalize.ts`). Threading it as a self-memoising
|
|
704
|
-
// thunk instead — the exact shape `ampNormalizedCommandThunk` below
|
|
705
|
-
// already uses for the same reason — lets `intercept()`'s
|
|
706
|
-
// `usesPerRepoBuiltins` gate decide whether the walk happens at all: a
|
|
707
|
-
// manifest with none of those policies never calls `segmentViewOf`, and
|
|
708
|
-
// a manifest that DOES still only pays the cost once per event, on
|
|
709
|
-
// first use, not once per Bash event regardless of need.
|
|
710
|
-
let commandSegmentsCache;
|
|
711
|
-
const commandSegmentsThunk = bashCommand === null
|
|
712
|
-
? undefined
|
|
713
|
-
: () => {
|
|
714
|
-
if (commandSegmentsCache === undefined) {
|
|
715
|
-
commandSegmentsCache = segmentViewOf(bashCommand);
|
|
716
|
-
}
|
|
717
|
-
return commandSegmentsCache;
|
|
718
|
-
};
|
|
719
|
-
// Memoised thunk for the ampersand-aware SECOND normalisation pass
|
|
720
|
-
// (task aabbad63). Unlike `normalizedCommand` above — eagerly computed
|
|
721
|
-
// once per Bash event, because `policyMatchesEvent`'s first two arms
|
|
722
|
-
// may need it on every raw-miss — the amp-aware pass is consulted only
|
|
723
|
-
// as a THIRD, additional arm, and only when a policy's regex has
|
|
724
|
-
// already missed BOTH the raw command and `normalizedCommand`; most
|
|
725
|
-
// events never reach it at all. Computing it eagerly here would pay
|
|
726
|
-
// its segmentation cost on the common path for every single Bash call
|
|
727
|
-
// inside the ~1000ms hook budget, whether or not any policy ends up
|
|
728
|
-
// needing it. Wrapping it in a lazily-evaluated, self-memoising thunk
|
|
729
|
-
// instead lets `policyMatchesEvent` call it from inside the manifest-
|
|
730
|
-
// wide `matching` loop below (once per policy that still hasn't
|
|
731
|
-
// matched) while the actual segmentation work happens AT MOST ONCE per
|
|
732
|
-
// event — the same "computed once per event, not once per policy"
|
|
733
|
-
// discipline `normalizedCommand` already has, just deferred until
|
|
734
|
-
// first use instead of forced up front.
|
|
735
|
-
let ampNormalizedCommandCache;
|
|
736
|
-
const ampNormalizedCommandThunk = bashCommand === null
|
|
737
|
-
? undefined
|
|
738
|
-
: () => (ampNormalizedCommandCache ??= normalizeCommandAmpAware(bashCommand));
|
|
739
|
-
// Memoised thunk for the quote-aware THIRD normalisation pass (task
|
|
740
|
-
// cf3dff51) — the SAME shape as `ampNormalizedCommandThunk` immediately
|
|
741
|
-
// above, for the same reason: `policyMatchesEvent`'s fourth arm calls
|
|
742
|
-
// this only when a policy's regex has already missed the raw command,
|
|
743
|
-
// `normalizedCommand`, AND the amp-aware pass, so most events never
|
|
744
|
-
// reach it at all; computing it eagerly here would pay its
|
|
745
|
-
// segmentation cost on the common path for every Bash call regardless
|
|
746
|
-
// of whether any policy ends up needing it.
|
|
747
|
-
let quoteNormalizedCommandCache;
|
|
748
|
-
const quoteNormalizedCommandThunk = bashCommand === null
|
|
749
|
-
? undefined
|
|
750
|
-
: () => (quoteNormalizedCommandCache ??= normalizeCommandQuoteAware(bashCommand));
|
|
751
|
-
// Above `MAX_NORMALIZE_LENGTH`, `normalizeCommand` skips normalisation
|
|
752
|
-
// entirely and `truncated` comes back `true`. Raw matching still
|
|
753
|
-
// applies regardless (`policyMatchesEvent`'s raw-OR-normalised-OR-amp-
|
|
754
|
-
// normalised-OR-quote-normalised construction), so an oversized command
|
|
755
|
-
// only loses the ADDITIONAL normalised-form coverage — but that skip
|
|
756
|
-
// must not be silent, so one stderr line reports it here, keeping the
|
|
757
|
-
// module itself pure and I/O-free. This one line also covers the
|
|
758
|
-
// amp-aware SECOND pass (`normalizeCommandAmpAware`, task `aabbad63`,
|
|
759
|
-
// fix round 1 finding F6) and the quote-aware THIRD pass
|
|
760
|
-
// (`normalizeCommandQuoteAware`, task `cf3dff51`): both share the
|
|
761
|
-
// identical `MAX_NORMALIZE_LENGTH` bound over the identical
|
|
762
|
-
// `bashCommand`, so neither one's own `truncated` can ever disagree
|
|
763
|
-
// with `normalizedCommand`'s for this call — no separate stderr line
|
|
764
|
-
// names either pass's own skip, nor does one need to.
|
|
765
|
-
if (normalizedCommand?.truncated === true) {
|
|
766
|
-
stderr.write(`harness policy intercept${hookSuffix(opts.hookName)}: Bash command exceeds ${MAX_NORMALIZE_LENGTH} chars; normalised-form matching skipped for this call (raw match only)\n`);
|
|
767
|
-
}
|
|
768
|
-
const builtins = {
|
|
769
|
-
SESSION_ID: builtinSessionId,
|
|
770
|
-
REPO: process.env.HARNESS_REPO ?? cwdGitContext.repo,
|
|
771
|
-
BRANCH: process.env.HARNESS_BRANCH ?? cwdGitContext.branch,
|
|
772
|
-
TOOL_NAME: typeof event.tool_name === "string" ? event.tool_name : "",
|
|
773
|
-
CWD: cwd,
|
|
774
|
-
};
|
|
775
|
-
// Risk Gate ambient context — resolved only when the manifest
|
|
776
|
-
// declares a `when:`-bearing policy, so a pure Phase-4 manifest pays
|
|
777
|
-
// no kube-config read. `intercept()` applies the same gate; this just
|
|
778
|
-
// avoids the host I/O when nothing would consume it.
|
|
779
|
-
let riskContext;
|
|
780
|
-
if (manifest.policies.some((p) => p.when !== undefined)) {
|
|
781
|
-
const kube = opts.kubeContext !== undefined || opts.kubeNamespace !== undefined
|
|
782
|
-
? {
|
|
783
|
-
context: opts.kubeContext ?? "",
|
|
784
|
-
namespace: opts.kubeNamespace ?? "",
|
|
785
|
-
}
|
|
786
|
-
: resolveKubeContext();
|
|
787
|
-
// Three POSIX Bash idioms (`VAR=value command`, `cd <path> &&
|
|
788
|
-
// command`, and — since task 341e024b — `git switch|checkout
|
|
789
|
-
// <branch> && command`) were invisible to the resolver before: the
|
|
790
|
-
// env resolver read `process.env` and the branch resolver read
|
|
791
|
-
// `.git/HEAD` under the hook's starting cwd, so a prod signal
|
|
792
|
-
// smuggled through any of the three idioms silently passed the
|
|
793
|
-
// gate. Parse the leading prefix once from the Bash command and
|
|
794
|
-
// merge the result into the resolver inputs only. ${CWD} always
|
|
795
|
-
// names the hook's own cwd; ${REPO}/${BRANCH} (`cwdGitContext`
|
|
796
|
-
// above, feeding `builtins`) are cwd-only too — see the comment
|
|
797
|
-
// above `cwdGitContext`'s declaration. The Risk Gate resolver's git
|
|
798
|
-
// base, `resolverGit` (risk-envelope-enrichment.ts), is its own narrow parse: it
|
|
799
|
-
// recognises only a leading `cd`, never `git -C` / `--work-tree` /
|
|
800
|
-
// `--git-dir` / `env -C`. Bottom line: `resolverGit` must never read
|
|
801
|
-
// anything other than `cwdGitContext`, or the leading-`cd` target
|
|
802
|
-
// within THIS same narrow parse — this feeds `when:`-clause risk
|
|
803
|
-
// classification, not ledger-tag namespacing.
|
|
804
|
-
//
|
|
805
|
-
// G5 (review round 2, 2026-07-27): that leading-`cd` parse is itself
|
|
806
|
-
// still a live declassification lever, PRE-EXISTING and NOT changed
|
|
807
|
-
// by this run — identical on the shipped 0.42.0 control, so this is
|
|
808
|
-
// documentation of a known asymmetry, not a new gap. From a repo on
|
|
809
|
-
// `main`, `rm -rf /data` BLOCKS, but `cd <repo-on-feature/x> && rm -rf
|
|
810
|
-
// /data` is ALLOWED: the `cd` target makes the resolver read
|
|
811
|
-
// feature/x's branch instead of cwd's `main`, so `branch_patterns:
|
|
812
|
-
// [main]` no longer matches. The asymmetry cuts both ways and neither
|
|
813
|
-
// direction is fixed here: a leading `cd` can make a prod signal
|
|
814
|
-
// VISIBLE that cwd alone would have missed (cwd on a feature branch,
|
|
815
|
-
// `cd` into a repo actually on `main`) just as readily as it can make
|
|
816
|
-
// one INVISIBLE (cwd on `main`, `cd` into a repo on a feature
|
|
817
|
-
// branch, as in the example above) — `resolverGit` trusts whichever
|
|
818
|
-
// of the two the command happens to name, with no independent check
|
|
819
|
-
// that the `cd` target is the repo the destructive command actually
|
|
820
|
-
// runs against. The `git switch`/`checkout` merge in that helper
|
|
821
|
-
// (`gitForRisk`, `applyBranchSwitchUpgrade`, same module) deliberately does
|
|
822
|
-
// NOT share this bidirectional-risk shape: it is a SEPARATE,
|
|
823
|
-
// upgrade-only step layered on top of `resolverGit`, not a change to
|
|
824
|
-
// `resolverGit` itself — a switch away from a production branch can
|
|
825
|
-
// never downgrade what `resolverGit` (cwd- or cd-based) already
|
|
826
|
-
// resolved.
|
|
827
|
-
const riskUser = safeOs(() => os.userInfo().username);
|
|
828
|
-
const riskHost = safeOs(() => os.hostname());
|
|
829
|
-
// The leading-prefix parse and the three merges it feeds (inline
|
|
830
|
-
// `VAR=value` env, leading `cd`, leading `git switch|checkout`)
|
|
831
|
-
// live in `resolveBashPrefixEnrichment`, shared with `harness
|
|
832
|
-
// explain-policy` so the debug verb cannot drift from this hook.
|
|
833
|
-
// The kubectl-target merge below stays hook-only.
|
|
834
|
-
const { bashPrefix, riskBashCommand, git: gitForRisk, env: resolverEnv } = resolveBashPrefixEnrichment({
|
|
835
|
-
event,
|
|
836
|
-
manifest,
|
|
837
|
-
cwd,
|
|
838
|
-
cwdGitContext,
|
|
839
|
-
env: opts.env ?? process.env,
|
|
840
|
-
kubeContext: kube.context,
|
|
841
|
-
kubeNamespace: kube.namespace,
|
|
842
|
-
user: riskUser,
|
|
843
|
-
host: riskHost,
|
|
844
|
-
now: opts.now,
|
|
845
|
-
});
|
|
846
|
-
// Explicit `--context`/`--namespace`/`-n` on a `kubectl ...`
|
|
847
|
-
// invocation (task a7eb1a71). Fed the REMAINDER after `bashPrefix`
|
|
848
|
-
// consumed a leading `cd <path> &&` / `VAR=value` / `git switch
|
|
849
|
-
// <branch> &&` prefix, not the raw command, so a wrapped invocation
|
|
850
|
-
// like `cd /tmp && kubectl ... --context x` is still covered; see
|
|
851
|
-
// `parseKubectlTarget`'s own doc comment for the narrow,
|
|
852
|
-
// command-head-anchored scope this is otherwise kept to, and for
|
|
853
|
-
// the forms still NOT unwrapped (sudo/time/env wrappers, a kubectl
|
|
854
|
-
// that is not the first chained segment, a piped kubectl).
|
|
855
|
-
const kubectlSubject = riskBashCommand === null ? null : riskBashCommand.slice(bashPrefix?.remainderStart ?? 0);
|
|
856
|
-
const kubectlTarget = kubectlSubject === null ? null : parseKubectlTarget(kubectlSubject);
|
|
857
|
-
// CONFLICT PRIORITY (task a7eb1a71): an explicit kubectl
|
|
858
|
-
// --context/--namespace/-n is merged on top of the AMBIENT kube
|
|
859
|
-
// state (`kube`), on top of `gitForRisk` above (the git context
|
|
860
|
-
// AFTER the branch-switch upgrade), upgrade-only, exactly the same
|
|
861
|
-
// asymmetric shape the branch-switch merge itself uses. Command
|
|
862
|
-
// text can only raise the resolved environment toward production,
|
|
863
|
-
// never lower an already-resolved production; see
|
|
864
|
-
// `applyKubeTargetUpgrade`'s own doc comment for the measured
|
|
865
|
-
// downgrade this fixes.
|
|
866
|
-
const kubeForRisk = applyKubeTargetUpgrade(event, manifest, cwd, gitForRisk, { context: kube.context, namespace: kube.namespace }, kubectlTarget, resolverEnv, riskUser, riskHost, opts.now);
|
|
867
|
-
riskContext = {
|
|
868
|
-
git: gitForRisk,
|
|
869
|
-
cwd,
|
|
870
|
-
user: riskUser,
|
|
871
|
-
host: riskHost,
|
|
872
|
-
env: resolverEnv,
|
|
873
|
-
kubeContext: kubeForRisk.context,
|
|
874
|
-
kubeNamespace: kubeForRisk.namespace,
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
let result;
|
|
878
|
-
try {
|
|
879
|
-
result = await intercept({
|
|
880
|
-
manifest,
|
|
881
|
-
event,
|
|
882
|
-
ledger,
|
|
883
|
-
builtins,
|
|
884
|
-
stderr,
|
|
885
|
-
...(opts.ledgerTimeoutMs !== undefined && { ledgerTimeoutMs: opts.ledgerTimeoutMs }),
|
|
886
|
-
...(opts.now && { now: opts.now }),
|
|
887
|
-
...(cwdGitContext.sha.length > 0 && { currentHeadSha: cwdGitContext.sha }),
|
|
888
|
-
...(riskContext && { riskContext }),
|
|
889
|
-
...(normalizedCommand && { normalizedCommand }),
|
|
890
|
-
...(ampNormalizedCommandThunk && { ampNormalizedCommandThunk }),
|
|
891
|
-
...(quoteNormalizedCommandThunk && { quoteNormalizedCommandThunk }),
|
|
892
|
-
...(commandSegmentsThunk && { commandSegmentsThunk }),
|
|
893
|
-
...(process.env.HARNESS_REPO !== undefined && { repoOverridden: true }),
|
|
894
|
-
...(process.env.HARNESS_BRANCH !== undefined && { branchOverridden: true }),
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
finally {
|
|
898
|
-
// Tear down the pooled grounding-mcp session (no-op for injected test
|
|
899
|
-
// doubles and the degraded client). All queries/records are awaited
|
|
900
|
-
// inside intercept(), so nothing is in flight here.
|
|
901
|
-
ledger.dispose?.();
|
|
902
|
-
}
|
|
903
|
-
// Stage the session id for a later arg-less `harness approve risk`
|
|
904
|
-
// whenever the FIRST blocking decision is `require_approval`. Mirrors
|
|
905
|
-
// the Understanding Gate hook (src/cli/pack/hook-pre-tool-use.ts):
|
|
906
|
-
// the producer side knows the live session id (it just received it on
|
|
907
|
-
// the hook event), but `harness approve risk` runs from the operator's
|
|
908
|
-
// `!`-shell where `$CLAUDE_SESSION_ID` is unset, so it has to read the
|
|
909
|
-
// marker. `deny` and `deny-degraded` decisions are deliberately not
|
|
910
|
-
// staged: `harness approve risk` cannot unblock either (a deny is a
|
|
911
|
-
// real verdict, a deny-degraded means the evidence source is
|
|
912
|
-
// unreadable, so no approval tag could even be read), and writing a
|
|
913
|
-
// marker the verb cannot act on would just lie about the
|
|
914
|
-
// recoverability of the block. The "first blocking decision" check
|
|
915
|
-
// (rather than `.some()`) matters when two `when:`-bearing policies
|
|
916
|
-
// fire on the same event: the runtime's `intercept()` picks the first
|
|
917
|
-
// blocking decision (its `isBlockingDecision`, imported here so the
|
|
918
|
-
// two surfaces cannot drift — review 2026-08-08), so a blocking-first
|
|
919
|
-
// / `require_approval`-second order produces an unrecoverable block;
|
|
920
|
-
// we mustn't stage in that case either.
|
|
921
|
-
//
|
|
922
|
-
// Best-effort: a staging-write failure must never escalate a gate block
|
|
923
|
-
// into a thrown hook error. `resolveGeneratedDir` is skipped (along
|
|
924
|
-
// with the write) when no manifest path could be resolved — tests
|
|
925
|
-
// exercise the policy logic with injected manifests and no on-disk
|
|
926
|
-
// path, and the test path is also the only place an empty
|
|
927
|
-
// `eventSessionId` is interesting.
|
|
928
|
-
const firstBlocking = result.decisions.find(isBlockingDecision);
|
|
929
|
-
if (result.blockJson &&
|
|
930
|
-
eventSessionId !== undefined &&
|
|
931
|
-
eventSessionId.length > 0 &&
|
|
932
|
-
firstBlocking?.outcome === "require_approval" &&
|
|
933
|
-
// An empty-identifier decision never reads the ledger, so no approval
|
|
934
|
-
// tag could unblock it: staging a marker `harness approve risk` cannot
|
|
935
|
-
// act on would misstate the recoverability of the block.
|
|
936
|
-
firstBlocking.emptyIdentifier === undefined) {
|
|
937
|
-
const generatedDir = opts.generatedDir
|
|
938
|
-
?? (manifestPath !== undefined
|
|
939
|
-
? resolveGeneratedDir({
|
|
940
|
-
...(opts.homeDir !== undefined ? { homeDir: opts.homeDir } : {}),
|
|
941
|
-
manifestPath,
|
|
942
|
-
})
|
|
943
|
-
: undefined);
|
|
944
|
-
if (generatedDir !== undefined) {
|
|
945
|
-
try {
|
|
946
|
-
writePendingApproval(generatedDir, eventSessionId);
|
|
947
|
-
}
|
|
948
|
-
catch {
|
|
949
|
-
/* best-effort; the block below proceeds regardless */
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
if (result.blockJson) {
|
|
954
|
-
stdout.write(`${JSON.stringify(result.blockJson)}\n`);
|
|
955
|
-
}
|
|
956
|
-
// No-match diagnostic. Always (not gated on --verbose) when the
|
|
957
|
-
// manifest has at least one policy but none of them matched this
|
|
958
|
-
// event. Catches the common debug footgun where an operator probes
|
|
959
|
-
// `harness policy intercept` by hand and forgets `hook_event_name`,
|
|
960
|
-
// making the engine return exit 0 + empty stdout. The probe then
|
|
961
|
-
// looks like "policy did not load", when in fact the trigger filter
|
|
962
|
-
// simply rejected the input. Emits to stderr so the Claude Code
|
|
963
|
-
// hook contract on stdout is preserved.
|
|
964
|
-
if (result.decisions.length === 0 && manifest.policies.length > 0) {
|
|
965
|
-
stderr.write(formatNoMatchHint(event, manifest, opts.hookName));
|
|
966
|
-
}
|
|
967
|
-
// Degraded-deny operator hint. Default-verbosity, ONE line per event:
|
|
968
|
-
// the envelope deliberately tells the agent only to involve the
|
|
969
|
-
// operator, and the full diagnostic (which names the fail_open
|
|
970
|
-
// opt-out) is verbose-only — without this line an operator whose
|
|
971
|
-
// fleet wedges on a degraded ledger has no default-verbosity pointer
|
|
972
|
-
// at all (review 2026-08-08, round 2). Two deliberate properties:
|
|
973
|
-
// - It carries the decision's OWN reason (sanitised: it can embed
|
|
974
|
-
// grounding-mcp subprocess output) instead of asserting a ledger
|
|
975
|
-
// fault — deny-degraded has five causes and only one of them is
|
|
976
|
-
// the ledger; a hardcoded "ledger unreadable" sent operators of
|
|
977
|
-
// the other four down a false doctor-path (round 3, medium).
|
|
978
|
-
// - It does NOT name the opt-out knob: hook stderr can be surfaced
|
|
979
|
-
// to the model in some harness configurations, so the default
|
|
980
|
-
// channel points at the operator docs and the verbose diagnostic
|
|
981
|
-
// instead of carrying the disable recipe (deviation from round
|
|
982
|
-
// 2's suggested fix, recorded in the run's 03-decisions.md).
|
|
983
|
-
// Suppressed under --verbose, where formatDecisionDiagnostic already
|
|
984
|
-
// prints the same reason with more context.
|
|
985
|
-
const firstDegradedDeny = result.decisions.find((d) => d.outcome === "deny-degraded");
|
|
986
|
-
if (firstDegradedDeny && !verbose) {
|
|
987
|
-
stderr.write(`harness policy intercept${hookSuffix(opts.hookName)}: ${firstDegradedDeny.policyName}: ` +
|
|
988
|
-
`deny-degraded (evidence could not be evaluated: ` +
|
|
989
|
-
`${sanitizeEnvelopeReason(firstDegradedDeny.reason)}); failing closed. ` +
|
|
990
|
-
`Operator recovery: check the reason above, then harness doctor if it names ` +
|
|
991
|
-
`the ledger; details and the availability opt-out: docs/risk-gate.md ` +
|
|
992
|
-
`("Degraded mode") or rerun with HARNESS_POLICY_VERBOSE=1\n`);
|
|
993
|
-
}
|
|
994
|
-
if (verbose) {
|
|
995
|
-
for (const decision of result.decisions) {
|
|
996
|
-
if (decision.outcome === "allow")
|
|
997
|
-
continue;
|
|
998
|
-
stderr.write(formatDecisionDiagnostic(decision, opts.hookName));
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
return {
|
|
1002
|
-
exitCode: 0,
|
|
1003
|
-
decisions: result.decisions,
|
|
1004
|
-
blocked: result.blockJson !== null,
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
1007
|
-
function formatNoMatchHint(event, manifest, hookName) {
|
|
1008
|
-
const observedEvent = typeof event.hook_event_name === "string" && event.hook_event_name.length > 0
|
|
1009
|
-
? `"${event.hook_event_name}"`
|
|
1010
|
-
: "(missing)";
|
|
1011
|
-
const observedTool = typeof event.tool_name === "string" && event.tool_name.length > 0
|
|
1012
|
-
? `"${event.tool_name}"`
|
|
1013
|
-
: "(missing)";
|
|
1014
|
-
const registeredEvents = Array.from(new Set(manifest.policies.map((p) => p.trigger.event))).sort();
|
|
1015
|
-
return (`harness policy intercept${hookSuffix(hookName)}: no policy matched event ` +
|
|
1016
|
-
`hook_event_name=${observedEvent} tool_name=${observedTool} ` +
|
|
1017
|
-
`(registered policy events: ${registeredEvents.join(", ")}). ` +
|
|
1018
|
-
`If probing by hand, ensure stdin includes hook_event_name (e.g. "PreToolUse" for tool gates).\n`);
|
|
1019
|
-
}
|
|
1020
|
-
//# sourceMappingURL=intercept.js.map
|