@guilz-dev/belay 0.8.1 → 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 (104) 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 +73 -6
  9. package/dist/commands/explain.js +3 -2
  10. package/dist/commands/metrics.js +42 -0
  11. package/dist/commands/recovery-checkpoints.d.ts +8 -1
  12. package/dist/commands/recovery-checkpoints.js +44 -11
  13. package/dist/commands/session.d.ts +5 -0
  14. package/dist/commands/session.js +8 -1
  15. package/dist/config-io.js +2 -2
  16. package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
  17. package/dist/conformance/contained-execution-guarantee.js +121 -0
  18. package/dist/conformance/guarantee-table.js +7 -0
  19. package/dist/core/audit-io.js +8 -0
  20. package/dist/core/audit-metrics.d.ts +11 -0
  21. package/dist/core/audit-metrics.js +14 -0
  22. package/dist/core/audit-recovery-metrics.d.ts +33 -0
  23. package/dist/core/audit-recovery-metrics.js +146 -0
  24. package/dist/core/audit-types.d.ts +7 -1
  25. package/dist/core/audit-types.js +1 -1
  26. package/dist/core/bounded-output.d.ts +11 -0
  27. package/dist/core/bounded-output.js +26 -0
  28. package/dist/core/capability/attestation.d.ts +56 -0
  29. package/dist/core/capability/attestation.js +110 -3
  30. package/dist/core/capability/boundary-session.d.ts +5 -0
  31. package/dist/core/capability/boundary-session.js +38 -1
  32. package/dist/core/capability/index.d.ts +1 -1
  33. package/dist/core/capability/index.js +1 -1
  34. package/dist/core/config.d.ts +18 -0
  35. package/dist/core/config.js +89 -10
  36. package/dist/core/contained-execution/docker-policy.d.ts +18 -0
  37. package/dist/core/contained-execution/docker-policy.js +97 -0
  38. package/dist/core/contained-execution/docker.d.ts +223 -0
  39. package/dist/core/contained-execution/docker.js +846 -0
  40. package/dist/core/contained-execution/eligibility.d.ts +9 -0
  41. package/dist/core/contained-execution/eligibility.js +103 -0
  42. package/dist/core/contained-execution/failure.d.ts +18 -0
  43. package/dist/core/contained-execution/failure.js +105 -0
  44. package/dist/core/contained-execution/mirror.d.ts +54 -0
  45. package/dist/core/contained-execution/mirror.js +587 -0
  46. package/dist/core/contained-execution/policy.d.ts +10 -0
  47. package/dist/core/contained-execution/policy.js +20 -0
  48. package/dist/core/effect-ir/index.d.ts +1 -1
  49. package/dist/core/effect-ir/index.js +1 -1
  50. package/dist/core/effect-ir/normalize.d.ts +5 -0
  51. package/dist/core/effect-ir/normalize.js +17 -7
  52. package/dist/core/effect-ir/shell-build.js +2 -14
  53. package/dist/core/effect-ir/shell-lower.js +100 -25
  54. package/dist/core/gate-contract.d.ts +4 -1
  55. package/dist/core/gate-contract.js +2 -0
  56. package/dist/core/process-runner.d.ts +10 -3
  57. package/dist/core/process-runner.js +66 -10
  58. package/dist/core/recovery/checkpoint.d.ts +3 -0
  59. package/dist/core/recovery/checkpoint.js +33 -4
  60. package/dist/core/recovery/types.d.ts +1 -0
  61. package/dist/core/scrub.d.ts +17 -0
  62. package/dist/core/scrub.js +499 -5
  63. package/dist/core/standing-allow.d.ts +0 -20
  64. package/dist/core/standing-allow.js +0 -71
  65. package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
  66. package/dist/core/transactional/apply-observed-changes.js +11 -0
  67. package/dist/core/transactional/backend-selector.d.ts +2 -1
  68. package/dist/core/transactional/backend-selector.js +32 -29
  69. package/dist/core/transactional/backend.d.ts +3 -0
  70. package/dist/core/transactional/file-checkpoint-backend.js +242 -54
  71. package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
  72. package/dist/core/transactional/file-checkpoint-git.js +53 -0
  73. package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
  74. package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
  75. package/dist/core/transactional/git-worktree-backend.js +3 -0
  76. package/dist/core/transactional/git-worktree.d.ts +1 -0
  77. package/dist/core/transactional/git-worktree.js +1 -0
  78. package/dist/core/transactional/runner.js +59 -13
  79. package/dist/core/types.d.ts +13 -0
  80. package/dist/core/verdict/parser.d.ts +10 -0
  81. package/dist/core/verdict/parser.js +271 -40
  82. package/dist/core/verdict/shell-semantics.js +21 -0
  83. package/dist/corpus/adversarial-probe.d.ts +1 -1
  84. package/dist/corpus/adversarial-probe.js +3 -3
  85. package/dist/corpus/benign-probe-cores.d.ts +6 -0
  86. package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
  87. package/dist/corpus/coverage-compare.d.ts +36 -0
  88. package/dist/corpus/coverage-compare.js +197 -0
  89. package/dist/corpus/coverage-contexts.d.ts +17 -0
  90. package/dist/corpus/coverage-contexts.js +88 -0
  91. package/dist/corpus/coverage-matrix.d.ts +37 -0
  92. package/dist/corpus/coverage-matrix.js +159 -0
  93. package/dist/corpus/coverage-probe.d.ts +91 -0
  94. package/dist/corpus/coverage-probe.js +341 -0
  95. package/dist/corpus/evaluate.d.ts +1 -1
  96. package/dist/corpus/evaluate.js +1 -1
  97. package/dist/corpus/structural-fixture-root.d.ts +2 -0
  98. package/dist/corpus/structural-fixture-root.js +5 -0
  99. package/dist/version.d.ts +1 -1
  100. package/dist/version.js +1 -1
  101. package/package.json +3 -1
  102. package/skills/belay/SKILL.md +19 -0
  103. package/skills/belay/belay-status.md +5 -0
  104. package/dist/corpus/must-allow-commands.d.ts +0 -6
@@ -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,4 +1,5 @@
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;
@@ -22,6 +23,13 @@ export interface AuditMetricsCohort {
22
23
  reason: string;
23
24
  count: number;
24
25
  }>;
26
+ containedExecution: ContainedExecutionMetrics;
27
+ }
28
+ export interface ContainedExecutionMetrics {
29
+ wouldMediate: number;
30
+ complete: number;
31
+ failed: number;
32
+ timedOut: number;
25
33
  }
26
34
  export interface AuditMetricsReport {
27
35
  schemaVersion: number;
@@ -52,6 +60,7 @@ export interface AuditMetricsReport {
52
60
  reason: string;
53
61
  count: number;
54
62
  }>;
63
+ containedExecution: ContainedExecutionMetrics;
55
64
  approvalLatency: {
56
65
  count: number;
57
66
  medianMs: number | null;
@@ -71,6 +80,8 @@ export interface AuditMetricsReport {
71
80
  readyForEnforce: boolean;
72
81
  notes: string[];
73
82
  };
83
+ recovery: RecoveryMetrics;
84
+ currentCohortRecovery: RecoveryMetricsCohort;
74
85
  }
75
86
  export declare function parseAuditNdjson(raw: string): Record<string, unknown>[];
76
87
  export declare function computeAuditMetrics(records: Record<string, unknown>[], options?: {
@@ -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')) {
@@ -146,6 +155,7 @@ export function computeAuditMetrics(records, options = {}) {
146
155
  availabilityAsks: cohortAvailabilityAsks,
147
156
  wouldBlockByReason: cohortWouldBlockByReason,
148
157
  topWouldBlockSummaries: cohortTopWouldBlockSummaries,
158
+ containedExecution: containedExecutionMetrics(cohortGateRecords),
149
159
  };
150
160
  const mode = options.mode ?? null;
151
161
  const unknownLocalEffect = options.unknownLocalEffect ?? null;
@@ -201,6 +211,7 @@ export function computeAuditMetrics(records, options = {}) {
201
211
  if (cohortNoisyRuleCandidates.length > 0) {
202
212
  notes.push(`${cohortNoisyRuleCandidates.length} active-cohort noisy rule candidate(s) — high deny-then-approve rate.`);
203
213
  }
214
+ const recoveryMetrics = computeRecoveryMetrics(auditRecords, { activeCohort });
204
215
  return {
205
216
  schemaVersion: AUDIT_METRICS_SCHEMA_VERSION,
206
217
  auditLogPath: options.auditLogPath ?? 'belay/audit.ndjson',
@@ -226,6 +237,7 @@ export function computeAuditMetrics(records, options = {}) {
226
237
  currentCohort,
227
238
  approvalRecordedCount,
228
239
  topWouldBlockSummaries,
240
+ containedExecution: containedExecutionMetrics(auditRecords),
229
241
  approvalLatency,
230
242
  gateEventsByDay: bucketGateEventsByDay(auditRecords),
231
243
  bypassAttemptCount: bypassAttempts.length,
@@ -236,6 +248,8 @@ export function computeAuditMetrics(records, options = {}) {
236
248
  readyForEnforce,
237
249
  notes,
238
250
  },
251
+ recovery: recoveryMetrics.allTime,
252
+ currentCohortRecovery: recoveryMetrics.currentCohort,
239
253
  };
240
254
  }
241
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
+ };
@@ -0,0 +1,146 @@
1
+ import { GATE_EVENTS } from './audit-types.js';
2
+ const RESTORE_EVENTS = new Set(['recoveryApplied', 'recoveryConflict', 'recoveryRejected']);
3
+ const STABLE_FAILURE_REASON = /^[a-z][a-z0-9_]*$/;
4
+ function increment(bucket, key) {
5
+ bucket[key] = (bucket[key] ?? 0) + 1;
6
+ }
7
+ function percentile(sorted, p) {
8
+ if (sorted.length === 0) {
9
+ return null;
10
+ }
11
+ const index = Math.ceil((p / 100) * sorted.length) - 1;
12
+ return sorted[Math.max(0, Math.min(sorted.length - 1, index))] ?? null;
13
+ }
14
+ export function sanitizeRecoveryFailureReason(record) {
15
+ const reason = record.transactionalSkipReason ?? record.reason;
16
+ if (typeof reason === 'string' && STABLE_FAILURE_REASON.test(reason)) {
17
+ return reason;
18
+ }
19
+ return 'transactional_execution_failed';
20
+ }
21
+ function isRecoverySnapshotRecord(record) {
22
+ if (typeof record.transactional === 'boolean') {
23
+ return true;
24
+ }
25
+ if (typeof record.transactionalSkipReason === 'string') {
26
+ return true;
27
+ }
28
+ if (record.recoveryFailClosed === true) {
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+ function isRecoveryRestoreRecord(record) {
34
+ const event = typeof record.event === 'string' ? record.event : '';
35
+ return RESTORE_EVENTS.has(event);
36
+ }
37
+ function matchesCohort(record, cohort) {
38
+ return (record.runtimeBuildStamp === cohort.runtimeBuildStamp &&
39
+ record.configFingerprint === cohort.configFingerprint);
40
+ }
41
+ function emptySnapshotMetrics() {
42
+ return {
43
+ attempts: 0,
44
+ applied: 0,
45
+ skipped: 0,
46
+ byBackend: {},
47
+ byResourceKind: {},
48
+ prepareSampleCount: 0,
49
+ prepareMsP50: null,
50
+ prepareMsP95: null,
51
+ failuresByReason: {},
52
+ };
53
+ }
54
+ function emptyRestoreMetrics() {
55
+ return {
56
+ applied: 0,
57
+ conflict: 0,
58
+ rejected: 0,
59
+ };
60
+ }
61
+ function isRecoverySnapshotApplied(record) {
62
+ return (record.transactional === true &&
63
+ (record.reason === 'transactional_already_applied' ||
64
+ typeof record.recoveryCheckpointId === 'string' ||
65
+ record.recoveryState === 'applied'));
66
+ }
67
+ function computeSnapshotMetrics(records) {
68
+ const metrics = emptySnapshotMetrics();
69
+ const prepareSamples = [];
70
+ for (const record of records) {
71
+ const event = typeof record.event === 'string' ? record.event : '';
72
+ if (!GATE_EVENTS.has(event)) {
73
+ continue;
74
+ }
75
+ if (!isRecoverySnapshotRecord(record)) {
76
+ continue;
77
+ }
78
+ metrics.attempts += 1;
79
+ if (isRecoverySnapshotApplied(record)) {
80
+ metrics.applied += 1;
81
+ }
82
+ else {
83
+ metrics.skipped += 1;
84
+ if (record.transactionalReason === 'transactional_observed_risk') {
85
+ increment(metrics.failuresByReason, 'transactional_observed_risk');
86
+ }
87
+ else {
88
+ increment(metrics.failuresByReason, sanitizeRecoveryFailureReason(record));
89
+ }
90
+ }
91
+ const backend = typeof record.transactionalBackend === 'string' ? record.transactionalBackend : 'unknown';
92
+ increment(metrics.byBackend, backend);
93
+ const resourceKind = typeof record.resourceKind === 'string' ? record.resourceKind : 'unknown';
94
+ increment(metrics.byResourceKind, resourceKind);
95
+ if (typeof record.snapshotPrepareMs === 'number' && Number.isFinite(record.snapshotPrepareMs)) {
96
+ prepareSamples.push(record.snapshotPrepareMs);
97
+ }
98
+ }
99
+ prepareSamples.sort((left, right) => left - right);
100
+ metrics.prepareSampleCount = prepareSamples.length;
101
+ metrics.prepareMsP50 = percentile(prepareSamples, 50);
102
+ metrics.prepareMsP95 = percentile(prepareSamples, 95);
103
+ return metrics;
104
+ }
105
+ function computeRestoreMetrics(records) {
106
+ const metrics = emptyRestoreMetrics();
107
+ for (const record of records) {
108
+ const event = typeof record.event === 'string' ? record.event : '';
109
+ if (event === 'recoveryApplied') {
110
+ metrics.applied += 1;
111
+ }
112
+ else if (event === 'recoveryConflict') {
113
+ metrics.conflict += 1;
114
+ }
115
+ else if (event === 'recoveryRejected') {
116
+ metrics.rejected += 1;
117
+ }
118
+ }
119
+ return metrics;
120
+ }
121
+ export function computeRecoveryMetrics(records, options = {}) {
122
+ const gateSnapshotRecords = records.filter((record) => {
123
+ const event = typeof record.event === 'string' ? record.event : '';
124
+ return GATE_EVENTS.has(event) && isRecoverySnapshotRecord(record);
125
+ });
126
+ const restoreRecords = records.filter(isRecoveryRestoreRecord);
127
+ const activeCohort = options.activeCohort ?? null;
128
+ const cohortGateSnapshotRecords = activeCohort
129
+ ? gateSnapshotRecords.filter((record) => matchesCohort(record, activeCohort))
130
+ : [];
131
+ const cohortRestoreRecords = activeCohort
132
+ ? restoreRecords.filter((record) => matchesCohort(record, activeCohort))
133
+ : [];
134
+ return {
135
+ allTime: {
136
+ snapshot: computeSnapshotMetrics(gateSnapshotRecords),
137
+ restore: computeRestoreMetrics(restoreRecords),
138
+ },
139
+ currentCohort: {
140
+ snapshot: computeSnapshotMetrics(cohortGateSnapshotRecords),
141
+ restore: computeRestoreMetrics(cohortRestoreRecords),
142
+ excludedSnapshotAttempts: gateSnapshotRecords.length - cohortGateSnapshotRecords.length,
143
+ excludedRestoreEvents: restoreRecords.length - cohortRestoreRecords.length,
144
+ },
145
+ };
146
+ }
@@ -1,7 +1,7 @@
1
1
  import type { AuditActionSnapshot, AuditReplayContext } from './audit-replay-context.js';
2
2
  import type { Assessment } from './types.js';
3
3
  export type { AuditActionSnapshot, AuditReplayContext } from './audit-replay-context.js';
4
- export declare const AUDIT_METRICS_SCHEMA_VERSION = 3;
4
+ export declare const AUDIT_METRICS_SCHEMA_VERSION = 4;
5
5
  export declare const GATE_EVENTS: Set<string>;
6
6
  export interface AuditRecord {
7
7
  timestamp?: string;
@@ -42,6 +42,12 @@ export interface AuditRecord {
42
42
  recoveryState?: string;
43
43
  replayContext?: AuditReplayContext;
44
44
  actionSnapshot?: AuditActionSnapshot;
45
+ wouldMediate?: boolean;
46
+ receiptHash?: string;
47
+ imageId?: string;
48
+ mirrorBackend?: 'file_copy';
49
+ exitCode?: number | null;
50
+ timedOut?: boolean;
45
51
  [key: string]: unknown;
46
52
  }
47
53
  export interface AuditFilter {
@@ -1,2 +1,2 @@
1
- export const AUDIT_METRICS_SCHEMA_VERSION = 3;
1
+ export const AUDIT_METRICS_SCHEMA_VERSION = 4;
2
2
  export const GATE_EVENTS = new Set(['beforeShellExecution', 'preToolUse', 'subagentGate']);
@@ -0,0 +1,11 @@
1
+ export declare const OUTPUT_TAIL_LIMIT_BYTES = 16384;
2
+ export interface BoundedOutputTail {
3
+ tail: Buffer;
4
+ truncated: boolean;
5
+ }
6
+ export declare function appendBoundedOutputTail(current: Buffer, chunk: Buffer | string, limitBytes?: number): BoundedOutputTail;
7
+ export declare function decodeBoundedOutputTail(tail: Buffer): string;
8
+ export declare function boundedUtf8Tail(value: string, limitBytes?: number): {
9
+ value: string;
10
+ truncated: boolean;
11
+ };
@@ -0,0 +1,26 @@
1
+ export const OUTPUT_TAIL_LIMIT_BYTES = 16_384;
2
+ export function appendBoundedOutputTail(current, chunk, limitBytes = OUTPUT_TAIL_LIMIT_BYTES) {
3
+ const next = Buffer.concat([current, Buffer.from(chunk)]);
4
+ return {
5
+ tail: next.length > limitBytes ? next.subarray(-limitBytes) : next,
6
+ truncated: next.length > limitBytes,
7
+ };
8
+ }
9
+ export function decodeBoundedOutputTail(tail) {
10
+ for (let offset = 0; offset <= Math.min(3, tail.length); offset += 1) {
11
+ try {
12
+ return new TextDecoder('utf-8', { fatal: true }).decode(tail.subarray(offset));
13
+ }
14
+ catch {
15
+ // A byte cap may have cut a leading multi-byte character; try its next boundary.
16
+ }
17
+ }
18
+ return tail.toString('utf8');
19
+ }
20
+ export function boundedUtf8Tail(value, limitBytes = OUTPUT_TAIL_LIMIT_BYTES) {
21
+ const encoded = Buffer.from(value);
22
+ if (encoded.length <= limitBytes)
23
+ return { value, truncated: false };
24
+ const tail = encoded.subarray(-limitBytes);
25
+ return { value: decodeBoundedOutputTail(tail), truncated: true };
26
+ }
@@ -1,5 +1,56 @@
1
1
  export declare const BOUNDARY_ATTESTATION_VERSION: 1;
2
+ export declare const CONTAINED_EXECUTION_ATTESTATION_VERSION: 1;
2
3
  export type BoundaryDriverId = 'container' | 'cursor-sandbox' | 'seatbelt' | 'landlock' | 'host-integration';
4
+ export interface ContainedExecutionResourceLimits {
5
+ timeoutMs: number;
6
+ memoryMiB: number;
7
+ cpus: number;
8
+ pids: number;
9
+ }
10
+ export interface DockerSubstrateIdentity {
11
+ binaryPath: string;
12
+ binarySha256: string;
13
+ endpoint: string;
14
+ daemonId: string;
15
+ }
16
+ /** A separate execution-only capability; it does not attest L1 grant enforcement. */
17
+ export interface ContainedExecutionAttestation {
18
+ version: typeof CONTAINED_EXECUTION_ATTESTATION_VERSION;
19
+ imageId: string;
20
+ imageReference: string;
21
+ networkNone: true;
22
+ isolatesWorkspaceMirror: true;
23
+ readOnlyRoot: true;
24
+ sanitizedEnvironment: true;
25
+ dockerSubstrate: DockerSubstrateIdentity;
26
+ dockerConfiguration: {
27
+ executable: string;
28
+ host: string;
29
+ };
30
+ user: string;
31
+ entrypoint: '/bin/sh';
32
+ capDropAll: true;
33
+ noNewPrivileges: true;
34
+ logDriver: 'none';
35
+ proxyEnvironment: 'neutralized-empty';
36
+ tmpfs: {
37
+ path: '/tmp';
38
+ sizeBytes: number;
39
+ mode: 0o1777;
40
+ exec: false;
41
+ nosuid: true;
42
+ nodev: true;
43
+ };
44
+ memorySwapMiB: number;
45
+ shmSizeMiB: number;
46
+ healthcheckDisabled: true;
47
+ privateNamespaces: true;
48
+ privileged: false;
49
+ devicesNone: true;
50
+ resourceLimits: ContainedExecutionResourceLimits;
51
+ probedAt: string;
52
+ expiresAt: string;
53
+ }
3
54
  export interface BoundaryAttestation {
4
55
  version: typeof BOUNDARY_ATTESTATION_VERSION;
5
56
  driver: BoundaryDriverId;
@@ -10,7 +61,12 @@ export interface BoundaryAttestation {
10
61
  probeSignals: string[];
11
62
  /** When true, the driver can mount an execution mirror at the original workspace path. */
12
63
  isolatesWorkspaceMounts?: boolean;
64
+ /** Optional, execution-only proof. This must never be treated as an L1-full attestation. */
65
+ containedExecution?: ContainedExecutionAttestation;
13
66
  }
14
67
  export declare function validateBoundaryAttestation(value: unknown): value is BoundaryAttestation;
68
+ export declare function validateContainedExecutionAttestation(value: unknown): value is ContainedExecutionAttestation;
69
+ export declare function isContainedExecutionAttestationFresh(attestation: ContainedExecutionAttestation | null | undefined, now?: number): boolean;
70
+ export declare function isAttestationCurrent(attestation: BoundaryAttestation, now?: number): boolean;
15
71
  export declare function isAttestationFresh(attestation: BoundaryAttestation, now?: number): boolean;
16
72
  export declare function attestsWorkspaceMountIsolation(attestation: BoundaryAttestation | null | undefined): boolean;