@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
@@ -6,13 +6,13 @@ export interface PathTargetAnalysis {
6
6
  signals: string[];
7
7
  }
8
8
  export declare function resolveTrustedPath(token: string, trustedCwd: string, trusted: boolean): string | null;
9
- export declare function locationForPath(resolvedPath: string | null, repoRoot: string): VerdictLocation;
9
+ export declare function locationForPath(resolvedPath: string | null, repoRoot: string, trustedWorkspaceRoots?: string[]): VerdictLocation;
10
10
  export declare function isGitPath(resolvedPath: string, repoRoot: string): boolean;
11
- export declare function isHighStakesPath(resolvedPath: string, repoRoot: string, sensitivePaths: string[], protectedRoots?: string[]): boolean;
11
+ export declare function isHighStakesPath(resolvedPath: string, repoRoot: string, sensitivePaths: string[], protectedRoots?: string[], trustedWorkspaceRoots?: string[]): boolean;
12
12
  /** ADR-002 M3: destructive shell head on git/sensitive path (not broad category ask). */
13
13
  export declare function isDestructiveMutationHead(head: string): boolean;
14
14
  export declare function touchesProtectedRoot(resolvedPath: string, protectedRoots: string[]): boolean;
15
- export declare function isDestructiveHighStakesMutation(head: string, resolvedPath: string, repoRoot: string, sensitivePaths: string[], protectedRoots?: string[]): boolean;
15
+ export declare function isDestructiveHighStakesMutation(head: string, resolvedPath: string, repoRoot: string, sensitivePaths: string[], protectedRoots?: string[], trustedWorkspaceRoots?: string[]): boolean;
16
16
  export declare function analyzePathTargets(params: {
17
17
  targets: string[];
18
18
  cwd: string;
@@ -20,5 +20,6 @@ export declare function analyzePathTargets(params: {
20
20
  trustedCwd: boolean;
21
21
  sensitivePaths: string[];
22
22
  protectedArtifactRoots?: string[];
23
+ trustedWorkspaceRoots?: string[];
23
24
  }): PathTargetAnalysis;
24
25
  export declare function cwdRelative(repoRoot: string, cwd: string): string;
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { matchesSensitivePath } from '../glob.js';
3
- import { canonicalPath, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from '../path-utils.js';
3
+ import { canonicalPath, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, resolveWorkspaceRootMatch, } from '../path-utils.js';
4
4
  import { isOutsideRepoSecretCredentialPath } from './persistent-paths.js';
5
5
  export { isOutsideRepoSecretCredentialPath, isPersistentAgentPath } from './persistent-paths.js';
6
6
  function expandHome(token) {
@@ -26,11 +26,11 @@ export function resolveTrustedPath(token, trustedCwd, trusted) {
26
26
  }
27
27
  return canonicalPath(path.resolve(trustedCwd, expanded));
28
28
  }
29
- export function locationForPath(resolvedPath, repoRoot) {
29
+ export function locationForPath(resolvedPath, repoRoot, trustedWorkspaceRoots = []) {
30
30
  if (!resolvedPath) {
31
31
  return 'unknown';
32
32
  }
33
- if (pathWithinRoot(repoRoot, resolvedPath)) {
33
+ if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolvedPath)) {
34
34
  return 'repo_local';
35
35
  }
36
36
  return 'repo_outside';
@@ -41,17 +41,22 @@ export function isGitPath(resolvedPath, repoRoot) {
41
41
  return false;
42
42
  }
43
43
  const normalized = relative.replaceAll('\\', '/');
44
- return normalized === '.git' || normalized.startsWith('.git/');
44
+ return (normalized === '.git' ||
45
+ normalized.startsWith('.git/') ||
46
+ normalized.endsWith('/.git') ||
47
+ normalized.includes('/.git/'));
45
48
  }
46
- export function isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
49
+ export function isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, protectedRoots = [], trustedWorkspaceRoots = []) {
47
50
  if (isGitPath(resolvedPath, repoRoot)) {
48
51
  return true;
49
52
  }
50
- const relative = relativeWithinRepo(repoRoot, resolvedPath);
51
- if (relative !== null && matchesSensitivePath(relative.replaceAll('\\', '/'), sensitivePaths)) {
53
+ const workspaceMatch = resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolvedPath);
54
+ if (workspaceMatch !== null &&
55
+ matchesSensitivePath(workspaceMatch.relativePath.replaceAll('\\', '/'), sensitivePaths)) {
52
56
  return true;
53
57
  }
54
- if (relative === null && isOutsideRepoSecretCredentialPath(resolvedPath)) {
58
+ if ((workspaceMatch === null || workspaceMatch.kind === 'trusted') &&
59
+ isOutsideRepoSecretCredentialPath(resolvedPath)) {
55
60
  return true;
56
61
  }
57
62
  return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
@@ -73,14 +78,14 @@ export function isDestructiveMutationHead(head) {
73
78
  export function touchesProtectedRoot(resolvedPath, protectedRoots) {
74
79
  return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath) || root === resolvedPath);
75
80
  }
76
- export function isDestructiveHighStakesMutation(head, resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
81
+ export function isDestructiveHighStakesMutation(head, resolvedPath, repoRoot, sensitivePaths, protectedRoots = [], trustedWorkspaceRoots = []) {
77
82
  if (touchesProtectedRoot(resolvedPath, protectedRoots)) {
78
83
  return true;
79
84
  }
80
85
  if (!isDestructiveMutationHead(head)) {
81
86
  return false;
82
87
  }
83
- return isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, []);
88
+ return isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, [], trustedWorkspaceRoots);
84
89
  }
85
90
  export function analyzePathTargets(params) {
86
91
  const signals = [];
@@ -96,10 +101,10 @@ export function analyzePathTargets(params) {
96
101
  for (const target of params.targets) {
97
102
  const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ??
98
103
  resolveMutationTarget(target, params.cwd);
99
- const location = locationForPath(resolved, params.repoRoot);
104
+ const location = locationForPath(resolved, params.repoRoot, params.trustedWorkspaceRoots);
100
105
  locations.add(location);
101
106
  if (resolved &&
102
- isHighStakesPath(resolved, params.repoRoot, params.sensitivePaths, params.protectedArtifactRoots)) {
107
+ isHighStakesPath(resolved, params.repoRoot, params.sensitivePaths, params.protectedArtifactRoots, params.trustedWorkspaceRoots)) {
103
108
  isHighStakes = true;
104
109
  signals.push('high_stakes_path');
105
110
  }
@@ -0,0 +1,67 @@
1
+ import { type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'node:child_process';
2
+ import type { CliJudgeRunCommand } from './judge-cli.js';
3
+ interface AcpSessionResponse {
4
+ sessionId: string;
5
+ models?: {
6
+ currentModelId?: string;
7
+ availableModels?: Array<{
8
+ modelId?: string;
9
+ }>;
10
+ };
11
+ configOptions?: Array<{
12
+ id?: string;
13
+ currentValue?: string;
14
+ options?: Array<{
15
+ value?: string;
16
+ }>;
17
+ }>;
18
+ }
19
+ export type CursorAcpSpawn = (binary: string, args: string[], options: SpawnOptionsWithoutStdio & {
20
+ stdio: ['pipe', 'pipe', 'pipe'];
21
+ }) => ChildProcessWithoutNullStreams;
22
+ export type CursorAcpErrorKind = 'connect_timeout' | 'eval_timeout' | 'process_exit' | 'protocol_error';
23
+ export declare class CursorAcpError extends Error {
24
+ readonly kind: CursorAcpErrorKind;
25
+ constructor(kind: CursorAcpErrorKind, message: string);
26
+ }
27
+ export interface CursorAcpJudgeRunnerOptions {
28
+ cwd: string;
29
+ connectTimeoutMs: number;
30
+ binary?: string;
31
+ spawnProcess?: CursorAcpSpawn;
32
+ }
33
+ export declare function resolveCursorAcpModelId(requested: string, session: AcpSessionResponse): string;
34
+ export declare class CursorAcpJudgeRunner {
35
+ private readonly options;
36
+ private readonly binary;
37
+ private readonly spawnProcess;
38
+ private child;
39
+ private startPromise;
40
+ private connected;
41
+ private nextId;
42
+ private stdoutBuffer;
43
+ private stderrTail;
44
+ private readonly pending;
45
+ private readonly messageBuffers;
46
+ private activeSessionId;
47
+ constructor(options: CursorAcpJudgeRunnerOptions);
48
+ get sessionId(): string | null;
49
+ readonly run: CliJudgeRunCommand;
50
+ private evaluate;
51
+ stop(): Promise<void>;
52
+ private ensureConnected;
53
+ private start;
54
+ private createSession;
55
+ private request;
56
+ private notify;
57
+ private handleStdout;
58
+ private handleMessage;
59
+ private respond;
60
+ private respondError;
61
+ private handleProcessExit;
62
+ private rejectPending;
63
+ private withStderr;
64
+ private terminateChild;
65
+ private signalAndWait;
66
+ }
67
+ export {};
@@ -0,0 +1,345 @@
1
+ import { spawn, } from 'node:child_process';
2
+ export class CursorAcpError extends Error {
3
+ kind;
4
+ constructor(kind, message) {
5
+ super(message);
6
+ this.kind = kind;
7
+ this.name = 'CursorAcpError';
8
+ }
9
+ }
10
+ const STDERR_TAIL_LIMIT = 2_048;
11
+ const TERMINATE_GRACE_MS = 250;
12
+ function invocationValue(invocation, flag) {
13
+ const index = invocation.args.indexOf(flag);
14
+ return index >= 0 ? invocation.args[index + 1] : undefined;
15
+ }
16
+ function promptFromInvocation(invocation) {
17
+ if (invocation.stdin !== undefined) {
18
+ return invocation.stdin;
19
+ }
20
+ return invocation.args[invocation.args.length - 1] ?? '';
21
+ }
22
+ function modelIdsFromSession(session) {
23
+ const fromModels = session.models?.availableModels
24
+ ?.map((entry) => entry.modelId)
25
+ .filter((value) => Boolean(value));
26
+ if (fromModels?.length) {
27
+ return fromModels;
28
+ }
29
+ const modelOption = session.configOptions?.find((option) => option.id === 'model');
30
+ return (modelOption?.options
31
+ ?.map((entry) => entry.value)
32
+ .filter((value) => Boolean(value)) ?? []);
33
+ }
34
+ export function resolveCursorAcpModelId(requested, session) {
35
+ const available = modelIdsFromSession(session);
36
+ if (available.includes(requested)) {
37
+ return requested;
38
+ }
39
+ const parameterized = available.find((modelId) => modelId.startsWith(`${requested}[`));
40
+ return parameterized ?? requested;
41
+ }
42
+ export class CursorAcpJudgeRunner {
43
+ options;
44
+ binary;
45
+ spawnProcess;
46
+ child = null;
47
+ startPromise = null;
48
+ connected = false;
49
+ nextId = 0;
50
+ stdoutBuffer = '';
51
+ stderrTail = '';
52
+ pending = new Map();
53
+ messageBuffers = new Map();
54
+ activeSessionId = null;
55
+ constructor(options) {
56
+ this.options = options;
57
+ this.binary = options.binary ?? 'cursor-agent';
58
+ this.spawnProcess = options.spawnProcess ?? spawn;
59
+ }
60
+ get sessionId() {
61
+ return this.activeSessionId;
62
+ }
63
+ run = async (invocation, timeoutMs) => {
64
+ if (invocation.binary !== this.binary && invocation.binary !== 'cursor-agent') {
65
+ throw new CursorAcpError('protocol_error', `Cursor ACP runner cannot execute ${invocation.binary}`);
66
+ }
67
+ let totalTimer = null;
68
+ try {
69
+ const evaluation = this.evaluate(invocation, timeoutMs);
70
+ const totalTimeout = new Promise((_resolve, reject) => {
71
+ totalTimer = setTimeout(() => {
72
+ reject(new CursorAcpError('eval_timeout', 'Cursor ACP evaluation timed out'));
73
+ }, timeoutMs);
74
+ });
75
+ return await Promise.race([evaluation, totalTimeout]);
76
+ }
77
+ catch (error) {
78
+ if (error instanceof CursorAcpError && error.kind === 'eval_timeout') {
79
+ this.notify('session/cancel', { sessionId: this.activeSessionId });
80
+ }
81
+ await this.stop();
82
+ throw error;
83
+ }
84
+ finally {
85
+ if (totalTimer) {
86
+ clearTimeout(totalTimer);
87
+ }
88
+ }
89
+ };
90
+ async evaluate(invocation, timeoutMs) {
91
+ await this.ensureConnected();
92
+ const requestedResumeId = invocationValue(invocation, '--resume');
93
+ const model = invocationValue(invocation, '--model') ?? 'composer-2.5';
94
+ const canReuse = Boolean(requestedResumeId) && requestedResumeId === this.activeSessionId;
95
+ if (!canReuse) {
96
+ await this.createSession(model);
97
+ }
98
+ const sessionId = this.activeSessionId;
99
+ if (!sessionId) {
100
+ throw new CursorAcpError('protocol_error', 'Cursor ACP session was not created');
101
+ }
102
+ this.messageBuffers.set(sessionId, '');
103
+ try {
104
+ await this.request('session/prompt', {
105
+ sessionId,
106
+ prompt: [{ type: 'text', text: promptFromInvocation(invocation) }],
107
+ }, timeoutMs, 'eval_timeout');
108
+ const raw = this.messageBuffers.get(sessionId)?.trim() ?? '';
109
+ if (!raw) {
110
+ throw new CursorAcpError('protocol_error', 'Cursor ACP returned no agent message');
111
+ }
112
+ return raw;
113
+ }
114
+ finally {
115
+ this.messageBuffers.delete(sessionId);
116
+ }
117
+ }
118
+ async stop() {
119
+ const child = this.child;
120
+ this.child = null;
121
+ this.connected = false;
122
+ this.startPromise = null;
123
+ this.activeSessionId = null;
124
+ this.stdoutBuffer = '';
125
+ this.messageBuffers.clear();
126
+ this.rejectPending(new CursorAcpError('process_exit', this.withStderr('Cursor ACP process stopped')));
127
+ if (child && child.exitCode === null) {
128
+ await this.terminateChild(child);
129
+ }
130
+ }
131
+ async ensureConnected() {
132
+ if (this.connected && this.child) {
133
+ return;
134
+ }
135
+ if (!this.startPromise) {
136
+ this.startPromise = this.start().finally(() => {
137
+ this.startPromise = null;
138
+ });
139
+ }
140
+ await this.startPromise;
141
+ }
142
+ async start() {
143
+ this.stderrTail = '';
144
+ const child = this.spawnProcess(this.binary, ['acp'], {
145
+ cwd: this.options.cwd,
146
+ stdio: ['pipe', 'pipe', 'pipe'],
147
+ });
148
+ this.child = child;
149
+ child.stdout.on('data', (chunk) => this.handleStdout(String(chunk)));
150
+ child.stderr.on('data', (chunk) => {
151
+ this.stderrTail = `${this.stderrTail}${String(chunk)}`.slice(-STDERR_TAIL_LIMIT);
152
+ });
153
+ child.once('error', (error) => {
154
+ this.handleProcessExit(new CursorAcpError('process_exit', this.withStderr(error.message)), child);
155
+ });
156
+ child.once('close', (code) => {
157
+ this.handleProcessExit(new CursorAcpError('process_exit', this.withStderr(`Cursor ACP exited with code ${code ?? 'unknown'}`)), child);
158
+ });
159
+ try {
160
+ await this.request('initialize', {
161
+ protocolVersion: 1,
162
+ clientCapabilities: {
163
+ fs: { readTextFile: false, writeTextFile: false },
164
+ terminal: false,
165
+ },
166
+ clientInfo: {
167
+ name: 'belay-judge',
168
+ title: 'Belay Judge',
169
+ version: '1',
170
+ },
171
+ }, this.options.connectTimeoutMs, 'connect_timeout');
172
+ await this.request('authenticate', { methodId: 'cursor_login' }, this.options.connectTimeoutMs, 'connect_timeout');
173
+ this.connected = true;
174
+ }
175
+ catch (error) {
176
+ await this.stop();
177
+ throw error;
178
+ }
179
+ }
180
+ async createSession(model) {
181
+ const previous = this.activeSessionId;
182
+ this.activeSessionId = null;
183
+ if (previous) {
184
+ await this.request('session/close', { sessionId: previous }, Math.min(this.options.connectTimeoutMs, 1_000), 'connect_timeout').catch(() => undefined);
185
+ }
186
+ const session = (await this.request('session/new', { cwd: this.options.cwd, mcpServers: [] }, this.options.connectTimeoutMs, 'connect_timeout'));
187
+ if (!session?.sessionId) {
188
+ throw new CursorAcpError('protocol_error', 'Cursor ACP session/new omitted sessionId');
189
+ }
190
+ this.activeSessionId = session.sessionId;
191
+ await this.request('session/set_config_option', { sessionId: session.sessionId, configId: 'mode', value: 'ask' }, this.options.connectTimeoutMs, 'connect_timeout');
192
+ await this.request('session/set_config_option', {
193
+ sessionId: session.sessionId,
194
+ configId: 'model',
195
+ value: resolveCursorAcpModelId(model, session),
196
+ }, this.options.connectTimeoutMs, 'connect_timeout');
197
+ }
198
+ request(method, params, timeoutMs, timeoutKind) {
199
+ const child = this.child;
200
+ if (!child?.stdin.writable) {
201
+ return Promise.reject(new CursorAcpError('process_exit', this.withStderr('Cursor ACP stdin is unavailable')));
202
+ }
203
+ const id = ++this.nextId;
204
+ return new Promise((resolve, reject) => {
205
+ const timer = setTimeout(() => {
206
+ this.pending.delete(id);
207
+ reject(new CursorAcpError(timeoutKind, `Cursor ACP ${method} timed out`));
208
+ }, timeoutMs);
209
+ this.pending.set(id, { method, resolve, reject, timer });
210
+ child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
211
+ });
212
+ }
213
+ notify(method, params) {
214
+ if (!this.child?.stdin.writable) {
215
+ return;
216
+ }
217
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`);
218
+ }
219
+ handleStdout(chunk) {
220
+ this.stdoutBuffer += chunk;
221
+ const lines = this.stdoutBuffer.split('\n');
222
+ this.stdoutBuffer = lines.pop() ?? '';
223
+ for (const line of lines) {
224
+ if (!line.trim()) {
225
+ continue;
226
+ }
227
+ try {
228
+ this.handleMessage(JSON.parse(line));
229
+ }
230
+ catch {
231
+ const child = this.child;
232
+ this.handleProcessExit(new CursorAcpError('protocol_error', 'Cursor ACP emitted invalid JSON-RPC'), child);
233
+ if (child?.exitCode === null) {
234
+ void this.terminateChild(child);
235
+ }
236
+ }
237
+ }
238
+ }
239
+ handleMessage(message) {
240
+ if (message.id !== undefined && !message.method) {
241
+ const pending = this.pending.get(message.id);
242
+ if (!pending) {
243
+ return;
244
+ }
245
+ clearTimeout(pending.timer);
246
+ this.pending.delete(message.id);
247
+ if (message.error !== undefined) {
248
+ pending.reject(new CursorAcpError('protocol_error', `Cursor ACP ${pending.method} failed: ${JSON.stringify(message.error)}`));
249
+ }
250
+ else {
251
+ pending.resolve(message.result);
252
+ }
253
+ return;
254
+ }
255
+ if (message.method === 'session/update') {
256
+ const sessionId = message.params?.sessionId;
257
+ const update = message.params?.update;
258
+ if (typeof sessionId !== 'string' || !update || typeof update !== 'object') {
259
+ return;
260
+ }
261
+ const record = update;
262
+ const content = record.content;
263
+ if (record.sessionUpdate === 'agent_message_chunk' &&
264
+ content &&
265
+ typeof content === 'object' &&
266
+ typeof content.text === 'string') {
267
+ const text = content.text;
268
+ this.messageBuffers.set(sessionId, `${this.messageBuffers.get(sessionId) ?? ''}${text}`);
269
+ }
270
+ return;
271
+ }
272
+ if (message.id !== undefined && message.method === 'session/request_permission') {
273
+ this.respond(message.id, { outcome: { outcome: 'cancelled' } });
274
+ return;
275
+ }
276
+ if (message.id !== undefined && message.method) {
277
+ this.respondError(message.id, -32601, 'Belay Judge does not expose client tools');
278
+ }
279
+ }
280
+ respond(id, result) {
281
+ this.child?.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`);
282
+ }
283
+ respondError(id, code, message) {
284
+ this.child?.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } })}\n`);
285
+ }
286
+ handleProcessExit(error, exitedChild = this.child) {
287
+ if (exitedChild && this.child !== exitedChild) {
288
+ return;
289
+ }
290
+ this.child = null;
291
+ this.connected = false;
292
+ this.activeSessionId = null;
293
+ this.messageBuffers.clear();
294
+ this.rejectPending(error);
295
+ }
296
+ rejectPending(error) {
297
+ for (const pending of this.pending.values()) {
298
+ clearTimeout(pending.timer);
299
+ pending.reject(error);
300
+ }
301
+ this.pending.clear();
302
+ }
303
+ withStderr(message) {
304
+ const stderr = this.stderrTail.trim();
305
+ return stderr ? `${message}: ${stderr}` : message;
306
+ }
307
+ async terminateChild(child) {
308
+ if (child.exitCode !== null) {
309
+ return;
310
+ }
311
+ if (await this.signalAndWait(child, 'SIGTERM')) {
312
+ return;
313
+ }
314
+ await this.signalAndWait(child, 'SIGKILL');
315
+ }
316
+ signalAndWait(child, signal) {
317
+ if (child.exitCode !== null) {
318
+ return Promise.resolve(true);
319
+ }
320
+ return new Promise((resolve) => {
321
+ let settled = false;
322
+ const finish = (exited) => {
323
+ if (settled)
324
+ return;
325
+ settled = true;
326
+ clearTimeout(timer);
327
+ child.off('close', onExit);
328
+ child.off('error', onExit);
329
+ resolve(exited);
330
+ };
331
+ const onExit = () => finish(true);
332
+ const timer = setTimeout(() => finish(child.exitCode !== null), TERMINATE_GRACE_MS);
333
+ child.once('close', onExit);
334
+ child.once('error', onExit);
335
+ try {
336
+ if (!child.kill(signal)) {
337
+ finish(child.exitCode !== null);
338
+ }
339
+ }
340
+ catch {
341
+ finish(child.exitCode !== null);
342
+ }
343
+ });
344
+ }
345
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Tier0 egress tool classification (SPEC v2.1.3 R33–R34).
3
- * destructive → Tier0 ask | read → Tier0 allow | ambiguous → Tier1 (fail-closed)
2
+ * Egress tool classification (SPEC v2.1.3 R33–R34).
3
+ * destructive → ask | read → allow at egress layer | ambiguous → require_approval via PolicyEngine
4
4
  */
5
5
  export type EgressClassification = 'destructive' | 'read' | 'ambiguous';
6
6
  export declare function isEgressToolHead(head: string): boolean;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Tier0 egress tool classification (SPEC v2.1.3 R33–R34).
3
- * destructive → Tier0 ask | read → Tier0 allow | ambiguous → Tier1 (fail-closed)
2
+ * Egress tool classification (SPEC v2.1.3 R33–R34).
3
+ * destructive → ask | read → allow at egress layer | ambiguous → require_approval via PolicyEngine
4
4
  */
5
5
  const EGRESS_TOOL_HEADS = new Set([
6
6
  'aws',
@@ -0,0 +1,26 @@
1
+ import type { VerdictEffect } from './types.js';
2
+ export interface NormalizedGitInvocation {
3
+ subcommand: string;
4
+ args: string[];
5
+ effectiveCwd?: string;
6
+ gitDir?: string;
7
+ workTree?: string;
8
+ }
9
+ export interface GitCommandSemantics {
10
+ effect: VerdictEffect;
11
+ pathTargets: string[];
12
+ scopeTargets: string[];
13
+ signals: string[];
14
+ egressClass?: 'read' | 'destructive' | 'ambiguous';
15
+ requiresAsk?: {
16
+ reason: string;
17
+ signals: string[];
18
+ };
19
+ effectiveCwd?: string;
20
+ gitWorkTree?: string;
21
+ normalizedKey: string;
22
+ isReadOnly: boolean;
23
+ }
24
+ export declare function normalizeGitInvocation(tokens: string[], baseCwd: string): NormalizedGitInvocation | null;
25
+ export declare function isGitReadOnlyInvocation(tokens: string[], baseCwd: string): boolean;
26
+ export declare function classifyGitCommand(tokens: string[], baseCwd: string): GitCommandSemantics | null;