@guilz-dev/belay 0.6.0 → 0.8.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 (183) hide show
  1. package/README.md +39 -20
  2. package/dist/adapters/claude/runtime-entry.js +54 -25
  3. package/dist/adapters/shared/gate-runtime.d.ts +7 -0
  4. package/dist/adapters/shared/gate-runtime.js +443 -225
  5. package/dist/bundle/claude-runtime.mjs +13027 -7271
  6. package/dist/bundle/codex-runtime.mjs +12700 -6977
  7. package/dist/bundle/cursor-runtime.mjs +12695 -6977
  8. package/dist/cli.js +37 -1
  9. package/dist/commands/approve.js +34 -10
  10. package/dist/commands/doctor.js +58 -0
  11. package/dist/commands/dogfood.js +1 -1
  12. package/dist/commands/explain.js +2 -2
  13. package/dist/commands/harvest.d.ts +0 -1
  14. package/dist/commands/harvest.js +1 -4
  15. package/dist/commands/metrics.js +6 -0
  16. package/dist/commands/quality.js +1 -3
  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/status.js +7 -0
  21. package/dist/config-io.js +49 -18
  22. package/dist/conformance/guarantee-table.js +137 -9
  23. package/dist/conformance/types.d.ts +2 -0
  24. package/dist/core/approval-replay.d.ts +1 -1
  25. package/dist/core/approval-replay.js +4 -1
  26. package/dist/core/approval-service.d.ts +6 -1
  27. package/dist/core/approval-service.js +86 -27
  28. package/dist/core/approval.d.ts +2 -0
  29. package/dist/core/approval.js +6 -3
  30. package/dist/core/audit-metrics.d.ts +1 -0
  31. package/dist/core/audit-metrics.js +12 -4
  32. package/dist/core/audit-types.d.ts +16 -0
  33. package/dist/core/capability/approval-state-mutation.d.ts +19 -0
  34. package/dist/core/capability/approval-state-mutation.js +76 -3
  35. package/dist/core/capability/approval-v3.d.ts +18 -1
  36. package/dist/core/capability/approval-v3.js +63 -23
  37. package/dist/core/capability/attestation.d.ts +3 -0
  38. package/dist/core/capability/attestation.js +7 -0
  39. package/dist/core/capability/boundary-driver-container.d.ts +12 -0
  40. package/dist/core/capability/boundary-driver-container.js +101 -34
  41. package/dist/core/capability/boundary-driver.js +1 -0
  42. package/dist/core/capability/boundary-grant-materialize.d.ts +1 -0
  43. package/dist/core/capability/boundary-grant-materialize.js +15 -0
  44. package/dist/core/capability/boundary-run.d.ts +25 -0
  45. package/dist/core/capability/boundary-run.js +37 -9
  46. package/dist/core/capability/boundary-session.d.ts +4 -0
  47. package/dist/core/capability/boundary-session.js +9 -0
  48. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  49. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  50. package/dist/core/capability/gate-policy-shadow.js +23 -1
  51. package/dist/core/capability/grant-consumption.d.ts +4 -0
  52. package/dist/core/capability/grant-consumption.js +11 -0
  53. package/dist/core/capability/grant-lease.d.ts +36 -1
  54. package/dist/core/capability/grant-lease.js +213 -16
  55. package/dist/core/capability/grant-match.js +3 -0
  56. package/dist/core/capability/index.d.ts +2 -2
  57. package/dist/core/capability/index.js +1 -1
  58. package/dist/core/capability/policy-engine.d.ts +38 -1
  59. package/dist/core/capability/policy-engine.js +388 -15
  60. package/dist/core/capability/request.d.ts +12 -0
  61. package/dist/core/capability/resolver.d.ts +0 -1
  62. package/dist/core/capability/resolver.js +0 -1
  63. package/dist/core/config.d.ts +20 -0
  64. package/dist/core/config.js +62 -2
  65. package/dist/core/effect-ir/audit.d.ts +9 -0
  66. package/dist/core/effect-ir/audit.js +87 -0
  67. package/dist/core/effect-ir/build.d.ts +31 -0
  68. package/dist/core/effect-ir/build.js +281 -0
  69. package/dist/core/effect-ir/index.d.ts +13 -0
  70. package/dist/core/effect-ir/index.js +9 -0
  71. package/dist/core/effect-ir/normalize.d.ts +4 -0
  72. package/dist/core/effect-ir/normalize.js +110 -0
  73. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  74. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  75. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  76. package/dist/core/effect-ir/package-exec.js +292 -0
  77. package/dist/core/effect-ir/policy.d.ts +14 -0
  78. package/dist/core/effect-ir/policy.js +164 -0
  79. package/dist/core/effect-ir/shell-build.d.ts +43 -0
  80. package/dist/core/effect-ir/shell-build.js +77 -0
  81. package/dist/core/effect-ir/shell-lower.d.ts +14 -0
  82. package/dist/core/effect-ir/shell-lower.js +1509 -0
  83. package/dist/core/effect-ir/types.d.ts +62 -0
  84. package/dist/core/effect-ir/types.js +1 -0
  85. package/dist/core/egress/allowlist.d.ts +1 -0
  86. package/dist/core/egress/allowlist.js +8 -0
  87. package/dist/core/egress-approval.js +45 -51
  88. package/dist/core/gate-contract.d.ts +2 -0
  89. package/dist/core/gate-contract.js +2 -0
  90. package/dist/core/gate-engine.js +82 -25
  91. package/dist/core/git-resource-identity.d.ts +26 -0
  92. package/dist/core/git-resource-identity.js +284 -0
  93. package/dist/core/harvest.d.ts +3 -7
  94. package/dist/core/harvest.js +4 -27
  95. package/dist/core/index.d.ts +5 -1
  96. package/dist/core/index.js +3 -1
  97. package/dist/core/judge-cloud-consent.js +29 -15
  98. package/dist/core/network-endpoint.d.ts +10 -0
  99. package/dist/core/network-endpoint.js +51 -0
  100. package/dist/core/path-utils.js +42 -23
  101. package/dist/core/process-runner.d.ts +10 -0
  102. package/dist/core/process-runner.js +124 -0
  103. package/dist/core/recover-advice.js +2 -1
  104. package/dist/core/recovery/artifact-store.d.ts +29 -0
  105. package/dist/core/recovery/artifact-store.js +336 -0
  106. package/dist/core/recovery/checkpoint.d.ts +25 -0
  107. package/dist/core/recovery/checkpoint.js +260 -0
  108. package/dist/core/recovery/index.d.ts +3 -1
  109. package/dist/core/recovery/index.js +2 -0
  110. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  111. package/dist/core/recovery/operator-guidance.js +39 -0
  112. package/dist/core/recovery/reconcile.d.ts +3 -0
  113. package/dist/core/recovery/reconcile.js +48 -0
  114. package/dist/core/recovery/resource-identity.d.ts +4 -0
  115. package/dist/core/recovery/resource-identity.js +34 -0
  116. package/dist/core/recovery/restore.d.ts +13 -0
  117. package/dist/core/recovery/restore.js +114 -0
  118. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  119. package/dist/core/recovery/snapshot-node.js +261 -0
  120. package/dist/core/recovery/types.d.ts +85 -1
  121. package/dist/core/shell-substitution.d.ts +1 -0
  122. package/dist/core/shell-substitution.js +61 -0
  123. package/dist/core/standing-allow.d.ts +1 -2
  124. package/dist/core/standing-allow.js +3 -20
  125. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  126. package/dist/core/transactional/apply-observed-changes.js +275 -0
  127. package/dist/core/transactional/backend-selector.d.ts +8 -0
  128. package/dist/core/transactional/backend-selector.js +118 -0
  129. package/dist/core/transactional/backend.d.ts +55 -0
  130. package/dist/core/transactional/backend.js +1 -0
  131. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  132. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  133. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  134. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  135. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  136. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  137. package/dist/core/transactional/file-clone.d.ts +15 -0
  138. package/dist/core/transactional/file-clone.js +139 -0
  139. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  140. package/dist/core/transactional/file-tree-path.js +50 -0
  141. package/dist/core/transactional/file-tree.d.ts +41 -0
  142. package/dist/core/transactional/file-tree.js +186 -0
  143. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  144. package/dist/core/transactional/git-worktree-backend.js +50 -0
  145. package/dist/core/transactional/git-worktree.d.ts +9 -11
  146. package/dist/core/transactional/git-worktree.js +24 -119
  147. package/dist/core/transactional/index.d.ts +11 -0
  148. package/dist/core/transactional/index.js +10 -0
  149. package/dist/core/transactional/reasons.js +1 -0
  150. package/dist/core/transactional/runner.js +157 -25
  151. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  152. package/dist/core/transactional/snapshot-node.js +87 -0
  153. package/dist/core/transactional/types.d.ts +17 -1
  154. package/dist/core/types.d.ts +13 -1
  155. package/dist/core/verdict/adapter.js +23 -15
  156. package/dist/core/verdict/containment.js +4 -0
  157. package/dist/core/verdict/egress-classify.d.ts +12 -0
  158. package/dist/core/verdict/egress-classify.js +746 -0
  159. package/dist/core/verdict/git-classifier.d.ts +12 -0
  160. package/dist/core/verdict/git-classifier.js +320 -0
  161. package/dist/core/verdict/launcher-resolve.js +79 -37
  162. package/dist/core/verdict/parser.d.ts +2 -0
  163. package/dist/core/verdict/parser.js +95 -1
  164. package/dist/core/verdict/types.d.ts +6 -2
  165. package/dist/core/verdict/verdict.js +103 -935
  166. package/dist/corpus/must-allow-commands.d.ts +2 -1
  167. package/dist/corpus/must-allow-commands.js +2 -1
  168. package/dist/corpus/runtime-match.d.ts +2 -1
  169. package/dist/corpus/runtime-match.js +2 -1
  170. package/dist/corpus/types.d.ts +2 -2
  171. package/dist/templates.js +7 -2
  172. package/dist/types.d.ts +11 -0
  173. package/dist/version.d.ts +1 -1
  174. package/dist/version.js +1 -1
  175. package/package.json +2 -2
  176. package/skills/belay/SKILL.md +8 -3
  177. package/skills/belay/belay-approve.md +10 -7
  178. package/dist/core/verdict/overrides.d.ts +0 -7
  179. package/dist/core/verdict/overrides.js +0 -37
  180. package/dist/core/verdict/shell-policy.d.ts +0 -25
  181. package/dist/core/verdict/shell-policy.js +0 -40
  182. package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
  183. package/dist/corpus/standing-allow-catalog.generated.js +0 -99
@@ -0,0 +1,284 @@
1
+ import { lstatSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ function canonicalExistingPath(targetPath) {
4
+ try {
5
+ return realpathSync.native(targetPath);
6
+ }
7
+ catch {
8
+ return null;
9
+ }
10
+ }
11
+ function isDirectory(targetPath) {
12
+ try {
13
+ return statSync(targetPath).isDirectory();
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ function nearestExistingDirectory(targetPath) {
20
+ let current = path.resolve(targetPath);
21
+ while (true) {
22
+ try {
23
+ const stats = statSync(current);
24
+ const directory = stats.isDirectory() ? current : path.dirname(current);
25
+ const canonical = canonicalExistingPath(directory);
26
+ return canonical ? { status: 'resolved', directory: canonical } : { status: 'invalid' };
27
+ }
28
+ catch (error) {
29
+ const code = error.code;
30
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') {
31
+ return { status: 'invalid' };
32
+ }
33
+ }
34
+ const parent = path.dirname(current);
35
+ if (parent === current) {
36
+ return { status: 'invalid' };
37
+ }
38
+ current = parent;
39
+ }
40
+ }
41
+ function readSingleMetadataLine(metadataPath) {
42
+ try {
43
+ const value = readFileSync(metadataPath, 'utf8').trim();
44
+ if (!value || value.includes('\0') || value.includes('\n') || value.includes('\r')) {
45
+ return null;
46
+ }
47
+ return value;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ function pathEntryStatus(targetPath) {
54
+ try {
55
+ lstatSync(targetPath);
56
+ return 'present';
57
+ }
58
+ catch (error) {
59
+ if (error.code === 'ENOENT') {
60
+ return 'absent';
61
+ }
62
+ return 'unreadable';
63
+ }
64
+ }
65
+ function resolveMetadataDirectory(value, baseDirectory) {
66
+ const candidate = path.isAbsolute(value) ? value : path.resolve(baseDirectory, value);
67
+ if (!isDirectory(candidate)) {
68
+ return null;
69
+ }
70
+ return canonicalExistingPath(candidate);
71
+ }
72
+ function hasValidCommonDirStructure(commonDir) {
73
+ return (hasValidHead(commonDir) &&
74
+ hasValidGitConfig(commonDir) &&
75
+ isDirectory(path.join(commonDir, 'objects')) &&
76
+ isDirectory(path.join(commonDir, 'refs')));
77
+ }
78
+ function hasValidHead(gitDir) {
79
+ const head = readSingleMetadataLine(path.join(gitDir, 'HEAD'));
80
+ return head !== null && (/^ref:\s+refs\/\S+$/.test(head) || /^[0-9a-f]{40,64}$/i.test(head));
81
+ }
82
+ function hasValidGitConfig(gitDir) {
83
+ const configPath = path.join(gitDir, 'config');
84
+ try {
85
+ return (statSync(configPath).isFile() && /^\s*\[core\]\s*$/im.test(readFileSync(configPath, 'utf8')));
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ function looksLikeBareBoundary(candidate) {
92
+ const markers = [
93
+ pathEntryStatus(path.join(candidate, 'HEAD')),
94
+ pathEntryStatus(path.join(candidate, 'config')),
95
+ pathEntryStatus(path.join(candidate, 'objects')),
96
+ pathEntryStatus(path.join(candidate, 'refs')),
97
+ ];
98
+ return markers.filter((status) => status !== 'absent').length >= 2;
99
+ }
100
+ function invalidInspection(boundaryPath, metadataRoots) {
101
+ return {
102
+ status: 'invalid',
103
+ boundaryPath,
104
+ metadataRoots: [...new Set(metadataRoots.map((root) => path.resolve(root)))],
105
+ };
106
+ }
107
+ function resolveBareIdentity(repositoryRoot) {
108
+ if (!looksLikeBareBoundary(repositoryRoot)) {
109
+ return null;
110
+ }
111
+ if (!hasValidCommonDirStructure(repositoryRoot)) {
112
+ return invalidInspection(repositoryRoot, [repositoryRoot]);
113
+ }
114
+ return {
115
+ status: 'resolved',
116
+ identity: {
117
+ repositoryRoot,
118
+ gitDir: repositoryRoot,
119
+ commonDir: repositoryRoot,
120
+ gitEntryPath: repositoryRoot,
121
+ },
122
+ };
123
+ }
124
+ function resolveLinkedIdentity(repositoryRoot, gitEntryPath) {
125
+ const forwardMetadata = readSingleMetadataLine(gitEntryPath);
126
+ const match = forwardMetadata?.match(/^gitdir:\s*(.+)$/);
127
+ const gitDirValue = match?.[1]?.trim();
128
+ if (!gitDirValue) {
129
+ return invalidInspection(repositoryRoot, [gitEntryPath]);
130
+ }
131
+ const gitDir = resolveMetadataDirectory(gitDirValue, repositoryRoot);
132
+ if (!gitDir) {
133
+ return invalidInspection(repositoryRoot, [gitEntryPath]);
134
+ }
135
+ const commonDirValue = readSingleMetadataLine(path.join(gitDir, 'commondir'));
136
+ const commonDir = commonDirValue ? resolveMetadataDirectory(commonDirValue, gitDir) : null;
137
+ const backpointerValue = readSingleMetadataLine(path.join(gitDir, 'gitdir'));
138
+ const backpointer = backpointerValue
139
+ ? canonicalExistingPath(path.isAbsolute(backpointerValue)
140
+ ? backpointerValue
141
+ : path.resolve(gitDir, backpointerValue))
142
+ : null;
143
+ const canonicalGitEntry = canonicalExistingPath(gitEntryPath);
144
+ const expectedWorktreesRoot = commonDir ? path.join(commonDir, 'worktrees') : null;
145
+ const linkedStructureValid = hasValidHead(gitDir) &&
146
+ commonDir !== null &&
147
+ hasValidCommonDirStructure(commonDir) &&
148
+ expectedWorktreesRoot !== null &&
149
+ path.dirname(gitDir) === expectedWorktreesRoot &&
150
+ backpointer !== null &&
151
+ canonicalGitEntry !== null &&
152
+ backpointer === canonicalGitEntry;
153
+ if (!linkedStructureValid || !commonDir || !canonicalGitEntry) {
154
+ return invalidInspection(repositoryRoot, [
155
+ gitEntryPath,
156
+ gitDir,
157
+ ...(commonDir ? [commonDir] : []),
158
+ ]);
159
+ }
160
+ return {
161
+ status: 'resolved',
162
+ identity: {
163
+ repositoryRoot,
164
+ gitDir,
165
+ commonDir,
166
+ gitEntryPath: canonicalGitEntry,
167
+ },
168
+ };
169
+ }
170
+ function inspectWorktreeBoundary(repositoryRoot) {
171
+ const gitEntryPath = path.join(repositoryRoot, '.git');
172
+ const entryStatus = pathEntryStatus(gitEntryPath);
173
+ if (entryStatus === 'absent') {
174
+ return null;
175
+ }
176
+ if (entryStatus === 'unreadable') {
177
+ return invalidInspection(repositoryRoot, [gitEntryPath]);
178
+ }
179
+ try {
180
+ if (lstatSync(gitEntryPath).isSymbolicLink()) {
181
+ return invalidInspection(repositoryRoot, [gitEntryPath]);
182
+ }
183
+ const entryStats = statSync(gitEntryPath);
184
+ if (entryStats.isFile()) {
185
+ return resolveLinkedIdentity(repositoryRoot, gitEntryPath);
186
+ }
187
+ if (!entryStats.isDirectory()) {
188
+ return invalidInspection(repositoryRoot, [gitEntryPath]);
189
+ }
190
+ }
191
+ catch {
192
+ return invalidInspection(repositoryRoot, [gitEntryPath]);
193
+ }
194
+ const gitDir = canonicalExistingPath(gitEntryPath);
195
+ const canonicalGitEntry = canonicalExistingPath(gitEntryPath);
196
+ if (!gitDir || !canonicalGitEntry || !hasValidCommonDirStructure(gitDir)) {
197
+ return invalidInspection(repositoryRoot, [gitEntryPath]);
198
+ }
199
+ return {
200
+ status: 'resolved',
201
+ identity: {
202
+ repositoryRoot,
203
+ gitDir,
204
+ commonDir: gitDir,
205
+ gitEntryPath: canonicalGitEntry,
206
+ },
207
+ };
208
+ }
209
+ /**
210
+ * Inspects the Git boundary containing a path without executing Git or a shell.
211
+ * Malformed or unreadable metadata is distinct from an absent Git boundary.
212
+ */
213
+ export function inspectGitResourceIdentity(targetPath) {
214
+ const nearest = nearestExistingDirectory(targetPath);
215
+ if (nearest.status === 'invalid') {
216
+ return invalidInspection(path.resolve(targetPath), [path.resolve(targetPath)]);
217
+ }
218
+ let current = nearest.directory;
219
+ while (true) {
220
+ const worktree = inspectWorktreeBoundary(current);
221
+ if (worktree) {
222
+ return worktree;
223
+ }
224
+ const bare = resolveBareIdentity(current);
225
+ if (bare) {
226
+ return bare;
227
+ }
228
+ const parent = path.dirname(current);
229
+ if (parent === current) {
230
+ return { status: 'absent' };
231
+ }
232
+ current = parent;
233
+ }
234
+ }
235
+ /** Resolves only structurally valid Git metadata; otherwise returns null. */
236
+ export function resolveGitResourceIdentity(targetPath) {
237
+ const inspection = inspectGitResourceIdentity(targetPath);
238
+ return inspection.status === 'resolved' ? inspection.identity : null;
239
+ }
240
+ export function sameGitResourceIdentity(leftPath, rightPath) {
241
+ const left = resolveGitResourceIdentity(leftPath);
242
+ const right = resolveGitResourceIdentity(rightPath);
243
+ return left !== null && right !== null && left.commonDir === right.commonDir;
244
+ }
245
+ function canonicalTargetPath(targetPath) {
246
+ const resolved = path.resolve(targetPath);
247
+ let current = resolved;
248
+ const suffix = [];
249
+ while (true) {
250
+ const canonical = canonicalExistingPath(current);
251
+ if (canonical) {
252
+ return path.join(canonical, ...suffix);
253
+ }
254
+ const parent = path.dirname(current);
255
+ if (parent === current) {
256
+ return resolved;
257
+ }
258
+ suffix.unshift(path.basename(current));
259
+ current = parent;
260
+ }
261
+ }
262
+ function pathWithin(root, targetPath) {
263
+ const relative = path.relative(root, targetPath);
264
+ return (relative === '' ||
265
+ (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`)));
266
+ }
267
+ /** Protects control data for any proven repository and any malformed Git boundary. */
268
+ export function isGitMetadataPath(targetPath, _repositoryPath) {
269
+ const inspection = inspectGitResourceIdentity(targetPath);
270
+ const target = canonicalTargetPath(targetPath);
271
+ if (inspection.status === 'invalid') {
272
+ return inspection.metadataRoots.some((root) => {
273
+ const canonicalRoot = canonicalTargetPath(root);
274
+ return target === canonicalRoot || pathWithin(canonicalRoot, target);
275
+ });
276
+ }
277
+ if (inspection.status !== 'resolved') {
278
+ return false;
279
+ }
280
+ const { identity } = inspection;
281
+ return (target === identity.gitEntryPath ||
282
+ pathWithin(identity.gitDir, target) ||
283
+ pathWithin(identity.commonDir, target));
284
+ }
@@ -1,7 +1,7 @@
1
1
  import type { CorpusCase } from '../corpus/types.js';
2
2
  import type { AuditRecord } from './audit-types.js';
3
3
  export declare const HARVEST_REPORT_SCHEMA_VERSION = 1;
4
- export type HarvestCandidateSource = 'deny_then_approve' | 'repeated_ask' | 'read_style_signal' | 'overrides_allow';
4
+ export type HarvestCandidateSource = 'deny_then_approve' | 'repeated_ask' | 'read_style_signal';
5
5
  export type HarvestReviewOutcome = 'provably-benign' | 'accepted-benign' | 'reject';
6
6
  export interface HarvestCandidate {
7
7
  kind: 'shell';
@@ -35,12 +35,8 @@ export declare function filterRecordsForHarvest(records: AuditRecord[], options?
35
35
  since?: string;
36
36
  until?: string;
37
37
  }): AuditRecord[];
38
- export declare function extractHarvestCandidates(records: AuditRecord[], options?: {
39
- allowPatterns?: string[];
40
- }): HarvestCandidate[];
41
- export declare function buildHarvestReport(records: AuditRecord[], options?: {
42
- allowPatterns?: string[];
43
- }): HarvestReport;
38
+ export declare function extractHarvestCandidates(records: AuditRecord[]): HarvestCandidate[];
39
+ export declare function buildHarvestReport(records: AuditRecord[]): HarvestReport;
44
40
  export declare function applyHarvestReview(cases: CorpusCase[], params: {
45
41
  command: string;
46
42
  outcome: HarvestReviewOutcome;
@@ -1,6 +1,5 @@
1
1
  import { computeRepeatedFingerprintAsks, isAvailabilityCausedAsk } from './audit-analysis.js';
2
2
  import { buildApprovalRoundTrips, filterAuditRecords, inferWouldBlock, isApprovalRecorded, isShellGateRecord, parseTimestamp, } from './audit-query.js';
3
- import { matchesCustomCommand } from './custom-command-match.js';
4
3
  export const HARVEST_REPORT_SCHEMA_VERSION = 1;
5
4
  const READ_STYLE_COMMAND_PATTERN = /^\s*(git\s+status|git\s+diff|git\s+log|git\s+show|ls\b|cat\b|head\b|tail\b|find\b|rg\b|grep\b|fd\b|bat\b|less\b|more\b|pwd\b|whoami\b|env\b|printenv\b|npm\s+(test|run\s+test)|pnpm\s+(test|run\s+test)|yarn\s+test)/;
6
5
  function shellRecords(records) {
@@ -145,7 +144,7 @@ export function filterRecordsForHarvest(records, options = {}) {
145
144
  });
146
145
  return augmentHarvestRoundTripPairs(scoped, timeFiltered);
147
146
  }
148
- export function extractHarvestCandidates(records, options = {}) {
147
+ export function extractHarvestCandidates(records) {
149
148
  const shellOnly = shellRecords(records);
150
149
  const classifierAsks = shellOnly.filter((record) => inferWouldBlock(record) && !isAvailabilityCausedAsk(record));
151
150
  const map = new Map();
@@ -187,28 +186,6 @@ export function extractHarvestCandidates(records, options = {}) {
187
186
  source: 'read_style_signal',
188
187
  });
189
188
  }
190
- // Current overrides.allow match on past would-blocks — not a changelog of new additions.
191
- const allowPatterns = options.allowPatterns ?? [];
192
- for (const record of classifierAsks) {
193
- if (!record.fingerprint) {
194
- continue;
195
- }
196
- const command = commandFromRecord(record);
197
- if (!command) {
198
- continue;
199
- }
200
- for (const pattern of allowPatterns) {
201
- if (matchesCustomCommand(command, command, pattern)) {
202
- upsertCandidate(map, {
203
- fingerprint: record.fingerprint,
204
- command,
205
- reason: record.reason ?? 'unknown',
206
- source: 'overrides_allow',
207
- });
208
- break;
209
- }
210
- }
211
- }
212
189
  return [...map.values()].sort((left, right) => {
213
190
  if (right.askCount !== left.askCount) {
214
191
  return right.askCount - left.askCount;
@@ -216,11 +193,11 @@ export function extractHarvestCandidates(records, options = {}) {
216
193
  return left.command.localeCompare(right.command);
217
194
  });
218
195
  }
219
- export function buildHarvestReport(records, options = {}) {
196
+ export function buildHarvestReport(records) {
220
197
  return {
221
198
  schemaVersion: HARVEST_REPORT_SCHEMA_VERSION,
222
199
  scope: 'shell',
223
- candidates: extractHarvestCandidates(records, options),
200
+ candidates: extractHarvestCandidates(records),
224
201
  availabilityQueue: extractAvailabilityQueue(records),
225
202
  };
226
203
  }
@@ -265,7 +242,7 @@ export function applyHarvestReview(cases, params) {
265
242
  ...(reason ? { reason } : {}),
266
243
  };
267
244
  const followUp = category === 'provably-benign'
268
- ? 'Next: run `pnpm corpus` and confirm hard gates pass before `pnpm build` refreshes the standing-allow catalog.'
245
+ ? 'Next: run `pnpm corpus` and confirm the CI-only hard gates pass. Corpus labels never grant runtime shell authority.'
269
246
  : 'Next: run `pnpm corpus` to verify corpus evaluation (accepted-benign is soft-gated).';
270
247
  return {
271
248
  cases: [...cases, nextCase],
@@ -3,7 +3,7 @@ export type { ApprovalReplayHint, ReplayActionContext, ReplayAdapterId, } from '
3
3
  /** @deprecated Use `buildRetryInstructionForConfig` */
4
4
  export { approvalFlow, buildApprovalRecordedMessage, buildReplayEnvelopeFields, buildReplayHint, buildRetryInstructionForConfig, buildRetryInstructionForConfig as buildRetryInstruction, canAutoReplay, getExecutionLeaseMs, replayShellCommand, validateReplayEnvelope, } from './approval-replay.js';
5
5
  export type { ApprovalStore } from './approval-service.js';
6
- export { consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
6
+ export { claimApprovedForReplay, consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
7
7
  export type { AuditMetricsReport } from './audit-metrics.js';
8
8
  export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
9
9
  export { classifySubagent } from './classify-subagent.js';
@@ -14,8 +14,12 @@ export { canonicalStringify, hashValue, shellFingerprint, subagentFingerprint, t
14
14
  export type { GatedAction, GatedActionKind, GatePermissionResponse, GateVerdict, } from './gate-contract.js';
15
15
  export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unnormalizedGateVerdict, } from './gate-contract.js';
16
16
  export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
17
+ export type { GitResourceIdentity, GitResourceIdentityInspection, } from './git-resource-identity.js';
18
+ export { inspectGitResourceIdentity, isGitMetadataPath, resolveGitResourceIdentity, sameGitResourceIdentity, } from './git-resource-identity.js';
17
19
  export { matchesSensitivePath } from './glob.js';
18
20
  export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
21
+ export type { RecoveryBackend, RecoveryCheckpointEntry, RecoveryCheckpointEntryV1, RecoveryCheckpointEntryV2, RecoveryCheckpointManifest, RecoveryCheckpointManifestV1, RecoveryCheckpointManifestV2, RecoveryCheckpointState, RecoveryCheckpointSummary, RecoveryFileSnapshotV1, RecoveryFileSnapshotV2, RecoveryProofV1, RecoveryReceiptV1, } from './recovery/index.js';
22
+ export { listRecoveryCheckpoints, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_CHECKPOINT_QUOTA, RECOVERY_RESTORE_CONFLICT, recoveryCheckpointStorageBytes, recoveryRestoreBinding, restoreRecoveryCheckpoint, showRecoveryCheckpoint, } from './recovery/index.js';
19
23
  export { fingerprintReplayPayload, subagentFingerprintSource } from './replay-scrub.js';
20
24
  export { scrubString, scrubValue } from './scrub.js';
21
25
  export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
@@ -1,7 +1,7 @@
1
1
  export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, compactApprovals, createApprovalRecord, createApprovalRecordWithEnvelope, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
2
2
  /** @deprecated Use `buildRetryInstructionForConfig` */
3
3
  export { approvalFlow, buildApprovalRecordedMessage, buildReplayEnvelopeFields, buildReplayHint, buildRetryInstructionForConfig, buildRetryInstructionForConfig as buildRetryInstruction, canAutoReplay, getExecutionLeaseMs, replayShellCommand, validateReplayEnvelope, } from './approval-replay.js';
4
- export { consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
4
+ export { claimApprovedForReplay, consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
5
5
  export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
6
6
  export { classifySubagent } from './classify-subagent.js';
7
7
  export { classifyToolUse } from './classify-tool.js';
@@ -10,8 +10,10 @@ export { matchesCustomCommand } from './custom-command-match.js';
10
10
  export { canonicalStringify, hashValue, shellFingerprint, subagentFingerprint, toolFingerprint, } from './fingerprint.js';
11
11
  export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unnormalizedGateVerdict, } from './gate-contract.js';
12
12
  export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
13
+ export { inspectGitResourceIdentity, isGitMetadataPath, resolveGitResourceIdentity, sameGitResourceIdentity, } from './git-resource-identity.js';
13
14
  export { matchesSensitivePath } from './glob.js';
14
15
  export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
16
+ export { listRecoveryCheckpoints, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_CHECKPOINT_QUOTA, RECOVERY_RESTORE_CONFLICT, recoveryCheckpointStorageBytes, recoveryRestoreBinding, restoreRecoveryCheckpoint, showRecoveryCheckpoint, } from './recovery/index.js';
15
17
  export { fingerprintReplayPayload, subagentFingerprintSource } from './replay-scrub.js';
16
18
  export { scrubString, scrubValue } from './scrub.js';
17
19
  export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { loadApprovalState, saveApprovalState } from '../config-io.js';
2
+ import { loadApprovalState, pendingApprovalsPath, saveApprovalState } from '../config-io.js';
3
3
  import { compactApprovals, createApprovalRecord } from './approval.js';
4
+ import { mutateApprovalStateWithRetry } from './capability/approval-state-mutation.js';
4
5
  import { JUDGE_CLOUD_CONSENT_REASON } from './capability/reasons.js';
5
6
  export { JUDGE_CLOUD_CONSENT_REASON };
6
7
  export function judgeCloudConsentFingerprint(providerId, endpoint) {
@@ -10,18 +11,8 @@ export async function ensurePendingJudgeCloudConsentApproval(params) {
10
11
  const { repoRoot, config, providerId, endpoint } = params;
11
12
  const normalizedEndpoint = endpoint.trim();
12
13
  const fingerprint = judgeCloudConsentFingerprint(providerId, normalizedEndpoint);
13
- const pending = await loadApprovalState(repoRoot, 'pending-approvals.json', config);
14
- const compacted = compactApprovals(pending);
15
- const existing = compacted.approvals.find((approval) => approval.kind === 'capability' &&
16
- approval.reason === JUDGE_CLOUD_CONSENT_REASON &&
17
- approval.fingerprint === fingerprint &&
18
- approval.repoRoot === repoRoot);
19
- if (existing) {
20
- await saveApprovalState(repoRoot, 'pending-approvals.json', compacted, config);
21
- return { approvalId: existing.approvalId, created: false };
22
- }
23
14
  const approvalId = `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`;
24
- const approval = createApprovalRecord({
15
+ const candidate = createApprovalRecord({
25
16
  kind: 'capability',
26
17
  fingerprint,
27
18
  repoRoot,
@@ -32,7 +23,30 @@ export async function ensurePendingJudgeCloudConsentApproval(params) {
32
23
  input: `belay judge use ${providerId} --endpoint ${normalizedEndpoint}`,
33
24
  inputKind: 'shell',
34
25
  });
35
- compacted.approvals.push(approval);
36
- await saveApprovalState(repoRoot, 'pending-approvals.json', compacted, config);
37
- return { approvalId, created: true };
26
+ const outcome = await mutateApprovalStateWithRetry({
27
+ load: async () => ({
28
+ filePath: pendingApprovalsPath(repoRoot, config),
29
+ state: await loadApprovalState(repoRoot, 'pending-approvals.json', config),
30
+ }),
31
+ write: async (_filePath, state) => saveApprovalState(repoRoot, 'pending-approvals.json', state, config),
32
+ mutate: (state) => {
33
+ const compacted = compactApprovals(state);
34
+ const existing = compacted.approvals.find((approval) => approval.kind === 'capability' &&
35
+ approval.reason === JUDGE_CLOUD_CONSENT_REASON &&
36
+ approval.fingerprint === fingerprint &&
37
+ approval.repoRoot === repoRoot);
38
+ if (existing) {
39
+ return {
40
+ state: compacted,
41
+ result: { approvalId: existing.approvalId, created: false },
42
+ };
43
+ }
44
+ compacted.approvals.push(candidate);
45
+ return { state: compacted, result: { approvalId, created: true } };
46
+ },
47
+ });
48
+ if (!outcome) {
49
+ throw new Error('Failed to persist cloud judge consent approval');
50
+ }
51
+ return outcome;
38
52
  }
@@ -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
+ }
@@ -1,5 +1,6 @@
1
- import { existsSync, realpathSync, statSync } from 'node:fs';
1
+ import { existsSync, realpathSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { inspectGitResourceIdentity } from './git-resource-identity.js';
3
4
  import { isFdDuplication, isRedirectOperator } from './shell-tokenizer.js';
4
5
  /**
5
6
  * Resolve symlinks for the longest existing prefix of `targetPath`, then append
@@ -65,9 +66,42 @@ export function relativeWithinRepo(repoRoot, targetPath) {
65
66
  }
66
67
  export function resolveWorkspaceRootMatch(repoRoot, trustedRoots = [], targetPath) {
67
68
  const canonicalRepoRoot = canonicalPath(repoRoot);
68
- const repoRelative = relativeWithinRoot(repoRoot, targetPath);
69
- if (repoRelative !== null) {
70
- return { kind: 'repo', root: canonicalRepoRoot, relativePath: repoRelative };
69
+ const repoInspection = inspectGitResourceIdentity(repoRoot);
70
+ const targetInspection = inspectGitResourceIdentity(targetPath);
71
+ if (repoInspection.status === 'resolved') {
72
+ if (targetInspection.status === 'resolved') {
73
+ if (repoInspection.identity.commonDir === targetInspection.identity.commonDir) {
74
+ const relativePath = relativeWithinRoot(targetInspection.identity.repositoryRoot, targetPath);
75
+ if (relativePath !== null) {
76
+ return {
77
+ kind: 'repo',
78
+ root: targetInspection.identity.repositoryRoot,
79
+ relativePath,
80
+ };
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ if (targetInspection.status === 'invalid') {
86
+ return null;
87
+ }
88
+ if (relativeWithinRoot(repoRoot, targetPath) !== null) {
89
+ return null;
90
+ }
91
+ }
92
+ else if (repoInspection.status === 'invalid') {
93
+ if (relativeWithinRoot(repoRoot, targetPath) !== null || targetInspection.status !== 'absent') {
94
+ return null;
95
+ }
96
+ }
97
+ else if (targetInspection.status === 'absent') {
98
+ const repoRelative = relativeWithinRoot(repoRoot, targetPath);
99
+ if (repoRelative !== null) {
100
+ return { kind: 'repo', root: canonicalRepoRoot, relativePath: repoRelative };
101
+ }
102
+ }
103
+ else {
104
+ return null;
71
105
  }
72
106
  const normalizedTrustedRoots = [...new Set(trustedRoots.map((root) => path.resolve(root)))]
73
107
  .filter((root) => root !== canonicalRepoRoot)
@@ -137,24 +171,9 @@ export function hasOutsideRepoPath(tokens, cwd, repoRoot) {
137
171
  });
138
172
  }
139
173
  export function containingGitRoot(targetPath) {
140
- const resolved = canonicalPath(targetPath);
141
- let current = resolved;
142
- try {
143
- if (!statSync(current).isDirectory()) {
144
- current = path.dirname(current);
145
- }
146
- }
147
- catch {
148
- current = path.dirname(current);
149
- }
150
- while (true) {
151
- if (existsSync(path.join(current, '.git'))) {
152
- return current;
153
- }
154
- const parent = path.dirname(current);
155
- if (parent === current) {
156
- return null;
157
- }
158
- current = parent;
174
+ const inspection = inspectGitResourceIdentity(targetPath);
175
+ if (inspection.status === 'resolved') {
176
+ return inspection.identity.repositoryRoot;
159
177
  }
178
+ return inspection.status === 'invalid' ? inspection.boundaryPath : null;
160
179
  }
@@ -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>;