@guilz-dev/belay 0.1.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 (266) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +268 -0
  3. package/agent-belay-logo.png +0 -0
  4. package/dist/adapters/claude/adapter.d.ts +7 -0
  5. package/dist/adapters/claude/adapter.js +114 -0
  6. package/dist/adapters/claude/hooks.d.ts +13 -0
  7. package/dist/adapters/claude/hooks.js +49 -0
  8. package/dist/adapters/claude/runtime-entry.d.ts +4 -0
  9. package/dist/adapters/claude/runtime-entry.js +260 -0
  10. package/dist/adapters/codex/adapter.d.ts +7 -0
  11. package/dist/adapters/codex/adapter.js +73 -0
  12. package/dist/adapters/codex/hooks.d.ts +21 -0
  13. package/dist/adapters/codex/hooks.js +78 -0
  14. package/dist/adapters/codex/runtime-entry.d.ts +4 -0
  15. package/dist/adapters/codex/runtime-entry.js +237 -0
  16. package/dist/adapters/cursor/adapter.d.ts +7 -0
  17. package/dist/adapters/cursor/adapter.js +29 -0
  18. package/dist/adapters/cursor/hooks.d.ts +2 -0
  19. package/dist/adapters/cursor/hooks.js +26 -0
  20. package/dist/adapters/cursor/runtime-entry.d.ts +4 -0
  21. package/dist/adapters/cursor/runtime-entry.js +143 -0
  22. package/dist/adapters/layouts/claude.d.ts +2 -0
  23. package/dist/adapters/layouts/claude.js +40 -0
  24. package/dist/adapters/layouts/codex.d.ts +2 -0
  25. package/dist/adapters/layouts/codex.js +43 -0
  26. package/dist/adapters/layouts/cursor.d.ts +2 -0
  27. package/dist/adapters/layouts/cursor.js +40 -0
  28. package/dist/adapters/layouts/index.d.ts +7 -0
  29. package/dist/adapters/layouts/index.js +23 -0
  30. package/dist/adapters/layouts/protected-paths.d.ts +3 -0
  31. package/dist/adapters/layouts/protected-paths.js +15 -0
  32. package/dist/adapters/layouts/scope.d.ts +19 -0
  33. package/dist/adapters/layouts/scope.js +86 -0
  34. package/dist/adapters/layouts/types.d.ts +14 -0
  35. package/dist/adapters/layouts/types.js +1 -0
  36. package/dist/adapters/registry.d.ts +4 -0
  37. package/dist/adapters/registry.js +14 -0
  38. package/dist/adapters/shared/gate-runtime.d.ts +51 -0
  39. package/dist/adapters/shared/gate-runtime.js +518 -0
  40. package/dist/adapters/shared/repo-root.d.ts +2 -0
  41. package/dist/adapters/shared/repo-root.js +17 -0
  42. package/dist/adapters/types.d.ts +19 -0
  43. package/dist/adapters/types.js +1 -0
  44. package/dist/branding.d.ts +3 -0
  45. package/dist/branding.js +3 -0
  46. package/dist/bundle/claude-runtime.mjs +5323 -0
  47. package/dist/bundle/codex-runtime.mjs +5310 -0
  48. package/dist/bundle/cursor-runtime.mjs +5208 -0
  49. package/dist/cleanup-orphans.d.ts +7 -0
  50. package/dist/cleanup-orphans.js +59 -0
  51. package/dist/cli.d.ts +2 -0
  52. package/dist/cli.js +631 -0
  53. package/dist/commands/approve.d.ts +14 -0
  54. package/dist/commands/approve.js +65 -0
  55. package/dist/commands/audit.d.ts +59 -0
  56. package/dist/commands/audit.js +132 -0
  57. package/dist/commands/classify-for-report.d.ts +9 -0
  58. package/dist/commands/classify-for-report.js +85 -0
  59. package/dist/commands/doctor.d.ts +3 -0
  60. package/dist/commands/doctor.js +366 -0
  61. package/dist/commands/dogfood.d.ts +5 -0
  62. package/dist/commands/dogfood.js +71 -0
  63. package/dist/commands/explain.d.ts +3 -0
  64. package/dist/commands/explain.js +133 -0
  65. package/dist/commands/health-snapshot.d.ts +2 -0
  66. package/dist/commands/health-snapshot.js +166 -0
  67. package/dist/commands/init-wizard.d.ts +16 -0
  68. package/dist/commands/init-wizard.js +50 -0
  69. package/dist/commands/metrics.d.ts +7 -0
  70. package/dist/commands/metrics.js +89 -0
  71. package/dist/commands/recover.d.ts +3 -0
  72. package/dist/commands/recover.js +105 -0
  73. package/dist/commands/report.d.ts +3 -0
  74. package/dist/commands/report.js +65 -0
  75. package/dist/commands/revoke.d.ts +5 -0
  76. package/dist/commands/revoke.js +22 -0
  77. package/dist/commands/simulate.d.ts +14 -0
  78. package/dist/commands/simulate.js +55 -0
  79. package/dist/commands/status.d.ts +5 -0
  80. package/dist/commands/status.js +107 -0
  81. package/dist/config-io.d.ts +23 -0
  82. package/dist/config-io.js +180 -0
  83. package/dist/conformance/guarantee-table.d.ts +14 -0
  84. package/dist/conformance/guarantee-table.js +95 -0
  85. package/dist/conformance/types.d.ts +6 -0
  86. package/dist/conformance/types.js +1 -0
  87. package/dist/core/approval-service.d.ts +26 -0
  88. package/dist/core/approval-service.js +41 -0
  89. package/dist/core/approval-token.d.ts +11 -0
  90. package/dist/core/approval-token.js +61 -0
  91. package/dist/core/approval.d.ts +19 -0
  92. package/dist/core/approval.js +58 -0
  93. package/dist/core/audit-analysis.d.ts +10 -0
  94. package/dist/core/audit-analysis.js +147 -0
  95. package/dist/core/audit-metrics.d.ts +51 -0
  96. package/dist/core/audit-metrics.js +155 -0
  97. package/dist/core/audit-query.d.ts +11 -0
  98. package/dist/core/audit-query.js +142 -0
  99. package/dist/core/audit-summary.d.ts +33 -0
  100. package/dist/core/audit-summary.js +111 -0
  101. package/dist/core/audit-types.d.ts +65 -0
  102. package/dist/core/audit-types.js +2 -0
  103. package/dist/core/capability/allowlist.d.ts +10 -0
  104. package/dist/core/capability/allowlist.js +53 -0
  105. package/dist/core/capability/broker.d.ts +17 -0
  106. package/dist/core/capability/broker.js +29 -0
  107. package/dist/core/capability/index.d.ts +5 -0
  108. package/dist/core/capability/index.js +4 -0
  109. package/dist/core/capability/paths.d.ts +1 -0
  110. package/dist/core/capability/paths.js +20 -0
  111. package/dist/core/capability/reasons.d.ts +2 -0
  112. package/dist/core/capability/reasons.js +4 -0
  113. package/dist/core/capability/types.d.ts +10 -0
  114. package/dist/core/capability/types.js +1 -0
  115. package/dist/core/capability-approval.d.ts +28 -0
  116. package/dist/core/capability-approval.js +43 -0
  117. package/dist/core/classify-subagent.d.ts +2 -0
  118. package/dist/core/classify-subagent.js +69 -0
  119. package/dist/core/classify-tool.d.ts +3 -0
  120. package/dist/core/classify-tool.js +311 -0
  121. package/dist/core/config-layers.d.ts +23 -0
  122. package/dist/core/config-layers.js +59 -0
  123. package/dist/core/config.d.ts +219 -0
  124. package/dist/core/config.js +720 -0
  125. package/dist/core/control-plane-isolation.d.ts +10 -0
  126. package/dist/core/control-plane-isolation.js +83 -0
  127. package/dist/core/custom-command-match.d.ts +2 -0
  128. package/dist/core/custom-command-match.js +8 -0
  129. package/dist/core/egress/allowlist.d.ts +7 -0
  130. package/dist/core/egress/allowlist.js +33 -0
  131. package/dist/core/egress/env.d.ts +3 -0
  132. package/dist/core/egress/env.js +17 -0
  133. package/dist/core/egress/fingerprint.d.ts +3 -0
  134. package/dist/core/egress/fingerprint.js +35 -0
  135. package/dist/core/egress/policy.d.ts +8 -0
  136. package/dist/core/egress/policy.js +47 -0
  137. package/dist/core/egress/proxy-server.d.ts +21 -0
  138. package/dist/core/egress/proxy-server.js +263 -0
  139. package/dist/core/egress/types.d.ts +25 -0
  140. package/dist/core/egress/types.js +1 -0
  141. package/dist/core/egress-approval.d.ts +48 -0
  142. package/dist/core/egress-approval.js +129 -0
  143. package/dist/core/fingerprint.d.ts +6 -0
  144. package/dist/core/fingerprint.js +24 -0
  145. package/dist/core/gate-contract.d.ts +48 -0
  146. package/dist/core/gate-contract.js +50 -0
  147. package/dist/core/gate-engine.d.ts +20 -0
  148. package/dist/core/gate-engine.js +172 -0
  149. package/dist/core/glob.d.ts +1 -0
  150. package/dist/core/glob.js +39 -0
  151. package/dist/core/index.d.ts +19 -0
  152. package/dist/core/index.js +15 -0
  153. package/dist/core/integrity.d.ts +15 -0
  154. package/dist/core/integrity.js +68 -0
  155. package/dist/core/judge-api-key.d.ts +4 -0
  156. package/dist/core/judge-api-key.js +11 -0
  157. package/dist/core/judge-config.d.ts +29 -0
  158. package/dist/core/judge-config.js +85 -0
  159. package/dist/core/judge-doctor.d.ts +7 -0
  160. package/dist/core/judge-doctor.js +124 -0
  161. package/dist/core/judgment.d.ts +6 -0
  162. package/dist/core/judgment.js +38 -0
  163. package/dist/core/notify.d.ts +13 -0
  164. package/dist/core/notify.js +44 -0
  165. package/dist/core/path-utils.d.ts +12 -0
  166. package/dist/core/path-utils.js +107 -0
  167. package/dist/core/reclassify.d.ts +15 -0
  168. package/dist/core/reclassify.js +82 -0
  169. package/dist/core/recover-advice.d.ts +30 -0
  170. package/dist/core/recover-advice.js +177 -0
  171. package/dist/core/recover-git-probe.d.ts +8 -0
  172. package/dist/core/recover-git-probe.js +50 -0
  173. package/dist/core/recover-select.d.ts +10 -0
  174. package/dist/core/recover-select.js +60 -0
  175. package/dist/core/scrub.d.ts +3 -0
  176. package/dist/core/scrub.js +87 -0
  177. package/dist/core/shell-substitution.d.ts +6 -0
  178. package/dist/core/shell-substitution.js +130 -0
  179. package/dist/core/shell-tokenizer.d.ts +5 -0
  180. package/dist/core/shell-tokenizer.js +129 -0
  181. package/dist/core/shell-unparseable.d.ts +4 -0
  182. package/dist/core/shell-unparseable.js +96 -0
  183. package/dist/core/transactional/diff-evaluator.d.ts +2 -0
  184. package/dist/core/transactional/diff-evaluator.js +84 -0
  185. package/dist/core/transactional/eligibility.d.ts +4 -0
  186. package/dist/core/transactional/eligibility.js +44 -0
  187. package/dist/core/transactional/git-worktree.d.ts +13 -0
  188. package/dist/core/transactional/git-worktree.js +189 -0
  189. package/dist/core/transactional/index.d.ts +5 -0
  190. package/dist/core/transactional/index.js +4 -0
  191. package/dist/core/transactional/reasons.d.ts +4 -0
  192. package/dist/core/transactional/reasons.js +8 -0
  193. package/dist/core/transactional/runner.d.ts +2 -0
  194. package/dist/core/transactional/runner.js +113 -0
  195. package/dist/core/transactional/types.d.ts +46 -0
  196. package/dist/core/transactional/types.js +1 -0
  197. package/dist/core/types.d.ts +90 -0
  198. package/dist/core/types.js +1 -0
  199. package/dist/core/v2/adapter.d.ts +14 -0
  200. package/dist/core/v2/adapter.js +118 -0
  201. package/dist/core/v2/containment.d.ts +19 -0
  202. package/dist/core/v2/containment.js +91 -0
  203. package/dist/core/v2/egress-classify.d.ts +7 -0
  204. package/dist/core/v2/egress-classify.js +216 -0
  205. package/dist/core/v2/fingerprint.d.ts +1 -0
  206. package/dist/core/v2/fingerprint.js +4 -0
  207. package/dist/core/v2/index.d.ts +12 -0
  208. package/dist/core/v2/index.js +10 -0
  209. package/dist/core/v2/judge-audit.d.ts +2 -0
  210. package/dist/core/v2/judge-audit.js +15 -0
  211. package/dist/core/v2/judge-factory.d.ts +25 -0
  212. package/dist/core/v2/judge-factory.js +75 -0
  213. package/dist/core/v2/judge-outbound.d.ts +12 -0
  214. package/dist/core/v2/judge-outbound.js +73 -0
  215. package/dist/core/v2/judge.d.ts +47 -0
  216. package/dist/core/v2/judge.js +264 -0
  217. package/dist/core/v2/launcher-resolve.d.ts +12 -0
  218. package/dist/core/v2/launcher-resolve.js +190 -0
  219. package/dist/core/v2/overrides.d.ts +7 -0
  220. package/dist/core/v2/overrides.js +37 -0
  221. package/dist/core/v2/parser.d.ts +21 -0
  222. package/dist/core/v2/parser.js +213 -0
  223. package/dist/core/v2/types.d.ts +67 -0
  224. package/dist/core/v2/types.js +1 -0
  225. package/dist/core/v2/verdict.d.ts +2 -0
  226. package/dist/core/v2/verdict.js +699 -0
  227. package/dist/corpus/evaluate.d.ts +24 -0
  228. package/dist/corpus/evaluate.js +69 -0
  229. package/dist/defaults.d.ts +18 -0
  230. package/dist/defaults.js +155 -0
  231. package/dist/egress-daemon.d.ts +1 -0
  232. package/dist/egress-daemon.js +52 -0
  233. package/dist/index.d.ts +17 -0
  234. package/dist/index.js +15 -0
  235. package/dist/installer/bootstrap.d.ts +5 -0
  236. package/dist/installer/bootstrap.js +61 -0
  237. package/dist/installer/runtime-artifacts.d.ts +3 -0
  238. package/dist/installer/runtime-artifacts.js +23 -0
  239. package/dist/installer/scope-config.d.ts +8 -0
  240. package/dist/installer/scope-config.js +25 -0
  241. package/dist/installer.d.ts +22 -0
  242. package/dist/installer.js +169 -0
  243. package/dist/node-resolution.d.ts +8 -0
  244. package/dist/node-resolution.js +237 -0
  245. package/dist/operational-insights.d.ts +19 -0
  246. package/dist/operational-insights.js +24 -0
  247. package/dist/presets.d.ts +4 -0
  248. package/dist/presets.js +95 -0
  249. package/dist/services/egress-service.d.ts +57 -0
  250. package/dist/services/egress-service.js +334 -0
  251. package/dist/services/sandbox-service.d.ts +38 -0
  252. package/dist/services/sandbox-service.js +95 -0
  253. package/dist/templates.d.ts +7 -0
  254. package/dist/templates.js +56 -0
  255. package/dist/types.d.ts +230 -0
  256. package/dist/types.js +1 -0
  257. package/dist/version.d.ts +1 -0
  258. package/dist/version.js +1 -0
  259. package/package.json +65 -0
  260. package/skills/belay/SKILL.md +52 -0
  261. package/skills/belay/belay-approve.md +7 -0
  262. package/skills/belay/belay-explain.md +11 -0
  263. package/skills/belay/belay-recover.md +13 -0
  264. package/skills/belay/belay-report.md +7 -0
  265. package/skills/belay/belay-status.md +9 -0
  266. package/skills/belay/belay-why.md +11 -0
@@ -0,0 +1,85 @@
1
+ import { normalizeJudgeProvider } from './config.js';
2
+ export const JUDGE_PROFILE_LOCAL_OLLAMA = {
3
+ provider: 'ollama',
4
+ model: 'gemma4:e2b',
5
+ endpoint: 'http://localhost:11434',
6
+ timeoutMs: 25000,
7
+ keepAlive: '30m',
8
+ };
9
+ export const JUDGE_PROFILES = {
10
+ 'local-ollama': JUDGE_PROFILE_LOCAL_OLLAMA,
11
+ };
12
+ export function resolveJudgeConfig(input = {}) {
13
+ if (input.judgeProvider) {
14
+ const provider = normalizeJudgeProvider(input.judgeProvider);
15
+ const base = provider === 'openai-compatible' ? openAiCompatibleBase(input) : JUDGE_PROFILE_LOCAL_OLLAMA;
16
+ return {
17
+ ...base,
18
+ model: input.judgeModel ?? base.model,
19
+ endpoint: input.judgeEndpoint?.trim() || base.endpoint,
20
+ };
21
+ }
22
+ if (input.judgeProfile) {
23
+ const profile = JUDGE_PROFILES[input.judgeProfile];
24
+ return {
25
+ ...profile,
26
+ model: input.judgeModel ?? profile.model,
27
+ endpoint: input.judgeEndpoint?.trim() || profile.endpoint,
28
+ };
29
+ }
30
+ if (input.existingJudge) {
31
+ return { ...input.existingJudge };
32
+ }
33
+ return { ...JUDGE_PROFILE_LOCAL_OLLAMA };
34
+ }
35
+ function openAiCompatibleBase(input) {
36
+ return {
37
+ provider: 'openai-compatible',
38
+ model: input.judgeModel ?? 'auto',
39
+ timeoutMs: 8000,
40
+ endpoint: input.judgeEndpoint?.trim() ?? null,
41
+ keepAlive: null,
42
+ };
43
+ }
44
+ export class CloudJudgeConsentRequiredError extends Error {
45
+ constructor() {
46
+ super('Cloud judge sends redacted shell commands to an external endpoint and requires an API key in BELAY_JUDGE_API_KEY or OPENAI_API_KEY. ' +
47
+ 'Pass --accept-cloud-judge to confirm, or use --judge-profile local-ollama for local-only Tier1.');
48
+ this.name = 'CloudJudgeConsentRequiredError';
49
+ }
50
+ }
51
+ export class JudgeEndpointRequiredError extends Error {
52
+ constructor() {
53
+ super('openai-compatible judge requires --judge-endpoint (or judge.endpoint in config). No default cloud base URL is applied.');
54
+ this.name = 'JudgeEndpointRequiredError';
55
+ }
56
+ }
57
+ function isCloudJudgeConfig(judge) {
58
+ return judge.provider === 'openai-compatible';
59
+ }
60
+ export function assertJudgeEndpoint(judge) {
61
+ if (judge.provider === 'openai-compatible' && !judge.endpoint?.trim()) {
62
+ throw new JudgeEndpointRequiredError();
63
+ }
64
+ }
65
+ export function resolveInitJudgeConfig(input) {
66
+ if (input.hasExplicitJudgeFlags) {
67
+ const judge = resolveJudgeConfig({
68
+ judgeProfile: input.judgeProfile,
69
+ judgeProvider: input.judgeProvider,
70
+ judgeModel: input.judgeModel,
71
+ judgeEndpoint: input.judgeEndpoint,
72
+ });
73
+ if (isCloudJudgeConfig(judge) && !input.acceptCloudJudge) {
74
+ throw new CloudJudgeConsentRequiredError();
75
+ }
76
+ assertJudgeEndpoint(judge);
77
+ return judge;
78
+ }
79
+ if (!input.isFresh && input.existingJudge) {
80
+ const judge = resolveJudgeConfig({ existingJudge: input.existingJudge });
81
+ assertJudgeEndpoint(judge);
82
+ return judge;
83
+ }
84
+ return resolveJudgeConfig({ judgeProfile: 'local-ollama' });
85
+ }
@@ -0,0 +1,7 @@
1
+ import type { BelayConfigV4 } from './config.js';
2
+ export interface JudgeDoctorResult {
3
+ issues: string[];
4
+ warnings: string[];
5
+ notes: string[];
6
+ }
7
+ export declare function diagnoseJudge(config: BelayConfigV4): Promise<JudgeDoctorResult>;
@@ -0,0 +1,124 @@
1
+ import { normalizeJudgeProvider, scrubOptionsFromConfig } from './config.js';
2
+ import { resolveJudgeApiKey } from './judge-api-key.js';
3
+ import { assertJudgeEndpoint } from './judge-config.js';
4
+ import { createOllamaJudge, createOpenAiCompatibleJudge } from './v2/judge.js';
5
+ import { loadPinnedJudgeModels, resolveCloudModel } from './v2/judge-factory.js';
6
+ export async function diagnoseJudge(config) {
7
+ const issues = [];
8
+ const warnings = [];
9
+ const notes = [];
10
+ const judge = config.judge;
11
+ const provider = normalizeJudgeProvider(judge.provider);
12
+ notes.push(`Judge provider: ${provider}`);
13
+ notes.push(`Judge model requested: ${judge.model}`);
14
+ if (config.policy.modelAssist.enabled) {
15
+ warnings.push('policy.modelAssist is enabled but is not wired to v2 Tier1. Use top-level judge instead.');
16
+ }
17
+ if (provider === 'openai-compatible') {
18
+ warnings.push('Cloud judge egress is enabled. Commands are redacted (R23) before send, but path structure and intent may still leave the repo.');
19
+ try {
20
+ assertJudgeEndpoint(judge);
21
+ notes.push(`OpenAI-compatible endpoint: ${judge.endpoint}`);
22
+ }
23
+ catch {
24
+ issues.push('openai-compatible judge requires judge.endpoint. No default cloud base URL is applied.');
25
+ return { issues, warnings, notes };
26
+ }
27
+ const keyInfo = resolveJudgeApiKey();
28
+ if (!keyInfo.key) {
29
+ issues.push('BELAY_JUDGE_API_KEY / OPENAI_API_KEY is not set. Tier1 cloud judge will fail closed to ask.');
30
+ }
31
+ else {
32
+ notes.push(`API key source: ${keyInfo.source}`);
33
+ }
34
+ const pinnedModels = await loadPinnedJudgeModels();
35
+ const resolved = resolveCloudModel(judge.model, pinnedModels['openai-compatible']);
36
+ notes.push(`Resolved model: ${resolved.resolved}`);
37
+ if (keyInfo.key && judge.endpoint?.trim()) {
38
+ const traced = createOpenAiCompatibleJudge({
39
+ endpoint: judge.endpoint.trim(),
40
+ modelRequested: judge.model,
41
+ modelResolved: resolved.resolved,
42
+ timeoutMs: Math.min(judge.timeoutMs, 5000),
43
+ apiKey: keyInfo.key,
44
+ sensitivePaths: config.classifier.sensitivePaths,
45
+ scrubOptions: scrubOptionsFromConfig(config),
46
+ fetchImpl: async () => new Response(JSON.stringify({
47
+ choices: [
48
+ {
49
+ message: {
50
+ content: JSON.stringify({
51
+ external_change: false,
52
+ destroys_outside_repo: false,
53
+ destroys_history_or_secrets: false,
54
+ reason: 'doctor_dry_run',
55
+ }),
56
+ },
57
+ },
58
+ ],
59
+ }), { status: 200 }),
60
+ });
61
+ const dryRun = await traced.evaluate({
62
+ text: 'git status',
63
+ context: { cwd: process.cwd(), repoRoot: process.cwd() },
64
+ });
65
+ if (dryRun.reason.startsWith('openai_compatible_') ||
66
+ dryRun.reason === 'outbound_scrub_failed') {
67
+ issues.push(`OpenAI-compatible judge dry-run failed: ${dryRun.reason}`);
68
+ }
69
+ else {
70
+ notes.push('OpenAI-compatible judge dry-run succeeded.');
71
+ }
72
+ }
73
+ return { issues, warnings, notes };
74
+ }
75
+ const endpoint = judge.endpoint ?? 'http://127.0.0.1:11434';
76
+ notes.push(`Ollama endpoint: ${endpoint}`);
77
+ try {
78
+ const response = await fetch(`${endpoint.replace(/\/$/, '')}/api/tags`, {
79
+ signal: AbortSignal.timeout(3000),
80
+ });
81
+ if (!response.ok) {
82
+ issues.push(`Ollama endpoint unreachable (HTTP ${response.status}). Tier1 will fail closed.`);
83
+ }
84
+ else {
85
+ const tags = (await response.json());
86
+ const names = (tags.models ?? []).map((entry) => entry.name ?? '');
87
+ const hasModel = names.some((name) => name === judge.model || name.startsWith(`${judge.model}:`));
88
+ if (!hasModel) {
89
+ issues.push(`Ollama model "${judge.model}" is not present. Pull it before enforce mode.`);
90
+ }
91
+ else {
92
+ notes.push(`Ollama model "${judge.model}" is available.`);
93
+ }
94
+ }
95
+ }
96
+ catch (error) {
97
+ const detail = error instanceof Error ? error.message : 'connection failed';
98
+ issues.push(`Ollama endpoint unreachable (${detail}). Tier1 will fail closed.`);
99
+ }
100
+ const warm = createOllamaJudge({
101
+ model: judge.model,
102
+ baseUrl: endpoint,
103
+ timeoutMs: Math.min(judge.timeoutMs, 5000),
104
+ fetchImpl: async () => new Response(JSON.stringify({
105
+ response: JSON.stringify({
106
+ external_change: false,
107
+ destroys_outside_repo: false,
108
+ destroys_history_or_secrets: false,
109
+ reason: 'doctor_warm',
110
+ }),
111
+ }), { status: 200 }),
112
+ });
113
+ const warmResult = await warm.evaluate({
114
+ text: 'git status',
115
+ context: { cwd: process.cwd(), repoRoot: process.cwd() },
116
+ });
117
+ if (warmResult.reason === 'ollama_unavailable' || warmResult.reason === 'ollama_parse_error') {
118
+ issues.push(`Ollama warm call failed: ${warmResult.reason}`);
119
+ }
120
+ else {
121
+ notes.push('Ollama warm call succeeded.');
122
+ }
123
+ return { issues, warnings, notes };
124
+ }
@@ -0,0 +1,6 @@
1
+ import type { Assessment, ConfidenceThresholds, HookVerdict } from './types.js';
2
+ export declare function verdictFromConfidence(assessment: Assessment, thresholds: ConfidenceThresholds, unknownLocalEffect: 'allow_flagged' | 'deny'): HookVerdict;
3
+ export declare function mergeAgentAssessment(independent: Assessment, agent?: Assessment): {
4
+ assessment: Assessment;
5
+ mismatch: boolean;
6
+ };
@@ -0,0 +1,38 @@
1
+ export function verdictFromConfidence(assessment, thresholds, unknownLocalEffect) {
2
+ if (assessment.external || assessment.reversibility === 'irreversible') {
3
+ if (assessment.confidence >= thresholds.allow && !assessment.external) {
4
+ return 'allow_flagged';
5
+ }
6
+ return 'deny_pending_approval';
7
+ }
8
+ if (assessment.confidence >= thresholds.allow) {
9
+ return 'allow';
10
+ }
11
+ if (assessment.confidence >= thresholds.flag) {
12
+ return 'allow_flagged';
13
+ }
14
+ if (unknownLocalEffect === 'deny') {
15
+ return 'deny_pending_approval';
16
+ }
17
+ return 'allow_flagged';
18
+ }
19
+ export function mergeAgentAssessment(independent, agent) {
20
+ if (!agent) {
21
+ return { assessment: independent, mismatch: false };
22
+ }
23
+ const mismatch = (agent.external === false && independent.external === true) ||
24
+ (agent.reversibility === 'reversible' && independent.reversibility === 'irreversible');
25
+ const confidence = mismatch
26
+ ? Math.min(independent.confidence, 0.55)
27
+ : Math.min(0.99, independent.confidence + 0.05);
28
+ return {
29
+ assessment: {
30
+ ...independent,
31
+ confidence,
32
+ signals: mismatch
33
+ ? [...independent.signals, 'agent_assessment_mismatch']
34
+ : [...independent.signals, 'agent_assessment_agreement'],
35
+ },
36
+ mismatch,
37
+ };
38
+ }
@@ -0,0 +1,13 @@
1
+ export interface DenyNotificationConfig {
2
+ webhookUrl?: string;
3
+ commandHook?: string;
4
+ }
5
+ export interface DenyNotificationEvent {
6
+ approvalId: string;
7
+ reason: string;
8
+ summary: string;
9
+ repoRoot: string;
10
+ fingerprint: string;
11
+ approvalToken?: string;
12
+ }
13
+ export declare function notifyDeny(config: DenyNotificationConfig, event: DenyNotificationEvent): Promise<void>;
@@ -0,0 +1,44 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const execFileAsync = promisify(execFile);
4
+ export async function notifyDeny(config, event) {
5
+ const payload = JSON.stringify(event);
6
+ if (config.webhookUrl) {
7
+ try {
8
+ const controller = new AbortController();
9
+ const timeout = setTimeout(() => controller.abort(), 5000);
10
+ try {
11
+ await fetch(config.webhookUrl, {
12
+ method: 'POST',
13
+ headers: { 'content-type': 'application/json' },
14
+ body: payload,
15
+ signal: controller.signal,
16
+ });
17
+ }
18
+ finally {
19
+ clearTimeout(timeout);
20
+ }
21
+ }
22
+ catch {
23
+ // best-effort notification
24
+ }
25
+ }
26
+ if (config.commandHook) {
27
+ try {
28
+ await execFileAsync(config.commandHook, [], {
29
+ env: {
30
+ ...process.env,
31
+ BELAY_APPROVAL_ID: event.approvalId,
32
+ BELAY_REASON: event.reason,
33
+ BELAY_SUMMARY: event.summary,
34
+ BELAY_REPO_ROOT: event.repoRoot,
35
+ BELAY_FINGERPRINT: event.fingerprint,
36
+ BELAY_APPROVAL_TOKEN: event.approvalToken ?? '',
37
+ },
38
+ });
39
+ }
40
+ catch {
41
+ // best-effort notification
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Resolve symlinks for the longest existing prefix of `targetPath`, then append
3
+ * any non-existent suffix without further resolution. Keeps path comparisons
4
+ * symmetric when one side is a not-yet-created file (e.g. transactional diff,
5
+ * fs-scope allowlist matching).
6
+ */
7
+ export declare function canonicalPath(targetPath: string): string;
8
+ export declare function pathWithinRoot(root: string, targetPath: string): boolean;
9
+ export declare function relativeWithinRepo(repoRoot: string, targetPath: string): string | null;
10
+ export declare function normalizeToken(token: string, repoRoot: string): string;
11
+ export declare function resolveMutationTarget(token: string, cwd: string): string | null;
12
+ export declare function hasOutsideRepoPath(tokens: string[], cwd: string, repoRoot: string): boolean;
@@ -0,0 +1,107 @@
1
+ import { existsSync, realpathSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * Resolve symlinks for the longest existing prefix of `targetPath`, then append
5
+ * any non-existent suffix without further resolution. Keeps path comparisons
6
+ * symmetric when one side is a not-yet-created file (e.g. transactional diff,
7
+ * fs-scope allowlist matching).
8
+ */
9
+ export function canonicalPath(targetPath) {
10
+ const resolved = path.resolve(targetPath);
11
+ if (!resolved) {
12
+ return resolved;
13
+ }
14
+ const parsed = path.parse(resolved);
15
+ let current = parsed.root;
16
+ const relativeParts = path
17
+ .relative(parsed.root || '.', resolved)
18
+ .split(path.sep)
19
+ .filter(Boolean);
20
+ for (let i = 0; i < relativeParts.length; i++) {
21
+ const segment = relativeParts[i];
22
+ if (!segment) {
23
+ continue;
24
+ }
25
+ const candidate = current === '' ? segment : path.join(current, segment);
26
+ if (!existsSync(candidate)) {
27
+ return path.join(candidate, ...relativeParts.slice(i + 1));
28
+ }
29
+ try {
30
+ current = realpathSync.native(candidate);
31
+ }
32
+ catch {
33
+ return path.join(candidate, ...relativeParts.slice(i + 1));
34
+ }
35
+ }
36
+ return current;
37
+ }
38
+ export function pathWithinRoot(root, targetPath) {
39
+ const resolvedRoot = canonicalPath(root);
40
+ const resolvedTarget = canonicalPath(targetPath);
41
+ const relativePath = path.relative(resolvedRoot, resolvedTarget);
42
+ if (relativePath === '') {
43
+ return true;
44
+ }
45
+ return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
46
+ }
47
+ export function relativeWithinRepo(repoRoot, targetPath) {
48
+ const resolvedRoot = canonicalPath(repoRoot);
49
+ const resolvedTarget = canonicalPath(targetPath);
50
+ const relativePath = path.relative(resolvedRoot, resolvedTarget);
51
+ if (relativePath === '') {
52
+ return '.';
53
+ }
54
+ if (relativePath.startsWith('..')) {
55
+ return null;
56
+ }
57
+ return relativePath;
58
+ }
59
+ export function normalizeToken(token, repoRoot) {
60
+ if (!path.isAbsolute(token)) {
61
+ return token;
62
+ }
63
+ const relativePath = relativeWithinRepo(repoRoot, token);
64
+ return relativePath ?? token;
65
+ }
66
+ export function resolveMutationTarget(token, cwd) {
67
+ if (!token || token === '--' || token.startsWith('-')) {
68
+ return null;
69
+ }
70
+ if (token === '2>' || token === '1>' || token === '&>' || token === '1>>' || token === '2>>') {
71
+ return null;
72
+ }
73
+ if (path.isAbsolute(token)) {
74
+ return canonicalPath(token);
75
+ }
76
+ if (token.startsWith('./') || token.startsWith('../')) {
77
+ return canonicalPath(path.resolve(cwd, token));
78
+ }
79
+ if (!token.includes('/') && !token.includes('\\')) {
80
+ return canonicalPath(path.resolve(cwd, token));
81
+ }
82
+ return canonicalPath(path.resolve(cwd, token));
83
+ }
84
+ function looksLikePathToken(token) {
85
+ if (!token || token === '--' || token.startsWith('-')) {
86
+ return false;
87
+ }
88
+ if (path.isAbsolute(token)) {
89
+ return true;
90
+ }
91
+ if (token.startsWith('./') || token.startsWith('../')) {
92
+ return true;
93
+ }
94
+ return token.includes('/') || token.includes('\\');
95
+ }
96
+ export function hasOutsideRepoPath(tokens, cwd, repoRoot) {
97
+ return tokens.some((token) => {
98
+ if (!looksLikePathToken(token)) {
99
+ return false;
100
+ }
101
+ const resolved = resolveMutationTarget(token, cwd);
102
+ if (!resolved) {
103
+ return false;
104
+ }
105
+ return relativeWithinRepo(repoRoot, resolved) === null;
106
+ });
107
+ }
@@ -0,0 +1,15 @@
1
+ import type { AuditRecord } from './audit-types.js';
2
+ import type { BelayConfigV3 } from './config.js';
3
+ import type { ClassifyResult } from './types.js';
4
+ export interface ReclassifyDiff {
5
+ timestamp?: string;
6
+ event?: string;
7
+ summary?: string;
8
+ fingerprint?: string;
9
+ previousVerdict: string;
10
+ previousReason: string;
11
+ nextVerdict: string;
12
+ nextReason: string;
13
+ }
14
+ export declare function reclassifyAuditRecord(record: AuditRecord, config: BelayConfigV3, repoRoot: string): Promise<ClassifyResult | null>;
15
+ export declare function diffReclassification(record: AuditRecord, config: BelayConfigV3, repoRoot: string): Promise<ReclassifyDiff | null>;
@@ -0,0 +1,82 @@
1
+ import { getAdapter } from '../adapters/registry.js';
2
+ import { repoShellClassifierOptions } from '../adapters/shared/gate-runtime.js';
3
+ import { detectAdapterName } from '../config-io.js';
4
+ import { GATE_EVENTS } from './audit-types.js';
5
+ import { classifyGatedAction, normalizeGatedAction } from './gate-engine.js';
6
+ function shellCommandFromSummary(summary) {
7
+ const trimmed = summary.trim();
8
+ return trimmed || null;
9
+ }
10
+ function classifierOptionsForRepo(config, repoRoot) {
11
+ const adapter = getAdapter(config.adapter ?? detectAdapterName(repoRoot));
12
+ return repoShellClassifierOptions(config, repoRoot, adapter.layout);
13
+ }
14
+ export async function reclassifyAuditRecord(record, config, repoRoot) {
15
+ if (!record.event || !GATE_EVENTS.has(record.event)) {
16
+ return null;
17
+ }
18
+ const kind = record.kind === 'tool' || record.kind === 'subagent' ? record.kind : 'shell';
19
+ const summary = record.summary ?? '';
20
+ try {
21
+ if (kind === 'shell') {
22
+ const command = shellCommandFromSummary(summary);
23
+ if (!command) {
24
+ return null;
25
+ }
26
+ const action = normalizeGatedAction({
27
+ kind: 'shell',
28
+ repoRoot,
29
+ cwd: repoRoot,
30
+ command,
31
+ });
32
+ return await classifyGatedAction(action, config, classifierOptionsForRepo(config, repoRoot));
33
+ }
34
+ if (kind === 'subagent') {
35
+ const action = normalizeGatedAction({
36
+ kind: 'subagent',
37
+ repoRoot,
38
+ cwd: repoRoot,
39
+ payload: {
40
+ tool_name: 'Task',
41
+ tool_input: { description: summary },
42
+ },
43
+ });
44
+ return await classifyGatedAction(action, config, classifierOptionsForRepo(config, repoRoot));
45
+ }
46
+ const action = normalizeGatedAction({
47
+ kind: 'tool',
48
+ repoRoot,
49
+ cwd: repoRoot,
50
+ toolName: 'Shell',
51
+ payload: {
52
+ tool_name: 'Shell',
53
+ tool_input: { command: summary },
54
+ },
55
+ });
56
+ return await classifyGatedAction(action, config, classifierOptionsForRepo(config, repoRoot));
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ export async function diffReclassification(record, config, repoRoot) {
63
+ const next = await reclassifyAuditRecord(record, config, repoRoot);
64
+ if (!next) {
65
+ return null;
66
+ }
67
+ const previousVerdict = record.verdict ?? 'unknown';
68
+ const previousReason = record.reason ?? 'unknown';
69
+ if (previousVerdict === next.verdict && previousReason === next.reason) {
70
+ return null;
71
+ }
72
+ return {
73
+ timestamp: record.timestamp,
74
+ event: record.event,
75
+ summary: record.summary,
76
+ fingerprint: record.fingerprint,
77
+ previousVerdict,
78
+ previousReason,
79
+ nextVerdict: next.verdict,
80
+ nextReason: next.reason,
81
+ };
82
+ }
@@ -0,0 +1,30 @@
1
+ import type { GitProbeResult } from './recover-git-probe.js';
2
+ import type { Assessment } from './types.js';
3
+ export interface RecoverTargetInput {
4
+ timestamp?: string;
5
+ fingerprint?: string;
6
+ summary: string;
7
+ reason: string;
8
+ effect?: string;
9
+ location?: string;
10
+ permission?: string;
11
+ assessment?: Assessment;
12
+ }
13
+ export interface RecoverAdviceInput {
14
+ repoRoot: string;
15
+ target: RecoverTargetInput;
16
+ git?: GitProbeResult;
17
+ /** Assessment confidence below this skips specific recovery commands (defaults to flag threshold). */
18
+ minAssessmentConfidence?: number;
19
+ }
20
+ export interface RecoverAdviceResult {
21
+ recoverable: boolean;
22
+ confidence: 'high' | 'medium';
23
+ disclaimer: string[];
24
+ advice: string[];
25
+ warnings: string[];
26
+ }
27
+ export declare const RECOVER_DISCLAIMER: string[];
28
+ export declare const SHOW_DONT_RUN_LEAD = "These steps may help undo the observed effect \u2014 confirm each step before running:";
29
+ export declare function containsDeniedRecoveryPattern(text: string): boolean;
30
+ export declare function buildRecoverAdvice(input: RecoverAdviceInput): RecoverAdviceResult;