@guilz-dev/belay 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +39 -20
  2. package/dist/adapters/claude/runtime-entry.js +54 -25
  3. package/dist/adapters/shared/gate-runtime.d.ts +7 -0
  4. package/dist/adapters/shared/gate-runtime.js +443 -225
  5. package/dist/bundle/claude-runtime.mjs +13027 -7271
  6. package/dist/bundle/codex-runtime.mjs +12700 -6977
  7. package/dist/bundle/cursor-runtime.mjs +12695 -6977
  8. package/dist/cli.js +37 -1
  9. package/dist/commands/approve.js +34 -10
  10. package/dist/commands/doctor.js +58 -0
  11. package/dist/commands/dogfood.js +1 -1
  12. package/dist/commands/explain.js +2 -2
  13. package/dist/commands/harvest.d.ts +0 -1
  14. package/dist/commands/harvest.js +1 -4
  15. package/dist/commands/metrics.js +6 -0
  16. package/dist/commands/quality.js +1 -3
  17. package/dist/commands/recovery-checkpoints.d.ts +164 -0
  18. package/dist/commands/recovery-checkpoints.js +347 -0
  19. package/dist/commands/revoke.js +19 -7
  20. package/dist/commands/status.js +7 -0
  21. package/dist/config-io.js +49 -18
  22. package/dist/conformance/guarantee-table.js +137 -9
  23. package/dist/conformance/types.d.ts +2 -0
  24. package/dist/core/approval-replay.d.ts +1 -1
  25. package/dist/core/approval-replay.js +4 -1
  26. package/dist/core/approval-service.d.ts +6 -1
  27. package/dist/core/approval-service.js +86 -27
  28. package/dist/core/approval.d.ts +2 -0
  29. package/dist/core/approval.js +6 -3
  30. package/dist/core/audit-metrics.d.ts +1 -0
  31. package/dist/core/audit-metrics.js +12 -4
  32. package/dist/core/audit-types.d.ts +16 -0
  33. package/dist/core/capability/approval-state-mutation.d.ts +19 -0
  34. package/dist/core/capability/approval-state-mutation.js +76 -3
  35. package/dist/core/capability/approval-v3.d.ts +18 -1
  36. package/dist/core/capability/approval-v3.js +63 -23
  37. package/dist/core/capability/attestation.d.ts +3 -0
  38. package/dist/core/capability/attestation.js +7 -0
  39. package/dist/core/capability/boundary-driver-container.d.ts +12 -0
  40. package/dist/core/capability/boundary-driver-container.js +101 -34
  41. package/dist/core/capability/boundary-driver.js +1 -0
  42. package/dist/core/capability/boundary-grant-materialize.d.ts +1 -0
  43. package/dist/core/capability/boundary-grant-materialize.js +15 -0
  44. package/dist/core/capability/boundary-run.d.ts +25 -0
  45. package/dist/core/capability/boundary-run.js +37 -9
  46. package/dist/core/capability/boundary-session.d.ts +4 -0
  47. package/dist/core/capability/boundary-session.js +9 -0
  48. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  49. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  50. package/dist/core/capability/gate-policy-shadow.js +23 -1
  51. package/dist/core/capability/grant-consumption.d.ts +4 -0
  52. package/dist/core/capability/grant-consumption.js +11 -0
  53. package/dist/core/capability/grant-lease.d.ts +36 -1
  54. package/dist/core/capability/grant-lease.js +213 -16
  55. package/dist/core/capability/grant-match.js +3 -0
  56. package/dist/core/capability/index.d.ts +2 -2
  57. package/dist/core/capability/index.js +1 -1
  58. package/dist/core/capability/policy-engine.d.ts +38 -1
  59. package/dist/core/capability/policy-engine.js +388 -15
  60. package/dist/core/capability/request.d.ts +12 -0
  61. package/dist/core/capability/resolver.d.ts +0 -1
  62. package/dist/core/capability/resolver.js +0 -1
  63. package/dist/core/config.d.ts +20 -0
  64. package/dist/core/config.js +62 -2
  65. package/dist/core/effect-ir/audit.d.ts +9 -0
  66. package/dist/core/effect-ir/audit.js +87 -0
  67. package/dist/core/effect-ir/build.d.ts +31 -0
  68. package/dist/core/effect-ir/build.js +281 -0
  69. package/dist/core/effect-ir/index.d.ts +13 -0
  70. package/dist/core/effect-ir/index.js +9 -0
  71. package/dist/core/effect-ir/normalize.d.ts +4 -0
  72. package/dist/core/effect-ir/normalize.js +110 -0
  73. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  74. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  75. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  76. package/dist/core/effect-ir/package-exec.js +292 -0
  77. package/dist/core/effect-ir/policy.d.ts +14 -0
  78. package/dist/core/effect-ir/policy.js +164 -0
  79. package/dist/core/effect-ir/shell-build.d.ts +43 -0
  80. package/dist/core/effect-ir/shell-build.js +77 -0
  81. package/dist/core/effect-ir/shell-lower.d.ts +14 -0
  82. package/dist/core/effect-ir/shell-lower.js +1509 -0
  83. package/dist/core/effect-ir/types.d.ts +62 -0
  84. package/dist/core/effect-ir/types.js +1 -0
  85. package/dist/core/egress/allowlist.d.ts +1 -0
  86. package/dist/core/egress/allowlist.js +8 -0
  87. package/dist/core/egress-approval.js +45 -51
  88. package/dist/core/gate-contract.d.ts +2 -0
  89. package/dist/core/gate-contract.js +2 -0
  90. package/dist/core/gate-engine.js +82 -25
  91. package/dist/core/git-resource-identity.d.ts +26 -0
  92. package/dist/core/git-resource-identity.js +284 -0
  93. package/dist/core/harvest.d.ts +3 -7
  94. package/dist/core/harvest.js +4 -27
  95. package/dist/core/index.d.ts +5 -1
  96. package/dist/core/index.js +3 -1
  97. package/dist/core/judge-cloud-consent.js +29 -15
  98. package/dist/core/network-endpoint.d.ts +10 -0
  99. package/dist/core/network-endpoint.js +51 -0
  100. package/dist/core/path-utils.js +42 -23
  101. package/dist/core/process-runner.d.ts +10 -0
  102. package/dist/core/process-runner.js +124 -0
  103. package/dist/core/recover-advice.js +2 -1
  104. package/dist/core/recovery/artifact-store.d.ts +29 -0
  105. package/dist/core/recovery/artifact-store.js +336 -0
  106. package/dist/core/recovery/checkpoint.d.ts +25 -0
  107. package/dist/core/recovery/checkpoint.js +260 -0
  108. package/dist/core/recovery/index.d.ts +3 -1
  109. package/dist/core/recovery/index.js +2 -0
  110. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  111. package/dist/core/recovery/operator-guidance.js +39 -0
  112. package/dist/core/recovery/reconcile.d.ts +3 -0
  113. package/dist/core/recovery/reconcile.js +48 -0
  114. package/dist/core/recovery/resource-identity.d.ts +4 -0
  115. package/dist/core/recovery/resource-identity.js +34 -0
  116. package/dist/core/recovery/restore.d.ts +13 -0
  117. package/dist/core/recovery/restore.js +114 -0
  118. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  119. package/dist/core/recovery/snapshot-node.js +261 -0
  120. package/dist/core/recovery/types.d.ts +85 -1
  121. package/dist/core/shell-substitution.d.ts +1 -0
  122. package/dist/core/shell-substitution.js +61 -0
  123. package/dist/core/standing-allow.d.ts +1 -2
  124. package/dist/core/standing-allow.js +3 -20
  125. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  126. package/dist/core/transactional/apply-observed-changes.js +275 -0
  127. package/dist/core/transactional/backend-selector.d.ts +8 -0
  128. package/dist/core/transactional/backend-selector.js +118 -0
  129. package/dist/core/transactional/backend.d.ts +55 -0
  130. package/dist/core/transactional/backend.js +1 -0
  131. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  132. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  133. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  134. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  135. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  136. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  137. package/dist/core/transactional/file-clone.d.ts +15 -0
  138. package/dist/core/transactional/file-clone.js +139 -0
  139. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  140. package/dist/core/transactional/file-tree-path.js +50 -0
  141. package/dist/core/transactional/file-tree.d.ts +41 -0
  142. package/dist/core/transactional/file-tree.js +186 -0
  143. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  144. package/dist/core/transactional/git-worktree-backend.js +50 -0
  145. package/dist/core/transactional/git-worktree.d.ts +9 -11
  146. package/dist/core/transactional/git-worktree.js +24 -119
  147. package/dist/core/transactional/index.d.ts +11 -0
  148. package/dist/core/transactional/index.js +10 -0
  149. package/dist/core/transactional/reasons.js +1 -0
  150. package/dist/core/transactional/runner.js +157 -25
  151. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  152. package/dist/core/transactional/snapshot-node.js +87 -0
  153. package/dist/core/transactional/types.d.ts +17 -1
  154. package/dist/core/types.d.ts +13 -1
  155. package/dist/core/verdict/adapter.js +23 -15
  156. package/dist/core/verdict/containment.js +4 -0
  157. package/dist/core/verdict/egress-classify.d.ts +12 -0
  158. package/dist/core/verdict/egress-classify.js +746 -0
  159. package/dist/core/verdict/git-classifier.d.ts +12 -0
  160. package/dist/core/verdict/git-classifier.js +320 -0
  161. package/dist/core/verdict/launcher-resolve.js +79 -37
  162. package/dist/core/verdict/parser.d.ts +2 -0
  163. package/dist/core/verdict/parser.js +95 -1
  164. package/dist/core/verdict/types.d.ts +6 -2
  165. package/dist/core/verdict/verdict.js +103 -935
  166. package/dist/corpus/must-allow-commands.d.ts +2 -1
  167. package/dist/corpus/must-allow-commands.js +2 -1
  168. package/dist/corpus/runtime-match.d.ts +2 -1
  169. package/dist/corpus/runtime-match.js +2 -1
  170. package/dist/corpus/types.d.ts +2 -2
  171. package/dist/templates.js +7 -2
  172. package/dist/types.d.ts +11 -0
  173. package/dist/version.d.ts +1 -1
  174. package/dist/version.js +1 -1
  175. package/package.json +2 -2
  176. package/skills/belay/SKILL.md +8 -3
  177. package/skills/belay/belay-approve.md +10 -7
  178. package/dist/core/verdict/overrides.d.ts +0 -7
  179. package/dist/core/verdict/overrides.js +0 -37
  180. package/dist/core/verdict/shell-policy.d.ts +0 -25
  181. package/dist/core/verdict/shell-policy.js +0 -40
  182. package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
  183. package/dist/corpus/standing-allow-catalog.generated.js +0 -99
package/README.md CHANGED
@@ -86,7 +86,8 @@ npx @guilz-dev/belay status
86
86
 
87
87
  Fresh installs default to **fail-closed** shell policy: unknown or unparseable
88
88
  shell commands are denied until approved. Use `belay explain` to inspect a
89
- verdict and `overrides.allow` to whitelist commands you trust.
89
+ verdict. Correct inaccurate EffectPlan semantics or resource scope; otherwise
90
+ approve the exact request for one-shot, resource-scoped authorization.
90
91
 
91
92
  ## How it works
92
93
 
@@ -109,6 +110,16 @@ When an action is denied, approve the **next matching action once** by sending:
109
110
  /belay-approve <approval-id>
110
111
  ```
111
112
 
113
+ With the default `approval.flow: one_step`, an editor approval immediately replays the exact denied
114
+ shell action; no follow-up prompt is required. Tool and subagent approvals still require retrying
115
+ the original action unchanged. The one-shot grant is claimed before shell replay, so a failed or
116
+ timed-out replay requires a fresh approval. Replay runs through the configured `BoundaryDriver`.
117
+ After replay succeeds, the approval prompt continues the current host turn so the agent can report
118
+ the result and resume its workflow without another user message.
119
+ You may put a follow-up instruction on the lines after `/belay-approve <id>`; Belay replays the
120
+ approved shell action first, then lets the remaining prompt continue. CLI replay remains explicit:
121
+ `belay approve <approval-id> --replay`.
122
+
112
123
  Approvals are one-shot and expire after 15 minutes by default. Every decision is
113
124
  written to `.cursor/belay/audit.ndjson`, `.claude/belay/audit.ndjson`, or
114
125
  `.codex/belay/audit.ndjson` (depending on the adapter).
@@ -128,12 +139,12 @@ Belay is a layered hook gate, not a static denylist. Higher layers are opt-in.
128
139
  | **L3** Prediction | Policy rules + command heuristics | default |
129
140
  | **L4** Approval | Human one-shot / scoped approvals | default |
130
141
 
131
- - L3 command lists are **not security boundaries** by themselves — see
132
- [docs/ops/semver-policy.md](./docs/ops/semver-policy.md) and
133
- [docs/guarantee-table.md](./docs/guarantee-table.md).
134
- - At default L3, **local-recoverable mutations outside the repo** pass after Tier1
135
- (e.g. IDE plan files). To deny repo-external writes at the OS boundary, enable L1-full
136
- (`sandbox.runtime` ≠ `none`). Tier1 requires a working judge (local Ollama by default).
142
+ - Normalized shell authorization uses only canonical `EffectPlan` requirements.
143
+ Command lists, legacy overrides, corpus labels, and shell standing allows are inert.
144
+ - Payload-free reads are `allow`; reversible repository-local writes (including implicit
145
+ download output) are `allow_flagged`. Outside-repository writes, external mutation,
146
+ explicit payload/file/secret sends, high-stakes effects, and partial/indeterminate plans
147
+ require approval. See [ADR-004](./docs/adr/ADR-004-effectplan-shell-authority.md).
137
148
  - Adversarial resistance requires the full L1 stack:
138
149
  `belay init --preset l1-full-recommended`, verified with `belay sandbox status`.
139
150
 
@@ -178,18 +189,18 @@ npx @guilz-dev/belay dogfood # mode: audit, unknownLocalEffect: deny
178
189
  # ...run normal agent work...
179
190
  npx @guilz-dev/belay metrics # review what would have been blocked
180
191
  npx @guilz-dev/belay status # check readiness
181
- # tune overrides.allow with `belay explain`, then:
192
+ # inspect EffectPlan semantics and resource scope with `belay explain`, then:
182
193
  npx @guilz-dev/belay dogfood --enforce
183
194
  ```
184
195
 
185
196
  ## Configuration
186
197
 
187
- `belay.config.json` uses `version: 3`. v1/v2 configs migrate automatically on
198
+ `belay.config.json` uses `version: 4`. v1/v2/v3 configs migrate automatically on
188
199
  load.
189
200
 
190
201
  ```json
191
202
  {
192
- "version": 3,
203
+ "version": 4,
193
204
  "mode": "enforce",
194
205
  "gates": {
195
206
  "shell": true,
@@ -204,10 +215,6 @@ load.
204
215
  "policy": {
205
216
  "unknownLocalEffect": "allow_flagged"
206
217
  },
207
- "overrides": {
208
- "allow": ["pnpm release:staging"],
209
- "external": ["./scripts/release.sh"]
210
- },
211
218
  "redaction": {
212
219
  "maskApprovalIds": true,
213
220
  "maskBearerTokens": true,
@@ -228,17 +235,25 @@ load.
228
235
 
229
236
  Notable settings:
230
237
 
231
- - **`policy.unknownLocalEffect: "allow_flagged"`** (fresh default) — after Tier1
232
- says recoverable, structurally unknown local commands run with an audit flag. Use
233
- `"deny"` (via `belay dogfood`) to ask on those commands instead.
238
+ - **`policy.unknownLocalEffect: "allow_flagged"`** (fresh default) — compatibility fallback
239
+ for legacy, non-EffectPlan classification paths. It cannot loosen a normalized shell
240
+ EffectPlan: partial/indeterminate plans and outside-repository writes still ask. Use
241
+ `"deny"` (via `belay dogfood`) for a stricter fallback.
234
242
  - **`classifier.strictChains: true`** (default) — scans every `&&`, `|`, and `;`
235
- segment and keeps the strictest verdict. Override lists match exact command or
236
- segment keys only.
243
+ segment into the EffectPlan and keeps the strictest policy projection. Legacy
244
+ `overrides.allow` / `overrides.external` lists are accepted only for config
245
+ compatibility; shell authorization ignores them and `belay doctor` warns when
246
+ either list is non-empty.
237
247
  - **`controlPlane.enabled: true`** — stores approval state under
238
248
  `~/.config/belay/` (or `XDG_CONFIG_HOME/belay`), shared across repos for the
239
249
  current OS user. `upgrade` migrates repo-local approvals in; disabling merges
240
250
  them back. File-mutation tools and shell redirects cannot write control-plane
241
251
  paths while it is enabled.
252
+ - **`policy.transactional.checkpoint.enabled: true`** — after enabling the
253
+ transactional git-worktree runner, persist repo-local pre-images and expose them through
254
+ `belay recover list`. Restore is conflict-checked and always requires a signed
255
+ out-of-band, exact one-shot approval. Network, remote Git, databases, processes, and
256
+ repo-external effects are outside this guarantee.
242
257
  - **Cloud judge** — configure with `belay config` (interactive) or `belay config set judge.providerId <id>`.
243
258
  Providers: `ollama`, `codex`, `claude`, `cursor`. **Provider** is the vendor/service
244
259
  (`judge.providerId`); **driver** is the API compatibility layer (`judge.provider`:
@@ -281,7 +296,11 @@ belay doctor [--fix] # check (and repair) floor health
281
296
  belay status # show install scope / skill-only state
282
297
  belay metrics # would-block / verdict summary
283
298
  belay report # audit log report
284
- belay recover [--command "rm important.ts"] # find recovery candidates
299
+ belay recover [advice] [--command "rm important.ts"] # advisory candidates only
300
+ belay recover status # checkpoint backend and state counts
301
+ belay recover list # proven repo-local recovery points
302
+ belay recover show <checkpoint-id>
303
+ belay recover apply <checkpoint-id> # signed OOB exact one-shot approval required
285
304
  belay explain -- <shell-command> # inspect a verdict
286
305
  belay explain --kind subagent -- "deploy to production"
287
306
  belay explain --kind tool --tool Write -- .env
@@ -1,4 +1,7 @@
1
1
  import process from 'node:process';
2
+ import { effectPlanAuditFields } from '../../core/effect-ir/audit.js';
3
+ import { buildCapabilityEffectPlan } from '../../core/effect-ir/build.js';
4
+ import { hashValue, toolFingerprint } from '../../core/fingerprint.js';
2
5
  import { unnormalizedGateVerdict } from '../../core/gate-contract.js';
3
6
  import { claudeLayout } from '../layouts/claude.js';
4
7
  import { appendObservedAudit, createDefaultGateRuntimeDeps, evaluateGatedAction, gateVerdictToClaudePreToolUseResponse, gateVerdictToClaudeUserPromptResponse, processApprovalPrompt, resolveGateConfig, } from '../shared/gate-runtime.js';
@@ -22,6 +25,23 @@ async function readStdinJson() {
22
25
  function jsonResponse(value) {
23
26
  process.stdout.write(`${JSON.stringify(value)}\n`);
24
27
  }
28
+ function claudeFallbackToolVerdict(params) {
29
+ return {
30
+ ...unnormalizedGateVerdict({
31
+ reason: params.reason,
32
+ mode: params.mode,
33
+ user_message: params.userMessage,
34
+ agent_message: params.agentMessage,
35
+ }),
36
+ effectPlan: buildCapabilityEffectPlan({
37
+ actionKind: 'tool',
38
+ summary: params.toolName,
39
+ inputFingerprint: hashValue(`claude-fallback:${params.reason}:${toolFingerprint(params.toolName, params.payload, params.repoRoot)}`),
40
+ requests: [],
41
+ effectFree: false,
42
+ }),
43
+ };
44
+ }
25
45
  async function loadRuntimeContext(cwd) {
26
46
  const repoRoot = findRepoRoot(cwd, claudeLayout);
27
47
  const configPath = claudeLayout.configPath(repoRoot);
@@ -120,13 +140,10 @@ export async function runBeforeSubmitPromptHook() {
120
140
  jsonResponse(gateVerdictToClaudeUserPromptResponse(result));
121
141
  }
122
142
  catch {
123
- jsonResponse({
124
- hookSpecificOutput: {
125
- hookEventName: 'UserPromptSubmit',
126
- continue: false,
127
- user_message: 'belay failed while processing approval state. Run belay doctor, then retry.',
128
- },
129
- });
143
+ jsonResponse(gateVerdictToClaudeUserPromptResponse({
144
+ continue: false,
145
+ user_message: 'belay failed while processing approval state. Run belay doctor, then retry.',
146
+ }));
130
147
  }
131
148
  }
132
149
  export async function runShellGateHook() {
@@ -167,6 +184,15 @@ export async function runToolGateHook(_eventName) {
167
184
  const toolName = String(payload.tool_name ?? '');
168
185
  const mappedKind = mapClaudeToolName(toolName);
169
186
  if (mappedKind === 'mcp') {
187
+ const verdict = claudeFallbackToolVerdict({
188
+ toolName,
189
+ payload,
190
+ repoRoot: ctx.repoRoot,
191
+ reason: 'unsupported_mcp_tool',
192
+ mode: ctx.config.mode,
193
+ userMessage: 'belay blocked this MCP tool because Claude MCP payloads are not normalized safely yet.',
194
+ agentMessage: 'Belay denied this MCP tool because its payload shape is unsupported.',
195
+ });
170
196
  await deps.appendAudit(ctx, {
171
197
  event: 'preToolUse',
172
198
  kind: 'tool',
@@ -176,17 +202,21 @@ export async function runToolGateHook(_eventName) {
176
202
  wouldBlock: true,
177
203
  permission: 'deny',
178
204
  summary: toolName,
179
- });
180
- const verdict = unnormalizedGateVerdict({
181
- reason: 'unsupported_mcp_tool',
182
- mode: ctx.config.mode,
183
- user_message: 'belay blocked this MCP tool because Claude MCP payloads are not normalized safely yet.',
184
- agent_message: 'Belay denied this MCP tool because its payload shape is unsupported.',
205
+ ...effectPlanAuditFields(verdict.effectPlan),
185
206
  });
186
207
  jsonResponse(gateVerdictToClaudePreToolUseResponse(verdict));
187
208
  return;
188
209
  }
189
210
  if (!mappedKind) {
211
+ const verdict = claudeFallbackToolVerdict({
212
+ toolName,
213
+ payload,
214
+ repoRoot: ctx.repoRoot,
215
+ reason: 'unmapped_tool',
216
+ mode: ctx.config.mode,
217
+ userMessage: 'belay does not recognize this tool action. Run belay doctor, then retry.',
218
+ agentMessage: 'Belay denied this action because the tool could not be normalized.',
219
+ });
190
220
  await deps.appendAudit(ctx, {
191
221
  event: 'preToolUse',
192
222
  kind: 'tool',
@@ -196,18 +226,22 @@ export async function runToolGateHook(_eventName) {
196
226
  wouldBlock: true,
197
227
  permission: 'deny',
198
228
  summary: toolName,
199
- });
200
- const verdict = unnormalizedGateVerdict({
201
- reason: 'unmapped_tool',
202
- mode: ctx.config.mode,
203
- user_message: 'belay does not recognize this tool action. Run belay doctor, then retry.',
204
- agent_message: 'Belay denied this action because the tool could not be normalized.',
229
+ ...effectPlanAuditFields(verdict.effectPlan),
205
230
  });
206
231
  jsonResponse(gateVerdictToClaudePreToolUseResponse(verdict));
207
232
  return;
208
233
  }
209
234
  const normalizedPayload = normalizeClaudeToolPayload(toolName, payload);
210
235
  if (!normalizedPayload) {
236
+ const verdict = claudeFallbackToolVerdict({
237
+ toolName,
238
+ payload,
239
+ repoRoot: ctx.repoRoot,
240
+ reason: 'normalization_failed',
241
+ mode: ctx.config.mode,
242
+ userMessage: 'belay could not normalize this Claude tool payload. Run belay doctor, then retry.',
243
+ agentMessage: 'Belay denied this action because the Claude tool payload could not be normalized.',
244
+ });
211
245
  await deps.appendAudit(ctx, {
212
246
  event: 'preToolUse',
213
247
  kind: 'tool',
@@ -217,12 +251,7 @@ export async function runToolGateHook(_eventName) {
217
251
  wouldBlock: true,
218
252
  permission: 'deny',
219
253
  summary: toolName,
220
- });
221
- const verdict = unnormalizedGateVerdict({
222
- reason: 'normalization_failed',
223
- mode: ctx.config.mode,
224
- user_message: 'belay could not normalize this Claude tool payload. Run belay doctor, then retry.',
225
- agent_message: 'Belay denied this action because the Claude tool payload could not be normalized.',
254
+ ...effectPlanAuditFields(verdict.effectPlan),
226
255
  });
227
256
  jsonResponse(gateVerdictToClaudePreToolUseResponse(verdict));
228
257
  return;
@@ -23,6 +23,13 @@ export interface GateRuntimeDeps {
23
23
  state: ApprovalStateFile;
24
24
  }>;
25
25
  writeApprovals: (filePath: string, state: ApprovalStateFile) => Promise<void>;
26
+ replayApprovedShell: (ctx: GateRuntimeContext, command: string, cwd: string, timeoutMs: number) => Promise<{
27
+ exitCode: number | null;
28
+ timedOut: boolean;
29
+ signal: string | null;
30
+ stdout?: string;
31
+ stderr?: string;
32
+ }>;
26
33
  }
27
34
  export declare function createDefaultGateRuntimeDeps(): GateRuntimeDeps;
28
35
  export declare function resolveGateConfig(ctx: Pick<GateRuntimeContext, 'layout' | 'repoRoot' | 'configPath'>, deps: GateRuntimeDeps): Promise<BelayConfigV3>;