@guilz-dev/belay 0.1.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/LICENSE +21 -0
- package/README.md +268 -0
- package/agent-belay-logo.png +0 -0
- package/dist/adapters/claude/adapter.d.ts +7 -0
- package/dist/adapters/claude/adapter.js +114 -0
- package/dist/adapters/claude/hooks.d.ts +13 -0
- package/dist/adapters/claude/hooks.js +49 -0
- package/dist/adapters/claude/runtime-entry.d.ts +4 -0
- package/dist/adapters/claude/runtime-entry.js +260 -0
- package/dist/adapters/codex/adapter.d.ts +7 -0
- package/dist/adapters/codex/adapter.js +73 -0
- package/dist/adapters/codex/hooks.d.ts +21 -0
- package/dist/adapters/codex/hooks.js +78 -0
- package/dist/adapters/codex/runtime-entry.d.ts +4 -0
- package/dist/adapters/codex/runtime-entry.js +237 -0
- package/dist/adapters/cursor/adapter.d.ts +7 -0
- package/dist/adapters/cursor/adapter.js +29 -0
- package/dist/adapters/cursor/hooks.d.ts +2 -0
- package/dist/adapters/cursor/hooks.js +26 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +4 -0
- package/dist/adapters/cursor/runtime-entry.js +143 -0
- package/dist/adapters/layouts/claude.d.ts +2 -0
- package/dist/adapters/layouts/claude.js +40 -0
- package/dist/adapters/layouts/codex.d.ts +2 -0
- package/dist/adapters/layouts/codex.js +43 -0
- package/dist/adapters/layouts/cursor.d.ts +2 -0
- package/dist/adapters/layouts/cursor.js +40 -0
- package/dist/adapters/layouts/index.d.ts +7 -0
- package/dist/adapters/layouts/index.js +23 -0
- package/dist/adapters/layouts/protected-paths.d.ts +3 -0
- package/dist/adapters/layouts/protected-paths.js +15 -0
- package/dist/adapters/layouts/scope.d.ts +19 -0
- package/dist/adapters/layouts/scope.js +86 -0
- package/dist/adapters/layouts/types.d.ts +14 -0
- package/dist/adapters/layouts/types.js +1 -0
- package/dist/adapters/registry.d.ts +4 -0
- package/dist/adapters/registry.js +14 -0
- package/dist/adapters/shared/gate-runtime.d.ts +51 -0
- package/dist/adapters/shared/gate-runtime.js +518 -0
- package/dist/adapters/shared/repo-root.d.ts +2 -0
- package/dist/adapters/shared/repo-root.js +17 -0
- package/dist/adapters/types.d.ts +19 -0
- package/dist/adapters/types.js +1 -0
- package/dist/branding.d.ts +3 -0
- package/dist/branding.js +3 -0
- package/dist/bundle/claude-runtime.mjs +5323 -0
- package/dist/bundle/codex-runtime.mjs +5310 -0
- package/dist/bundle/cursor-runtime.mjs +5208 -0
- package/dist/cleanup-orphans.d.ts +7 -0
- package/dist/cleanup-orphans.js +59 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +631 -0
- package/dist/commands/approve.d.ts +14 -0
- package/dist/commands/approve.js +65 -0
- package/dist/commands/audit.d.ts +59 -0
- package/dist/commands/audit.js +132 -0
- package/dist/commands/classify-for-report.d.ts +9 -0
- package/dist/commands/classify-for-report.js +85 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +366 -0
- package/dist/commands/dogfood.d.ts +5 -0
- package/dist/commands/dogfood.js +71 -0
- package/dist/commands/explain.d.ts +3 -0
- package/dist/commands/explain.js +133 -0
- package/dist/commands/health-snapshot.d.ts +2 -0
- package/dist/commands/health-snapshot.js +166 -0
- package/dist/commands/init-wizard.d.ts +16 -0
- package/dist/commands/init-wizard.js +50 -0
- package/dist/commands/metrics.d.ts +7 -0
- package/dist/commands/metrics.js +89 -0
- package/dist/commands/recover.d.ts +3 -0
- package/dist/commands/recover.js +105 -0
- package/dist/commands/report.d.ts +3 -0
- package/dist/commands/report.js +65 -0
- package/dist/commands/revoke.d.ts +5 -0
- package/dist/commands/revoke.js +22 -0
- package/dist/commands/simulate.d.ts +14 -0
- package/dist/commands/simulate.js +55 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +107 -0
- package/dist/config-io.d.ts +23 -0
- package/dist/config-io.js +180 -0
- package/dist/conformance/guarantee-table.d.ts +14 -0
- package/dist/conformance/guarantee-table.js +95 -0
- package/dist/conformance/types.d.ts +6 -0
- package/dist/conformance/types.js +1 -0
- package/dist/core/approval-service.d.ts +26 -0
- package/dist/core/approval-service.js +41 -0
- package/dist/core/approval-token.d.ts +11 -0
- package/dist/core/approval-token.js +61 -0
- package/dist/core/approval.d.ts +19 -0
- package/dist/core/approval.js +58 -0
- package/dist/core/audit-analysis.d.ts +10 -0
- package/dist/core/audit-analysis.js +147 -0
- package/dist/core/audit-metrics.d.ts +51 -0
- package/dist/core/audit-metrics.js +155 -0
- package/dist/core/audit-query.d.ts +11 -0
- package/dist/core/audit-query.js +142 -0
- package/dist/core/audit-summary.d.ts +33 -0
- package/dist/core/audit-summary.js +111 -0
- package/dist/core/audit-types.d.ts +65 -0
- package/dist/core/audit-types.js +2 -0
- package/dist/core/capability/allowlist.d.ts +10 -0
- package/dist/core/capability/allowlist.js +53 -0
- package/dist/core/capability/broker.d.ts +17 -0
- package/dist/core/capability/broker.js +29 -0
- package/dist/core/capability/index.d.ts +5 -0
- package/dist/core/capability/index.js +4 -0
- package/dist/core/capability/paths.d.ts +1 -0
- package/dist/core/capability/paths.js +20 -0
- package/dist/core/capability/reasons.d.ts +2 -0
- package/dist/core/capability/reasons.js +4 -0
- package/dist/core/capability/types.d.ts +10 -0
- package/dist/core/capability/types.js +1 -0
- package/dist/core/capability-approval.d.ts +28 -0
- package/dist/core/capability-approval.js +43 -0
- package/dist/core/classify-subagent.d.ts +2 -0
- package/dist/core/classify-subagent.js +69 -0
- package/dist/core/classify-tool.d.ts +3 -0
- package/dist/core/classify-tool.js +311 -0
- package/dist/core/config-layers.d.ts +23 -0
- package/dist/core/config-layers.js +59 -0
- package/dist/core/config.d.ts +219 -0
- package/dist/core/config.js +720 -0
- package/dist/core/control-plane-isolation.d.ts +10 -0
- package/dist/core/control-plane-isolation.js +83 -0
- package/dist/core/custom-command-match.d.ts +2 -0
- package/dist/core/custom-command-match.js +8 -0
- package/dist/core/egress/allowlist.d.ts +7 -0
- package/dist/core/egress/allowlist.js +33 -0
- package/dist/core/egress/env.d.ts +3 -0
- package/dist/core/egress/env.js +17 -0
- package/dist/core/egress/fingerprint.d.ts +3 -0
- package/dist/core/egress/fingerprint.js +35 -0
- package/dist/core/egress/policy.d.ts +8 -0
- package/dist/core/egress/policy.js +47 -0
- package/dist/core/egress/proxy-server.d.ts +21 -0
- package/dist/core/egress/proxy-server.js +263 -0
- package/dist/core/egress/types.d.ts +25 -0
- package/dist/core/egress/types.js +1 -0
- package/dist/core/egress-approval.d.ts +48 -0
- package/dist/core/egress-approval.js +129 -0
- package/dist/core/fingerprint.d.ts +6 -0
- package/dist/core/fingerprint.js +24 -0
- package/dist/core/gate-contract.d.ts +48 -0
- package/dist/core/gate-contract.js +50 -0
- package/dist/core/gate-engine.d.ts +20 -0
- package/dist/core/gate-engine.js +172 -0
- package/dist/core/glob.d.ts +1 -0
- package/dist/core/glob.js +39 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +15 -0
- package/dist/core/integrity.d.ts +15 -0
- package/dist/core/integrity.js +68 -0
- package/dist/core/judge-api-key.d.ts +4 -0
- package/dist/core/judge-api-key.js +11 -0
- package/dist/core/judge-config.d.ts +29 -0
- package/dist/core/judge-config.js +85 -0
- package/dist/core/judge-doctor.d.ts +7 -0
- package/dist/core/judge-doctor.js +124 -0
- package/dist/core/judgment.d.ts +6 -0
- package/dist/core/judgment.js +38 -0
- package/dist/core/notify.d.ts +13 -0
- package/dist/core/notify.js +44 -0
- package/dist/core/path-utils.d.ts +12 -0
- package/dist/core/path-utils.js +107 -0
- package/dist/core/reclassify.d.ts +15 -0
- package/dist/core/reclassify.js +82 -0
- package/dist/core/recover-advice.d.ts +30 -0
- package/dist/core/recover-advice.js +177 -0
- package/dist/core/recover-git-probe.d.ts +8 -0
- package/dist/core/recover-git-probe.js +50 -0
- package/dist/core/recover-select.d.ts +10 -0
- package/dist/core/recover-select.js +60 -0
- package/dist/core/scrub.d.ts +3 -0
- package/dist/core/scrub.js +87 -0
- package/dist/core/shell-substitution.d.ts +6 -0
- package/dist/core/shell-substitution.js +130 -0
- package/dist/core/shell-tokenizer.d.ts +5 -0
- package/dist/core/shell-tokenizer.js +129 -0
- package/dist/core/shell-unparseable.d.ts +4 -0
- package/dist/core/shell-unparseable.js +96 -0
- package/dist/core/transactional/diff-evaluator.d.ts +2 -0
- package/dist/core/transactional/diff-evaluator.js +84 -0
- package/dist/core/transactional/eligibility.d.ts +4 -0
- package/dist/core/transactional/eligibility.js +44 -0
- package/dist/core/transactional/git-worktree.d.ts +13 -0
- package/dist/core/transactional/git-worktree.js +189 -0
- package/dist/core/transactional/index.d.ts +5 -0
- package/dist/core/transactional/index.js +4 -0
- package/dist/core/transactional/reasons.d.ts +4 -0
- package/dist/core/transactional/reasons.js +8 -0
- package/dist/core/transactional/runner.d.ts +2 -0
- package/dist/core/transactional/runner.js +113 -0
- package/dist/core/transactional/types.d.ts +46 -0
- package/dist/core/transactional/types.js +1 -0
- package/dist/core/types.d.ts +90 -0
- package/dist/core/types.js +1 -0
- package/dist/core/v2/adapter.d.ts +14 -0
- package/dist/core/v2/adapter.js +118 -0
- package/dist/core/v2/containment.d.ts +19 -0
- package/dist/core/v2/containment.js +91 -0
- package/dist/core/v2/egress-classify.d.ts +7 -0
- package/dist/core/v2/egress-classify.js +216 -0
- package/dist/core/v2/fingerprint.d.ts +1 -0
- package/dist/core/v2/fingerprint.js +4 -0
- package/dist/core/v2/index.d.ts +12 -0
- package/dist/core/v2/index.js +10 -0
- package/dist/core/v2/judge-audit.d.ts +2 -0
- package/dist/core/v2/judge-audit.js +15 -0
- package/dist/core/v2/judge-factory.d.ts +25 -0
- package/dist/core/v2/judge-factory.js +75 -0
- package/dist/core/v2/judge-outbound.d.ts +12 -0
- package/dist/core/v2/judge-outbound.js +73 -0
- package/dist/core/v2/judge.d.ts +47 -0
- package/dist/core/v2/judge.js +264 -0
- package/dist/core/v2/launcher-resolve.d.ts +12 -0
- package/dist/core/v2/launcher-resolve.js +190 -0
- package/dist/core/v2/overrides.d.ts +7 -0
- package/dist/core/v2/overrides.js +37 -0
- package/dist/core/v2/parser.d.ts +21 -0
- package/dist/core/v2/parser.js +213 -0
- package/dist/core/v2/types.d.ts +67 -0
- package/dist/core/v2/types.js +1 -0
- package/dist/core/v2/verdict.d.ts +2 -0
- package/dist/core/v2/verdict.js +699 -0
- package/dist/corpus/evaluate.d.ts +24 -0
- package/dist/corpus/evaluate.js +69 -0
- package/dist/defaults.d.ts +18 -0
- package/dist/defaults.js +155 -0
- package/dist/egress-daemon.d.ts +1 -0
- package/dist/egress-daemon.js +52 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/installer/bootstrap.d.ts +5 -0
- package/dist/installer/bootstrap.js +61 -0
- package/dist/installer/runtime-artifacts.d.ts +3 -0
- package/dist/installer/runtime-artifacts.js +23 -0
- package/dist/installer/scope-config.d.ts +8 -0
- package/dist/installer/scope-config.js +25 -0
- package/dist/installer.d.ts +22 -0
- package/dist/installer.js +169 -0
- package/dist/node-resolution.d.ts +8 -0
- package/dist/node-resolution.js +237 -0
- package/dist/operational-insights.d.ts +19 -0
- package/dist/operational-insights.js +24 -0
- package/dist/presets.d.ts +4 -0
- package/dist/presets.js +95 -0
- package/dist/services/egress-service.d.ts +57 -0
- package/dist/services/egress-service.js +334 -0
- package/dist/services/sandbox-service.d.ts +38 -0
- package/dist/services/sandbox-service.js +95 -0
- package/dist/templates.d.ts +7 -0
- package/dist/templates.js +56 -0
- package/dist/types.d.ts +230 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +65 -0
- package/skills/belay/SKILL.md +52 -0
- package/skills/belay/belay-approve.md +7 -0
- package/skills/belay/belay-explain.md +11 -0
- package/skills/belay/belay-recover.md +13 -0
- package/skills/belay/belay-report.md +7 -0
- package/skills/belay/belay-status.md +9 -0
- package/skills/belay/belay-why.md +11 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { normalizeJudgeProvider, scrubOptionsFromConfig } from '../config.js';
|
|
4
|
+
import { createDeterministicJudgeStub, createFailClosedJudge, createOllamaJudge, createOpenAiCompatibleJudge, } from './judge.js';
|
|
5
|
+
const FIXTURE_MODELS_URL = new URL('../../../fixtures/judge-models.json', import.meta.url);
|
|
6
|
+
let cachedPinnedModels = null;
|
|
7
|
+
export function resetPinnedJudgeModelsCache() {
|
|
8
|
+
cachedPinnedModels = null;
|
|
9
|
+
}
|
|
10
|
+
export async function loadPinnedJudgeModels() {
|
|
11
|
+
if (cachedPinnedModels) {
|
|
12
|
+
return cachedPinnedModels;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(fileURLToPath(FIXTURE_MODELS_URL), 'utf8');
|
|
16
|
+
cachedPinnedModels = JSON.parse(raw);
|
|
17
|
+
return cachedPinnedModels;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
cachedPinnedModels = {
|
|
21
|
+
'openai-compatible': { autoResolved: 'composer-2.5' },
|
|
22
|
+
ollama: { ciPin: 'gemma4:e2b' },
|
|
23
|
+
};
|
|
24
|
+
return cachedPinnedModels;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function resolveCloudModel(requested, pinned) {
|
|
28
|
+
if (requested === 'auto') {
|
|
29
|
+
const envResolved = process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim();
|
|
30
|
+
return {
|
|
31
|
+
requested,
|
|
32
|
+
resolved: envResolved || pinned.autoResolved,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return { requested, resolved: requested };
|
|
36
|
+
}
|
|
37
|
+
/** @deprecated Use resolveCloudModel */
|
|
38
|
+
export const resolveCursorModel = resolveCloudModel;
|
|
39
|
+
export function createJudgeFromConfig(config, options = {}) {
|
|
40
|
+
const judgeConfig = config.judge;
|
|
41
|
+
const provider = normalizeJudgeProvider(judgeConfig.provider);
|
|
42
|
+
if (provider === 'openai-compatible') {
|
|
43
|
+
const endpoint = judgeConfig.endpoint?.trim();
|
|
44
|
+
if (!endpoint) {
|
|
45
|
+
return createFailClosedJudge({
|
|
46
|
+
reason: 'openai_compatible_endpoint_missing',
|
|
47
|
+
fallbackReason: 'missing_endpoint',
|
|
48
|
+
modelRequested: judgeConfig.model,
|
|
49
|
+
modelResolved: judgeConfig.model,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const pinned = options.pinnedModels ?? { autoResolved: 'composer-2.5' };
|
|
53
|
+
const { resolved } = resolveCloudModel(judgeConfig.model, pinned);
|
|
54
|
+
return createOpenAiCompatibleJudge({
|
|
55
|
+
endpoint,
|
|
56
|
+
modelRequested: judgeConfig.model,
|
|
57
|
+
modelResolved: resolved,
|
|
58
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
59
|
+
sensitivePaths: config.classifier.sensitivePaths,
|
|
60
|
+
scrubOptions: scrubOptionsFromConfig(config),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (provider === 'ollama') {
|
|
64
|
+
return createOllamaJudge({
|
|
65
|
+
model: judgeConfig.model,
|
|
66
|
+
baseUrl: judgeConfig.endpoint ?? 'http://127.0.0.1:11434',
|
|
67
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
68
|
+
keepAlive: judgeConfig.keepAlive,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return createDeterministicJudgeStub();
|
|
72
|
+
}
|
|
73
|
+
export function judgeConfigSummary(judge) {
|
|
74
|
+
return `${judge.provider}/${judge.model}`;
|
|
75
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ScrubOptions } from '../types.js';
|
|
2
|
+
export interface OutboundScrubOptions {
|
|
3
|
+
sensitivePaths: string[];
|
|
4
|
+
scrubOptions: ScrubOptions;
|
|
5
|
+
}
|
|
6
|
+
export declare function scrubOutboundForJudge(text: string, options: OutboundScrubOptions): {
|
|
7
|
+
ok: true;
|
|
8
|
+
text: string;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
reason: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { matchesSensitivePath } from '../glob.js';
|
|
2
|
+
import { scrubString } from '../scrub.js';
|
|
3
|
+
const PATH_LIKE = /(?:^|[\s"'`=])(~\/[^\s"'`]+|\/[^\s"'`]+|\.\/[^\s"'`]+|\.\.\/[^\s"'`]+|[A-Za-z]:\\[^\s"'`]+)/g;
|
|
4
|
+
const REDACTED_PLACEHOLDER = /^(?:<redacted>|\[REDACTED\]|<secret>|<high-entropy>|<approval-id>)$/i;
|
|
5
|
+
const URL_CREDENTIALS_PATTERN = /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/\s:@]+):([^@\s/]+)@/gi;
|
|
6
|
+
const GENERIC_AUTH_HEADER_PATTERN = /\b(?:Authorization|X-Api-Key|X-Auth-Token|Private-Token):\s*([^\s]+)/gi;
|
|
7
|
+
function hasResidualBearerToken(text) {
|
|
8
|
+
for (const match of text.matchAll(/\bBearer\s+(\S+)/gi)) {
|
|
9
|
+
const token = match[1] ?? '';
|
|
10
|
+
if (!REDACTED_PLACEHOLDER.test(token)) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
function hasResidualApiKey(text) {
|
|
17
|
+
return /\bsk-(?![^\s]*<redacted>)[A-Za-z0-9_-]{4,}/i.test(text);
|
|
18
|
+
}
|
|
19
|
+
function hasResidualUrlCredentials(text) {
|
|
20
|
+
for (const match of text.matchAll(URL_CREDENTIALS_PATTERN)) {
|
|
21
|
+
const username = (match[1] ?? '').replace(/^['"]|['"]$/g, '');
|
|
22
|
+
const password = (match[2] ?? '').replace(/^['"]|['"]$/g, '');
|
|
23
|
+
if (!REDACTED_PLACEHOLDER.test(username) || !REDACTED_PLACEHOLDER.test(password)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function hasResidualAuthHeader(text) {
|
|
30
|
+
for (const match of text.matchAll(GENERIC_AUTH_HEADER_PATTERN)) {
|
|
31
|
+
const token = (match[1] ?? '').replace(/^['"]|['"]$/g, '');
|
|
32
|
+
if (!REDACTED_PLACEHOLDER.test(token)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
function redactSensitivePathToken(token, sensitivePaths) {
|
|
39
|
+
const trimmed = token.replace(/^['"`]+|['"`]+$/g, '');
|
|
40
|
+
const normalized = trimmed.replaceAll('\\', '/');
|
|
41
|
+
if (!matchesSensitivePath(normalized, sensitivePaths)) {
|
|
42
|
+
return token;
|
|
43
|
+
}
|
|
44
|
+
const segments = normalized.split('/');
|
|
45
|
+
const basename = segments.at(-1) ?? normalized;
|
|
46
|
+
if (segments.length > 1) {
|
|
47
|
+
return token.replace(basename, '[REDACTED]');
|
|
48
|
+
}
|
|
49
|
+
return '[REDACTED]';
|
|
50
|
+
}
|
|
51
|
+
export function scrubOutboundForJudge(text, options) {
|
|
52
|
+
try {
|
|
53
|
+
let scrubbed = scrubString(text, {
|
|
54
|
+
...options.scrubOptions,
|
|
55
|
+
maskHighEntropyStrings: options.scrubOptions.maskHighEntropyStrings !== false,
|
|
56
|
+
});
|
|
57
|
+
scrubbed = scrubbed.replace(PATH_LIKE, (match, pathToken) => {
|
|
58
|
+
const redacted = redactSensitivePathToken(pathToken, options.sensitivePaths);
|
|
59
|
+
return match.replace(pathToken, redacted);
|
|
60
|
+
});
|
|
61
|
+
if (hasResidualApiKey(scrubbed) ||
|
|
62
|
+
hasResidualBearerToken(scrubbed) ||
|
|
63
|
+
hasResidualUrlCredentials(scrubbed) ||
|
|
64
|
+
hasResidualAuthHeader(scrubbed)) {
|
|
65
|
+
return { ok: false, reason: 'residual_secret_detected' };
|
|
66
|
+
}
|
|
67
|
+
return { ok: true, text: scrubbed };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const reason = error instanceof Error ? error.message : 'scrub_failed';
|
|
71
|
+
return { ok: false, reason };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ScrubOptions } from '../types.js';
|
|
2
|
+
import type { Tier1Judge, Tier1Verdict } from './types.js';
|
|
3
|
+
export interface Tier1JudgeTrace {
|
|
4
|
+
provider: 'openai-compatible' | 'ollama' | 'fallback';
|
|
5
|
+
modelRequested: string;
|
|
6
|
+
modelResolved: string;
|
|
7
|
+
latencyMs: number;
|
|
8
|
+
outboundRedacted?: boolean;
|
|
9
|
+
fallbackReason?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface TracedTier1Judge extends Tier1Judge {
|
|
12
|
+
lastTrace?: Tier1JudgeTrace;
|
|
13
|
+
}
|
|
14
|
+
export declare function prescanInterpreterCode(code: string): Tier1Verdict | null;
|
|
15
|
+
/** Conservative stub: Tier1 defers to Tier0; returns safe negatives for structural suite. */
|
|
16
|
+
export declare function createDeterministicJudgeStub(): TracedTier1Judge;
|
|
17
|
+
/** Fail-closed judge for when Tier1 is required but unavailable. */
|
|
18
|
+
export declare function createFailClosedJudge(options?: {
|
|
19
|
+
reason?: string;
|
|
20
|
+
fallbackReason?: string;
|
|
21
|
+
modelRequested?: string;
|
|
22
|
+
modelResolved?: string;
|
|
23
|
+
}): TracedTier1Judge;
|
|
24
|
+
export interface OllamaJudgeOptions {
|
|
25
|
+
model?: string;
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
keepAlive?: string | null;
|
|
29
|
+
fetchImpl?: typeof fetch;
|
|
30
|
+
}
|
|
31
|
+
export declare function createOllamaJudge(options?: OllamaJudgeOptions): TracedTier1Judge;
|
|
32
|
+
export interface OpenAiCompatibleJudgeOptions {
|
|
33
|
+
endpoint: string;
|
|
34
|
+
modelRequested: string;
|
|
35
|
+
modelResolved: string;
|
|
36
|
+
timeoutMs: number;
|
|
37
|
+
apiKey?: string;
|
|
38
|
+
sensitivePaths: string[];
|
|
39
|
+
scrubOptions: ScrubOptions;
|
|
40
|
+
fetchImpl?: typeof fetch;
|
|
41
|
+
}
|
|
42
|
+
export declare function createOpenAiCompatibleJudge(options: OpenAiCompatibleJudgeOptions): TracedTier1Judge;
|
|
43
|
+
/** @deprecated Use createOpenAiCompatibleJudge */
|
|
44
|
+
export declare const createCursorJudge: typeof createOpenAiCompatibleJudge;
|
|
45
|
+
export interface CursorJudgeOptions extends OpenAiCompatibleJudgeOptions {
|
|
46
|
+
}
|
|
47
|
+
export declare function tier1RequiresAsk(verdict: Tier1Verdict): boolean;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { scrubOutboundForJudge } from './judge-outbound.js';
|
|
2
|
+
const SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
|
|
3
|
+
const DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
|
|
4
|
+
const GIT_PATTERNS = /\.git\b/i;
|
|
5
|
+
const TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"destroys_outside_repo":boolean,"destroys_history_or_secrets":boolean,"reason":string}
|
|
6
|
+
Default all three booleans to true when unsure. reason should be a short snake_case label.
|
|
7
|
+
|
|
8
|
+
external_change: CREATE/MODIFY/DELETE on database, network endpoint, cloud, remote git, registry, queue. READ/QUERY/GET is NOT a change.
|
|
9
|
+
destroys_outside_repo: DELETE/OVERWRITE files OUTSIDE the current repository working tree.
|
|
10
|
+
destroys_history_or_secrets: DELETE/OVERWRITE .git directory OR secret/credential files (.env, *.pem, id_rsa, credentials).
|
|
11
|
+
|
|
12
|
+
Command/code:
|
|
13
|
+
`;
|
|
14
|
+
function failClosedVerdict(reason) {
|
|
15
|
+
return {
|
|
16
|
+
external_change: true,
|
|
17
|
+
destroys_outside_repo: true,
|
|
18
|
+
destroys_history_or_secrets: true,
|
|
19
|
+
reason,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function parseTier1Json(raw) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
if (typeof parsed.external_change !== 'boolean' ||
|
|
26
|
+
typeof parsed.destroys_outside_repo !== 'boolean' ||
|
|
27
|
+
typeof parsed.destroys_history_or_secrets !== 'boolean') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
external_change: parsed.external_change !== false,
|
|
32
|
+
destroys_outside_repo: parsed.destroys_outside_repo !== false,
|
|
33
|
+
destroys_history_or_secrets: parsed.destroys_history_or_secrets !== false,
|
|
34
|
+
reason: typeof parsed.reason === 'string' ? parsed.reason : 'tier1_llm',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function prescanInterpreterCode(code) {
|
|
42
|
+
const normalized = code.replaceAll('\\', '/');
|
|
43
|
+
const hitsSecret = SECRET_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
44
|
+
const hitsGit = GIT_PATTERNS.test(normalized);
|
|
45
|
+
const hitsDestructive = DESTRUCTIVE_VERBS.test(normalized);
|
|
46
|
+
if ((hitsSecret || hitsGit) && hitsDestructive) {
|
|
47
|
+
return {
|
|
48
|
+
external_change: false,
|
|
49
|
+
destroys_outside_repo: false,
|
|
50
|
+
destroys_history_or_secrets: true,
|
|
51
|
+
reason: 'prescan_destructive_secret',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/** Conservative stub: Tier1 defers to Tier0; returns safe negatives for structural suite. */
|
|
57
|
+
export function createDeterministicJudgeStub() {
|
|
58
|
+
return {
|
|
59
|
+
evaluate() {
|
|
60
|
+
return Promise.resolve({
|
|
61
|
+
external_change: false,
|
|
62
|
+
destroys_outside_repo: false,
|
|
63
|
+
destroys_history_or_secrets: false,
|
|
64
|
+
reason: 'deterministic_stub',
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Fail-closed judge for when Tier1 is required but unavailable. */
|
|
70
|
+
export function createFailClosedJudge(options) {
|
|
71
|
+
const reason = options?.reason ?? 'fail_closed';
|
|
72
|
+
const judge = {
|
|
73
|
+
evaluate() {
|
|
74
|
+
const started = Date.now();
|
|
75
|
+
if (options?.fallbackReason) {
|
|
76
|
+
judge.lastTrace = {
|
|
77
|
+
provider: 'fallback',
|
|
78
|
+
modelRequested: options.modelRequested ?? 'unknown',
|
|
79
|
+
modelResolved: options.modelResolved ?? 'unknown',
|
|
80
|
+
latencyMs: Date.now() - started,
|
|
81
|
+
fallbackReason: options.fallbackReason,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return Promise.resolve(failClosedVerdict(reason));
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
return judge;
|
|
88
|
+
}
|
|
89
|
+
export function createOllamaJudge(options = {}) {
|
|
90
|
+
const model = options.model ?? 'gemma4:e2b';
|
|
91
|
+
const baseUrl = (options.baseUrl ?? 'http://127.0.0.1:11434').replace(/\/$/, '');
|
|
92
|
+
const timeoutMs = options.timeoutMs ?? 25000;
|
|
93
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
94
|
+
const judge = {
|
|
95
|
+
async evaluate(input) {
|
|
96
|
+
const started = Date.now();
|
|
97
|
+
const prescan = input.innerCode ? prescanInterpreterCode(input.innerCode) : null;
|
|
98
|
+
if (prescan?.destroys_history_or_secrets) {
|
|
99
|
+
judge.lastTrace = {
|
|
100
|
+
provider: 'ollama',
|
|
101
|
+
modelRequested: model,
|
|
102
|
+
modelResolved: model,
|
|
103
|
+
latencyMs: Date.now() - started,
|
|
104
|
+
};
|
|
105
|
+
return prescan;
|
|
106
|
+
}
|
|
107
|
+
const body = `${TIER1_PROMPT}${input.text}`;
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetchImpl(`${baseUrl}/api/generate`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'content-type': 'application/json' },
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
model,
|
|
114
|
+
prompt: body,
|
|
115
|
+
stream: false,
|
|
116
|
+
format: 'json',
|
|
117
|
+
keep_alive: options.keepAlive ?? undefined,
|
|
118
|
+
}),
|
|
119
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
120
|
+
});
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
judge.lastTrace = {
|
|
123
|
+
provider: 'fallback',
|
|
124
|
+
modelRequested: model,
|
|
125
|
+
modelResolved: model,
|
|
126
|
+
latencyMs: Date.now() - started,
|
|
127
|
+
fallbackReason: `ollama_http_${response.status}`,
|
|
128
|
+
};
|
|
129
|
+
return failClosedVerdict('ollama_unavailable');
|
|
130
|
+
}
|
|
131
|
+
const payload = (await response.json());
|
|
132
|
+
const parsed = parseTier1Json(payload.response ?? '{}');
|
|
133
|
+
if (!parsed) {
|
|
134
|
+
judge.lastTrace = {
|
|
135
|
+
provider: 'fallback',
|
|
136
|
+
modelRequested: model,
|
|
137
|
+
modelResolved: model,
|
|
138
|
+
latencyMs: Date.now() - started,
|
|
139
|
+
fallbackReason: 'ollama_parse_error',
|
|
140
|
+
};
|
|
141
|
+
return failClosedVerdict('ollama_parse_error');
|
|
142
|
+
}
|
|
143
|
+
judge.lastTrace = {
|
|
144
|
+
provider: 'ollama',
|
|
145
|
+
modelRequested: model,
|
|
146
|
+
modelResolved: model,
|
|
147
|
+
latencyMs: Date.now() - started,
|
|
148
|
+
};
|
|
149
|
+
return parsed;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
judge.lastTrace = {
|
|
153
|
+
provider: 'fallback',
|
|
154
|
+
modelRequested: model,
|
|
155
|
+
modelResolved: model,
|
|
156
|
+
latencyMs: Date.now() - started,
|
|
157
|
+
fallbackReason: error instanceof Error ? error.message : 'ollama_error',
|
|
158
|
+
};
|
|
159
|
+
return failClosedVerdict('ollama_unavailable');
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
return judge;
|
|
164
|
+
}
|
|
165
|
+
export function createOpenAiCompatibleJudge(options) {
|
|
166
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
167
|
+
const apiBase = options.endpoint.replace(/\/$/, '');
|
|
168
|
+
const judge = {
|
|
169
|
+
async evaluate(input) {
|
|
170
|
+
const started = Date.now();
|
|
171
|
+
const prescan = input.innerCode ? prescanInterpreterCode(input.innerCode) : null;
|
|
172
|
+
if (prescan?.destroys_history_or_secrets) {
|
|
173
|
+
judge.lastTrace = {
|
|
174
|
+
provider: 'openai-compatible',
|
|
175
|
+
modelRequested: options.modelRequested,
|
|
176
|
+
modelResolved: options.modelResolved,
|
|
177
|
+
latencyMs: Date.now() - started,
|
|
178
|
+
};
|
|
179
|
+
return prescan;
|
|
180
|
+
}
|
|
181
|
+
const scrubbed = scrubOutboundForJudge(input.text, {
|
|
182
|
+
sensitivePaths: options.sensitivePaths,
|
|
183
|
+
scrubOptions: options.scrubOptions,
|
|
184
|
+
});
|
|
185
|
+
if (!scrubbed.ok) {
|
|
186
|
+
judge.lastTrace = {
|
|
187
|
+
provider: 'fallback',
|
|
188
|
+
modelRequested: options.modelRequested,
|
|
189
|
+
modelResolved: options.modelResolved,
|
|
190
|
+
latencyMs: Date.now() - started,
|
|
191
|
+
fallbackReason: scrubbed.reason,
|
|
192
|
+
};
|
|
193
|
+
return failClosedVerdict('outbound_scrub_failed');
|
|
194
|
+
}
|
|
195
|
+
const { resolveJudgeApiKey } = await import('../judge-api-key.js');
|
|
196
|
+
const resolvedKey = resolveJudgeApiKey();
|
|
197
|
+
const apiKey = options.apiKey ?? resolvedKey.key;
|
|
198
|
+
if (!apiKey) {
|
|
199
|
+
judge.lastTrace = {
|
|
200
|
+
provider: 'fallback',
|
|
201
|
+
modelRequested: options.modelRequested,
|
|
202
|
+
modelResolved: options.modelResolved,
|
|
203
|
+
latencyMs: Date.now() - started,
|
|
204
|
+
fallbackReason: 'missing_api_key',
|
|
205
|
+
};
|
|
206
|
+
return failClosedVerdict('openai_compatible_auth_error');
|
|
207
|
+
}
|
|
208
|
+
const prompt = `${TIER1_PROMPT}${scrubbed.text}`;
|
|
209
|
+
try {
|
|
210
|
+
const response = await fetchImpl(`${apiBase}/chat/completions`, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: {
|
|
213
|
+
'content-type': 'application/json',
|
|
214
|
+
authorization: `Bearer ${apiKey}`,
|
|
215
|
+
},
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
model: options.modelResolved,
|
|
218
|
+
messages: [{ role: 'user', content: prompt }],
|
|
219
|
+
response_format: { type: 'json_object' },
|
|
220
|
+
}),
|
|
221
|
+
signal: AbortSignal.timeout(options.timeoutMs),
|
|
222
|
+
});
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
judge.lastTrace = {
|
|
225
|
+
provider: 'fallback',
|
|
226
|
+
modelRequested: options.modelRequested,
|
|
227
|
+
modelResolved: options.modelResolved,
|
|
228
|
+
latencyMs: Date.now() - started,
|
|
229
|
+
fallbackReason: `openai_compatible_http_${response.status}`,
|
|
230
|
+
};
|
|
231
|
+
return failClosedVerdict('openai_compatible_unavailable');
|
|
232
|
+
}
|
|
233
|
+
const payload = (await response.json());
|
|
234
|
+
const content = payload.choices?.[0]?.message?.content ?? '{}';
|
|
235
|
+
const parsed = parseTier1Json(content);
|
|
236
|
+
judge.lastTrace = {
|
|
237
|
+
provider: parsed ? 'openai-compatible' : 'fallback',
|
|
238
|
+
modelRequested: options.modelRequested,
|
|
239
|
+
modelResolved: options.modelResolved,
|
|
240
|
+
latencyMs: Date.now() - started,
|
|
241
|
+
outboundRedacted: true,
|
|
242
|
+
fallbackReason: parsed ? undefined : 'openai_compatible_parse_error',
|
|
243
|
+
};
|
|
244
|
+
return parsed ?? failClosedVerdict('openai_compatible_parse_error');
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
judge.lastTrace = {
|
|
248
|
+
provider: 'fallback',
|
|
249
|
+
modelRequested: options.modelRequested,
|
|
250
|
+
modelResolved: options.modelResolved,
|
|
251
|
+
latencyMs: Date.now() - started,
|
|
252
|
+
fallbackReason: error instanceof Error ? error.message : 'openai_compatible_error',
|
|
253
|
+
};
|
|
254
|
+
return failClosedVerdict('openai_compatible_unavailable');
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
return judge;
|
|
259
|
+
}
|
|
260
|
+
/** @deprecated Use createOpenAiCompatibleJudge */
|
|
261
|
+
export const createCursorJudge = createOpenAiCompatibleJudge;
|
|
262
|
+
export function tier1RequiresAsk(verdict) {
|
|
263
|
+
return (verdict.external_change || verdict.destroys_outside_repo || verdict.destroys_history_or_secrets);
|
|
264
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LauncherResolution {
|
|
2
|
+
recipes: string[];
|
|
3
|
+
opaque: boolean;
|
|
4
|
+
reason: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveLauncherRecipe(params: {
|
|
7
|
+
tokens: string[];
|
|
8
|
+
cwd: string;
|
|
9
|
+
repoRoot: string;
|
|
10
|
+
depth: number;
|
|
11
|
+
}): LauncherResolution | null;
|
|
12
|
+
export declare function isRoutineLauncher(tokens: string[]): boolean;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const MAX_RESOLVE_DEPTH = 8;
|
|
4
|
+
function readPackageJson(dir) {
|
|
5
|
+
const packagePath = path.join(dir, 'package.json');
|
|
6
|
+
if (!existsSync(packagePath)) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function findPackageJson(startDir, stopDir) {
|
|
17
|
+
let current = path.resolve(startDir);
|
|
18
|
+
const stop = path.resolve(stopDir);
|
|
19
|
+
while (true) {
|
|
20
|
+
const packagePath = path.join(current, 'package.json');
|
|
21
|
+
if (existsSync(packagePath)) {
|
|
22
|
+
return packagePath;
|
|
23
|
+
}
|
|
24
|
+
if (current === stop || current === path.dirname(current)) {
|
|
25
|
+
return existsSync(packagePath) ? packagePath : null;
|
|
26
|
+
}
|
|
27
|
+
const parent = path.dirname(current);
|
|
28
|
+
if (!parent.startsWith(stop) && parent !== current) {
|
|
29
|
+
// still allow walking up until repo root parent
|
|
30
|
+
}
|
|
31
|
+
if (parent === current) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
current = parent;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function launcherTokens(tokens) {
|
|
39
|
+
const dashIndex = tokens.indexOf('--');
|
|
40
|
+
return dashIndex === -1 ? tokens : tokens.slice(0, dashIndex);
|
|
41
|
+
}
|
|
42
|
+
function forwardedArgs(tokens) {
|
|
43
|
+
const dashIndex = tokens.indexOf('--');
|
|
44
|
+
if (dashIndex === -1) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
return tokens.slice(dashIndex + 1);
|
|
48
|
+
}
|
|
49
|
+
function npmScriptName(tokens) {
|
|
50
|
+
const launcher = launcherTokens(tokens);
|
|
51
|
+
if (launcher[0] === 'npm' && launcher[1] === 'test') {
|
|
52
|
+
return 'test';
|
|
53
|
+
}
|
|
54
|
+
if (launcher[0] === 'npm' && launcher[1] === 'run' && launcher[2]) {
|
|
55
|
+
return launcher[2];
|
|
56
|
+
}
|
|
57
|
+
if (launcher[0] === 'pnpm' && launcher[1] === 'run' && launcher[2]) {
|
|
58
|
+
return launcher[2];
|
|
59
|
+
}
|
|
60
|
+
if (launcher[0] === 'npm' && launcher[1] && launcher[1] !== 'run' && launcher[1] !== 'install') {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function applyForwardedArgs(recipe, extra) {
|
|
66
|
+
if (extra.length === 0) {
|
|
67
|
+
return recipe.trim();
|
|
68
|
+
}
|
|
69
|
+
return `${recipe.trim()} ${extra.join(' ')}`.trim();
|
|
70
|
+
}
|
|
71
|
+
function resolveNpmRecipe(cwd, repoRoot, scriptName, extraArgs) {
|
|
72
|
+
const packagePath = findPackageJson(cwd, repoRoot) ?? findPackageJson(cwd, cwd);
|
|
73
|
+
if (!packagePath) {
|
|
74
|
+
if (/deploy|publish|release|ship|prod/i.test(scriptName)) {
|
|
75
|
+
return { recipes: [], opaque: true, reason: 'external_script' };
|
|
76
|
+
}
|
|
77
|
+
return { recipes: [], opaque: true, reason: 'package_json_missing' };
|
|
78
|
+
}
|
|
79
|
+
const pkg = readPackageJson(path.dirname(packagePath));
|
|
80
|
+
const scripts = pkg?.scripts;
|
|
81
|
+
if (!scripts || typeof scripts !== 'object') {
|
|
82
|
+
return { recipes: [], opaque: true, reason: 'package_scripts_missing' };
|
|
83
|
+
}
|
|
84
|
+
const recipe = scripts[scriptName];
|
|
85
|
+
if (!recipe || typeof recipe !== 'string') {
|
|
86
|
+
if (/deploy|publish|release|ship|prod/i.test(scriptName)) {
|
|
87
|
+
return { recipes: [], opaque: true, reason: 'external_script' };
|
|
88
|
+
}
|
|
89
|
+
return { recipes: [], opaque: true, reason: 'npm_script_undefined' };
|
|
90
|
+
}
|
|
91
|
+
if (/\$\(/.test(recipe) || /\$\{/.test(recipe)) {
|
|
92
|
+
return { recipes: [], opaque: true, reason: 'npm_script_dynamic' };
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
recipes: [applyForwardedArgs(recipe, extraArgs)],
|
|
96
|
+
opaque: false,
|
|
97
|
+
reason: 'npm_script_resolved',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function parseMakefileRecipes(makefilePath) {
|
|
101
|
+
const recipes = new Map();
|
|
102
|
+
try {
|
|
103
|
+
const content = readFileSync(makefilePath, 'utf8');
|
|
104
|
+
const lines = content.split('\n');
|
|
105
|
+
let currentTarget = null;
|
|
106
|
+
let recipeLines = [];
|
|
107
|
+
const flush = () => {
|
|
108
|
+
if (currentTarget && recipeLines.length > 0) {
|
|
109
|
+
recipes.set(currentTarget, recipeLines.map((line) => line.trim()).filter((line) => line.length > 0));
|
|
110
|
+
}
|
|
111
|
+
currentTarget = null;
|
|
112
|
+
recipeLines = [];
|
|
113
|
+
};
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
if (line.trim().startsWith('#')) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const targetMatch = /^([A-Za-z0-9_.-]+)\s*:(?!=)/.exec(line);
|
|
119
|
+
if (targetMatch) {
|
|
120
|
+
flush();
|
|
121
|
+
currentTarget = targetMatch[1] ?? null;
|
|
122
|
+
const inline = line.slice(targetMatch[0].length).trim();
|
|
123
|
+
if (inline && !inline.startsWith('#')) {
|
|
124
|
+
recipeLines.push(inline);
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (currentTarget && /^\t/.test(line)) {
|
|
129
|
+
recipeLines.push(line.trim());
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
flush();
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return recipes;
|
|
136
|
+
}
|
|
137
|
+
return recipes;
|
|
138
|
+
}
|
|
139
|
+
function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
140
|
+
const candidates = ['Makefile', 'makefile', 'GNUmakefile'];
|
|
141
|
+
let makefilePath = null;
|
|
142
|
+
let searchDir = path.resolve(cwd);
|
|
143
|
+
const stop = path.resolve(repoRoot);
|
|
144
|
+
while (true) {
|
|
145
|
+
for (const name of candidates) {
|
|
146
|
+
const candidate = path.join(searchDir, name);
|
|
147
|
+
if (existsSync(candidate)) {
|
|
148
|
+
makefilePath = candidate;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (makefilePath || searchDir === stop || searchDir === path.dirname(searchDir)) {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
searchDir = path.dirname(searchDir);
|
|
156
|
+
}
|
|
157
|
+
if (!makefilePath) {
|
|
158
|
+
return { recipes: [], opaque: true, reason: 'unknown_local_effect' };
|
|
159
|
+
}
|
|
160
|
+
const recipes = parseMakefileRecipes(makefilePath);
|
|
161
|
+
const recipeLines = recipes.get(target);
|
|
162
|
+
if (!recipeLines || recipeLines.length === 0) {
|
|
163
|
+
return { recipes: [], opaque: true, reason: 'make_target_undefined' };
|
|
164
|
+
}
|
|
165
|
+
for (const line of recipeLines) {
|
|
166
|
+
if (/\$\(/.test(line) || /\$\{/.test(line)) {
|
|
167
|
+
return { recipes: [], opaque: true, reason: 'make_recipe_dynamic' };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { recipes: recipeLines, opaque: false, reason: 'make_recipe_resolved' };
|
|
171
|
+
}
|
|
172
|
+
export function resolveLauncherRecipe(params) {
|
|
173
|
+
if (params.depth >= MAX_RESOLVE_DEPTH) {
|
|
174
|
+
return { recipes: [], opaque: true, reason: 'launcher_depth_exceeded' };
|
|
175
|
+
}
|
|
176
|
+
const tokens = params.tokens;
|
|
177
|
+
const scriptName = npmScriptName(tokens);
|
|
178
|
+
if (scriptName) {
|
|
179
|
+
return resolveNpmRecipe(params.cwd, params.repoRoot, scriptName, forwardedArgs(tokens));
|
|
180
|
+
}
|
|
181
|
+
if (tokens[0] === 'make' && tokens[1] && !tokens[1].startsWith('-')) {
|
|
182
|
+
return resolveMakeRecipe(params.cwd, params.repoRoot, tokens[1]);
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
export function isRoutineLauncher(tokens) {
|
|
187
|
+
return ((tokens[0] === 'npm' && (tokens[1] === 'run' || tokens[1] === 'test')) ||
|
|
188
|
+
tokens[0] === 'pnpm' ||
|
|
189
|
+
tokens[0] === 'make');
|
|
190
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ParsedSegment } from './parser.js';
|
|
2
|
+
import type { InternalSegmentVerdict, VerdictContext } from './types.js';
|
|
3
|
+
export declare function matchesCustomPatterns(command: string, segment: ParsedSegment, patterns: string[] | undefined): boolean;
|
|
4
|
+
export declare function customAllowMatch(command: string, segment: ParsedSegment, context: VerdictContext): boolean;
|
|
5
|
+
export declare function customExternalMatch(command: string, segment: ParsedSegment, context: VerdictContext): boolean;
|
|
6
|
+
export declare function allowFromCustomOverride(opacity: InternalSegmentVerdict['opacity']): InternalSegmentVerdict;
|
|
7
|
+
export declare function askFromCustomExternal(opacity: InternalSegmentVerdict['opacity']): InternalSegmentVerdict;
|