deepseek-pp-shell-host 0.6.3 → 0.6.4
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/native/shell-mcp-host.mjs +462 -3
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync, spawn } from 'node:child_process';
|
|
3
|
-
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import {
|
|
6
6
|
arch,
|
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
hostname,
|
|
9
9
|
platform,
|
|
10
10
|
release as osRelease,
|
|
11
|
+
tmpdir,
|
|
11
12
|
type as osType,
|
|
12
13
|
version as osVersion,
|
|
13
14
|
} from 'node:os';
|
|
14
|
-
import { existsSync } from 'node:fs';
|
|
15
|
+
import { existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
|
15
16
|
|
|
16
17
|
// Resolve package root from this script's location (native/ -> package root).
|
|
17
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -32,8 +33,15 @@ const userBinDirs = platform() === 'win32'
|
|
|
32
33
|
? [resolve(localAppData, 'OfficeCLI')]
|
|
33
34
|
: [
|
|
34
35
|
resolve(homedir(), '.local', 'bin'),
|
|
36
|
+
resolve(homedir(), '.pyenv', 'shims'),
|
|
37
|
+
resolve(homedir(), 'miniconda3', 'bin'),
|
|
38
|
+
resolve(homedir(), 'anaconda3', 'bin'),
|
|
39
|
+
resolve(homedir(), 'miniforge3', 'bin'),
|
|
40
|
+
resolve(homedir(), 'mambaforge', 'bin'),
|
|
35
41
|
'/opt/homebrew/bin',
|
|
36
42
|
'/usr/local/bin',
|
|
43
|
+
'/usr/bin',
|
|
44
|
+
'/bin',
|
|
37
45
|
];
|
|
38
46
|
const managedPathDirs = new Set([nodeBinDir, ...localBinDirs, ...userBinDirs]);
|
|
39
47
|
const existingPathDirs = splitPath(currentPath).filter(d => !managedPathDirs.has(d));
|
|
@@ -49,6 +57,11 @@ setEnvironmentPath(process.env, hostPath);
|
|
|
49
57
|
const MCP_PROTOCOL_VERSION = '2025-06-18';
|
|
50
58
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
51
59
|
const MAX_OUTPUT_BYTES = 128_000;
|
|
60
|
+
const DEFAULT_PYTHON_TIMEOUT_MS = 10_000;
|
|
61
|
+
const MAX_PYTHON_TIMEOUT_MS = 30_000;
|
|
62
|
+
const MAX_PYTHON_CODE_BYTES = 60_000;
|
|
63
|
+
const MAX_PYTHON_OUTPUT_BYTES = 64_000;
|
|
64
|
+
const PYTHON_PACKAGE_CHECKS = ['numpy', 'pandas', 'sympy'];
|
|
52
65
|
const DEFAULT_SHELL = platform() === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/sh';
|
|
53
66
|
const WINDOWS_POWERSHELL_UTF8_PREAMBLE = [
|
|
54
67
|
'[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)',
|
|
@@ -82,6 +95,28 @@ const TOOL_DEFINITIONS = [
|
|
|
82
95
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
83
96
|
annotations: { operation: 'read', risk: 'low' },
|
|
84
97
|
},
|
|
98
|
+
{
|
|
99
|
+
name: 'python_status',
|
|
100
|
+
title: 'Python Interpreter Status',
|
|
101
|
+
description: 'Report whether a local Python interpreter is available and which quick-validation packages can be imported.',
|
|
102
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
103
|
+
annotations: { operation: 'read', risk: 'low' },
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'python_exec',
|
|
107
|
+
title: 'Execute Python Code',
|
|
108
|
+
description: 'Run short Python code for calculation, reasoning checks, and small data transformations. Do not install packages, access sensitive local files, or use network access.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
code: { type: 'string', description: 'Short Python code to execute. Keep it focused on computation or validation.' },
|
|
113
|
+
timeout_ms: { type: 'integer', minimum: 1000, maximum: MAX_PYTHON_TIMEOUT_MS, description: 'Timeout in milliseconds. Default 10000.' },
|
|
114
|
+
},
|
|
115
|
+
required: ['code'],
|
|
116
|
+
additionalProperties: false,
|
|
117
|
+
},
|
|
118
|
+
annotations: { operation: 'execute', risk: 'high' },
|
|
119
|
+
},
|
|
85
120
|
];
|
|
86
121
|
|
|
87
122
|
// --- Native messaging framing (4-byte LE length prefix) ---
|
|
@@ -176,7 +211,7 @@ function handleInitialize(id) {
|
|
|
176
211
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
177
212
|
capabilities: { tools: {} },
|
|
178
213
|
serverInfo: { name: 'deepseek-pp-shell', version: '1.0.0' },
|
|
179
|
-
instructions: 'General-purpose shell execution host. Use shell_exec
|
|
214
|
+
instructions: 'General-purpose shell execution host. Use shell_exec for local commands and python_exec only for short computation or validation snippets.',
|
|
180
215
|
});
|
|
181
216
|
}
|
|
182
217
|
|
|
@@ -244,6 +279,14 @@ async function handleCallTool(id, params) {
|
|
|
244
279
|
}
|
|
245
280
|
}
|
|
246
281
|
|
|
282
|
+
if (name === 'python_status') {
|
|
283
|
+
return jsonRpcResult(id, await createPythonStatusResult());
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (name === 'python_exec') {
|
|
287
|
+
return jsonRpcResult(id, await executePythonTool(args));
|
|
288
|
+
}
|
|
289
|
+
|
|
247
290
|
return jsonRpcError(id, -32602, `Unknown tool: ${name}`);
|
|
248
291
|
}
|
|
249
292
|
|
|
@@ -310,6 +353,412 @@ function execCommand(command, { cwd, env, timeoutMs }) {
|
|
|
310
353
|
});
|
|
311
354
|
}
|
|
312
355
|
|
|
356
|
+
async function createPythonStatusResult() {
|
|
357
|
+
const status = await detectPythonStatus();
|
|
358
|
+
const text = status.available
|
|
359
|
+
? `Python ${status.version} ready at ${status.executable}`
|
|
360
|
+
: 'No local Python interpreter found. Tried environment variables, common paths, and python/python3/py --version.';
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
content: [{ type: 'text', text }],
|
|
364
|
+
structuredContent: {
|
|
365
|
+
ok: true,
|
|
366
|
+
data: status,
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function executePythonTool(args) {
|
|
372
|
+
const code = args?.code;
|
|
373
|
+
if (typeof code !== 'string' || code.trim().length === 0) {
|
|
374
|
+
return {
|
|
375
|
+
isError: true,
|
|
376
|
+
content: [{ type: 'text', text: 'code is required and must be a non-empty string.' }],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const codeBytes = Buffer.byteLength(code, 'utf8');
|
|
381
|
+
if (codeBytes > MAX_PYTHON_CODE_BYTES) {
|
|
382
|
+
return {
|
|
383
|
+
isError: true,
|
|
384
|
+
content: [{ type: 'text', text: `code exceeds ${MAX_PYTHON_CODE_BYTES} bytes.` }],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const timeoutMs = typeof args.timeout_ms === 'number' && args.timeout_ms >= 1000
|
|
389
|
+
? Math.min(Math.floor(args.timeout_ms), MAX_PYTHON_TIMEOUT_MS)
|
|
390
|
+
: DEFAULT_PYTHON_TIMEOUT_MS;
|
|
391
|
+
const status = await detectPythonStatus();
|
|
392
|
+
|
|
393
|
+
if (!status.available || !status.command) {
|
|
394
|
+
return {
|
|
395
|
+
isError: true,
|
|
396
|
+
content: [{ type: 'text', text: 'No local Python interpreter found. Tried environment variables, common paths, and python/python3/py --version.' }],
|
|
397
|
+
structuredContent: {
|
|
398
|
+
ok: false,
|
|
399
|
+
data: status,
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const cwd = mkdtempSync(join(tmpdir(), 'deepseek-pp-python-'));
|
|
405
|
+
try {
|
|
406
|
+
const result = await execPythonProcess(status.command, status.commandArgs ?? [], {
|
|
407
|
+
code,
|
|
408
|
+
cwd,
|
|
409
|
+
timeoutMs,
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
content: [{ type: 'text', text: formatPythonExecSummary(result) }],
|
|
413
|
+
structuredContent: {
|
|
414
|
+
ok: result.exitCode === 0,
|
|
415
|
+
data: {
|
|
416
|
+
...result,
|
|
417
|
+
pythonPath: status.executable,
|
|
418
|
+
pythonVersion: status.version,
|
|
419
|
+
cwd: '(temporary scratch directory)',
|
|
420
|
+
limits: getPythonLimits(),
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
isError: result.exitCode !== 0,
|
|
424
|
+
};
|
|
425
|
+
} catch (err) {
|
|
426
|
+
return {
|
|
427
|
+
isError: true,
|
|
428
|
+
content: [{ type: 'text', text: err.message }],
|
|
429
|
+
};
|
|
430
|
+
} finally {
|
|
431
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function detectPythonStatus() {
|
|
436
|
+
const candidates = getPythonCandidates();
|
|
437
|
+
const candidateLabels = candidates.map(formatPythonCandidate);
|
|
438
|
+
|
|
439
|
+
for (const candidate of candidates) {
|
|
440
|
+
let versionText = null;
|
|
441
|
+
try {
|
|
442
|
+
const versionProbe = await execPythonVersionProbe(candidate);
|
|
443
|
+
versionText = parsePythonVersionOutput(versionProbe);
|
|
444
|
+
if (versionProbe.exitCode !== 0 || !versionText) continue;
|
|
445
|
+
} catch {
|
|
446
|
+
// Try the next environment value, path, or command name.
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
const probe = await execPythonProbe(candidate);
|
|
452
|
+
if (probe.exitCode !== 0 || !probe.stdout.trim()) continue;
|
|
453
|
+
const data = JSON.parse(probe.stdout.trim());
|
|
454
|
+
return {
|
|
455
|
+
available: true,
|
|
456
|
+
command: candidate.command,
|
|
457
|
+
commandArgs: getPythonCommandArgs(candidate),
|
|
458
|
+
executable: typeof data.executable === 'string' ? data.executable : candidate.command,
|
|
459
|
+
version: typeof data.version === 'string' ? data.version : versionText,
|
|
460
|
+
versionCheck: versionText,
|
|
461
|
+
packages: normalizePythonPackages(data.packages),
|
|
462
|
+
candidates: candidateLabels,
|
|
463
|
+
isolation: 'python -I',
|
|
464
|
+
policy: getPythonPolicy(),
|
|
465
|
+
limits: getPythonLimits(),
|
|
466
|
+
};
|
|
467
|
+
} catch {
|
|
468
|
+
// --version worked, but the JSON probe failed; try the next common executable name.
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
available: false,
|
|
474
|
+
command: null,
|
|
475
|
+
commandArgs: [],
|
|
476
|
+
executable: null,
|
|
477
|
+
version: null,
|
|
478
|
+
versionCheck: null,
|
|
479
|
+
packages: Object.fromEntries(PYTHON_PACKAGE_CHECKS.map((name) => [name, false])),
|
|
480
|
+
candidates: candidateLabels,
|
|
481
|
+
isolation: 'python -I',
|
|
482
|
+
policy: getPythonPolicy(),
|
|
483
|
+
limits: getPythonLimits(),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function getPythonCandidates() {
|
|
488
|
+
const envCandidates = getPythonEnvCandidates();
|
|
489
|
+
const pathCandidates = getPythonPathCandidates();
|
|
490
|
+
const fallbackCandidates = platform() === 'win32'
|
|
491
|
+
? [
|
|
492
|
+
{ command: 'py', args: [], launcherArgs: ['-3'], source: 'command:py -3 --version' },
|
|
493
|
+
{ command: 'py.exe', args: [], launcherArgs: ['-3'], source: 'command:py.exe -3 --version' },
|
|
494
|
+
{ command: 'python', args: [], source: 'command:python --version' },
|
|
495
|
+
{ command: 'python.exe', args: [], source: 'command:python.exe --version' },
|
|
496
|
+
{ command: 'python3', args: [], source: 'command:python3 --version' },
|
|
497
|
+
{ command: 'python3.exe', args: [], source: 'command:python3.exe --version' },
|
|
498
|
+
]
|
|
499
|
+
: [
|
|
500
|
+
{ command: 'python3', args: [], source: 'command:python3 --version' },
|
|
501
|
+
{ command: 'python', args: [], source: 'command:python --version' },
|
|
502
|
+
{ command: 'py', args: [], source: 'command:py --version' },
|
|
503
|
+
];
|
|
504
|
+
return dedupePythonCandidates([...envCandidates, ...pathCandidates, ...fallbackCandidates]);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function getPythonEnvCandidates() {
|
|
508
|
+
const names = [
|
|
509
|
+
'DEEPSEEK_PP_PYTHON',
|
|
510
|
+
'PYTHON_EXECUTABLE',
|
|
511
|
+
'PYTHON',
|
|
512
|
+
'PYTHON3',
|
|
513
|
+
];
|
|
514
|
+
const candidates = [];
|
|
515
|
+
for (const name of names) {
|
|
516
|
+
const value = process.env[name];
|
|
517
|
+
if (typeof value !== 'string' || value.trim().length === 0) continue;
|
|
518
|
+
candidates.push({ command: value.trim(), args: [], source: 'env:' + name });
|
|
519
|
+
}
|
|
520
|
+
return candidates;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function getPythonPathCandidates() {
|
|
524
|
+
return platform() === 'win32' ? getWindowsPythonPathCandidates() : getPosixPythonPathCandidates();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function getPosixPythonPathCandidates() {
|
|
528
|
+
const candidates = [];
|
|
529
|
+
const directPaths = [
|
|
530
|
+
resolve(homedir(), '.pyenv', 'shims', 'python3'),
|
|
531
|
+
resolve(homedir(), '.pyenv', 'shims', 'python'),
|
|
532
|
+
resolve(homedir(), 'miniconda3', 'bin', 'python'),
|
|
533
|
+
resolve(homedir(), 'anaconda3', 'bin', 'python'),
|
|
534
|
+
resolve(homedir(), 'miniforge3', 'bin', 'python'),
|
|
535
|
+
resolve(homedir(), 'mambaforge', 'bin', 'python'),
|
|
536
|
+
'/opt/homebrew/bin/python3',
|
|
537
|
+
'/opt/homebrew/bin/python',
|
|
538
|
+
'/usr/local/bin/python3',
|
|
539
|
+
'/usr/local/bin/python',
|
|
540
|
+
'/usr/bin/python3',
|
|
541
|
+
'/usr/bin/python',
|
|
542
|
+
'/bin/python3',
|
|
543
|
+
'/bin/python',
|
|
544
|
+
];
|
|
545
|
+
for (const pythonPath of directPaths) addPythonPathCandidate(candidates, pythonPath, 'path:file');
|
|
546
|
+
for (const root of ['miniconda3', 'anaconda3', 'miniforge3', 'mambaforge']) {
|
|
547
|
+
addPythonEnvDirCandidates(candidates, resolve(homedir(), root, 'envs'));
|
|
548
|
+
}
|
|
549
|
+
addPythonEnvDirCandidates(candidates, resolve(homedir(), '.pyenv', 'versions'));
|
|
550
|
+
return candidates;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function getWindowsPythonPathCandidates() {
|
|
554
|
+
const candidates = [];
|
|
555
|
+
const dirs = [
|
|
556
|
+
resolve(localAppData, 'Programs', 'Python'),
|
|
557
|
+
process.env.ProgramFiles ? resolve(process.env.ProgramFiles) : '',
|
|
558
|
+
process.env['ProgramFiles(x86)'] ? resolve(process.env['ProgramFiles(x86)']) : '',
|
|
559
|
+
].filter(Boolean);
|
|
560
|
+
addPythonPathCandidate(candidates, resolve(localAppData, 'Microsoft', 'WindowsApps', 'python.exe'), 'path:file');
|
|
561
|
+
for (const dir of dirs) {
|
|
562
|
+
for (const entry of readDirectoryEntries(dir)) {
|
|
563
|
+
if (!/^Python\d+/i.test(entry.name)) continue;
|
|
564
|
+
addPythonPathCandidate(candidates, resolve(dir, entry.name, 'python.exe'), 'path:file');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return candidates;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function addPythonEnvDirCandidates(candidates, envsDir) {
|
|
571
|
+
for (const entry of readDirectoryEntries(envsDir)) {
|
|
572
|
+
if (!entry.isDirectory()) continue;
|
|
573
|
+
const pythonPath = platform() === 'win32'
|
|
574
|
+
? resolve(envsDir, entry.name, 'python.exe')
|
|
575
|
+
: resolve(envsDir, entry.name, 'bin', 'python');
|
|
576
|
+
addPythonPathCandidate(candidates, pythonPath, 'path:env');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function addPythonPathCandidate(candidates, pythonPath, source) {
|
|
581
|
+
if (!existsSync(pythonPath)) return;
|
|
582
|
+
candidates.push({ command: pythonPath, args: [], source });
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function readDirectoryEntries(dir) {
|
|
586
|
+
try {
|
|
587
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
588
|
+
} catch {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function dedupePythonCandidates(candidates) {
|
|
593
|
+
const seen = new Set();
|
|
594
|
+
const result = [];
|
|
595
|
+
for (const candidate of candidates) {
|
|
596
|
+
const key = [candidate.command, ...(candidate.launcherArgs ?? []), ...candidate.args].join('\0');
|
|
597
|
+
if (seen.has(key)) continue;
|
|
598
|
+
seen.add(key);
|
|
599
|
+
result.push(candidate);
|
|
600
|
+
}
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function getPythonCommandArgs(candidate) {
|
|
605
|
+
return [...(candidate.launcherArgs ?? []), ...candidate.args];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function formatPythonCandidate(candidate) {
|
|
609
|
+
const label = [candidate.command, ...getPythonCommandArgs(candidate)].join(' ');
|
|
610
|
+
return candidate.source ? label + ' (' + candidate.source + ')' : label;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function execPythonVersionProbe(candidate) {
|
|
614
|
+
return execProcess(candidate.command, [...getPythonCommandArgs(candidate), '--version'], {
|
|
615
|
+
cwd: homedir(),
|
|
616
|
+
env: createPythonChildEnv(),
|
|
617
|
+
timeoutMs: 2_000,
|
|
618
|
+
maxOutputBytes: 2_000,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function parsePythonVersionOutput(probe) {
|
|
623
|
+
const text = [probe.stdout, probe.stderr].join(' ').replace(/\s+/g, ' ').trim();
|
|
624
|
+
const match = text.match(/Python\s+([0-9]+(?:\.[0-9]+){1,2})/i);
|
|
625
|
+
return match ? match[1] : null;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function execPythonProbe(candidate) {
|
|
629
|
+
const code = [
|
|
630
|
+
'import importlib.util, json, sys',
|
|
631
|
+
`packages = {name: importlib.util.find_spec(name) is not None for name in ${JSON.stringify(PYTHON_PACKAGE_CHECKS)}}`,
|
|
632
|
+
'print(json.dumps({"executable": sys.executable, "version": sys.version.split()[0], "packages": packages}, ensure_ascii=False))',
|
|
633
|
+
].join('\n');
|
|
634
|
+
|
|
635
|
+
return execProcess(candidate.command, [...getPythonCommandArgs(candidate), '-I', '-c', code], {
|
|
636
|
+
cwd: homedir(),
|
|
637
|
+
env: createPythonChildEnv(),
|
|
638
|
+
timeoutMs: 5_000,
|
|
639
|
+
maxOutputBytes: 16_000,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function execPythonProcess(command, commandArgs, { code, cwd, timeoutMs }) {
|
|
644
|
+
return execProcess(command, [...commandArgs, '-I', '-'], {
|
|
645
|
+
cwd,
|
|
646
|
+
env: createPythonChildEnv(),
|
|
647
|
+
input: code,
|
|
648
|
+
timeoutMs,
|
|
649
|
+
maxOutputBytes: MAX_PYTHON_OUTPUT_BYTES,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function execProcess(command, args, { cwd, env, input, timeoutMs, maxOutputBytes }) {
|
|
654
|
+
return new Promise((resolve, reject) => {
|
|
655
|
+
const startedAt = Date.now();
|
|
656
|
+
const child = spawn(command, args, {
|
|
657
|
+
cwd,
|
|
658
|
+
env,
|
|
659
|
+
shell: false,
|
|
660
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
661
|
+
windowsHide: true,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
const stdout = [];
|
|
665
|
+
const stderr = [];
|
|
666
|
+
let stdoutBytes = 0;
|
|
667
|
+
let stderrBytes = 0;
|
|
668
|
+
let timedOut = false;
|
|
669
|
+
|
|
670
|
+
const timer = setTimeout(() => {
|
|
671
|
+
timedOut = true;
|
|
672
|
+
child.kill('SIGTERM');
|
|
673
|
+
setTimeout(() => child.kill('SIGKILL'), 3000);
|
|
674
|
+
}, timeoutMs);
|
|
675
|
+
|
|
676
|
+
child.stdout.on('data', (chunk) => {
|
|
677
|
+
if (stdoutBytes < maxOutputBytes) {
|
|
678
|
+
const remaining = maxOutputBytes - stdoutBytes;
|
|
679
|
+
stdout.push(chunk.length <= remaining ? chunk : chunk.subarray(0, remaining));
|
|
680
|
+
}
|
|
681
|
+
stdoutBytes += chunk.length;
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
child.stderr.on('data', (chunk) => {
|
|
685
|
+
if (stderrBytes < maxOutputBytes) {
|
|
686
|
+
const remaining = maxOutputBytes - stderrBytes;
|
|
687
|
+
stderr.push(chunk.length <= remaining ? chunk : chunk.subarray(0, remaining));
|
|
688
|
+
}
|
|
689
|
+
stderrBytes += chunk.length;
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
child.on('error', (err) => {
|
|
693
|
+
clearTimeout(timer);
|
|
694
|
+
reject(new Error(`Failed to spawn ${command}: ${err.message}`));
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
child.on('close', (exitCode, signal) => {
|
|
698
|
+
clearTimeout(timer);
|
|
699
|
+
resolve({
|
|
700
|
+
command: [command, ...args].join(' '),
|
|
701
|
+
exitCode: timedOut ? -1 : (exitCode ?? -1),
|
|
702
|
+
signal: signal || (timedOut ? 'SIGTERM' : null),
|
|
703
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
704
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
705
|
+
truncated: stdoutBytes > maxOutputBytes || stderrBytes > maxOutputBytes,
|
|
706
|
+
timedOut,
|
|
707
|
+
durationMs: Date.now() - startedAt,
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
if (input != null) {
|
|
712
|
+
child.stdin.end(input);
|
|
713
|
+
} else {
|
|
714
|
+
child.stdin.end();
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function createPythonChildEnv() {
|
|
720
|
+
const env = {};
|
|
721
|
+
const keys = platform() === 'win32'
|
|
722
|
+
? ['SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT', 'TEMP', 'TMP', 'USERPROFILE', 'LOCALAPPDATA', 'APPDATA']
|
|
723
|
+
: ['HOME', 'TMPDIR', 'TEMP', 'TMP', 'LANG', 'LC_ALL', 'LC_CTYPE'];
|
|
724
|
+
|
|
725
|
+
for (const key of keys) {
|
|
726
|
+
if (typeof process.env[key] === 'string') env[key] = process.env[key];
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
setEnvironmentPath(env, getEnvironmentPath(process.env));
|
|
730
|
+
env.PYTHONUTF8 = '1';
|
|
731
|
+
env.PYTHONIOENCODING = 'utf-8';
|
|
732
|
+
env.PYTHONNOUSERSITE = '1';
|
|
733
|
+
env.PIP_DISABLE_PIP_VERSION_CHECK = '1';
|
|
734
|
+
return env;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function normalizePythonPackages(value) {
|
|
738
|
+
const input = value && typeof value === 'object' ? value : {};
|
|
739
|
+
return Object.fromEntries(
|
|
740
|
+
PYTHON_PACKAGE_CHECKS.map((name) => [name, input[name] === true]),
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function getPythonPolicy() {
|
|
745
|
+
return {
|
|
746
|
+
purpose: 'short computation, idea validation, and small data transformations',
|
|
747
|
+
packageInstall: false,
|
|
748
|
+
networkAccess: 'not_allowed_by_policy_not_os_enforced',
|
|
749
|
+
filesystemAccess: 'temporary_cwd_only_by_policy_not_os_enforced',
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function getPythonLimits() {
|
|
754
|
+
return {
|
|
755
|
+
timeoutMsDefault: DEFAULT_PYTHON_TIMEOUT_MS,
|
|
756
|
+
timeoutMsMax: MAX_PYTHON_TIMEOUT_MS,
|
|
757
|
+
codeBytesMax: MAX_PYTHON_CODE_BYTES,
|
|
758
|
+
outputBytesMax: MAX_PYTHON_OUTPUT_BYTES,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
313
762
|
function createChildEnv(extraEnv) {
|
|
314
763
|
const explicitPath = getExplicitPathOverride(extraEnv);
|
|
315
764
|
const env = extraEnv && typeof extraEnv === 'object' ? { ...process.env, ...extraEnv } : { ...process.env };
|
|
@@ -433,6 +882,16 @@ function formatExecSummary(result) {
|
|
|
433
882
|
return parts.join('\n') || '(no output)';
|
|
434
883
|
}
|
|
435
884
|
|
|
885
|
+
function formatPythonExecSummary(result) {
|
|
886
|
+
const parts = [];
|
|
887
|
+
if (result.timedOut) parts.push('[TIMED OUT]');
|
|
888
|
+
if (result.exitCode !== 0) parts.push(`[exit ${result.exitCode}]`);
|
|
889
|
+
if (result.truncated) parts.push('[output truncated]');
|
|
890
|
+
if (result.stdout) parts.push(result.stdout.slice(0, 4000));
|
|
891
|
+
if (result.stderr) parts.push(`STDERR: ${result.stderr.slice(0, 2000)}`);
|
|
892
|
+
return parts.join('\n') || '(no output)';
|
|
893
|
+
}
|
|
894
|
+
|
|
436
895
|
// --- Message dispatch ---
|
|
437
896
|
|
|
438
897
|
async function handleMessage(envelope) {
|