@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.
- package/README.md +60 -10
- package/dist/adapters/shared/gate-runtime.d.ts +5 -0
- package/dist/adapters/shared/gate-runtime.js +237 -33
- package/dist/bundle/claude-runtime.mjs +4082 -1230
- package/dist/bundle/codex-runtime.mjs +4057 -1205
- package/dist/bundle/cursor-runtime.mjs +4057 -1205
- package/dist/commands/classify-for-report.js +5 -0
- package/dist/commands/doctor.js +73 -6
- package/dist/commands/explain.js +3 -2
- package/dist/commands/metrics.js +42 -0
- package/dist/commands/recovery-checkpoints.d.ts +8 -1
- package/dist/commands/recovery-checkpoints.js +44 -11
- package/dist/commands/session.d.ts +5 -0
- package/dist/commands/session.js +8 -1
- package/dist/config-io.js +2 -2
- package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
- package/dist/conformance/contained-execution-guarantee.js +121 -0
- package/dist/conformance/guarantee-table.js +7 -0
- package/dist/core/audit-io.js +8 -0
- package/dist/core/audit-metrics.d.ts +11 -0
- package/dist/core/audit-metrics.js +14 -0
- package/dist/core/audit-recovery-metrics.d.ts +33 -0
- package/dist/core/audit-recovery-metrics.js +146 -0
- package/dist/core/audit-types.d.ts +7 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/bounded-output.d.ts +11 -0
- package/dist/core/bounded-output.js +26 -0
- package/dist/core/capability/attestation.d.ts +56 -0
- package/dist/core/capability/attestation.js +110 -3
- package/dist/core/capability/boundary-session.d.ts +5 -0
- package/dist/core/capability/boundary-session.js +38 -1
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/index.js +1 -1
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +89 -10
- package/dist/core/contained-execution/docker-policy.d.ts +18 -0
- package/dist/core/contained-execution/docker-policy.js +97 -0
- package/dist/core/contained-execution/docker.d.ts +223 -0
- package/dist/core/contained-execution/docker.js +846 -0
- package/dist/core/contained-execution/eligibility.d.ts +9 -0
- package/dist/core/contained-execution/eligibility.js +103 -0
- package/dist/core/contained-execution/failure.d.ts +18 -0
- package/dist/core/contained-execution/failure.js +105 -0
- package/dist/core/contained-execution/mirror.d.ts +54 -0
- package/dist/core/contained-execution/mirror.js +587 -0
- package/dist/core/contained-execution/policy.d.ts +10 -0
- package/dist/core/contained-execution/policy.js +20 -0
- package/dist/core/effect-ir/index.d.ts +1 -1
- package/dist/core/effect-ir/index.js +1 -1
- package/dist/core/effect-ir/normalize.d.ts +5 -0
- package/dist/core/effect-ir/normalize.js +17 -7
- package/dist/core/effect-ir/shell-build.js +2 -14
- package/dist/core/effect-ir/shell-lower.js +100 -25
- package/dist/core/gate-contract.d.ts +4 -1
- package/dist/core/gate-contract.js +2 -0
- package/dist/core/process-runner.d.ts +10 -3
- package/dist/core/process-runner.js +66 -10
- package/dist/core/recovery/checkpoint.d.ts +3 -0
- package/dist/core/recovery/checkpoint.js +33 -4
- package/dist/core/recovery/types.d.ts +1 -0
- package/dist/core/scrub.d.ts +17 -0
- package/dist/core/scrub.js +499 -5
- package/dist/core/standing-allow.d.ts +0 -20
- package/dist/core/standing-allow.js +0 -71
- package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
- package/dist/core/transactional/apply-observed-changes.js +11 -0
- package/dist/core/transactional/backend-selector.d.ts +2 -1
- package/dist/core/transactional/backend-selector.js +32 -29
- package/dist/core/transactional/backend.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-backend.js +242 -54
- package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-git.js +53 -0
- package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
- package/dist/core/transactional/git-worktree-backend.js +3 -0
- package/dist/core/transactional/git-worktree.d.ts +1 -0
- package/dist/core/transactional/git-worktree.js +1 -0
- package/dist/core/transactional/runner.js +59 -13
- package/dist/core/types.d.ts +13 -0
- package/dist/core/verdict/parser.d.ts +10 -0
- package/dist/core/verdict/parser.js +271 -40
- package/dist/core/verdict/shell-semantics.js +21 -0
- package/dist/corpus/adversarial-probe.d.ts +1 -1
- package/dist/corpus/adversarial-probe.js +3 -3
- package/dist/corpus/benign-probe-cores.d.ts +6 -0
- package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
- package/dist/corpus/coverage-compare.d.ts +36 -0
- package/dist/corpus/coverage-compare.js +197 -0
- package/dist/corpus/coverage-contexts.d.ts +17 -0
- package/dist/corpus/coverage-contexts.js +88 -0
- package/dist/corpus/coverage-matrix.d.ts +37 -0
- package/dist/corpus/coverage-matrix.js +159 -0
- package/dist/corpus/coverage-probe.d.ts +91 -0
- package/dist/corpus/coverage-probe.js +341 -0
- package/dist/corpus/evaluate.d.ts +1 -1
- package/dist/corpus/evaluate.js +1 -1
- package/dist/corpus/structural-fixture-root.d.ts +2 -0
- package/dist/corpus/structural-fixture-root.js +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
- package/skills/belay/SKILL.md +19 -0
- package/skills/belay/belay-status.md +5 -0
- 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',
|
package/dist/core/audit-io.js
CHANGED
|
@@ -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 =
|
|
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 {
|
package/dist/core/audit-types.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const AUDIT_METRICS_SCHEMA_VERSION =
|
|
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;
|