@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,1313 +0,0 @@
|
|
|
1
|
-
// Phase 4 #5 — runtime hook interceptor + policy_decision audit log.
|
|
2
|
-
//
|
|
3
|
-
// Pure orchestration: takes a parsed event + a ledger client + the manifest,
|
|
4
|
-
// runs every matching policy through the Phase 4 #1/#2/#3 pipeline, returns
|
|
5
|
-
// the decisions and the Claude Code deny-JSON (or null when all allow).
|
|
6
|
-
// Side effects (stdin, stdout, ledger I/O) live in the thin CLI entrypoint
|
|
7
|
-
// that wraps this.
|
|
8
|
-
import * as fs from "node:fs";
|
|
9
|
-
import * as path from "node:path";
|
|
10
|
-
import { evaluateExtract, evaluateRequires, firstInputMatchMismatch, parseDurationSeconds, substituteTemplate, } from "../policies/index.js";
|
|
11
|
-
import { renderProducers } from "../policies/producers.js";
|
|
12
|
-
import { buildActionEnvelope } from "./action-envelope.js";
|
|
13
|
-
import { renderAgentFacing } from "./agent-facing.js";
|
|
14
|
-
import { normalizeCommand, normalizeCommandAmpAware, normalizeCommandQuoteAware, segmentViewOf, } from "./command-normalize.js";
|
|
15
|
-
import { resolveEnvironment, } from "./environment-resolver.js";
|
|
16
|
-
import { resolveDeletionTarget, } from "./deletion-target-resolve.js";
|
|
17
|
-
import { DEFAULT_SAFE_DELETION_ROOTS } from "../schema/risk.js";
|
|
18
|
-
import { resolveGitContext } from "./git-context.js";
|
|
19
|
-
import { POLICY_DECISION_TYPE } from "../io/ledger-record.js";
|
|
20
|
-
import { classifyRisk } from "./risk-classifier.js";
|
|
21
|
-
import { resolveSessionId } from "./session-id.js";
|
|
22
|
-
import { expandToolNameAliases, extractShellCommand, } from "./tool-name-aliases.js";
|
|
23
|
-
import { evaluateWhen, UNCLASSIFIED_FALLBACK_SEVERITY } from "./when-eval.js";
|
|
24
|
-
/**
|
|
25
|
-
* The sentence PREPENDED to a `ux:`-declared policy's `cannot:` text when
|
|
26
|
-
* the deny was caused by the unclassified fallback rather than a real
|
|
27
|
-
* classification (task 2929c5b7).
|
|
28
|
-
*
|
|
29
|
-
* Everything variable in it is interpolated: the RESOLVED environment
|
|
30
|
-
* name and the policy's OWN declared threshold. Hard-coding "production"
|
|
31
|
-
* and "critical" made both halves wrong for an unscoped
|
|
32
|
-
* `severity_at_least: high` policy on a feature branch, which reported a
|
|
33
|
-
* production context that had not been resolved and a critical-severity
|
|
34
|
-
* comparison that never ran. The fallback rung itself comes from
|
|
35
|
-
* `when-eval.ts`'s exported constant, not a second literal here.
|
|
36
|
-
*
|
|
37
|
-
* A policy whose `when:` block declares no `severity_at_least` can still
|
|
38
|
-
* set the flag (via `risk.category_in` / `action.reversible`, which keep
|
|
39
|
-
* the blanket "unknown is not safe" fallback), so the threshold-free
|
|
40
|
-
* wording is a real case, not a defensive branch.
|
|
41
|
-
*/
|
|
42
|
-
function unclassifiedFallbackPrefix(environmentName, threshold) {
|
|
43
|
-
const env = environmentName ?? "unknown";
|
|
44
|
-
const article = /^[aeiou]/i.test(env) ? "an" : "a";
|
|
45
|
-
const lead = `This is an unclassified action in ${article} ${env} context: no risk classifier pattern recognized it, so`;
|
|
46
|
-
return threshold === undefined
|
|
47
|
-
? `${lead} the fail-closed unclassified rule satisfied this policy's when: clause, rather than a genuine risk classification.`
|
|
48
|
-
: `${lead} the fail-closed severity fallback (treated as ${UNCLASSIFIED_FALLBACK_SEVERITY}) satisfied this policy's severity_at_least: ${threshold}, rather than a genuine ${threshold}-severity match.`;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Build the Action Envelope for an event and run it through the Risk
|
|
52
|
-
* Classifier (#3) and Context Resolver (#4). Pure: every host fact
|
|
53
|
-
* arrives via `riskContext`; when it is absent the envelope is built
|
|
54
|
-
* from the event alone (unclassified risk, `unknown` environment).
|
|
55
|
-
*/
|
|
56
|
-
function enrichEnvelope(manifest, event, riskContext, now) {
|
|
57
|
-
const rc = riskContext;
|
|
58
|
-
const envelope = buildActionEnvelope(event, {
|
|
59
|
-
cwd: rc?.cwd ?? (typeof event.cwd === "string" ? event.cwd : ""),
|
|
60
|
-
git: rc?.git ?? { repo: "", branch: "", sha: "" },
|
|
61
|
-
user: rc?.user ?? "",
|
|
62
|
-
host: rc?.host ?? "",
|
|
63
|
-
now: now ?? new Date(),
|
|
64
|
-
});
|
|
65
|
-
const risk = classifyRisk(envelope, manifest.risk.classifiers);
|
|
66
|
-
const environment = resolveEnvironment(envelope, manifest.environments.resolvers, {
|
|
67
|
-
env: rc?.env ?? {},
|
|
68
|
-
kubeContext: rc?.kubeContext ?? "",
|
|
69
|
-
kubeNamespace: rc?.kubeNamespace ?? "",
|
|
70
|
-
});
|
|
71
|
-
// Static deletion-target resolution (task d03af8f6) needs only the raw
|
|
72
|
-
// command — unlike the environment resolver above, it deliberately
|
|
73
|
-
// does not consult `riskContext` (cwd, env, kube): a relative target is
|
|
74
|
-
// UNRESOLVABLE by design, not resolved against ambient cwd. See
|
|
75
|
-
// `deletion-target-resolve.ts`'s module doc for the full rationale.
|
|
76
|
-
const deletionShellCommand = extractShellCommand({ raw_input: envelope.raw_input });
|
|
77
|
-
const deletionTarget = deletionShellCommand === null
|
|
78
|
-
? null
|
|
79
|
-
: resolveDeletionTarget(deletionShellCommand,
|
|
80
|
-
// Defensive fallback (not just the schema `.default()`): a
|
|
81
|
-
// hand-built `Manifest` test fixture that constructs `risk:`
|
|
82
|
-
// directly, bypassing `RiskSchema.parse`, may omit this field
|
|
83
|
-
// entirely — never trust it present.
|
|
84
|
-
manifest.risk.safe_deletion_roots ?? DEFAULT_SAFE_DELETION_ROOTS);
|
|
85
|
-
return { risk, environment, deletionTarget };
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Does a policy's `trigger:` match this event? This is the WHICH-tool-
|
|
89
|
-
* calls filter; the WHETHER-it-applies filter is `policy.when:`,
|
|
90
|
-
* evaluated separately (`evaluateWhen`). A policy fires only when both
|
|
91
|
-
* hold. Exported so `harness explain-policy` can report the trigger
|
|
92
|
-
* verdict on its own.
|
|
93
|
-
*
|
|
94
|
-
* `precomputedNormalizedCommand` is an optional caller-supplied
|
|
95
|
-
* `NormalizedCommand` for the event's command: `intercept()` below
|
|
96
|
-
* resolves ONE `NormalizedCommand` per event (via
|
|
97
|
-
* `options.normalizedCommand`, itself computed once by
|
|
98
|
-
* `runInterceptCli`) and threads it into every `policyMatchesEvent`
|
|
99
|
-
* call in its `matching` loop, so a raw-miss event normalises the
|
|
100
|
-
* command exactly ONCE across the whole manifest instead of once per
|
|
101
|
-
* policy. Omitted by standalone callers (`harness explain-policy`, most
|
|
102
|
-
* tests), which fall back to computing it lazily right here — correct
|
|
103
|
-
* either way, since `normalizeCommand` is a pure function of the
|
|
104
|
-
* command string alone.
|
|
105
|
-
*
|
|
106
|
-
* `ampNormalizedCommandThunk` (task aabbad63) is the SAME
|
|
107
|
-
* resolved-by-the-wrapper pattern for the ampersand-aware SECOND
|
|
108
|
-
* normalisation pass, but threaded as a memoised THUNK rather than a
|
|
109
|
-
* precomputed value — see `InterceptOptions.ampNormalizedCommandThunk`
|
|
110
|
-
* for why laziness matters here specifically. Omitted callers fall back
|
|
111
|
-
* to calling `normalizeCommandAmpAware` directly, same shape as the
|
|
112
|
-
* `precomputedNormalizedCommand` fallback above.
|
|
113
|
-
*
|
|
114
|
-
* `quoteNormalizedCommandThunk` (task cf3dff51) is the SAME thunk pattern
|
|
115
|
-
* again, for the quote-aware THIRD normalisation pass — see
|
|
116
|
-
* `InterceptOptions.quoteNormalizedCommandThunk`. Omitted callers fall
|
|
117
|
-
* back to calling `normalizeCommandQuoteAware` directly, same shape as
|
|
118
|
-
* the two fallbacks above.
|
|
119
|
-
*/
|
|
120
|
-
export function policyMatchesEvent(policy, event, precomputedNormalizedCommand, ampNormalizedCommandThunk, quoteNormalizedCommandThunk) {
|
|
121
|
-
if (policy.trigger.event !== event.hook_event_name)
|
|
122
|
-
return false;
|
|
123
|
-
if (policy.trigger.match !== undefined) {
|
|
124
|
-
if (typeof event.tool_name !== "string")
|
|
125
|
-
return false;
|
|
126
|
-
const toolNames = expandToolNameAliases(event.tool_name);
|
|
127
|
-
if (!toolNames.some((toolName) => toolName.includes(policy.trigger.match))) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
// `input_match` (task 2699b476): literal equality against the tool
|
|
132
|
-
// call's own arguments, ANDed onto the tool-name match above. This is
|
|
133
|
-
// what separates `task_finish { autoMerge: true }` (a merge, gated)
|
|
134
|
-
// from a plain `task_finish` (not a merge, not gated) without needing
|
|
135
|
-
// two different tool names. Evaluated from the SAME `toolArgs` context
|
|
136
|
-
// `trigger.extract` reads (`buildEventContext`) for a SINGLE-envelope
|
|
137
|
-
// event, and additionally against BOTH `tool_input` and `raw_input`
|
|
138
|
-
// when a payload carries both as non-null objects (review round 1,
|
|
139
|
-
// task 2699b476 round 2, see `inputMatchMismatchesEvent` below).
|
|
140
|
-
// Mirrored in `policyMatchesTool` (`src/cli/dry-run.ts`) for the
|
|
141
|
-
// single-envelope case only, since dry-run's `--input` is always one
|
|
142
|
-
// object; `harness policy dry-run` cannot reproduce the mixed-envelope
|
|
143
|
-
// arm. Both `input_match` parity and the mixed-envelope-is-intercept-only
|
|
144
|
-
// caveat are recorded in docs/okf/debug-verb-selection.md's
|
|
145
|
-
// trigger-matching parity paragraph.
|
|
146
|
-
if (policy.trigger.input_match !== undefined) {
|
|
147
|
-
if (inputMatchMismatchesEvent(policy.trigger.input_match, event)) {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (policy.trigger.bash_match !== undefined) {
|
|
152
|
-
const command = extractShellCommand(event);
|
|
153
|
-
if (command === null)
|
|
154
|
-
return false;
|
|
155
|
-
let re;
|
|
156
|
-
try {
|
|
157
|
-
re = new RegExp(policy.trigger.bash_match);
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
// Raw-OR-normalised-OR-amp-normalised-OR-quote-normalised (D-003, run
|
|
163
|
-
// 2026-07-27-gate-target-repo-resolution; third arm added task
|
|
164
|
-
// aabbad63; fourth arm added task cf3dff51): test the RAW command
|
|
165
|
-
// first — cheap, and byte-identical to the pre-fix behaviour — then,
|
|
166
|
-
// only if that fails, the primary NORMALISED command (wrapper
|
|
167
|
-
// prefixes peeled, git global options dropped, whitespace collapsed,
|
|
168
|
-
// BOUNDARY_RE segmentation) — then, only if THAT also fails, the
|
|
169
|
-
// ampersand-aware second pass (AMP_BOUNDARY_RE segmentation, closing
|
|
170
|
-
// the bare-`&` family BOUNDARY_RE cannot see: `A=x&env -C /tmp git
|
|
171
|
-
// status`, `echo hi & nice git status`) — then, only if THAT also
|
|
172
|
-
// fails, the quote-aware third pass (BOUNDARY_RE's own alphabet, but
|
|
173
|
-
// quote-tracking, closing a shell-boundary character sitting INSIDE a
|
|
174
|
-
// quoted assignment value: `VAR='a; b' git push origin master`).
|
|
175
|
-
// Strictly additive at every step: a command that matched today keeps
|
|
176
|
-
// matching via the raw test alone; the primary normalised form can
|
|
177
|
-
// only ADD a match (env/nice/command wrappers, extra git global
|
|
178
|
-
// options, doubled whitespace); the amp-aware form can only add a
|
|
179
|
-
// FURTHER match on top of those two; the quote-aware form can only
|
|
180
|
-
// add a FOURTH match on top of all three, never remove one any of the
|
|
181
|
-
// others already found. Replacing the matcher input instead of OR-ing
|
|
182
|
-
// it in risks silently REMOVING an existing match if some pass ever
|
|
183
|
-
// mangles a shape — the fail-open direction for a gate — so this
|
|
184
|
-
// stays additive rather than a substitution at every arm.
|
|
185
|
-
if (!re.test(command)) {
|
|
186
|
-
const { normalized } = precomputedNormalizedCommand ?? normalizeCommand(command);
|
|
187
|
-
if (!re.test(normalized)) {
|
|
188
|
-
const amp = ampNormalizedCommandThunk
|
|
189
|
-
? ampNormalizedCommandThunk()
|
|
190
|
-
: normalizeCommandAmpAware(command);
|
|
191
|
-
if (!re.test(amp.normalized)) {
|
|
192
|
-
const quoted = quoteNormalizedCommandThunk
|
|
193
|
-
? quoteNormalizedCommandThunk()
|
|
194
|
-
: normalizeCommandQuoteAware(command);
|
|
195
|
-
if (!re.test(quoted.normalized))
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
function buildEventContext(event) {
|
|
204
|
-
return contextWithToolArgs(event, event.tool_input ?? event.raw_input ?? event.input);
|
|
205
|
-
}
|
|
206
|
-
function contextWithToolArgs(event, toolArgs) {
|
|
207
|
-
return {
|
|
208
|
-
toolArgs,
|
|
209
|
-
event,
|
|
210
|
-
session: { id: event.session_id ?? "" },
|
|
211
|
-
git: {},
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
function isNonNullObject(value) {
|
|
215
|
-
return typeof value === "object" && value !== null;
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Whether `input_match` fails to arm the gate for this event (review
|
|
219
|
-
* round 1, task 2699b476 round 2, MEDIUM security finding).
|
|
220
|
-
*
|
|
221
|
-
* The default `toolArgs` context (`buildEventContext`) resolves ONE
|
|
222
|
-
* field with `tool_input ?? raw_input ?? input`, so a payload that
|
|
223
|
-
* carries a benign value in the PREFERRED field and the actual merge
|
|
224
|
-
* request in the OTHER field never gets read:
|
|
225
|
-
* `{tool_input:{taskId:"t"}, raw_input:{taskId:"t",autoMerge:true}}`
|
|
226
|
-
* resolves `toolArgs` to `tool_input` alone, `autoMerge` reads as
|
|
227
|
-
* absent, and the narrowed `task_finish` gate stays unarmed for a call
|
|
228
|
-
* that DOES request an auto-merge.
|
|
229
|
-
*
|
|
230
|
-
* Mirrors the two-field handling `resolveCodexExemptionCommand`
|
|
231
|
-
* (`hook-codex-pre-tool-use.ts`) already established for this exact
|
|
232
|
-
* shape, but in the opposite fail-closed direction: that function
|
|
233
|
-
* REFUSES an exemption when the two fields disagree (an exemption is an
|
|
234
|
-
* allow, so disagreement must not grant it); here a `requires:` gate is
|
|
235
|
-
* a block, so disagreement must not WITHHOLD it. When both `tool_input`
|
|
236
|
-
* and `raw_input` are present as non-null objects, `input_match` is
|
|
237
|
-
* evaluated against BOTH; if either one matches, the predicate holds
|
|
238
|
-
* (the gate is armed) regardless of what the other field says. Only
|
|
239
|
-
* when NEITHER field matches does the gate stay unarmed.
|
|
240
|
-
*/
|
|
241
|
-
function inputMatchMismatchesEvent(inputMatch, event) {
|
|
242
|
-
const toolInput = event.tool_input;
|
|
243
|
-
const rawInput = event.raw_input;
|
|
244
|
-
if (isNonNullObject(toolInput) && isNonNullObject(rawInput)) {
|
|
245
|
-
const toolInputMismatch = firstInputMatchMismatch(inputMatch, contextWithToolArgs(event, toolInput));
|
|
246
|
-
const rawInputMismatch = firstInputMatchMismatch(inputMatch, contextWithToolArgs(event, rawInput));
|
|
247
|
-
return toolInputMismatch !== null && rawInputMismatch !== null;
|
|
248
|
-
}
|
|
249
|
-
return firstInputMatchMismatch(inputMatch, buildEventContext(event)) !== null;
|
|
250
|
-
}
|
|
251
|
-
/** Map a failed-`requires` policy to its decision outcome by enforcement. */
|
|
252
|
-
function outcomeForFailedRequires(enforcement) {
|
|
253
|
-
switch (enforcement) {
|
|
254
|
-
case "block":
|
|
255
|
-
return "deny";
|
|
256
|
-
case "warn":
|
|
257
|
-
return "warn";
|
|
258
|
-
case "require_approval":
|
|
259
|
-
return "require_approval";
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Does a decision abort the tool call? Phase 7 #6 makes the Risk Gate
|
|
264
|
-
* authoritative at the `PreToolUse` boundary:
|
|
265
|
-
* - `deny` aborts (a `block`-enforcement policy whose requires failed,
|
|
266
|
-
* the Phase 4 mechanism, unchanged).
|
|
267
|
-
* - `require_approval` aborts until the approval evidence exists. In
|
|
268
|
-
* Phase 7 #5 this outcome was returned but did not block; #6 makes
|
|
269
|
-
* it block. The approval tag is satisfiable through the policy's
|
|
270
|
-
* `requires:` (an operator runs `harness approve risk`); once the
|
|
271
|
-
* tag is on record the requires evaluation passes and the outcome
|
|
272
|
-
* is `allow` instead.
|
|
273
|
-
* - `deny-degraded` aborts (task f1aea826): it is only ever produced
|
|
274
|
-
* for `block` / `require_approval` policies under the default
|
|
275
|
-
* `preserve_enforcement` posture (see `degradedOutcome`), so its
|
|
276
|
-
* presence alone means "an incident-preventing gate could not read
|
|
277
|
-
* its evidence" — fail closed.
|
|
278
|
-
* - `allow` / `warn` / `warn-degraded` never abort.
|
|
279
|
-
*
|
|
280
|
-
* Exported for the CLI wrapper's pending-approval staging
|
|
281
|
-
* (`src/cli/policy/intercept.ts`), which must agree with the runtime on
|
|
282
|
-
* WHICH decision is the first blocking one — a hand-rolled copy there
|
|
283
|
-
* drifted when `deny-degraded` was added (review 2026-08-08, low
|
|
284
|
-
* finding). Keep this the single definition.
|
|
285
|
-
*/
|
|
286
|
-
export function isBlockingDecision(d) {
|
|
287
|
-
if (d.outcome === "deny")
|
|
288
|
-
return d.enforcement === "block";
|
|
289
|
-
if (d.outcome === "deny-degraded")
|
|
290
|
-
return true;
|
|
291
|
-
return d.outcome === "require_approval";
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Outcome for a policy whose `requires` could NOT be evaluated at all —
|
|
295
|
-
* degraded/thrown ledger query, unresolved template variables, invalid
|
|
296
|
-
* `within`, thrown `evaluateRequires`, or the defensive
|
|
297
|
-
* schema-invariant branch. The fail posture is derived from the
|
|
298
|
-
* policy's own `enforcement:` (task f1aea826):
|
|
299
|
-
*
|
|
300
|
-
* warn → `warn-degraded` (availability first,
|
|
301
|
-
* unchanged: advisory friction never
|
|
302
|
-
* bricks the session)
|
|
303
|
-
* block / require_approval → `deny-degraded` (fail closed: a gate
|
|
304
|
-
* against an irreversible incident must
|
|
305
|
-
* not open because its evidence source
|
|
306
|
-
* is unreadable)
|
|
307
|
-
*
|
|
308
|
-
* The manifest-level opt-out `risk.degraded_fail_posture: fail_open`
|
|
309
|
-
* restores the pre-0.45 availability-first mapping (`warn-degraded`
|
|
310
|
-
* for every tier). The hand-built-manifest case (`options.manifest.risk`
|
|
311
|
-
* absent, only possible past `harness validate`) defaults to the
|
|
312
|
-
* fail-closed posture, matching the schema default.
|
|
313
|
-
*/
|
|
314
|
-
function degradedOutcome(policy, manifest) {
|
|
315
|
-
if (policy.enforcement === "warn")
|
|
316
|
-
return "warn-degraded";
|
|
317
|
-
const posture = manifest.risk?.degraded_fail_posture ?? "preserve_enforcement";
|
|
318
|
-
return posture === "fail_open" ? "warn-degraded" : "deny-degraded";
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Control characters (C0 range plus DEL), built via fromCharCode so the
|
|
322
|
-
* source file itself stays free of raw control bytes. Matches what the
|
|
323
|
-
* envelope sanitiser collapses to a single space.
|
|
324
|
-
*/
|
|
325
|
-
const ENVELOPE_CONTROL_CHARS = new RegExp(`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}]+`, "g");
|
|
326
|
-
/**
|
|
327
|
-
* Bound and clean a transport-level reason before it is interpolated
|
|
328
|
-
* into the agent-facing deny-degraded envelope. The string can embed
|
|
329
|
-
* output captured from the grounding-mcp SUBPROCESS (`exitDiagnostic`
|
|
330
|
-
* in `src/policies/ledger-client.ts` appends the child's last stderr
|
|
331
|
-
* line), i.e. untrusted content that previously only reached stderr
|
|
332
|
-
* and the audit ledger, never model-visible text. The full, untouched
|
|
333
|
-
* string still goes to the audit row and the verbose diagnostic; only
|
|
334
|
-
* the envelope interpolation and the default-verbosity operator hint
|
|
335
|
-
* are bounded.
|
|
336
|
-
*
|
|
337
|
-
* Exported for the CLI wrapper's deny-degraded operator hint (review
|
|
338
|
-
* 2026-08-08, round 3; default-verbosity only, suppressed under
|
|
339
|
-
* verbose), which interpolates the same untrusted reason into its
|
|
340
|
-
* one-line stderr surface.
|
|
341
|
-
*/
|
|
342
|
-
export function sanitizeEnvelopeReason(reason) {
|
|
343
|
-
const stripped = reason.replace(ENVELOPE_CONTROL_CHARS, " ");
|
|
344
|
-
return stripped.length > 200 ? `${stripped.slice(0, 200)}...` : stripped;
|
|
345
|
-
}
|
|
346
|
-
const EMPTY_REPO_MESSAGE = "no git repository was found for this command: the working directory (or the directory the command targets) is not inside a git repository, so the repository-scoped evidence this policy checks cannot be looked up. " +
|
|
347
|
-
"Change into the target repository first (`cd <repo>`) or name it explicitly (`git -C <repo> ...`), then retry the command.";
|
|
348
|
-
/**
|
|
349
|
-
* Names the repository whose HEAD is detached, so a detached working
|
|
350
|
-
* directory that is denied next to a `git -C <other repo>` target is
|
|
351
|
-
* recognisable as the working directory's own repository. The name is the
|
|
352
|
-
* resolved `${REPO}` value: the work-tree basename, a `HARNESS_REPO`
|
|
353
|
-
* override, or the value of a policy extract named `REPO` (which can
|
|
354
|
-
* carry tool input), so it is cleaned and bounded like any other untrusted
|
|
355
|
-
* text before it reaches the agent envelope: control characters blanked,
|
|
356
|
-
* at most 200 characters (`sanitizeEnvelopeReason`).
|
|
357
|
-
*/
|
|
358
|
-
function emptyBranchMessage(repo) {
|
|
359
|
-
const name = sanitizeEnvelopeReason(repo);
|
|
360
|
-
return (`no branch is checked out in repository \`${name}\`: HEAD is detached, so the branch-scoped evidence this policy checks cannot be looked up. ` +
|
|
361
|
-
"Check out a named branch there (`git switch <branch>`, or `git switch -c <branch>` for a new one), create the evidence for that branch, then retry the command.");
|
|
362
|
-
}
|
|
363
|
-
function isBlankIdentifier(value) {
|
|
364
|
-
return value === undefined || value.trim().length === 0;
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Does `ledgerTagTemplate` reference `${REPO}` / `${BRANCH}` while the
|
|
368
|
-
* value resolved for this context is empty? An empty identifier must
|
|
369
|
-
* never be rendered into a ledger tag: `preflight:` is a substring of
|
|
370
|
-
* EVERY preflight fact, so a blank tag lets any unrelated fact satisfy
|
|
371
|
-
* the gate (and `preflight-before-push` would render "You cannot push
|
|
372
|
-
* branch yet."). The caller decides per enforcement with
|
|
373
|
-
* `message` instead of querying the ledger.
|
|
374
|
-
*
|
|
375
|
-
* `REPO` wins over `BRANCH`, and a branch-only template evaluated where
|
|
376
|
-
* REPO is empty too (no repository at all) gets the no-repository text,
|
|
377
|
-
* since "HEAD is detached" would be false there. The messages name a
|
|
378
|
-
* state the agent can establish (`cd` / `git -C` / `git switch`), never
|
|
379
|
-
* a tag to produce, so there is no producer trap, and never an opt-out
|
|
380
|
-
* (a block that names its own disable path is not a gate).
|
|
381
|
-
*
|
|
382
|
-
* Exported so `harness dry-run` shows the same hint instead of a blank
|
|
383
|
-
* tag (`src/cli/dry-run.ts`).
|
|
384
|
-
*/
|
|
385
|
-
export function emptyIdentifierGuard(ledgerTagTemplate, values) {
|
|
386
|
-
const refsRepo = ledgerTagTemplate.includes("${REPO}");
|
|
387
|
-
const refsBranch = ledgerTagTemplate.includes("${BRANCH}");
|
|
388
|
-
if (refsRepo && isBlankIdentifier(values.REPO)) {
|
|
389
|
-
return { identifier: "REPO", message: EMPTY_REPO_MESSAGE };
|
|
390
|
-
}
|
|
391
|
-
if (refsBranch && isBlankIdentifier(values.BRANCH)) {
|
|
392
|
-
return isBlankIdentifier(values.REPO)
|
|
393
|
-
? { identifier: "REPO", message: EMPTY_REPO_MESSAGE }
|
|
394
|
-
: { identifier: "BRANCH", message: emptyBranchMessage(values.REPO ?? "") };
|
|
395
|
-
}
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Placeholder `ledgerTag` recorded on an `operator_only` decision (and on
|
|
400
|
-
* the defensive schema-invariant-violated branch below). Both outcomes
|
|
401
|
-
* are decided WITHOUT ever substituting or querying a real tag, so this
|
|
402
|
-
* is a readable marker for `harness audit` / `explain --trace` / the
|
|
403
|
-
* stderr diagnostic, not a value anything matches against.
|
|
404
|
-
*/
|
|
405
|
-
const NO_LEDGER_TAG = "(operator-only: no ledger tag — never evaluated)";
|
|
406
|
-
async function evaluateOnePolicy(policy, options) {
|
|
407
|
-
const evaluatedAt = (options.now ?? new Date()).toISOString();
|
|
408
|
-
const ctx = buildEventContext(options.event);
|
|
409
|
-
const extract = evaluateExtract(policy.trigger.extract ?? {}, ctx, options.builtins);
|
|
410
|
-
// Unconditional operator-only deny (schema `operator_only: true`, task
|
|
411
|
-
// 2cc73f55). The schema's superRefine guarantees this form carries NO
|
|
412
|
-
// `requires:` and `enforcement: block`, so short-circuit here, BEFORE
|
|
413
|
-
// the requires pipeline: no ledger query, no template substitution, no
|
|
414
|
-
// `evaluateRequires` call. That is the load-bearing property — there is
|
|
415
|
-
// no in-session evidence (ledger tag, marker file, flag) this branch
|
|
416
|
-
// ever reads, so none can flip the outcome to allow. `extract` above is
|
|
417
|
-
// still computed (cheap, pure) only because a `ux:`/`producers:` block
|
|
418
|
-
// on this policy may reference `${VAR}`s from `trigger.extract`; it
|
|
419
|
-
// plays no role in the outcome below.
|
|
420
|
-
if (policy.operator_only === true) {
|
|
421
|
-
return {
|
|
422
|
-
policyName: policy.name,
|
|
423
|
-
enforcement: policy.enforcement,
|
|
424
|
-
outcome: "deny",
|
|
425
|
-
reason: "operator-only: this policy declares no requires: and cannot be satisfied by any in-session evidence",
|
|
426
|
-
extractValues: extract.values,
|
|
427
|
-
ledgerTag: NO_LEDGER_TAG,
|
|
428
|
-
evaluatedAt,
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
const requires = policy.requires;
|
|
432
|
-
if (requires === undefined) {
|
|
433
|
-
// Unreachable under a schema-validated manifest: PolicySchema's
|
|
434
|
-
// superRefine requires either `requires:` or `operator_only: true`.
|
|
435
|
-
// Defensive branch only, for a manifest that bypassed
|
|
436
|
-
// `harness validate` (hand-built Policy object, stale cached parse).
|
|
437
|
-
//
|
|
438
|
-
// Deliberately NOT a plain `deny`: this is the SAME "could not
|
|
439
|
-
// decide" family as the other degraded returns in this function
|
|
440
|
-
// (unresolved template variables, a degraded ledger query, an
|
|
441
|
-
// invalid `within`, a throwing `evaluateRequires`) — the contract
|
|
442
|
-
// for all of them is "the evaluator could not form a real verdict",
|
|
443
|
-
// routed through `degradedOutcome` so the policy's own enforcement
|
|
444
|
-
// decides the fail posture (task f1aea826): `warn` stays the
|
|
445
|
-
// non-blocking `warn-degraded`, `block`/`require_approval` fail
|
|
446
|
-
// closed as `deny-degraded`. Failing this branch as plain `deny`
|
|
447
|
-
// would treat an internal schema-invariant violation as if it were
|
|
448
|
-
// a deliberate `operator_only: true` policy authored by the
|
|
449
|
-
// operator, which it is not — `deny-degraded` keeps the two
|
|
450
|
-
// observably distinct in every audit row and envelope.
|
|
451
|
-
return {
|
|
452
|
-
policyName: policy.name,
|
|
453
|
-
enforcement: policy.enforcement,
|
|
454
|
-
outcome: degradedOutcome(policy, options.manifest),
|
|
455
|
-
reason: "policy declares neither requires: nor operator_only: true (schema invariant violated)",
|
|
456
|
-
extractValues: extract.values,
|
|
457
|
-
ledgerTag: NO_LEDGER_TAG,
|
|
458
|
-
evaluatedAt,
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
// Empty-identifier guard: decided BEFORE any tag is rendered or the
|
|
462
|
-
// ledger is queried, per the policy's own enforcement (block -> deny,
|
|
463
|
-
// require_approval -> require_approval, warn -> warn). Deliberately NOT
|
|
464
|
-
// routed through `degradedOutcome` / the unresolved-variable branch
|
|
465
|
-
// below: that path ends in the `deny-degraded` envelope, which blames
|
|
466
|
-
// an unreadable ledger and tells the agent to ask the operator to
|
|
467
|
-
// check grounding-mcp, false and misleading for an empty identifier.
|
|
468
|
-
const emptyGuard = emptyIdentifierGuard(requires.ledger_tag, extract.values);
|
|
469
|
-
if (emptyGuard !== null) {
|
|
470
|
-
return {
|
|
471
|
-
policyName: policy.name,
|
|
472
|
-
enforcement: policy.enforcement,
|
|
473
|
-
outcome: outcomeForFailedRequires(policy.enforcement),
|
|
474
|
-
reason: emptyGuard.message,
|
|
475
|
-
extractValues: extract.values,
|
|
476
|
-
ledgerTag: `(empty ${emptyGuard.identifier}: no ledger tag rendered, no ledger query)`,
|
|
477
|
-
emptyIdentifier: emptyGuard.identifier,
|
|
478
|
-
evaluatedAt,
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
const missingExtracts = extract.traceData
|
|
482
|
-
.filter((t) => t.source === "missing")
|
|
483
|
-
.map((t) => t.var);
|
|
484
|
-
const sub = substituteTemplate(requires.ledger_tag, extract.values);
|
|
485
|
-
const ledgerTag = sub.result;
|
|
486
|
-
const unresolved = [...missingExtracts, ...sub.missing];
|
|
487
|
-
if (unresolved.length > 0) {
|
|
488
|
-
return {
|
|
489
|
-
policyName: policy.name,
|
|
490
|
-
enforcement: policy.enforcement,
|
|
491
|
-
outcome: degradedOutcome(policy, options.manifest),
|
|
492
|
-
reason: `template variables unresolved: ${unresolved.join(", ")}`,
|
|
493
|
-
extractValues: extract.values,
|
|
494
|
-
ledgerTag,
|
|
495
|
-
evaluatedAt,
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
const sessionId = resolveSessionId(options.event.session_id);
|
|
499
|
-
let queryResult;
|
|
500
|
-
try {
|
|
501
|
-
queryResult = await options.ledger.query(ledgerTag, sessionId, options.ledgerTimeoutMs);
|
|
502
|
-
}
|
|
503
|
-
catch (err) {
|
|
504
|
-
queryResult = {
|
|
505
|
-
kind: "degraded",
|
|
506
|
-
reason: `ledger query threw: ${err.message}`,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
if (queryResult.kind === "degraded") {
|
|
510
|
-
return {
|
|
511
|
-
policyName: policy.name,
|
|
512
|
-
enforcement: policy.enforcement,
|
|
513
|
-
outcome: degradedOutcome(policy, options.manifest),
|
|
514
|
-
reason: queryResult.reason,
|
|
515
|
-
extractValues: extract.values,
|
|
516
|
-
ledgerTag,
|
|
517
|
-
evaluatedAt,
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
// Pre-validate `within` against the runtime parser so a manifest that
|
|
521
|
-
// bypassed `harness validate` doesn't throw uncaught.
|
|
522
|
-
if (requires.within !== undefined) {
|
|
523
|
-
try {
|
|
524
|
-
parseDurationSeconds(requires.within);
|
|
525
|
-
}
|
|
526
|
-
catch {
|
|
527
|
-
return {
|
|
528
|
-
policyName: policy.name,
|
|
529
|
-
enforcement: policy.enforcement,
|
|
530
|
-
outcome: degradedOutcome(policy, options.manifest),
|
|
531
|
-
reason: `invalid within: ${requires.within}`,
|
|
532
|
-
extractValues: extract.values,
|
|
533
|
-
ledgerTag,
|
|
534
|
-
evaluatedAt,
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
const evalOpts = {
|
|
539
|
-
...(options.now && { now: options.now }),
|
|
540
|
-
...(options.currentHeadSha !== undefined &&
|
|
541
|
-
options.currentHeadSha.length > 0 && {
|
|
542
|
-
currentHeadSha: options.currentHeadSha,
|
|
543
|
-
}),
|
|
544
|
-
};
|
|
545
|
-
const filtered = filterEntriesByTag(queryResult.entries, ledgerTag);
|
|
546
|
-
let evaluation;
|
|
547
|
-
try {
|
|
548
|
-
evaluation = evaluateRequires({ ...requires, ledger_tag: ledgerTag }, filtered, evalOpts);
|
|
549
|
-
}
|
|
550
|
-
catch (err) {
|
|
551
|
-
return {
|
|
552
|
-
policyName: policy.name,
|
|
553
|
-
enforcement: policy.enforcement,
|
|
554
|
-
outcome: degradedOutcome(policy, options.manifest),
|
|
555
|
-
reason: `requires eval threw: ${err.message}`,
|
|
556
|
-
extractValues: extract.values,
|
|
557
|
-
ledgerTag,
|
|
558
|
-
evaluatedAt,
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
// Four-way decision (Phase 7 #5). A satisfied `requires` always
|
|
562
|
-
// `allow`s; a failed one is mapped by the policy's enforcement —
|
|
563
|
-
// `block` → `deny`, `warn` → `warn`, `require_approval` →
|
|
564
|
-
// `require_approval`. The evaluator only RETURNS `require_approval`
|
|
565
|
-
// here; Phase 7 #6 makes it block.
|
|
566
|
-
const outcome = evaluation.allowed
|
|
567
|
-
? "allow"
|
|
568
|
-
: outcomeForFailedRequires(policy.enforcement);
|
|
569
|
-
return {
|
|
570
|
-
policyName: policy.name,
|
|
571
|
-
enforcement: policy.enforcement,
|
|
572
|
-
outcome,
|
|
573
|
-
reason: evaluation.reason,
|
|
574
|
-
extractValues: extract.values,
|
|
575
|
-
ledgerTag,
|
|
576
|
-
requiresEval: {
|
|
577
|
-
matchedCount: evaluation.matchedCount,
|
|
578
|
-
reason: evaluation.reason,
|
|
579
|
-
},
|
|
580
|
-
recordHint: evaluation.recordHint,
|
|
581
|
-
evaluatedAt,
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
function filterEntriesByTag(entries, tag) {
|
|
585
|
-
// The ledger client returns the entire session's entries; filter to those
|
|
586
|
-
// whose content/source matches the substituted tag. evaluateRequires also
|
|
587
|
-
// does a substring match, but pre-filtering here keeps the trace quieter
|
|
588
|
-
// and avoids the requires evaluator iterating unrelated session entries.
|
|
589
|
-
//
|
|
590
|
-
// Phase 5 #4 — also drop `policy_decision` rows so a past audit
|
|
591
|
-
// payload doesn't incidentally match the same tag the decision was
|
|
592
|
-
// about (the substring-pollution bug from PR #39's dogfood). The
|
|
593
|
-
// requires evaluator's entryMatches has the same guard, but
|
|
594
|
-
// pre-filtering keeps matchedCount honest in the trace data.
|
|
595
|
-
return entries.filter((e) => e.type !== POLICY_DECISION_TYPE &&
|
|
596
|
-
// Legacy backstop for pre-Phase-5-#4 rows: they were stored as
|
|
597
|
-
// type='fact' but carry the `policy_decision:` content prefix.
|
|
598
|
-
// Drop them at the same gate so a user upgrading harness without
|
|
599
|
-
// flushing their dev ledger doesn't keep paying the pollution
|
|
600
|
-
// tax. New rows are caught by the type check above.
|
|
601
|
-
!e.content.startsWith(`${POLICY_DECISION_TYPE}:`) &&
|
|
602
|
-
(e.content.includes(tag) ||
|
|
603
|
-
(e.source !== undefined && e.source.includes(tag))));
|
|
604
|
-
}
|
|
605
|
-
/**
|
|
606
|
-
* Which of a policy's own trigger-satisfying segments name a target — the
|
|
607
|
-
* attribution sibling of `policyMatchesEvent` (task `98ad072f`, T-003).
|
|
608
|
-
* Re-tests the policy's OWN `bash_match` regex against each segment's
|
|
609
|
-
* `text` in isolation: every shipped `bash_match` alternation includes a
|
|
610
|
-
* `^` branch, so testing it against one segment's canonicalised text
|
|
611
|
-
* alone (no leading boundary character — see `CommandSegment.text`'s own
|
|
612
|
-
* doc comment) is well-defined. Returns every segment that matches on its
|
|
613
|
-
* own — usually one, occasionally several (D-004: a decoy read and a cwd
|
|
614
|
-
* read chained in one command can each independently satisfy the SAME
|
|
615
|
-
* trigger).
|
|
616
|
-
*
|
|
617
|
-
* Never changes WHETHER a policy matches — `policyMatchesEvent` already
|
|
618
|
-
* decided that from the WHOLE command (raw-or-normalised-or-amp-
|
|
619
|
-
* normalised), unchanged by this function. This only narrows down WHICH
|
|
620
|
-
* segment(s), if any, are individually responsible for the match, so
|
|
621
|
-
* `intercept()` below knows which segment's target — if any — to resolve
|
|
622
|
-
* the `${REPO}`/`${BRANCH}`/`currentHeadSha` builtins from instead of the
|
|
623
|
-
* event's own cwd. `[]` when the policy has no `bash_match` trigger (an
|
|
624
|
-
* MCP-tool-name-triggered policy — no segment concept applies), when its
|
|
625
|
-
* regex is malformed (mirrors `policyMatchesEvent`'s own defensive
|
|
626
|
-
* `try/catch`), or when the whole-command match came ONLY from the
|
|
627
|
-
* ampersand-aware third arm: `segments` here is always built from the
|
|
628
|
-
* PRIMARY (`BOUNDARY_RE`) segmentation, which — by construction — cannot
|
|
629
|
-
* itself contain the bare-`&` split the amp arm relies on, so an amp-
|
|
630
|
-
* only match finds no individually-matching segment and this returns
|
|
631
|
-
* `[]` (D-003: cwd builtins, identical to shipped).
|
|
632
|
-
*/
|
|
633
|
-
export function attributeTriggerSegments(policy, segments) {
|
|
634
|
-
if (policy.trigger.bash_match === undefined)
|
|
635
|
-
return [];
|
|
636
|
-
let re;
|
|
637
|
-
try {
|
|
638
|
-
re = new RegExp(policy.trigger.bash_match);
|
|
639
|
-
}
|
|
640
|
-
catch {
|
|
641
|
-
return [];
|
|
642
|
-
}
|
|
643
|
-
return segments.filter((seg) => re.test(seg.text));
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Does a policy's `requires:` reference the per-repo `${REPO}`/`${BRANCH}`
|
|
647
|
-
* builtins, or ask for `at_head`? Only these policies pay the per-policy
|
|
648
|
-
* attribution cost below — every other matching policy keeps the plain,
|
|
649
|
-
* per-event cwd builtins `options.builtins` already carries, byte-
|
|
650
|
-
* identical to before this task. D-005: `at_head` is included even when
|
|
651
|
-
* `ledger_tag` itself doesn't reference `${REPO}`/`${BRANCH}`, because
|
|
652
|
-
* `at_head` compares against `currentHeadSha`, which this task resolves
|
|
653
|
-
* from the SAME per-policy context — splitting the two would check
|
|
654
|
-
* `at_head` against the cwd's HEAD while `${REPO}` (if present elsewhere)
|
|
655
|
-
* named a different repo, the worse inconsistency D-005 rejects.
|
|
656
|
-
*/
|
|
657
|
-
function usesPerRepoBuiltins(policy) {
|
|
658
|
-
const requires = policy.requires;
|
|
659
|
-
if (requires === undefined)
|
|
660
|
-
return false;
|
|
661
|
-
return (requires.at_head === true ||
|
|
662
|
-
requires.ledger_tag.includes("${REPO}") ||
|
|
663
|
-
requires.ledger_tag.includes("${BRANCH}"));
|
|
664
|
-
}
|
|
665
|
-
/**
|
|
666
|
-
* Bound on the number of DISTINCT attributed contexts one policy is
|
|
667
|
-
* evaluated against for one event (D-013, fix round, run
|
|
668
|
-
* 2026-08-02-per-repo-gate-scoping-redesign). Each distinct context costs
|
|
669
|
-
* one ledger query and one audit write in `intercept()`'s evaluation loop
|
|
670
|
-
* below; on a manifest with several per-repo-builtins policies (the
|
|
671
|
-
* shipped `FULL_TEMPLATE` has four), an event naming K distinct targets
|
|
672
|
-
* amplifies to 4K queries/writes, unbounded by `MAX_NORMALIZE_LENGTH` —
|
|
673
|
-
* measured 200/200 at K=200 by reviewer 2, on a hook budget whose timeout
|
|
674
|
-
* is ALLOW (same class as the 07-27 quadratic hot-path fail-open: a slow
|
|
675
|
-
* enough event silently passes). `resolveAttributedContexts` returns a
|
|
676
|
-
* `"bounded"` result instead of a `contexts` array once a policy's DISTINCT
|
|
677
|
-
* targets would exceed this constant; `intercept()` denies that policy
|
|
678
|
-
* directly (naming the ambiguity) without querying the ledger for any of
|
|
679
|
-
* them, rather than silently evaluating all of them.
|
|
680
|
-
*/
|
|
681
|
-
export const MAX_ATTRIBUTED_CONTEXTS = 4;
|
|
682
|
-
/**
|
|
683
|
-
* Realpath a path for identity comparison, never throwing (D-012, fix
|
|
684
|
-
* round, run 2026-08-02-per-repo-gate-scoping-redesign). Falls back to the
|
|
685
|
-
* LEXICAL path unchanged when the target does not exist or is otherwise
|
|
686
|
-
* unreadable (`fs.realpathSync.native` throws `ENOENT` for a path this
|
|
687
|
-
* module was never guaranteed to have on disk, and this function must
|
|
688
|
-
* never throw — same fail-safe posture as `resolveGitContext` itself,
|
|
689
|
-
* which returns empty strings rather than throwing on a bad path).
|
|
690
|
-
* `.native` (not the plain JS `fs.realpathSync`) resolves via the OS
|
|
691
|
-
* syscall directly — cheaper, and avoids Node's own pure-JS symlink-loop
|
|
692
|
-
* bookkeeping for a value this function only uses for an identity
|
|
693
|
-
* comparison, never for a filesystem walk of its own.
|
|
694
|
-
*/
|
|
695
|
-
function realpathOrSelf(p) {
|
|
696
|
-
try {
|
|
697
|
-
return fs.realpathSync.native(p);
|
|
698
|
-
}
|
|
699
|
-
catch {
|
|
700
|
-
return p;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
|
-
* Resolve the distinct `${REPO}`/`${BRANCH}`/`currentHeadSha` contexts a
|
|
705
|
-
* `usesPerRepoBuiltins` policy must be evaluated against (task `98ad072f`,
|
|
706
|
-
* T-003, `01-plan.md` Proposed Approach items 2-4). The engine trusts a
|
|
707
|
-
* trigger-satisfying segment's `effectiveTarget` UNIFORMLY as the
|
|
708
|
-
* directory that segment's own invocation genuinely runs in (bash
|
|
709
|
-
* semantics) — orchestrator decision D-010, 2026-08-02: an earlier
|
|
710
|
-
* revision of this function additionally distrusted an inherited target
|
|
711
|
-
* whenever a DIFFERENT invocation intervened between the `cd` and the
|
|
712
|
-
* satisfying segment — REJECTED: that coupling has no basis in bash's own
|
|
713
|
-
* semantics, is dodgeable by inserting any harmless read between the `cd`
|
|
714
|
-
* and the gated verb, and carried its own unverified `cd`-recognition
|
|
715
|
-
* gap. The `cd <B> && git log && git push` shape this was meant to guard
|
|
716
|
-
* is not a regression at all under this design — `git push` genuinely
|
|
717
|
-
* runs inside B — see `tests/runtime/intercept-cli.test.ts`'s "leading-cd
|
|
718
|
-
* is now a deliverable" block.
|
|
719
|
-
*
|
|
720
|
-
* D-021 (UNIVERSAL-ADDITIVE, operator decision, fix round, run
|
|
721
|
-
* 2026-08-02-per-repo-gate-scoping-redesign): attribution is uniformly
|
|
722
|
-
* ADDITIVE, never REPLACE. Every satisfying segment with an attributable
|
|
723
|
-
* target — `seg.effectiveTarget !== null`, whether it came from the
|
|
724
|
-
* segment's OWN explicit repo-relocating flag (`-C`/`env -C`/`--git-dir`;
|
|
725
|
-
* `--work-tree` is excluded from ever producing a target at all, D-017)
|
|
726
|
-
* or was INHERITED from a preceding `cd` — demands the cwd context AND
|
|
727
|
-
* that target's own context, side by side; `seg.ownTarget` is not
|
|
728
|
-
* consulted for this decision. Any unsatisfied context blocks the whole
|
|
729
|
-
* event (the existing per-context evaluation machinery below is
|
|
730
|
-
* unchanged). `seg.effectiveTarget === null` (fully unattributable —
|
|
731
|
-
* D-003) still adds ONLY the cwd context — there is no foreign target to
|
|
732
|
-
* be additive WITH.
|
|
733
|
-
*
|
|
734
|
-
* Demanding cwd unconditionally makes the engine structurally immune to
|
|
735
|
-
* misattribution of the foreign target: no gap in `command-normalize.ts`'s
|
|
736
|
-
* static, string-only model of shell control flow (subshells, pipes,
|
|
737
|
-
* `cd -`, a later `cd`, an unmodeled git flag composition) can ever make a
|
|
738
|
-
* gate WEAKER than the shipped, cwd-only engine, because cwd's own demand
|
|
739
|
-
* is never dropped — only added to. This replaces an earlier REPLACE-for-
|
|
740
|
-
* own-target design (D-011) that trusted a statically extracted own
|
|
741
|
-
* target as proof of "this one invocation operates there"; four
|
|
742
|
-
* independent review passes each measured a live bypass of that
|
|
743
|
-
* invariant against the shipped binary (a forged `.git/HEAD` directory
|
|
744
|
-
* reached through, respectively, a non-persisting `cd`, `--work-tree`,
|
|
745
|
-
* more than one repo-relocating flag, and a tilde-valued flag not counted
|
|
746
|
-
* by the multi-flag guard) — see `03-decisions.md` D-011/D-017/D-018/
|
|
747
|
-
* D-019/D-020/D-021 for the full history. The static-analysis-proves-it
|
|
748
|
-
* invariant those bypasses each disproved is not carried forward here.
|
|
749
|
-
* Cost accepted by the operator: a legitimate `git -C B` from cwd A now
|
|
750
|
-
* demands BOTH A's and B's context, where the pre-D-021 engine demanded
|
|
751
|
-
* only B's.
|
|
752
|
-
*
|
|
753
|
-
* One exception to "never dropped": when the working directory is outside
|
|
754
|
-
* every git repository (a blank `${REPO}` for the policy, as the guard
|
|
755
|
-
* sees it), its cwd context is not demanded next to a segment whose own
|
|
756
|
-
* target resolved to a real repository (see the loop body). That
|
|
757
|
-
* exception relies on two static models: the target attribution, whose
|
|
758
|
-
* known misattribution (a `GIT_DIR=` prefix or a third repository reached
|
|
759
|
-
* through another construct) exists for every cwd, and the check that the
|
|
760
|
-
* cwd is outside every repository, which errs toward inside: it holds only
|
|
761
|
-
* when neither the cwd nor any ancestor has an entry named `HEAD` or
|
|
762
|
-
* `.git` (`mayBeInsideRepository`). A detached cwd and every other
|
|
763
|
-
* non-blank cwd context are still never dropped.
|
|
764
|
-
*
|
|
765
|
-
* D-012: a target reached through a symlink resolves to its REAL
|
|
766
|
-
* (realpath'd) repository identity, not the symlink's own lexical
|
|
767
|
-
* basename — `resolveGitContext` derives `repo` from the basename of
|
|
768
|
-
* wherever it's pointed, and never itself realpaths, so an agent could
|
|
769
|
-
* otherwise pick the demanded repo identity by naming a symlink.
|
|
770
|
-
*
|
|
771
|
-
* D-013: distinct contexts are bounded at `MAX_ATTRIBUTED_CONTEXTS`; see
|
|
772
|
-
* that constant's own comment.
|
|
773
|
-
*
|
|
774
|
-
* D-015: the cwd context is deduped by its OWN `[REPO, BRANCH, sha]`
|
|
775
|
-
* signature (not the literal string `"cwd"`), so a foreign path that
|
|
776
|
-
* resolves to the SAME repository identity as cwd (e.g. a subdirectory of
|
|
777
|
-
* the cwd repo reached via an explicit `-C`) collapses into the cwd
|
|
778
|
-
* context instead of producing a spurious duplicate decision/audit write.
|
|
779
|
-
* `repoOverridden` / `branchOverridden` (from `InterceptOptions`, both
|
|
780
|
-
* default `false`) keep an operator's `HARNESS_REPO`/`HARNESS_BRANCH`
|
|
781
|
-
* override intact in an attributed foreign context instead of letting the
|
|
782
|
-
* target repo's own identity silently overwrite it — independently per
|
|
783
|
-
* field. `satisfying` is a `Set` (not `Array.includes` per segment) for
|
|
784
|
-
* O(1) membership tests instead of O(n) per segment.
|
|
785
|
-
*
|
|
786
|
-
* A policy with no attributable foreign target (no `bash_match`, no
|
|
787
|
-
* individually-matching segment, only an amp-arm-only match, an
|
|
788
|
-
* unattributable composition, a target resolving to the SAME repository
|
|
789
|
-
* identity as cwd, or a target outside any git repo — D-003) is evaluated
|
|
790
|
-
* EXACTLY as it is today, with the EXACT SAME `options.builtins` object
|
|
791
|
-
* reference (no clone) when nothing attributed, keeping that the
|
|
792
|
-
* byte-identical common case.
|
|
793
|
-
*
|
|
794
|
-
* `resolveGitContextMemo` is a per-`intercept()`-call cache keyed by the
|
|
795
|
-
* REALPATH'D absolute path (never module-level state — no cross-event
|
|
796
|
-
* caching), so several policies (or several satisfying segments) naming
|
|
797
|
-
* the same foreign path within one event pay the `fs` cost once, not
|
|
798
|
-
* once per policy per segment. `mayBeInsideRepositoryMemo` is the same
|
|
799
|
-
* kind of per-call cache for the cwd's own outside-every-repository
|
|
800
|
-
* check, keyed by the cwd's real path, so that walk runs once per event.
|
|
801
|
-
*/
|
|
802
|
-
function resolveAttributedContexts(policy, segments, cwdBuiltins, cwdCurrentHeadSha, resolveGitContextMemo, mayBeInsideRepositoryMemo, repoOverridden, branchOverridden, extractContext) {
|
|
803
|
-
const cwdContext = { builtins: cwdBuiltins, currentHeadSha: cwdCurrentHeadSha };
|
|
804
|
-
const satisfying = new Set(attributeTriggerSegments(policy, segments));
|
|
805
|
-
if (satisfying.size === 0)
|
|
806
|
-
return { kind: "contexts", contexts: [cwdContext] };
|
|
807
|
-
const cwdSignature = [cwdBuiltins.REPO, cwdBuiltins.BRANCH, cwdCurrentHeadSha ?? ""].join("|");
|
|
808
|
-
const seenSignatures = new Set();
|
|
809
|
-
const contexts = [];
|
|
810
|
-
const addCwdOnce = () => {
|
|
811
|
-
if (seenSignatures.has(cwdSignature))
|
|
812
|
-
return;
|
|
813
|
-
seenSignatures.add(cwdSignature);
|
|
814
|
-
contexts.push(cwdContext);
|
|
815
|
-
};
|
|
816
|
-
const cwdReal = realpathOrSelf(cwdBuiltins.CWD);
|
|
817
|
-
// Decided from the same values `evaluateOnePolicy` guards on: the
|
|
818
|
-
// policy's own `trigger.extract` evaluated against the cwd builtins, so
|
|
819
|
-
// an extract named `REPO` / `BRANCH` that shadows a builtin is honoured
|
|
820
|
-
// here exactly as it is there.
|
|
821
|
-
const cwdGuard = emptyIdentifierGuard(policy.requires?.ledger_tag ?? "", evaluateExtract(policy.trigger.extract ?? {}, extractContext, cwdBuiltins).values);
|
|
822
|
-
// "Outside every repository" is checked on the cwd's real path: the
|
|
823
|
-
// builtins resolve the path as given, while git runs in the physical
|
|
824
|
-
// directory, so a symlink into a repository is not outside it. An empty
|
|
825
|
-
// CWD names no directory at all and never qualifies. The check is the
|
|
826
|
-
// conservative `mayBeInsideRepository`, not the static
|
|
827
|
-
// `resolveGitContext` the builtins come from: that resolver misses
|
|
828
|
-
// repositories git still finds (see the helper's doc comment), and here
|
|
829
|
-
// a miss would drop a demand. It runs only for a blank `${REPO}`, at
|
|
830
|
-
// most once per event (`mayBeInsideRepositoryMemo`).
|
|
831
|
-
const cwdOutsideEveryRepository = cwdGuard?.identifier === "REPO" &&
|
|
832
|
-
cwdBuiltins.CWD.length > 0 &&
|
|
833
|
-
!mayBeInsideRepositoryMemoised(cwdReal, mayBeInsideRepositoryMemo);
|
|
834
|
-
for (const seg of segments) {
|
|
835
|
-
if (!satisfying.has(seg))
|
|
836
|
-
continue;
|
|
837
|
-
if (seg.effectiveTarget === null) {
|
|
838
|
-
// D-003: fully unattributable — cwd only, no foreign target to be
|
|
839
|
-
// additive with.
|
|
840
|
-
addCwdOnce();
|
|
841
|
-
continue;
|
|
842
|
-
}
|
|
843
|
-
// D-021 (UNIVERSAL-ADDITIVE, operator decision after the four-pass
|
|
844
|
-
// halt — see this function's own doc comment): the cwd context is
|
|
845
|
-
// demanded UNCONDITIONALLY here, regardless of whether the target came
|
|
846
|
-
// from the segment's own explicit flag or was inherited from a
|
|
847
|
-
// preceding `cd`. `seg.ownTarget` is no longer read for this decision.
|
|
848
|
-
// The one exception is `cwdOutsideEveryRepository` below: it is
|
|
849
|
-
// decided after the target resolved, so the cwd context is added from
|
|
850
|
-
// the branches of this loop body instead of up front, in the same
|
|
851
|
-
// order as before.
|
|
852
|
-
const resolvedLexical = path.resolve(cwdBuiltins.CWD, seg.effectiveTarget);
|
|
853
|
-
const resolved = realpathOrSelf(resolvedLexical);
|
|
854
|
-
if (resolved === cwdReal) {
|
|
855
|
-
addCwdOnce();
|
|
856
|
-
continue;
|
|
857
|
-
}
|
|
858
|
-
let gitCtx = resolveGitContextMemo.get(resolved);
|
|
859
|
-
if (gitCtx === undefined) {
|
|
860
|
-
gitCtx = resolveGitContext(resolved);
|
|
861
|
-
resolveGitContextMemo.set(resolved, gitCtx);
|
|
862
|
-
}
|
|
863
|
-
if (gitCtx.repo.length === 0) {
|
|
864
|
-
// D-003: outside any repo → cwd fallback, not a distinct context.
|
|
865
|
-
addCwdOnce();
|
|
866
|
-
continue;
|
|
867
|
-
}
|
|
868
|
-
const signature = [gitCtx.repo, gitCtx.branch, gitCtx.sha].join("|");
|
|
869
|
-
// Empty-identifier exception to the universal-additive rule, limited
|
|
870
|
-
// to a working directory outside every git repository. That cwd
|
|
871
|
-
// context has a blank `${REPO}` (the value the guard sees, after the
|
|
872
|
-
// policy's own extract) and can never be satisfied: the
|
|
873
|
-
// empty-identifier guard denies it without a ledger query. Demanding
|
|
874
|
-
// it next to a target that resolved to a real repository would deny
|
|
875
|
-
// the very remedy the guard's message names (`git -C <repo> ...`, or
|
|
876
|
-
// `cd <repo> && ...` in one command). The skip applies only when the
|
|
877
|
-
// cwd is outside every repository and this segment's own target
|
|
878
|
-
// resolved to a real repository; the target's own context is still
|
|
879
|
-
// demanded in full. It relies on two static models: the target
|
|
880
|
-
// attribution, whose known misattribution (a `GIT_DIR=` prefix or a
|
|
881
|
-
// third repository reached through another construct) exists for every
|
|
882
|
-
// cwd, and the cwd resolution, which is conservative: the skip applies
|
|
883
|
-
// only when neither the cwd's real path nor any ancestor up to the
|
|
884
|
-
// filesystem root holds an entry named `HEAD` or `.git` (any type) and
|
|
885
|
-
// no lookup failed other than with ENOENT (`mayBeInsideRepository`).
|
|
886
|
-
// What remains outside both models: a third repository the command
|
|
887
|
-
// really runs in (a `GIT_DIR=` prefix, or a `cd` into another
|
|
888
|
-
// repository before the misattributed segment), state the command
|
|
889
|
-
// itself creates while it runs (a `.git` it links or writes before the
|
|
890
|
-
// git verb), and an ambient `GIT_DIR` / `GIT_COMMON_DIR` in the
|
|
891
|
-
// environment git runs with. A
|
|
892
|
-
// detached cwd (non-blank `${REPO}`, blank `${BRANCH}`) and
|
|
893
|
-
// every non-blank cwd context keep the rule above unchanged: the
|
|
894
|
-
// detached remedy (`git switch` in that repository) is establishable,
|
|
895
|
-
// and dropping that context would let a misattributed push from the
|
|
896
|
-
// detached HEAD pass on the target's evidence. A segment without a
|
|
897
|
-
// resolved target (a bare `git status`) keeps its cwd context and
|
|
898
|
-
// still denies with the hint.
|
|
899
|
-
if (!cwdOutsideEveryRepository)
|
|
900
|
-
addCwdOnce();
|
|
901
|
-
if (signature === cwdSignature) {
|
|
902
|
-
// D-015: a foreign target that resolves to cwd's own REAL identity
|
|
903
|
-
// (e.g. a subdirectory of the cwd repo reached via `-C`, a
|
|
904
|
-
// different literal path than `cwdBuiltins.CWD` itself but the SAME
|
|
905
|
-
// `.git`) collapses into the cwd context regardless of segment
|
|
906
|
-
// order — not just when a bare cwd-reading segment happened to add
|
|
907
|
-
// it first.
|
|
908
|
-
addCwdOnce();
|
|
909
|
-
continue;
|
|
910
|
-
}
|
|
911
|
-
if (seenSignatures.has(signature))
|
|
912
|
-
continue;
|
|
913
|
-
if (contexts.length >= MAX_ATTRIBUTED_CONTEXTS) {
|
|
914
|
-
// D-013: fail CLOSED — do not evaluate any of them, name the
|
|
915
|
-
// ambiguity instead. `contexts.length + 1` names "at least this
|
|
916
|
-
// many distinct targets", the count observed before bailing, not
|
|
917
|
-
// necessarily the final total (the loop stops here).
|
|
918
|
-
return { kind: "bounded", distinctCount: contexts.length + 1 };
|
|
919
|
-
}
|
|
920
|
-
seenSignatures.add(signature);
|
|
921
|
-
contexts.push({
|
|
922
|
-
builtins: {
|
|
923
|
-
...cwdBuiltins,
|
|
924
|
-
REPO: repoOverridden ? cwdBuiltins.REPO : gitCtx.repo,
|
|
925
|
-
BRANCH: branchOverridden ? cwdBuiltins.BRANCH : gitCtx.branch,
|
|
926
|
-
},
|
|
927
|
-
currentHeadSha: gitCtx.sha.length > 0 ? gitCtx.sha : undefined,
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
// Defensive fallback: every satisfying segment existed but somehow none
|
|
931
|
-
// was added above (should not happen — every branch above adds either
|
|
932
|
-
// the cwd context or a foreign one). Never leave a matched, per-repo-
|
|
933
|
-
// builtins policy with zero contexts to evaluate against.
|
|
934
|
-
return { kind: "contexts", contexts: contexts.length > 0 ? contexts : [cwdContext] };
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* Could git find a repository from `dir`? Decided for the one place that
|
|
938
|
-
* drops a demand on the answer (the cwd-outside-every-repository exception
|
|
939
|
-
* in `resolveAttributedContexts`), so every doubt counts as inside. The
|
|
940
|
-
* static `resolveGitContext` walk that resolves the `REPO` / `BRANCH`
|
|
941
|
-
* builtins is left as it is for its other callers; it reports no
|
|
942
|
-
* repository where git still finds one: a `.git` directory without `HEAD`
|
|
943
|
-
* ends its walk (git skips it and walks on), its walk stops after 128
|
|
944
|
-
* levels, and it never recognises a directory that is itself a git
|
|
945
|
-
* directory (a bare repository, or one whose objects live elsewhere
|
|
946
|
-
* through a `commondir` file).
|
|
947
|
-
*
|
|
948
|
-
* This walk does not list layouts. It uses the one structural fact every
|
|
949
|
-
* git directory shares: git accepts a directory as a git directory only
|
|
950
|
-
* when it holds an entry named `HEAD`, and it reaches a repository from a
|
|
951
|
-
* working directory only through a `.git` entry (directory or `gitdir:`
|
|
952
|
-
* file) or a git directory on the way up. So, from `dir` up to the
|
|
953
|
-
* filesystem root with no depth bound:
|
|
954
|
-
*
|
|
955
|
-
* - an entry named `HEAD` or `.git`, of any type, valid or not, counts as
|
|
956
|
-
* inside (never followed, read or validated);
|
|
957
|
-
* - an `lstat` error other than ENOENT counts as inside (the entry may
|
|
958
|
-
* exist).
|
|
959
|
-
*
|
|
960
|
-
* The cost is a conservative false inside: a directory that merely holds
|
|
961
|
-
* an entry named `HEAD` (also `head` or `Head` on a case-insensitive
|
|
962
|
-
* volume), or lies below one, keeps its cwd context and denies with the
|
|
963
|
-
* no-repository hint (fail closed). An ambient `GIT_DIR` /
|
|
964
|
-
* `GIT_COMMON_DIR` in git's environment is not visible here.
|
|
965
|
-
*/
|
|
966
|
-
function mayBeInsideRepository(dir) {
|
|
967
|
-
let current = path.resolve(dir);
|
|
968
|
-
for (;;) {
|
|
969
|
-
if (entryMayExist(path.join(current, ".git")))
|
|
970
|
-
return true;
|
|
971
|
-
if (entryMayExist(path.join(current, "HEAD")))
|
|
972
|
-
return true;
|
|
973
|
-
const parent = path.dirname(current);
|
|
974
|
-
if (parent === current)
|
|
975
|
-
return false;
|
|
976
|
-
current = parent;
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
/**
|
|
980
|
-
* `false` only when `lstat` proves the entry absent (ENOENT, reported as
|
|
981
|
-
* `undefined` without building an exception); every other error counts as
|
|
982
|
-
* present.
|
|
983
|
-
*/
|
|
984
|
-
function entryMayExist(entryPath) {
|
|
985
|
-
try {
|
|
986
|
-
return fs.lstatSync(entryPath, { throwIfNoEntry: false }) !== undefined;
|
|
987
|
-
}
|
|
988
|
-
catch (error) {
|
|
989
|
-
return error.code !== "ENOENT";
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
/**
|
|
993
|
-
* `mayBeInsideRepository(dir)`, computed at most once per `dir` within one
|
|
994
|
-
* `intercept()` call. `memo` is that call's own map (never module-level
|
|
995
|
-
* state), so no answer outlives the event it was computed for.
|
|
996
|
-
*/
|
|
997
|
-
function mayBeInsideRepositoryMemoised(dir, memo) {
|
|
998
|
-
const known = memo.get(dir);
|
|
999
|
-
if (known !== undefined)
|
|
1000
|
-
return known;
|
|
1001
|
-
const inside = mayBeInsideRepository(dir);
|
|
1002
|
-
memo.set(dir, inside);
|
|
1003
|
-
return inside;
|
|
1004
|
-
}
|
|
1005
|
-
/**
|
|
1006
|
-
* Lazily resolve the event's per-segment view for attribution, computed
|
|
1007
|
-
* at most once per `intercept()` call (the caller, `intercept()` below,
|
|
1008
|
-
* memoises the RESULT in its own `segmentsForAttribution` local — this
|
|
1009
|
-
* function itself is called at most once per call already, but stays
|
|
1010
|
-
* side-effect-free so that remains true regardless of caller changes).
|
|
1011
|
-
* Prefers `options.commandSegmentsThunk` (D-015 fix round: the deferred,
|
|
1012
|
-
* compute-only-if-needed seam — see its own doc comment), then the eager
|
|
1013
|
-
* `options.commandSegments`, then falls back to computing it directly
|
|
1014
|
-
* from the event's own command for standalone callers/tests that inject
|
|
1015
|
-
* neither. `null` (truncated — see `segmentViewOf`) and "no Bash command
|
|
1016
|
-
* on this event" both collapse to `[]`, the same "nothing to attribute,
|
|
1017
|
-
* cwd builtins" shape as a policy with no `bash_match` at all.
|
|
1018
|
-
*/
|
|
1019
|
-
function resolveCommandSegments(options) {
|
|
1020
|
-
if (options.commandSegmentsThunk !== undefined)
|
|
1021
|
-
return options.commandSegmentsThunk() ?? [];
|
|
1022
|
-
if (options.commandSegments !== undefined)
|
|
1023
|
-
return options.commandSegments ?? [];
|
|
1024
|
-
const command = extractShellCommand(options.event);
|
|
1025
|
-
if (command === null)
|
|
1026
|
-
return [];
|
|
1027
|
-
return segmentViewOf(command) ?? [];
|
|
1028
|
-
}
|
|
1029
|
-
/**
|
|
1030
|
-
* Synthesise the single decision `intercept()` records for a policy whose
|
|
1031
|
-
* distinct attributed contexts exceeded `MAX_ATTRIBUTED_CONTEXTS` (D-013).
|
|
1032
|
-
* Routes through `outcomeForFailedRequires` — the SAME enforcement-to-
|
|
1033
|
-
* outcome mapping every other "could not safely evaluate" branch in
|
|
1034
|
-
* `evaluateOnePolicy` uses — rather than a hardcoded outcome, so a
|
|
1035
|
-
* `block`-enforcement policy (the security-critical case: preflight/push/
|
|
1036
|
-
* merge gates) genuinely denies, while a `warn`-enforcement policy warns
|
|
1037
|
-
* instead of hard-blocking, consistent with how every other failed-
|
|
1038
|
-
* evaluation branch in this module already respects the policy's own
|
|
1039
|
-
* declared enforcement. No ledger query, no template substitution against
|
|
1040
|
-
* a resolved `ledger_tag` (there is no single context to substitute one
|
|
1041
|
-
* against) — `extractValues` is still computed from `trigger.extract`
|
|
1042
|
-
* against the CWD builtins (cheap, pure, matches the `operator_only`
|
|
1043
|
-
* short-circuit's own reasoning) so a `ux:`/`producers:` block on this
|
|
1044
|
-
* policy can still render.
|
|
1045
|
-
*/
|
|
1046
|
-
function boundedContextsDecision(policy, event, distinctCount, cwdBuiltins, evaluatedAt) {
|
|
1047
|
-
const extract = evaluateExtract(policy.trigger.extract ?? {}, buildEventContext(event), cwdBuiltins);
|
|
1048
|
-
return {
|
|
1049
|
-
policyName: policy.name,
|
|
1050
|
-
enforcement: policy.enforcement,
|
|
1051
|
-
outcome: outcomeForFailedRequires(policy.enforcement),
|
|
1052
|
-
reason: `ambiguous: this command names at least ${distinctCount} distinct repository targets for this policy, ` +
|
|
1053
|
-
`exceeding the ${MAX_ATTRIBUTED_CONTEXTS}-context bound — refusing to evaluate all of them`,
|
|
1054
|
-
extractValues: extract.values,
|
|
1055
|
-
ledgerTag: "(bounded: too many distinct attributed targets — no context queried)",
|
|
1056
|
-
evaluatedAt,
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
export async function intercept(options) {
|
|
1060
|
-
const { manifest, event } = options;
|
|
1061
|
-
// The Risk Gate is active only when some policy declares a `when:`
|
|
1062
|
-
// block. A manifest with none — every Phase 4 / 5 / 6 manifest — skips
|
|
1063
|
-
// envelope enrichment entirely: no `buildActionEnvelope`, no
|
|
1064
|
-
// classifier, no resolver, and decisions carry no `risk` / `environment`.
|
|
1065
|
-
// That keeps such manifests byte-for-byte identical to pre-Phase-7-#5.
|
|
1066
|
-
const riskGateActive = manifest.policies.some((p) => p.when !== undefined);
|
|
1067
|
-
const enriched = riskGateActive
|
|
1068
|
-
? enrichEnvelope(manifest, event, options.riskContext, options.now)
|
|
1069
|
-
: undefined;
|
|
1070
|
-
// A policy fires only when its `trigger:` matches AND — when declared
|
|
1071
|
-
// — every `when:` clause holds against the enriched envelope.
|
|
1072
|
-
//
|
|
1073
|
-
// For each matching when:-bearing policy we also record its
|
|
1074
|
-
// `unclassifiedFallback` flag so the decision record and deny message
|
|
1075
|
-
// can distinguish a genuine classification hit from a fail-closed
|
|
1076
|
-
// unclassified command (M7: runtime audit + block message). Policies
|
|
1077
|
-
// that have no `when:` block are not inserted into the map, so a
|
|
1078
|
-
// later `whenFallbackMap.get(name) === true` test is unambiguous.
|
|
1079
|
-
//
|
|
1080
|
-
// Explicit loop rather than Array.filter() so the map is built as a
|
|
1081
|
-
// first-class step: a filter predicate is expected to be pure, and a
|
|
1082
|
-
// future refactor that parallelises the filter would silently break the
|
|
1083
|
-
// audit flag if the mutation were still hiding inside the predicate.
|
|
1084
|
-
const whenFallbackMap = new Map();
|
|
1085
|
-
const matching = [];
|
|
1086
|
-
for (const p of manifest.policies) {
|
|
1087
|
-
if (!policyMatchesEvent(p, event, options.normalizedCommand, options.ampNormalizedCommandThunk, options.quoteNormalizedCommandThunk)) {
|
|
1088
|
-
continue;
|
|
1089
|
-
}
|
|
1090
|
-
if (p.when === undefined) {
|
|
1091
|
-
matching.push(p);
|
|
1092
|
-
continue;
|
|
1093
|
-
}
|
|
1094
|
-
// `enriched` is defined here: a policy with `when:` set `riskGateActive`.
|
|
1095
|
-
const whenEval = evaluateWhen(p.when, enriched);
|
|
1096
|
-
if (whenEval.matched) {
|
|
1097
|
-
whenFallbackMap.set(p.name, whenEval.unclassifiedFallback);
|
|
1098
|
-
matching.push(p);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
// Per-policy attribution (task `98ad072f`, T-003): `segmentsForAttribution`
|
|
1102
|
-
// is resolved at most once, lazily, only if some matching policy actually
|
|
1103
|
-
// uses `${REPO}`/`${BRANCH}`/`at_head` — a manifest with none of those
|
|
1104
|
-
// (every Phase 4/5/6-only manifest) never computes it.
|
|
1105
|
-
// `resolveGitContextMemo` is this call's own, non-module-level cache
|
|
1106
|
-
// (task constraint: no eager/global filesystem work) — see
|
|
1107
|
-
// `resolveAttributedContexts`'s own comment. `mayBeInsideRepositoryMemo`
|
|
1108
|
-
// is the same kind of per-call cache for the cwd walk.
|
|
1109
|
-
let segmentsForAttribution;
|
|
1110
|
-
const resolveGitContextMemo = new Map();
|
|
1111
|
-
const mayBeInsideRepositoryMemo = new Map();
|
|
1112
|
-
const decisions = [];
|
|
1113
|
-
for (const policy of matching) {
|
|
1114
|
-
const attributed = usesPerRepoBuiltins(policy)
|
|
1115
|
-
? resolveAttributedContexts(policy, (segmentsForAttribution ??= resolveCommandSegments(options)), options.builtins, options.currentHeadSha, resolveGitContextMemo, mayBeInsideRepositoryMemo, options.repoOverridden === true, options.branchOverridden === true, buildEventContext(options.event))
|
|
1116
|
-
: { kind: "contexts", contexts: [{ builtins: options.builtins, currentHeadSha: options.currentHeadSha }] };
|
|
1117
|
-
if (attributed.kind === "bounded") {
|
|
1118
|
-
// D-013: fail CLOSED without querying the ledger for any of the
|
|
1119
|
-
// (too many) distinct targets — one synthetic decision, one audit
|
|
1120
|
-
// write, then move on to the next policy. Ledger-query count for
|
|
1121
|
-
// THIS policy stays at zero regardless of how many distinct targets
|
|
1122
|
-
// the command actually names, instead of scaling with them.
|
|
1123
|
-
const evaluatedAt = (options.now ?? new Date()).toISOString();
|
|
1124
|
-
const decision = boundedContextsDecision(policy, event, attributed.distinctCount, options.builtins, evaluatedAt);
|
|
1125
|
-
decisions.push(decision);
|
|
1126
|
-
try {
|
|
1127
|
-
await options.ledger.record(decision, resolveSessionId(event.session_id));
|
|
1128
|
-
}
|
|
1129
|
-
catch (err) {
|
|
1130
|
-
(options.stderr ?? process.stderr).write(`harness runtime intercept: audit-write failed for ${decision.policyName}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1131
|
-
}
|
|
1132
|
-
continue;
|
|
1133
|
-
}
|
|
1134
|
-
for (const context of attributed.contexts) {
|
|
1135
|
-
// Same object reference as `options` on the (overwhelmingly common)
|
|
1136
|
-
// single-cwd-context path — no clone, byte-identical to the
|
|
1137
|
-
// pre-attribution call shape. Only a genuinely foreign context
|
|
1138
|
-
// builds a shallow override.
|
|
1139
|
-
const contextOptions = context.builtins === options.builtins &&
|
|
1140
|
-
context.currentHeadSha === options.currentHeadSha
|
|
1141
|
-
? options
|
|
1142
|
-
: { ...options, builtins: context.builtins, currentHeadSha: context.currentHeadSha };
|
|
1143
|
-
const base = await evaluateOnePolicy(policy, contextOptions);
|
|
1144
|
-
// Attach the per-event Risk Gate verdicts so `harness audit` /
|
|
1145
|
-
// `explain --trace` can replay the classification + environment
|
|
1146
|
-
// that the `when:` match was made against. Also carry the
|
|
1147
|
-
// unclassifiedFallback flag (M7) when the when: evaluation set it
|
|
1148
|
-
// to true; leave the field absent otherwise so decisions from
|
|
1149
|
-
// manifests without a `when:` policy stay byte-identical.
|
|
1150
|
-
const whenFallback = whenFallbackMap.get(policy.name);
|
|
1151
|
-
const decision = enriched
|
|
1152
|
-
? {
|
|
1153
|
-
...base,
|
|
1154
|
-
risk: enriched.risk,
|
|
1155
|
-
environment: enriched.environment,
|
|
1156
|
-
...(whenFallback === true ? { whenUnclassifiedFallback: true } : {}),
|
|
1157
|
-
}
|
|
1158
|
-
: base;
|
|
1159
|
-
decisions.push(decision);
|
|
1160
|
-
try {
|
|
1161
|
-
await options.ledger.record(decision, resolveSessionId(event.session_id));
|
|
1162
|
-
}
|
|
1163
|
-
catch (err) {
|
|
1164
|
-
// Audit-write failure must not block; the decision is still applied.
|
|
1165
|
-
// Surface the failure to stderr so a persistently-failing recorder
|
|
1166
|
-
// does not silently leave `harness audit` / `explain --trace` blind.
|
|
1167
|
-
// Goes to stderr to keep the stdout deny-JSON contract intact.
|
|
1168
|
-
(options.stderr ?? process.stderr).write(`harness runtime intercept: audit-write failed for ${decision.policyName}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
// First blocking decision wins the envelope. `deny` and
|
|
1173
|
-
// `require_approval` both abort (Phase 7 #6); the search order is the
|
|
1174
|
-
// manifest's policy order, same as Phase 4.
|
|
1175
|
-
const blocking = decisions.find(isBlockingDecision);
|
|
1176
|
-
if (blocking) {
|
|
1177
|
-
const sessionId = resolveSessionId(options.event.session_id);
|
|
1178
|
-
// Append the "to satisfy" hint so Claude Code's deny message tells
|
|
1179
|
-
// the operator (or the agent reading the same surface) what evidence
|
|
1180
|
-
// would unblock the gate, instead of just naming the missing tag.
|
|
1181
|
-
// The hint is content + window only; it does not prescribe a
|
|
1182
|
-
// recording verb so the deny path stays neutral on producer (see
|
|
1183
|
-
// agent-tasks/88ca4bb3 for why "use mcp__..." would be the wrong
|
|
1184
|
-
// suggestion when the engine is the source of that suggestion).
|
|
1185
|
-
//
|
|
1186
|
-
// It DOES name the sessionId namespace the entry must be written
|
|
1187
|
-
// under — this runtime session's id (the value shown), not the
|
|
1188
|
-
// agent-tasks task UUID. Naming an identity is not a producer
|
|
1189
|
-
// verb, so this stays compatible with the producer-neutrality above.
|
|
1190
|
-
// The two namespaces are a known production footgun: an entry written
|
|
1191
|
-
// under the agent-tasks task UUID never satisfies a harness runtime
|
|
1192
|
-
// gate, which keys off the runtime session id (2026-05-17 incident,
|
|
1193
|
-
// harness PRs #174/#175 — first attempt used the task UUID and was
|
|
1194
|
-
// rejected, second used the session id and passed).
|
|
1195
|
-
const hintSuffix = blocking.recordHint
|
|
1196
|
-
? ` To satisfy: ${blocking.recordHint}, under this runtime session's id \`${sessionId}\` (not the agent-tasks task UUID).`
|
|
1197
|
-
: "";
|
|
1198
|
-
// Opt-in producer block: when the policy declares `producers:` in
|
|
1199
|
-
// the manifest, render the structured remediation list (bash / mcp
|
|
1200
|
-
// / ask recipes) with ${VAR} placeholders substituted against the
|
|
1201
|
-
// same extract.values the ledger_tag was resolved with. Schema
|
|
1202
|
-
// validation guarantees at least one `mcp` producer per declared
|
|
1203
|
-
// list, so an agent stuck in a Bash lockout always has an ungated
|
|
1204
|
-
// recovery path. Policies without `producers:` get the legacy
|
|
1205
|
-
// neutral deny envelope unchanged (agent-tasks/3804b785).
|
|
1206
|
-
const blockingPolicy = matching.find((p) => p.name === blocking.policyName);
|
|
1207
|
-
// When the policy declares `ux:`, the agent-facing surface
|
|
1208
|
-
// becomes the plain-language `{cannot, required, run}` shape
|
|
1209
|
-
// instead of the engine-vocabulary deny envelope. The internal
|
|
1210
|
-
// decision (reason, recordHint, requiresEval, ledgerTag) is
|
|
1211
|
-
// unchanged and still recorded to the audit ledger above. The
|
|
1212
|
-
// producers block is suppressed when ux is declared because
|
|
1213
|
-
// `run:` is the canonical remedy surface and rendering both would
|
|
1214
|
-
// give the agent two different command suggestions.
|
|
1215
|
-
let reasonText;
|
|
1216
|
-
if (blocking.outcome === "deny-degraded") {
|
|
1217
|
-
// Degraded-specific envelope (task f1aea826). Deliberately takes
|
|
1218
|
-
// precedence over the policy's `ux:` surface: the operator-curated
|
|
1219
|
-
// ux text describes the MISSING-evidence case ("run the producer,
|
|
1220
|
-
// then retry"), which is actively misleading here — the evidence
|
|
1221
|
-
// could not be READ, so producing the tag cannot unblock anything
|
|
1222
|
-
// until the ledger is reachable again. The envelope names the real
|
|
1223
|
-
// cause (the transport-level reason, sanitised — it embeds output
|
|
1224
|
-
// from the grounding-mcp subprocess) and the posture, so nobody
|
|
1225
|
-
// debugs a phantom missing tag.
|
|
1226
|
-
//
|
|
1227
|
-
// The `risk.degraded_fail_posture: fail_open` opt-out is
|
|
1228
|
-
// deliberately NOT named here (review 2026-08-08, high finding):
|
|
1229
|
-
// this text is fed back to the very agent that was just blocked,
|
|
1230
|
-
// and nothing prevents that agent from editing the manifest — a
|
|
1231
|
-
// deny that includes its own disable recipe is not a gate. The
|
|
1232
|
-
// opt-out lives on operator surfaces only: the stderr diagnostic
|
|
1233
|
-
// (`formatDecisionDiagnostic`), docs/risk-gate.md, and the OKF
|
|
1234
|
-
// fail-posture matrix. Same reasoning as the producer-neutrality
|
|
1235
|
-
// norm above and the operator-only framing of `harness pause`.
|
|
1236
|
-
reasonText =
|
|
1237
|
-
`${blocking.policyName}: required evidence could not be read ` +
|
|
1238
|
-
`(evidence ledger degraded: ${sanitizeEnvelopeReason(blocking.reason)}). ` +
|
|
1239
|
-
`This ${blocking.enforcement} policy fails closed while its evidence ` +
|
|
1240
|
-
`source is unreadable; producing the required tag will not unblock it ` +
|
|
1241
|
-
`until the ledger is reachable again. Ask your operator to check ` +
|
|
1242
|
-
`grounding-mcp (harness doctor), then retry. Session: ${sessionId}.`;
|
|
1243
|
-
}
|
|
1244
|
-
else if (blocking.emptyIdentifier !== undefined) {
|
|
1245
|
-
// Empty ${REPO} / ${BRANCH}: the decision's own reason names the
|
|
1246
|
-
// state the agent can establish (`cd` / `git -C` / `git switch`).
|
|
1247
|
-
// Takes precedence over `ux:` (whose `cannot:` text would render
|
|
1248
|
-
// the blank identifier, "You cannot push branch yet.") and over
|
|
1249
|
-
// `producers:` / the record hint (both would send the agent to
|
|
1250
|
-
// produce a tag the gate will not read in this state). Names no
|
|
1251
|
-
// opt-out, same reasoning as the degraded envelope above.
|
|
1252
|
-
reasonText = `${blocking.policyName}: ${blocking.reason}`;
|
|
1253
|
-
}
|
|
1254
|
-
else if (blockingPolicy?.ux) {
|
|
1255
|
-
// The ux surface is operator-curated plain language. Task
|
|
1256
|
-
// 2929c5b7: a ux-declared policy's `cannot:` text used to be
|
|
1257
|
-
// rendered unchanged even when the match was a fail-closed
|
|
1258
|
-
// unclassified hit, not a genuine classification — so
|
|
1259
|
-
// gate-prod-destructive's "You cannot run this critical
|
|
1260
|
-
// destructive action against production." was shown verbatim for
|
|
1261
|
-
// an unrecognized READ (e.g. `cat`/`sed -n`/`curl`) the moment the
|
|
1262
|
-
// environment resolved to production, which is exactly the false
|
|
1263
|
-
// positive this task exists to fix. When `whenUnclassifiedFallback`
|
|
1264
|
-
// is set, a fallback-specific sentence is PREPENDED naming the real
|
|
1265
|
-
// cause before the operator's own `cannot:` text, which is left
|
|
1266
|
-
// byte-for-byte intact (added to, never replaced): the operator
|
|
1267
|
-
// still chose that wording for the genuine-classification case,
|
|
1268
|
-
// which keeps rendering unchanged. See docs/risk-gate.md,
|
|
1269
|
-
// "Unclassified actions and the fail-close rule".
|
|
1270
|
-
const uxText = renderAgentFacing(blockingPolicy.ux, {
|
|
1271
|
-
...blocking.extractValues,
|
|
1272
|
-
SESSION_ID: sessionId,
|
|
1273
|
-
});
|
|
1274
|
-
reasonText = blocking.whenUnclassifiedFallback
|
|
1275
|
-
? `${unclassifiedFallbackPrefix(enriched?.environment.name, blockingPolicy.when?.["risk.severity_at_least"])} ${uxText}`
|
|
1276
|
-
: uxText;
|
|
1277
|
-
}
|
|
1278
|
-
else {
|
|
1279
|
-
const producersBlock = renderProducers(blockingPolicy?.producers, blocking.extractValues);
|
|
1280
|
-
// M7: when the policy matched only because the action was
|
|
1281
|
-
// unclassified (fail-closed), insert an operator-facing note so a
|
|
1282
|
-
// deny caused by an unknown command is distinguishable from a deny
|
|
1283
|
-
// caused by a genuine critical-severity classification. The note is
|
|
1284
|
-
// placed after the base reason but BEFORE hintSuffix so the cause
|
|
1285
|
-
// precedes the remedy ("why this fired" before "how to unblock").
|
|
1286
|
-
// Neutral deny envelope only (not the ux path above).
|
|
1287
|
-
const unclassifiedClause = blocking.whenUnclassifiedFallback
|
|
1288
|
-
? " (matched via the fail-closed unclassified rule, not a real risk classification)"
|
|
1289
|
-
: "";
|
|
1290
|
-
reasonText = `${blocking.policyName}: ${blocking.reason}.${unclassifiedClause}${hintSuffix}${producersBlock}`;
|
|
1291
|
-
}
|
|
1292
|
-
const block = {
|
|
1293
|
-
decision: "block",
|
|
1294
|
-
reason: reasonText,
|
|
1295
|
-
};
|
|
1296
|
-
// permissionDecision is documented for PreToolUse only; emitting the
|
|
1297
|
-
// envelope on other events would invent a shape Claude Code does not
|
|
1298
|
-
// define, so we restrict it strictly. Other event kinds still block
|
|
1299
|
-
// via the top-level `decision: "block"`.
|
|
1300
|
-
if (options.event.hook_event_name === "PreToolUse") {
|
|
1301
|
-
block.hookSpecificOutput = {
|
|
1302
|
-
hookEventName: "PreToolUse",
|
|
1303
|
-
permissionDecision: "deny",
|
|
1304
|
-
// Duplicates `reason` intentionally: legacy consumers read the
|
|
1305
|
-
// top-level field, modern PreToolUse consumers read this one.
|
|
1306
|
-
permissionDecisionReason: reasonText,
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
return { decisions, blockJson: block };
|
|
1310
|
-
}
|
|
1311
|
-
return { decisions, blockJson: null };
|
|
1312
|
-
}
|
|
1313
|
-
//# sourceMappingURL=intercept.js.map
|