@guilz-dev/belay 0.3.0 → 0.4.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/dist/adapters/cursor/runtime-entry.js +1 -0
- package/dist/adapters/shared/gate-runtime.d.ts +11 -14
- package/dist/adapters/shared/gate-runtime.js +277 -19
- package/dist/bundle/claude-runtime.mjs +3138 -699
- package/dist/bundle/codex-runtime.mjs +3162 -711
- package/dist/bundle/cursor-runtime.mjs +3140 -701
- package/dist/cli.js +137 -5
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +45 -26
- package/dist/commands/config.js +135 -52
- package/dist/commands/doctor.js +11 -1
- package/dist/commands/explain.js +6 -1
- package/dist/commands/harvest.d.ts +29 -0
- package/dist/commands/harvest.js +72 -0
- package/dist/commands/judge.d.ts +11 -1
- package/dist/commands/judge.js +21 -2
- package/dist/commands/metrics.js +34 -2
- package/dist/commands/quality.d.ts +38 -0
- package/dist/commands/quality.js +105 -0
- package/dist/commands/simulate.d.ts +1 -0
- package/dist/commands/simulate.js +7 -2
- package/dist/commands/standing-allow.d.ts +10 -0
- package/dist/commands/standing-allow.js +26 -0
- package/dist/commands/tui.d.ts +48 -0
- package/dist/commands/tui.js +150 -0
- package/dist/config-io.js +10 -5
- package/dist/core/approval-replay-cli.d.ts +7 -0
- package/dist/core/approval-replay-cli.js +36 -0
- package/dist/core/approval-replay.d.ts +45 -0
- package/dist/core/approval-replay.js +141 -0
- package/dist/core/approval-service.d.ts +15 -0
- package/dist/core/approval-service.js +47 -2
- package/dist/core/approval.d.ts +24 -3
- package/dist/core/approval.js +47 -4
- package/dist/core/audit-analysis.d.ts +7 -1
- package/dist/core/audit-analysis.js +111 -0
- package/dist/core/audit-metrics.d.ts +7 -0
- package/dist/core/audit-metrics.js +24 -4
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +3 -0
- package/dist/core/audit-replay-context.d.ts +35 -0
- package/dist/core/audit-replay-context.js +88 -0
- package/dist/core/audit-types.d.ts +24 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/capability/index.d.ts +2 -1
- package/dist/core/capability/index.js +1 -0
- package/dist/core/capability/paths.d.ts +2 -2
- package/dist/core/capability/paths.js +9 -9
- package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
- package/dist/core/capability/trusted-workspace-roots.js +149 -0
- package/dist/core/capability/types.d.ts +11 -1
- package/dist/core/capability-approval.d.ts +2 -1
- package/dist/core/capability-approval.js +100 -2
- package/dist/core/classify-subagent.js +2 -20
- package/dist/core/classify-tool.js +32 -3
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +49 -0
- package/dist/core/gate-engine.js +6 -6
- package/dist/core/harvest.d.ts +53 -0
- package/dist/core/harvest.js +276 -0
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.js +6 -2
- package/dist/core/judge-doctor.d.ts +3 -0
- package/dist/core/judge-doctor.js +54 -0
- package/dist/core/path-utils.d.ts +11 -0
- package/dist/core/path-utils.js +56 -6
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- package/dist/core/replay-scrub.d.ts +9 -0
- package/dist/core/replay-scrub.js +43 -0
- package/dist/core/shell-tokenizer.d.ts +2 -0
- package/dist/core/shell-tokenizer.js +82 -23
- package/dist/core/standing-allow.d.ts +50 -0
- package/dist/core/standing-allow.js +175 -0
- package/dist/core/types.d.ts +15 -0
- package/dist/core/verdict/adapter.js +10 -1
- package/dist/core/verdict/containment.d.ts +4 -3
- package/dist/core/verdict/containment.js +17 -12
- package/dist/core/verdict/judge-audit.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +32 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +48 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +279 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +137 -16
- package/dist/core/verdict/judge-factory.js +13 -4
- package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
- package/dist/core/verdict/judge-provider-matrix.js +66 -0
- package/dist/core/verdict/judge-runtime-config.d.ts +42 -0
- package/dist/core/verdict/judge-runtime-config.js +92 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +195 -0
- package/dist/core/verdict/judge-session-guard.d.ts +27 -0
- package/dist/core/verdict/judge-session-guard.js +91 -0
- package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
- package/dist/core/verdict/judge-session-kill-switch.js +43 -0
- package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
- package/dist/core/verdict/judge-session-mutex.js +23 -0
- package/dist/core/verdict/judge-shadow.d.ts +24 -0
- package/dist/core/verdict/judge-shadow.js +116 -0
- package/dist/core/verdict/judge-transport.d.ts +40 -0
- package/dist/core/verdict/judge-transport.js +258 -0
- package/dist/core/verdict/judge.d.ts +14 -1
- package/dist/core/verdict/judge.js +6 -5
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/types.d.ts +13 -0
- package/dist/core/verdict/verdict.js +7 -4
- package/dist/corpus/adversarial-probe.d.ts +86 -0
- package/dist/corpus/adversarial-probe.js +220 -0
- package/dist/corpus/evaluate.d.ts +45 -12
- package/dist/corpus/evaluate.js +62 -5
- package/dist/corpus/gates.d.ts +35 -0
- package/dist/corpus/gates.js +81 -0
- package/dist/corpus/judge-accuracy.d.ts +8 -0
- package/dist/corpus/judge-accuracy.js +39 -0
- package/dist/corpus/must-allow-commands.d.ts +5 -0
- package/dist/corpus/must-allow-commands.js +13 -0
- package/dist/corpus/mutators.d.ts +28 -0
- package/dist/corpus/mutators.js +151 -0
- package/dist/corpus/ratchet.d.ts +42 -0
- package/dist/corpus/ratchet.js +116 -0
- package/dist/corpus/runtime-match.d.ts +19 -0
- package/dist/corpus/runtime-match.js +45 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +79 -0
- package/dist/corpus/types.d.ts +47 -0
- package/dist/corpus/types.js +157 -0
- package/dist/installer.js +4 -1
- package/dist/judge-broker-daemon.d.ts +1 -0
- package/dist/judge-broker-daemon.js +123 -0
- package/dist/services/sandbox-service.d.ts +1 -0
- package/dist/services/sandbox-service.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +7 -1
- package/skills/belay/belay-approve.md +17 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { scrubValue } from './scrub.js';
|
|
2
|
+
/** Subagent fingerprint input — must match classify-subagent `fingerprintSource`. */
|
|
3
|
+
export function subagentFingerprintSource(payload, scrubOptions) {
|
|
4
|
+
const toolInput = payload.tool_input;
|
|
5
|
+
if (toolInput && typeof toolInput === 'object') {
|
|
6
|
+
const input = toolInput;
|
|
7
|
+
return scrubValue({
|
|
8
|
+
description: input.description ?? '',
|
|
9
|
+
prompt: input.prompt ?? '',
|
|
10
|
+
}, scrubOptions);
|
|
11
|
+
}
|
|
12
|
+
const task = payload.task;
|
|
13
|
+
if (typeof task === 'string') {
|
|
14
|
+
return scrubValue({ task }, scrubOptions);
|
|
15
|
+
}
|
|
16
|
+
if (task && typeof task === 'object') {
|
|
17
|
+
const taskObj = task;
|
|
18
|
+
return scrubValue({
|
|
19
|
+
description: taskObj.description ?? '',
|
|
20
|
+
prompt: taskObj.prompt ?? '',
|
|
21
|
+
}, scrubOptions);
|
|
22
|
+
}
|
|
23
|
+
return scrubValue(payload, scrubOptions);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Scrubbed payload used for replay envelope hashing — aligned with classifier fingerprints.
|
|
27
|
+
* Tool: scrubbed `tool_input`. Subagent: description/prompt subset. Other: full payload.
|
|
28
|
+
*/
|
|
29
|
+
export function fingerprintReplayPayload(kind, payload, scrubOptions) {
|
|
30
|
+
if (!payload) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (kind === 'tool') {
|
|
34
|
+
const toolInput = payload.tool_input;
|
|
35
|
+
if (toolInput && typeof toolInput === 'object') {
|
|
36
|
+
return scrubValue(toolInput, scrubOptions);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (kind === 'subagent') {
|
|
40
|
+
return subagentFingerprintSource(payload, scrubOptions);
|
|
41
|
+
}
|
|
42
|
+
return scrubValue(payload, scrubOptions);
|
|
43
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export declare function isRedirectOperator(token: string): boolean;
|
|
2
|
+
export declare function isFdDuplication(token: string): boolean;
|
|
1
3
|
export declare function tokenizeShell(input: string): string[];
|
|
2
4
|
export declare function normalizeShellCommand(command: string, repoRoot: string, normalizeToken: (t: string, r: string) => string): string;
|
|
3
5
|
export declare function splitTopLevelSegments(tokens: string[]): string[][];
|
|
@@ -1,4 +1,72 @@
|
|
|
1
1
|
const ENV_PREFIX_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)$/;
|
|
2
|
+
const FD_DUPLICATION_PATTERN = /^\d+[<>]&(?:\d+|-)$/;
|
|
3
|
+
const FD_REDIRECT_PATTERN = /^\d+(?:>>?|<)$/;
|
|
4
|
+
function readDigits(input, index) {
|
|
5
|
+
let end = index;
|
|
6
|
+
while (end < input.length && /[0-9]/.test(input[end] ?? '')) {
|
|
7
|
+
end += 1;
|
|
8
|
+
}
|
|
9
|
+
return input.slice(index, end);
|
|
10
|
+
}
|
|
11
|
+
// This tokenizer only recognizes FD redirects when the digit run is immediately
|
|
12
|
+
// followed by `>` or `<` (for example `2>&1`, `3>file`, `12>>log`, `3<file`,
|
|
13
|
+
// `3<&1`). It does not try to recover bash's full word-boundary rules for cases
|
|
14
|
+
// like `foo12>>bar` or `foo12<bar`.
|
|
15
|
+
function readShellOperator(input, index) {
|
|
16
|
+
const char = input[index];
|
|
17
|
+
const next = input[index + 1] ?? '';
|
|
18
|
+
const digits = /[0-9]/.test(char) ? readDigits(input, index) : '';
|
|
19
|
+
const digitsLength = digits.length;
|
|
20
|
+
const afterDigits = digitsLength > 0 ? (input[index + digitsLength] ?? '') : '';
|
|
21
|
+
const afterDigitsNext = digitsLength > 0 ? (input[index + digitsLength + 1] ?? '') : '';
|
|
22
|
+
if (char === '&' && next === '&') {
|
|
23
|
+
return { token: '&&', length: 2 };
|
|
24
|
+
}
|
|
25
|
+
if (char === '|' && next === '|') {
|
|
26
|
+
return { token: '||', length: 2 };
|
|
27
|
+
}
|
|
28
|
+
if (char === '|' && next === '&') {
|
|
29
|
+
return { token: '|&', length: 2 };
|
|
30
|
+
}
|
|
31
|
+
if (char === '&' && next === '>') {
|
|
32
|
+
return { token: '&>', length: 2 };
|
|
33
|
+
}
|
|
34
|
+
if (digitsLength > 0 && (afterDigits === '>' || afterDigits === '<')) {
|
|
35
|
+
if (afterDigitsNext === '&') {
|
|
36
|
+
let end = index + digitsLength + 2;
|
|
37
|
+
while (end < input.length && /[0-9-]/.test(input[end] ?? '')) {
|
|
38
|
+
end += 1;
|
|
39
|
+
}
|
|
40
|
+
if (end > index + digitsLength + 2) {
|
|
41
|
+
return { token: input.slice(index, end), length: end - index };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (afterDigits === '>' && afterDigitsNext === '>') {
|
|
45
|
+
return { token: `${digits}>>`, length: digitsLength + 2 };
|
|
46
|
+
}
|
|
47
|
+
return { token: `${digits}${afterDigits}`, length: digitsLength + 1 };
|
|
48
|
+
}
|
|
49
|
+
if (char === '>' && next === '>') {
|
|
50
|
+
return { token: '>>', length: 2 };
|
|
51
|
+
}
|
|
52
|
+
if (char === '&') {
|
|
53
|
+
return { token: '&', length: 1 };
|
|
54
|
+
}
|
|
55
|
+
if (char === '|' || char === ';' || char === '>' || char === '<') {
|
|
56
|
+
return { token: char, length: 1 };
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
export function isRedirectOperator(token) {
|
|
61
|
+
return (token === '>' ||
|
|
62
|
+
token === '>>' ||
|
|
63
|
+
token === '<' ||
|
|
64
|
+
token === '&>' ||
|
|
65
|
+
FD_REDIRECT_PATTERN.test(token));
|
|
66
|
+
}
|
|
67
|
+
export function isFdDuplication(token) {
|
|
68
|
+
return FD_DUPLICATION_PATTERN.test(token);
|
|
69
|
+
}
|
|
2
70
|
export function tokenizeShell(input) {
|
|
3
71
|
const tokens = [];
|
|
4
72
|
let buffer = '';
|
|
@@ -12,7 +80,6 @@ export function tokenizeShell(input) {
|
|
|
12
80
|
};
|
|
13
81
|
for (let index = 0; index < input.length; index += 1) {
|
|
14
82
|
const char = input[index];
|
|
15
|
-
const next = input[index + 1] ?? '';
|
|
16
83
|
if (escaping) {
|
|
17
84
|
buffer += char;
|
|
18
85
|
escaping = false;
|
|
@@ -35,27 +102,11 @@ export function tokenizeShell(input) {
|
|
|
35
102
|
quote = char;
|
|
36
103
|
continue;
|
|
37
104
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
tokens.push('&&');
|
|
41
|
-
index += 1;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (char === '|' && next === '|') {
|
|
45
|
-
flush();
|
|
46
|
-
tokens.push('||');
|
|
47
|
-
index += 1;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (char === '>' && next === '>') {
|
|
105
|
+
const operator = readShellOperator(input, index);
|
|
106
|
+
if (operator) {
|
|
51
107
|
flush();
|
|
52
|
-
tokens.push(
|
|
53
|
-
index += 1;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (char === '|' || char === ';' || char === '>' || char === '<') {
|
|
57
|
-
flush();
|
|
58
|
-
tokens.push(char);
|
|
108
|
+
tokens.push(operator.token);
|
|
109
|
+
index += operator.length - 1;
|
|
59
110
|
continue;
|
|
60
111
|
}
|
|
61
112
|
if (char === '\n' || char === '\r') {
|
|
@@ -84,7 +135,12 @@ export function splitTopLevelSegments(tokens) {
|
|
|
84
135
|
const segments = [];
|
|
85
136
|
let current = [];
|
|
86
137
|
for (const token of tokens) {
|
|
87
|
-
if (token === '&&' ||
|
|
138
|
+
if (token === '&&' ||
|
|
139
|
+
token === '||' ||
|
|
140
|
+
token === ';' ||
|
|
141
|
+
token === '|' ||
|
|
142
|
+
token === '&' ||
|
|
143
|
+
token === '|&') {
|
|
88
144
|
if (current.length > 0) {
|
|
89
145
|
segments.push(current);
|
|
90
146
|
}
|
|
@@ -118,7 +174,10 @@ export function extractRedirectTargets(tokens) {
|
|
|
118
174
|
const targets = [];
|
|
119
175
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
120
176
|
const token = tokens[index];
|
|
121
|
-
if (token
|
|
177
|
+
if (isFdDuplication(token)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (isRedirectOperator(token)) {
|
|
122
181
|
const next = tokens[index + 1];
|
|
123
182
|
if (next) {
|
|
124
183
|
targets.push(next);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { BelayConfigV4 } from './config.js';
|
|
2
|
+
import type { GatedActionKind } from './gate-contract.js';
|
|
3
|
+
import type { ClassifyResult } from './types.js';
|
|
4
|
+
export type StandingAllowSource = 'provably-benign-corpus' | 'must-allow-catalog' | 'operator' | 'availability-reconfirmed';
|
|
5
|
+
export interface StandingAllowEntry {
|
|
6
|
+
kind: GatedActionKind;
|
|
7
|
+
fingerprint: string;
|
|
8
|
+
source: StandingAllowSource;
|
|
9
|
+
reason: string;
|
|
10
|
+
summary?: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
repoRoot?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface StandingAllowFile {
|
|
16
|
+
version: 1;
|
|
17
|
+
entries: StandingAllowEntry[];
|
|
18
|
+
}
|
|
19
|
+
export interface StandingAllowMatch {
|
|
20
|
+
source: StandingAllowSource;
|
|
21
|
+
catalogCommand?: string;
|
|
22
|
+
entryId?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Default TTL for operator / availability-reconfirmed standing-allow entries. */
|
|
25
|
+
export declare const DEFAULT_STANDING_ALLOW_TTL_MS: number;
|
|
26
|
+
export declare function standingAllowFile(config: BelayConfigV4, repoLocalStateDir: string): string;
|
|
27
|
+
export declare function isTier0StandingAllowBlocked(result: ClassifyResult): boolean;
|
|
28
|
+
export declare function compactStandingAllow(state: StandingAllowFile, now?: number): StandingAllowFile;
|
|
29
|
+
export declare function loadStandingAllow(filePath: string): Promise<StandingAllowFile>;
|
|
30
|
+
export declare function saveStandingAllow(filePath: string, state: StandingAllowFile): Promise<void>;
|
|
31
|
+
export declare function resolveStandingAllowMatch(params: {
|
|
32
|
+
kind: GatedActionKind;
|
|
33
|
+
result: ClassifyResult;
|
|
34
|
+
repoRoot: string;
|
|
35
|
+
state: StandingAllowFile;
|
|
36
|
+
now?: number;
|
|
37
|
+
}): StandingAllowMatch | null;
|
|
38
|
+
export declare function addStandingAllowEntry(state: StandingAllowFile, entry: Omit<StandingAllowEntry, 'createdAt' | 'expiresAt'> & {
|
|
39
|
+
createdAt?: string;
|
|
40
|
+
expiresAt?: string;
|
|
41
|
+
ttlMs?: number;
|
|
42
|
+
}): StandingAllowFile;
|
|
43
|
+
export declare function revokeStandingAllowEntry(state: StandingAllowFile, params: {
|
|
44
|
+
kind: GatedActionKind;
|
|
45
|
+
fingerprint: string;
|
|
46
|
+
repoRoot?: string;
|
|
47
|
+
}): {
|
|
48
|
+
state: StandingAllowFile;
|
|
49
|
+
removed: boolean;
|
|
50
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { STANDING_ALLOW_CATALOG } from '../corpus/standing-allow-catalog.generated.js';
|
|
5
|
+
import { belayStateDir } from './config.js';
|
|
6
|
+
const EMPTY_STANDING_ALLOW = {
|
|
7
|
+
version: 1,
|
|
8
|
+
entries: [],
|
|
9
|
+
};
|
|
10
|
+
/** Default TTL for operator / availability-reconfirmed standing-allow entries. */
|
|
11
|
+
export const DEFAULT_STANDING_ALLOW_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
12
|
+
const PROVABLY_BENIGN_COMMANDS = new Set(STANDING_ALLOW_CATALOG.shell.provablyBenign.map((entry) => entry.normalizedCommand));
|
|
13
|
+
const MUST_ALLOW_COMMANDS = new Set(STANDING_ALLOW_CATALOG.shell.mustAllow.map((entry) => entry.normalizedCommand));
|
|
14
|
+
/** Reasons that must never be silenced via standing-allow (defense in depth with signal checks). */
|
|
15
|
+
const STANDING_ALLOW_BLOCKED_REASONS = new Set([
|
|
16
|
+
'external_effect',
|
|
17
|
+
'tier1_catastrophic',
|
|
18
|
+
'protected_artifact',
|
|
19
|
+
'pipe_to_shell',
|
|
20
|
+
'command_substitution',
|
|
21
|
+
]);
|
|
22
|
+
export function standingAllowFile(config, repoLocalStateDir) {
|
|
23
|
+
return `${belayStateDir(config, repoLocalStateDir)}/standing-allow.json`;
|
|
24
|
+
}
|
|
25
|
+
export function isTier0StandingAllowBlocked(result) {
|
|
26
|
+
if (result.reason.startsWith('tier0_') || STANDING_ALLOW_BLOCKED_REASONS.has(result.reason)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const signals = result.assessment?.signals ?? [];
|
|
30
|
+
return signals.some((signal) => signal === 'tier0_external' || signal === 'tier1_catastrophic');
|
|
31
|
+
}
|
|
32
|
+
function isExpired(iso, now = Date.now()) {
|
|
33
|
+
const parsed = Date.parse(iso);
|
|
34
|
+
return Number.isNaN(parsed) || parsed <= now;
|
|
35
|
+
}
|
|
36
|
+
export function compactStandingAllow(state, now = Date.now()) {
|
|
37
|
+
return {
|
|
38
|
+
version: 1,
|
|
39
|
+
entries: state.entries.filter((entry) => !isExpired(entry.expiresAt, now)),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function sanitizeStandingAllowEntries(input) {
|
|
43
|
+
if (!Array.isArray(input)) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
return input.flatMap((entry) => {
|
|
47
|
+
if (!entry || typeof entry !== 'object') {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const record = entry;
|
|
51
|
+
if ((record.kind !== 'shell' && record.kind !== 'tool' && record.kind !== 'subagent') ||
|
|
52
|
+
typeof record.fingerprint !== 'string' ||
|
|
53
|
+
!record.fingerprint.trim() ||
|
|
54
|
+
typeof record.expiresAt !== 'string' ||
|
|
55
|
+
typeof record.createdAt !== 'string' ||
|
|
56
|
+
typeof record.reason !== 'string') {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const source = record.source;
|
|
60
|
+
if (source !== 'operator' && source !== 'availability-reconfirmed') {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
kind: record.kind,
|
|
66
|
+
fingerprint: record.fingerprint,
|
|
67
|
+
source,
|
|
68
|
+
reason: record.reason,
|
|
69
|
+
createdAt: record.createdAt,
|
|
70
|
+
expiresAt: record.expiresAt,
|
|
71
|
+
...(typeof record.summary === 'string' ? { summary: record.summary } : {}),
|
|
72
|
+
...(typeof record.repoRoot === 'string' ? { repoRoot: record.repoRoot } : {}),
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
export async function loadStandingAllow(filePath) {
|
|
78
|
+
if (!existsSync(filePath)) {
|
|
79
|
+
return { ...EMPTY_STANDING_ALLOW };
|
|
80
|
+
}
|
|
81
|
+
const raw = JSON.parse(await readFile(filePath, 'utf8'));
|
|
82
|
+
const rawCount = Array.isArray(raw.entries) ? raw.entries.length : 0;
|
|
83
|
+
const entries = sanitizeStandingAllowEntries(raw.entries);
|
|
84
|
+
const compacted = compactStandingAllow({ version: 1, entries });
|
|
85
|
+
if (rawCount !== compacted.entries.length || entries.length !== compacted.entries.length) {
|
|
86
|
+
await saveStandingAllow(filePath, compacted);
|
|
87
|
+
}
|
|
88
|
+
return compacted;
|
|
89
|
+
}
|
|
90
|
+
export async function saveStandingAllow(filePath, state) {
|
|
91
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
92
|
+
const compacted = compactStandingAllow(state);
|
|
93
|
+
await writeFile(filePath, `${JSON.stringify(compacted, null, 2)}\n`, 'utf8');
|
|
94
|
+
}
|
|
95
|
+
function matchBundledCatalog(kind, normalizedCommand) {
|
|
96
|
+
if (kind !== 'shell' || !normalizedCommand) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (PROVABLY_BENIGN_COMMANDS.has(normalizedCommand)) {
|
|
100
|
+
return { source: 'provably-benign-corpus', catalogCommand: normalizedCommand };
|
|
101
|
+
}
|
|
102
|
+
if (MUST_ALLOW_COMMANDS.has(normalizedCommand)) {
|
|
103
|
+
return { source: 'must-allow-catalog', catalogCommand: normalizedCommand };
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function matchStateEntry(params) {
|
|
108
|
+
const now = params.now ?? Date.now();
|
|
109
|
+
const match = params.state.entries.find((entry) => entry.kind === params.kind &&
|
|
110
|
+
entry.fingerprint === params.fingerprint &&
|
|
111
|
+
!isExpired(entry.expiresAt, now) &&
|
|
112
|
+
(entry.repoRoot === undefined || entry.repoRoot === params.repoRoot));
|
|
113
|
+
if (!match) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return { source: match.source, entryId: match.fingerprint };
|
|
117
|
+
}
|
|
118
|
+
export function resolveStandingAllowMatch(params) {
|
|
119
|
+
if (params.result.verdict !== 'deny_pending_approval') {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (isTier0StandingAllowBlocked(params.result)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const normalizedCommand = params.result.normalizedCommand ?? params.result.summary ?? '';
|
|
126
|
+
const catalogMatch = matchBundledCatalog(params.kind, normalizedCommand);
|
|
127
|
+
if (catalogMatch) {
|
|
128
|
+
return catalogMatch;
|
|
129
|
+
}
|
|
130
|
+
return matchStateEntry({
|
|
131
|
+
kind: params.kind,
|
|
132
|
+
fingerprint: params.result.fingerprint,
|
|
133
|
+
repoRoot: params.repoRoot,
|
|
134
|
+
state: params.state,
|
|
135
|
+
now: params.now,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
export function addStandingAllowEntry(state, entry) {
|
|
139
|
+
const createdAt = entry.createdAt ?? new Date().toISOString();
|
|
140
|
+
const ttlMs = entry.ttlMs ?? DEFAULT_STANDING_ALLOW_TTL_MS;
|
|
141
|
+
const expiresAt = entry.expiresAt ?? new Date(Date.parse(createdAt) + ttlMs).toISOString();
|
|
142
|
+
const next = {
|
|
143
|
+
kind: entry.kind,
|
|
144
|
+
fingerprint: entry.fingerprint,
|
|
145
|
+
source: entry.source,
|
|
146
|
+
reason: entry.reason,
|
|
147
|
+
createdAt,
|
|
148
|
+
expiresAt,
|
|
149
|
+
...(entry.summary ? { summary: entry.summary } : {}),
|
|
150
|
+
...(entry.repoRoot ? { repoRoot: entry.repoRoot } : {}),
|
|
151
|
+
};
|
|
152
|
+
const filtered = state.entries.filter((existing) => !(existing.kind === next.kind &&
|
|
153
|
+
existing.fingerprint === next.fingerprint &&
|
|
154
|
+
existing.repoRoot === next.repoRoot));
|
|
155
|
+
return compactStandingAllow({
|
|
156
|
+
version: 1,
|
|
157
|
+
entries: [...filtered, next],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
export function revokeStandingAllowEntry(state, params) {
|
|
161
|
+
const before = state.entries.length;
|
|
162
|
+
const entries = state.entries.filter((entry) => {
|
|
163
|
+
if (entry.kind !== params.kind || entry.fingerprint !== params.fingerprint) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
if (params.repoRoot && entry.repoRoot && entry.repoRoot !== params.repoRoot) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
state: { version: 1, entries },
|
|
173
|
+
removed: entries.length < before,
|
|
174
|
+
};
|
|
175
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -67,11 +67,16 @@ export interface ClassifierOptions {
|
|
|
67
67
|
/** When true with sandbox enabled, outside-repo rules defer to fs-scope allowlist. */
|
|
68
68
|
brokerFsScope?: boolean;
|
|
69
69
|
fsScopeAllowlist?: FsScopeAllowlistFile;
|
|
70
|
+
trustedWorkspaceRoots?: string[];
|
|
70
71
|
/** Test override: inject Tier1 judge without changing config.judge. */
|
|
71
72
|
tier1Judge?: import('./verdict/types.js').Tier1Judge;
|
|
72
73
|
/** When false, path resolution stays fail-closed (opaque cd chains). Default: Boolean(cwd). */
|
|
73
74
|
trustedCwd?: boolean;
|
|
74
75
|
}
|
|
76
|
+
export interface ApprovalScopeHint {
|
|
77
|
+
scope: 'workspace-root';
|
|
78
|
+
path: string;
|
|
79
|
+
}
|
|
75
80
|
export interface ApprovalRecord {
|
|
76
81
|
approvalId: string;
|
|
77
82
|
kind: 'shell' | 'subagent' | 'tool' | 'egress' | 'capability';
|
|
@@ -87,6 +92,16 @@ export interface ApprovalRecord {
|
|
|
87
92
|
/** Original gated input for explain-last-ask (ApprovalState v2). */
|
|
88
93
|
input?: string;
|
|
89
94
|
inputKind?: 'shell' | 'tool' | 'subagent';
|
|
95
|
+
/** Replay envelope: working directory when the action was gated. */
|
|
96
|
+
cwd?: string;
|
|
97
|
+
/** Replay envelope: tool name for tool/subagent actions. */
|
|
98
|
+
toolName?: string;
|
|
99
|
+
/** Replay envelope: canonical payload hash for strict replay validation. */
|
|
100
|
+
payloadHash?: string;
|
|
101
|
+
/** Replay envelope: scrubbed payload JSON for explain (size-capped at write time). */
|
|
102
|
+
payloadJson?: string;
|
|
103
|
+
/** Optional scope hint for structured capability approvals. */
|
|
104
|
+
scopeHint?: ApprovalScopeHint;
|
|
90
105
|
}
|
|
91
106
|
export interface ApprovalStateFile {
|
|
92
107
|
version: 1 | 2;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { judgeTraceAuditFields } from './judge-audit.js';
|
|
2
|
+
import { recordJudgeLatency } from './judge-baseline.js';
|
|
2
3
|
import { createJudgeFromConfig } from './judge-factory.js';
|
|
3
4
|
import { verdict } from './verdict.js';
|
|
4
5
|
export function resolveClassifierTrustedCwd(cwd, options, explicit) {
|
|
@@ -16,11 +17,14 @@ export function buildVerdictContext(params) {
|
|
|
16
17
|
cwd: params.cwd,
|
|
17
18
|
repoRoot: params.repoRoot,
|
|
18
19
|
trustedCwd: resolveClassifierTrustedCwd(params.cwd, params.options, params.trustedCwd),
|
|
20
|
+
trustedWorkspaceRoots: params.options?.trustedWorkspaceRoots ?? [],
|
|
19
21
|
sensitivePaths: params.options?.sensitivePaths ?? params.config.classifier.sensitivePaths,
|
|
20
22
|
protectedArtifactRoots: protectedArtifactRoots.length > 0 ? [...new Set(protectedArtifactRoots)] : undefined,
|
|
21
23
|
customAllowCommands: params.options?.customAllowCommands ?? params.config.overrides.allow,
|
|
22
24
|
customExternalCommands: params.options?.customExternalCommands ?? params.config.overrides.external,
|
|
23
|
-
judge: params.judge ??
|
|
25
|
+
judge: params.judge ??
|
|
26
|
+
params.options?.tier1Judge ??
|
|
27
|
+
createJudgeFromConfig(params.config, { repoRoot: params.repoRoot }),
|
|
24
28
|
mode: params.config.mode,
|
|
25
29
|
unknownLocalEffect: params.options?.unknownLocalEffect ?? params.config.policy.unknownLocalEffect,
|
|
26
30
|
unparseableShell: params.options?.unparseableShell ?? params.config.policy.unparseableShell,
|
|
@@ -28,7 +32,12 @@ export function buildVerdictContext(params) {
|
|
|
28
32
|
}
|
|
29
33
|
export async function classifyShell(command, cwd, repoRoot, config, options = {}, judge) {
|
|
30
34
|
const context = buildVerdictContext({ cwd, repoRoot, config, options, judge });
|
|
35
|
+
const started = Date.now();
|
|
31
36
|
const result = await verdict(command, context);
|
|
37
|
+
const elapsed = Date.now() - started;
|
|
38
|
+
if (result.confidence !== 'llm') {
|
|
39
|
+
recordJudgeLatency('tier0', elapsed);
|
|
40
|
+
}
|
|
32
41
|
return verdictToClassifyResult(result);
|
|
33
42
|
}
|
|
34
43
|
function mapLegacyReason(result) {
|
|
@@ -6,13 +6,13 @@ export interface PathTargetAnalysis {
|
|
|
6
6
|
signals: string[];
|
|
7
7
|
}
|
|
8
8
|
export declare function resolveTrustedPath(token: string, trustedCwd: string, trusted: boolean): string | null;
|
|
9
|
-
export declare function locationForPath(resolvedPath: string | null, repoRoot: string): VerdictLocation;
|
|
9
|
+
export declare function locationForPath(resolvedPath: string | null, repoRoot: string, trustedWorkspaceRoots?: string[]): VerdictLocation;
|
|
10
10
|
export declare function isGitPath(resolvedPath: string, repoRoot: string): boolean;
|
|
11
|
-
export declare function isHighStakesPath(resolvedPath: string, repoRoot: string, sensitivePaths: string[], protectedRoots?: string[]): boolean;
|
|
11
|
+
export declare function isHighStakesPath(resolvedPath: string, repoRoot: string, sensitivePaths: string[], protectedRoots?: string[], trustedWorkspaceRoots?: string[]): boolean;
|
|
12
12
|
/** ADR-002 M3: destructive shell head on git/sensitive path (not broad category ask). */
|
|
13
13
|
export declare function isDestructiveMutationHead(head: string): boolean;
|
|
14
14
|
export declare function touchesProtectedRoot(resolvedPath: string, protectedRoots: string[]): boolean;
|
|
15
|
-
export declare function isDestructiveHighStakesMutation(head: string, resolvedPath: string, repoRoot: string, sensitivePaths: string[], protectedRoots?: string[]): boolean;
|
|
15
|
+
export declare function isDestructiveHighStakesMutation(head: string, resolvedPath: string, repoRoot: string, sensitivePaths: string[], protectedRoots?: string[], trustedWorkspaceRoots?: string[]): boolean;
|
|
16
16
|
export declare function analyzePathTargets(params: {
|
|
17
17
|
targets: string[];
|
|
18
18
|
cwd: string;
|
|
@@ -20,5 +20,6 @@ export declare function analyzePathTargets(params: {
|
|
|
20
20
|
trustedCwd: boolean;
|
|
21
21
|
sensitivePaths: string[];
|
|
22
22
|
protectedArtifactRoots?: string[];
|
|
23
|
+
trustedWorkspaceRoots?: string[];
|
|
23
24
|
}): PathTargetAnalysis;
|
|
24
25
|
export declare function cwdRelative(repoRoot: string, cwd: string): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { matchesSensitivePath } from '../glob.js';
|
|
3
|
-
import { canonicalPath, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from '../path-utils.js';
|
|
3
|
+
import { canonicalPath, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, resolveWorkspaceRootMatch, } from '../path-utils.js';
|
|
4
4
|
import { isOutsideRepoSecretCredentialPath } from './persistent-paths.js';
|
|
5
5
|
export { isOutsideRepoSecretCredentialPath, isPersistentAgentPath } from './persistent-paths.js';
|
|
6
6
|
function expandHome(token) {
|
|
@@ -26,11 +26,11 @@ export function resolveTrustedPath(token, trustedCwd, trusted) {
|
|
|
26
26
|
}
|
|
27
27
|
return canonicalPath(path.resolve(trustedCwd, expanded));
|
|
28
28
|
}
|
|
29
|
-
export function locationForPath(resolvedPath, repoRoot) {
|
|
29
|
+
export function locationForPath(resolvedPath, repoRoot, trustedWorkspaceRoots = []) {
|
|
30
30
|
if (!resolvedPath) {
|
|
31
31
|
return 'unknown';
|
|
32
32
|
}
|
|
33
|
-
if (
|
|
33
|
+
if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolvedPath)) {
|
|
34
34
|
return 'repo_local';
|
|
35
35
|
}
|
|
36
36
|
return 'repo_outside';
|
|
@@ -41,17 +41,22 @@ export function isGitPath(resolvedPath, repoRoot) {
|
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
43
|
const normalized = relative.replaceAll('\\', '/');
|
|
44
|
-
return normalized === '.git' ||
|
|
44
|
+
return (normalized === '.git' ||
|
|
45
|
+
normalized.startsWith('.git/') ||
|
|
46
|
+
normalized.endsWith('/.git') ||
|
|
47
|
+
normalized.includes('/.git/'));
|
|
45
48
|
}
|
|
46
|
-
export function isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
|
|
49
|
+
export function isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, protectedRoots = [], trustedWorkspaceRoots = []) {
|
|
47
50
|
if (isGitPath(resolvedPath, repoRoot)) {
|
|
48
51
|
return true;
|
|
49
52
|
}
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
53
|
+
const workspaceMatch = resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolvedPath);
|
|
54
|
+
if (workspaceMatch !== null &&
|
|
55
|
+
matchesSensitivePath(workspaceMatch.relativePath.replaceAll('\\', '/'), sensitivePaths)) {
|
|
52
56
|
return true;
|
|
53
57
|
}
|
|
54
|
-
if (
|
|
58
|
+
if ((workspaceMatch === null || workspaceMatch.kind === 'trusted') &&
|
|
59
|
+
isOutsideRepoSecretCredentialPath(resolvedPath)) {
|
|
55
60
|
return true;
|
|
56
61
|
}
|
|
57
62
|
return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
|
|
@@ -73,14 +78,14 @@ export function isDestructiveMutationHead(head) {
|
|
|
73
78
|
export function touchesProtectedRoot(resolvedPath, protectedRoots) {
|
|
74
79
|
return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath) || root === resolvedPath);
|
|
75
80
|
}
|
|
76
|
-
export function isDestructiveHighStakesMutation(head, resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
|
|
81
|
+
export function isDestructiveHighStakesMutation(head, resolvedPath, repoRoot, sensitivePaths, protectedRoots = [], trustedWorkspaceRoots = []) {
|
|
77
82
|
if (touchesProtectedRoot(resolvedPath, protectedRoots)) {
|
|
78
83
|
return true;
|
|
79
84
|
}
|
|
80
85
|
if (!isDestructiveMutationHead(head)) {
|
|
81
86
|
return false;
|
|
82
87
|
}
|
|
83
|
-
return isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, []);
|
|
88
|
+
return isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, [], trustedWorkspaceRoots);
|
|
84
89
|
}
|
|
85
90
|
export function analyzePathTargets(params) {
|
|
86
91
|
const signals = [];
|
|
@@ -96,10 +101,10 @@ export function analyzePathTargets(params) {
|
|
|
96
101
|
for (const target of params.targets) {
|
|
97
102
|
const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ??
|
|
98
103
|
resolveMutationTarget(target, params.cwd);
|
|
99
|
-
const location = locationForPath(resolved, params.repoRoot);
|
|
104
|
+
const location = locationForPath(resolved, params.repoRoot, params.trustedWorkspaceRoots);
|
|
100
105
|
locations.add(location);
|
|
101
106
|
if (resolved &&
|
|
102
|
-
isHighStakesPath(resolved, params.repoRoot, params.sensitivePaths, params.protectedArtifactRoots)) {
|
|
107
|
+
isHighStakesPath(resolved, params.repoRoot, params.sensitivePaths, params.protectedArtifactRoots, params.trustedWorkspaceRoots)) {
|
|
103
108
|
isHighStakes = true;
|
|
104
109
|
signals.push('high_stakes_path');
|
|
105
110
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export function hashJudgeSessionRef(value) {
|
|
3
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
4
|
+
}
|
|
1
5
|
export function judgeTraceAuditFields(trace) {
|
|
2
6
|
if (!trace) {
|
|
3
7
|
return {};
|
|
@@ -10,6 +14,33 @@ export function judgeTraceAuditFields(trace) {
|
|
|
10
14
|
...(trace.outboundRedacted !== undefined
|
|
11
15
|
? { judgeOutboundRedacted: trace.outboundRedacted }
|
|
12
16
|
: {}),
|
|
13
|
-
...(trace.
|
|
17
|
+
...(trace.judgeFallbackReason
|
|
18
|
+
? { judgeFallbackReason: trace.judgeFallbackReason }
|
|
19
|
+
: trace.fallbackReason
|
|
20
|
+
? { judgeFallbackReason: trace.fallbackReason }
|
|
21
|
+
: {}),
|
|
22
|
+
...(trace.judgeSessionUsed !== undefined ? { judgeSessionUsed: trace.judgeSessionUsed } : {}),
|
|
23
|
+
...(trace.judgeSessionReused !== undefined
|
|
24
|
+
? { judgeSessionReused: trace.judgeSessionReused }
|
|
25
|
+
: {}),
|
|
26
|
+
...(trace.judgeSessionRefHash ? { judgeSessionRefHash: trace.judgeSessionRefHash } : {}),
|
|
27
|
+
...(trace.judgeSessionResetReason
|
|
28
|
+
? { judgeSessionResetReason: trace.judgeSessionResetReason }
|
|
29
|
+
: {}),
|
|
30
|
+
...(trace.judgeConnectMs !== undefined ? { judgeConnectMs: trace.judgeConnectMs } : {}),
|
|
31
|
+
...(trace.judgeEvalMs !== undefined ? { judgeEvalMs: trace.judgeEvalMs } : {}),
|
|
32
|
+
...(trace.judgeParseMs !== undefined ? { judgeParseMs: trace.judgeParseMs } : {}),
|
|
33
|
+
...(trace.judgeShadowCompared !== undefined
|
|
34
|
+
? { judgeShadowCompared: trace.judgeShadowCompared }
|
|
35
|
+
: {}),
|
|
36
|
+
...(trace.judgeShadowMismatch !== undefined
|
|
37
|
+
? { judgeShadowMismatch: trace.judgeShadowMismatch }
|
|
38
|
+
: {}),
|
|
39
|
+
...(trace.judgeShadowMismatchRateWindow !== undefined
|
|
40
|
+
? { judgeShadowMismatchRateWindow: trace.judgeShadowMismatchRateWindow }
|
|
41
|
+
: {}),
|
|
42
|
+
...(trace.judgeKillSwitchTriggered !== undefined
|
|
43
|
+
? { judgeKillSwitchTriggered: trace.judgeKillSwitchTriggered }
|
|
44
|
+
: {}),
|
|
14
45
|
};
|
|
15
46
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { JUDGE_LATENCY_SLO } from './judge-runtime-config.js';
|
|
2
|
+
export type JudgeLatencyPhase = 'tier0' | 'tier1_spawn' | 'tier1_session' | 'tier1_connect' | 'tier1_eval' | 'tier1_parse';
|
|
3
|
+
export interface JudgeLatencySample {
|
|
4
|
+
phase: JudgeLatencyPhase;
|
|
5
|
+
latencyMs: number;
|
|
6
|
+
at: number;
|
|
7
|
+
}
|
|
8
|
+
export interface JudgeLatencyPercentiles {
|
|
9
|
+
p50: number;
|
|
10
|
+
p95: number;
|
|
11
|
+
count: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function resetJudgeLatencySamples(): void;
|
|
14
|
+
export declare function recordJudgeLatency(phase: JudgeLatencyPhase, latencyMs: number, at?: number): void;
|
|
15
|
+
export declare function judgeLatencyPercentiles(phase: JudgeLatencyPhase): JudgeLatencyPercentiles;
|
|
16
|
+
export declare function judgeLatencySloReport(): {
|
|
17
|
+
slo: typeof JUDGE_LATENCY_SLO;
|
|
18
|
+
measured: Record<JudgeLatencyPhase, JudgeLatencyPercentiles>;
|
|
19
|
+
sessionMeetsTarget: boolean;
|
|
20
|
+
};
|