@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
@@ -0,0 +1,149 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { belayStateDir } from '../config.js';
5
+ import { canonicalPath, containingGitRoot, pathWithinRoot, resolveWorkspaceRootMatch, } from '../path-utils.js';
6
+ export function trustedWorkspaceRootsPath(config, repoLocalStateDir) {
7
+ return path.join(belayStateDir(config, repoLocalStateDir), 'trusted-workspace-roots.json');
8
+ }
9
+ export async function loadTrustedWorkspaceRoots(filePath) {
10
+ if (!existsSync(filePath)) {
11
+ return { version: 1, roots: [] };
12
+ }
13
+ const raw = JSON.parse(await readFile(filePath, 'utf8'));
14
+ return { version: 1, roots: sanitizeTrustedWorkspaceRootEntries(raw.roots) };
15
+ }
16
+ export function loadTrustedWorkspaceRootsSync(filePath) {
17
+ if (!existsSync(filePath)) {
18
+ return { version: 1, roots: [] };
19
+ }
20
+ const raw = JSON.parse(readFileSync(filePath, 'utf8'));
21
+ return { version: 1, roots: sanitizeTrustedWorkspaceRootEntries(raw.roots) };
22
+ }
23
+ export async function saveTrustedWorkspaceRoots(filePath, state) {
24
+ await mkdir(path.dirname(filePath), { recursive: true });
25
+ await writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
26
+ }
27
+ export function normalizeTrustedWorkspaceRootPath(targetPath) {
28
+ return canonicalPath(targetPath);
29
+ }
30
+ function sanitizeTrustedWorkspaceRootEntries(input) {
31
+ if (!Array.isArray(input)) {
32
+ return [];
33
+ }
34
+ return input.flatMap((entry) => {
35
+ if (!entry || typeof entry !== 'object') {
36
+ return [];
37
+ }
38
+ const record = entry;
39
+ if (typeof record.path !== 'string' || !record.path.trim()) {
40
+ return [];
41
+ }
42
+ const approvedAt = typeof record.approvedAt === 'string' ? record.approvedAt : new Date(0).toISOString();
43
+ const approvalId = typeof record.approvalId === 'string' ? record.approvalId : 'unknown';
44
+ const source = record.source === 'approval' ? 'approval' : undefined;
45
+ return [
46
+ {
47
+ path: path.resolve(record.path),
48
+ approvedAt,
49
+ approvalId,
50
+ ...(source ? { source } : {}),
51
+ },
52
+ ];
53
+ });
54
+ }
55
+ const SYSTEM_HIGH_STAKES_PREFIXES = [
56
+ '/etc',
57
+ '/private/etc',
58
+ '/bin',
59
+ '/sbin',
60
+ '/usr',
61
+ '/opt',
62
+ '/System',
63
+ '/Library',
64
+ ];
65
+ const HOME_HIGH_STAKES_SEGMENTS = ['.ssh', '.gnupg', '.aws', '.kube', '.docker', '.config'];
66
+ function isDirectoryPath(targetPath) {
67
+ try {
68
+ return statSync(targetPath).isDirectory();
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ export function isBroadTrustedWorkspaceRoot(targetPath) {
75
+ const home = process.env.HOME ?? process.env.USERPROFILE;
76
+ const root = normalizeTrustedWorkspaceRootPath(targetPath);
77
+ if (root === normalizeTrustedWorkspaceRootPath(path.parse(root).root)) {
78
+ return true;
79
+ }
80
+ if (home && normalizeTrustedWorkspaceRootPath(home) === root) {
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+ export function isHighStakesTrustedWorkspaceRoot(targetPath) {
86
+ const normalized = normalizeTrustedWorkspaceRootPath(targetPath);
87
+ if (SYSTEM_HIGH_STAKES_PREFIXES.some((prefix) => normalized === normalizeTrustedWorkspaceRootPath(prefix) ||
88
+ pathWithinRoot(normalizeTrustedWorkspaceRootPath(prefix), normalized))) {
89
+ return true;
90
+ }
91
+ const home = process.env.HOME ?? process.env.USERPROFILE;
92
+ if (!home) {
93
+ return false;
94
+ }
95
+ const homeRoot = normalizeTrustedWorkspaceRootPath(home);
96
+ if (!pathWithinRoot(homeRoot, normalized)) {
97
+ return false;
98
+ }
99
+ return HOME_HIGH_STAKES_SEGMENTS.some((segment) => pathWithinRoot(path.join(homeRoot, segment), normalized));
100
+ }
101
+ export function validateTrustedWorkspaceRootCandidate(params) {
102
+ const normalizedPath = normalizeTrustedWorkspaceRootPath(params.candidatePath);
103
+ if (params.requireExistingDirectory !== false && !isDirectoryPath(normalizedPath)) {
104
+ return { ok: false, normalizedPath, reason: 'not_directory' };
105
+ }
106
+ if (isBroadTrustedWorkspaceRoot(normalizedPath)) {
107
+ return { ok: false, normalizedPath, reason: 'broad_root' };
108
+ }
109
+ if (isHighStakesTrustedWorkspaceRoot(normalizedPath)) {
110
+ return { ok: false, normalizedPath, reason: 'high_stakes' };
111
+ }
112
+ if (resolveWorkspaceRootMatch(params.repoRoot, [], normalizedPath) !== null) {
113
+ return { ok: false, normalizedPath, reason: 'inside_repo' };
114
+ }
115
+ if (params.requireNonGit !== false && containingGitRoot(normalizedPath)) {
116
+ return { ok: false, normalizedPath, reason: 'inside_git_repo' };
117
+ }
118
+ const normalizedControlPlane = params.controlPlaneDir?.trim()
119
+ ? normalizeTrustedWorkspaceRootPath(params.controlPlaneDir)
120
+ : null;
121
+ if (normalizedControlPlane &&
122
+ (pathWithinRoot(normalizedControlPlane, normalizedPath) ||
123
+ pathWithinRoot(normalizedPath, normalizedControlPlane))) {
124
+ return { ok: false, normalizedPath, reason: 'control_plane_overlap' };
125
+ }
126
+ if ((params.protectedRoots ?? []).some((root) => {
127
+ const normalizedRoot = normalizeTrustedWorkspaceRootPath(root);
128
+ return (pathWithinRoot(normalizedRoot, normalizedPath) ||
129
+ pathWithinRoot(normalizedPath, normalizedRoot));
130
+ })) {
131
+ return { ok: false, normalizedPath, reason: 'protected_root_overlap' };
132
+ }
133
+ return { ok: true, normalizedPath };
134
+ }
135
+ export function isPathWithinTrustedWorkspaceRoots(absolutePath, roots) {
136
+ const resolved = normalizeTrustedWorkspaceRootPath(absolutePath);
137
+ return roots.roots.some((entry) => {
138
+ const root = path.resolve(entry.path);
139
+ return resolved === root || pathWithinRoot(root, resolved);
140
+ });
141
+ }
142
+ export function addTrustedWorkspaceRoot(roots, entry) {
143
+ const normalized = normalizeTrustedWorkspaceRootPath(entry.path);
144
+ const filtered = roots.roots.filter((existing) => normalizeTrustedWorkspaceRootPath(existing.path) !== normalized);
145
+ return {
146
+ version: 1,
147
+ roots: [...filtered, { ...entry, path: normalized }],
148
+ };
149
+ }
@@ -1,4 +1,4 @@
1
- export type CapabilityApprovalScope = 'once' | 'path';
1
+ export type CapabilityApprovalScope = 'once' | 'path' | 'workspace-root';
2
2
  export interface FsScopeAllowlistEntry {
3
3
  path: string;
4
4
  approvedAt: string;
@@ -8,3 +8,13 @@ export interface FsScopeAllowlistFile {
8
8
  version: 1;
9
9
  paths: FsScopeAllowlistEntry[];
10
10
  }
11
+ export interface TrustedWorkspaceRootEntry {
12
+ path: string;
13
+ approvedAt: string;
14
+ approvalId: string;
15
+ source?: 'approval';
16
+ }
17
+ export interface TrustedWorkspaceRootsFile {
18
+ version: 1;
19
+ roots: TrustedWorkspaceRootEntry[];
20
+ }
@@ -1,5 +1,5 @@
1
1
  import type { CapabilityApprovalScope } from './capability/types.js';
2
- import type { BelayConfigV3 } from './config.js';
2
+ import { type BelayConfigV3 } from './config.js';
3
3
  import type { ApprovalStateFile } from './types.js';
4
4
  export interface CapabilityApprovalStore {
5
5
  loadPending: () => Promise<{
@@ -13,6 +13,7 @@ export interface CapabilityApprovalStore {
13
13
  writePending: (filePath: string, state: ApprovalStateFile) => Promise<void>;
14
14
  writeApproved: (filePath: string, state: ApprovalStateFile) => Promise<void>;
15
15
  allowlistPath: string;
16
+ trustedRootsPath: string;
16
17
  }
17
18
  export declare function recordCapabilityApproval(params: {
18
19
  approvalId: string;
@@ -1,11 +1,13 @@
1
1
  import { recordApproval } from './approval-service.js';
2
2
  import { addPathToAllowlist, loadFsScopeAllowlist, normalizeAllowlistPath, saveFsScopeAllowlist, } from './capability/allowlist.js';
3
3
  import { FS_SCOPE_REASONS } from './capability/reasons.js';
4
+ import { addTrustedWorkspaceRoot, loadTrustedWorkspaceRoots, normalizeTrustedWorkspaceRootPath, saveTrustedWorkspaceRoots, validateTrustedWorkspaceRootCandidate, } from './capability/trusted-workspace-roots.js';
5
+ import { resolveControlPlaneDir } from './config.js';
4
6
  export async function recordCapabilityApproval(params) {
5
7
  const pending = await params.store.loadPending();
6
8
  const match = pending.state.approvals.find((approval) => approval.approvalId === params.approvalId);
7
9
  if (params.scope === 'path') {
8
- if (!match || !FS_SCOPE_REASONS.has(match.reason)) {
10
+ if (!match || match.kind !== 'shell' || !FS_SCOPE_REASONS.has(match.reason)) {
9
11
  return {
10
12
  ok: false,
11
13
  message: 'Path scope approvals apply only to pending outside-repo shell actions (outside_repo_mutation / outside_repo_redirect).',
@@ -18,6 +20,85 @@ export async function recordCapabilityApproval(params) {
18
20
  };
19
21
  }
20
22
  }
23
+ if (params.scope === 'workspace-root') {
24
+ if (!match || !FS_SCOPE_REASONS.has(match.reason)) {
25
+ return {
26
+ ok: false,
27
+ message: 'workspace-root scope approvals apply only to pending outside-repo actions (outside_repo_mutation / outside_repo_redirect).',
28
+ };
29
+ }
30
+ if (!params.scopePath?.trim()) {
31
+ return {
32
+ ok: false,
33
+ message: 'approve --scope workspace-root requires --path <absolute-directory-path>.',
34
+ };
35
+ }
36
+ if (!match.scopeHint || match.scopeHint.scope !== 'workspace-root') {
37
+ return {
38
+ ok: false,
39
+ message: 'workspace-root approval requires a pending scope hint. Re-run the original blocked action to mint a scoped approval.',
40
+ };
41
+ }
42
+ const normalizedPath = normalizeTrustedWorkspaceRootPath(params.scopePath.trim());
43
+ const normalizedHint = normalizeTrustedWorkspaceRootPath(match.scopeHint.path);
44
+ if (normalizedPath !== normalizedHint) {
45
+ return {
46
+ ok: false,
47
+ message: `workspace-root approval path must exactly match the suggested path: ${normalizedHint}`,
48
+ };
49
+ }
50
+ const validation = validateTrustedWorkspaceRootCandidate({
51
+ candidatePath: normalizedPath,
52
+ repoRoot: match.repoRoot,
53
+ controlPlaneDir: params.config.controlPlane.enabled
54
+ ? resolveControlPlaneDir(params.config)
55
+ : undefined,
56
+ requireExistingDirectory: true,
57
+ requireNonGit: true,
58
+ });
59
+ if (!validation.ok) {
60
+ if (validation.reason === 'not_directory') {
61
+ return {
62
+ ok: false,
63
+ message: 'workspace-root approval requires an existing directory path.',
64
+ };
65
+ }
66
+ if (validation.reason === 'broad_root') {
67
+ return {
68
+ ok: false,
69
+ message: 'workspace-root approval rejected: broad roots (/, drive root, HOME) are not allowed.',
70
+ };
71
+ }
72
+ if (validation.reason === 'high_stakes') {
73
+ return {
74
+ ok: false,
75
+ message: 'workspace-root approval rejected: high-stakes directories cannot be trusted roots.',
76
+ };
77
+ }
78
+ if (validation.reason === 'inside_repo') {
79
+ return {
80
+ ok: false,
81
+ message: 'workspace-root approval rejected: the path is already inside the repository root.',
82
+ };
83
+ }
84
+ if (validation.reason === 'inside_git_repo') {
85
+ return {
86
+ ok: false,
87
+ message: `workspace-root approval rejected: ${normalizedPath} is under a git repository.`,
88
+ };
89
+ }
90
+ if (validation.reason === 'control_plane_overlap') {
91
+ return {
92
+ ok: false,
93
+ message: 'workspace-root approval rejected: control plane paths cannot be trusted roots.',
94
+ };
95
+ }
96
+ return {
97
+ ok: false,
98
+ message: 'workspace-root approval rejected by trusted-root policy.',
99
+ };
100
+ }
101
+ }
21
102
  const result = await recordApproval({
22
103
  approvalId: params.approvalId,
23
104
  config: params.config,
@@ -25,9 +106,26 @@ export async function recordCapabilityApproval(params) {
25
106
  requireSignedToken: params.requireSignedToken ?? false,
26
107
  store: params.store,
27
108
  });
28
- if (!result.ok || params.scope !== 'path' || !params.scopePath?.trim()) {
109
+ if (!result.ok ||
110
+ (params.scope !== 'path' && params.scope !== 'workspace-root') ||
111
+ !params.scopePath?.trim()) {
29
112
  return { ok: result.ok, message: result.message };
30
113
  }
114
+ if (params.scope === 'workspace-root') {
115
+ const trustedRoots = await loadTrustedWorkspaceRoots(params.store.trustedRootsPath);
116
+ const normalizedPath = normalizeTrustedWorkspaceRootPath(params.scopePath.trim());
117
+ const updated = addTrustedWorkspaceRoot(trustedRoots, {
118
+ path: normalizedPath,
119
+ approvedAt: new Date().toISOString(),
120
+ approvalId: params.approvalId,
121
+ source: 'approval',
122
+ });
123
+ await saveTrustedWorkspaceRoots(params.store.trustedRootsPath, updated);
124
+ return {
125
+ ok: true,
126
+ message: `${result.message} Trusted workspace root ${normalizedPath} added.`,
127
+ };
128
+ }
31
129
  const allowlist = await loadFsScopeAllowlist(params.store.allowlistPath);
32
130
  const normalizedPath = normalizeAllowlistPath(params.scopePath.trim());
33
131
  const updated = addPathToAllowlist(allowlist, {
@@ -1,2 +1,3 @@
1
+ import type { BelayConfigV3 } from './config.js';
1
2
  import type { ClassifierOptions, ClassifyResult } from './types.js';
2
- export declare function classifySubagent(payload: Record<string, unknown>, repoRoot: string, options?: ClassifierOptions): ClassifyResult;
3
+ export declare function classifySubagent(payload: Record<string, unknown>, repoRoot: string, options: ClassifierOptions | undefined, config: BelayConfigV3): ClassifyResult;
@@ -1,4 +1,8 @@
1
+ import { BOUNDARY_PROFILE_L3_L4_ONLY } from './capability/boundary-profile.js';
2
+ import { policyReasonToLegacyReason } from './capability/policy-bridge.js';
3
+ import { evaluateSubagentPolicy, policyDecisionRequiresAsk } from './capability/policy-engine.js';
1
4
  import { canonicalStringify, subagentFingerprint } from './fingerprint.js';
5
+ import { subagentFingerprintSource } from './replay-scrub.js';
2
6
  import { scrubValue } from './scrub.js';
3
7
  const EXTERNAL_TERMS = ['deploy', 'production', 'publish', 'release', 'ship', 'notify', 'email'];
4
8
  function extractSubagentText(payload, options) {
@@ -22,28 +26,9 @@ function extractSubagentText(payload, options) {
22
26
  return canonicalStringify(scrubValue(payload, options.scrubOptions));
23
27
  }
24
28
  function fingerprintSource(payload, options) {
25
- const toolInput = payload.tool_input;
26
- if (toolInput && typeof toolInput === 'object') {
27
- const input = toolInput;
28
- return scrubValue({
29
- description: input.description ?? '',
30
- prompt: input.prompt ?? '',
31
- }, options.scrubOptions);
32
- }
33
- const task = payload.task;
34
- if (typeof task === 'string') {
35
- return scrubValue({ task }, options.scrubOptions);
36
- }
37
- if (task && typeof task === 'object') {
38
- const taskObj = task;
39
- return scrubValue({
40
- description: taskObj.description ?? '',
41
- prompt: taskObj.prompt ?? '',
42
- }, options.scrubOptions);
43
- }
44
- return scrubValue(payload, options.scrubOptions);
29
+ return subagentFingerprintSource(payload, options.scrubOptions ?? {});
45
30
  }
46
- export function classifySubagent(payload, repoRoot, options = {}) {
31
+ export function classifySubagent(payload, repoRoot, options = {}, config) {
47
32
  const kind = payload.tool_name === 'Task' ? 'Task' : String(payload.subagent_type ?? 'generalPurpose');
48
33
  const scrubbed = fingerprintSource(payload, options);
49
34
  const summary = extractSubagentText(payload, options);
@@ -53,6 +38,36 @@ export function classifySubagent(payload, repoRoot, options = {}) {
53
38
  const pattern = new RegExp(`\\b${term}\\b`, 'i');
54
39
  return pattern.test(lowered);
55
40
  });
41
+ const signals = hasExternalTerm ? ['subagent_external_intent_hint'] : ['subagent_default_review'];
42
+ const { request, decision } = evaluateSubagentPolicy({
43
+ subagentType: kind,
44
+ summary,
45
+ repoRoot,
46
+ cwd: repoRoot,
47
+ inputFingerprint: fingerprint,
48
+ signals,
49
+ }, config, {
50
+ grants: options.grants,
51
+ attestation: options.attestation,
52
+ egressProxyActive: options.egressProxyActive,
53
+ });
54
+ if (policyDecisionRequiresAsk(decision)) {
55
+ return {
56
+ verdict: 'deny_pending_approval',
57
+ reason: policyReasonToLegacyReason(decision),
58
+ summary,
59
+ fingerprint,
60
+ assessment: {
61
+ reversibility: 'recoverable_with_cost',
62
+ external: hasExternalTerm,
63
+ blastRadius: 'subagent task scope',
64
+ confidence: hasExternalTerm ? 0.7 : 0.67,
65
+ signals: [...signals, ...decision.signals],
66
+ },
67
+ capabilityRequests: [request],
68
+ authorizationDecision: decision,
69
+ };
70
+ }
56
71
  return {
57
72
  verdict: 'allow_flagged',
58
73
  reason: 'subagent_review',
@@ -63,7 +78,10 @@ export function classifySubagent(payload, repoRoot, options = {}) {
63
78
  external: false,
64
79
  blastRadius: 'subagent task scope',
65
80
  confidence: hasExternalTerm ? 0.7 : 0.67,
66
- signals: hasExternalTerm ? ['subagent_external_intent_hint'] : ['subagent_default_review'],
81
+ signals,
67
82
  },
83
+ capabilityRequests: [request],
84
+ authorizationDecision: decision,
85
+ boundaryProfile: options.boundaryProfile ?? BOUNDARY_PROFILE_L3_L4_ONLY,
68
86
  };
69
87
  }