@guilz-dev/belay 0.2.0 → 0.3.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 +39 -12
- package/dist/adapters/shared/gate-runtime.js +13 -5
- package/dist/bundle/claude-runtime.mjs +1659 -371
- package/dist/bundle/codex-runtime.mjs +1658 -371
- package/dist/bundle/cursor-runtime.mjs +1659 -371
- package/dist/cli.js +198 -8
- package/dist/commands/approve.js +11 -0
- package/dist/commands/config.d.ts +70 -0
- package/dist/commands/config.js +494 -0
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/health-snapshot.d.ts +6 -0
- package/dist/commands/health-snapshot.js +34 -0
- package/dist/commands/judge.d.ts +90 -0
- package/dist/commands/judge.js +285 -0
- package/dist/commands/status.js +2 -2
- package/dist/commands/stdin-key.d.ts +1 -0
- package/dist/commands/stdin-key.js +8 -0
- package/dist/conformance/guarantee-table.js +12 -0
- package/dist/conformance/types.d.ts +2 -0
- package/dist/core/audit-io.d.ts +2 -0
- package/dist/core/audit-io.js +14 -0
- package/dist/core/capability/index.d.ts +2 -2
- package/dist/core/capability/index.js +2 -2
- package/dist/core/capability/paths.d.ts +1 -0
- package/dist/core/capability/paths.js +74 -6
- package/dist/core/capability/reasons.d.ts +3 -0
- package/dist/core/capability/reasons.js +8 -0
- package/dist/core/classify-tool.js +97 -27
- package/dist/core/config-layers.js +2 -1
- package/dist/core/config.d.ts +22 -2
- package/dist/core/config.js +110 -11
- package/dist/core/credential-store.d.ts +11 -0
- package/dist/core/credential-store.js +60 -0
- package/dist/core/gate-engine.js +104 -13
- package/dist/core/integrity.d.ts +2 -0
- package/dist/core/integrity.js +13 -0
- package/dist/core/judge-api-key.d.ts +19 -0
- package/dist/core/judge-api-key.js +74 -0
- package/dist/core/judge-cloud-consent.d.ts +13 -0
- package/dist/core/judge-cloud-consent.js +38 -0
- package/dist/core/judge-config.d.ts +41 -4
- package/dist/core/judge-config.js +263 -57
- package/dist/core/judge-doctor.d.ts +6 -1
- package/dist/core/judge-doctor.js +147 -96
- package/dist/core/judge-model-discovery.d.ts +24 -0
- package/dist/core/judge-model-discovery.js +168 -0
- package/dist/core/judge-model-policy.d.ts +5 -0
- package/dist/core/judge-model-policy.js +21 -0
- package/dist/core/judge-runtime-detection.d.ts +9 -0
- package/dist/core/judge-runtime-detection.js +68 -0
- package/dist/core/transactional/diff-evaluator.js +1 -19
- package/dist/core/types.d.ts +2 -0
- package/dist/core/verdict/adapter.d.ts +1 -0
- package/dist/core/verdict/adapter.js +7 -1
- package/dist/core/verdict/containment.d.ts +5 -0
- package/dist/core/verdict/containment.js +32 -2
- package/dist/core/verdict/judge-catalog.d.ts +40 -0
- package/dist/core/verdict/judge-catalog.js +148 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +280 -0
- package/dist/core/verdict/judge-factory.d.ts +15 -4
- package/dist/core/verdict/judge-factory.js +117 -14
- package/dist/core/verdict/judge.d.ts +20 -1
- package/dist/core/verdict/judge.js +83 -14
- package/dist/core/verdict/persistent-paths.d.ts +8 -0
- package/dist/core/verdict/persistent-paths.js +52 -0
- package/dist/core/verdict/types.d.ts +6 -2
- package/dist/core/verdict/verdict.js +161 -47
- package/dist/installer.js +66 -15
- package/dist/types.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/belay/SKILL.md +8 -7
- package/dist/commands/init-wizard.d.ts +0 -21
- package/dist/commands/init-wizard.js +0 -63
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { repoLocalStateDirFor } from '../../config-io.js';
|
|
3
4
|
import { normalizeJudgeProvider, scrubOptionsFromConfig } from '../config.js';
|
|
5
|
+
import { resolveJudgeCredential } from '../judge-api-key.js';
|
|
6
|
+
import { hasValidCloudConsent, isCloudJudgeConfig } from '../judge-config.js';
|
|
7
|
+
import { rejectDeprecatedJudgeModelAuto } from '../judge-model-policy.js';
|
|
8
|
+
import { detectJudgeRuntimeCapabilities } from '../judge-runtime-detection.js';
|
|
4
9
|
import { createDeterministicJudgeStub, createFailClosedJudge, createOllamaJudge, createOpenAiCompatibleJudge, } from './judge.js';
|
|
10
|
+
import { getJudgeProviderSpec, inferProviderIdFromConfig, isRemovedProviderId, normalizeLegacyProviderId, } from './judge-catalog.js';
|
|
11
|
+
import { createClaudeCliJudge, createCodexCliJudge, createCursorCliJudge } from './judge-cli.js';
|
|
5
12
|
const FIXTURE_MODELS_URL = new URL('../../../fixtures/judge-models.json', import.meta.url);
|
|
6
13
|
let cachedPinnedModels = null;
|
|
7
14
|
export function resetPinnedJudgeModelsCache() {
|
|
@@ -24,33 +31,99 @@ export async function loadPinnedJudgeModels() {
|
|
|
24
31
|
return cachedPinnedModels;
|
|
25
32
|
}
|
|
26
33
|
}
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
export function resolveJudgeModel(judge) {
|
|
35
|
+
rejectDeprecatedJudgeModelAuto(judge.model);
|
|
36
|
+
const requested = judge.model;
|
|
37
|
+
const envOverride = testJudgeModelOverride();
|
|
38
|
+
return { requested, resolved: envOverride || requested };
|
|
39
|
+
}
|
|
40
|
+
function testJudgeModelOverride() {
|
|
41
|
+
if (!process.env.VITEST && !process.env.VITEST_WORKER_ID) {
|
|
42
|
+
return undefined;
|
|
34
43
|
}
|
|
35
|
-
return
|
|
44
|
+
return process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim() || undefined;
|
|
36
45
|
}
|
|
37
|
-
/** @deprecated Use
|
|
46
|
+
/** @deprecated Use resolveJudgeModel */
|
|
47
|
+
export const resolveCloudModel = (requested, _pinned) => {
|
|
48
|
+
rejectDeprecatedJudgeModelAuto(requested);
|
|
49
|
+
const envResolved = testJudgeModelOverride();
|
|
50
|
+
return {
|
|
51
|
+
requested,
|
|
52
|
+
resolved: envResolved || requested,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/** @deprecated Use resolveJudgeModel */
|
|
38
56
|
export const resolveCursorModel = resolveCloudModel;
|
|
57
|
+
function providerIdFromJudge(judge) {
|
|
58
|
+
if (judge.providerId) {
|
|
59
|
+
const normalized = normalizeLegacyProviderId(judge.providerId);
|
|
60
|
+
if (normalized) {
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return inferProviderIdFromConfig(judge);
|
|
65
|
+
}
|
|
66
|
+
function createCliJudgeForProvider(providerId, judgeConfig, config) {
|
|
67
|
+
const { resolved } = resolveJudgeModel(judgeConfig);
|
|
68
|
+
const options = {
|
|
69
|
+
modelRequested: judgeConfig.model,
|
|
70
|
+
modelResolved: resolved,
|
|
71
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
72
|
+
sensitivePaths: config.classifier.sensitivePaths,
|
|
73
|
+
scrubOptions: scrubOptionsFromConfig(config),
|
|
74
|
+
};
|
|
75
|
+
if (providerId === 'codex') {
|
|
76
|
+
return createCodexCliJudge(options);
|
|
77
|
+
}
|
|
78
|
+
if (providerId === 'cursor') {
|
|
79
|
+
return createCursorCliJudge(options);
|
|
80
|
+
}
|
|
81
|
+
return createClaudeCliJudge(options);
|
|
82
|
+
}
|
|
39
83
|
export function createJudgeFromConfig(config, options = {}) {
|
|
84
|
+
if (process.env.BELAY_DETERMINISTIC_JUDGE === '1') {
|
|
85
|
+
return createDeterministicJudgeStub();
|
|
86
|
+
}
|
|
40
87
|
const judgeConfig = config.judge;
|
|
88
|
+
if (judgeConfig.providerId && isRemovedProviderId(String(judgeConfig.providerId))) {
|
|
89
|
+
const { resolved } = resolveJudgeModel(judgeConfig);
|
|
90
|
+
return createFailClosedJudge({
|
|
91
|
+
reason: 'judge_provider_removed',
|
|
92
|
+
fallbackReason: 'provider_migration_required',
|
|
93
|
+
modelRequested: judgeConfig.model,
|
|
94
|
+
modelResolved: resolved,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
41
97
|
const provider = normalizeJudgeProvider(judgeConfig.provider);
|
|
98
|
+
const providerId = providerIdFromJudge(judgeConfig);
|
|
99
|
+
const catalogSpec = getJudgeProviderSpec(providerId);
|
|
100
|
+
const { resolved } = resolveJudgeModel(judgeConfig);
|
|
101
|
+
const runtime = detectJudgeRuntimeCapabilities(providerId);
|
|
42
102
|
if (provider === 'openai-compatible') {
|
|
43
103
|
const endpoint = judgeConfig.endpoint?.trim();
|
|
104
|
+
if (!endpoint && runtime.cliTransport) {
|
|
105
|
+
if (providerId === 'codex' || providerId === 'cursor') {
|
|
106
|
+
return createCliJudgeForProvider(providerId, judgeConfig, config);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
44
109
|
if (!endpoint) {
|
|
45
110
|
return createFailClosedJudge({
|
|
46
111
|
reason: 'openai_compatible_endpoint_missing',
|
|
47
112
|
fallbackReason: 'missing_endpoint',
|
|
48
113
|
modelRequested: judgeConfig.model,
|
|
49
|
-
modelResolved:
|
|
114
|
+
modelResolved: resolved,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (isCloudJudgeConfig(judgeConfig) && !hasValidCloudConsent(judgeConfig)) {
|
|
118
|
+
return createFailClosedJudge({
|
|
119
|
+
reason: 'openai_compatible_consent_missing',
|
|
120
|
+
fallbackReason: 'cloud_consent_missing',
|
|
121
|
+
modelRequested: judgeConfig.model,
|
|
122
|
+
modelResolved: resolved,
|
|
50
123
|
});
|
|
51
124
|
}
|
|
52
|
-
const
|
|
53
|
-
const
|
|
125
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
126
|
+
const repoLocalStateDir = repoLocalStateDirFor(repoRoot, config);
|
|
54
127
|
return createOpenAiCompatibleJudge({
|
|
55
128
|
endpoint,
|
|
56
129
|
modelRequested: judgeConfig.model,
|
|
@@ -58,6 +131,13 @@ export function createJudgeFromConfig(config, options = {}) {
|
|
|
58
131
|
timeoutMs: judgeConfig.timeoutMs,
|
|
59
132
|
sensitivePaths: config.classifier.sensitivePaths,
|
|
60
133
|
scrubOptions: scrubOptionsFromConfig(config),
|
|
134
|
+
resolveApiKey: async () => resolveJudgeCredential({
|
|
135
|
+
judge: judgeConfig,
|
|
136
|
+
catalogSpec: catalogSpec ?? undefined,
|
|
137
|
+
repoRoot,
|
|
138
|
+
repoLocalStateDir,
|
|
139
|
+
config,
|
|
140
|
+
}),
|
|
61
141
|
});
|
|
62
142
|
}
|
|
63
143
|
if (provider === 'ollama') {
|
|
@@ -68,8 +148,31 @@ export function createJudgeFromConfig(config, options = {}) {
|
|
|
68
148
|
keepAlive: judgeConfig.keepAlive,
|
|
69
149
|
});
|
|
70
150
|
}
|
|
71
|
-
|
|
151
|
+
if (provider === 'anthropic') {
|
|
152
|
+
if (runtime.cliTransport) {
|
|
153
|
+
return createClaudeCliJudge({
|
|
154
|
+
modelRequested: judgeConfig.model,
|
|
155
|
+
modelResolved: resolved,
|
|
156
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
157
|
+
sensitivePaths: config.classifier.sensitivePaths,
|
|
158
|
+
scrubOptions: scrubOptionsFromConfig(config),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return createFailClosedJudge({
|
|
162
|
+
reason: 'anthropic_not_implemented',
|
|
163
|
+
fallbackReason: 'anthropic_runtime_unavailable',
|
|
164
|
+
modelRequested: judgeConfig.model,
|
|
165
|
+
modelResolved: resolved,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return createFailClosedJudge({
|
|
169
|
+
reason: 'unsupported_judge_provider',
|
|
170
|
+
fallbackReason: 'unsupported_provider',
|
|
171
|
+
modelRequested: judgeConfig.model,
|
|
172
|
+
modelResolved: resolved,
|
|
173
|
+
});
|
|
72
174
|
}
|
|
73
175
|
export function judgeConfigSummary(judge) {
|
|
74
|
-
|
|
176
|
+
const id = judge.providerId ?? judge.provider;
|
|
177
|
+
return `${id}/${judge.model}`;
|
|
75
178
|
}
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
import type { ScrubOptions } from '../types.js';
|
|
2
2
|
import type { Tier1Judge, Tier1Verdict } from './types.js';
|
|
3
|
+
export type Tier1JudgeTransport = 'http' | 'ollama-http' | 'codex-cli' | 'cursor-cli' | 'claude-cli' | 'unavailable';
|
|
3
4
|
export interface Tier1JudgeTrace {
|
|
4
|
-
provider: 'openai-compatible' | 'ollama' | 'fallback';
|
|
5
|
+
provider: 'openai-compatible' | 'ollama' | 'anthropic' | 'fallback';
|
|
5
6
|
modelRequested: string;
|
|
6
7
|
modelResolved: string;
|
|
7
8
|
latencyMs: number;
|
|
9
|
+
transport?: Tier1JudgeTransport;
|
|
8
10
|
outboundRedacted?: boolean;
|
|
9
11
|
fallbackReason?: string;
|
|
10
12
|
}
|
|
11
13
|
export interface TracedTier1Judge extends Tier1Judge {
|
|
12
14
|
lastTrace?: Tier1JudgeTrace;
|
|
13
15
|
}
|
|
16
|
+
export declare function buildTier1Prompt(text: string): string;
|
|
17
|
+
export declare function parseTier1Json(raw: string): Tier1Verdict | null;
|
|
14
18
|
export declare function prescanInterpreterCode(code: string): Tier1Verdict | null;
|
|
19
|
+
export interface MutationPrescanParams {
|
|
20
|
+
targets: string[];
|
|
21
|
+
cwd: string;
|
|
22
|
+
repoRoot: string;
|
|
23
|
+
trustedCwd: boolean;
|
|
24
|
+
sensitivePaths: string[];
|
|
25
|
+
}
|
|
26
|
+
/** ADR-002 M3: structural prescan for sensitive / persistent mutation targets (shell redirects, etc.). */
|
|
27
|
+
export declare function prescanMutationTargets(params: MutationPrescanParams): Tier1Verdict | null;
|
|
28
|
+
/** Returns prescan verdict when structural M3 rules require ask (before Tier1 LLM). */
|
|
29
|
+
export declare function mutationPrescanRequiresAsk(params: MutationPrescanParams): Tier1Verdict | null;
|
|
15
30
|
/** Conservative stub: Tier1 defers to Tier0; returns safe negatives for structural suite. */
|
|
16
31
|
export declare function createDeterministicJudgeStub(): TracedTier1Judge;
|
|
17
32
|
/** Fail-closed judge for when Tier1 is required but unavailable. */
|
|
@@ -35,6 +50,10 @@ export interface OpenAiCompatibleJudgeOptions {
|
|
|
35
50
|
modelResolved: string;
|
|
36
51
|
timeoutMs: number;
|
|
37
52
|
apiKey?: string;
|
|
53
|
+
resolveApiKey?: () => Promise<{
|
|
54
|
+
key: string | null;
|
|
55
|
+
source: string | null;
|
|
56
|
+
}>;
|
|
38
57
|
sensitivePaths: string[];
|
|
39
58
|
scrubOptions: ScrubOptions;
|
|
40
59
|
fetchImpl?: typeof fetch;
|
|
@@ -1,34 +1,60 @@
|
|
|
1
|
+
import { matchesSensitivePath } from '../glob.js';
|
|
2
|
+
import { relativeWithinRepo, resolveMutationTarget } from '../path-utils.js';
|
|
3
|
+
import { resolveTrustedPath } from './containment.js';
|
|
1
4
|
import { scrubOutboundForJudge } from './judge-outbound.js';
|
|
5
|
+
import { isOutsideRepoSecretCredentialPath, isPersistentAgentPath } from './persistent-paths.js';
|
|
2
6
|
const SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
|
|
3
7
|
const DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
|
|
4
8
|
const GIT_PATTERNS = /\.git\b/i;
|
|
5
|
-
const TIER1_PROMPT = `Answer ONLY with JSON: {"
|
|
9
|
+
const TIER1_PROMPT = `Answer ONLY with JSON: {"local_recoverable":boolean,"destroys_history_or_secrets":boolean,"reason":string}
|
|
6
10
|
reason should be a short snake_case label.
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
local_recoverable: true if a mistaken operation can be undone locally (git revert, fs snapshot, trivial manual revert).
|
|
13
|
+
local_recoverable: false if remote/destructive (git push --force, aws s3 rm, secret send) OR persistent local harm (~/.ssh/authorized_keys, shell rc, crontab, ~/.config persistence).
|
|
14
|
+
|
|
15
|
+
destroys_history_or_secrets: true if the operation deletes or corrupts git history (.git), overwrites secrets/credentials, or modifies persistent agent startup (shell rc, crontab, launchd, authorized_keys).
|
|
16
|
+
|
|
17
|
+
Examples local_recoverable=true, destroys_history_or_secrets=false: IDE plan files under the user home, /tmp files, in-repo edits, READ/QUERY/GET.
|
|
18
|
+
Examples local_recoverable=false OR destroys_history_or_secrets=true: remote mutations, credential paths, agent startup persistence, rm -rf .git.
|
|
19
|
+
If genuinely ambiguous about recoverability or persistent harm, answer false.
|
|
11
20
|
|
|
12
21
|
Command/code:
|
|
13
22
|
`;
|
|
14
23
|
function failClosedVerdict(reason) {
|
|
15
24
|
return {
|
|
16
|
-
|
|
25
|
+
local_recoverable: false,
|
|
17
26
|
destroys_outside_repo: true,
|
|
18
27
|
destroys_history_or_secrets: true,
|
|
28
|
+
external_change: true,
|
|
19
29
|
reason,
|
|
20
30
|
};
|
|
21
31
|
}
|
|
22
|
-
function
|
|
32
|
+
function parseLocalRecoverable(parsed) {
|
|
33
|
+
if (typeof parsed.local_recoverable === 'boolean') {
|
|
34
|
+
return parsed.local_recoverable;
|
|
35
|
+
}
|
|
36
|
+
if (typeof parsed.external_change === 'boolean') {
|
|
37
|
+
return !parsed.external_change;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
export function buildTier1Prompt(text) {
|
|
42
|
+
return `${TIER1_PROMPT}${text}`;
|
|
43
|
+
}
|
|
44
|
+
export function parseTier1Json(raw) {
|
|
23
45
|
try {
|
|
24
46
|
const parsed = JSON.parse(raw);
|
|
25
|
-
|
|
47
|
+
const localRecoverable = parseLocalRecoverable(parsed);
|
|
48
|
+
if (localRecoverable === null) {
|
|
26
49
|
return null;
|
|
27
50
|
}
|
|
28
51
|
return {
|
|
52
|
+
local_recoverable: localRecoverable,
|
|
53
|
+
destroys_outside_repo: typeof parsed.destroys_outside_repo === 'boolean' ? parsed.destroys_outside_repo : false,
|
|
54
|
+
destroys_history_or_secrets: typeof parsed.destroys_history_or_secrets === 'boolean'
|
|
55
|
+
? parsed.destroys_history_or_secrets
|
|
56
|
+
: false,
|
|
29
57
|
external_change: parsed.external_change,
|
|
30
|
-
destroys_outside_repo: false,
|
|
31
|
-
destroys_history_or_secrets: false,
|
|
32
58
|
reason: typeof parsed.reason === 'string' ? parsed.reason : 'tier1_llm',
|
|
33
59
|
};
|
|
34
60
|
}
|
|
@@ -43,7 +69,7 @@ export function prescanInterpreterCode(code) {
|
|
|
43
69
|
const hitsDestructive = DESTRUCTIVE_VERBS.test(normalized);
|
|
44
70
|
if ((hitsSecret || hitsGit) && hitsDestructive) {
|
|
45
71
|
return {
|
|
46
|
-
|
|
72
|
+
local_recoverable: true,
|
|
47
73
|
destroys_outside_repo: false,
|
|
48
74
|
destroys_history_or_secrets: true,
|
|
49
75
|
reason: 'prescan_destructive_secret',
|
|
@@ -51,12 +77,54 @@ export function prescanInterpreterCode(code) {
|
|
|
51
77
|
}
|
|
52
78
|
return null;
|
|
53
79
|
}
|
|
80
|
+
/** ADR-002 M3: structural prescan for sensitive / persistent mutation targets (shell redirects, etc.). */
|
|
81
|
+
export function prescanMutationTargets(params) {
|
|
82
|
+
for (const target of params.targets) {
|
|
83
|
+
const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ??
|
|
84
|
+
resolveMutationTarget(target, params.cwd);
|
|
85
|
+
if (!resolved) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const relative = relativeWithinRepo(params.repoRoot, resolved);
|
|
89
|
+
if (relative !== null &&
|
|
90
|
+
matchesSensitivePath(relative.replaceAll('\\', '/'), params.sensitivePaths)) {
|
|
91
|
+
return {
|
|
92
|
+
local_recoverable: false,
|
|
93
|
+
destroys_outside_repo: false,
|
|
94
|
+
destroys_history_or_secrets: true,
|
|
95
|
+
reason: 'sensitive_path_mutation',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (relative === null && isOutsideRepoSecretCredentialPath(resolved)) {
|
|
99
|
+
return {
|
|
100
|
+
local_recoverable: false,
|
|
101
|
+
destroys_outside_repo: false,
|
|
102
|
+
destroys_history_or_secrets: true,
|
|
103
|
+
reason: 'outside_repo_secret_credential_path',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (isPersistentAgentPath(resolved)) {
|
|
107
|
+
return {
|
|
108
|
+
local_recoverable: false,
|
|
109
|
+
destroys_outside_repo: false,
|
|
110
|
+
destroys_history_or_secrets: true,
|
|
111
|
+
reason: 'persistent_agent_path',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
/** Returns prescan verdict when structural M3 rules require ask (before Tier1 LLM). */
|
|
118
|
+
export function mutationPrescanRequiresAsk(params) {
|
|
119
|
+
const prescan = prescanMutationTargets(params);
|
|
120
|
+
return prescan && tier1RequiresAsk(prescan) ? prescan : null;
|
|
121
|
+
}
|
|
54
122
|
/** Conservative stub: Tier1 defers to Tier0; returns safe negatives for structural suite. */
|
|
55
123
|
export function createDeterministicJudgeStub() {
|
|
56
124
|
return {
|
|
57
125
|
evaluate() {
|
|
58
126
|
return Promise.resolve({
|
|
59
|
-
|
|
127
|
+
local_recoverable: true,
|
|
60
128
|
destroys_outside_repo: false,
|
|
61
129
|
destroys_history_or_secrets: false,
|
|
62
130
|
reason: 'deterministic_stub',
|
|
@@ -190,8 +258,9 @@ export function createOpenAiCompatibleJudge(options) {
|
|
|
190
258
|
};
|
|
191
259
|
return failClosedVerdict('outbound_scrub_failed');
|
|
192
260
|
}
|
|
193
|
-
const
|
|
194
|
-
|
|
261
|
+
const resolvedKey = options.resolveApiKey
|
|
262
|
+
? await options.resolveApiKey()
|
|
263
|
+
: { key: options.apiKey ?? null, source: null };
|
|
195
264
|
const apiKey = options.apiKey ?? resolvedKey.key;
|
|
196
265
|
if (!apiKey) {
|
|
197
266
|
judge.lastTrace = {
|
|
@@ -258,5 +327,5 @@ export function createOpenAiCompatibleJudge(options) {
|
|
|
258
327
|
/** @deprecated Use createOpenAiCompatibleJudge */
|
|
259
328
|
export const createCursorJudge = createOpenAiCompatibleJudge;
|
|
260
329
|
export function tier1RequiresAsk(verdict) {
|
|
261
|
-
return verdict.
|
|
330
|
+
return !verdict.local_recoverable || verdict.destroys_history_or_secrets;
|
|
262
331
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M3 catalog: paths that survive reboot or change agent startup.
|
|
3
|
+
* @see docs/adr/tier0-retention-ledger.md
|
|
4
|
+
*/
|
|
5
|
+
export declare const PERSISTENT_AGENT_PATH_MARKERS: readonly [".ssh/authorized_keys", ".zshrc", ".bashrc", ".bash_profile", ".profile", ".config/", "/crontab", "LaunchAgents/", "/Library/LaunchDaemons/"];
|
|
6
|
+
export declare const OUTSIDE_REPO_SECRET_CREDENTIAL_MARKERS: readonly [".env", ".env.*", "*.pem", ".ssh/id_*", ".aws/", ".npmrc", ".git-credentials", ".netrc", ".kube/config", ".docker/config.json", ".gnupg/", ".pypirc"];
|
|
7
|
+
export declare function isPersistentAgentPath(resolvedPath: string): boolean;
|
|
8
|
+
export declare function isOutsideRepoSecretCredentialPath(resolvedPath: string): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M3 catalog: paths that survive reboot or change agent startup.
|
|
3
|
+
* @see docs/adr/tier0-retention-ledger.md
|
|
4
|
+
*/
|
|
5
|
+
export const PERSISTENT_AGENT_PATH_MARKERS = [
|
|
6
|
+
'.ssh/authorized_keys',
|
|
7
|
+
'.zshrc',
|
|
8
|
+
'.bashrc',
|
|
9
|
+
'.bash_profile',
|
|
10
|
+
'.profile',
|
|
11
|
+
'.config/',
|
|
12
|
+
'/crontab',
|
|
13
|
+
'LaunchAgents/',
|
|
14
|
+
'/Library/LaunchDaemons/',
|
|
15
|
+
];
|
|
16
|
+
export const OUTSIDE_REPO_SECRET_CREDENTIAL_MARKERS = [
|
|
17
|
+
'.env',
|
|
18
|
+
'.env.*',
|
|
19
|
+
'*.pem',
|
|
20
|
+
'.ssh/id_*',
|
|
21
|
+
'.aws/',
|
|
22
|
+
'.npmrc',
|
|
23
|
+
'.git-credentials',
|
|
24
|
+
'.netrc',
|
|
25
|
+
'.kube/config',
|
|
26
|
+
'.docker/config.json',
|
|
27
|
+
'.gnupg/',
|
|
28
|
+
'.pypirc',
|
|
29
|
+
];
|
|
30
|
+
export function isPersistentAgentPath(resolvedPath) {
|
|
31
|
+
const normalized = resolvedPath.replaceAll('\\', '/');
|
|
32
|
+
return (/\.ssh\/authorized_keys$/i.test(normalized) ||
|
|
33
|
+
/\/\.(zshrc|bashrc|bash_profile|profile)$/i.test(normalized) ||
|
|
34
|
+
/\/\.config\//i.test(normalized) ||
|
|
35
|
+
/\/crontab$/i.test(normalized) ||
|
|
36
|
+
/LaunchAgents\//i.test(normalized) ||
|
|
37
|
+
/\/Library\/LaunchDaemons\//i.test(normalized));
|
|
38
|
+
}
|
|
39
|
+
export function isOutsideRepoSecretCredentialPath(resolvedPath) {
|
|
40
|
+
const normalized = resolvedPath.replaceAll('\\', '/');
|
|
41
|
+
return (/\/\.env(?:\.[^/]+)?$/i.test(normalized) ||
|
|
42
|
+
/\/[^/]+\.pem$/i.test(normalized) ||
|
|
43
|
+
/\/\.ssh\/id_[^/]+$/i.test(normalized) ||
|
|
44
|
+
/\/\.aws\//i.test(normalized) ||
|
|
45
|
+
/\/\.npmrc$/i.test(normalized) ||
|
|
46
|
+
/\/\.git-credentials$/i.test(normalized) ||
|
|
47
|
+
/\/\.netrc$/i.test(normalized) ||
|
|
48
|
+
/\/\.kube\/config$/i.test(normalized) ||
|
|
49
|
+
/\/\.docker\/config\.json$/i.test(normalized) ||
|
|
50
|
+
/\/\.gnupg\//i.test(normalized) ||
|
|
51
|
+
/\/\.pypirc$/i.test(normalized));
|
|
52
|
+
}
|
|
@@ -16,10 +16,14 @@ export interface VerdictResult {
|
|
|
16
16
|
judgeTrace?: JudgeTrace;
|
|
17
17
|
}
|
|
18
18
|
export interface Tier1Verdict {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/** true = undoable via git/fs snapshot or trivial local revert (ADR-002 local-recoverable). */
|
|
20
|
+
local_recoverable: boolean;
|
|
21
21
|
destroys_history_or_secrets: boolean;
|
|
22
22
|
reason: string;
|
|
23
|
+
/** @deprecated Legacy LLM field; parsed only for migration. */
|
|
24
|
+
external_change?: boolean;
|
|
25
|
+
/** @deprecated Unused in decision logic. */
|
|
26
|
+
destroys_outside_repo?: boolean;
|
|
23
27
|
}
|
|
24
28
|
export interface Tier1EvaluateInput {
|
|
25
29
|
text: string;
|