@larkup/sandbox 0.1.5 → 0.1.6
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +22 -13
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/local-runner.d.ts +3 -1
- package/dist/local-runner.js +132 -27
- package/dist/sandbox-manager.js +4 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/local-runner-provisioning.test.ts +96 -0
- package/src/local-runner.test.ts +2 -2
- package/src/local-runner.ts +212 -30
- package/src/sandbox-manager.test.ts +12 -0
- package/src/sandbox-manager.ts +4 -1
package/.turbo/turbo-build.log
CHANGED
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
|
|
2
|
-
> @larkup/sandbox@0.1.
|
|
2
|
+
> @larkup/sandbox@0.1.6 test /Users/abdelrahmanabounida/Desktop/my-ai-startup/larkup/larkup/packages/sandbox
|
|
3
3
|
> vitest run
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
RUN v4.1.11 /Users/abdelrahmanabounida/Desktop/my-ai-startup/larkup/larkup/packages/sandbox
|
|
7
|
+
|
|
8
|
+
✓ src/providers/vercel.test.ts (6 tests) 20ms
|
|
9
|
+
✓ src/sandbox-manager.test.ts (3 tests) 6ms
|
|
10
|
+
✓ src/remote-exec.test.ts (14 tests) 8ms
|
|
11
|
+
✓ src/providers/e2b.test.ts (9 tests) 18ms
|
|
12
|
+
✓ src/providers/browserbase.test.ts (5 tests) 4ms
|
|
13
|
+
✓ src/local-runner-provisioning.test.ts (1 test) 15ms
|
|
14
|
+
✓ src/providers/daytona.test.ts (5 tests) 6ms
|
|
15
|
+
✓ src/local-runner.test.ts (2 tests) 292ms
|
|
16
|
+
✓ src/providers/northflank.test.ts (7 tests) 9ms
|
|
17
|
+
✓ src/providers/flyio.test.ts (6 tests) 8ms
|
|
18
|
+
✓ src/registry.test.ts (9 tests) 10ms
|
|
19
|
+
✓ src/providers/cloudflare.test.ts (6 tests) 40ms
|
|
20
|
+
✓ src/providers/modal.test.ts (4 tests) 9ms
|
|
21
|
+
✓ src/providers/webcontainers.test.ts (3 tests) 36ms
|
|
22
|
+
|
|
23
|
+
Test Files 14 passed (14)
|
|
24
|
+
Tests 80 passed (80)
|
|
25
|
+
Start at 11:36:11
|
|
26
|
+
Duration 2.02s (transform 1.97s, setup 0ms, import 4.11s, tests 481ms, environment 3ms)
|
|
7
27
|
|
|
8
|
-
[32m✓[39m src/remote-exec.test.ts [2m([22m[2m14 tests[22m[2m)[22m[32m 116[2mms[22m[39m
|
|
9
|
-
[32m✓[39m src/providers/flyio.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
10
|
-
[32m✓[39m src/providers/modal.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 10[2mms[22m[39m
|
|
11
|
-
[32m✓[39m src/providers/daytona.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
12
|
-
[32m✓[39m src/providers/vercel.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 36[2mms[22m[39m
|
|
13
|
-
[32m✓[39m src/providers/e2b.test.ts [2m([22m[2m9 tests[22m[2m)[22m[32m 45[2mms[22m[39m
|
|
14
|
-
[32m✓[39m src/providers/northflank.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 39[2mms[22m[39m
|
|
15
|
-
[32m✓[39m src/local-runner.test.ts [2m([22m[2m2 tests[22m[2m)[22m[33m 505[2mms[22m[39m
|
|
16
|
-
[33m[2m✓[22m[39m runs JavaScript without Docker and returns output artifacts [33m 359[2mms[22m[39m
|
|
17
|
-
[32m✓[39m src/providers/cloudflare.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 14[2mms[22m[39m
|
|
18
|
-
ELIFECYCLE Test failed. See above for more details.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @larkup/sandbox
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ceb12a5: Prepare an isolated local Python environment automatically for analysis tools, without exposing automatic sandbox readiness or dependency status in Settings.
|
|
8
|
+
|
|
3
9
|
## 0.1.5
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { SandboxManager, verifySandboxProvider } from './sandbox-manager.js';
|
|
7
7
|
export { checkDockerHealth, buildSandboxImage, ensureImage, executeInDocker, } from './docker-runner.js';
|
|
8
|
-
export { checkLocalRuntime, executeLocally } from './local-runner.js';
|
|
8
|
+
export { checkLocalRuntime, executeLocally, setupLocalRuntime } from './local-runner.js';
|
|
9
9
|
export { SANDBOX_PROVIDERS, SANDBOX_PROVIDER_LIST, getSandboxProvider, validateSandboxCredentials, } from './registry.js';
|
|
10
10
|
export type { SandboxBackend, SandboxConfig, DockerConfig, SandboxLanguage, SandboxFile, ExecutionRequest, ExecutionResult, ExecutionArtifact, SandboxStatus, SandboxHealthCheck, SandboxCredentialField, SandboxProviderDescriptor, SandboxProviderAdapter, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { SandboxManager, verifySandboxProvider } from './sandbox-manager.js';
|
|
7
7
|
export { checkDockerHealth, buildSandboxImage, ensureImage, executeInDocker, } from './docker-runner.js';
|
|
8
|
-
export { checkLocalRuntime, executeLocally } from './local-runner.js';
|
|
8
|
+
export { checkLocalRuntime, executeLocally, setupLocalRuntime } from './local-runner.js';
|
|
9
9
|
export { SANDBOX_PROVIDERS, SANDBOX_PROVIDER_LIST, getSandboxProvider, validateSandboxCredentials, } from './registry.js';
|
package/dist/local-runner.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ExecutionRequest, ExecutionResult, SandboxHealthCheck } from './types.js';
|
|
2
|
-
/**
|
|
2
|
+
/** Creates or repairs Larkup's isolated local Python analysis environment. */
|
|
3
|
+
export declare function setupLocalRuntime(): Promise<void>;
|
|
4
|
+
/** Checks whether the host can bootstrap Larkup's managed Python environment. */
|
|
3
5
|
export declare function checkLocalRuntime(): Promise<SandboxHealthCheck>;
|
|
4
6
|
/** Executes trusted code in a temporary host workspace and collects its output. */
|
|
5
7
|
export declare function executeLocally(request: ExecutionRequest): Promise<ExecutionResult>;
|
package/dist/local-runner.js
CHANGED
|
@@ -5,15 +5,59 @@ import path from 'node:path';
|
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { getMimeType } from './mime.js';
|
|
7
7
|
let pythonCommand;
|
|
8
|
+
let managedPythonCommand;
|
|
8
9
|
// Keep this list in sync with the capabilities advertised by executeAnalysis
|
|
9
|
-
// and analyzeCorpusWithCode.
|
|
10
|
-
//
|
|
11
|
-
const ANALYSIS_PYTHON_MODULES = [
|
|
12
|
-
|
|
10
|
+
// and analyzeCorpusWithCode. These packages are installed in Larkup's managed
|
|
11
|
+
// virtual environment, never into the Python installation on the host.
|
|
12
|
+
const ANALYSIS_PYTHON_MODULES = [
|
|
13
|
+
'numpy',
|
|
14
|
+
'pandas',
|
|
15
|
+
'matplotlib',
|
|
16
|
+
'scipy',
|
|
17
|
+
'sklearn',
|
|
18
|
+
'seaborn',
|
|
19
|
+
'openpyxl',
|
|
20
|
+
'xlrd',
|
|
21
|
+
'pypdf',
|
|
22
|
+
'docx',
|
|
23
|
+
'pptx',
|
|
24
|
+
'PIL',
|
|
25
|
+
];
|
|
26
|
+
const ANALYSIS_PYTHON_PACKAGES = [
|
|
27
|
+
'pandas==2.2.3',
|
|
28
|
+
// Python 3.9 ships with several supported desktop environments. Keep the
|
|
29
|
+
// local pins compatible with it rather than requiring a user-managed upgrade.
|
|
30
|
+
'numpy==1.26.4',
|
|
31
|
+
'matplotlib==3.9.4',
|
|
32
|
+
'openpyxl==3.1.5',
|
|
33
|
+
'scipy==1.13.1',
|
|
34
|
+
'scikit-learn==1.6.1',
|
|
35
|
+
'seaborn==0.13.2',
|
|
36
|
+
'xlrd==2.0.1',
|
|
37
|
+
'pypdf==5.1.0',
|
|
38
|
+
'python-docx==1.1.2',
|
|
39
|
+
'python-pptx==1.0.2',
|
|
40
|
+
'Pillow==11.1.0',
|
|
41
|
+
];
|
|
42
|
+
function pythonEnvironment(virtualEnv) {
|
|
43
|
+
const env = { ...process.env };
|
|
44
|
+
// Let neither a user's active virtual environment nor PYTHONPATH leak into
|
|
45
|
+
// the managed runtime. This preserves the repeatable dependency set.
|
|
46
|
+
delete env.PYTHONHOME;
|
|
47
|
+
delete env.PYTHONPATH;
|
|
48
|
+
delete env.VIRTUAL_ENV;
|
|
49
|
+
if (virtualEnv) {
|
|
50
|
+
env.VIRTUAL_ENV = virtualEnv;
|
|
51
|
+
env.PYTHONNOUSERSITE = '1';
|
|
52
|
+
env.PATH = `${path.dirname(virtualEnvPython(virtualEnv))}${path.delimiter}${env.PATH ?? ''}`;
|
|
53
|
+
}
|
|
54
|
+
return env;
|
|
55
|
+
}
|
|
56
|
+
function run(command, args, cwd, timeoutMs = 10_000, env = process.env) {
|
|
13
57
|
return new Promise((resolve, reject) => {
|
|
14
58
|
const child = spawn(command.executable, [...command.args, ...args], {
|
|
15
59
|
cwd,
|
|
16
|
-
env
|
|
60
|
+
env,
|
|
17
61
|
shell: false,
|
|
18
62
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
19
63
|
});
|
|
@@ -63,7 +107,7 @@ async function resolvePython() {
|
|
|
63
107
|
];
|
|
64
108
|
for (const candidate of candidates) {
|
|
65
109
|
try {
|
|
66
|
-
if ((await run(candidate, ['--version'])).exitCode === 0) {
|
|
110
|
+
if ((await run(candidate, ['--version'], undefined, 10_000, pythonEnvironment())).exitCode === 0) {
|
|
67
111
|
pythonCommand = candidate;
|
|
68
112
|
return candidate;
|
|
69
113
|
}
|
|
@@ -75,6 +119,79 @@ async function resolvePython() {
|
|
|
75
119
|
pythonCommand = null;
|
|
76
120
|
return null;
|
|
77
121
|
}
|
|
122
|
+
function localRuntimeDirectory() {
|
|
123
|
+
const configured = process.env.LARKUP_SANDBOX_RUNTIME_DIR?.trim();
|
|
124
|
+
if (configured)
|
|
125
|
+
return path.resolve(configured);
|
|
126
|
+
const dataRoot = process.env.LARKUP_DATA_DIR?.trim();
|
|
127
|
+
return path.join(dataRoot ? path.resolve(dataRoot) : path.join(process.cwd(), '.larkup'), 'sandbox', 'python');
|
|
128
|
+
}
|
|
129
|
+
function virtualEnvPython(runtimeDirectory) {
|
|
130
|
+
return process.platform === 'win32'
|
|
131
|
+
? path.join(runtimeDirectory, 'Scripts', 'python.exe')
|
|
132
|
+
: path.join(runtimeDirectory, 'bin', 'python');
|
|
133
|
+
}
|
|
134
|
+
function dependencyCheckCode() {
|
|
135
|
+
return `import importlib.util\nmodules = ${JSON.stringify(ANALYSIS_PYTHON_MODULES)}\nmissing = [name for name in modules if importlib.util.find_spec(name) is None]\nprint(','.join(missing))\nraise SystemExit(bool(missing))`;
|
|
136
|
+
}
|
|
137
|
+
async function hasAnalysisDependencies(command, runtimeDirectory) {
|
|
138
|
+
try {
|
|
139
|
+
return ((await run(command, ['-c', dependencyCheckCode()], undefined, 30_000, pythonEnvironment(runtimeDirectory))).exitCode === 0);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function prepareManagedPythonRuntime() {
|
|
146
|
+
const bootstrapPython = await resolvePython();
|
|
147
|
+
if (!bootstrapPython) {
|
|
148
|
+
throw new Error('Local code execution needs Python 3.9 or later, or choose Docker or a remote sandbox provider.');
|
|
149
|
+
}
|
|
150
|
+
const runtimeDirectory = localRuntimeDirectory();
|
|
151
|
+
const managedPython = { executable: virtualEnvPython(runtimeDirectory), args: [] };
|
|
152
|
+
const bootstrapCheck = await run(bootstrapPython, ['-c', 'import sys, venv; raise SystemExit(sys.version_info < (3, 9))'], undefined, 10_000, pythonEnvironment());
|
|
153
|
+
if (bootstrapCheck.exitCode !== 0) {
|
|
154
|
+
throw new Error('Local code execution needs Python 3.9 or later with virtual-environment support. Install Python or choose Docker or a remote sandbox provider.');
|
|
155
|
+
}
|
|
156
|
+
const runtimeExists = await fs.access(managedPython.executable).then(() => true, () => false);
|
|
157
|
+
if (!runtimeExists) {
|
|
158
|
+
await fs.mkdir(path.dirname(runtimeDirectory), { recursive: true });
|
|
159
|
+
const created = await run(bootstrapPython, ['-m', 'venv', runtimeDirectory], undefined, 60_000, pythonEnvironment());
|
|
160
|
+
if (created.exitCode !== 0) {
|
|
161
|
+
throw new Error('Larkup could not create its local Python environment. Check that Python virtual environments are enabled, or choose Docker or a remote sandbox provider.');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (await hasAnalysisDependencies(managedPython, runtimeDirectory))
|
|
165
|
+
return managedPython;
|
|
166
|
+
const installed = await run(managedPython, [
|
|
167
|
+
'-m',
|
|
168
|
+
'pip',
|
|
169
|
+
'install',
|
|
170
|
+
'--disable-pip-version-check',
|
|
171
|
+
'--no-input',
|
|
172
|
+
...ANALYSIS_PYTHON_PACKAGES,
|
|
173
|
+
], undefined, 10 * 60_000, pythonEnvironment(runtimeDirectory));
|
|
174
|
+
if (installed.exitCode !== 0 ||
|
|
175
|
+
!(await hasAnalysisDependencies(managedPython, runtimeDirectory))) {
|
|
176
|
+
throw new Error('Larkup could not prepare its local analysis environment. Check the internet connection and retry, or choose Docker or a remote sandbox provider.');
|
|
177
|
+
}
|
|
178
|
+
return managedPython;
|
|
179
|
+
}
|
|
180
|
+
/** Creates or repairs Larkup's isolated local Python analysis environment. */
|
|
181
|
+
export async function setupLocalRuntime() {
|
|
182
|
+
if (!managedPythonCommand) {
|
|
183
|
+
managedPythonCommand = prepareManagedPythonRuntime().catch((error) => {
|
|
184
|
+
managedPythonCommand = undefined;
|
|
185
|
+
throw error;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
await managedPythonCommand;
|
|
189
|
+
}
|
|
190
|
+
async function getManagedPython() {
|
|
191
|
+
await setupLocalRuntime();
|
|
192
|
+
// setupLocalRuntime only resolves after prepareManagedPythonRuntime resolves.
|
|
193
|
+
return managedPythonCommand;
|
|
194
|
+
}
|
|
78
195
|
function assertSafeFileName(name) {
|
|
79
196
|
if (!name || name === '.' || name === '..' || /[\\/\0]/.test(name)) {
|
|
80
197
|
throw new Error('Sandbox file names must be plain file names.');
|
|
@@ -109,7 +226,7 @@ except Exception as error:
|
|
|
109
226
|
sys.exit(1)
|
|
110
227
|
`;
|
|
111
228
|
}
|
|
112
|
-
/** Checks whether the host can
|
|
229
|
+
/** Checks whether the host can bootstrap Larkup's managed Python environment. */
|
|
113
230
|
export async function checkLocalRuntime() {
|
|
114
231
|
const python = await resolvePython();
|
|
115
232
|
if (!python) {
|
|
@@ -120,17 +237,12 @@ export async function checkLocalRuntime() {
|
|
|
120
237
|
};
|
|
121
238
|
}
|
|
122
239
|
try {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
`import importlib.util\nmodules = ${JSON.stringify(ANALYSIS_PYTHON_MODULES)}\nmissing = [name for name in modules if importlib.util.find_spec(name) is None]\nprint(','.join(missing))\nraise SystemExit(bool(missing))`,
|
|
126
|
-
]);
|
|
127
|
-
if (dependencyCheck.exitCode !== 0) {
|
|
128
|
-
const missing = dependencyCheck.stdout.trim() || 'required analysis packages';
|
|
240
|
+
const bootstrapCheck = await run(python, ['-c', 'import sys, venv; raise SystemExit(sys.version_info < (3, 9))'], undefined, 10_000, pythonEnvironment());
|
|
241
|
+
if (bootstrapCheck.exitCode !== 0) {
|
|
129
242
|
return {
|
|
130
243
|
status: 'error',
|
|
131
244
|
backend: 'local',
|
|
132
|
-
error:
|
|
133
|
-
`(for example: pip install ${ANALYSIS_PYTHON_MODULES.join(' ')}) or choose Docker or a remote sandbox provider.`,
|
|
245
|
+
error: 'Local code execution needs Python 3.9 or later with virtual-environment support. Install Python or choose Docker or a remote sandbox provider.',
|
|
134
246
|
};
|
|
135
247
|
}
|
|
136
248
|
}
|
|
@@ -138,7 +250,7 @@ export async function checkLocalRuntime() {
|
|
|
138
250
|
return {
|
|
139
251
|
status: 'error',
|
|
140
252
|
backend: 'local',
|
|
141
|
-
error: 'Larkup could not verify
|
|
253
|
+
error: 'Larkup could not verify Python virtual-environment support. Choose Docker or a remote sandbox provider.',
|
|
142
254
|
};
|
|
143
255
|
}
|
|
144
256
|
return { status: 'ready', backend: 'local' };
|
|
@@ -157,22 +269,15 @@ export async function executeLocally(request) {
|
|
|
157
269
|
await fs.writeFile(path.join(inputDir, file.name), file.isBase64 ? Buffer.from(file.content, 'base64') : file.content);
|
|
158
270
|
}
|
|
159
271
|
const isPython = request.language === 'python';
|
|
160
|
-
const command = isPython
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
stdout: '',
|
|
164
|
-
stderr: 'Local code execution needs Python 3. Install Python or choose Docker or a remote sandbox provider.',
|
|
165
|
-
exitCode: 1,
|
|
166
|
-
artifacts: [],
|
|
167
|
-
executionTimeMs: Date.now() - startTime,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
272
|
+
const command = isPython
|
|
273
|
+
? await getManagedPython()
|
|
274
|
+
: { executable: process.execPath, args: [] };
|
|
170
275
|
const script = path.join(tempDir, isPython ? 'run.py' : 'run.js');
|
|
171
276
|
const source = isPython
|
|
172
277
|
? pythonWrapper(request.code, outputDir)
|
|
173
278
|
: `process.env.LARKUP_OUTPUT_DIR = ${JSON.stringify(outputDir)};\n${request.code}`;
|
|
174
279
|
await fs.writeFile(script, source, 'utf8');
|
|
175
|
-
const result = await run(command, [script], inputDir, request.timeout ?? 30_000);
|
|
280
|
+
const result = await run(command, [script], inputDir, request.timeout ?? 30_000, isPython ? pythonEnvironment(localRuntimeDirectory()) : process.env);
|
|
176
281
|
const artifacts = [];
|
|
177
282
|
for (const name of await fs.readdir(outputDir).catch(() => [])) {
|
|
178
283
|
const filePath = path.join(outputDir, name);
|
package/dist/sandbox-manager.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* lifecycle.
|
|
8
8
|
*/
|
|
9
9
|
import { checkDockerHealth, buildSandboxImage, ensureImage, executeInDocker, } from './docker-runner.js';
|
|
10
|
-
import { checkLocalRuntime, executeLocally } from './local-runner.js';
|
|
10
|
+
import { checkLocalRuntime, executeLocally, setupLocalRuntime } from './local-runner.js';
|
|
11
11
|
import { getSandboxProviderAdapter } from './providers/index.js';
|
|
12
12
|
const defaultDockerConfig = {
|
|
13
13
|
imageName: 'larkup-sandbox:latest',
|
|
@@ -45,6 +45,9 @@ export class SandboxManager {
|
|
|
45
45
|
async setup(onProgress) {
|
|
46
46
|
switch (this.config.backend) {
|
|
47
47
|
case 'local':
|
|
48
|
+
onProgress?.('Preparing Larkup local analysis environment…');
|
|
49
|
+
await setupLocalRuntime();
|
|
50
|
+
onProgress?.('Local analysis environment is ready ✓');
|
|
48
51
|
return;
|
|
49
52
|
case 'docker':
|
|
50
53
|
await buildSandboxImage(onProgress);
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export {
|
|
|
11
11
|
ensureImage,
|
|
12
12
|
executeInDocker,
|
|
13
13
|
} from './docker-runner.js';
|
|
14
|
-
export { checkLocalRuntime, executeLocally } from './local-runner.js';
|
|
14
|
+
export { checkLocalRuntime, executeLocally, setupLocalRuntime } from './local-runner.js';
|
|
15
15
|
export {
|
|
16
16
|
SANDBOX_PROVIDERS,
|
|
17
17
|
SANDBOX_PROVIDER_LIST,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
const childProcess = vi.hoisted(() => ({ spawn: vi.fn() }));
|
|
8
|
+
|
|
9
|
+
vi.mock('node:child_process', () => childProcess);
|
|
10
|
+
|
|
11
|
+
import { executeLocally, setupLocalRuntime } from './local-runner.js';
|
|
12
|
+
|
|
13
|
+
type SpawnCall = {
|
|
14
|
+
executable: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
options?: { env?: NodeJS.ProcessEnv };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const originalRuntimeDirectory = process.env.LARKUP_SANDBOX_RUNTIME_DIR;
|
|
20
|
+
let runtimeDirectory: string | undefined;
|
|
21
|
+
|
|
22
|
+
function successfulChild(stdout = '', stderr = '', exitCode = 0) {
|
|
23
|
+
const child = new EventEmitter() as EventEmitter & {
|
|
24
|
+
stdout: EventEmitter;
|
|
25
|
+
stderr: EventEmitter;
|
|
26
|
+
kill: () => void;
|
|
27
|
+
};
|
|
28
|
+
child.stdout = new EventEmitter();
|
|
29
|
+
child.stderr = new EventEmitter();
|
|
30
|
+
child.kill = () => {};
|
|
31
|
+
queueMicrotask(() => {
|
|
32
|
+
if (stdout) child.stdout.emit('data', Buffer.from(stdout));
|
|
33
|
+
if (stderr) child.stderr.emit('data', Buffer.from(stderr));
|
|
34
|
+
child.emit('close', exitCode);
|
|
35
|
+
});
|
|
36
|
+
return child;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
childProcess.spawn.mockReset();
|
|
41
|
+
if (runtimeDirectory) await rm(runtimeDirectory, { recursive: true, force: true });
|
|
42
|
+
runtimeDirectory = undefined;
|
|
43
|
+
if (originalRuntimeDirectory === undefined) delete process.env.LARKUP_SANDBOX_RUNTIME_DIR;
|
|
44
|
+
else process.env.LARKUP_SANDBOX_RUNTIME_DIR = originalRuntimeDirectory;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('managed local Python runtime', () => {
|
|
48
|
+
it('installs analysis packages in Larkup’s virtual environment before running Python', async () => {
|
|
49
|
+
runtimeDirectory = await mkdtemp(path.join(os.tmpdir(), 'larkup-sandbox-runtime-'));
|
|
50
|
+
process.env.LARKUP_SANDBOX_RUNTIME_DIR = runtimeDirectory;
|
|
51
|
+
const calls: SpawnCall[] = [];
|
|
52
|
+
let dependencyChecks = 0;
|
|
53
|
+
|
|
54
|
+
childProcess.spawn.mockImplementation(
|
|
55
|
+
(executable: string, args: string[], options?: SpawnCall['options']) => {
|
|
56
|
+
calls.push({ executable, args, options });
|
|
57
|
+
if (args[0] === '-c' && args[1]?.includes('import importlib.util')) {
|
|
58
|
+
dependencyChecks += 1;
|
|
59
|
+
return successfulChild(
|
|
60
|
+
dependencyChecks === 1 ? 'seaborn\n' : '',
|
|
61
|
+
'',
|
|
62
|
+
dependencyChecks === 1 ? 1 : 0,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (args[0] === '-m' && args[1] === 'pip') return successfulChild();
|
|
66
|
+
if (args.length === 1 && args[0]?.endsWith('run.py'))
|
|
67
|
+
return successfulChild('analysis complete\n');
|
|
68
|
+
return successfulChild('Python 3.12.0\n');
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await setupLocalRuntime();
|
|
73
|
+
const result = await executeLocally({ language: 'python', code: "print('analysis complete')" });
|
|
74
|
+
|
|
75
|
+
const pipCall = calls.find((call) => call.args[0] === '-m' && call.args[1] === 'pip');
|
|
76
|
+
expect(pipCall?.executable).toBe(
|
|
77
|
+
path.join(
|
|
78
|
+
runtimeDirectory,
|
|
79
|
+
process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python',
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
expect(pipCall?.args).toEqual(
|
|
83
|
+
expect.arrayContaining([
|
|
84
|
+
'numpy==1.26.4',
|
|
85
|
+
'pandas==2.2.3',
|
|
86
|
+
'openpyxl==3.1.5',
|
|
87
|
+
'pypdf==5.1.0',
|
|
88
|
+
'seaborn==0.13.2',
|
|
89
|
+
]),
|
|
90
|
+
);
|
|
91
|
+
const executionCall = calls.find((call) => call.args[0]?.endsWith('run.py'));
|
|
92
|
+
expect(executionCall?.executable).toBe(pipCall?.executable);
|
|
93
|
+
expect(executionCall?.options?.env?.PYTHONNOUSERSITE).toBe('1');
|
|
94
|
+
expect(result).toMatchObject({ exitCode: 0, stdout: 'analysis complete\n' });
|
|
95
|
+
});
|
|
96
|
+
});
|
package/src/local-runner.test.ts
CHANGED
|
@@ -20,13 +20,13 @@ describe('executeLocally', () => {
|
|
|
20
20
|
]);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it('
|
|
23
|
+
it('accepts a Python installation that can bootstrap the managed analysis environment', async () => {
|
|
24
24
|
const health = await checkLocalRuntime();
|
|
25
25
|
expect(health.backend).toBe('local');
|
|
26
26
|
if (health.status === 'ready') {
|
|
27
27
|
expect(health.error).toBeUndefined();
|
|
28
28
|
} else {
|
|
29
|
-
expect(health.error).toMatch(/Python|
|
|
29
|
+
expect(health.error).toMatch(/Python|virtual-environment/i);
|
|
30
30
|
}
|
|
31
31
|
});
|
|
32
32
|
});
|
package/src/local-runner.ts
CHANGED
|
@@ -15,17 +15,68 @@ import { getMimeType } from './mime.js';
|
|
|
15
15
|
type Command = { executable: string; args: string[] };
|
|
16
16
|
|
|
17
17
|
let pythonCommand: Command | null | undefined;
|
|
18
|
+
let managedPythonCommand: Promise<Command> | undefined;
|
|
18
19
|
|
|
19
20
|
// Keep this list in sync with the capabilities advertised by executeAnalysis
|
|
20
|
-
// and analyzeCorpusWithCode.
|
|
21
|
-
//
|
|
22
|
-
const ANALYSIS_PYTHON_MODULES = [
|
|
21
|
+
// and analyzeCorpusWithCode. These packages are installed in Larkup's managed
|
|
22
|
+
// virtual environment, never into the Python installation on the host.
|
|
23
|
+
const ANALYSIS_PYTHON_MODULES = [
|
|
24
|
+
'numpy',
|
|
25
|
+
'pandas',
|
|
26
|
+
'matplotlib',
|
|
27
|
+
'scipy',
|
|
28
|
+
'sklearn',
|
|
29
|
+
'seaborn',
|
|
30
|
+
'openpyxl',
|
|
31
|
+
'xlrd',
|
|
32
|
+
'pypdf',
|
|
33
|
+
'docx',
|
|
34
|
+
'pptx',
|
|
35
|
+
'PIL',
|
|
36
|
+
];
|
|
37
|
+
const ANALYSIS_PYTHON_PACKAGES = [
|
|
38
|
+
'pandas==2.2.3',
|
|
39
|
+
// Python 3.9 ships with several supported desktop environments. Keep the
|
|
40
|
+
// local pins compatible with it rather than requiring a user-managed upgrade.
|
|
41
|
+
'numpy==1.26.4',
|
|
42
|
+
'matplotlib==3.9.4',
|
|
43
|
+
'openpyxl==3.1.5',
|
|
44
|
+
'scipy==1.13.1',
|
|
45
|
+
'scikit-learn==1.6.1',
|
|
46
|
+
'seaborn==0.13.2',
|
|
47
|
+
'xlrd==2.0.1',
|
|
48
|
+
'pypdf==5.1.0',
|
|
49
|
+
'python-docx==1.1.2',
|
|
50
|
+
'python-pptx==1.0.2',
|
|
51
|
+
'Pillow==11.1.0',
|
|
52
|
+
];
|
|
23
53
|
|
|
24
|
-
function
|
|
54
|
+
function pythonEnvironment(virtualEnv?: string): NodeJS.ProcessEnv {
|
|
55
|
+
const env = { ...process.env };
|
|
56
|
+
// Let neither a user's active virtual environment nor PYTHONPATH leak into
|
|
57
|
+
// the managed runtime. This preserves the repeatable dependency set.
|
|
58
|
+
delete env.PYTHONHOME;
|
|
59
|
+
delete env.PYTHONPATH;
|
|
60
|
+
delete env.VIRTUAL_ENV;
|
|
61
|
+
if (virtualEnv) {
|
|
62
|
+
env.VIRTUAL_ENV = virtualEnv;
|
|
63
|
+
env.PYTHONNOUSERSITE = '1';
|
|
64
|
+
env.PATH = `${path.dirname(virtualEnvPython(virtualEnv))}${path.delimiter}${env.PATH ?? ''}`;
|
|
65
|
+
}
|
|
66
|
+
return env;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function run(
|
|
70
|
+
command: Command,
|
|
71
|
+
args: string[],
|
|
72
|
+
cwd?: string,
|
|
73
|
+
timeoutMs = 10_000,
|
|
74
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
75
|
+
) {
|
|
25
76
|
return new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve, reject) => {
|
|
26
77
|
const child = spawn(command.executable, [...command.args, ...args], {
|
|
27
78
|
cwd,
|
|
28
|
-
env
|
|
79
|
+
env,
|
|
29
80
|
shell: false,
|
|
30
81
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
82
|
});
|
|
@@ -76,7 +127,9 @@ async function resolvePython(): Promise<Command | null> {
|
|
|
76
127
|
|
|
77
128
|
for (const candidate of candidates) {
|
|
78
129
|
try {
|
|
79
|
-
if (
|
|
130
|
+
if (
|
|
131
|
+
(await run(candidate, ['--version'], undefined, 10_000, pythonEnvironment())).exitCode === 0
|
|
132
|
+
) {
|
|
80
133
|
pythonCommand = candidate;
|
|
81
134
|
return candidate;
|
|
82
135
|
}
|
|
@@ -88,6 +141,138 @@ async function resolvePython(): Promise<Command | null> {
|
|
|
88
141
|
return null;
|
|
89
142
|
}
|
|
90
143
|
|
|
144
|
+
function localRuntimeDirectory(): string {
|
|
145
|
+
const configured = process.env.LARKUP_SANDBOX_RUNTIME_DIR?.trim();
|
|
146
|
+
if (configured) return path.resolve(configured);
|
|
147
|
+
|
|
148
|
+
const dataRoot = process.env.LARKUP_DATA_DIR?.trim();
|
|
149
|
+
return path.join(
|
|
150
|
+
dataRoot ? path.resolve(dataRoot) : path.join(process.cwd(), '.larkup'),
|
|
151
|
+
'sandbox',
|
|
152
|
+
'python',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function virtualEnvPython(runtimeDirectory: string): string {
|
|
157
|
+
return process.platform === 'win32'
|
|
158
|
+
? path.join(runtimeDirectory, 'Scripts', 'python.exe')
|
|
159
|
+
: path.join(runtimeDirectory, 'bin', 'python');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function dependencyCheckCode(): string {
|
|
163
|
+
return `import importlib.util\nmodules = ${JSON.stringify(
|
|
164
|
+
ANALYSIS_PYTHON_MODULES,
|
|
165
|
+
)}\nmissing = [name for name in modules if importlib.util.find_spec(name) is None]\nprint(','.join(missing))\nraise SystemExit(bool(missing))`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function hasAnalysisDependencies(
|
|
169
|
+
command: Command,
|
|
170
|
+
runtimeDirectory: string,
|
|
171
|
+
): Promise<boolean> {
|
|
172
|
+
try {
|
|
173
|
+
return (
|
|
174
|
+
(
|
|
175
|
+
await run(
|
|
176
|
+
command,
|
|
177
|
+
['-c', dependencyCheckCode()],
|
|
178
|
+
undefined,
|
|
179
|
+
30_000,
|
|
180
|
+
pythonEnvironment(runtimeDirectory),
|
|
181
|
+
)
|
|
182
|
+
).exitCode === 0
|
|
183
|
+
);
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function prepareManagedPythonRuntime(): Promise<Command> {
|
|
190
|
+
const bootstrapPython = await resolvePython();
|
|
191
|
+
if (!bootstrapPython) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
'Local code execution needs Python 3.9 or later, or choose Docker or a remote sandbox provider.',
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const runtimeDirectory = localRuntimeDirectory();
|
|
198
|
+
const managedPython: Command = { executable: virtualEnvPython(runtimeDirectory), args: [] };
|
|
199
|
+
const bootstrapCheck = await run(
|
|
200
|
+
bootstrapPython,
|
|
201
|
+
['-c', 'import sys, venv; raise SystemExit(sys.version_info < (3, 9))'],
|
|
202
|
+
undefined,
|
|
203
|
+
10_000,
|
|
204
|
+
pythonEnvironment(),
|
|
205
|
+
);
|
|
206
|
+
if (bootstrapCheck.exitCode !== 0) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
'Local code execution needs Python 3.9 or later with virtual-environment support. Install Python or choose Docker or a remote sandbox provider.',
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const runtimeExists = await fs.access(managedPython.executable).then(
|
|
213
|
+
() => true,
|
|
214
|
+
() => false,
|
|
215
|
+
);
|
|
216
|
+
if (!runtimeExists) {
|
|
217
|
+
await fs.mkdir(path.dirname(runtimeDirectory), { recursive: true });
|
|
218
|
+
const created = await run(
|
|
219
|
+
bootstrapPython,
|
|
220
|
+
['-m', 'venv', runtimeDirectory],
|
|
221
|
+
undefined,
|
|
222
|
+
60_000,
|
|
223
|
+
pythonEnvironment(),
|
|
224
|
+
);
|
|
225
|
+
if (created.exitCode !== 0) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
'Larkup could not create its local Python environment. Check that Python virtual environments are enabled, or choose Docker or a remote sandbox provider.',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (await hasAnalysisDependencies(managedPython, runtimeDirectory)) return managedPython;
|
|
233
|
+
|
|
234
|
+
const installed = await run(
|
|
235
|
+
managedPython,
|
|
236
|
+
[
|
|
237
|
+
'-m',
|
|
238
|
+
'pip',
|
|
239
|
+
'install',
|
|
240
|
+
'--disable-pip-version-check',
|
|
241
|
+
'--no-input',
|
|
242
|
+
...ANALYSIS_PYTHON_PACKAGES,
|
|
243
|
+
],
|
|
244
|
+
undefined,
|
|
245
|
+
10 * 60_000,
|
|
246
|
+
pythonEnvironment(runtimeDirectory),
|
|
247
|
+
);
|
|
248
|
+
if (
|
|
249
|
+
installed.exitCode !== 0 ||
|
|
250
|
+
!(await hasAnalysisDependencies(managedPython, runtimeDirectory))
|
|
251
|
+
) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
'Larkup could not prepare its local analysis environment. Check the internet connection and retry, or choose Docker or a remote sandbox provider.',
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return managedPython;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Creates or repairs Larkup's isolated local Python analysis environment. */
|
|
260
|
+
export async function setupLocalRuntime(): Promise<void> {
|
|
261
|
+
if (!managedPythonCommand) {
|
|
262
|
+
managedPythonCommand = prepareManagedPythonRuntime().catch((error) => {
|
|
263
|
+
managedPythonCommand = undefined;
|
|
264
|
+
throw error;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
await managedPythonCommand;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function getManagedPython(): Promise<Command> {
|
|
271
|
+
await setupLocalRuntime();
|
|
272
|
+
// setupLocalRuntime only resolves after prepareManagedPythonRuntime resolves.
|
|
273
|
+
return managedPythonCommand!;
|
|
274
|
+
}
|
|
275
|
+
|
|
91
276
|
function assertSafeFileName(name: string) {
|
|
92
277
|
if (!name || name === '.' || name === '..' || /[\\/\0]/.test(name)) {
|
|
93
278
|
throw new Error('Sandbox file names must be plain file names.');
|
|
@@ -128,7 +313,7 @@ except Exception as error:
|
|
|
128
313
|
`;
|
|
129
314
|
}
|
|
130
315
|
|
|
131
|
-
/** Checks whether the host can
|
|
316
|
+
/** Checks whether the host can bootstrap Larkup's managed Python environment. */
|
|
132
317
|
export async function checkLocalRuntime(): Promise<SandboxHealthCheck> {
|
|
133
318
|
const python = await resolvePython();
|
|
134
319
|
if (!python) {
|
|
@@ -141,20 +326,19 @@ export async function checkLocalRuntime(): Promise<SandboxHealthCheck> {
|
|
|
141
326
|
}
|
|
142
327
|
|
|
143
328
|
try {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
329
|
+
const bootstrapCheck = await run(
|
|
330
|
+
python,
|
|
331
|
+
['-c', 'import sys, venv; raise SystemExit(sys.version_info < (3, 9))'],
|
|
332
|
+
undefined,
|
|
333
|
+
10_000,
|
|
334
|
+
pythonEnvironment(),
|
|
335
|
+
);
|
|
336
|
+
if (bootstrapCheck.exitCode !== 0) {
|
|
152
337
|
return {
|
|
153
338
|
status: 'error',
|
|
154
339
|
backend: 'local',
|
|
155
340
|
error:
|
|
156
|
-
|
|
157
|
-
`(for example: pip install ${ANALYSIS_PYTHON_MODULES.join(' ')}) or choose Docker or a remote sandbox provider.`,
|
|
341
|
+
'Local code execution needs Python 3.9 or later with virtual-environment support. Install Python or choose Docker or a remote sandbox provider.',
|
|
158
342
|
};
|
|
159
343
|
}
|
|
160
344
|
} catch {
|
|
@@ -162,7 +346,7 @@ export async function checkLocalRuntime(): Promise<SandboxHealthCheck> {
|
|
|
162
346
|
status: 'error',
|
|
163
347
|
backend: 'local',
|
|
164
348
|
error:
|
|
165
|
-
'Larkup could not verify
|
|
349
|
+
'Larkup could not verify Python virtual-environment support. Choose Docker or a remote sandbox provider.',
|
|
166
350
|
};
|
|
167
351
|
}
|
|
168
352
|
return { status: 'ready', backend: 'local' };
|
|
@@ -186,24 +370,22 @@ export async function executeLocally(request: ExecutionRequest): Promise<Executi
|
|
|
186
370
|
}
|
|
187
371
|
|
|
188
372
|
const isPython = request.language === 'python';
|
|
189
|
-
const command = isPython
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
stdout: '',
|
|
193
|
-
stderr:
|
|
194
|
-
'Local code execution needs Python 3. Install Python or choose Docker or a remote sandbox provider.',
|
|
195
|
-
exitCode: 1,
|
|
196
|
-
artifacts: [],
|
|
197
|
-
executionTimeMs: Date.now() - startTime,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
373
|
+
const command = isPython
|
|
374
|
+
? await getManagedPython()
|
|
375
|
+
: { executable: process.execPath, args: [] };
|
|
200
376
|
|
|
201
377
|
const script = path.join(tempDir, isPython ? 'run.py' : 'run.js');
|
|
202
378
|
const source = isPython
|
|
203
379
|
? pythonWrapper(request.code, outputDir)
|
|
204
380
|
: `process.env.LARKUP_OUTPUT_DIR = ${JSON.stringify(outputDir)};\n${request.code}`;
|
|
205
381
|
await fs.writeFile(script, source, 'utf8');
|
|
206
|
-
const result = await run(
|
|
382
|
+
const result = await run(
|
|
383
|
+
command,
|
|
384
|
+
[script],
|
|
385
|
+
inputDir,
|
|
386
|
+
request.timeout ?? 30_000,
|
|
387
|
+
isPython ? pythonEnvironment(localRuntimeDirectory()) : process.env,
|
|
388
|
+
);
|
|
207
389
|
const artifacts: ExecutionArtifact[] = [];
|
|
208
390
|
for (const name of await fs.readdir(outputDir).catch(() => [])) {
|
|
209
391
|
const filePath = path.join(outputDir, name);
|
|
@@ -8,6 +8,7 @@ const dockerRunner = vi.hoisted(() => ({
|
|
|
8
8
|
const localRunner = vi.hoisted(() => ({
|
|
9
9
|
checkLocalRuntime: vi.fn(),
|
|
10
10
|
executeLocally: vi.fn(),
|
|
11
|
+
setupLocalRuntime: vi.fn(),
|
|
11
12
|
}));
|
|
12
13
|
|
|
13
14
|
vi.mock('./docker-runner.js', () => ({
|
|
@@ -23,6 +24,7 @@ vi.mock('./providers/index.js', () => ({
|
|
|
23
24
|
vi.mock('./local-runner.js', () => ({
|
|
24
25
|
checkLocalRuntime: localRunner.checkLocalRuntime,
|
|
25
26
|
executeLocally: localRunner.executeLocally,
|
|
27
|
+
setupLocalRuntime: localRunner.setupLocalRuntime,
|
|
26
28
|
}));
|
|
27
29
|
|
|
28
30
|
import { SandboxManager } from './sandbox-manager.js';
|
|
@@ -71,4 +73,14 @@ describe('SandboxManager', () => {
|
|
|
71
73
|
expect(localRunner.executeLocally).toHaveBeenCalledOnce();
|
|
72
74
|
expect(dockerRunner.checkDockerHealth).not.toHaveBeenCalled();
|
|
73
75
|
});
|
|
76
|
+
|
|
77
|
+
it('provisions the managed local runtime when setup is requested', async () => {
|
|
78
|
+
const progress = vi.fn();
|
|
79
|
+
|
|
80
|
+
await new SandboxManager({ backend: 'local' }).setup(progress);
|
|
81
|
+
|
|
82
|
+
expect(localRunner.setupLocalRuntime).toHaveBeenCalledOnce();
|
|
83
|
+
expect(progress).toHaveBeenNthCalledWith(1, 'Preparing Larkup local analysis environment…');
|
|
84
|
+
expect(progress).toHaveBeenLastCalledWith('Local analysis environment is ready ✓');
|
|
85
|
+
});
|
|
74
86
|
});
|
package/src/sandbox-manager.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
ensureImage,
|
|
21
21
|
executeInDocker,
|
|
22
22
|
} from './docker-runner.js';
|
|
23
|
-
import { checkLocalRuntime, executeLocally } from './local-runner.js';
|
|
23
|
+
import { checkLocalRuntime, executeLocally, setupLocalRuntime } from './local-runner.js';
|
|
24
24
|
import { getSandboxProviderAdapter } from './providers/index.js';
|
|
25
25
|
|
|
26
26
|
const defaultDockerConfig: DockerConfig = {
|
|
@@ -62,6 +62,9 @@ export class SandboxManager {
|
|
|
62
62
|
async setup(onProgress?: (msg: string) => void): Promise<void> {
|
|
63
63
|
switch (this.config.backend) {
|
|
64
64
|
case 'local':
|
|
65
|
+
onProgress?.('Preparing Larkup local analysis environment…');
|
|
66
|
+
await setupLocalRuntime();
|
|
67
|
+
onProgress?.('Local analysis environment is ready ✓');
|
|
65
68
|
return;
|
|
66
69
|
case 'docker':
|
|
67
70
|
await buildSandboxImage(onProgress);
|