@guilz-dev/belay 0.3.0 → 0.6.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 (242) hide show
  1. package/README.md +7 -1
  2. package/dist/adapters/cursor/runtime-entry.js +1 -0
  3. package/dist/adapters/layouts/scope.js +3 -2
  4. package/dist/adapters/shared/gate-runtime.d.ts +12 -16
  5. package/dist/adapters/shared/gate-runtime.js +499 -43
  6. package/dist/bundle/claude-runtime.mjs +8004 -2676
  7. package/dist/bundle/codex-runtime.mjs +8021 -2683
  8. package/dist/bundle/cursor-runtime.mjs +7993 -2667
  9. package/dist/cli.js +180 -6
  10. package/dist/commands/approve.d.ts +1 -0
  11. package/dist/commands/approve.js +45 -26
  12. package/dist/commands/classify-for-report.js +1 -1
  13. package/dist/commands/config.d.ts +8 -0
  14. package/dist/commands/config.js +264 -58
  15. package/dist/commands/doctor.js +47 -2
  16. package/dist/commands/explain.js +17 -2
  17. package/dist/commands/harvest.d.ts +29 -0
  18. package/dist/commands/harvest.js +72 -0
  19. package/dist/commands/judge.d.ts +11 -1
  20. package/dist/commands/judge.js +24 -4
  21. package/dist/commands/metrics.js +34 -2
  22. package/dist/commands/quality.d.ts +38 -0
  23. package/dist/commands/quality.js +105 -0
  24. package/dist/commands/session.d.ts +20 -0
  25. package/dist/commands/session.js +51 -0
  26. package/dist/commands/simulate.d.ts +1 -0
  27. package/dist/commands/simulate.js +7 -2
  28. package/dist/commands/standing-allow.d.ts +10 -0
  29. package/dist/commands/standing-allow.js +26 -0
  30. package/dist/commands/tui.d.ts +48 -0
  31. package/dist/commands/tui.js +150 -0
  32. package/dist/config-io.js +59 -17
  33. package/dist/conformance/guarantee-posture.d.ts +21 -0
  34. package/dist/conformance/guarantee-posture.js +46 -0
  35. package/dist/conformance/guarantee-table.d.ts +1 -0
  36. package/dist/conformance/guarantee-table.js +14 -9
  37. package/dist/core/approval-replay-cli.d.ts +7 -0
  38. package/dist/core/approval-replay-cli.js +36 -0
  39. package/dist/core/approval-replay.d.ts +45 -0
  40. package/dist/core/approval-replay.js +141 -0
  41. package/dist/core/approval-service.d.ts +15 -0
  42. package/dist/core/approval-service.js +53 -4
  43. package/dist/core/approval.d.ts +26 -3
  44. package/dist/core/approval.js +61 -7
  45. package/dist/core/audit-analysis.d.ts +7 -1
  46. package/dist/core/audit-analysis.js +111 -0
  47. package/dist/core/audit-metrics.d.ts +7 -0
  48. package/dist/core/audit-metrics.js +24 -4
  49. package/dist/core/audit-query.d.ts +1 -0
  50. package/dist/core/audit-query.js +3 -0
  51. package/dist/core/audit-replay-context.d.ts +35 -0
  52. package/dist/core/audit-replay-context.js +88 -0
  53. package/dist/core/audit-types.d.ts +24 -1
  54. package/dist/core/audit-types.js +1 -1
  55. package/dist/core/capability/approval-state-mutation.d.ts +13 -0
  56. package/dist/core/capability/approval-state-mutation.js +92 -0
  57. package/dist/core/capability/approval-v3.d.ts +15 -0
  58. package/dist/core/capability/approval-v3.js +74 -0
  59. package/dist/core/capability/attestation.d.ts +13 -0
  60. package/dist/core/capability/attestation.js +55 -0
  61. package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
  62. package/dist/core/capability/boundary-attestation-sign.js +84 -0
  63. package/dist/core/capability/boundary-driver-container.d.ts +7 -0
  64. package/dist/core/capability/boundary-driver-container.js +122 -0
  65. package/dist/core/capability/boundary-driver.d.ts +29 -0
  66. package/dist/core/capability/boundary-driver.js +43 -0
  67. package/dist/core/capability/boundary-egress.d.ts +21 -0
  68. package/dist/core/capability/boundary-egress.js +109 -0
  69. package/dist/core/capability/boundary-grant-materialize.d.ts +13 -0
  70. package/dist/core/capability/boundary-grant-materialize.js +78 -0
  71. package/dist/core/capability/boundary-profile.d.ts +15 -0
  72. package/dist/core/capability/boundary-profile.js +25 -0
  73. package/dist/core/capability/boundary-run.d.ts +23 -0
  74. package/dist/core/capability/boundary-run.js +23 -0
  75. package/dist/core/capability/boundary-session.d.ts +57 -0
  76. package/dist/core/capability/boundary-session.js +129 -0
  77. package/dist/core/capability/capability-request-hash.d.ts +2 -0
  78. package/dist/core/capability/capability-request-hash.js +8 -0
  79. package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
  80. package/dist/core/capability/gate-latency-ratchet.js +18 -0
  81. package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
  82. package/dist/core/capability/gate-policy-shadow.js +108 -0
  83. package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
  84. package/dist/core/capability/gate-shadow-audit.js +1 -0
  85. package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
  86. package/dist/core/capability/gate-shadow-ratchet.js +81 -0
  87. package/dist/core/capability/grant-lease.d.ts +9 -0
  88. package/dist/core/capability/grant-lease.js +46 -0
  89. package/dist/core/capability/grant-loader.d.ts +12 -0
  90. package/dist/core/capability/grant-loader.js +23 -0
  91. package/dist/core/capability/grant-match.d.ts +6 -0
  92. package/dist/core/capability/grant-match.js +93 -0
  93. package/dist/core/capability/grant.d.ts +16 -0
  94. package/dist/core/capability/grant.js +16 -0
  95. package/dist/core/capability/index.d.ts +10 -1
  96. package/dist/core/capability/index.js +8 -0
  97. package/dist/core/capability/limits.d.ts +7 -0
  98. package/dist/core/capability/limits.js +55 -0
  99. package/dist/core/capability/paths.d.ts +2 -2
  100. package/dist/core/capability/paths.js +9 -9
  101. package/dist/core/capability/policy-bridge.d.ts +18 -0
  102. package/dist/core/capability/policy-bridge.js +51 -0
  103. package/dist/core/capability/policy-engine.d.ts +70 -0
  104. package/dist/core/capability/policy-engine.js +476 -0
  105. package/dist/core/capability/policy-types.d.ts +20 -0
  106. package/dist/core/capability/policy-types.js +1 -0
  107. package/dist/core/capability/request.d.ts +45 -0
  108. package/dist/core/capability/request.js +1 -0
  109. package/dist/core/capability/resolver.d.ts +4 -0
  110. package/dist/core/capability/resolver.js +4 -0
  111. package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
  112. package/dist/core/capability/trusted-workspace-roots.js +149 -0
  113. package/dist/core/capability/types.d.ts +11 -1
  114. package/dist/core/capability-approval.d.ts +2 -1
  115. package/dist/core/capability-approval.js +100 -2
  116. package/dist/core/classify-subagent.d.ts +2 -1
  117. package/dist/core/classify-subagent.js +40 -22
  118. package/dist/core/classify-tool.js +157 -64
  119. package/dist/core/config.d.ts +32 -3
  120. package/dist/core/config.js +105 -3
  121. package/dist/core/gate-contract.d.ts +3 -0
  122. package/dist/core/gate-contract.js +3 -0
  123. package/dist/core/gate-engine.js +12 -7
  124. package/dist/core/harvest.d.ts +53 -0
  125. package/dist/core/harvest.js +276 -0
  126. package/dist/core/index.d.ts +9 -3
  127. package/dist/core/index.js +6 -2
  128. package/dist/core/judge-doctor.d.ts +3 -0
  129. package/dist/core/judge-doctor.js +90 -11
  130. package/dist/core/judge-fallback-hints.d.ts +13 -0
  131. package/dist/core/judge-fallback-hints.js +108 -0
  132. package/dist/core/path-utils.d.ts +12 -0
  133. package/dist/core/path-utils.js +60 -7
  134. package/dist/core/reclassify.d.ts +3 -0
  135. package/dist/core/reclassify.js +44 -14
  136. package/dist/core/recovery/capability.d.ts +3 -0
  137. package/dist/core/recovery/capability.js +22 -0
  138. package/dist/core/recovery/fail-closed.d.ts +6 -0
  139. package/dist/core/recovery/fail-closed.js +25 -0
  140. package/dist/core/recovery/index.d.ts +3 -0
  141. package/dist/core/recovery/index.js +2 -0
  142. package/dist/core/recovery/types.d.ts +20 -0
  143. package/dist/core/recovery/types.js +1 -0
  144. package/dist/core/replay-scrub.d.ts +9 -0
  145. package/dist/core/replay-scrub.js +43 -0
  146. package/dist/core/shell-tokenizer.d.ts +2 -0
  147. package/dist/core/shell-tokenizer.js +82 -23
  148. package/dist/core/standing-allow.d.ts +50 -0
  149. package/dist/core/standing-allow.js +178 -0
  150. package/dist/core/transactional/diff-evaluator.js +4 -1
  151. package/dist/core/transactional/eligibility.js +4 -0
  152. package/dist/core/transactional/git-worktree.d.ts +10 -3
  153. package/dist/core/transactional/git-worktree.js +81 -11
  154. package/dist/core/transactional/reasons.d.ts +1 -0
  155. package/dist/core/transactional/reasons.js +4 -0
  156. package/dist/core/transactional/runner.js +22 -6
  157. package/dist/core/transactional/types.d.ts +4 -0
  158. package/dist/core/types.d.ts +36 -3
  159. package/dist/core/verdict/adapter.d.ts +3 -4
  160. package/dist/core/verdict/adapter.js +19 -6
  161. package/dist/core/verdict/containment.d.ts +4 -3
  162. package/dist/core/verdict/containment.js +17 -12
  163. package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
  164. package/dist/core/verdict/cursor-acp-client.js +345 -0
  165. package/dist/core/verdict/egress-classify.d.ts +2 -2
  166. package/dist/core/verdict/egress-classify.js +2 -2
  167. package/dist/core/verdict/git-classifier.d.ts +26 -0
  168. package/dist/core/verdict/git-classifier.js +533 -0
  169. package/dist/core/verdict/judge-audit.d.ts +1 -0
  170. package/dist/core/verdict/judge-audit.js +33 -1
  171. package/dist/core/verdict/judge-baseline.d.ts +20 -0
  172. package/dist/core/verdict/judge-baseline.js +49 -0
  173. package/dist/core/verdict/judge-broker-service.d.ts +42 -0
  174. package/dist/core/verdict/judge-broker-service.js +291 -0
  175. package/dist/core/verdict/judge-catalog.js +1 -0
  176. package/dist/core/verdict/judge-cli-fingerprint.d.ts +6 -0
  177. package/dist/core/verdict/judge-cli-fingerprint.js +67 -0
  178. package/dist/core/verdict/judge-cli.d.ts +23 -0
  179. package/dist/core/verdict/judge-cli.js +139 -16
  180. package/dist/core/verdict/judge-factory.js +13 -4
  181. package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
  182. package/dist/core/verdict/judge-provider-matrix.js +66 -0
  183. package/dist/core/verdict/judge-runtime-config.d.ts +44 -0
  184. package/dist/core/verdict/judge-runtime-config.js +96 -0
  185. package/dist/core/verdict/judge-session-broker.d.ts +48 -0
  186. package/dist/core/verdict/judge-session-broker.js +200 -0
  187. package/dist/core/verdict/judge-session-guard.d.ts +27 -0
  188. package/dist/core/verdict/judge-session-guard.js +91 -0
  189. package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
  190. package/dist/core/verdict/judge-session-kill-switch.js +43 -0
  191. package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
  192. package/dist/core/verdict/judge-session-mutex.js +23 -0
  193. package/dist/core/verdict/judge-shadow.d.ts +24 -0
  194. package/dist/core/verdict/judge-shadow.js +116 -0
  195. package/dist/core/verdict/judge-transport.d.ts +44 -0
  196. package/dist/core/verdict/judge-transport.js +268 -0
  197. package/dist/core/verdict/judge.d.ts +14 -14
  198. package/dist/core/verdict/judge.js +5 -67
  199. package/dist/core/verdict/launcher-resolve.d.ts +2 -0
  200. package/dist/core/verdict/launcher-resolve.js +16 -0
  201. package/dist/core/verdict/parser.js +6 -1
  202. package/dist/core/verdict/prescan.d.ts +15 -0
  203. package/dist/core/verdict/prescan.js +68 -0
  204. package/dist/core/verdict/shell-policy.d.ts +25 -0
  205. package/dist/core/verdict/shell-policy.js +40 -0
  206. package/dist/core/verdict/shell-semantics.d.ts +24 -0
  207. package/dist/core/verdict/shell-semantics.js +225 -0
  208. package/dist/core/verdict/types.d.ts +26 -1
  209. package/dist/core/verdict/verdict.js +300 -179
  210. package/dist/corpus/adversarial-probe.d.ts +86 -0
  211. package/dist/corpus/adversarial-probe.js +220 -0
  212. package/dist/corpus/evaluate.d.ts +45 -12
  213. package/dist/corpus/evaluate.js +61 -8
  214. package/dist/corpus/gate-latency-budget.d.ts +39 -0
  215. package/dist/corpus/gate-latency-budget.js +33 -0
  216. package/dist/corpus/gates.d.ts +35 -0
  217. package/dist/corpus/gates.js +81 -0
  218. package/dist/corpus/judge-accuracy.d.ts +8 -0
  219. package/dist/corpus/judge-accuracy.js +39 -0
  220. package/dist/corpus/must-allow-commands.d.ts +5 -0
  221. package/dist/corpus/must-allow-commands.js +13 -0
  222. package/dist/corpus/mutators.d.ts +28 -0
  223. package/dist/corpus/mutators.js +162 -0
  224. package/dist/corpus/ratchet.d.ts +42 -0
  225. package/dist/corpus/ratchet.js +116 -0
  226. package/dist/corpus/runtime-match.d.ts +19 -0
  227. package/dist/corpus/runtime-match.js +43 -0
  228. package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
  229. package/dist/corpus/standing-allow-catalog.generated.js +99 -0
  230. package/dist/corpus/types.d.ts +47 -0
  231. package/dist/corpus/types.js +157 -0
  232. package/dist/installer.js +4 -1
  233. package/dist/judge-broker-daemon.d.ts +1 -0
  234. package/dist/judge-broker-daemon.js +137 -0
  235. package/dist/services/sandbox-service.d.ts +5 -0
  236. package/dist/services/sandbox-service.js +30 -2
  237. package/dist/types.d.ts +2 -2
  238. package/dist/version.d.ts +1 -1
  239. package/dist/version.js +1 -1
  240. package/package.json +7 -3
  241. package/skills/belay/SKILL.md +7 -1
  242. package/skills/belay/belay-approve.md +17 -0
@@ -1,12 +1,17 @@
1
- import { resolveMutationTarget } from '../path-utils.js';
2
- import { extractRedirectTargets, tokenizeShell } from '../shell-tokenizer.js';
1
+ import path from 'node:path';
2
+ import { policyDecisionToLegacyReason } from '../capability/policy-bridge.js';
3
+ import { policyDecisionRequiresAsk } from '../capability/policy-engine.js';
4
+ import { canonicalPath, resolveMutationTarget } from '../path-utils.js';
5
+ import { extractRedirectTargets, isFdDuplication, isRedirectOperator, tokenizeShell, } from '../shell-tokenizer.js';
3
6
  import { analyzePathTargets, cwdRelative, isDestructiveHighStakesMutation, resolveTrustedPath, touchesProtectedRoot, } from './containment.js';
4
7
  import { classifyEgressTool } from './egress-classify.js';
5
8
  import { verdictFingerprint } from './fingerprint.js';
6
- import { mutationPrescanRequiresAsk, prescanInterpreterCode, tier1RequiresAsk } from './judge.js';
7
- import { isRoutineLauncher, resolveLauncherRecipe } from './launcher-resolve.js';
9
+ import { isReadOnlyLauncherInvocation, isRoutineLauncher, resolveLauncherRecipe, } from './launcher-resolve.js';
8
10
  import { allowFromCustomOverride, askFromCustomExternal, customAllowMatch, customExternalMatch, } from './overrides.js';
9
11
  import { extractRecursiveScript, isBareInterpreter, isVariableIndirectHead, parseSegment, peelTransparentWrappers, redactCommand, segmentOpacity, splitTopLevelSegments, substitutionInners, } from './parser.js';
12
+ import { mutationPrescanRequiresAsk, prescanInterpreterCode, tier1RequiresAsk } from './prescan.js';
13
+ import { evaluateSegmentShellPolicy, shellPolicyMetadata, } from './shell-policy.js';
14
+ import { analyzeShellCommandSemantics, isPureReadOnlySemantics, LOCAL_MUTATION_KEYS, } from './shell-semantics.js';
10
15
  const DEFAULT_MAX_DEPTH = 8;
11
16
  const TIER0_EXTERNAL_KEYS = new Set([
12
17
  'git push',
@@ -20,7 +25,6 @@ const TIER0_EXTERNAL_KEYS = new Set([
20
25
  'supabase',
21
26
  'scp',
22
27
  'ssh',
23
- 'rsync',
24
28
  ]);
25
29
  const TIER0_EXTERNAL_HEADS = new Set([
26
30
  'dropdb',
@@ -30,66 +34,6 @@ const TIER0_EXTERNAL_HEADS = new Set([
30
34
  'mongosh',
31
35
  'redis-cli',
32
36
  ]);
33
- const READ_ONLY_KEYS = new Set([
34
- 'cat',
35
- 'cd',
36
- 'echo',
37
- 'git diff',
38
- 'git log',
39
- 'git rev-parse',
40
- 'git show',
41
- 'git status',
42
- 'head',
43
- 'ls',
44
- 'pwd',
45
- 'rg',
46
- 'sort',
47
- 'tail',
48
- 'wc',
49
- 'which',
50
- 'find',
51
- ]);
52
- const PURE_READ_ONLY_KEYS = new Set([
53
- 'echo',
54
- 'git diff',
55
- 'git log',
56
- 'git rev-parse',
57
- 'git show',
58
- 'git status',
59
- 'pwd',
60
- 'which',
61
- ]);
62
- const LOCAL_MUTATION_KEYS = new Set([
63
- 'chmod',
64
- 'cp',
65
- 'git add',
66
- 'git clean',
67
- 'git commit',
68
- 'git mv',
69
- 'git reset',
70
- 'mkdir',
71
- 'mv',
72
- 'rm',
73
- 'sed',
74
- 'tee',
75
- 'touch',
76
- 'truncate',
77
- ]);
78
- /** Routine local build/test runners resolved from launcher recipes. */
79
- const LOCAL_ROUTINE_HEADS = new Set([
80
- 'tsc',
81
- 'vitest',
82
- 'vite',
83
- 'webpack',
84
- 'esbuild',
85
- 'rollup',
86
- 'jest',
87
- 'mocha',
88
- 'cargo',
89
- 'go',
90
- 'make',
91
- 'cmake',
92
- ]);
93
37
  const BELAY_SELF_COMMANDS = new Set(['approve', 'revoke']);
94
38
  const FIND_DANGEROUS_FLAGS = new Set(['-delete', '-exec', '-execdir', '-ok', '-okdir']);
95
39
  function isFindDangerous(tokens) {
@@ -126,7 +70,7 @@ async function evaluateSubstitutions(command, context, depth) {
126
70
  }
127
71
  worst = worst ? combineInternal(worst, innerVerdict) : innerVerdict;
128
72
  }
129
- if (!worst) {
73
+ if (!worst || worst.permission === 'allow') {
130
74
  return null;
131
75
  }
132
76
  return {
@@ -177,26 +121,46 @@ function combineInternal(left, right) {
177
121
  : right.reason,
178
122
  signals: [...new Set([...left.signals, ...right.signals])],
179
123
  judgeTrace: right.judgeTrace ?? left.judgeTrace,
124
+ capabilityRequests: [...(left.capabilityRequests ?? []), ...(right.capabilityRequests ?? [])],
125
+ authorizationDecision: worsePermission(left.permission, right.permission) === 'ask'
126
+ ? right.permission === 'ask'
127
+ ? (right.authorizationDecision ?? left.authorizationDecision)
128
+ : left.authorizationDecision
129
+ : (right.authorizationDecision ?? left.authorizationDecision),
180
130
  };
181
131
  }
182
132
  function askVerdict(params) {
183
133
  return { ...params, permission: 'ask' };
184
134
  }
185
- function allowVerdict(params) {
186
- return { ...params, permission: 'allow' };
135
+ function resolveShellPathTargets(pathArgs, context) {
136
+ if (!context.cwd) {
137
+ return [];
138
+ }
139
+ return pathArgs.map((target) => {
140
+ const resolved = (context.trustedCwd ? resolveTrustedPath(target, context.cwd, context.trustedCwd) : null) ??
141
+ resolveMutationTarget(target, context.cwd);
142
+ if (!resolved) {
143
+ const joined = path.isAbsolute(target) ? target : path.join(context.cwd, target);
144
+ return canonicalPath(joined);
145
+ }
146
+ return canonicalPath(resolved);
147
+ });
187
148
  }
188
- function withJudgeTrace(verdict, judgeTrace) {
189
- if (!judgeTrace) {
190
- return verdict;
149
+ function withShellPolicyMetadata(input, existing = {}) {
150
+ if (existing.capabilityRequests?.length) {
151
+ return existing;
191
152
  }
192
- return { ...verdict, judgeTrace };
153
+ return shellPolicyMetadata(input);
154
+ }
155
+ function allowVerdict(params) {
156
+ return { ...params, permission: 'allow' };
193
157
  }
194
158
  function extractPathArgs(tokens) {
195
159
  const redirects = extractRedirectTargets(tokens);
196
160
  const args = [...redirects];
197
161
  for (let index = 1; index < tokens.length; index += 1) {
198
162
  const token = tokens[index];
199
- if (!token || token.startsWith('-') || token === '>' || token === '>>' || token === '<') {
163
+ if (!token || token.startsWith('-') || isRedirectOperator(token) || isFdDuplication(token)) {
200
164
  continue;
201
165
  }
202
166
  if (redirects.includes(token)) {
@@ -209,8 +173,15 @@ function extractPathArgs(tokens) {
209
173
  function isVariableOrOpaquePathToken(token) {
210
174
  return token.includes('$') || token.includes('`');
211
175
  }
212
- function isPureReadOnlySegment(segment) {
213
- return PURE_READ_ONLY_KEYS.has(segment.key) || PURE_READ_ONLY_KEYS.has(segment.head);
176
+ function pathContextFromSemantics(context, semantics) {
177
+ const cwd = semantics.gitWorkTree ?? semantics.effectiveCwd;
178
+ if (!cwd || cwd === context.cwd) {
179
+ return context;
180
+ }
181
+ return { ...context, cwd };
182
+ }
183
+ function isPureReadOnlySegment(segment, semantics) {
184
+ return isPureReadOnlySemantics(segment, semantics);
214
185
  }
215
186
  function updateChainState(command, state) {
216
187
  const segment = parseSegment(command);
@@ -233,10 +204,13 @@ function updateChainState(command, state) {
233
204
  trustedCwd: true,
234
205
  };
235
206
  }
236
- function tier0ExternalMatch(key, head, tokens) {
207
+ function tier0ExternalMatch(key, head, tokens, signals = []) {
237
208
  if (TIER0_EXTERNAL_KEYS.has(key)) {
238
209
  return true;
239
210
  }
211
+ if (signals.includes('git.push')) {
212
+ return true;
213
+ }
240
214
  if (TIER0_EXTERNAL_HEADS.has(head)) {
241
215
  return true;
242
216
  }
@@ -248,9 +222,6 @@ function tier0ExternalMatch(key, head, tokens) {
248
222
  tokens.some((t) => t === '--push' || t.startsWith('--output=type=registry')))) {
249
223
  return true;
250
224
  }
251
- if (head === 'git' && tokens[1] === 'push') {
252
- return true;
253
- }
254
225
  if (head === 'terraform' && tokens[1] === 'apply') {
255
226
  return true;
256
227
  }
@@ -304,6 +275,7 @@ function tier0HighStakesRm(tokens, context) {
304
275
  cwd: context.cwd,
305
276
  repoRoot: context.repoRoot,
306
277
  trustedCwd: context.trustedCwd,
278
+ trustedWorkspaceRoots: context.trustedWorkspaceRoots,
307
279
  sensitivePaths: context.sensitivePaths,
308
280
  protectedArtifactRoots: context.protectedArtifactRoots,
309
281
  });
@@ -336,7 +308,7 @@ function tier0HighStakesRm(tokens, context) {
336
308
  const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
337
309
  resolveMutationTarget(target, context.cwd);
338
310
  if (resolved &&
339
- isDestructiveHighStakesMutation(head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots)) {
311
+ isDestructiveHighStakesMutation(head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots, context.trustedWorkspaceRoots)) {
340
312
  destructiveHighStakes = true;
341
313
  break;
342
314
  }
@@ -354,6 +326,39 @@ function tier0HighStakesRm(tokens, context) {
354
326
  }
355
327
  return null;
356
328
  }
329
+ function verdictFromOpacityPolicy(params) {
330
+ const { request, decision } = evaluateSegmentShellPolicy({
331
+ command: params.command,
332
+ segmentHead: params.segmentHead,
333
+ effect: params.effect,
334
+ location: params.location,
335
+ opacity: params.opacity,
336
+ pathArgs: params.pathArgs,
337
+ signals: params.signals,
338
+ context: params.context,
339
+ });
340
+ const base = {
341
+ location: params.location,
342
+ opacity: params.opacity,
343
+ effect: params.effect,
344
+ confidence: 'deterministic',
345
+ signals: [...params.signals, ...decision.signals],
346
+ capabilityRequests: [request],
347
+ authorizationDecision: decision,
348
+ };
349
+ if (policyDecisionRequiresAsk(decision)) {
350
+ return {
351
+ permission: 'ask',
352
+ reason: policyDecisionToLegacyReason(decision),
353
+ ...base,
354
+ };
355
+ }
356
+ return {
357
+ permission: 'allow',
358
+ reason: policyDecisionToLegacyReason(decision),
359
+ ...base,
360
+ };
361
+ }
357
362
  async function evaluateSegment(command, context, depth) {
358
363
  const maxDepth = context.maxRecursionDepth ?? DEFAULT_MAX_DEPTH;
359
364
  if (depth > maxDepth) {
@@ -368,23 +373,15 @@ async function evaluateSegment(command, context, depth) {
368
373
  }
369
374
  const opacity = segmentOpacity(command);
370
375
  if (opacity === 'unparseable') {
371
- if (context.unparseableShell === 'deny') {
372
- return askVerdict({
373
- location: 'unknown',
374
- opacity: 'unparseable',
375
- effect: 'unknown',
376
- confidence: 'deterministic',
377
- reason: 'unparseable_shell',
378
- signals: ['unparseable_shell'],
379
- });
380
- }
381
- return allowVerdict({
382
- location: 'unknown',
376
+ return verdictFromOpacityPolicy({
377
+ command,
378
+ segmentHead: parseSegment(command).head,
383
379
  opacity: 'unparseable',
384
380
  effect: 'unknown',
385
- confidence: 'deterministic',
386
- reason: 'unparseable_shell',
381
+ location: 'unknown',
382
+ pathArgs: [],
387
383
  signals: ['unparseable_shell'],
384
+ context,
388
385
  });
389
386
  }
390
387
  const substitutionResult = await evaluateSubstitutions(command, context, depth);
@@ -394,13 +391,15 @@ async function evaluateSegment(command, context, depth) {
394
391
  const tokens = tokenizeShell(command);
395
392
  const { tokens: peeled, xargsStdinOpaque } = peelTransparentWrappers(tokens);
396
393
  if (xargsStdinOpaque || isBareInterpreter(tokens)) {
397
- return askVerdict({
398
- location: 'unknown',
394
+ return verdictFromOpacityPolicy({
395
+ command,
396
+ segmentHead: parseSegment(command).head,
399
397
  opacity: 'opaque',
400
398
  effect: 'unknown',
401
- confidence: 'deterministic',
402
- reason: 'opaque_execution',
399
+ location: 'unknown',
400
+ pathArgs: [],
403
401
  signals: ['opaque_execution'],
402
+ context,
404
403
  });
405
404
  }
406
405
  const segment = parseSegment(command);
@@ -429,13 +428,28 @@ async function evaluateSegment(command, context, depth) {
429
428
  if (recursiveScript) {
430
429
  const prescan = prescanInterpreterCode(recursiveScript);
431
430
  if (prescan && tier1RequiresAsk(prescan)) {
431
+ const { request, decision } = evaluateSegmentShellPolicy({
432
+ command: recursiveScript,
433
+ segmentHead: segment.head,
434
+ effect: 'unknown',
435
+ location: 'unknown',
436
+ opacity: 'recursive',
437
+ pathArgs: [],
438
+ signals: ['interpreter_secret_prescan', prescan.reason],
439
+ context,
440
+ });
441
+ const legacyReason = policyDecisionRequiresAsk(decision)
442
+ ? policyDecisionToLegacyReason(decision)
443
+ : 'interpreter_secret_prescan';
432
444
  return askVerdict({
433
445
  location: 'unknown',
434
446
  opacity: 'recursive',
435
447
  effect: 'unknown',
436
448
  confidence: 'deterministic',
437
- reason: 'interpreter_secret_prescan',
438
- signals: ['interpreter_secret_prescan'],
449
+ reason: legacyReason,
450
+ signals: ['interpreter_secret_prescan', prescan.reason, ...decision.signals],
451
+ capabilityRequests: [request],
452
+ authorizationDecision: decision,
439
453
  });
440
454
  }
441
455
  const innerVerdict = await evaluateSegment(recursiveScript, context, depth + 1);
@@ -480,8 +494,10 @@ async function evaluateSegment(command, context, depth) {
480
494
  }
481
495
  let innerVerdict = null;
482
496
  for (const recipe of resolution.recipes) {
483
- const evaluated = await evaluateSegment(recipe, context, depth + 1);
484
- innerVerdict = innerVerdict ? combineInternal(innerVerdict, evaluated) : evaluated;
497
+ for (const sub of splitTopLevelSegments(recipe)) {
498
+ const evaluated = await evaluateSegment(sub, context, depth + 1);
499
+ innerVerdict = innerVerdict ? combineInternal(innerVerdict, evaluated) : evaluated;
500
+ }
485
501
  }
486
502
  if (!innerVerdict) {
487
503
  return askVerdict({
@@ -499,38 +515,78 @@ async function evaluateSegment(command, context, depth) {
499
515
  signals: [...innerVerdict.signals, resolution.reason],
500
516
  };
501
517
  }
502
- const egressClass = classifyEgressTool(segment.head, peeled);
518
+ const semantics = analyzeShellCommandSemantics(peeled, segment, context.cwd);
519
+ const pathContext = pathContextFromSemantics(context, semantics);
520
+ if (semantics.requiresAsk) {
521
+ const { request, decision } = evaluateSegmentShellPolicy({
522
+ command,
523
+ segmentHead: segment.head,
524
+ effect: 'local_mutation',
525
+ location: 'repo_local',
526
+ opacity: 'transparent',
527
+ pathArgs: semantics.pathTargets,
528
+ signals: semantics.requiresAsk.signals,
529
+ context: pathContext,
530
+ });
531
+ return askVerdict({
532
+ location: 'repo_local',
533
+ opacity: 'transparent',
534
+ effect: 'local_mutation',
535
+ confidence: 'deterministic',
536
+ reason: semantics.requiresAsk.reason,
537
+ signals: semantics.requiresAsk.signals,
538
+ capabilityRequests: [request],
539
+ authorizationDecision: decision,
540
+ });
541
+ }
542
+ const egressClass = semantics.egressClass ?? classifyEgressTool(segment.head, peeled);
503
543
  if (egressClass === 'destructive') {
544
+ const { request, decision } = evaluateSegmentShellPolicy({
545
+ command,
546
+ segmentHead: segment.head,
547
+ effect: 'remote_mutation',
548
+ location: 'external',
549
+ opacity: 'transparent',
550
+ egressClass: 'destructive',
551
+ pathArgs: [],
552
+ signals: ['tier0_external', segment.head],
553
+ context,
554
+ });
504
555
  return askVerdict({
505
556
  location: 'external',
506
557
  opacity: 'transparent',
507
558
  effect: 'remote_mutation',
508
559
  confidence: 'deterministic',
509
560
  reason: 'tier0_external',
510
- signals: ['tier0_external', segment.head],
561
+ signals: ['tier0_external', segment.head, ...decision.signals],
562
+ capabilityRequests: [request],
563
+ authorizationDecision: decision,
511
564
  });
512
565
  }
513
- if (egressClass === 'read') {
514
- return allowVerdict({
566
+ if (tier0ExternalMatch(semantics.normalizedKey ?? segment.key, segment.head, peeled, semantics.signals)) {
567
+ const tier0Key = semantics.normalizedKey ?? segment.key;
568
+ const { request, decision } = evaluateSegmentShellPolicy({
569
+ command,
570
+ segmentHead: segment.head,
571
+ effect: 'remote_mutation',
515
572
  location: 'external',
516
573
  opacity: 'transparent',
517
- effect: 'read_only',
518
- confidence: 'deterministic',
519
- reason: 'egress_read',
520
- signals: ['egress_read', segment.head],
574
+ pathArgs: [],
575
+ signals: ['tier0_external', tier0Key, ...semantics.signals],
576
+ context: pathContext,
521
577
  });
522
- }
523
- if (tier0ExternalMatch(segment.key, segment.head, peeled)) {
524
578
  return askVerdict({
525
579
  location: 'external',
526
580
  opacity: 'transparent',
527
581
  effect: 'remote_mutation',
528
582
  confidence: 'deterministic',
529
583
  reason: 'tier0_external',
530
- signals: ['tier0_external', segment.key],
584
+ signals: ['tier0_external', tier0Key, ...decision.signals],
585
+ capabilityRequests: [request],
586
+ authorizationDecision: decision,
531
587
  });
532
588
  }
533
- const rmVerdict = tier0HighStakesRm(peeled, context);
589
+ const rmVerdict = tier0HighStakesRm(peeled, pathContext);
534
590
  if (rmVerdict) {
535
591
  return rmVerdict;
536
592
  }
@@ -544,28 +600,29 @@ async function evaluateSegment(command, context, depth) {
544
600
  signals: ['belay_control_plane_command', segment.head],
545
601
  });
546
602
  }
547
- let effect = 'unknown';
548
- if (READ_ONLY_KEYS.has(segment.key) || READ_ONLY_KEYS.has(segment.head)) {
603
+ let effect = semantics.effect;
604
+ if (isReadOnlyLauncherInvocation(peeled)) {
549
605
  effect = 'read_only';
550
606
  }
551
- else if (LOCAL_MUTATION_KEYS.has(segment.key) || LOCAL_MUTATION_KEYS.has(segment.head)) {
552
- effect = 'local_mutation';
553
- }
554
- else if (LOCAL_ROUTINE_HEADS.has(segment.head)) {
555
- effect = 'local_mutation';
556
- }
557
- const pathArgs = extractPathArgs(peeled);
558
- if (extractRedirectTargets(peeled).length > 0 && effect === 'read_only') {
559
- effect = 'local_mutation';
560
- }
607
+ const pathArgs = semantics.pathTargets;
608
+ const policyPathArgs = [...new Set([...pathArgs, ...(semantics.scopeTargets ?? [])])];
561
609
  const pathAnalysis = analyzePathTargets({
562
- targets: pathArgs,
563
- cwd: context.cwd,
564
- repoRoot: context.repoRoot,
565
- trustedCwd: context.trustedCwd,
566
- sensitivePaths: context.sensitivePaths,
567
- protectedArtifactRoots: context.protectedArtifactRoots,
610
+ targets: policyPathArgs,
611
+ cwd: pathContext.cwd,
612
+ repoRoot: pathContext.repoRoot,
613
+ trustedCwd: pathContext.trustedCwd,
614
+ trustedWorkspaceRoots: pathContext.trustedWorkspaceRoots,
615
+ sensitivePaths: pathContext.sensitivePaths,
616
+ protectedArtifactRoots: pathContext.protectedArtifactRoots,
568
617
  });
618
+ // Path-less read/mutation in a trusted cwd: assume repo-local (e.g. git status, tsc --noEmit).
619
+ const effectiveLocation = policyPathArgs.length === 0 &&
620
+ pathContext.trustedCwd &&
621
+ pathContext.cwd &&
622
+ (effect === 'read_only' || effect === 'local_mutation') &&
623
+ pathAnalysis.location === 'unknown'
624
+ ? 'repo_local'
625
+ : pathAnalysis.location;
569
626
  if (!context.trustedCwd || !context.cwd) {
570
627
  if (opacity === 'opaque' || effect === 'unknown' || effect === 'local_mutation') {
571
628
  return askVerdict({
@@ -577,7 +634,7 @@ async function evaluateSegment(command, context, depth) {
577
634
  signals: ['missing_trusted_cwd'],
578
635
  });
579
636
  }
580
- if (effect === 'read_only' && !isPureReadOnlySegment(segment)) {
637
+ if (effect === 'read_only' && !isPureReadOnlySegment(segment, semantics)) {
581
638
  return askVerdict({
582
639
  location: 'unknown',
583
640
  opacity,
@@ -592,9 +649,9 @@ async function evaluateSegment(command, context, depth) {
592
649
  context.protectedArtifactRoots &&
593
650
  context.protectedArtifactRoots.length > 0) {
594
651
  for (const target of pathArgs) {
595
- const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
596
- resolveMutationTarget(target, context.cwd);
597
- if (resolved && touchesProtectedRoot(resolved, context.protectedArtifactRoots)) {
652
+ const resolved = resolveTrustedPath(target, pathContext.cwd, pathContext.trustedCwd) ??
653
+ resolveMutationTarget(target, pathContext.cwd);
654
+ if (resolved && touchesProtectedRoot(resolved, pathContext.protectedArtifactRoots ?? [])) {
598
655
  return askVerdict({
599
656
  location: pathAnalysis.location,
600
657
  opacity: 'transparent',
@@ -609,10 +666,10 @@ async function evaluateSegment(command, context, depth) {
609
666
  if (effect === 'local_mutation' || effect === 'unknown') {
610
667
  let destructiveHighStakes = false;
611
668
  for (const target of pathArgs) {
612
- const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
613
- resolveMutationTarget(target, context.cwd);
669
+ const resolved = resolveTrustedPath(target, pathContext.cwd, pathContext.trustedCwd) ??
670
+ resolveMutationTarget(target, pathContext.cwd);
614
671
  if (resolved &&
615
- isDestructiveHighStakesMutation(segment.head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots)) {
672
+ isDestructiveHighStakesMutation(segment.head, resolved, pathContext.repoRoot, pathContext.sensitivePaths, pathContext.protectedArtifactRoots, pathContext.trustedWorkspaceRoots)) {
616
673
  destructiveHighStakes = true;
617
674
  break;
618
675
  }
@@ -661,17 +718,43 @@ async function evaluateSegment(command, context, depth) {
661
718
  signals: ['unknown_location_mutation'],
662
719
  });
663
720
  }
721
+ const resolvedPathTargets = resolveShellPathTargets(policyPathArgs, pathContext);
722
+ const semanticSignals = semantics.signals;
723
+ const buildPolicyInput = (overrides) => ({
724
+ command: recursiveScript ?? command,
725
+ segmentHead: segment.head,
726
+ opacity,
727
+ egressClass: egressClass ?? undefined,
728
+ pathArgs: policyPathArgs,
729
+ resolvedPathTargets,
730
+ signals: [...semanticSignals, ...pathAnalysis.signals],
731
+ context: pathContext,
732
+ ...overrides,
733
+ });
664
734
  const outsideMutation = pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed';
665
735
  const mutationPrescan = (effect === 'local_mutation' || effect === 'unknown') && pathArgs.length > 0
666
736
  ? mutationPrescanRequiresAsk({
667
737
  targets: pathArgs,
668
- cwd: context.cwd,
669
- repoRoot: context.repoRoot,
670
- trustedCwd: context.trustedCwd,
671
- sensitivePaths: context.sensitivePaths,
738
+ cwd: pathContext.cwd,
739
+ repoRoot: pathContext.repoRoot,
740
+ trustedCwd: pathContext.trustedCwd,
741
+ trustedWorkspaceRoots: pathContext.trustedWorkspaceRoots,
742
+ sensitivePaths: pathContext.sensitivePaths,
672
743
  })
673
744
  : null;
674
745
  if (mutationPrescan) {
746
+ const { request, decision } = evaluateSegmentShellPolicy(buildPolicyInput({
747
+ effect: 'local_mutation',
748
+ location: pathAnalysis.location === 'unknown'
749
+ ? 'unknown'
750
+ : pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed'
751
+ ? pathAnalysis.location
752
+ : 'repo_local',
753
+ signals: ['tier1_catastrophic', mutationPrescan.reason, ...pathAnalysis.signals],
754
+ }));
755
+ const legacyReason = policyDecisionRequiresAsk(decision)
756
+ ? policyDecisionToLegacyReason(decision, { outsideMutation, effect: 'local_mutation' })
757
+ : 'tier1_catastrophic';
675
758
  return askVerdict({
676
759
  location: pathAnalysis.location === 'unknown'
677
760
  ? 'unknown'
@@ -681,24 +764,28 @@ async function evaluateSegment(command, context, depth) {
681
764
  opacity,
682
765
  effect: 'local_mutation',
683
766
  confidence: 'deterministic',
684
- reason: 'tier1_catastrophic',
685
- signals: ['tier1_catastrophic', mutationPrescan.reason],
767
+ reason: legacyReason,
768
+ signals: ['tier1_catastrophic', mutationPrescan.reason, ...decision.signals],
769
+ capabilityRequests: [request],
770
+ authorizationDecision: decision,
686
771
  });
687
772
  }
688
- const needsTier1 = effect === 'unknown' ||
773
+ const needsPolicy = effect === 'unknown' ||
689
774
  TIER0_EXTERNAL_HEADS.has(segment.head) ||
690
775
  egressClass === 'ambiguous' ||
776
+ egressClass === 'read' ||
691
777
  (outsideMutation && effect !== 'read_only');
692
- let tier1Trace;
693
- if (needsTier1) {
694
- const tier1Text = recursiveScript ?? command;
695
- const tier1 = await context.judge.evaluate({
696
- text: tier1Text,
697
- context: { cwd: context.cwd, repoRoot: context.repoRoot },
698
- innerCode: recursiveScript ?? undefined,
699
- });
700
- tier1Trace = context.judge.lastTrace;
701
- if (tier1RequiresAsk(tier1)) {
778
+ let policyMetadata = {};
779
+ if (needsPolicy) {
780
+ const { request, decision } = evaluateSegmentShellPolicy(buildPolicyInput({
781
+ effect,
782
+ location: pathAnalysis.location,
783
+ }));
784
+ policyMetadata = {
785
+ capabilityRequests: [request],
786
+ authorizationDecision: decision,
787
+ };
788
+ if (policyDecisionRequiresAsk(decision)) {
702
789
  return askVerdict({
703
790
  location: pathAnalysis.location === 'unknown'
704
791
  ? 'unknown'
@@ -706,72 +793,104 @@ async function evaluateSegment(command, context, depth) {
706
793
  ? pathAnalysis.location
707
794
  : 'repo_local',
708
795
  opacity,
709
- effect: !tier1.local_recoverable ? 'remote_mutation' : effect,
710
- confidence: 'llm',
711
- reason: 'tier1_catastrophic',
712
- signals: ['tier1_catastrophic', tier1.reason],
713
- judgeTrace: tier1Trace,
796
+ effect: decision.reason === 'external_effect' || decision.reason === 'network_connect'
797
+ ? 'remote_mutation'
798
+ : effect,
799
+ confidence: 'deterministic',
800
+ reason: policyDecisionToLegacyReason(decision, { outsideMutation, effect }),
801
+ signals: ['policy_required', ...decision.signals],
802
+ ...policyMetadata,
714
803
  });
715
804
  }
716
805
  }
717
806
  if (outsideMutation && effect !== 'read_only') {
718
- return withJudgeTrace(allowVerdict({
807
+ const policyAllowsOutside = policyMetadata.authorizationDecision?.outcome === 'allow' &&
808
+ (policyMetadata.authorizationDecision.matchedRule === 'grant.exact' ||
809
+ policyMetadata.authorizationDecision.matchedRule === 'boundary.verified' ||
810
+ policyMetadata.authorizationDecision.matchedRule === 'builtin.trusted_workspace');
811
+ if (!policyAllowsOutside) {
812
+ const legacyReason = policyMetadata.authorizationDecision &&
813
+ policyDecisionRequiresAsk(policyMetadata.authorizationDecision)
814
+ ? policyDecisionToLegacyReason(policyMetadata.authorizationDecision, {
815
+ outsideMutation,
816
+ effect,
817
+ })
818
+ : 'outside_repo_mutation';
819
+ return askVerdict({
820
+ location: pathAnalysis.location,
821
+ opacity,
822
+ effect: 'local_mutation',
823
+ confidence: 'deterministic',
824
+ reason: legacyReason,
825
+ signals: ['outside_repo_mutation', ...pathAnalysis.signals],
826
+ ...policyMetadata,
827
+ });
828
+ }
829
+ return allowVerdict({
719
830
  location: pathAnalysis.location,
720
831
  opacity,
721
832
  effect: 'local_mutation',
722
- confidence: tier1Trace ? 'llm' : 'deterministic',
833
+ confidence: 'deterministic',
723
834
  reason: 'repo_outside_local_mutation',
724
835
  signals: ['repo_outside_local_mutation', ...pathAnalysis.signals],
725
- }), tier1Trace);
836
+ ...policyMetadata,
837
+ });
726
838
  }
727
- if (pathAnalysis.location === 'repo_local' &&
839
+ if (effectiveLocation === 'repo_local' &&
728
840
  (effect === 'read_only' || effect === 'local_mutation') &&
729
841
  opacity !== 'opaque') {
730
- return withJudgeTrace(allowVerdict({
842
+ return allowVerdict({
731
843
  location: 'repo_local',
732
844
  opacity,
733
845
  effect,
734
846
  confidence: 'assumed_repo_local',
735
847
  reason: effect === 'read_only' ? 'read_only' : 'repo_local_mutation',
736
848
  signals: effect === 'read_only' ? ['read_only'] : ['repo_local_mutation'],
737
- }), tier1Trace);
849
+ ...withShellPolicyMetadata(buildPolicyInput({ effect, location: 'repo_local' }), policyMetadata),
850
+ });
738
851
  }
739
852
  if (effect === 'read_only') {
740
- const readOnlyLocation = context.trustedCwd && context.cwd
741
- ? pathAnalysis.location === 'unknown'
853
+ const readOnlyLocation = pathContext.trustedCwd && pathContext.cwd
854
+ ? effectiveLocation === 'unknown'
742
855
  ? 'repo_local'
743
- : pathAnalysis.location
856
+ : effectiveLocation
744
857
  : 'unknown';
745
- return withJudgeTrace(allowVerdict({
858
+ return allowVerdict({
746
859
  location: readOnlyLocation,
747
860
  opacity,
748
861
  effect: 'read_only',
749
862
  confidence: context.trustedCwd && context.cwd ? 'assumed_repo_local' : 'deterministic',
750
863
  reason: 'read_only',
751
864
  signals: ['read_only'],
752
- }), tier1Trace);
865
+ ...withShellPolicyMetadata(buildPolicyInput({ effect: 'read_only', location: readOnlyLocation }), policyMetadata),
866
+ });
753
867
  }
754
868
  if (allowOverride) {
755
- return withJudgeTrace(allowFromCustomOverride(opacity), tier1Trace);
869
+ return allowFromCustomOverride(opacity);
756
870
  }
757
871
  if (context.unknownLocalEffect === 'allow_flagged') {
758
- return withJudgeTrace(allowVerdict({
759
- location: pathAnalysis.location === 'unknown' ? 'repo_local' : pathAnalysis.location,
872
+ return allowVerdict({
873
+ location: effectiveLocation === 'unknown' ? 'repo_local' : effectiveLocation,
760
874
  opacity,
761
875
  effect: 'unknown',
762
876
  confidence: 'assumed_repo_local',
763
877
  reason: 'unknown_local_effect',
764
878
  signals: ['unknown_local_effect'],
765
- }), tier1Trace);
879
+ ...withShellPolicyMetadata(buildPolicyInput({
880
+ effect: 'unknown',
881
+ location: effectiveLocation === 'unknown' ? 'repo_local' : effectiveLocation,
882
+ }), policyMetadata),
883
+ });
766
884
  }
767
- return withJudgeTrace(askVerdict({
768
- location: pathAnalysis.location,
885
+ return askVerdict({
886
+ location: effectiveLocation,
769
887
  opacity,
770
888
  effect,
771
889
  confidence: 'deterministic',
772
890
  reason: 'unknown_local_effect',
773
891
  signals: ['unknown_local_effect'],
774
- }), tier1Trace);
892
+ ...withShellPolicyMetadata(buildPolicyInput({ effect, location: pathAnalysis.location }), policyMetadata),
893
+ });
775
894
  }
776
895
  function toVerdictResult(internal, command, context, fingerprintCwd = context.cwd) {
777
896
  const commandRedacted = redactCommand(command);
@@ -787,6 +906,8 @@ function toVerdictResult(internal, command, context, fingerprintCwd = context.cw
787
906
  fingerprint: verdictFingerprint(relative, commandRedacted),
788
907
  signals: internal.signals,
789
908
  judgeTrace: internal.judgeTrace,
909
+ capabilityRequests: internal.capabilityRequests,
910
+ authorizationDecision: internal.authorizationDecision,
790
911
  };
791
912
  }
792
913
  export async function verdict(command, context) {