@guilz-dev/belay 0.8.0 → 0.9.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 (111) hide show
  1. package/README.md +60 -10
  2. package/dist/adapters/shared/gate-runtime.d.ts +5 -0
  3. package/dist/adapters/shared/gate-runtime.js +237 -33
  4. package/dist/bundle/claude-runtime.mjs +4082 -1230
  5. package/dist/bundle/codex-runtime.mjs +4057 -1205
  6. package/dist/bundle/cursor-runtime.mjs +4057 -1205
  7. package/dist/commands/classify-for-report.js +5 -0
  8. package/dist/commands/doctor.js +90 -27
  9. package/dist/commands/explain.js +3 -2
  10. package/dist/commands/metrics.js +75 -4
  11. package/dist/commands/quality.js +2 -6
  12. package/dist/commands/recovery-checkpoints.d.ts +8 -1
  13. package/dist/commands/recovery-checkpoints.js +44 -11
  14. package/dist/commands/session.d.ts +5 -0
  15. package/dist/commands/session.js +8 -1
  16. package/dist/commands/status.js +1 -1
  17. package/dist/config-io.js +2 -2
  18. package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
  19. package/dist/conformance/contained-execution-guarantee.js +121 -0
  20. package/dist/conformance/guarantee-table.js +7 -0
  21. package/dist/core/audit-io.js +8 -0
  22. package/dist/core/audit-metrics.d.ts +34 -0
  23. package/dist/core/audit-metrics.js +87 -15
  24. package/dist/core/audit-recovery-metrics.d.ts +33 -0
  25. package/dist/core/audit-recovery-metrics.js +146 -0
  26. package/dist/core/audit-types.d.ts +7 -1
  27. package/dist/core/audit-types.js +1 -1
  28. package/dist/core/bounded-output.d.ts +11 -0
  29. package/dist/core/bounded-output.js +26 -0
  30. package/dist/core/capability/attestation.d.ts +56 -0
  31. package/dist/core/capability/attestation.js +110 -3
  32. package/dist/core/capability/boundary-session.d.ts +5 -0
  33. package/dist/core/capability/boundary-session.js +38 -1
  34. package/dist/core/capability/index.d.ts +1 -1
  35. package/dist/core/capability/index.js +1 -1
  36. package/dist/core/config.d.ts +18 -0
  37. package/dist/core/config.js +89 -10
  38. package/dist/core/contained-execution/docker-policy.d.ts +18 -0
  39. package/dist/core/contained-execution/docker-policy.js +97 -0
  40. package/dist/core/contained-execution/docker.d.ts +223 -0
  41. package/dist/core/contained-execution/docker.js +846 -0
  42. package/dist/core/contained-execution/eligibility.d.ts +9 -0
  43. package/dist/core/contained-execution/eligibility.js +103 -0
  44. package/dist/core/contained-execution/failure.d.ts +18 -0
  45. package/dist/core/contained-execution/failure.js +105 -0
  46. package/dist/core/contained-execution/mirror.d.ts +54 -0
  47. package/dist/core/contained-execution/mirror.js +587 -0
  48. package/dist/core/contained-execution/policy.d.ts +10 -0
  49. package/dist/core/contained-execution/policy.js +20 -0
  50. package/dist/core/effect-ir/index.d.ts +1 -1
  51. package/dist/core/effect-ir/index.js +1 -1
  52. package/dist/core/effect-ir/normalize.d.ts +5 -0
  53. package/dist/core/effect-ir/normalize.js +17 -7
  54. package/dist/core/effect-ir/shell-build.js +2 -14
  55. package/dist/core/effect-ir/shell-lower.js +100 -25
  56. package/dist/core/gate-contract.d.ts +4 -1
  57. package/dist/core/gate-contract.js +2 -0
  58. package/dist/core/process-runner.d.ts +10 -3
  59. package/dist/core/process-runner.js +66 -10
  60. package/dist/core/recovery/checkpoint.d.ts +3 -0
  61. package/dist/core/recovery/checkpoint.js +33 -4
  62. package/dist/core/recovery/types.d.ts +1 -0
  63. package/dist/core/scrub.d.ts +17 -0
  64. package/dist/core/scrub.js +499 -5
  65. package/dist/core/standing-allow.d.ts +0 -20
  66. package/dist/core/standing-allow.js +0 -71
  67. package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
  68. package/dist/core/transactional/apply-observed-changes.js +11 -0
  69. package/dist/core/transactional/backend-selector.d.ts +2 -1
  70. package/dist/core/transactional/backend-selector.js +32 -29
  71. package/dist/core/transactional/backend.d.ts +3 -0
  72. package/dist/core/transactional/file-checkpoint-backend.js +242 -54
  73. package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
  74. package/dist/core/transactional/file-checkpoint-git.js +53 -0
  75. package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
  76. package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
  77. package/dist/core/transactional/git-worktree-backend.js +3 -0
  78. package/dist/core/transactional/git-worktree.d.ts +1 -0
  79. package/dist/core/transactional/git-worktree.js +1 -0
  80. package/dist/core/transactional/runner.js +59 -13
  81. package/dist/core/types.d.ts +13 -0
  82. package/dist/core/verdict/parser.d.ts +10 -0
  83. package/dist/core/verdict/parser.js +271 -40
  84. package/dist/core/verdict/shell-semantics.js +21 -0
  85. package/dist/corpus/adversarial-probe.d.ts +1 -1
  86. package/dist/corpus/adversarial-probe.js +3 -3
  87. package/dist/corpus/benign-probe-cores.d.ts +6 -0
  88. package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
  89. package/dist/corpus/coverage-compare.d.ts +36 -0
  90. package/dist/corpus/coverage-compare.js +197 -0
  91. package/dist/corpus/coverage-contexts.d.ts +17 -0
  92. package/dist/corpus/coverage-contexts.js +88 -0
  93. package/dist/corpus/coverage-matrix.d.ts +37 -0
  94. package/dist/corpus/coverage-matrix.js +159 -0
  95. package/dist/corpus/coverage-probe.d.ts +91 -0
  96. package/dist/corpus/coverage-probe.js +341 -0
  97. package/dist/corpus/evaluate.d.ts +1 -1
  98. package/dist/corpus/evaluate.js +1 -1
  99. package/dist/corpus/structural-fixture-root.d.ts +2 -0
  100. package/dist/corpus/structural-fixture-root.js +5 -0
  101. package/dist/operational-insights.d.ts +3 -0
  102. package/dist/operational-insights.js +7 -3
  103. package/dist/runtime-provenance.d.ts +8 -0
  104. package/dist/runtime-provenance.js +34 -0
  105. package/dist/types.d.ts +3 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/package.json +3 -1
  109. package/skills/belay/SKILL.md +19 -0
  110. package/skills/belay/belay-status.md +5 -0
  111. package/dist/corpus/must-allow-commands.d.ts +0 -6
@@ -0,0 +1,341 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { classifierOptionsFromConfig } from '../core/config.js';
5
+ import { classifyShell } from '../core/verdict/adapter.js';
6
+ import { CoverageCompareError, compareBaselineWarnings, compareCoverageReports, formatCoverageCompareReport, parseCoverageProbeReportForCompare, } from './coverage-compare.js';
7
+ import { buildCoverageEvalContexts, hashStableJson, resolvedConfigHash, } from './coverage-contexts.js';
8
+ import { assertKnownContextIds, CoverageMatrixSchemaError, DEFAULT_PROBE_CONTEXT_IDS, defaultCoverageMatrixPath, flattenCoverageCases, loadCoverageMatrix, } from './coverage-matrix.js';
9
+ export class CoverageProbeCliError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'CoverageProbeCliError';
13
+ }
14
+ }
15
+ export const COVERAGE_PROBE_REPORT_SCHEMA_VERSION = 2;
16
+ function defaultRepoRoot() {
17
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
18
+ }
19
+ export const defaultClassifyFn = async (command, evalContext) => {
20
+ const options = {
21
+ ...classifierOptionsFromConfig(evalContext.config),
22
+ ...evalContext.options,
23
+ };
24
+ return classifyShell(command, evalContext.cwd, evalContext.repoRoot, evalContext.config, options);
25
+ };
26
+ function matchesExpectation(expectation, actual) {
27
+ if (actual.verdict !== expectation.verdict) {
28
+ return false;
29
+ }
30
+ if (expectation.reason !== undefined && actual.reason !== expectation.reason) {
31
+ return false;
32
+ }
33
+ return true;
34
+ }
35
+ function caseMatchesFilter(testCase, filters) {
36
+ if (filters.length === 0) {
37
+ return true;
38
+ }
39
+ const filterSet = new Set(filters);
40
+ if (filterSet.has(testCase.groupId) || filterSet.has(testCase.id)) {
41
+ return true;
42
+ }
43
+ return testCase.tags.some((tag) => filterSet.has(tag));
44
+ }
45
+ async function readPackageVersion(repoRoot) {
46
+ try {
47
+ const raw = JSON.parse(await readFile(path.join(repoRoot, 'package.json'), 'utf8'));
48
+ return raw.version ?? null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function readGitSha() {
55
+ return process.env.GITHUB_SHA ?? process.env.GIT_SHA ?? null;
56
+ }
57
+ function initBucket() {
58
+ return { total: 0, matched: 0, mismatched: 0, observeOnly: 0 };
59
+ }
60
+ function recordBucket(bucket, result) {
61
+ bucket.total += 1;
62
+ if (result.observeOnly) {
63
+ bucket.observeOnly += 1;
64
+ return;
65
+ }
66
+ if (result.match) {
67
+ bucket.matched += 1;
68
+ }
69
+ else {
70
+ bucket.mismatched += 1;
71
+ }
72
+ }
73
+ export async function evaluateCoverageMatrix(matrix, options = {}) {
74
+ const classifyFn = options.classifyFn ?? defaultClassifyFn;
75
+ const contextIds = options.contextIds ?? [...DEFAULT_PROBE_CONTEXT_IDS];
76
+ const repoRoot = path.resolve(options.repoRoot ?? defaultRepoRoot());
77
+ const filters = options.filters ?? [];
78
+ const contexts = options.evalContexts ?? (await buildCoverageEvalContexts(contextIds, repoRoot));
79
+ const cases = flattenCoverageCases(matrix).filter((testCase) => caseMatchesFilter(testCase, filters));
80
+ const results = [];
81
+ for (const testCase of cases) {
82
+ for (const evalContext of contexts) {
83
+ const expectation = testCase.expectations?.[evalContext.id];
84
+ const observeOnly = expectation === undefined;
85
+ const actual = await classifyFn(testCase.command, evalContext);
86
+ const match = observeOnly || expectation === undefined ? null : matchesExpectation(expectation, actual);
87
+ results.push({
88
+ caseId: testCase.id,
89
+ groupId: testCase.groupId,
90
+ command: testCase.command,
91
+ commandHash: hashStableJson(testCase.command),
92
+ context: evalContext.id,
93
+ tags: testCase.tags,
94
+ observeOnly,
95
+ expectation,
96
+ expectationHash: expectation ? hashStableJson(expectation) : null,
97
+ actual: {
98
+ verdict: actual.verdict,
99
+ reason: actual.reason,
100
+ fingerprint: actual.fingerprint,
101
+ },
102
+ match,
103
+ });
104
+ }
105
+ }
106
+ return results;
107
+ }
108
+ function summarizeResults(results, filters) {
109
+ const summary = {
110
+ total: results.length,
111
+ observeOnly: 0,
112
+ matched: 0,
113
+ mismatched: 0,
114
+ filteredEmpty: filters.length > 0 && results.length === 0,
115
+ byGroup: {},
116
+ byTag: {},
117
+ };
118
+ for (const result of results) {
119
+ if (result.observeOnly) {
120
+ summary.observeOnly += 1;
121
+ }
122
+ else if (result.match) {
123
+ summary.matched += 1;
124
+ }
125
+ else {
126
+ summary.mismatched += 1;
127
+ }
128
+ if (!summary.byGroup[result.groupId]) {
129
+ summary.byGroup[result.groupId] = initBucket();
130
+ }
131
+ recordBucket(summary.byGroup[result.groupId], result);
132
+ for (const tag of result.tags) {
133
+ if (!summary.byTag[tag]) {
134
+ summary.byTag[tag] = initBucket();
135
+ }
136
+ recordBucket(summary.byTag[tag], result);
137
+ }
138
+ }
139
+ return summary;
140
+ }
141
+ function resultsSignature(results) {
142
+ return hashStableJson(results.map((result) => ({
143
+ caseId: result.caseId,
144
+ context: result.context,
145
+ verdict: result.actual.verdict,
146
+ reason: result.actual.reason,
147
+ fingerprint: result.actual.fingerprint,
148
+ })));
149
+ }
150
+ export async function runCoverageProbe(options = {}) {
151
+ const repoRoot = path.resolve(options.repoRoot ?? defaultRepoRoot());
152
+ const matrixPath = path.resolve(options.matrixPath ?? defaultCoverageMatrixPath(repoRoot));
153
+ const matrix = await loadCoverageMatrix(matrixPath);
154
+ const contextIds = options.contextIds ?? [...DEFAULT_PROBE_CONTEXT_IDS];
155
+ const contexts = await buildCoverageEvalContexts(contextIds, repoRoot);
156
+ const repeat = Math.max(1, options.repeat ?? 1);
157
+ const filters = options.filters ?? [];
158
+ const probeOptions = { ...options, evalContexts: contexts };
159
+ let results = await evaluateCoverageMatrix(matrix, probeOptions);
160
+ let driftRuns = 0;
161
+ const firstResults = results;
162
+ const firstSignature = resultsSignature(results);
163
+ for (let run = 2; run <= repeat; run += 1) {
164
+ const nextResults = await evaluateCoverageMatrix(matrix, probeOptions);
165
+ if (resultsSignature(nextResults) !== firstSignature) {
166
+ driftRuns += 1;
167
+ }
168
+ }
169
+ results = firstResults;
170
+ const mismatches = results.filter((result) => !result.observeOnly && result.match === false);
171
+ return {
172
+ reportSchemaVersion: COVERAGE_PROBE_REPORT_SCHEMA_VERSION,
173
+ generatedAt: new Date().toISOString(),
174
+ packageVersion: await readPackageVersion(repoRoot),
175
+ gitSha: readGitSha(),
176
+ matrixPath,
177
+ matrixHash: hashStableJson(matrix),
178
+ repeat,
179
+ driftRuns,
180
+ contexts: contexts.map((evalContext) => ({
181
+ id: evalContext.id,
182
+ cwd: evalContext.cwd,
183
+ repoRoot: evalContext.repoRoot,
184
+ resolvedConfigHash: resolvedConfigHash(evalContext),
185
+ configProvenance: evalContext.configProvenance,
186
+ })),
187
+ summary: summarizeResults(results, filters),
188
+ results,
189
+ mismatches,
190
+ };
191
+ }
192
+ export function formatCoverageProbeSummary(report) {
193
+ const lines = [
194
+ 'Coverage probe',
195
+ ` cases: ${report.summary.total} (observe-only: ${report.summary.observeOnly})`,
196
+ ` matched: ${report.summary.matched}`,
197
+ ` mismatched: ${report.summary.mismatched}`,
198
+ ` repeat: ${report.repeat} (drift runs: ${report.driftRuns})`,
199
+ ' contexts:',
200
+ ];
201
+ for (const evalContext of report.contexts) {
202
+ lines.push(` ${evalContext.id}: cwd=${evalContext.cwd} configHash=${evalContext.resolvedConfigHash.slice(0, 12)}…`);
203
+ }
204
+ if (report.mismatches.length > 0) {
205
+ lines.push(' mismatches:');
206
+ for (const mismatch of report.mismatches.slice(0, 15)) {
207
+ lines.push(` - [${mismatch.context}] ${mismatch.caseId}: expected=${JSON.stringify(mismatch.expectation)} actual=${mismatch.actual.verdict}/${mismatch.actual.reason}`);
208
+ }
209
+ if (report.mismatches.length > 15) {
210
+ lines.push(` … and ${report.mismatches.length - 15} more`);
211
+ }
212
+ }
213
+ if (report.summary.filteredEmpty) {
214
+ lines.push(' warning: filter matched zero cases');
215
+ }
216
+ return lines.join('\n');
217
+ }
218
+ export function parseCoverageProbeCliArgs(argv) {
219
+ let repoRoot;
220
+ let matrixPath;
221
+ let contextIds;
222
+ let filters = [];
223
+ let repeat = 1;
224
+ let strict = false;
225
+ let outputDir;
226
+ let comparePath;
227
+ let json = false;
228
+ for (let i = 0; i < argv.length; i += 1) {
229
+ const arg = argv[i];
230
+ if (arg === '--repo-root' && argv[i + 1]) {
231
+ repoRoot = argv[++i];
232
+ }
233
+ else if (arg === '--matrix' && argv[i + 1]) {
234
+ matrixPath = argv[++i];
235
+ }
236
+ else if (arg === '--context' && i + 1 < argv.length) {
237
+ const rawContexts = argv[++i]
238
+ .split(',')
239
+ .map((value) => value.trim())
240
+ .filter(Boolean);
241
+ contextIds = assertKnownContextIds(rawContexts);
242
+ }
243
+ else if (arg === '--filter' && i + 1 < argv.length) {
244
+ filters = argv[++i]
245
+ .split(',')
246
+ .map((value) => value.trim())
247
+ .filter(Boolean);
248
+ if (filters.length === 0) {
249
+ throw new CoverageProbeCliError('--filter requires at least one tag, group id, or case id');
250
+ }
251
+ }
252
+ else if (arg === '--repeat' && argv[i + 1]) {
253
+ repeat = Number.parseInt(argv[++i], 10);
254
+ if (!Number.isFinite(repeat) || repeat < 1) {
255
+ throw new CoverageProbeCliError('--repeat must be a positive integer');
256
+ }
257
+ }
258
+ else if (arg === '--output-dir' && argv[i + 1]) {
259
+ outputDir = argv[++i];
260
+ }
261
+ else if (arg === '--compare' && argv[i + 1]) {
262
+ comparePath = argv[++i];
263
+ }
264
+ else if (arg === '--strict') {
265
+ strict = true;
266
+ }
267
+ else if (arg === '--json') {
268
+ json = true;
269
+ }
270
+ }
271
+ return { repoRoot, matrixPath, contextIds, filters, repeat, strict, outputDir, comparePath, json };
272
+ }
273
+ export async function main(argv = process.argv.slice(2)) {
274
+ try {
275
+ const parsed = parseCoverageProbeCliArgs(argv);
276
+ const report = await runCoverageProbe({
277
+ repoRoot: parsed.repoRoot,
278
+ matrixPath: parsed.matrixPath,
279
+ contextIds: parsed.contextIds,
280
+ filters: parsed.filters,
281
+ repeat: parsed.repeat,
282
+ outputDir: parsed.outputDir,
283
+ json: parsed.json,
284
+ });
285
+ if (parsed.json) {
286
+ console.log(JSON.stringify(report, null, 2));
287
+ }
288
+ else {
289
+ console.log(formatCoverageProbeSummary(report));
290
+ }
291
+ if (parsed.outputDir) {
292
+ await mkdir(parsed.outputDir, { recursive: true });
293
+ const stamp = report.generatedAt.replace(/[:.]/g, '-');
294
+ const outputPath = path.join(parsed.outputDir, `run-${stamp}.json`);
295
+ await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
296
+ if (!parsed.json) {
297
+ console.log(`\nWrote ${outputPath}`);
298
+ }
299
+ }
300
+ if (parsed.comparePath) {
301
+ let baselineRaw;
302
+ try {
303
+ baselineRaw = JSON.parse(await readFile(path.resolve(parsed.comparePath), 'utf8'));
304
+ }
305
+ catch {
306
+ throw new CoverageCompareError(`--compare baseline is not valid JSON: ${parsed.comparePath}`);
307
+ }
308
+ const baseline = parseCoverageProbeReportForCompare(baselineRaw);
309
+ const compareReport = compareCoverageReports(baseline, report);
310
+ for (const warning of compareBaselineWarnings(baseline, report)) {
311
+ console.warn(`\nWARNING: compare baseline mismatch: ${warning}`);
312
+ }
313
+ if (!parsed.json) {
314
+ console.log(`\n${formatCoverageCompareReport(compareReport)}`);
315
+ }
316
+ if (compareReport.configDrift.length > 0) {
317
+ console.warn(`\nWARNING: config drift detected for ${compareReport.configDrift.length} context(s); compare may include classifier drift from config changes`);
318
+ }
319
+ }
320
+ if (report.driftRuns > 0 && !parsed.json) {
321
+ console.warn(`\nWARNING: classifier drift detected in ${report.driftRuns}/${report.repeat} repeats; report uses first run results`);
322
+ }
323
+ if (report.summary.filteredEmpty) {
324
+ console.warn('\nWARNING: --filter matched zero cases; check tag/group/case ids');
325
+ return 2;
326
+ }
327
+ if (parsed.strict && report.summary.mismatched > 0) {
328
+ return 1;
329
+ }
330
+ return 0;
331
+ }
332
+ catch (error) {
333
+ if (error instanceof CoverageProbeCliError ||
334
+ error instanceof CoverageMatrixSchemaError ||
335
+ error instanceof CoverageCompareError) {
336
+ console.error(error.message);
337
+ return 1;
338
+ }
339
+ throw error;
340
+ }
341
+ }
@@ -49,7 +49,7 @@ export interface FpCommandBatchResult {
49
49
  mutatorId: string;
50
50
  isFp: boolean;
51
51
  }
52
- /** False-positive probe: must-allow cores that receive deny_pending_approval count as FP. */
52
+ /** False-positive probe: benign probe cores that receive deny_pending_approval count as FP. */
53
53
  export declare function evaluateFpCommandBatch(cases: Array<{
54
54
  command: string;
55
55
  core: string;
@@ -102,7 +102,7 @@ export async function evaluateCommandBatch(cases, repoRoot = defaultCorpusEvalPa
102
102
  }
103
103
  return results;
104
104
  }
105
- /** False-positive probe: must-allow cores that receive deny_pending_approval count as FP. */
105
+ /** False-positive probe: benign probe cores that receive deny_pending_approval count as FP. */
106
106
  export async function evaluateFpCommandBatch(cases, repoRoot = defaultCorpusEvalPaths().repoRoot) {
107
107
  const { cwd } = defaultCorpusEvalPaths(repoRoot);
108
108
  const options = classifierOptionsFromConfig(DEFAULT_CONFIG_V3);
@@ -0,0 +1,2 @@
1
+ /** Resolve structural suite fixture root for a repository checkout. */
2
+ export declare function structuralFixtureRoot(repoRoot: string): string;
@@ -0,0 +1,5 @@
1
+ import path from 'node:path';
2
+ /** Resolve structural suite fixture root for a repository checkout. */
3
+ export function structuralFixtureRoot(repoRoot) {
4
+ return path.join(repoRoot, 'src', '__tests__', 'verdict', 'fixtures');
5
+ }
@@ -7,6 +7,9 @@ export interface DogfoodStatus {
7
7
  gateEvents: number;
8
8
  wouldBlockCount: number;
9
9
  wouldBlockRate: number;
10
+ excludedGateEvents: number;
11
+ runtimeBuildStamp?: string;
12
+ configFingerprint?: string;
10
13
  notes: string[];
11
14
  }
12
15
  export interface OperationalInsights {
@@ -8,6 +8,7 @@ export async function loadOperationalInsights(options = {}) {
8
8
  const repoRoot = path.resolve(options.targetDir ?? process.cwd());
9
9
  const config = await loadConfigFile(repoRoot);
10
10
  const metrics = await metricsProject({ targetDir: repoRoot });
11
+ const cohort = metrics.currentCohort;
11
12
  return {
12
13
  repoRoot,
13
14
  dogfood: {
@@ -15,9 +16,12 @@ export async function loadOperationalInsights(options = {}) {
15
16
  mode: config.mode,
16
17
  unknownLocalEffect: config.policy.unknownLocalEffect,
17
18
  readyForEnforce: metrics.dogfood.readyForEnforce,
18
- gateEvents: metrics.gateEvents,
19
- wouldBlockCount: metrics.wouldBlockCount,
20
- wouldBlockRate: metrics.wouldBlockRate,
19
+ gateEvents: cohort.gateEvents,
20
+ wouldBlockCount: cohort.wouldBlockCount,
21
+ wouldBlockRate: cohort.wouldBlockRate,
22
+ excludedGateEvents: cohort.excludedGateEvents,
23
+ runtimeBuildStamp: cohort.identity?.runtimeBuildStamp,
24
+ configFingerprint: cohort.identity?.configFingerprint,
21
25
  notes: metrics.dogfood.notes,
22
26
  },
23
27
  };
@@ -0,0 +1,8 @@
1
+ import type { AuditCohortIdentity } from './core/audit-metrics.js';
2
+ import type { BelayConfigV3 } from './core/config.js';
3
+ export interface InstalledRuntimeProvenance {
4
+ stamp?: string;
5
+ version?: string;
6
+ }
7
+ export declare function readInstalledRuntimeProvenance(corePath: string): Promise<InstalledRuntimeProvenance>;
8
+ export declare function resolveActiveAuditCohort(repoRoot: string, config: BelayConfigV3): Promise<AuditCohortIdentity | null>;
@@ -0,0 +1,34 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getAdapterLayout } from './adapters/layouts/index.js';
4
+ import { resolveScopedPaths } from './adapters/layouts/scope.js';
5
+ import { resolveAdapterName } from './config-io.js';
6
+ import { canonicalStringify, hashValue } from './core/fingerprint.js';
7
+ export async function readInstalledRuntimeProvenance(corePath) {
8
+ try {
9
+ const content = await readFile(corePath, 'utf8');
10
+ const stampMatch = content.match(/RUNTIME_BUILD_STAMP\s*=\s*"([^"]+)"/);
11
+ const versionMatch = content.match(/RUNTIME_PACKAGE_VERSION\s*=\s*"([^"]+)"/);
12
+ return {
13
+ stamp: stampMatch?.[1],
14
+ version: versionMatch?.[1],
15
+ };
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ }
21
+ export async function resolveActiveAuditCohort(repoRoot, config) {
22
+ const adapter = resolveAdapterName(config);
23
+ const layout = getAdapterLayout(adapter);
24
+ const installScope = config.installScope === 'global' ? 'global' : 'project';
25
+ const scopedPaths = resolveScopedPaths(layout, installScope, repoRoot);
26
+ const runtime = await readInstalledRuntimeProvenance(path.join(scopedPaths.runtimeDir, 'core.mjs'));
27
+ if (!runtime.stamp) {
28
+ return null;
29
+ }
30
+ return {
31
+ runtimeBuildStamp: runtime.stamp,
32
+ configFingerprint: hashValue(canonicalStringify(config)),
33
+ };
34
+ }
package/dist/types.d.ts CHANGED
@@ -56,6 +56,9 @@ export interface DogfoodStatus {
56
56
  gateEvents: number;
57
57
  wouldBlockCount: number;
58
58
  wouldBlockRate: number;
59
+ excludedGateEvents: number;
60
+ runtimeBuildStamp?: string;
61
+ configFingerprint?: string;
59
62
  notes: string[];
60
63
  }
61
64
  export interface ConfigProvenanceNote {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "0.8.0";
1
+ export declare const PACKAGE_VERSION = "0.9.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/sync-version.mjs — do not edit.
2
- export const PACKAGE_VERSION = '0.8.0';
2
+ export const PACKAGE_VERSION = '0.9.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guilz-dev/belay",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Belay-style approval and audit gating for agent runtimes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -60,6 +60,8 @@
60
60
  "test:stable": "pnpm build && vitest run && vitest run && vitest run",
61
61
  "corpus": "pnpm build && node scripts/corpus.mjs",
62
62
  "probe:adversarial": "pnpm build && node scripts/adversarial-probe.mjs",
63
+ "probe:coverage": "pnpm build && node scripts/coverage-probe.mjs",
64
+ "probe:coverage:repeat": "pnpm build && node scripts/coverage-probe.mjs --repeat 3",
63
65
  "corpus:ratchet": "pnpm build && node scripts/corpus-ratchet.mjs"
64
66
  },
65
67
  "devDependencies": {
@@ -53,10 +53,28 @@ To restore the legacy two-step UX (approve, then always retry manually), set
53
53
 
54
54
  For why it was blocked, use `/belay why <command>` or `belay explain --command "<command>"`.
55
55
  For the latest pending ask, use `/belay explain` or `belay explain`.
56
+
57
+ **Do not use command allowlists** (`overrides.allow`) or legacy standing-allow records
58
+ (shell, tool, or subagent) to fix blocks; none of them change runtime authorization.
59
+ Improve EffectPlan semantics, approve once with `/belay-approve`, or use an exact
60
+ resource-scoped grant. See ADR-005 in the belay repository.
56
61
  For install health and audit visibility, use `/belay status` or `belay status`.
57
62
  For audit-only summary, use `/belay report` or `belay report`.
58
63
  For recovery advice after a block, use `/belay recover` or `belay recover`.
59
64
 
65
+ ## Contained unknown execution
66
+
67
+ When an operator has explicitly enabled Docker contained execution, an eligible repository-local
68
+ `unknown_local_effect` may run once in a disposable mirror instead of using a command allowlist.
69
+ The route has no network, never mounts the source workspace or control plane, discards all guest
70
+ changes, and always applies mandatory credential scrubbing to its 16 KiB stdout/stderr tails.
71
+
72
+ If Belay reports that the contained capability, image, or Docker substrate no longer matches, ask
73
+ the operator to run `belay session start`; do not silently run it from this skill. Only typed Docker
74
+ substrate/daemon unavailability returns to the normal one-shot approval flow. Missing or stale
75
+ capability/image, mirror/lease, create/inspect/start, timeout, and cleanup failures deny without
76
+ approval or host replay.
77
+
60
78
  ## Install or repair
61
79
 
62
80
  - Interactive setup: `belay config`
@@ -75,4 +93,5 @@ Do not run init, config, or doctor implicitly from this skill — only when the
75
93
  | Status / dogfood / audit visibility | `belay status` |
76
94
  | Audit summary (read-only) | `belay report` |
77
95
  | Recovery advice (advisory only) | `belay recover` |
96
+ | Refresh contained Docker attestation | `belay session start` |
78
97
  | Approve once | `belay approve <id>` |
@@ -6,4 +6,9 @@ belay status
6
6
 
7
7
  For audit-only summary: `belay report`. For recovery advice: `belay recover`.
8
8
 
9
+ For opt-in contained execution, report the configured and attested Docker capability separately
10
+ from L1-full. If the image, executable, Unix socket, daemon, or limits no longer match, suggest
11
+ `belay session start`. Do not run it implicitly. Contained guest output is always credential-
12
+ scrubbed and capped even when ordinary audit redaction switches are disabled.
13
+
9
14
  If hooks are missing, suggest `npx @guilz-dev/belay init` and `belay doctor`.
@@ -1,6 +0,0 @@
1
- /**
2
- * Structural-suite MUST-ALLOW CI expectations.
3
- * These commands guard classifier availability in tests and never grant runtime authority.
4
- * @see src/__tests__/verdict/structural-suite.test.ts
5
- */
6
- export declare const MUST_ALLOW_SHELL_COMMANDS: readonly ["npm test", "npm run build", "pnpm test", "pnpm build", "pnpm vitest run src/example.test.ts", "bash -lc 'git status'", "belay approve belay_deadbeef1234"];