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