@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,96 @@
1
+ /**
2
+ * Detects shell constructs that agent-belay cannot reliably parse (R1).
3
+ */
4
+ export function detectUnparseableShell(command) {
5
+ if (hasProcessSubstitution(command)) {
6
+ return true;
7
+ }
8
+ if (hasSubshell(command)) {
9
+ return true;
10
+ }
11
+ if (hasBraceGroup(command)) {
12
+ return true;
13
+ }
14
+ if (hasUnclosedQuote(command)) {
15
+ return true;
16
+ }
17
+ if (hasUnbalancedDollarParen(command)) {
18
+ return true;
19
+ }
20
+ return false;
21
+ }
22
+ function hasProcessSubstitution(command) {
23
+ return /<\s*\(/.test(command);
24
+ }
25
+ function hasSubshell(command) {
26
+ const trimmed = command.trim();
27
+ if (trimmed.startsWith('(')) {
28
+ return true;
29
+ }
30
+ return /(?:^|[;&|]\s*)\(/.test(trimmed);
31
+ }
32
+ function hasBraceGroup(command) {
33
+ const stripped = command.replace(/'[^']*'|"[^"]*"/g, ' ');
34
+ return /\{\s*[^\s}]/.test(stripped) || /;\s*\}/.test(stripped);
35
+ }
36
+ function hasUnclosedQuote(command) {
37
+ let quote = null;
38
+ let escaping = false;
39
+ for (const char of command) {
40
+ if (escaping) {
41
+ escaping = false;
42
+ continue;
43
+ }
44
+ if (char === '\\') {
45
+ escaping = true;
46
+ continue;
47
+ }
48
+ if (quote) {
49
+ if (char === quote) {
50
+ quote = null;
51
+ }
52
+ continue;
53
+ }
54
+ if (char === '"' || char === "'") {
55
+ quote = char;
56
+ }
57
+ }
58
+ return quote !== null;
59
+ }
60
+ function hasUnbalancedDollarParen(command) {
61
+ let depth = 0;
62
+ let inSingle = false;
63
+ let inDouble = false;
64
+ let escaping = false;
65
+ for (let index = 0; index < command.length; index += 1) {
66
+ const char = command[index];
67
+ if (escaping) {
68
+ escaping = false;
69
+ continue;
70
+ }
71
+ if (char === '\\' && (inSingle || inDouble)) {
72
+ escaping = true;
73
+ continue;
74
+ }
75
+ if (!inDouble && char === "'") {
76
+ inSingle = !inSingle;
77
+ continue;
78
+ }
79
+ if (!inSingle && char === '"') {
80
+ inDouble = !inDouble;
81
+ continue;
82
+ }
83
+ if (inSingle || inDouble) {
84
+ continue;
85
+ }
86
+ if (char === '$' && command[index + 1] === '(') {
87
+ depth += 1;
88
+ index += 1;
89
+ continue;
90
+ }
91
+ if (char === ')' && depth > 0) {
92
+ depth -= 1;
93
+ }
94
+ }
95
+ return depth > 0;
96
+ }
@@ -0,0 +1,2 @@
1
+ import type { TransactionalDiffContext, TransactionalDiffEvaluation, TransactionalFileChange } from './types.js';
2
+ export declare function evaluateTransactionalDiff(changes: TransactionalFileChange[], ctx: TransactionalDiffContext): TransactionalDiffEvaluation;
@@ -0,0 +1,84 @@
1
+ import path from 'node:path';
2
+ import { matchesSensitivePath } from '../glob.js';
3
+ import { canonicalPath, pathWithinRoot } from '../path-utils.js';
4
+ function categorizeChange(change, ctx) {
5
+ const absolutePath = canonicalPath(path.join(ctx.repoRoot, change.relativePath));
6
+ if (!pathWithinRoot(ctx.repoRoot, absolutePath)) {
7
+ return 'repo_outside';
8
+ }
9
+ if (ctx.protectedRoots.some((root) => pathWithinRoot(root, absolutePath) || root === absolutePath)) {
10
+ return 'control_plane';
11
+ }
12
+ if (matchesSensitivePath(change.relativePath, ctx.sensitivePaths)) {
13
+ return 'sensitive_path';
14
+ }
15
+ return 'repo_local';
16
+ }
17
+ function observedAssessment(evaluation) {
18
+ const signals = ['transactional_observed'];
19
+ for (const category of evaluation.categories) {
20
+ if (category !== 'repo_local') {
21
+ signals.push(`observed_${category}`);
22
+ }
23
+ }
24
+ if (evaluation.deletedCount > 0) {
25
+ signals.push('observed_deletions');
26
+ }
27
+ if (evaluation.categories.includes('repo_outside') ||
28
+ evaluation.categories.includes('control_plane') ||
29
+ evaluation.categories.includes('sensitive_path')) {
30
+ return {
31
+ reversibility: 'irreversible',
32
+ external: evaluation.categories.includes('repo_outside'),
33
+ blastRadius: evaluation.categories.includes('control_plane')
34
+ ? 'agent-belay control plane'
35
+ : evaluation.categories.includes('repo_outside')
36
+ ? 'outside the repository'
37
+ : 'sensitive path',
38
+ confidence: 1,
39
+ signals,
40
+ };
41
+ }
42
+ if (evaluation.categories.includes('large_deletion')) {
43
+ return {
44
+ reversibility: 'irreversible',
45
+ external: false,
46
+ blastRadius: 'directory tree',
47
+ confidence: 1,
48
+ signals,
49
+ };
50
+ }
51
+ return {
52
+ reversibility: evaluation.deletedCount > 0 ? 'recoverable_with_cost' : 'reversible',
53
+ external: false,
54
+ blastRadius: evaluation.changes.length <= 1 ? 'single file' : 'this repository',
55
+ confidence: 1,
56
+ signals,
57
+ };
58
+ }
59
+ export function evaluateTransactionalDiff(changes, ctx) {
60
+ const categories = new Set();
61
+ const deletedCount = changes.filter((change) => change.kind === 'deleted').length;
62
+ for (const change of changes) {
63
+ categories.add(categorizeChange(change, ctx));
64
+ }
65
+ if (deletedCount > ctx.maxDeletionCount) {
66
+ categories.add('large_deletion');
67
+ }
68
+ const categoryList = [...categories];
69
+ const dangerous = categoryList.includes('repo_outside') ||
70
+ categoryList.includes('control_plane') ||
71
+ categoryList.includes('sensitive_path') ||
72
+ categoryList.includes('large_deletion');
73
+ const base = {
74
+ categories: categoryList,
75
+ changes,
76
+ deletedCount,
77
+ verdict: dangerous ? 'deny_pending_approval' : 'allow',
78
+ reason: dangerous ? 'transactional_observed_risk' : 'transactional_observed_safe',
79
+ };
80
+ return {
81
+ ...base,
82
+ assessment: observedAssessment(base),
83
+ };
84
+ }
@@ -0,0 +1,4 @@
1
+ import type { BelayConfigV3 } from '../config.js';
2
+ import type { GatedActionKind } from '../gate-contract.js';
3
+ import type { ClassifyResult } from '../types.js';
4
+ export declare function isTransactionalEligible(config: BelayConfigV3, kind: GatedActionKind, result: ClassifyResult): boolean;
@@ -0,0 +1,44 @@
1
+ const EXCLUDED_REASONS = new Set([
2
+ 'unparseable_shell',
3
+ 'external_effect',
4
+ 'l3_external_hint',
5
+ 'custom_external',
6
+ 'external_script',
7
+ 'outside_repo_redirect',
8
+ 'outside_repo_mutation',
9
+ 'control_plane_mutation',
10
+ 'dynamic_shell_evaluation',
11
+ 'pipe_to_shell',
12
+ 'command_substitution',
13
+ 'agent_assessment_mismatch',
14
+ 'find_dangerous_action',
15
+ 'read_only',
16
+ 'custom_allow',
17
+ ]);
18
+ export function isTransactionalEligible(config, kind, result) {
19
+ const transactional = config.policy.transactional;
20
+ if (!transactional.enabled) {
21
+ return false;
22
+ }
23
+ if (kind !== 'shell' || !config.gates.shell || !transactional.gates.shell) {
24
+ return false;
25
+ }
26
+ if (EXCLUDED_REASONS.has(result.reason)) {
27
+ return false;
28
+ }
29
+ const { assessment } = result;
30
+ if (assessment.external) {
31
+ return false;
32
+ }
33
+ if (result.verdict === 'deny_pending_approval') {
34
+ return false;
35
+ }
36
+ if (result.verdict === 'allow' && assessment.confidence >= transactional.maxConfidence) {
37
+ return false;
38
+ }
39
+ const confidence = assessment.confidence;
40
+ if (confidence < transactional.minConfidence || confidence >= transactional.maxConfidence) {
41
+ return false;
42
+ }
43
+ return result.verdict === 'allow_flagged';
44
+ }
@@ -0,0 +1,13 @@
1
+ import type { TransactionalFileChange, TransactionalSnapshot } from './types.js';
2
+ export declare function isGitWorktreeAvailable(repoRoot: string): Promise<boolean>;
3
+ export declare function isDirtyWorktree(repoRoot: string): Promise<boolean>;
4
+ export declare function createGitWorktreeSnapshot(repoRoot: string, stateDir: string): Promise<TransactionalSnapshot>;
5
+ export declare function resolveWorktreeCwd(repoRoot: string, worktreePath: string, cwd: string): string;
6
+ export interface ShellRunResult {
7
+ exitCode: number | null;
8
+ signal: string | null;
9
+ timedOut: boolean;
10
+ }
11
+ export declare function runShellCommand(command: string, cwd: string, timeoutMs: number): Promise<ShellRunResult>;
12
+ export declare function collectWorktreeChanges(worktreePath: string): Promise<TransactionalFileChange[]>;
13
+ export declare function applyWorktreeChanges(worktreePath: string, repoRoot: string, changes: TransactionalFileChange[]): Promise<void>;
@@ -0,0 +1,189 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { existsSync } from 'node:fs';
4
+ import { copyFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { canonicalPath } from '../path-utils.js';
8
+ function execGit(repoRoot, args) {
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn('git', ['-C', repoRoot, ...args], {
11
+ stdio: ['ignore', 'pipe', 'pipe'],
12
+ });
13
+ const stdout = [];
14
+ const stderr = [];
15
+ child.stdout.on('data', (chunk) => stdout.push(Buffer.from(chunk)));
16
+ child.stderr.on('data', (chunk) => stderr.push(Buffer.from(chunk)));
17
+ child.on('error', reject);
18
+ child.on('close', (code) => {
19
+ if (code === 0) {
20
+ resolve(Buffer.concat(stdout).toString('utf8'));
21
+ return;
22
+ }
23
+ reject(new Error(`git ${args.join(' ')} failed (${code}): ${Buffer.concat(stderr).toString('utf8').trim()}`));
24
+ });
25
+ });
26
+ }
27
+ export async function isGitWorktreeAvailable(repoRoot) {
28
+ try {
29
+ await execGit(repoRoot, ['rev-parse', '--git-dir']);
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ export async function isDirtyWorktree(repoRoot) {
37
+ try {
38
+ const status = await execGit(repoRoot, ['status', '--porcelain', '--untracked-files=no']);
39
+ return status.trim().length > 0;
40
+ }
41
+ catch {
42
+ return true;
43
+ }
44
+ }
45
+ export async function createGitWorktreeSnapshot(repoRoot, stateDir) {
46
+ const worktreePath = path.join(stateDir, `tx-${randomUUID().replaceAll('-', '')}`);
47
+ await mkdir(stateDir, { recursive: true });
48
+ await execGit(repoRoot, ['worktree', 'add', '--detach', worktreePath, 'HEAD']);
49
+ return {
50
+ worktreePath,
51
+ cleanup: async () => {
52
+ try {
53
+ await execGit(repoRoot, ['worktree', 'remove', '--force', worktreePath]);
54
+ }
55
+ catch {
56
+ await rm(worktreePath, { recursive: true, force: true });
57
+ try {
58
+ await execGit(repoRoot, ['worktree', 'prune']);
59
+ }
60
+ catch {
61
+ // best effort
62
+ }
63
+ }
64
+ },
65
+ };
66
+ }
67
+ export function resolveWorktreeCwd(repoRoot, worktreePath, cwd) {
68
+ const resolvedCwd = canonicalPath(cwd);
69
+ const relative = path.relative(canonicalPath(repoRoot), resolvedCwd);
70
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
71
+ return worktreePath;
72
+ }
73
+ if (relative === '') {
74
+ return worktreePath;
75
+ }
76
+ return path.join(worktreePath, relative);
77
+ }
78
+ export function runShellCommand(command, cwd, timeoutMs) {
79
+ return new Promise((resolve) => {
80
+ const child = spawn(command, {
81
+ cwd,
82
+ shell: true,
83
+ stdio: 'ignore',
84
+ env: process.env,
85
+ });
86
+ let timedOut = false;
87
+ const timer = setTimeout(() => {
88
+ timedOut = true;
89
+ child.kill('SIGTERM');
90
+ }, timeoutMs);
91
+ child.on('error', () => {
92
+ clearTimeout(timer);
93
+ resolve({ exitCode: 1, signal: null, timedOut });
94
+ });
95
+ child.on('close', (exitCode, signal) => {
96
+ clearTimeout(timer);
97
+ resolve({
98
+ exitCode,
99
+ signal: signal ? String(signal) : null,
100
+ timedOut,
101
+ });
102
+ });
103
+ });
104
+ }
105
+ function parseStatusLine(line) {
106
+ if (line.length < 4) {
107
+ return null;
108
+ }
109
+ const status = line.slice(0, 2);
110
+ const relativePath = line.slice(3).trim();
111
+ if (!relativePath) {
112
+ return null;
113
+ }
114
+ if (status.includes('D')) {
115
+ return { relativePath, kind: 'deleted' };
116
+ }
117
+ if (status === '??') {
118
+ return { relativePath, kind: 'added' };
119
+ }
120
+ if (status.includes('A') || status.includes('?')) {
121
+ return { relativePath, kind: 'added' };
122
+ }
123
+ return { relativePath, kind: 'modified' };
124
+ }
125
+ export async function collectWorktreeChanges(worktreePath) {
126
+ const status = await execGit(worktreePath, ['status', '--porcelain']);
127
+ const changes = [];
128
+ const seen = new Set();
129
+ for (const line of status.split('\n')) {
130
+ if (!line.trim()) {
131
+ continue;
132
+ }
133
+ const change = parseStatusLine(line);
134
+ if (!change || seen.has(change.relativePath)) {
135
+ continue;
136
+ }
137
+ seen.add(change.relativePath);
138
+ changes.push(change);
139
+ }
140
+ return changes;
141
+ }
142
+ async function rollbackAppliedChanges(actions) {
143
+ for (const action of [...actions].reverse()) {
144
+ try {
145
+ if (action.type === 'restore') {
146
+ await mkdir(path.dirname(action.target), { recursive: true });
147
+ await copyFile(action.backupPath, action.target);
148
+ }
149
+ else {
150
+ await rm(action.target, { force: true });
151
+ }
152
+ }
153
+ catch {
154
+ // best effort
155
+ }
156
+ }
157
+ }
158
+ export async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
159
+ const backupRoot = await mkdtemp(path.join(os.tmpdir(), 'belay-tx-rollback-'));
160
+ const rollbackActions = [];
161
+ try {
162
+ for (const change of changes) {
163
+ const target = path.join(repoRoot, change.relativePath);
164
+ if (existsSync(target)) {
165
+ const backupPath = path.join(backupRoot, change.relativePath);
166
+ await mkdir(path.dirname(backupPath), { recursive: true });
167
+ await copyFile(target, backupPath);
168
+ rollbackActions.push({ type: 'restore', target, backupPath });
169
+ }
170
+ else if (change.kind !== 'deleted') {
171
+ rollbackActions.push({ type: 'remove', target });
172
+ }
173
+ if (change.kind === 'deleted') {
174
+ await rm(target, { force: true });
175
+ continue;
176
+ }
177
+ const source = path.join(worktreePath, change.relativePath);
178
+ await mkdir(path.dirname(target), { recursive: true });
179
+ await copyFile(source, target);
180
+ }
181
+ }
182
+ catch (error) {
183
+ await rollbackAppliedChanges(rollbackActions);
184
+ throw error;
185
+ }
186
+ finally {
187
+ await rm(backupRoot, { recursive: true, force: true });
188
+ }
189
+ }
@@ -0,0 +1,5 @@
1
+ export { evaluateTransactionalDiff } from './diff-evaluator.js';
2
+ export { isTransactionalEligible } from './eligibility.js';
3
+ export { TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, TRANSACTIONAL_OBSERVED_RISK, } from './reasons.js';
4
+ export { runTransactionalExecution } from './runner.js';
5
+ export type { TransactionalDiffEvaluation, TransactionalExecutionResult, TransactionalFileChange, TransactionalRunnerParams, } from './types.js';
@@ -0,0 +1,4 @@
1
+ export { evaluateTransactionalDiff } from './diff-evaluator.js';
2
+ export { isTransactionalEligible } from './eligibility.js';
3
+ export { TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, TRANSACTIONAL_OBSERVED_RISK, } from './reasons.js';
4
+ export { runTransactionalExecution } from './runner.js';
@@ -0,0 +1,4 @@
1
+ export declare const TRANSACTIONAL_ALREADY_APPLIED = "transactional_already_applied";
2
+ export declare const TRANSACTIONAL_OBSERVED_RISK = "transactional_observed_risk";
3
+ export declare const TRANSACTIONAL_APPLY_FAILED = "transactional_apply_failed";
4
+ export declare const TRANSACTIONAL_APPROVAL_BYPASS_REASONS: Set<string>;
@@ -0,0 +1,8 @@
1
+ export const TRANSACTIONAL_ALREADY_APPLIED = 'transactional_already_applied';
2
+ export const TRANSACTIONAL_OBSERVED_RISK = 'transactional_observed_risk';
3
+ export const TRANSACTIONAL_APPLY_FAILED = 'transactional_apply_failed';
4
+ export const TRANSACTIONAL_APPROVAL_BYPASS_REASONS = new Set([
5
+ TRANSACTIONAL_OBSERVED_RISK,
6
+ TRANSACTIONAL_ALREADY_APPLIED,
7
+ TRANSACTIONAL_APPLY_FAILED,
8
+ ]);
@@ -0,0 +1,2 @@
1
+ import type { TransactionalExecutionResult, TransactionalRunnerParams } from './types.js';
2
+ export declare function runTransactionalExecution(params: TransactionalRunnerParams): Promise<TransactionalExecutionResult>;
@@ -0,0 +1,113 @@
1
+ import { evaluateTransactionalDiff } from './diff-evaluator.js';
2
+ import { applyWorktreeChanges, collectWorktreeChanges, createGitWorktreeSnapshot, isDirtyWorktree, isGitWorktreeAvailable, resolveWorktreeCwd, runShellCommand, } from './git-worktree.js';
3
+ import { TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPLY_FAILED, TRANSACTIONAL_OBSERVED_RISK, } from './reasons.js';
4
+ export async function runTransactionalExecution(params) {
5
+ const { predicted, repoRoot, stateDir, command, cwd, timeoutMs, diffContext } = params;
6
+ if (!(await isGitWorktreeAvailable(repoRoot))) {
7
+ return {
8
+ ok: false,
9
+ skipped: true,
10
+ skipReason: 'git_worktree_unavailable',
11
+ predicted,
12
+ result: predicted,
13
+ };
14
+ }
15
+ if (await isDirtyWorktree(repoRoot)) {
16
+ return {
17
+ ok: false,
18
+ skipped: true,
19
+ skipReason: 'dirty_worktree',
20
+ predicted,
21
+ result: predicted,
22
+ };
23
+ }
24
+ let snapshot = null;
25
+ try {
26
+ snapshot = await createGitWorktreeSnapshot(repoRoot, stateDir);
27
+ const execCwd = resolveWorktreeCwd(repoRoot, snapshot.worktreePath, cwd);
28
+ const shellResult = await runShellCommand(command, execCwd, timeoutMs);
29
+ if (shellResult.timedOut) {
30
+ return {
31
+ ok: false,
32
+ skipped: true,
33
+ skipReason: 'transactional_timed_out',
34
+ predicted,
35
+ result: predicted,
36
+ commandExitCode: shellResult.exitCode,
37
+ commandSignal: shellResult.signal,
38
+ timedOut: true,
39
+ };
40
+ }
41
+ if (shellResult.exitCode !== 0 && shellResult.exitCode !== null) {
42
+ return {
43
+ ok: false,
44
+ skipped: true,
45
+ skipReason: 'transactional_command_failed',
46
+ predicted,
47
+ result: predicted,
48
+ commandExitCode: shellResult.exitCode,
49
+ commandSignal: shellResult.signal,
50
+ };
51
+ }
52
+ const changes = await collectWorktreeChanges(snapshot.worktreePath);
53
+ const observed = evaluateTransactionalDiff(changes, diffContext);
54
+ if (observed.verdict === 'allow') {
55
+ try {
56
+ await applyWorktreeChanges(snapshot.worktreePath, repoRoot, changes);
57
+ }
58
+ catch {
59
+ const result = {
60
+ ...predicted,
61
+ verdict: 'deny_pending_approval',
62
+ reason: TRANSACTIONAL_APPLY_FAILED,
63
+ assessment: {
64
+ ...observed.assessment,
65
+ reversibility: 'irreversible',
66
+ confidence: 1,
67
+ signals: [...observed.assessment.signals, 'transactional_apply_failed'],
68
+ },
69
+ };
70
+ return {
71
+ ok: true,
72
+ predicted,
73
+ observed,
74
+ result,
75
+ worktreePath: snapshot.worktreePath,
76
+ commandExitCode: shellResult.exitCode,
77
+ commandSignal: shellResult.signal,
78
+ timedOut: shellResult.timedOut,
79
+ };
80
+ }
81
+ }
82
+ const result = {
83
+ ...predicted,
84
+ verdict: observed.verdict === 'allow' ? 'allow' : 'deny_pending_approval',
85
+ reason: observed.verdict === 'allow' ? TRANSACTIONAL_ALREADY_APPLIED : TRANSACTIONAL_OBSERVED_RISK,
86
+ assessment: observed.assessment,
87
+ };
88
+ return {
89
+ ok: true,
90
+ predicted,
91
+ observed,
92
+ result,
93
+ worktreePath: snapshot.worktreePath,
94
+ commandExitCode: shellResult.exitCode,
95
+ commandSignal: shellResult.signal,
96
+ timedOut: shellResult.timedOut,
97
+ };
98
+ }
99
+ catch (error) {
100
+ return {
101
+ ok: false,
102
+ skipped: true,
103
+ skipReason: error instanceof Error ? error.message : 'transactional_execution_failed',
104
+ predicted,
105
+ result: predicted,
106
+ };
107
+ }
108
+ finally {
109
+ if (snapshot) {
110
+ await snapshot.cleanup();
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,46 @@
1
+ import type { Assessment, ClassifyResult, HookVerdict } from '../types.js';
2
+ export type TransactionalFileChangeKind = 'added' | 'modified' | 'deleted';
3
+ export interface TransactionalFileChange {
4
+ relativePath: string;
5
+ kind: TransactionalFileChangeKind;
6
+ }
7
+ export type TransactionalDiffCategory = 'repo_local' | 'repo_outside' | 'sensitive_path' | 'control_plane' | 'large_deletion';
8
+ export interface TransactionalDiffEvaluation {
9
+ verdict: HookVerdict;
10
+ reason: string;
11
+ categories: TransactionalDiffCategory[];
12
+ changes: TransactionalFileChange[];
13
+ deletedCount: number;
14
+ assessment: Assessment;
15
+ }
16
+ export interface TransactionalExecutionResult {
17
+ ok: boolean;
18
+ skipped?: boolean;
19
+ skipReason?: string;
20
+ predicted: ClassifyResult;
21
+ observed?: TransactionalDiffEvaluation;
22
+ result: ClassifyResult;
23
+ worktreePath?: string;
24
+ commandExitCode?: number | null;
25
+ commandSignal?: string | null;
26
+ timedOut?: boolean;
27
+ }
28
+ export interface TransactionalSnapshot {
29
+ worktreePath: string;
30
+ cleanup: () => Promise<void>;
31
+ }
32
+ export interface TransactionalDiffContext {
33
+ repoRoot: string;
34
+ sensitivePaths: string[];
35
+ protectedRoots: string[];
36
+ maxDeletionCount: number;
37
+ }
38
+ export interface TransactionalRunnerParams {
39
+ command: string;
40
+ cwd: string;
41
+ repoRoot: string;
42
+ stateDir: string;
43
+ timeoutMs: number;
44
+ predicted: ClassifyResult;
45
+ diffContext: TransactionalDiffContext;
46
+ }
@@ -0,0 +1 @@
1
+ export {};