@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.
- 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 +90 -27
- package/dist/commands/explain.js +3 -2
- package/dist/commands/metrics.js +75 -4
- package/dist/commands/quality.js +2 -6
- 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/commands/status.js +1 -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 +34 -0
- package/dist/core/audit-metrics.js +87 -15
- 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/operational-insights.d.ts +3 -0
- package/dist/operational-insights.js +7 -3
- package/dist/runtime-provenance.d.ts +8 -0
- package/dist/runtime-provenance.js +34 -0
- package/dist/types.d.ts +3 -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 { 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',
|
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,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 (
|
|
105
|
-
notes.push('
|
|
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 (
|
|
108
|
-
if (
|
|
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 ${
|
|
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(`${
|
|
118
|
-
if (
|
|
119
|
-
notes.push(`${
|
|
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 (
|
|
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 (
|
|
203
|
+
if (cohortAvailabilityAsks.total > 0) {
|
|
137
204
|
readyForEnforce = false;
|
|
138
|
-
notes.push(`${
|
|
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 (
|
|
142
|
-
notes.push(`${
|
|
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 (
|
|
145
|
-
notes.push(`${
|
|
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
|
+
};
|