@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,121 @@
1
+ import { CONTAINED_EXECUTION_APPROVAL_FALLBACK_REASONS } from '../core/contained-execution/policy.js';
2
+ /**
3
+ * Normative contained-execution capability contract.
4
+ *
5
+ * This is intentionally not a LayerProfileId: it describes one opt-in mediated execution and
6
+ * never upgrades the surrounding configuration to L1-full.
7
+ */
8
+ export interface ContainedUnknownExecutionGuarantee {
9
+ id: 'contained-unknown-execution-v1';
10
+ optIn: true;
11
+ runtime: 'docker-only';
12
+ l1Full: false;
13
+ materializesGrants: false;
14
+ deniesUngrantedEffects: false;
15
+ authority: {
16
+ shell: 'effect-plan-only';
17
+ commandIdentityEligibility: false;
18
+ forbiddenInputs: readonly [
19
+ 'executable',
20
+ 'prefix',
21
+ 'fingerprint',
22
+ 'corpus-membership',
23
+ 'framework-identity'
24
+ ];
25
+ };
26
+ audit: {
27
+ wouldMediate: true;
28
+ containedRouteExecution: 'none';
29
+ readsAttestation: false;
30
+ preparesMirror: false;
31
+ startsContainer: false;
32
+ gatePermission: 'allow';
33
+ hostExecution: 'delegated-to-host';
34
+ };
35
+ enforce: {
36
+ originalHostCommand: 'deny';
37
+ mirror: 'file_copy';
38
+ startsAtMostOnce: true;
39
+ workspaceChanges: 'discard';
40
+ output: {
41
+ scrub: 'mandatory';
42
+ tailBytes: 16384;
43
+ userRedactionCanDisable: false;
44
+ };
45
+ audit: 'safe-metadata-only';
46
+ };
47
+ attestation: {
48
+ signed: true;
49
+ fresh: true;
50
+ immutableImageId: true;
51
+ configuredDockerBinary: 'absolute-bound';
52
+ configuredDockerSocket: 'local-unix-bound';
53
+ daemonIdentity: true;
54
+ configurationBound: true;
55
+ };
56
+ boundary: {
57
+ network: 'none';
58
+ readOnlyRoot: true;
59
+ sanitizedHostEnvironment: true;
60
+ resourceLimits: true;
61
+ logDriver: 'none';
62
+ mount: 'one-private-mirror-at-original-guest-path';
63
+ excluded: readonly [
64
+ 'host-source',
65
+ 'git-metadata',
66
+ 'control-plane',
67
+ 'docker-socket',
68
+ 'devices',
69
+ 'unrelated-host-paths'
70
+ ];
71
+ cleanupConfirmed: true;
72
+ };
73
+ fallback: {
74
+ approvalOnly: typeof CONTAINED_EXECUTION_APPROVAL_FALLBACK_REASONS;
75
+ };
76
+ failure: {
77
+ /** Every setup failure other than the exact approval fallback set denies before host replay. */
78
+ setup: {
79
+ categories: readonly [
80
+ 'boundary',
81
+ 'capability',
82
+ 'image',
83
+ 'mirror',
84
+ 'lease',
85
+ 'container-lifecycle',
86
+ 'cleanup'
87
+ ];
88
+ outcome: 'deny';
89
+ hostExecution: 'deny';
90
+ approval: 'none';
91
+ approvalStateMutation: 'none';
92
+ };
93
+ /** A started container that times out or exits nonzero is terminal, never an approval. */
94
+ command: {
95
+ timeout: 'contained_execution_failed';
96
+ nonzero: 'contained_execution_failed';
97
+ hostExecution: 'deny';
98
+ approval: 'none';
99
+ };
100
+ };
101
+ outcomes: {
102
+ success: {
103
+ exitCode: 0;
104
+ outcome: 'contained_execution_complete';
105
+ hostExecution: 'deny';
106
+ approval: 'none';
107
+ };
108
+ };
109
+ }
110
+ export type ContainedUnknownExecutionScenario = {
111
+ id: string;
112
+ kind: 'eligible-unknown-local';
113
+ command: string;
114
+ } | {
115
+ id: string;
116
+ kind: 'ineligible-network';
117
+ command: string;
118
+ };
119
+ export declare const CONTAINED_UNKNOWN_EXECUTION_GUARANTEE: ContainedUnknownExecutionGuarantee;
120
+ /** Executable EffectPlan scenarios; routing and output use the gate/executor suites. */
121
+ export declare const CONTAINED_UNKNOWN_EXECUTION_SCENARIOS: readonly ContainedUnknownExecutionScenario[];
@@ -0,0 +1,121 @@
1
+ import { OUTPUT_TAIL_LIMIT_BYTES } from '../core/bounded-output.js';
2
+ import { CONTAINED_EXECUTION_APPROVAL_FALLBACK_REASONS } from '../core/contained-execution/policy.js';
3
+ export const CONTAINED_UNKNOWN_EXECUTION_GUARANTEE = {
4
+ id: 'contained-unknown-execution-v1',
5
+ optIn: true,
6
+ runtime: 'docker-only',
7
+ l1Full: false,
8
+ materializesGrants: false,
9
+ deniesUngrantedEffects: false,
10
+ authority: {
11
+ shell: 'effect-plan-only',
12
+ commandIdentityEligibility: false,
13
+ forbiddenInputs: [
14
+ 'executable',
15
+ 'prefix',
16
+ 'fingerprint',
17
+ 'corpus-membership',
18
+ 'framework-identity',
19
+ ],
20
+ },
21
+ audit: {
22
+ wouldMediate: true,
23
+ containedRouteExecution: 'none',
24
+ readsAttestation: false,
25
+ preparesMirror: false,
26
+ startsContainer: false,
27
+ gatePermission: 'allow',
28
+ hostExecution: 'delegated-to-host',
29
+ },
30
+ enforce: {
31
+ originalHostCommand: 'deny',
32
+ mirror: 'file_copy',
33
+ startsAtMostOnce: true,
34
+ workspaceChanges: 'discard',
35
+ output: {
36
+ scrub: 'mandatory',
37
+ tailBytes: OUTPUT_TAIL_LIMIT_BYTES,
38
+ userRedactionCanDisable: false,
39
+ },
40
+ audit: 'safe-metadata-only',
41
+ },
42
+ attestation: {
43
+ signed: true,
44
+ fresh: true,
45
+ immutableImageId: true,
46
+ configuredDockerBinary: 'absolute-bound',
47
+ configuredDockerSocket: 'local-unix-bound',
48
+ daemonIdentity: true,
49
+ configurationBound: true,
50
+ },
51
+ boundary: {
52
+ network: 'none',
53
+ readOnlyRoot: true,
54
+ sanitizedHostEnvironment: true,
55
+ resourceLimits: true,
56
+ logDriver: 'none',
57
+ mount: 'one-private-mirror-at-original-guest-path',
58
+ excluded: [
59
+ 'host-source',
60
+ 'git-metadata',
61
+ 'control-plane',
62
+ 'docker-socket',
63
+ 'devices',
64
+ 'unrelated-host-paths',
65
+ ],
66
+ cleanupConfirmed: true,
67
+ },
68
+ fallback: {
69
+ approvalOnly: CONTAINED_EXECUTION_APPROVAL_FALLBACK_REASONS,
70
+ },
71
+ failure: {
72
+ setup: {
73
+ categories: [
74
+ 'boundary',
75
+ 'capability',
76
+ 'image',
77
+ 'mirror',
78
+ 'lease',
79
+ 'container-lifecycle',
80
+ 'cleanup',
81
+ ],
82
+ outcome: 'deny',
83
+ hostExecution: 'deny',
84
+ approval: 'none',
85
+ approvalStateMutation: 'none',
86
+ },
87
+ command: {
88
+ timeout: 'contained_execution_failed',
89
+ nonzero: 'contained_execution_failed',
90
+ hostExecution: 'deny',
91
+ approval: 'none',
92
+ },
93
+ },
94
+ outcomes: {
95
+ success: {
96
+ exitCode: 0,
97
+ outcome: 'contained_execution_complete',
98
+ hostExecution: 'deny',
99
+ approval: 'none',
100
+ },
101
+ },
102
+ };
103
+ /** Executable EffectPlan scenarios; routing and output use the gate/executor suites. */
104
+ export const CONTAINED_UNKNOWN_EXECUTION_SCENARIOS = [
105
+ {
106
+ id: 'cue-eligible-fictional',
107
+ kind: 'eligible-unknown-local',
108
+ command: 'fictional-runner verify',
109
+ },
110
+ {
111
+ id: 'cue-eligible-rails',
112
+ kind: 'eligible-unknown-local',
113
+ command: "bin/rails runner 'Record.count'",
114
+ },
115
+ {
116
+ id: 'cue-eligible-rspec',
117
+ kind: 'eligible-unknown-local',
118
+ command: 'bundle exec rspec --dry-run',
119
+ },
120
+ { id: 'cue-ineligible-network', kind: 'ineligible-network', command: 'curl https://example.com' },
121
+ ];
@@ -160,6 +160,13 @@ export const GUARANTEE_SCENARIOS = {
160
160
  hookVerdict: 'allow_flagged',
161
161
  substrate: 'dirty_git_file_checkpoint',
162
162
  },
163
+ {
164
+ id: 'l2-allow-flagged-non-git-file-checkpoint',
165
+ command: 'touch notes.txt',
166
+ permission: 'allow',
167
+ hookVerdict: 'allow_flagged',
168
+ substrate: 'non_git_file_checkpoint',
169
+ },
163
170
  {
164
171
  id: 'l2-deny-payload-send',
165
172
  command: 'curl -X POST -d payload https://example.com',
@@ -1,13 +1,21 @@
1
1
  import { appendFile, mkdir } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { resolveActiveAuditCohort } from '../runtime-provenance.js';
3
4
  export async function appendCliAuditEvent(repoRoot, config, event) {
4
5
  const auditPath = path.isAbsolute(config.audit.logPath)
5
6
  ? config.audit.logPath
6
7
  : path.join(repoRoot, config.audit.logPath);
7
8
  await mkdir(path.dirname(auditPath), { recursive: true });
9
+ const cohort = await resolveActiveAuditCohort(repoRoot, config);
8
10
  const line = JSON.stringify({
9
11
  ts: new Date().toISOString(),
10
12
  source: 'belay-cli',
13
+ ...(cohort
14
+ ? {
15
+ runtimeBuildStamp: cohort.runtimeBuildStamp,
16
+ configFingerprint: cohort.configFingerprint,
17
+ }
18
+ : {}),
11
19
  ...event,
12
20
  });
13
21
  await appendFile(auditPath, `${line}\n`, 'utf8');
@@ -1,7 +1,36 @@
1
1
  import { buildApprovalRoundTrips, filterAuditRecords, toAuditRecord } from './audit-query.js';
2
+ import { type RecoveryMetrics, type RecoveryMetricsCohort } from './audit-recovery-metrics.js';
2
3
  import type { AvailabilityAskCounts, ReasonApprovalRatio, RepeatedFingerprintAsk } from './audit-types.js';
3
4
  /** Minimum gate events before recommending enforce with zero would-block rate. */
4
5
  export declare const MIN_GATE_EVENTS_FOR_ENFORCE = 20;
6
+ export interface AuditCohortIdentity {
7
+ runtimeBuildStamp: string;
8
+ configFingerprint: string;
9
+ }
10
+ export interface AuditMetricsCohort {
11
+ identity: AuditCohortIdentity | null;
12
+ gateEvents: number;
13
+ excludedGateEvents: number;
14
+ wouldBlockCount: number;
15
+ wouldBlockRate: number;
16
+ classifierWouldBlockCount: number;
17
+ classifierWouldBlockRate: number;
18
+ approvalRecordedCount: number;
19
+ availabilityAsks: AvailabilityAskCounts;
20
+ wouldBlockByReason: Record<string, number>;
21
+ topWouldBlockSummaries: Array<{
22
+ summary: string;
23
+ reason: string;
24
+ count: number;
25
+ }>;
26
+ containedExecution: ContainedExecutionMetrics;
27
+ }
28
+ export interface ContainedExecutionMetrics {
29
+ wouldMediate: number;
30
+ complete: number;
31
+ failed: number;
32
+ timedOut: number;
33
+ }
5
34
  export interface AuditMetricsReport {
6
35
  schemaVersion: number;
7
36
  auditLogPath: string;
@@ -24,12 +53,14 @@ export interface AuditMetricsReport {
24
53
  byEffect: Record<string, number>;
25
54
  byConfidence: Record<string, number>;
26
55
  gateEventsByRuntime: Record<string, number>;
56
+ currentCohort: AuditMetricsCohort;
27
57
  approvalRecordedCount: number;
28
58
  topWouldBlockSummaries: Array<{
29
59
  summary: string;
30
60
  reason: string;
31
61
  count: number;
32
62
  }>;
63
+ containedExecution: ContainedExecutionMetrics;
33
64
  approvalLatency: {
34
65
  count: number;
35
66
  medianMs: number | null;
@@ -49,11 +80,14 @@ export interface AuditMetricsReport {
49
80
  readyForEnforce: boolean;
50
81
  notes: string[];
51
82
  };
83
+ recovery: RecoveryMetrics;
84
+ currentCohortRecovery: RecoveryMetricsCohort;
52
85
  }
53
86
  export declare function parseAuditNdjson(raw: string): Record<string, unknown>[];
54
87
  export declare function computeAuditMetrics(records: Record<string, unknown>[], options?: {
55
88
  auditLogPath?: string;
56
89
  mode?: string;
57
90
  unknownLocalEffect?: string;
91
+ activeCohort?: AuditCohortIdentity | null;
58
92
  }): AuditMetricsReport;
59
93
  export { buildApprovalRoundTrips, filterAuditRecords, toAuditRecord };
@@ -1,8 +1,17 @@
1
1
  import { bucketGateEventsByDay, computeApprovalLatencyStats, computeApprovalRatioByReason, computeAvailabilityAskCounts, computeRepeatedFingerprintAsks, computeWouldBlockByReason, countVerdicts, detectBypassAttempts, detectNoisyRules, } from './audit-analysis.js';
2
2
  import { buildApprovalRoundTrips, filterAuditRecords, inferWouldBlock, isApprovalRecorded, toAuditRecord, } from './audit-query.js';
3
+ import { computeRecoveryMetrics, } from './audit-recovery-metrics.js';
3
4
  import { AUDIT_METRICS_SCHEMA_VERSION, GATE_EVENTS } from './audit-types.js';
4
5
  /** Minimum gate events before recommending enforce with zero would-block rate. */
5
6
  export const MIN_GATE_EVENTS_FOR_ENFORCE = 20;
7
+ function containedExecutionMetrics(records) {
8
+ return {
9
+ wouldMediate: records.filter((record) => record.wouldMediate === true).length,
10
+ complete: records.filter((record) => record.reason === 'contained_execution_complete').length,
11
+ failed: records.filter((record) => record.reason === 'contained_execution_failed').length,
12
+ timedOut: records.filter((record) => record.reason === 'contained_execution_failed' && record.timedOut === true).length,
13
+ };
14
+ }
6
15
  export function parseAuditNdjson(raw) {
7
16
  const records = [];
8
17
  for (const line of raw.split('\n')) {
@@ -95,33 +104,91 @@ export function computeAuditMetrics(records, options = {}) {
95
104
  const topWouldBlockSummaries = [...summaryCounts.values()]
96
105
  .sort((left, right) => right.count - left.count)
97
106
  .slice(0, 10);
107
+ const activeCohort = options.activeCohort ?? null;
108
+ const cohortRecords = activeCohort
109
+ ? auditRecords.filter((record) => record.runtimeBuildStamp === activeCohort.runtimeBuildStamp &&
110
+ record.configFingerprint === activeCohort.configFingerprint)
111
+ : [];
112
+ const cohortGateRecords = cohortRecords.filter((record) => {
113
+ const event = typeof record.event === 'string' ? record.event : '';
114
+ return GATE_EVENTS.has(event) && !isApprovalRecorded(record);
115
+ });
116
+ const cohortGateEvents = cohortGateRecords.length;
117
+ const cohortWouldBlockCount = cohortGateRecords.filter(inferWouldBlock).length;
118
+ const cohortWouldBlockRate = cohortGateEvents > 0 ? cohortWouldBlockCount / cohortGateEvents : 0;
119
+ const cohortApprovalRecordedCount = cohortRecords.filter(isApprovalRecorded).length;
120
+ const cohortAvailabilityAsks = computeAvailabilityAskCounts(cohortRecords);
121
+ const cohortClassifierWouldBlockCount = Math.max(0, cohortWouldBlockCount - cohortAvailabilityAsks.total);
122
+ const cohortClassifierWouldBlockRate = cohortGateEvents > 0 ? cohortClassifierWouldBlockCount / cohortGateEvents : 0;
123
+ const cohortRoundTrips = buildApprovalRoundTrips(cohortRecords);
124
+ const cohortRepeatedFingerprintAsks = computeRepeatedFingerprintAsks(cohortRecords);
125
+ const cohortNoisyRuleCandidates = detectNoisyRules(cohortRecords, cohortRoundTrips);
126
+ const cohortWouldBlockByReason = computeWouldBlockByReason(cohortRecords);
127
+ const cohortSummaryCounts = new Map();
128
+ for (const record of cohortGateRecords) {
129
+ if (!inferWouldBlock(record)) {
130
+ continue;
131
+ }
132
+ const reason = typeof record.reason === 'string' ? record.reason : 'unknown';
133
+ const summary = typeof record.summary === 'string' ? record.summary : '';
134
+ const key = `${reason}::${summary}`;
135
+ const existing = cohortSummaryCounts.get(key);
136
+ if (existing) {
137
+ existing.count += 1;
138
+ }
139
+ else {
140
+ cohortSummaryCounts.set(key, { summary, reason, count: 1 });
141
+ }
142
+ }
143
+ const cohortTopWouldBlockSummaries = [...cohortSummaryCounts.values()]
144
+ .sort((left, right) => right.count - left.count)
145
+ .slice(0, 10);
146
+ const currentCohort = {
147
+ identity: activeCohort,
148
+ gateEvents: cohortGateEvents,
149
+ excludedGateEvents: gateEvents - cohortGateEvents,
150
+ wouldBlockCount: cohortWouldBlockCount,
151
+ wouldBlockRate: cohortWouldBlockRate,
152
+ classifierWouldBlockCount: cohortClassifierWouldBlockCount,
153
+ classifierWouldBlockRate: cohortClassifierWouldBlockRate,
154
+ approvalRecordedCount: cohortApprovalRecordedCount,
155
+ availabilityAsks: cohortAvailabilityAsks,
156
+ wouldBlockByReason: cohortWouldBlockByReason,
157
+ topWouldBlockSummaries: cohortTopWouldBlockSummaries,
158
+ containedExecution: containedExecutionMetrics(cohortGateRecords),
159
+ };
98
160
  const mode = options.mode ?? null;
99
161
  const unknownLocalEffect = options.unknownLocalEffect ?? null;
100
162
  const notes = [];
101
163
  let readyForEnforce = false;
102
164
  if (mode === 'audit' && unknownLocalEffect === 'deny') {
103
165
  notes.push('Dogfood config detected: audit mode with fail-closed shell policy.');
104
- if (gateEvents === 0) {
105
- notes.push('No gate events yetrun normal agent work, then re-check metrics.');
166
+ if (!activeCohort) {
167
+ notes.push('Active runtime provenance is unavailable readiness cannot use historical audit evidence.');
168
+ }
169
+ else if (cohortGateEvents === 0) {
170
+ notes.push('No gate events for the active runtime/config cohort — run normal agent work, then re-check metrics.');
106
171
  }
107
- else if (wouldBlockRate === 0) {
108
- if (gateEvents >= MIN_GATE_EVENTS_FOR_ENFORCE) {
172
+ else if (cohortWouldBlockRate === 0) {
173
+ if (cohortGateEvents >= MIN_GATE_EVENTS_FOR_ENFORCE) {
109
174
  readyForEnforce = true;
110
175
  notes.push('No would-block events recorded — safe to try mode: "enforce".');
111
176
  }
112
177
  else {
113
- notes.push(`Only ${gateEvents} gate event(s) recorded — collect at least ${MIN_GATE_EVENTS_FOR_ENFORCE} before enforce.`);
178
+ notes.push(`Only ${cohortGateEvents} active-cohort gate event(s) recorded — collect at least ${MIN_GATE_EVENTS_FOR_ENFORCE} before enforce.`);
114
179
  }
115
180
  }
116
181
  else {
117
- notes.push(`${wouldBlockCount} would-block event(s) (${(wouldBlockRate * 100).toFixed(1)}% of gate traffic; classifier-quality ${(classifierWouldBlockRate * 100).toFixed(1)}%). Review top summaries and correct EffectPlan semantics or resource scope; use exact approval only when the modeled effects are correct.`);
118
- if (approvalRecordedCount > 0) {
119
- notes.push(`${approvalRecordedCount} approval(s) recorded — these likely indicate actions operators wanted.`);
182
+ notes.push(`${cohortWouldBlockCount} active-cohort would-block event(s) (${(cohortWouldBlockRate * 100).toFixed(1)}% of gate traffic; classifier-quality ${(cohortClassifierWouldBlockRate * 100).toFixed(1)}%). Review top summaries and correct EffectPlan semantics or resource scope; use exact approval only when the modeled effects are correct.`);
183
+ if (cohortApprovalRecordedCount > 0) {
184
+ notes.push(`${cohortApprovalRecordedCount} active-cohort approval(s) recorded — these likely indicate actions operators wanted.`);
120
185
  }
121
186
  else {
122
187
  notes.push('Review top would-block summaries and correct EffectPlan semantics or resource scope before switching to enforce.');
123
188
  }
124
- if (classifierWouldBlockRate < 0.05 && gateEvents >= 20 && availabilityAsks.total === 0) {
189
+ if (cohortClassifierWouldBlockRate < 0.05 &&
190
+ cohortGateEvents >= 20 &&
191
+ cohortAvailabilityAsks.total === 0) {
125
192
  readyForEnforce = true;
126
193
  notes.push('Classifier-quality would-block rate is below 5% with sufficient sample size — consider enforce mode.');
127
194
  }
@@ -133,17 +200,18 @@ export function computeAuditMetrics(records, options = {}) {
133
200
  else {
134
201
  notes.push('Set policy.unknownLocalEffect to "deny" to dogfood fail-closed defaults.');
135
202
  }
136
- if (availabilityAsks.total > 0) {
203
+ if (cohortAvailabilityAsks.total > 0) {
137
204
  readyForEnforce = false;
138
- notes.push(`${availabilityAsks.total} availability-caused ask(s) — tune infrastructure before changing Effect semantics.`);
205
+ notes.push(`${cohortAvailabilityAsks.total} active-cohort availability-caused ask(s) — tune infrastructure before changing Effect semantics.`);
139
206
  notes.push('Ready for enforce withheld while availability-caused asks are present.');
140
207
  }
141
- if (repeatedFingerprintAsks.length > 0) {
142
- notes.push(`${repeatedFingerprintAsks.length} repeated fingerprint ask pattern(s) — review EffectPlan semantics and exact approval history.`);
208
+ if (cohortRepeatedFingerprintAsks.length > 0) {
209
+ notes.push(`${cohortRepeatedFingerprintAsks.length} active-cohort repeated fingerprint ask pattern(s) — review EffectPlan semantics and exact approval history.`);
143
210
  }
144
- if (noisyRuleCandidates.length > 0) {
145
- notes.push(`${noisyRuleCandidates.length} noisy rule candidate(s) — high deny-then-approve rate.`);
211
+ if (cohortNoisyRuleCandidates.length > 0) {
212
+ notes.push(`${cohortNoisyRuleCandidates.length} active-cohort noisy rule candidate(s) — high deny-then-approve rate.`);
146
213
  }
214
+ const recoveryMetrics = computeRecoveryMetrics(auditRecords, { activeCohort });
147
215
  return {
148
216
  schemaVersion: AUDIT_METRICS_SCHEMA_VERSION,
149
217
  auditLogPath: options.auditLogPath ?? 'belay/audit.ndjson',
@@ -166,8 +234,10 @@ export function computeAuditMetrics(records, options = {}) {
166
234
  byEffect,
167
235
  byConfidence,
168
236
  gateEventsByRuntime,
237
+ currentCohort,
169
238
  approvalRecordedCount,
170
239
  topWouldBlockSummaries,
240
+ containedExecution: containedExecutionMetrics(auditRecords),
171
241
  approvalLatency,
172
242
  gateEventsByDay: bucketGateEventsByDay(auditRecords),
173
243
  bypassAttemptCount: bypassAttempts.length,
@@ -178,6 +248,8 @@ export function computeAuditMetrics(records, options = {}) {
178
248
  readyForEnforce,
179
249
  notes,
180
250
  },
251
+ recovery: recoveryMetrics.allTime,
252
+ currentCohortRecovery: recoveryMetrics.currentCohort,
181
253
  };
182
254
  }
183
255
  export { buildApprovalRoundTrips, filterAuditRecords, toAuditRecord };
@@ -0,0 +1,33 @@
1
+ import type { AuditCohortIdentity } from './audit-metrics.js';
2
+ import type { AuditRecord } from './audit-types.js';
3
+ export interface RecoverySnapshotMetrics {
4
+ attempts: number;
5
+ applied: number;
6
+ skipped: number;
7
+ byBackend: Record<string, number>;
8
+ byResourceKind: Record<string, number>;
9
+ prepareSampleCount: number;
10
+ prepareMsP50: number | null;
11
+ prepareMsP95: number | null;
12
+ failuresByReason: Record<string, number>;
13
+ }
14
+ export interface RecoveryRestoreMetrics {
15
+ applied: number;
16
+ conflict: number;
17
+ rejected: number;
18
+ }
19
+ export interface RecoveryMetrics {
20
+ snapshot: RecoverySnapshotMetrics;
21
+ restore: RecoveryRestoreMetrics;
22
+ }
23
+ export interface RecoveryMetricsCohort extends RecoveryMetrics {
24
+ excludedSnapshotAttempts: number;
25
+ excludedRestoreEvents: number;
26
+ }
27
+ export declare function sanitizeRecoveryFailureReason(record: AuditRecord): string;
28
+ export declare function computeRecoveryMetrics(records: AuditRecord[], options?: {
29
+ activeCohort?: AuditCohortIdentity | null;
30
+ }): {
31
+ allTime: RecoveryMetrics;
32
+ currentCohort: RecoveryMetricsCohort;
33
+ };