@guilz-dev/belay 0.4.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +27 -2
  2. package/dist/adapters/claude/runtime-entry.js +54 -25
  3. package/dist/adapters/layouts/scope.js +3 -2
  4. package/dist/adapters/shared/gate-runtime.d.ts +8 -2
  5. package/dist/adapters/shared/gate-runtime.js +579 -181
  6. package/dist/bundle/claude-runtime.mjs +13574 -6334
  7. package/dist/bundle/codex-runtime.mjs +13108 -5903
  8. package/dist/bundle/cursor-runtime.mjs +12676 -5476
  9. package/dist/cli.js +80 -2
  10. package/dist/commands/approve.js +34 -10
  11. package/dist/commands/classify-for-report.js +1 -1
  12. package/dist/commands/config.d.ts +8 -0
  13. package/dist/commands/config.js +134 -11
  14. package/dist/commands/doctor.js +87 -1
  15. package/dist/commands/explain.js +11 -1
  16. package/dist/commands/judge.js +3 -2
  17. package/dist/commands/recovery-checkpoints.d.ts +164 -0
  18. package/dist/commands/recovery-checkpoints.js +347 -0
  19. package/dist/commands/revoke.js +19 -7
  20. package/dist/commands/session.d.ts +20 -0
  21. package/dist/commands/session.js +51 -0
  22. package/dist/commands/status.js +7 -0
  23. package/dist/config-io.js +91 -23
  24. package/dist/conformance/guarantee-posture.d.ts +21 -0
  25. package/dist/conformance/guarantee-posture.js +46 -0
  26. package/dist/conformance/guarantee-table.d.ts +1 -0
  27. package/dist/conformance/guarantee-table.js +20 -9
  28. package/dist/conformance/types.d.ts +1 -0
  29. package/dist/core/approval-replay.d.ts +1 -1
  30. package/dist/core/approval-replay.js +4 -1
  31. package/dist/core/approval-service.d.ts +6 -1
  32. package/dist/core/approval-service.js +87 -24
  33. package/dist/core/approval.d.ts +4 -0
  34. package/dist/core/approval.js +19 -5
  35. package/dist/core/audit-types.d.ts +13 -0
  36. package/dist/core/capability/approval-state-mutation.d.ts +32 -0
  37. package/dist/core/capability/approval-state-mutation.js +165 -0
  38. package/dist/core/capability/approval-v3.d.ts +32 -0
  39. package/dist/core/capability/approval-v3.js +114 -0
  40. package/dist/core/capability/attestation.d.ts +16 -0
  41. package/dist/core/capability/attestation.js +62 -0
  42. package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
  43. package/dist/core/capability/boundary-attestation-sign.js +84 -0
  44. package/dist/core/capability/boundary-driver-container.d.ts +19 -0
  45. package/dist/core/capability/boundary-driver-container.js +189 -0
  46. package/dist/core/capability/boundary-driver.d.ts +29 -0
  47. package/dist/core/capability/boundary-driver.js +44 -0
  48. package/dist/core/capability/boundary-egress.d.ts +21 -0
  49. package/dist/core/capability/boundary-egress.js +109 -0
  50. package/dist/core/capability/boundary-grant-materialize.d.ts +14 -0
  51. package/dist/core/capability/boundary-grant-materialize.js +93 -0
  52. package/dist/core/capability/boundary-profile.d.ts +15 -0
  53. package/dist/core/capability/boundary-profile.js +25 -0
  54. package/dist/core/capability/boundary-run.d.ts +48 -0
  55. package/dist/core/capability/boundary-run.js +51 -0
  56. package/dist/core/capability/boundary-session.d.ts +61 -0
  57. package/dist/core/capability/boundary-session.js +138 -0
  58. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  59. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  60. package/dist/core/capability/capability-request-hash.d.ts +2 -0
  61. package/dist/core/capability/capability-request-hash.js +8 -0
  62. package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
  63. package/dist/core/capability/gate-latency-ratchet.js +18 -0
  64. package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
  65. package/dist/core/capability/gate-policy-shadow.js +130 -0
  66. package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
  67. package/dist/core/capability/gate-shadow-audit.js +1 -0
  68. package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
  69. package/dist/core/capability/gate-shadow-ratchet.js +81 -0
  70. package/dist/core/capability/grant-consumption.d.ts +4 -0
  71. package/dist/core/capability/grant-consumption.js +11 -0
  72. package/dist/core/capability/grant-lease.d.ts +44 -0
  73. package/dist/core/capability/grant-lease.js +243 -0
  74. package/dist/core/capability/grant-loader.d.ts +12 -0
  75. package/dist/core/capability/grant-loader.js +23 -0
  76. package/dist/core/capability/grant-match.d.ts +6 -0
  77. package/dist/core/capability/grant-match.js +96 -0
  78. package/dist/core/capability/grant.d.ts +16 -0
  79. package/dist/core/capability/grant.js +16 -0
  80. package/dist/core/capability/index.d.ts +8 -0
  81. package/dist/core/capability/index.js +7 -0
  82. package/dist/core/capability/limits.d.ts +7 -0
  83. package/dist/core/capability/limits.js +55 -0
  84. package/dist/core/capability/policy-bridge.d.ts +18 -0
  85. package/dist/core/capability/policy-bridge.js +51 -0
  86. package/dist/core/capability/policy-engine.d.ts +77 -0
  87. package/dist/core/capability/policy-engine.js +663 -0
  88. package/dist/core/capability/policy-types.d.ts +20 -0
  89. package/dist/core/capability/policy-types.js +1 -0
  90. package/dist/core/capability/request.d.ts +48 -0
  91. package/dist/core/capability/request.js +1 -0
  92. package/dist/core/capability/resolver.d.ts +4 -0
  93. package/dist/core/capability/resolver.js +4 -0
  94. package/dist/core/classify-subagent.d.ts +2 -1
  95. package/dist/core/classify-subagent.js +38 -2
  96. package/dist/core/classify-tool.js +126 -62
  97. package/dist/core/config.d.ts +34 -3
  98. package/dist/core/config.js +118 -3
  99. package/dist/core/effect-ir/audit.d.ts +9 -0
  100. package/dist/core/effect-ir/audit.js +86 -0
  101. package/dist/core/effect-ir/build.d.ts +31 -0
  102. package/dist/core/effect-ir/build.js +277 -0
  103. package/dist/core/effect-ir/index.d.ts +8 -0
  104. package/dist/core/effect-ir/index.js +7 -0
  105. package/dist/core/effect-ir/normalize.d.ts +4 -0
  106. package/dist/core/effect-ir/normalize.js +104 -0
  107. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  108. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  109. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  110. package/dist/core/effect-ir/package-exec.js +292 -0
  111. package/dist/core/effect-ir/policy.d.ts +13 -0
  112. package/dist/core/effect-ir/policy.js +77 -0
  113. package/dist/core/effect-ir/types.d.ts +55 -0
  114. package/dist/core/effect-ir/types.js +1 -0
  115. package/dist/core/egress/allowlist.d.ts +1 -0
  116. package/dist/core/egress/allowlist.js +8 -0
  117. package/dist/core/egress-approval.js +45 -51
  118. package/dist/core/gate-contract.d.ts +5 -0
  119. package/dist/core/gate-contract.js +5 -0
  120. package/dist/core/gate-engine.js +30 -8
  121. package/dist/core/index.d.ts +3 -1
  122. package/dist/core/index.js +2 -1
  123. package/dist/core/judge-cloud-consent.js +29 -15
  124. package/dist/core/judge-doctor.js +39 -14
  125. package/dist/core/judge-fallback-hints.d.ts +13 -0
  126. package/dist/core/judge-fallback-hints.js +108 -0
  127. package/dist/core/network-endpoint.d.ts +10 -0
  128. package/dist/core/network-endpoint.js +51 -0
  129. package/dist/core/path-utils.d.ts +1 -0
  130. package/dist/core/path-utils.js +5 -2
  131. package/dist/core/process-runner.d.ts +10 -0
  132. package/dist/core/process-runner.js +124 -0
  133. package/dist/core/recover-advice.js +2 -1
  134. package/dist/core/recovery/artifact-store.d.ts +29 -0
  135. package/dist/core/recovery/artifact-store.js +336 -0
  136. package/dist/core/recovery/capability.d.ts +3 -0
  137. package/dist/core/recovery/capability.js +22 -0
  138. package/dist/core/recovery/checkpoint.d.ts +25 -0
  139. package/dist/core/recovery/checkpoint.js +260 -0
  140. package/dist/core/recovery/fail-closed.d.ts +6 -0
  141. package/dist/core/recovery/fail-closed.js +25 -0
  142. package/dist/core/recovery/index.d.ts +5 -0
  143. package/dist/core/recovery/index.js +4 -0
  144. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  145. package/dist/core/recovery/operator-guidance.js +39 -0
  146. package/dist/core/recovery/reconcile.d.ts +3 -0
  147. package/dist/core/recovery/reconcile.js +48 -0
  148. package/dist/core/recovery/resource-identity.d.ts +4 -0
  149. package/dist/core/recovery/resource-identity.js +34 -0
  150. package/dist/core/recovery/restore.d.ts +13 -0
  151. package/dist/core/recovery/restore.js +114 -0
  152. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  153. package/dist/core/recovery/snapshot-node.js +261 -0
  154. package/dist/core/recovery/types.d.ts +104 -0
  155. package/dist/core/recovery/types.js +1 -0
  156. package/dist/core/standing-allow.js +3 -0
  157. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  158. package/dist/core/transactional/apply-observed-changes.js +275 -0
  159. package/dist/core/transactional/backend-selector.d.ts +8 -0
  160. package/dist/core/transactional/backend-selector.js +118 -0
  161. package/dist/core/transactional/backend.d.ts +55 -0
  162. package/dist/core/transactional/backend.js +1 -0
  163. package/dist/core/transactional/diff-evaluator.js +4 -1
  164. package/dist/core/transactional/eligibility.js +4 -0
  165. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  166. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  167. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  168. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  169. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  170. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  171. package/dist/core/transactional/file-clone.d.ts +15 -0
  172. package/dist/core/transactional/file-clone.js +139 -0
  173. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  174. package/dist/core/transactional/file-tree-path.js +50 -0
  175. package/dist/core/transactional/file-tree.d.ts +41 -0
  176. package/dist/core/transactional/file-tree.js +186 -0
  177. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  178. package/dist/core/transactional/git-worktree-backend.js +50 -0
  179. package/dist/core/transactional/git-worktree.d.ts +14 -9
  180. package/dist/core/transactional/git-worktree.js +62 -87
  181. package/dist/core/transactional/index.d.ts +11 -0
  182. package/dist/core/transactional/index.js +10 -0
  183. package/dist/core/transactional/reasons.d.ts +1 -0
  184. package/dist/core/transactional/reasons.js +5 -0
  185. package/dist/core/transactional/runner.js +171 -23
  186. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  187. package/dist/core/transactional/snapshot-node.js +87 -0
  188. package/dist/core/transactional/types.d.ts +21 -1
  189. package/dist/core/types.d.ts +30 -3
  190. package/dist/core/verdict/adapter.d.ts +3 -4
  191. package/dist/core/verdict/adapter.js +24 -12
  192. package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
  193. package/dist/core/verdict/cursor-acp-client.js +345 -0
  194. package/dist/core/verdict/egress-classify.d.ts +2 -2
  195. package/dist/core/verdict/egress-classify.js +2 -2
  196. package/dist/core/verdict/git-classifier.d.ts +26 -0
  197. package/dist/core/verdict/git-classifier.js +533 -0
  198. package/dist/core/verdict/judge-audit.js +1 -0
  199. package/dist/core/verdict/judge-baseline.d.ts +1 -1
  200. package/dist/core/verdict/judge-baseline.js +1 -0
  201. package/dist/core/verdict/judge-broker-service.js +12 -0
  202. package/dist/core/verdict/judge-catalog.js +1 -0
  203. package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
  204. package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
  205. package/dist/core/verdict/judge-cli.js +5 -3
  206. package/dist/core/verdict/judge-provider-matrix.js +1 -1
  207. package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
  208. package/dist/core/verdict/judge-runtime-config.js +10 -6
  209. package/dist/core/verdict/judge-session-broker.d.ts +1 -1
  210. package/dist/core/verdict/judge-session-broker.js +10 -5
  211. package/dist/core/verdict/judge-transport.d.ts +5 -1
  212. package/dist/core/verdict/judge-transport.js +24 -14
  213. package/dist/core/verdict/judge.d.ts +2 -15
  214. package/dist/core/verdict/judge.js +5 -68
  215. package/dist/core/verdict/launcher-resolve.d.ts +2 -0
  216. package/dist/core/verdict/launcher-resolve.js +14 -16
  217. package/dist/core/verdict/prescan.d.ts +15 -0
  218. package/dist/core/verdict/prescan.js +68 -0
  219. package/dist/core/verdict/shell-policy.d.ts +26 -0
  220. package/dist/core/verdict/shell-policy.js +40 -0
  221. package/dist/core/verdict/shell-semantics.d.ts +24 -0
  222. package/dist/core/verdict/shell-semantics.js +225 -0
  223. package/dist/core/verdict/types.d.ts +18 -1
  224. package/dist/core/verdict/verdict.js +313 -178
  225. package/dist/corpus/evaluate.js +3 -7
  226. package/dist/corpus/gate-latency-budget.d.ts +39 -0
  227. package/dist/corpus/gate-latency-budget.js +33 -0
  228. package/dist/corpus/mutators.d.ts +1 -1
  229. package/dist/corpus/mutators.js +11 -0
  230. package/dist/corpus/runtime-match.js +1 -3
  231. package/dist/corpus/standing-allow-catalog.generated.js +29 -9
  232. package/dist/judge-broker-daemon.js +15 -1
  233. package/dist/services/sandbox-service.d.ts +4 -0
  234. package/dist/services/sandbox-service.js +28 -2
  235. package/dist/types.d.ts +13 -2
  236. package/dist/version.d.ts +1 -1
  237. package/dist/version.js +1 -1
  238. package/package.json +1 -1
  239. package/skills/belay/SKILL.md +8 -3
  240. package/skills/belay/belay-approve.md +10 -7
@@ -0,0 +1,108 @@
1
+ const INFRASTRUCTURE_FALLBACK_SUFFIXES = [
2
+ '_cli_nonzero',
3
+ '_cli_unavailable',
4
+ '_cli_parse_error',
5
+ '_cli_spawn_error',
6
+ ];
7
+ const INFRASTRUCTURE_FALLBACK_REASONS = new Set([
8
+ 'eval_timeout',
9
+ 'connect_timeout',
10
+ 'parse_timeout',
11
+ 'session_unavailable',
12
+ 'session_disabled',
13
+ 'provider_not_allowlisted',
14
+ 'guard_reset',
15
+ 'kill_switch',
16
+ 'non_json_response',
17
+ 'parse_error',
18
+ 'cli_error',
19
+ 'timeout',
20
+ 'version_mismatch',
21
+ 'shadow_forced_spawn',
22
+ 'unsafe_option_rejected',
23
+ 'broker_busy_reset',
24
+ ]);
25
+ function isInfrastructureFallbackReason(reason) {
26
+ if (INFRASTRUCTURE_FALLBACK_REASONS.has(reason)) {
27
+ return true;
28
+ }
29
+ return INFRASTRUCTURE_FALLBACK_SUFFIXES.some((suffix) => reason.endsWith(suffix));
30
+ }
31
+ function infrastructureSignal(signal) {
32
+ return isInfrastructureFallbackReason(signal);
33
+ }
34
+ export function extractJudgeFallbackReason(result) {
35
+ if (result.axes?.judgeFallbackReason) {
36
+ return result.axes.judgeFallbackReason;
37
+ }
38
+ return result.assessment.signals.find((signal) => isInfrastructureFallbackReason(signal));
39
+ }
40
+ export function isJudgeInfrastructureFailure(result) {
41
+ if (result.axes?.judgeProvider === 'fallback') {
42
+ return true;
43
+ }
44
+ const fallbackReason = extractJudgeFallbackReason(result);
45
+ if (fallbackReason && isInfrastructureFallbackReason(fallbackReason)) {
46
+ return true;
47
+ }
48
+ return result.assessment.signals.some(infrastructureSignal);
49
+ }
50
+ export function inferProviderIdFromFallbackReason(fallbackReason, fallbackProviderId = 'cursor') {
51
+ if (!fallbackReason) {
52
+ return fallbackProviderId;
53
+ }
54
+ const match = fallbackReason.match(/^(cursor|claude|codex)_/);
55
+ return match?.[1] ?? fallbackProviderId;
56
+ }
57
+ export function formatJudgeRecoveryHint(providerId, fallbackReason) {
58
+ if (!fallbackReason) {
59
+ return null;
60
+ }
61
+ if (fallbackReason.endsWith('_cli_nonzero') || fallbackReason.endsWith('_cli_unavailable')) {
62
+ switch (providerId) {
63
+ case 'cursor':
64
+ return 'Judge CLI is not authenticated. Run: agent login';
65
+ case 'claude':
66
+ return 'Claude CLI failed. Run: claude auth login';
67
+ case 'codex':
68
+ return 'Codex CLI failed. Check codex auth';
69
+ default:
70
+ return `Judge CLI transport failed (${fallbackReason}). Check ${providerId} CLI auth`;
71
+ }
72
+ }
73
+ if (fallbackReason.endsWith('_cli_parse_error')) {
74
+ return `Judge CLI returned an invalid response (${fallbackReason}). Retry after updating the ${providerId} CLI`;
75
+ }
76
+ if (fallbackReason.endsWith('_cli_spawn_error')) {
77
+ return `Judge CLI could not be started (${fallbackReason}). Install or repair the ${providerId} CLI`;
78
+ }
79
+ switch (fallbackReason) {
80
+ case 'eval_timeout':
81
+ case 'connect_timeout':
82
+ case 'parse_timeout':
83
+ case 'timeout':
84
+ return 'Judge timed out. Check network or increase judge.timeoutMs';
85
+ case 'session_unavailable':
86
+ case 'session_disabled':
87
+ return 'Judge session transport is unavailable. Run: belay doctor';
88
+ case 'kill_switch':
89
+ return 'Judge session kill switch is active. Run: belay judge status';
90
+ default:
91
+ if (isInfrastructureFallbackReason(fallbackReason)) {
92
+ return `Judge transport failed (${fallbackReason}). Run: belay doctor`;
93
+ }
94
+ return null;
95
+ }
96
+ }
97
+ export function formatJudgeInfrastructureDenyMessage(params) {
98
+ const fallbackReason = params.fallbackReason ?? 'judge_transport_unavailable';
99
+ const recoveryHint = formatJudgeRecoveryHint(params.providerId, fallbackReason) ??
100
+ 'Judge transport is unavailable. Run: belay doctor';
101
+ const commandHint = params.command
102
+ ? ` For details: belay explain --command "${params.command}"`
103
+ : ' For details: belay explain';
104
+ return {
105
+ user_message: `Belay could not reach the Tier1 judge (${fallbackReason}). ${recoveryHint}. Then retry.${commandHint}`,
106
+ agent_message: `Belay denied this action because the Tier1 judge transport failed (${fallbackReason}). Repair judge infrastructure, then retry the exact same action.`,
107
+ };
108
+ }
@@ -0,0 +1,10 @@
1
+ export interface NetworkEndpoint {
2
+ host: string;
3
+ port?: number;
4
+ protocol: string;
5
+ }
6
+ export interface NetworkEndpointParseOptions {
7
+ allowHostedGitShorthand?: boolean;
8
+ allowScpStyle?: boolean;
9
+ }
10
+ export declare function parseNetworkEndpoint(spec: string, options?: NetworkEndpointParseOptions): NetworkEndpoint | null;
@@ -0,0 +1,51 @@
1
+ const SUPPORTED_URL_PROTOCOLS = new Set(['http:', 'https:', 'ssh:', 'git:']);
2
+ const NON_NETWORK_COLON_PREFIXES = new Set([
3
+ 'catalog',
4
+ 'exec',
5
+ 'file',
6
+ 'link',
7
+ 'npm',
8
+ 'patch',
9
+ 'portal',
10
+ 'workspace',
11
+ ]);
12
+ const HOSTED_GIT_PREFIXES = {
13
+ bitbucket: 'bitbucket.org',
14
+ github: 'github.com',
15
+ gitlab: 'gitlab.com',
16
+ };
17
+ export function parseNetworkEndpoint(spec, options = {}) {
18
+ const normalized = spec.trim().replace(/^git\+/, '');
19
+ if (options.allowHostedGitShorthand) {
20
+ const hostedGit = normalized.match(/^([a-z]+):(.+)$/i);
21
+ const hostedGitHost = hostedGit?.[1]
22
+ ? HOSTED_GIT_PREFIXES[hostedGit[1].toLowerCase()]
23
+ : undefined;
24
+ if (hostedGitHost && hostedGit?.[2]) {
25
+ return { host: hostedGitHost, protocol: 'git' };
26
+ }
27
+ }
28
+ try {
29
+ const url = new URL(normalized);
30
+ if (SUPPORTED_URL_PROTOCOLS.has(url.protocol) && url.hostname) {
31
+ return {
32
+ host: url.hostname,
33
+ ...(url.port ? { port: Number(url.port) } : {}),
34
+ protocol: url.protocol.slice(0, -1),
35
+ };
36
+ }
37
+ }
38
+ catch { }
39
+ if (!options.allowScpStyle || /^[A-Za-z]:[\\/]/.test(normalized)) {
40
+ return null;
41
+ }
42
+ const scpStyle = normalized.match(/^(?:([^@/\s:]+)@)?([^:/\s]+):(.+)$/);
43
+ if (!scpStyle) {
44
+ return null;
45
+ }
46
+ const [, , host, remotePath] = scpStyle;
47
+ if (!host || !remotePath || NON_NETWORK_COLON_PREFIXES.has(host.toLowerCase())) {
48
+ return null;
49
+ }
50
+ return { host: host.toLowerCase(), protocol: 'ssh' };
51
+ }
@@ -5,6 +5,7 @@
5
5
  * fs-scope allowlist matching).
6
6
  */
7
7
  export declare function canonicalPath(targetPath: string): string;
8
+ export declare function isPathOutsideRoot(relativePath: string): boolean;
8
9
  export declare function pathWithinRoot(root: string, targetPath: string): boolean;
9
10
  export declare function relativeWithinRepo(repoRoot: string, targetPath: string): string | null;
10
11
  export type WorkspaceRootMatch = {
@@ -36,6 +36,9 @@ export function canonicalPath(targetPath) {
36
36
  }
37
37
  return current;
38
38
  }
39
+ export function isPathOutsideRoot(relativePath) {
40
+ return relativePath === '..' || relativePath.startsWith(`..${path.sep}`);
41
+ }
39
42
  export function pathWithinRoot(root, targetPath) {
40
43
  const resolvedRoot = canonicalPath(root);
41
44
  const resolvedTarget = canonicalPath(targetPath);
@@ -43,7 +46,7 @@ export function pathWithinRoot(root, targetPath) {
43
46
  if (relativePath === '') {
44
47
  return true;
45
48
  }
46
- return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
49
+ return !isPathOutsideRoot(relativePath) && !path.isAbsolute(relativePath);
47
50
  }
48
51
  function relativeWithinRoot(root, targetPath, options = {}) {
49
52
  const resolvedRoot = options.canonicalizeRoot === false ? path.resolve(root) : canonicalPath(root);
@@ -52,7 +55,7 @@ function relativeWithinRoot(root, targetPath, options = {}) {
52
55
  if (relativePath === '') {
53
56
  return '.';
54
57
  }
55
- if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
58
+ if (isPathOutsideRoot(relativePath) || path.isAbsolute(relativePath)) {
56
59
  return null;
57
60
  }
58
61
  return relativePath;
@@ -0,0 +1,10 @@
1
+ import { type SpawnOptionsWithoutStdio } from 'node:child_process';
2
+ export interface ShellRunResult {
3
+ exitCode: number | null;
4
+ signal: string | null;
5
+ timedOut: boolean;
6
+ stdout?: string;
7
+ stderr?: string;
8
+ }
9
+ export declare function windowsProcessTreeKillArgs(pid: number): string[];
10
+ export declare function runProcessWithBoundedOutput(file: string, args: string[], options: SpawnOptionsWithoutStdio, timeoutMs: number): Promise<ShellRunResult>;
@@ -0,0 +1,124 @@
1
+ import { spawn } from 'node:child_process';
2
+ const PROCESS_OUTPUT_TAIL_BYTES = 16_384;
3
+ const EXIT_OUTPUT_GRACE_MS = 25;
4
+ const TIMEOUT_KILL_GRACE_MS = 250;
5
+ const WINDOWS_TASKKILL_TIMEOUT_MS = 5_000;
6
+ function appendOutputTail(current, chunk) {
7
+ const next = Buffer.concat([current, Buffer.from(chunk)]);
8
+ return next.length > PROCESS_OUTPUT_TAIL_BYTES ? next.subarray(-PROCESS_OUTPUT_TAIL_BYTES) : next;
9
+ }
10
+ export function windowsProcessTreeKillArgs(pid) {
11
+ return ['/pid', String(pid), '/t', '/f'];
12
+ }
13
+ function forceKillWindowsProcessTree(pid, fallback) {
14
+ return new Promise((resolve) => {
15
+ const taskkill = spawn('taskkill', windowsProcessTreeKillArgs(pid), {
16
+ stdio: 'ignore',
17
+ windowsHide: true,
18
+ });
19
+ let settled = false;
20
+ const finish = (succeeded) => {
21
+ if (settled) {
22
+ return;
23
+ }
24
+ settled = true;
25
+ clearTimeout(timer);
26
+ if (!succeeded) {
27
+ fallback();
28
+ }
29
+ resolve();
30
+ };
31
+ const timer = setTimeout(() => {
32
+ taskkill.kill();
33
+ finish(false);
34
+ }, WINDOWS_TASKKILL_TIMEOUT_MS);
35
+ taskkill.on('error', () => finish(false));
36
+ taskkill.on('close', (code) => finish(code === 0));
37
+ });
38
+ }
39
+ export function runProcessWithBoundedOutput(file, args, options, timeoutMs) {
40
+ return new Promise((resolve) => {
41
+ const detached = process.platform !== 'win32';
42
+ const child = spawn(file, args, {
43
+ ...options,
44
+ detached,
45
+ stdio: ['ignore', 'pipe', 'pipe'],
46
+ });
47
+ let stdout = Buffer.alloc(0);
48
+ let stderr = Buffer.alloc(0);
49
+ let timedOut = false;
50
+ let settled = false;
51
+ let windowsCleanupPending = false;
52
+ let exitTimer;
53
+ child.stdout.on('data', (chunk) => {
54
+ stdout = appendOutputTail(stdout, chunk);
55
+ });
56
+ child.stderr.on('data', (chunk) => {
57
+ stderr = appendOutputTail(stderr, chunk);
58
+ });
59
+ const finish = (exitCode, signal) => {
60
+ if (settled) {
61
+ return;
62
+ }
63
+ settled = true;
64
+ clearTimeout(timeoutTimer);
65
+ if (exitTimer) {
66
+ clearTimeout(exitTimer);
67
+ }
68
+ child.stdout.destroy();
69
+ child.stderr.destroy();
70
+ resolve({
71
+ exitCode,
72
+ signal: signal ? String(signal) : null,
73
+ timedOut,
74
+ stdout: stdout.toString('utf8'),
75
+ stderr: stderr.toString('utf8'),
76
+ });
77
+ };
78
+ const kill = (signal) => {
79
+ try {
80
+ if (detached && child.pid) {
81
+ process.kill(-child.pid, signal);
82
+ return;
83
+ }
84
+ child.kill(signal);
85
+ }
86
+ catch {
87
+ // The process may already have exited between the state check and kill.
88
+ }
89
+ };
90
+ const timeoutTimer = setTimeout(() => {
91
+ timedOut = true;
92
+ if (process.platform === 'win32' && child.pid) {
93
+ windowsCleanupPending = true;
94
+ void forceKillWindowsProcessTree(child.pid, () => child.kill('SIGKILL')).then(() => {
95
+ windowsCleanupPending = false;
96
+ finish(child.exitCode, child.signalCode ?? 'SIGKILL');
97
+ });
98
+ return;
99
+ }
100
+ kill('SIGTERM');
101
+ setTimeout(() => {
102
+ kill('SIGKILL');
103
+ finish(null, 'SIGKILL');
104
+ }, TIMEOUT_KILL_GRACE_MS);
105
+ }, timeoutMs);
106
+ child.on('error', (error) => {
107
+ stderr = appendOutputTail(stderr, error.message);
108
+ finish(1, null);
109
+ });
110
+ child.on('exit', (exitCode, signal) => {
111
+ clearTimeout(timeoutTimer);
112
+ if (windowsCleanupPending) {
113
+ return;
114
+ }
115
+ exitTimer = setTimeout(() => finish(exitCode, signal), EXIT_OUTPUT_GRACE_MS);
116
+ });
117
+ child.on('close', (exitCode, signal) => {
118
+ if (windowsCleanupPending) {
119
+ return;
120
+ }
121
+ finish(exitCode, signal);
122
+ });
123
+ });
124
+ }
@@ -1,5 +1,6 @@
1
1
  export const RECOVER_DISCLAIMER = [
2
- 'Advisory only — belay does not run recovery commands.',
2
+ 'Advisory only — this report is not proof that the selected action executed. Only a checkpoint receipt shown by `belay recover list` is an executable recovery point.',
3
+ '`belay recover advice` does not run recovery commands; checkpoint restore requires a separate one-shot human approval.',
3
4
  'Advice is based on what belay observed through hooks; actions outside hook scope may not be visible.',
4
5
  'Recovery commands themselves pass through belay hooks — destructive undo steps may be blocked again.',
5
6
  ];
@@ -0,0 +1,29 @@
1
+ import type { RecoveryCheckpointManifest, RecoveryCheckpointState, RecoveryCheckpointStateV1, RecoveryReceiptV1 } from './types.js';
2
+ export declare const RECOVERY_CHECKPOINT_CORRUPT = "recovery_checkpoint_corrupt";
3
+ export interface PreparedRecoveryCheckpoint {
4
+ checkpointId: string;
5
+ manifest: RecoveryCheckpointManifest;
6
+ manifestHash: string;
7
+ proofHash: string;
8
+ }
9
+ export interface RecoveryArtifact {
10
+ artifactDir: string;
11
+ manifest: RecoveryCheckpointManifest;
12
+ state: RecoveryCheckpointStateV1;
13
+ manifestHash: string;
14
+ receipt?: RecoveryReceiptV1;
15
+ }
16
+ export declare function checkpointsRoot(stateDir: string): string;
17
+ export declare function checkpointDir(stateDir: string, checkpointId: string): string;
18
+ export declare function fsyncPath(filePath: string): Promise<void>;
19
+ export declare function atomicWriteJson(filePath: string, value: unknown): Promise<void>;
20
+ export declare function writeRecoveryState(artifactDir: string, state: RecoveryCheckpointState, manifestHash: string, detail?: string): Promise<void>;
21
+ export declare function directorySize(root: string): Promise<number>;
22
+ export declare function readRecoveryArtifact(stateDir: string, checkpointId: string): Promise<RecoveryArtifact>;
23
+ export declare function ensureRecoveryReceipt(artifactDir: string, manifest: RecoveryCheckpointManifest, manifestHash: string): Promise<RecoveryReceiptV1>;
24
+ export declare function checkpointIds(stateDir: string): Promise<string[]>;
25
+ export declare function artifactRepoRoot(stateDir: string, checkpointId: string): Promise<string | null>;
26
+ export declare function checkpointIdsForRepo(stateDir: string, repoRoot: string): Promise<string[]>;
27
+ export declare function cleanupOrphanedStaging(stateDir: string): Promise<void>;
28
+ export declare function markRecoveryCheckpointApplying(stateDir: string, checkpoint: PreparedRecoveryCheckpoint): Promise<void>;
29
+ export declare function markRecoveryCheckpointApplied(stateDir: string, checkpoint: PreparedRecoveryCheckpoint): Promise<RecoveryReceiptV1>;