klyro 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/anthropic-adapter.d.ts +49 -5
- package/dist/agent/anthropic-adapter.js +86 -17
- package/dist/agent/capabilities.d.ts +23 -0
- package/dist/agent/capabilities.js +53 -6
- package/dist/agent/child-worker.d.ts +104 -0
- package/dist/agent/child-worker.js +250 -0
- package/dist/agent/orchestrator.d.ts +124 -6
- package/dist/agent/orchestrator.js +425 -58
- package/dist/agent/provider-adapter.d.ts +8 -0
- package/dist/agent/provider-adapter.js +12 -3
- package/dist/agent/retry.d.ts +1 -1
- package/dist/agent/retry.js +54 -12
- package/dist/agent/runtime.d.ts +84 -8
- package/dist/agent/runtime.js +352 -39
- package/dist/agent/stream-budget.d.ts +36 -0
- package/dist/agent/stream-budget.js +121 -0
- package/dist/agent/worktree-manager.d.ts +74 -0
- package/dist/agent/worktree-manager.js +189 -0
- package/dist/checkpoints/store.d.ts +9 -0
- package/dist/checkpoints/store.js +56 -5
- package/dist/cli/auth.js +16 -1
- package/dist/cli/commit.d.ts +31 -0
- package/dist/cli/commit.js +142 -0
- package/dist/cli/config.d.ts +54 -3
- package/dist/cli/config.js +146 -3
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +71 -6
- package/dist/cli/eval.d.ts +6 -1
- package/dist/cli/eval.js +9 -0
- package/dist/cli/hooks.d.ts +47 -0
- package/dist/cli/hooks.js +181 -0
- package/dist/cli/repl.js +196 -29
- package/dist/cli/run.d.ts +13 -11
- package/dist/cli/run.js +144 -20
- package/dist/cli/update.d.ts +5 -0
- package/dist/cli/update.js +62 -10
- package/dist/context/import-graph.d.ts +2 -0
- package/dist/context/import-graph.js +31 -3
- package/dist/context/klyro-md.js +4 -1
- package/dist/context/memory.d.ts +8 -0
- package/dist/context/memory.js +50 -2
- package/dist/context/project-map.d.ts +6 -0
- package/dist/context/project-map.js +50 -2
- package/dist/context/repo-map.d.ts +2 -0
- package/dist/context/repo-map.js +31 -1
- package/dist/events/catalog.d.ts +37 -0
- package/dist/events/catalog.js +9 -0
- package/dist/index.js +177 -8
- package/dist/mcp/client.d.ts +6 -4
- package/dist/mcp/client.js +83 -14
- package/dist/mcp/config.d.ts +10 -0
- package/dist/mcp/config.js +18 -1
- package/dist/mcp/registry.d.ts +23 -19
- package/dist/mcp/registry.js +127 -8
- package/dist/mcp/schema.d.ts +11 -4
- package/dist/mcp/schema.js +27 -16
- package/dist/mcp/trust.d.ts +20 -0
- package/dist/mcp/trust.js +74 -0
- package/dist/persistence/audit.d.ts +28 -0
- package/dist/persistence/audit.js +101 -1
- package/dist/persistence/store.d.ts +26 -2
- package/dist/persistence/store.js +140 -13
- package/dist/policy/approval.d.ts +14 -0
- package/dist/policy/approval.js +44 -2
- package/dist/policy/engine.d.ts +17 -0
- package/dist/policy/engine.js +162 -9
- package/dist/policy/path-guard.d.ts +24 -0
- package/dist/policy/path-guard.js +46 -0
- package/dist/policy/secret-redactor.js +4 -0
- package/dist/providers/model-info.d.ts +23 -0
- package/dist/providers/model-info.js +43 -2
- package/dist/repl.d.ts +6 -0
- package/dist/repl.js +12 -7
- package/dist/tools/agent/spawn-agent.js +5 -5
- package/dist/tools/agent/task-apply.d.ts +4 -0
- package/dist/tools/agent/task-apply.js +44 -0
- package/dist/tools/agent/task-stop.d.ts +6 -0
- package/dist/tools/agent/task-stop.js +39 -0
- package/dist/tools/agent/task-wait.d.ts +17 -0
- package/dist/tools/agent/task-wait.js +79 -0
- package/dist/tools/fs/apply-patch.js +77 -1
- package/dist/tools/fs/edit-file.js +69 -1
- package/dist/tools/fs/multi-edit.d.ts +4 -0
- package/dist/tools/fs/multi-edit.js +70 -1
- package/dist/tools/fs/write-file.js +83 -6
- package/dist/tools/plan/todo-write.js +1 -1
- package/dist/tools/registry.js +6 -0
- package/dist/tools/shell/background.js +6 -3
- package/dist/tools/shell/sandbox.d.ts +51 -0
- package/dist/tools/shell/sandbox.js +143 -0
- package/dist/tools/shell/shell-exec.d.ts +29 -0
- package/dist/tools/shell/shell-exec.js +170 -12
- package/dist/tools/shell/worker-entry.d.ts +12 -0
- package/dist/tools/shell/worker-entry.js +43 -0
- package/dist/tools/types.d.ts +6 -0
- package/dist/tools/verify/run-verify.js +3 -1
- package/dist/trace/writer.d.ts +20 -0
- package/dist/trace/writer.js +62 -4
- package/dist/tui/app.js +1 -1
- package/dist/tui/approval.js +20 -21
- package/dist/util.d.ts +1 -0
- package/dist/util.js +1 -0
- package/dist/verification/baseline.js +17 -3
- package/dist/verification/classify.js +27 -15
- package/dist/verification/engine.d.ts +8 -0
- package/dist/verification/engine.js +28 -1
- package/dist/verification/registry.d.ts +2 -0
- package/dist/verification/registry.js +44 -0
- package/dist/verification/scoped.js +64 -11
- package/package.json +1 -1
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
|
+
import { filteredVerifyEnv } from './registry.js';
|
|
8
|
+
import { detect } from './detect.js';
|
|
9
|
+
function signatureOf(failure, fallbackExit) {
|
|
10
|
+
return {
|
|
11
|
+
type: failure?.type ?? 'unknown',
|
|
12
|
+
files: new Set((failure?.files ?? []).map((f) => f.path).filter(Boolean)),
|
|
13
|
+
exit: failure?.exitCode ?? fallbackExit,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
7
16
|
export function classifyFailure(current, baseline, flakyRerunOk) {
|
|
8
17
|
const combined = (current.stderr + '\n' + current.stdout).toLowerCase();
|
|
9
18
|
// env: missing binary, network, permission, no such file, EACCES etc
|
|
@@ -18,26 +27,29 @@ export function classifyFailure(current, baseline, flakyRerunOk) {
|
|
|
18
27
|
if (flakyRerunOk)
|
|
19
28
|
return 'flaky';
|
|
20
29
|
if (baseline && !baseline.ok) {
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
// Structured signature compare (no substring matching): pre-existing
|
|
31
|
+
// iff same failure type AND file-set overlap is non-empty (or same
|
|
32
|
+
// exit + same type when both sides are fileless).
|
|
33
|
+
const curSig = signatureOf(current.failure, -1);
|
|
34
|
+
const baseFailure = detect(baseline.stdout, baseline.stderr, baseline.exitCode);
|
|
35
|
+
const baseSig = signatureOf(baseFailure, baseline.exitCode);
|
|
36
|
+
if (curSig.type !== 'unknown' && curSig.type === baseSig.type) {
|
|
37
|
+
if (curSig.files.size > 0 && baseSig.files.size > 0) {
|
|
38
|
+
for (const f of curSig.files) {
|
|
39
|
+
if (baseSig.files.has(f))
|
|
40
|
+
return 'pre_existing';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (curSig.files.size === 0 && baseSig.files.size === 0 && curSig.exit === baseSig.exit) {
|
|
30
44
|
return 'pre_existing';
|
|
45
|
+
}
|
|
31
46
|
}
|
|
32
|
-
// if baseline failed and current also fails with similar raw length, assume pre-existing
|
|
33
|
-
if (current.failure && baseline.stderr.length > 0 && combined.includes(baseline.stderr.slice(0, 200).toLowerCase()))
|
|
34
|
-
return 'pre_existing';
|
|
35
47
|
}
|
|
36
48
|
return 'introduced';
|
|
37
49
|
}
|
|
38
50
|
export async function rerunOnce(cwd, command, timeoutMs = 45_000) {
|
|
39
51
|
return new Promise((resolve) => {
|
|
40
|
-
const child = spawn(command, { cwd, shell: true, env:
|
|
52
|
+
const child = spawn(command, { cwd, shell: true, env: filteredVerifyEnv() });
|
|
41
53
|
let done = false;
|
|
42
54
|
const t = setTimeout(() => { if (!done) {
|
|
43
55
|
done = true;
|
|
@@ -81,7 +93,7 @@ export async function gatherRepairContext(cwd, failure) {
|
|
|
81
93
|
}
|
|
82
94
|
function execCapturePipe(cwd, cmd) {
|
|
83
95
|
return new Promise((resolve) => {
|
|
84
|
-
const child = spawn(cmd, { cwd, shell: true, env:
|
|
96
|
+
const child = spawn(cmd, { cwd, shell: true, env: filteredVerifyEnv() });
|
|
85
97
|
const chunks = [];
|
|
86
98
|
child.stdout.on('data', (b) => { chunks.push(b); });
|
|
87
99
|
child.stderr.on('data', (b) => { chunks.push(b); });
|
|
@@ -91,7 +103,7 @@ function execCapturePipe(cwd, cmd) {
|
|
|
91
103
|
}
|
|
92
104
|
function execCaptureArgv(cwd, file, args) {
|
|
93
105
|
return new Promise((resolve) => {
|
|
94
|
-
const child = spawn('git', [...args, file], { cwd, shell: false, env:
|
|
106
|
+
const child = spawn('git', [...args, file], { cwd, shell: false, env: filteredVerifyEnv() });
|
|
95
107
|
const chunks = [];
|
|
96
108
|
child.stdout.on('data', (b) => { chunks.push(b); });
|
|
97
109
|
child.stderr.on('data', (b) => { chunks.push(b); });
|
|
@@ -8,10 +8,18 @@
|
|
|
8
8
|
* structured Failure (and the model has had up to N attempts to fix).
|
|
9
9
|
*/
|
|
10
10
|
import { type Failure } from './detect.js';
|
|
11
|
+
/** Verification mode. Canonical definition — agent/runtime.ts converges to
|
|
12
|
+
* this type automatically once this export exists (see its conditional
|
|
13
|
+
* VerifyMode). Backward-compatible: pure type, no runtime change. */
|
|
14
|
+
export type VerifyMode = 'strict' | 'advisory' | 'off';
|
|
11
15
|
export interface VerifyOptions {
|
|
12
16
|
cwd: string;
|
|
13
17
|
command: string;
|
|
14
18
|
timeoutMs?: number;
|
|
19
|
+
/** Owning session for `verification.*` bus events. Optional for backward
|
|
20
|
+
* compatibility — siblings pass `sessionId` at verify call sites; when
|
|
21
|
+
* absent the emitted events fall back to 'ephemeral'. */
|
|
22
|
+
sessionId?: string;
|
|
15
23
|
}
|
|
16
24
|
export interface VerifyResult {
|
|
17
25
|
ok: boolean;
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { spawn } from 'node:child_process';
|
|
11
11
|
import { detect, summarize } from './detect.js';
|
|
12
|
+
import { filteredVerifyEnv } from './registry.js';
|
|
12
13
|
import { redact } from '../policy/secret-redactor.js';
|
|
14
|
+
import { globalBus } from '../events/bus.js';
|
|
13
15
|
const MAX_VERIFY_BYTES = 256 * 1024;
|
|
14
16
|
function appendCapped(current, chunk) {
|
|
15
17
|
if (current.length >= MAX_VERIFY_BYTES)
|
|
@@ -47,9 +49,30 @@ const DANGEROUS_VERIFY_PATTERNS = [
|
|
|
47
49
|
{ pattern: /\|\|\s*rm\s+-rf/, reason: 'chained rm -rf' },
|
|
48
50
|
];
|
|
49
51
|
export async function verify(opts) {
|
|
52
|
+
const sid = opts.sessionId ?? 'ephemeral';
|
|
53
|
+
const emitStarted = () => {
|
|
54
|
+
try {
|
|
55
|
+
globalBus.emit({ type: 'verification.started', ts: Date.now(), sessionId: sid, command: opts.command });
|
|
56
|
+
}
|
|
57
|
+
catch { /* bus never throws */ }
|
|
58
|
+
};
|
|
59
|
+
const emitSucceeded = () => {
|
|
60
|
+
try {
|
|
61
|
+
globalBus.emit({ type: 'verification.succeeded', ts: Date.now(), sessionId: sid, command: opts.command });
|
|
62
|
+
}
|
|
63
|
+
catch { /* bus never throws */ }
|
|
64
|
+
};
|
|
65
|
+
const emitFailed = (reason) => {
|
|
66
|
+
try {
|
|
67
|
+
globalBus.emit({ type: 'verification.failed', ts: Date.now(), sessionId: sid, command: opts.command, reason });
|
|
68
|
+
}
|
|
69
|
+
catch { /* bus never throws */ }
|
|
70
|
+
};
|
|
71
|
+
emitStarted();
|
|
50
72
|
// SEC-004: reject dangerous patterns before spawning
|
|
51
73
|
for (const { pattern, reason } of DANGEROUS_VERIFY_PATTERNS) {
|
|
52
74
|
if (pattern.test(opts.command)) {
|
|
75
|
+
emitFailed(`Command blocked: ${reason}`);
|
|
53
76
|
return {
|
|
54
77
|
ok: false,
|
|
55
78
|
exitCode: -1,
|
|
@@ -61,7 +84,8 @@ export async function verify(opts) {
|
|
|
61
84
|
}
|
|
62
85
|
const timeout = opts.timeoutMs ?? 5 * 60 * 1000;
|
|
63
86
|
return new Promise((resolve) => {
|
|
64
|
-
|
|
87
|
+
// S2: verify commands run with filtered env; servers needing keys must use explicit config.
|
|
88
|
+
const child = spawn(opts.command, { cwd: opts.cwd, shell: true, env: filteredVerifyEnv() });
|
|
65
89
|
const outChunks = [];
|
|
66
90
|
const errChunks = [];
|
|
67
91
|
let done = false;
|
|
@@ -73,6 +97,7 @@ export async function verify(opts) {
|
|
|
73
97
|
const so = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
|
|
74
98
|
const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
|
|
75
99
|
const raw = se + '\n' + so;
|
|
100
|
+
emitFailed('[verify timeout]');
|
|
76
101
|
resolve({
|
|
77
102
|
ok: false,
|
|
78
103
|
exitCode: -1,
|
|
@@ -94,10 +119,12 @@ export async function verify(opts) {
|
|
|
94
119
|
const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
|
|
95
120
|
const exit = typeof code === 'number' ? code : -1;
|
|
96
121
|
if (exit === 0) {
|
|
122
|
+
emitSucceeded();
|
|
97
123
|
resolve({ ok: true, exitCode: 0, stdout: so, stderr: se });
|
|
98
124
|
return;
|
|
99
125
|
}
|
|
100
126
|
const failure = detect(so, se, exit);
|
|
127
|
+
emitFailed(failure.type);
|
|
101
128
|
resolve({ ok: false, exitCode: exit, stdout: so, stderr: se, failure });
|
|
102
129
|
});
|
|
103
130
|
});
|
|
@@ -12,6 +12,8 @@ export interface Verifier {
|
|
|
12
12
|
}
|
|
13
13
|
export declare function detectVerifiers(cwd: string): Verifier[];
|
|
14
14
|
export declare function primaryVerifyCommand(cwd: string): string | null;
|
|
15
|
+
/** Filtered env for verify children: allowlist + secret strip + preload-vector delete. */
|
|
16
|
+
export declare function filteredVerifyEnv(): NodeJS.ProcessEnv;
|
|
15
17
|
export interface VerifySettings {
|
|
16
18
|
commands?: string[];
|
|
17
19
|
onEdit?: boolean;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'node:fs';
|
|
7
7
|
import * as path from 'node:path';
|
|
8
|
+
import { filteredEnv as siblingFilteredEnv } from '../tools/shell/shell-exec.js';
|
|
8
9
|
function exists(p) {
|
|
9
10
|
try {
|
|
10
11
|
return fs.existsSync(p);
|
|
@@ -113,6 +114,49 @@ export function primaryVerifyCommand(cwd) {
|
|
|
113
114
|
const v = detectVerifiers(cwd);
|
|
114
115
|
return v[0]?.command ?? null;
|
|
115
116
|
}
|
|
117
|
+
// S2 — filtered env for ALL spawned verify commands.
|
|
118
|
+
//
|
|
119
|
+
// Delegates to the sibling-owned `filteredEnv` from
|
|
120
|
+
// `tools/shell/shell-exec.ts` (EXACT export name — verified via grep) for
|
|
121
|
+
// the allowlist base, then applies this module's stricter secret strip
|
|
122
|
+
// (sibling only strips SECRET/TOKEN + named keys; verify children also
|
|
123
|
+
// strip KEY-shaped names like NODE_API_KEY) plus the Windows shell vars a
|
|
124
|
+
// spawned shell needs (SystemRoot/COMSPEC/...) and preload-vector deletes.
|
|
125
|
+
// `filteredVerifyEnv` already exists — reuse it, don't duplicate it.
|
|
126
|
+
const VERIFY_ENV_PREFIXES = ['PATH', 'HOME', 'USER', 'SHELL', 'TERM', 'LANG', 'NODE_', 'NPM_', 'PNPM_', 'YARN_'];
|
|
127
|
+
// Exact keys: sibling's PWD/TMPDIR/TEMP plus the Windows OS vars a spawned
|
|
128
|
+
// shell needs to start (COMSPEC/SystemRoot/PATHEXT/...) — none carry secrets.
|
|
129
|
+
const VERIFY_ENV_EXACT = new Set([
|
|
130
|
+
'PWD', 'TMPDIR', 'TEMP', 'TMP',
|
|
131
|
+
'SystemRoot', 'windir', 'COMSPEC', 'PATHEXT', 'OS',
|
|
132
|
+
'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA',
|
|
133
|
+
]);
|
|
134
|
+
const VERIFY_ENV_SECRET_RE = /SECRET|TOKEN|KEY/i;
|
|
135
|
+
/** Filtered env for verify children: allowlist + secret strip + preload-vector delete. */
|
|
136
|
+
export function filteredVerifyEnv() {
|
|
137
|
+
// Sibling-owned base (allowlisted prefixes + secret strip).
|
|
138
|
+
const out = { ...siblingFilteredEnv() };
|
|
139
|
+
// Harden beyond the sibling: strip KEY-shaped names the sibling passes
|
|
140
|
+
// through (e.g. NODE_API_KEY) before overlaying our own allowlist.
|
|
141
|
+
for (const k of Object.keys(out)) {
|
|
142
|
+
if (VERIFY_ENV_SECRET_RE.test(k))
|
|
143
|
+
delete out[k];
|
|
144
|
+
}
|
|
145
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
146
|
+
if (v === undefined)
|
|
147
|
+
continue;
|
|
148
|
+
if (VERIFY_ENV_SECRET_RE.test(k))
|
|
149
|
+
continue;
|
|
150
|
+
if (VERIFY_ENV_PREFIXES.some((p) => k.startsWith(p)) || VERIFY_ENV_EXACT.has(k)) {
|
|
151
|
+
out[k] = v;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
delete out.NODE_OPTIONS;
|
|
155
|
+
delete out.LD_PRELOAD;
|
|
156
|
+
delete out.LD_LIBRARY_PATH;
|
|
157
|
+
out.PATH = process.env.PATH;
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
116
160
|
export function resolveVerifySettings(raw) {
|
|
117
161
|
if (!raw || typeof raw !== 'object')
|
|
118
162
|
return {};
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as fs from 'node:fs';
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
|
+
import { filteredVerifyEnv } from './registry.js';
|
|
8
9
|
export function findRelatedTests(cwd, editedFiles) {
|
|
9
10
|
if (editedFiles.length === 0)
|
|
10
11
|
return [];
|
|
@@ -48,32 +49,81 @@ export function findRelatedTests(cwd, editedFiles) {
|
|
|
48
49
|
}
|
|
49
50
|
return [...related];
|
|
50
51
|
}
|
|
52
|
+
const UNSAFE_SCOPED_RE = /["$`;&|()]/;
|
|
53
|
+
function isSafeScopedPath(cwd, f) {
|
|
54
|
+
if (UNSAFE_SCOPED_RE.test(f))
|
|
55
|
+
return false;
|
|
56
|
+
if (f.includes('\n') || f.includes('\r') || f.includes('\0'))
|
|
57
|
+
return false;
|
|
58
|
+
const rel = path.relative(path.resolve(cwd), path.resolve(cwd, f));
|
|
59
|
+
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel))
|
|
60
|
+
return false;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
51
63
|
export function buildScopedCommand(cwd, baseCommand, relatedTests) {
|
|
52
64
|
if (relatedTests.length === 0)
|
|
53
65
|
return null;
|
|
54
66
|
if (relatedTests.length > 10)
|
|
55
67
|
return null; // too many → full suite
|
|
68
|
+
// Validate every test path: must stay within cwd and contain no shell
|
|
69
|
+
// metacharacters. Drop offenders; if none survive, return null (caller
|
|
70
|
+
// falls back to the full suite — never interpolate untrusted paths).
|
|
71
|
+
const safe = relatedTests.filter((f) => isSafeScopedPath(cwd, f));
|
|
72
|
+
if (safe.length === 0)
|
|
73
|
+
return null;
|
|
56
74
|
// npm test heuristic: npm test -- <files> or vitest/jest file list
|
|
57
75
|
if (baseCommand.includes('npm test') || baseCommand.includes('pnpm test') || baseCommand.includes('yarn test') || baseCommand.includes('bun test')) {
|
|
58
|
-
return `${baseCommand} -- ${
|
|
76
|
+
return `${baseCommand} -- ${safe.map((f) => `"${f}"`).join(' ')}`;
|
|
59
77
|
}
|
|
60
78
|
if (baseCommand.includes('pytest')) {
|
|
61
|
-
return `pytest ${
|
|
62
|
-
}
|
|
63
|
-
if (baseCommand.includes('go test')) {
|
|
64
|
-
// go test ./... with file filter — fallback to full
|
|
65
|
-
return null;
|
|
79
|
+
return `pytest ${safe.map((f) => `"${f}"`).join(' ')}`;
|
|
66
80
|
}
|
|
67
81
|
if (baseCommand.includes('cargo test')) {
|
|
68
|
-
// cargo test
|
|
82
|
+
// Cargo keeps the full suite: `cargo test` selects by test-target name
|
|
83
|
+
// (`--test <name>`), lib/bin unit-test filters, or `-- <filter>` args —
|
|
84
|
+
// none of which map 1:1 onto file paths. Interpolating a file path as a
|
|
85
|
+
// filter would silently run the WRONG subset (substring match against
|
|
86
|
+
// test names), so fall back to the full suite rather than lie about
|
|
87
|
+
// coverage. (Checked before the `go test` branch: "cargo test" contains
|
|
88
|
+
// the substring "go test".)
|
|
69
89
|
return null;
|
|
70
90
|
}
|
|
91
|
+
// NOTE: `baseCommand.includes('go test')` must stay AFTER the cargo branch
|
|
92
|
+
// above — "cargo test" contains "go test" as a substring.
|
|
93
|
+
if (baseCommand.includes('go test')) {
|
|
94
|
+
// Go has no file-scoped test selection: `go test` takes package
|
|
95
|
+
// patterns, not file paths (passing `foo_test.go` directly errors with
|
|
96
|
+
// "no Go files in ..."). Derive `go test ./<pkgdirs...>` from the
|
|
97
|
+
// directories of the related test files instead: dedupe, cap 10, and
|
|
98
|
+
// reuse the same containment-validated `safe` list as every other
|
|
99
|
+
// branch (untrusted paths never reach the shell).
|
|
100
|
+
const dirs = [];
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
for (const f of safe) {
|
|
103
|
+
const d = path.dirname(f);
|
|
104
|
+
const pkg = d === '.' ? '.' : `./${d.split(path.sep).join('/')}`;
|
|
105
|
+
if (!seen.has(pkg)) {
|
|
106
|
+
seen.add(pkg);
|
|
107
|
+
dirs.push(pkg);
|
|
108
|
+
}
|
|
109
|
+
if (dirs.length >= 10)
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (dirs.length === 0)
|
|
113
|
+
return null;
|
|
114
|
+
// Preserve a './...'-style base by replacing it; otherwise append.
|
|
115
|
+
if (baseCommand.includes('./...')) {
|
|
116
|
+
return baseCommand.replace('./...', dirs.join(' '));
|
|
117
|
+
}
|
|
118
|
+
return `${baseCommand} ${dirs.join(' ')}`;
|
|
119
|
+
}
|
|
71
120
|
return null;
|
|
72
121
|
}
|
|
73
122
|
const MAX_SCOPED_BYTES = 256 * 1024;
|
|
74
123
|
export async function runScopedVerify(cwd, command, timeoutMs = 45_000) {
|
|
75
124
|
return new Promise((resolve) => {
|
|
76
|
-
|
|
125
|
+
// S2: verify commands run with filtered env; servers needing keys must use explicit config.
|
|
126
|
+
const child = spawn(command, { cwd, shell: true, env: filteredVerifyEnv() });
|
|
77
127
|
const outChunks = [];
|
|
78
128
|
const errChunks = [];
|
|
79
129
|
let done = false;
|
|
@@ -125,7 +175,8 @@ export async function syntaxCheck(cwd, file) {
|
|
|
125
175
|
// For now, use tsc --noEmit --skipLibCheck on single file quickly
|
|
126
176
|
if (ext === '.ts') {
|
|
127
177
|
const ok = await new Promise((resolve) => {
|
|
128
|
-
|
|
178
|
+
// S2: verify commands run with filtered env; servers needing keys must use explicit config.
|
|
179
|
+
const child = spawn('npx', ['tsc', '--noEmit', '--skipLibCheck', full], { cwd, shell: false, env: filteredVerifyEnv() });
|
|
129
180
|
let done = false;
|
|
130
181
|
const t = setTimeout(() => { if (!done) {
|
|
131
182
|
done = true;
|
|
@@ -145,7 +196,8 @@ export async function syntaxCheck(cwd, file) {
|
|
|
145
196
|
return { ok: true };
|
|
146
197
|
}
|
|
147
198
|
const ok2 = await new Promise((resolve) => {
|
|
148
|
-
|
|
199
|
+
// S2: verify commands run with filtered env; servers needing keys must use explicit config.
|
|
200
|
+
const child = spawn(process.execPath, ['--check', full], { cwd, shell: false, env: filteredVerifyEnv() });
|
|
149
201
|
let done = false;
|
|
150
202
|
const t = setTimeout(() => { if (!done) {
|
|
151
203
|
done = true;
|
|
@@ -170,7 +222,8 @@ export async function syntaxCheck(cwd, file) {
|
|
|
170
222
|
}
|
|
171
223
|
if (ext === '.py') {
|
|
172
224
|
const ok = await new Promise((resolve) => {
|
|
173
|
-
|
|
225
|
+
// S2: verify commands run with filtered env; servers needing keys must use explicit config.
|
|
226
|
+
const child = spawn('python', ['-m', 'py_compile', full], { cwd, shell: false, env: filteredVerifyEnv() });
|
|
174
227
|
let done = false;
|
|
175
228
|
const t = setTimeout(() => { if (!done) {
|
|
176
229
|
done = true;
|