@guilz-dev/belay 0.1.1 → 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 +55 -15
- package/dist/adapters/shared/gate-runtime.js +25 -9
- package/dist/bundle/claude-runtime.mjs +1786 -379
- package/dist/bundle/codex-runtime.mjs +1785 -379
- package/dist/bundle/cursor-runtime.mjs +1786 -379
- package/dist/cli.js +201 -11
- package/dist/commands/approve.js +11 -0
- package/dist/commands/classify-for-report.js +3 -3
- package/dist/commands/config.d.ts +70 -0
- package/dist/commands/config.js +494 -0
- package/dist/commands/doctor.js +3 -2
- package/dist/commands/explain.js +14 -14
- 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/recover.js +2 -2
- 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/approval.d.ts +3 -0
- package/dist/core/approval.js +18 -3
- package/dist/core/audit-io.d.ts +2 -0
- package/dist/core/audit-io.js +14 -0
- package/dist/core/audit-query.js +5 -1
- 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 +23 -3
- package/dist/core/config.js +112 -13
- package/dist/core/credential-store.d.ts +11 -0
- package/dist/core/credential-store.js +60 -0
- package/dist/core/gate-contract.d.ts +1 -1
- package/dist/core/gate-contract.js +1 -1
- package/dist/core/gate-engine.js +105 -14
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- 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 +45 -4
- package/dist/core/judge-config.js +266 -44
- package/dist/core/judge-doctor.d.ts +6 -1
- package/dist/core/judge-doctor.js +148 -97
- 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 +7 -3
- package/dist/core/{v2 → verdict}/adapter.d.ts +1 -0
- package/dist/core/{v2 → verdict}/adapter.js +16 -4
- package/dist/core/{v2 → verdict}/containment.d.ts +5 -0
- package/dist/core/{v2 → verdict}/containment.js +32 -2
- package/dist/core/{v2 → verdict}/egress-classify.js +3 -0
- 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/{v2 → verdict}/judge-factory.d.ts +15 -4
- package/dist/core/verdict/judge-factory.js +178 -0
- package/dist/core/{v2 → verdict}/judge.d.ts +20 -1
- package/dist/core/{v2 → verdict}/judge.js +85 -18
- package/dist/core/{v2 → verdict}/launcher-resolve.js +72 -1
- package/dist/core/verdict/persistent-paths.d.ts +8 -0
- package/dist/core/verdict/persistent-paths.js +52 -0
- package/dist/core/{v2 → verdict}/types.d.ts +6 -2
- package/dist/core/{v2 → verdict}/verdict.js +176 -46
- package/dist/corpus/evaluate.js +2 -2
- package/dist/installer.js +66 -14
- package/dist/types.d.ts +8 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/skills/belay/SKILL.md +8 -7
- package/dist/commands/init-wizard.d.ts +0 -16
- package/dist/commands/init-wizard.js +0 -50
- package/dist/core/v2/judge-factory.js +0 -75
- /package/dist/core/{v2 → verdict}/egress-classify.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/fingerprint.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/fingerprint.js +0 -0
- /package/dist/core/{v2 → verdict}/index.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/index.js +0 -0
- /package/dist/core/{v2 → verdict}/judge-audit.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/judge-audit.js +0 -0
- /package/dist/core/{v2 → verdict}/judge-outbound.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/judge-outbound.js +0 -0
- /package/dist/core/{v2 → verdict}/launcher-resolve.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/overrides.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/overrides.js +0 -0
- /package/dist/core/{v2 → verdict}/parser.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/parser.js +0 -0
- /package/dist/core/{v2 → verdict}/types.js +0 -0
- /package/dist/core/{v2 → verdict}/verdict.d.ts +0 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { repoLocalStateDirFor } from '../../config-io.js';
|
|
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';
|
|
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';
|
|
12
|
+
const FIXTURE_MODELS_URL = new URL('../../../fixtures/judge-models.json', import.meta.url);
|
|
13
|
+
let cachedPinnedModels = null;
|
|
14
|
+
export function resetPinnedJudgeModelsCache() {
|
|
15
|
+
cachedPinnedModels = null;
|
|
16
|
+
}
|
|
17
|
+
export async function loadPinnedJudgeModels() {
|
|
18
|
+
if (cachedPinnedModels) {
|
|
19
|
+
return cachedPinnedModels;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const raw = await readFile(fileURLToPath(FIXTURE_MODELS_URL), 'utf8');
|
|
23
|
+
cachedPinnedModels = JSON.parse(raw);
|
|
24
|
+
return cachedPinnedModels;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
cachedPinnedModels = {
|
|
28
|
+
'openai-compatible': { autoResolved: 'composer-2.5' },
|
|
29
|
+
ollama: { ciPin: 'gemma4:e2b' },
|
|
30
|
+
};
|
|
31
|
+
return cachedPinnedModels;
|
|
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;
|
|
43
|
+
}
|
|
44
|
+
return process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim() || undefined;
|
|
45
|
+
}
|
|
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 */
|
|
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
|
+
}
|
|
83
|
+
export function createJudgeFromConfig(config, options = {}) {
|
|
84
|
+
if (process.env.BELAY_DETERMINISTIC_JUDGE === '1') {
|
|
85
|
+
return createDeterministicJudgeStub();
|
|
86
|
+
}
|
|
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
|
+
}
|
|
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);
|
|
102
|
+
if (provider === 'openai-compatible') {
|
|
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
|
+
}
|
|
109
|
+
if (!endpoint) {
|
|
110
|
+
return createFailClosedJudge({
|
|
111
|
+
reason: 'openai_compatible_endpoint_missing',
|
|
112
|
+
fallbackReason: 'missing_endpoint',
|
|
113
|
+
modelRequested: judgeConfig.model,
|
|
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,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
126
|
+
const repoLocalStateDir = repoLocalStateDirFor(repoRoot, config);
|
|
127
|
+
return createOpenAiCompatibleJudge({
|
|
128
|
+
endpoint,
|
|
129
|
+
modelRequested: judgeConfig.model,
|
|
130
|
+
modelResolved: resolved,
|
|
131
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
132
|
+
sensitivePaths: config.classifier.sensitivePaths,
|
|
133
|
+
scrubOptions: scrubOptionsFromConfig(config),
|
|
134
|
+
resolveApiKey: async () => resolveJudgeCredential({
|
|
135
|
+
judge: judgeConfig,
|
|
136
|
+
catalogSpec: catalogSpec ?? undefined,
|
|
137
|
+
repoRoot,
|
|
138
|
+
repoLocalStateDir,
|
|
139
|
+
config,
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (provider === 'ollama') {
|
|
144
|
+
return createOllamaJudge({
|
|
145
|
+
model: judgeConfig.model,
|
|
146
|
+
baseUrl: judgeConfig.endpoint ?? 'http://127.0.0.1:11434',
|
|
147
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
148
|
+
keepAlive: judgeConfig.keepAlive,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
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
|
+
});
|
|
174
|
+
}
|
|
175
|
+
export function judgeConfigSummary(judge) {
|
|
176
|
+
const id = judge.providerId ?? judge.provider;
|
|
177
|
+
return `${id}/${judge.model}`;
|
|
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,36 +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: {"
|
|
6
|
-
|
|
9
|
+
const TIER1_PROMPT = `Answer ONLY with JSON: {"local_recoverable":boolean,"destroys_history_or_secrets":boolean,"reason":string}
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
typeof parsed.destroys_history_or_secrets !== 'boolean') {
|
|
47
|
+
const localRecoverable = parseLocalRecoverable(parsed);
|
|
48
|
+
if (localRecoverable === null) {
|
|
28
49
|
return null;
|
|
29
50
|
}
|
|
30
51
|
return {
|
|
31
|
-
|
|
32
|
-
destroys_outside_repo: parsed.destroys_outside_repo
|
|
33
|
-
destroys_history_or_secrets: parsed.destroys_history_or_secrets
|
|
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,
|
|
57
|
+
external_change: parsed.external_change,
|
|
34
58
|
reason: typeof parsed.reason === 'string' ? parsed.reason : 'tier1_llm',
|
|
35
59
|
};
|
|
36
60
|
}
|
|
@@ -45,7 +69,7 @@ export function prescanInterpreterCode(code) {
|
|
|
45
69
|
const hitsDestructive = DESTRUCTIVE_VERBS.test(normalized);
|
|
46
70
|
if ((hitsSecret || hitsGit) && hitsDestructive) {
|
|
47
71
|
return {
|
|
48
|
-
|
|
72
|
+
local_recoverable: true,
|
|
49
73
|
destroys_outside_repo: false,
|
|
50
74
|
destroys_history_or_secrets: true,
|
|
51
75
|
reason: 'prescan_destructive_secret',
|
|
@@ -53,12 +77,54 @@ export function prescanInterpreterCode(code) {
|
|
|
53
77
|
}
|
|
54
78
|
return null;
|
|
55
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
|
+
}
|
|
56
122
|
/** Conservative stub: Tier1 defers to Tier0; returns safe negatives for structural suite. */
|
|
57
123
|
export function createDeterministicJudgeStub() {
|
|
58
124
|
return {
|
|
59
125
|
evaluate() {
|
|
60
126
|
return Promise.resolve({
|
|
61
|
-
|
|
127
|
+
local_recoverable: true,
|
|
62
128
|
destroys_outside_repo: false,
|
|
63
129
|
destroys_history_or_secrets: false,
|
|
64
130
|
reason: 'deterministic_stub',
|
|
@@ -192,8 +258,9 @@ export function createOpenAiCompatibleJudge(options) {
|
|
|
192
258
|
};
|
|
193
259
|
return failClosedVerdict('outbound_scrub_failed');
|
|
194
260
|
}
|
|
195
|
-
const
|
|
196
|
-
|
|
261
|
+
const resolvedKey = options.resolveApiKey
|
|
262
|
+
? await options.resolveApiKey()
|
|
263
|
+
: { key: options.apiKey ?? null, source: null };
|
|
197
264
|
const apiKey = options.apiKey ?? resolvedKey.key;
|
|
198
265
|
if (!apiKey) {
|
|
199
266
|
judge.lastTrace = {
|
|
@@ -260,5 +327,5 @@ export function createOpenAiCompatibleJudge(options) {
|
|
|
260
327
|
/** @deprecated Use createOpenAiCompatibleJudge */
|
|
261
328
|
export const createCursorJudge = createOpenAiCompatibleJudge;
|
|
262
329
|
export function tier1RequiresAsk(verdict) {
|
|
263
|
-
return
|
|
330
|
+
return !verdict.local_recoverable || verdict.destroys_history_or_secrets;
|
|
264
331
|
}
|
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const MAX_RESOLVE_DEPTH = 8;
|
|
4
|
+
const PNPM_BUILTIN_COMMANDS = new Set([
|
|
5
|
+
'add',
|
|
6
|
+
'audit',
|
|
7
|
+
'cache',
|
|
8
|
+
'config',
|
|
9
|
+
'deploy',
|
|
10
|
+
'dlx',
|
|
11
|
+
'exec',
|
|
12
|
+
'fetch',
|
|
13
|
+
'help',
|
|
14
|
+
'import',
|
|
15
|
+
'init',
|
|
16
|
+
'install',
|
|
17
|
+
'i',
|
|
18
|
+
'licenses',
|
|
19
|
+
'link',
|
|
20
|
+
'list',
|
|
21
|
+
'outdated',
|
|
22
|
+
'pack',
|
|
23
|
+
'patch',
|
|
24
|
+
'patch-commit',
|
|
25
|
+
'patch-remove',
|
|
26
|
+
'publish',
|
|
27
|
+
'prune',
|
|
28
|
+
'rebuild',
|
|
29
|
+
'remove',
|
|
30
|
+
'rm',
|
|
31
|
+
'store',
|
|
32
|
+
'unlink',
|
|
33
|
+
'update',
|
|
34
|
+
'up',
|
|
35
|
+
'why',
|
|
36
|
+
]);
|
|
37
|
+
const PNPM_EXEC_LIKE_HEADS = new Set([
|
|
38
|
+
'vitest',
|
|
39
|
+
'vite',
|
|
40
|
+
'biome',
|
|
41
|
+
'eslint',
|
|
42
|
+
'jest',
|
|
43
|
+
'mocha',
|
|
44
|
+
'tsc',
|
|
45
|
+
'tsx',
|
|
46
|
+
'node',
|
|
47
|
+
]);
|
|
4
48
|
function readPackageJson(dir) {
|
|
5
49
|
const packagePath = path.join(dir, 'package.json');
|
|
6
50
|
if (!existsSync(packagePath)) {
|
|
@@ -57,6 +101,15 @@ function npmScriptName(tokens) {
|
|
|
57
101
|
if (launcher[0] === 'pnpm' && launcher[1] === 'run' && launcher[2]) {
|
|
58
102
|
return launcher[2];
|
|
59
103
|
}
|
|
104
|
+
if (launcher[0] === 'pnpm' && launcher[1] === 'test') {
|
|
105
|
+
return 'test';
|
|
106
|
+
}
|
|
107
|
+
if (launcher[0] === 'pnpm' &&
|
|
108
|
+
launcher[1] &&
|
|
109
|
+
!launcher[1].startsWith('-') &&
|
|
110
|
+
!PNPM_BUILTIN_COMMANDS.has(launcher[1])) {
|
|
111
|
+
return launcher[1];
|
|
112
|
+
}
|
|
60
113
|
if (launcher[0] === 'npm' && launcher[1] && launcher[1] !== 'run' && launcher[1] !== 'install') {
|
|
61
114
|
return null;
|
|
62
115
|
}
|
|
@@ -176,11 +229,29 @@ export function resolveLauncherRecipe(params) {
|
|
|
176
229
|
const tokens = params.tokens;
|
|
177
230
|
const scriptName = npmScriptName(tokens);
|
|
178
231
|
if (scriptName) {
|
|
179
|
-
|
|
232
|
+
const resolution = resolveNpmRecipe(params.cwd, params.repoRoot, scriptName, forwardedArgs(tokens));
|
|
233
|
+
if (tokens[0] === 'pnpm' &&
|
|
234
|
+
tokens[1] &&
|
|
235
|
+
PNPM_EXEC_LIKE_HEADS.has(tokens[1]) &&
|
|
236
|
+
resolution.reason === 'npm_script_undefined') {
|
|
237
|
+
return {
|
|
238
|
+
recipes: [tokens.slice(1).join(' ')],
|
|
239
|
+
opaque: false,
|
|
240
|
+
reason: 'pnpm_exec_like',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return resolution;
|
|
180
244
|
}
|
|
181
245
|
if (tokens[0] === 'make' && tokens[1] && !tokens[1].startsWith('-')) {
|
|
182
246
|
return resolveMakeRecipe(params.cwd, params.repoRoot, tokens[1]);
|
|
183
247
|
}
|
|
248
|
+
if (tokens[0] === 'pnpm' && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1])) {
|
|
249
|
+
return {
|
|
250
|
+
recipes: [tokens.slice(1).join(' ')],
|
|
251
|
+
opaque: false,
|
|
252
|
+
reason: 'pnpm_exec_like',
|
|
253
|
+
};
|
|
254
|
+
}
|
|
184
255
|
return null;
|
|
185
256
|
}
|
|
186
257
|
export function isRoutineLauncher(tokens) {
|
|
@@ -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;
|