@geminixiang/mama 0.2.0-beta.0 → 0.2.0-beta.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/README.md +94 -27
- package/dist/adapter.d.ts +9 -5
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +9 -6
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +16 -13
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +10 -2
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +196 -32
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +24 -17
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +2 -0
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +109 -29
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +8 -43
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/telegram/html.d.ts +3 -0
- package/dist/adapters/telegram/html.d.ts.map +1 -0
- package/dist/adapters/telegram/html.js +98 -0
- package/dist/adapters/telegram/html.js.map +1 -0
- package/dist/agent.d.ts +4 -9
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +141 -92
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +44 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +74 -0
- package/dist/bindings.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +53 -12
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +7 -7
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +9 -9
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +14 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +45 -10
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +20 -0
- package/dist/execution-resolver.d.ts.map +1 -0
- package/dist/execution-resolver.js +49 -0
- package/dist/execution-resolver.js.map +1 -0
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +2 -1
- package/dist/instrument.js.map +1 -1
- package/dist/link-server.d.ts +17 -0
- package/dist/link-server.d.ts.map +1 -0
- package/dist/link-server.js +899 -0
- package/dist/link-server.js.map +1 -0
- package/dist/link-token.d.ts +32 -0
- package/dist/link-token.d.ts.map +1 -0
- package/dist/link-token.js +68 -0
- package/dist/link-token.js.map +1 -0
- package/dist/log.d.ts +2 -2
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +7 -7
- package/dist/log.js.map +1 -1
- package/dist/login.d.ts +29 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +164 -0
- package/dist/login.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +226 -55
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +52 -0
- package/dist/provisioner.d.ts.map +1 -0
- package/dist/provisioner.js +291 -0
- package/dist/provisioner.js.map +1 -0
- package/dist/sandbox/container.d.ts +15 -0
- package/dist/sandbox/container.d.ts.map +1 -0
- package/dist/sandbox/container.js +122 -0
- package/dist/sandbox/container.js.map +1 -0
- package/dist/sandbox/errors.d.ts +6 -0
- package/dist/sandbox/errors.d.ts.map +1 -0
- package/dist/sandbox/errors.js +11 -0
- package/dist/sandbox/errors.js.map +1 -0
- package/dist/sandbox/firecracker.d.ts +16 -0
- package/dist/sandbox/firecracker.d.ts.map +1 -0
- package/dist/sandbox/firecracker.js +206 -0
- package/dist/sandbox/firecracker.js.map +1 -0
- package/dist/sandbox/host.d.ts +10 -0
- package/dist/sandbox/host.d.ts.map +1 -0
- package/dist/sandbox/host.js +85 -0
- package/dist/sandbox/host.js.map +1 -0
- package/dist/sandbox/image.d.ts +5 -0
- package/dist/sandbox/image.d.ts.map +1 -0
- package/dist/sandbox/image.js +30 -0
- package/dist/sandbox/image.js.map +1 -0
- package/dist/sandbox/index.d.ts +20 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +51 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/types.d.ts +51 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +2 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/sandbox/utils.d.ts +4 -0
- package/dist/sandbox/utils.d.ts.map +1 -0
- package/dist/sandbox/utils.js +51 -0
- package/dist/sandbox/utils.js.map +1 -0
- package/dist/sandbox.d.ts +1 -39
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +1 -286
- package/dist/sandbox.js.map +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +4 -2
- package/dist/sentry.js.map +1 -1
- package/dist/session-store.d.ts +2 -6
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +3 -10
- package/dist/session-store.js.map +1 -1
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +8 -8
- package/dist/store.js.map +1 -1
- package/dist/tools/event.d.ts +22 -0
- package/dist/tools/event.d.ts.map +1 -0
- package/dist/tools/event.js +104 -0
- package/dist/tools/event.js.map +1 -0
- package/dist/tools/index.d.ts +7 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/ui-copy.d.ts +12 -0
- package/dist/ui-copy.d.ts.map +1 -0
- package/dist/ui-copy.js +36 -0
- package/dist/ui-copy.js.map +1 -0
- package/dist/vault-routing.d.ts +9 -0
- package/dist/vault-routing.d.ts.map +1 -0
- package/dist/vault-routing.js +52 -0
- package/dist/vault-routing.js.map +1 -0
- package/dist/vault.d.ts +106 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +389 -0
- package/dist/vault.js.map +1 -0
- package/package.json +12 -11
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../../src/sandbox/container.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,sBAAsB,EACtB,WAAW,EACX,UAAU,EACV,QAAQ,EACR,cAAc,EACf,MAAM,YAAY,CAAC;AAQpB,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,sBAAsB,GAAG,SAAS,CAY1F;AAED,wBAAsB,wBAAwB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B5F;AAED,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAGR;AAED,qBAAa,iBAAkB,YAAW,QAAQ;IAE9C,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,GAAG,CAAC;IACZ,OAAO,CAAC,WAAW,CAAC;IAHtB,YACU,SAAS,EAAE,MAAM,EACjB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,YAAA,EAC5B,WAAW,CAAC,GAAE,MAAM,OAAO,CAAC,IAAI,CAAC,aAAA,EACvC;IAEE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAetE;IAED,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1C;IAED,gBAAgB,IAAI,sBAAsB,CAEzC;CACF;AAED,eAAO,MAAM,uBAAuB,EAAE,cAAc,CAAC,sBAAsB,CAM1E,CAAC","sourcesContent":["import { chmodSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type {\n ContainerSandboxConfig,\n ExecOptions,\n ExecResult,\n Executor,\n SandboxAdapter,\n} from \"./types.js\";\nimport { SandboxError } from \"./errors.js\";\nimport { execSimple, shellEscape } from \"./utils.js\";\nimport { HostExecutor } from \"./host.js\";\n\nconst PRIVATE_DIR_MODE = 0o700;\nconst PRIVATE_FILE_MODE = 0o600;\n\nexport function parseContainerSandboxArg(value: string): ContainerSandboxConfig | undefined {\n if (!value.startsWith(\"container:\")) {\n return undefined;\n }\n\n const container = value.slice(\"container:\".length);\n if (!container) {\n throw new SandboxError(\n \"Error: container sandbox requires container name (e.g., container:mama-sandbox)\",\n );\n }\n return { type: \"container\", container };\n}\n\nexport async function validateContainerSandbox(config: ContainerSandboxConfig): Promise<void> {\n try {\n await execSimple(\"docker\", [\"--version\"]);\n } catch {\n throw new SandboxError(\"Error: Docker is not installed or not in PATH\");\n }\n\n try {\n const result = await execSimple(\"docker\", [\n \"inspect\",\n \"-f\",\n \"{{.State.Running}}\",\n config.container,\n ]);\n if (result.trim() !== \"true\") {\n throw new SandboxError(`Error: Container '${config.container}' is not running.`, [\n `Start it with: docker start ${config.container}`,\n ]);\n }\n } catch (error) {\n if (error instanceof SandboxError) {\n throw error;\n }\n throw new SandboxError(`Error: Container '${config.container}' does not exist.`, [\n `Create it with: docker run -d --name ${config.container} -v <workspace>:/workspace alpine:latest sleep infinity`,\n ]);\n }\n\n console.log(` Container '${config.container}' is running.`);\n}\n\nexport function buildContainerExecCommand(\n container: string,\n command: string,\n envFilePath?: string,\n): string {\n const envPart = envFilePath ? `--env-file ${shellEscape(envFilePath)} ` : \"\";\n return `docker exec ${envPart}-w /workspace ${container} sh -c ${shellEscape(command)}`;\n}\n\nexport class ContainerExecutor implements Executor {\n constructor(\n private container: string,\n private env?: Record<string, string>,\n private ensureReady?: () => Promise<void>,\n ) {}\n\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n if (this.ensureReady) {\n await this.ensureReady();\n } else {\n await ensureContainerRunning(this.container);\n }\n\n const hostExecutor = new HostExecutor();\n const temp = this.env ? createSecureEnvFile(this.env) : undefined;\n try {\n const dockerCmd = buildContainerExecCommand(this.container, command, temp?.envFilePath);\n return await hostExecutor.exec(dockerCmd, options);\n } finally {\n temp?.cleanup();\n }\n }\n\n getWorkspacePath(_hostPath: string): string {\n return \"/workspace\";\n }\n\n getSandboxConfig(): ContainerSandboxConfig {\n return { type: \"container\", container: this.container };\n }\n}\n\nexport const containerSandboxAdapter: SandboxAdapter<ContainerSandboxConfig> = {\n type: \"container\",\n parse: parseContainerSandboxArg,\n validate: validateContainerSandbox,\n createExecutor: (config, env, ensureReady) =>\n new ContainerExecutor(config.container, env, ensureReady),\n};\n\nasync function ensureContainerRunning(container: string): Promise<void> {\n try {\n const running = await execSimple(\"docker\", [\"inspect\", \"-f\", \"{{.State.Running}}\", container]);\n if (running.trim() === \"true\") {\n return;\n }\n await execSimple(\"docker\", [\"start\", container]);\n } catch (error) {\n const details = error instanceof Error ? error.message : String(error);\n throw new Error(\n `Container \"${container}\" is not available. ` +\n `Expected a pre-existing container or image provisioning to keep it running.\\n${details}`.trim(),\n );\n }\n}\n\nfunction createSecureEnvFile(env: Record<string, string>): {\n envFilePath: string;\n cleanup: () => void;\n} {\n const tempDir = mkdtempSync(join(tmpdir(), \"mama-docker-env-\"));\n chmodSync(tempDir, PRIVATE_DIR_MODE);\n const envFilePath = join(tempDir, \"env.list\");\n const content =\n Object.entries(env)\n .map(([key, value]) => `${key}=${sanitizeEnvValue(value)}`)\n .join(\"\\n\") + \"\\n\";\n writeFileSync(envFilePath, content, { encoding: \"utf-8\", mode: PRIVATE_FILE_MODE });\n chmodSync(envFilePath, PRIVATE_FILE_MODE);\n\n return {\n envFilePath,\n cleanup: () => {\n rmSync(tempDir, { recursive: true, force: true });\n },\n };\n}\n\nfunction sanitizeEnvValue(value: string): string {\n return value.replace(/\\r?\\n/g, \"\");\n}\n"]}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { SandboxError } from "./errors.js";
|
|
5
|
+
import { execSimple, shellEscape } from "./utils.js";
|
|
6
|
+
import { HostExecutor } from "./host.js";
|
|
7
|
+
const PRIVATE_DIR_MODE = 0o700;
|
|
8
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
9
|
+
export function parseContainerSandboxArg(value) {
|
|
10
|
+
if (!value.startsWith("container:")) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const container = value.slice("container:".length);
|
|
14
|
+
if (!container) {
|
|
15
|
+
throw new SandboxError("Error: container sandbox requires container name (e.g., container:mama-sandbox)");
|
|
16
|
+
}
|
|
17
|
+
return { type: "container", container };
|
|
18
|
+
}
|
|
19
|
+
export async function validateContainerSandbox(config) {
|
|
20
|
+
try {
|
|
21
|
+
await execSimple("docker", ["--version"]);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new SandboxError("Error: Docker is not installed or not in PATH");
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const result = await execSimple("docker", [
|
|
28
|
+
"inspect",
|
|
29
|
+
"-f",
|
|
30
|
+
"{{.State.Running}}",
|
|
31
|
+
config.container,
|
|
32
|
+
]);
|
|
33
|
+
if (result.trim() !== "true") {
|
|
34
|
+
throw new SandboxError(`Error: Container '${config.container}' is not running.`, [
|
|
35
|
+
`Start it with: docker start ${config.container}`,
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof SandboxError) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
throw new SandboxError(`Error: Container '${config.container}' does not exist.`, [
|
|
44
|
+
`Create it with: docker run -d --name ${config.container} -v <workspace>:/workspace alpine:latest sleep infinity`,
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
console.log(` Container '${config.container}' is running.`);
|
|
48
|
+
}
|
|
49
|
+
export function buildContainerExecCommand(container, command, envFilePath) {
|
|
50
|
+
const envPart = envFilePath ? `--env-file ${shellEscape(envFilePath)} ` : "";
|
|
51
|
+
return `docker exec ${envPart}-w /workspace ${container} sh -c ${shellEscape(command)}`;
|
|
52
|
+
}
|
|
53
|
+
export class ContainerExecutor {
|
|
54
|
+
constructor(container, env, ensureReady) {
|
|
55
|
+
this.container = container;
|
|
56
|
+
this.env = env;
|
|
57
|
+
this.ensureReady = ensureReady;
|
|
58
|
+
}
|
|
59
|
+
async exec(command, options) {
|
|
60
|
+
if (this.ensureReady) {
|
|
61
|
+
await this.ensureReady();
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await ensureContainerRunning(this.container);
|
|
65
|
+
}
|
|
66
|
+
const hostExecutor = new HostExecutor();
|
|
67
|
+
const temp = this.env ? createSecureEnvFile(this.env) : undefined;
|
|
68
|
+
try {
|
|
69
|
+
const dockerCmd = buildContainerExecCommand(this.container, command, temp?.envFilePath);
|
|
70
|
+
return await hostExecutor.exec(dockerCmd, options);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
temp?.cleanup();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
getWorkspacePath(_hostPath) {
|
|
77
|
+
return "/workspace";
|
|
78
|
+
}
|
|
79
|
+
getSandboxConfig() {
|
|
80
|
+
return { type: "container", container: this.container };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export const containerSandboxAdapter = {
|
|
84
|
+
type: "container",
|
|
85
|
+
parse: parseContainerSandboxArg,
|
|
86
|
+
validate: validateContainerSandbox,
|
|
87
|
+
createExecutor: (config, env, ensureReady) => new ContainerExecutor(config.container, env, ensureReady),
|
|
88
|
+
};
|
|
89
|
+
async function ensureContainerRunning(container) {
|
|
90
|
+
try {
|
|
91
|
+
const running = await execSimple("docker", ["inspect", "-f", "{{.State.Running}}", container]);
|
|
92
|
+
if (running.trim() === "true") {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
await execSimple("docker", ["start", container]);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
99
|
+
throw new Error(`Container "${container}" is not available. ` +
|
|
100
|
+
`Expected a pre-existing container or image provisioning to keep it running.\n${details}`.trim());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function createSecureEnvFile(env) {
|
|
104
|
+
const tempDir = mkdtempSync(join(tmpdir(), "mama-docker-env-"));
|
|
105
|
+
chmodSync(tempDir, PRIVATE_DIR_MODE);
|
|
106
|
+
const envFilePath = join(tempDir, "env.list");
|
|
107
|
+
const content = Object.entries(env)
|
|
108
|
+
.map(([key, value]) => `${key}=${sanitizeEnvValue(value)}`)
|
|
109
|
+
.join("\n") + "\n";
|
|
110
|
+
writeFileSync(envFilePath, content, { encoding: "utf-8", mode: PRIVATE_FILE_MODE });
|
|
111
|
+
chmodSync(envFilePath, PRIVATE_FILE_MODE);
|
|
112
|
+
return {
|
|
113
|
+
envFilePath,
|
|
114
|
+
cleanup: () => {
|
|
115
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function sanitizeEnvValue(value) {
|
|
120
|
+
return value.replace(/\r?\n/g, "");
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=container.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container.js","sourceRoot":"","sources":["../../src/sandbox/container.ts"],"names":[],"mappings":"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","sourcesContent":["import { chmodSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type {\n ContainerSandboxConfig,\n ExecOptions,\n ExecResult,\n Executor,\n SandboxAdapter,\n} from \"./types.js\";\nimport { SandboxError } from \"./errors.js\";\nimport { execSimple, shellEscape } from \"./utils.js\";\nimport { HostExecutor } from \"./host.js\";\n\nconst PRIVATE_DIR_MODE = 0o700;\nconst PRIVATE_FILE_MODE = 0o600;\n\nexport function parseContainerSandboxArg(value: string): ContainerSandboxConfig | undefined {\n if (!value.startsWith(\"container:\")) {\n return undefined;\n }\n\n const container = value.slice(\"container:\".length);\n if (!container) {\n throw new SandboxError(\n \"Error: container sandbox requires container name (e.g., container:mama-sandbox)\",\n );\n }\n return { type: \"container\", container };\n}\n\nexport async function validateContainerSandbox(config: ContainerSandboxConfig): Promise<void> {\n try {\n await execSimple(\"docker\", [\"--version\"]);\n } catch {\n throw new SandboxError(\"Error: Docker is not installed or not in PATH\");\n }\n\n try {\n const result = await execSimple(\"docker\", [\n \"inspect\",\n \"-f\",\n \"{{.State.Running}}\",\n config.container,\n ]);\n if (result.trim() !== \"true\") {\n throw new SandboxError(`Error: Container '${config.container}' is not running.`, [\n `Start it with: docker start ${config.container}`,\n ]);\n }\n } catch (error) {\n if (error instanceof SandboxError) {\n throw error;\n }\n throw new SandboxError(`Error: Container '${config.container}' does not exist.`, [\n `Create it with: docker run -d --name ${config.container} -v <workspace>:/workspace alpine:latest sleep infinity`,\n ]);\n }\n\n console.log(` Container '${config.container}' is running.`);\n}\n\nexport function buildContainerExecCommand(\n container: string,\n command: string,\n envFilePath?: string,\n): string {\n const envPart = envFilePath ? `--env-file ${shellEscape(envFilePath)} ` : \"\";\n return `docker exec ${envPart}-w /workspace ${container} sh -c ${shellEscape(command)}`;\n}\n\nexport class ContainerExecutor implements Executor {\n constructor(\n private container: string,\n private env?: Record<string, string>,\n private ensureReady?: () => Promise<void>,\n ) {}\n\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n if (this.ensureReady) {\n await this.ensureReady();\n } else {\n await ensureContainerRunning(this.container);\n }\n\n const hostExecutor = new HostExecutor();\n const temp = this.env ? createSecureEnvFile(this.env) : undefined;\n try {\n const dockerCmd = buildContainerExecCommand(this.container, command, temp?.envFilePath);\n return await hostExecutor.exec(dockerCmd, options);\n } finally {\n temp?.cleanup();\n }\n }\n\n getWorkspacePath(_hostPath: string): string {\n return \"/workspace\";\n }\n\n getSandboxConfig(): ContainerSandboxConfig {\n return { type: \"container\", container: this.container };\n }\n}\n\nexport const containerSandboxAdapter: SandboxAdapter<ContainerSandboxConfig> = {\n type: \"container\",\n parse: parseContainerSandboxArg,\n validate: validateContainerSandbox,\n createExecutor: (config, env, ensureReady) =>\n new ContainerExecutor(config.container, env, ensureReady),\n};\n\nasync function ensureContainerRunning(container: string): Promise<void> {\n try {\n const running = await execSimple(\"docker\", [\"inspect\", \"-f\", \"{{.State.Running}}\", container]);\n if (running.trim() === \"true\") {\n return;\n }\n await execSimple(\"docker\", [\"start\", container]);\n } catch (error) {\n const details = error instanceof Error ? error.message : String(error);\n throw new Error(\n `Container \"${container}\" is not available. ` +\n `Expected a pre-existing container or image provisioning to keep it running.\\n${details}`.trim(),\n );\n }\n}\n\nfunction createSecureEnvFile(env: Record<string, string>): {\n envFilePath: string;\n cleanup: () => void;\n} {\n const tempDir = mkdtempSync(join(tmpdir(), \"mama-docker-env-\"));\n chmodSync(tempDir, PRIVATE_DIR_MODE);\n const envFilePath = join(tempDir, \"env.list\");\n const content =\n Object.entries(env)\n .map(([key, value]) => `${key}=${sanitizeEnvValue(value)}`)\n .join(\"\\n\") + \"\\n\";\n writeFileSync(envFilePath, content, { encoding: \"utf-8\", mode: PRIVATE_FILE_MODE });\n chmodSync(envFilePath, PRIVATE_FILE_MODE);\n\n return {\n envFilePath,\n cleanup: () => {\n rmSync(tempDir, { recursive: true, force: true });\n },\n };\n}\n\nfunction sanitizeEnvValue(value: string): string {\n return value.replace(/\\r?\\n/g, \"\");\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/sandbox/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAE3B,YAAY,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,EAI9C;IAED,YAAY,IAAI,MAAM,EAAE,CAEvB;CACF","sourcesContent":["export class SandboxError extends Error {\n readonly details: string[];\n\n constructor(message: string, details?: string[]) {\n super(message);\n this.name = \"SandboxError\";\n this.details = details ?? [];\n }\n\n formatForCli(): string[] {\n return [this.message, ...this.details];\n }\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class SandboxError extends Error {
|
|
2
|
+
constructor(message, details) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "SandboxError";
|
|
5
|
+
this.details = details ?? [];
|
|
6
|
+
}
|
|
7
|
+
formatForCli() {
|
|
8
|
+
return [this.message, ...this.details];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/sandbox/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,YAAa,SAAQ,KAAK;IAGrC,YAAY,OAAe,EAAE,OAAkB;QAC7C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,IAAI,EAAE,CAAC;IAC/B,CAAC;IAED,YAAY;QACV,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;IACzC,CAAC;CACF","sourcesContent":["export class SandboxError extends Error {\n readonly details: string[];\n\n constructor(message: string, details?: string[]) {\n super(message);\n this.name = \"SandboxError\";\n this.details = details ?? [];\n }\n\n formatForCli(): string[] {\n return [this.message, ...this.details];\n }\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ExecOptions, ExecResult, Executor, FirecrackerSandboxConfig, SandboxAdapter } from "./types.js";
|
|
2
|
+
export declare function parseFirecrackerSandboxArg(value: string): FirecrackerSandboxConfig | undefined;
|
|
3
|
+
export declare function validateFirecrackerSandbox(config: FirecrackerSandboxConfig): Promise<void>;
|
|
4
|
+
export declare class FirecrackerExecutor implements Executor {
|
|
5
|
+
private vmId;
|
|
6
|
+
private hostPath;
|
|
7
|
+
private sshUser;
|
|
8
|
+
private sshPort;
|
|
9
|
+
private env?;
|
|
10
|
+
constructor(vmId: string, hostPath: string, sshUser?: string, sshPort?: number, env?: Record<string, string> | undefined);
|
|
11
|
+
exec(command: string, options?: ExecOptions): Promise<ExecResult>;
|
|
12
|
+
getWorkspacePath(_hostPath: string): string;
|
|
13
|
+
getSandboxConfig(): FirecrackerSandboxConfig;
|
|
14
|
+
}
|
|
15
|
+
export declare const firecrackerSandboxAdapter: SandboxAdapter<FirecrackerSandboxConfig>;
|
|
16
|
+
//# sourceMappingURL=firecracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firecracker.d.ts","sourceRoot":"","sources":["../../src/sandbox/firecracker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,QAAQ,EACR,wBAAwB,EACxB,cAAc,EACf,MAAM,YAAY,CAAC;AAKpB,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,wBAAwB,GAAG,SAAS,CA8B9F;AAED,wBAAsB,0BAA0B,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6ChG;AAED,qBAAa,mBAAoB,YAAW,QAAQ;IAEhD,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,GAAG,CAAC;IALd,YACU,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,MAAe,EACxB,OAAO,GAAE,MAAW,EACpB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,YAAA,EAClC;IAEE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAqGtE;IAED,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1C;IAED,gBAAgB,IAAI,wBAAwB,CAQ3C;CACF;AAgBD,eAAO,MAAM,yBAAyB,EAAE,cAAc,CAAC,wBAAwB,CAM9E,CAAC","sourcesContent":["import { spawn } from \"child_process\";\nimport type {\n ExecOptions,\n ExecResult,\n Executor,\n FirecrackerSandboxConfig,\n SandboxAdapter,\n} from \"./types.js\";\nimport { SandboxError } from \"./errors.js\";\nimport { HostExecutor } from \"./host.js\";\nimport { execSimple, killProcessTree, shellEscape } from \"./utils.js\";\n\nexport function parseFirecrackerSandboxArg(value: string): FirecrackerSandboxConfig | undefined {\n if (!value.startsWith(\"firecracker:\")) {\n return undefined;\n }\n\n const arg = value.slice(\"firecracker:\".length);\n // Format: firecracker:<vm-id>:<host-path>[:<ssh-user>[:<ssh-port>]]\n // Example: firecracker:vm1:/home/user/workspace\n // firecracker:vm1:/home/user/workspace:root\n // firecracker:vm1:/home/user/workspace:root:22\n const parts = arg.split(\":\");\n if (parts.length < 2) {\n throw new SandboxError(\n \"Error: firecracker sandbox requires vm-id and host-path\\n\" +\n \"Usage: firecracker:<vm-id>:<host-path>[:<ssh-user>[:<ssh-port>]]\\n\" +\n \"Example: firecracker:vm1:/home/user/workspace\",\n );\n }\n const vmId = parts[0];\n const hostPath = parts[1];\n const sshUser = parts[2] || \"root\";\n const sshPort = parts[3] ? parseInt(parts[3], 10) : 22;\n\n if (!vmId || !hostPath) {\n throw new SandboxError(\"Error: firecracker sandbox requires vm-id and host-path\");\n }\n if (isNaN(sshPort) || sshPort <= 0 || sshPort > 65535) {\n throw new SandboxError(\"Error: invalid SSH port\");\n }\n return { type: \"firecracker\", vmId, hostPath, sshUser, sshPort };\n}\n\nexport async function validateFirecrackerSandbox(config: FirecrackerSandboxConfig): Promise<void> {\n // Check if fc-agent or firecracker CLI is available\n try {\n await execSimple(\"fc-agent\", [\"--version\"]);\n } catch {\n // Try alternative: firecracker\n try {\n await execSimple(\"firecracker\", [\"--version\"]);\n } catch {\n throw new SandboxError(\n \"Error: Firecracker tools (fc-agent or firecracker) not found in PATH\",\n [\"Install firecracker: https://github.com/firecracker-microvm/firecracker\"],\n );\n }\n }\n\n // Check if VM is running using fc-agent\n try {\n const result = await execSimple(\"fc-agent\", [\"status\", config.vmId]);\n if (!result.includes(\"running\") && !result.includes(\"Running\")) {\n throw new SandboxError(`Error: Firecracker VM '${config.vmId}' is not running.`, [\n `Start it with: fc-agent start ${config.vmId}`,\n ]);\n }\n } catch (error) {\n if (error instanceof SandboxError) {\n throw error;\n }\n // Try alternative: firecracker-ctl or direct check\n try {\n await execSimple(\"firecracker-ctl\", [\"status\", config.vmId]);\n } catch {\n console.error(`Warning: Could not verify if VM '${config.vmId}' is running.`);\n console.error(\"Make sure the VM is started before running mama.\");\n }\n }\n\n // Verify host path exists\n try {\n await execSimple(\"ls\", [\"-d\", config.hostPath]);\n } catch {\n throw new SandboxError(`Error: Host path '${config.hostPath}' does not exist.`);\n }\n\n console.log(` Firecracker VM '${config.vmId}' configured with workspace '${config.hostPath}'.`);\n}\n\nexport class FirecrackerExecutor implements Executor {\n constructor(\n private vmId: string,\n private hostPath: string,\n private sshUser: string = \"root\",\n private sshPort: number = 22,\n private env?: Record<string, string>,\n ) {}\n\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n if (!this.env || Object.keys(this.env).length === 0) {\n const sshCmd =\n this.sshPort === 22\n ? `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${this.sshUser}@${this.vmId} sh -c ${shellEscape(command)}`\n : `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -p ${this.sshPort} ${this.sshUser}@${this.vmId} sh -c ${shellEscape(command)}`;\n const hostExecutor = new HostExecutor();\n return hostExecutor.exec(sshCmd, options);\n }\n\n return new Promise((resolve, reject) => {\n const sshArgs = [\"-o\", \"StrictHostKeyChecking=no\", \"-o\", \"ConnectTimeout=10\"];\n if (this.sshPort !== 22) {\n sshArgs.push(\"-p\", String(this.sshPort));\n }\n sshArgs.push(`${this.sshUser}@${this.vmId}`, \"sh\", \"-se\");\n\n const child = spawn(\"ssh\", sshArgs, {\n detached: true,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n let timedOut = false;\n let settled = false;\n\n const timeoutHandle =\n options?.timeout && options.timeout > 0\n ? setTimeout(() => {\n timedOut = true;\n if (child.pid) killProcessTree(child.pid);\n }, options.timeout * 1000)\n : undefined;\n\n const onAbort = () => {\n if (child.pid) killProcessTree(child.pid);\n };\n\n if (options?.signal) {\n if (options.signal.aborted) {\n onAbort();\n } else {\n options.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n\n child.on(\"error\", (error) => {\n if (settled) return;\n settled = true;\n if (timeoutHandle) clearTimeout(timeoutHandle);\n if (options?.signal) {\n options.signal.removeEventListener(\"abort\", onAbort);\n }\n reject(error);\n });\n\n child.stdout?.on(\"data\", (data) => {\n stdout += data.toString();\n if (stdout.length > 10 * 1024 * 1024) {\n stdout = stdout.slice(0, 10 * 1024 * 1024);\n }\n });\n\n child.stderr?.on(\"data\", (data) => {\n stderr += data.toString();\n if (stderr.length > 10 * 1024 * 1024) {\n stderr = stderr.slice(0, 10 * 1024 * 1024);\n }\n });\n\n child.stdin?.on(\"error\", (error) => {\n stderr += `${error.message}\\n`;\n });\n child.stdin?.end(buildRemoteScript(command, this.env));\n\n child.on(\"close\", (code) => {\n if (settled) return;\n settled = true;\n if (timeoutHandle) clearTimeout(timeoutHandle);\n if (options?.signal) {\n options.signal.removeEventListener(\"abort\", onAbort);\n }\n\n if (options?.signal?.aborted) {\n reject(new Error(`${stdout}\\n${stderr}\\nCommand aborted`.trim()));\n return;\n }\n\n if (timedOut) {\n reject(\n new Error(\n `${stdout}\\n${stderr}\\nCommand timed out after ${options?.timeout} seconds`.trim(),\n ),\n );\n return;\n }\n\n resolve({ stdout, stderr, code: code ?? 0 });\n });\n });\n }\n\n getWorkspacePath(_hostPath: string): string {\n return \"/workspace\";\n }\n\n getSandboxConfig(): FirecrackerSandboxConfig {\n return {\n type: \"firecracker\",\n vmId: this.vmId,\n hostPath: this.hostPath,\n sshUser: this.sshUser,\n sshPort: this.sshPort,\n };\n }\n}\n\nfunction buildRemoteScript(command: string, env?: Record<string, string>): string {\n const exports = env\n ? Object.entries(env)\n .map(([key, value]) => {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {\n throw new SandboxError(`Invalid environment variable name for firecracker: ${key}`);\n }\n return `export ${key}=${shellEscape(value)}`;\n })\n .join(\"\\n\") + \"\\n\"\n : \"\";\n return `${exports}${command}\\n`;\n}\n\nexport const firecrackerSandboxAdapter: SandboxAdapter<FirecrackerSandboxConfig> = {\n type: \"firecracker\",\n parse: parseFirecrackerSandboxArg,\n validate: validateFirecrackerSandbox,\n createExecutor: (config, env) =>\n new FirecrackerExecutor(config.vmId, config.hostPath, config.sshUser, config.sshPort, env),\n};\n"]}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { SandboxError } from "./errors.js";
|
|
3
|
+
import { HostExecutor } from "./host.js";
|
|
4
|
+
import { execSimple, killProcessTree, shellEscape } from "./utils.js";
|
|
5
|
+
export function parseFirecrackerSandboxArg(value) {
|
|
6
|
+
if (!value.startsWith("firecracker:")) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const arg = value.slice("firecracker:".length);
|
|
10
|
+
// Format: firecracker:<vm-id>:<host-path>[:<ssh-user>[:<ssh-port>]]
|
|
11
|
+
// Example: firecracker:vm1:/home/user/workspace
|
|
12
|
+
// firecracker:vm1:/home/user/workspace:root
|
|
13
|
+
// firecracker:vm1:/home/user/workspace:root:22
|
|
14
|
+
const parts = arg.split(":");
|
|
15
|
+
if (parts.length < 2) {
|
|
16
|
+
throw new SandboxError("Error: firecracker sandbox requires vm-id and host-path\n" +
|
|
17
|
+
"Usage: firecracker:<vm-id>:<host-path>[:<ssh-user>[:<ssh-port>]]\n" +
|
|
18
|
+
"Example: firecracker:vm1:/home/user/workspace");
|
|
19
|
+
}
|
|
20
|
+
const vmId = parts[0];
|
|
21
|
+
const hostPath = parts[1];
|
|
22
|
+
const sshUser = parts[2] || "root";
|
|
23
|
+
const sshPort = parts[3] ? parseInt(parts[3], 10) : 22;
|
|
24
|
+
if (!vmId || !hostPath) {
|
|
25
|
+
throw new SandboxError("Error: firecracker sandbox requires vm-id and host-path");
|
|
26
|
+
}
|
|
27
|
+
if (isNaN(sshPort) || sshPort <= 0 || sshPort > 65535) {
|
|
28
|
+
throw new SandboxError("Error: invalid SSH port");
|
|
29
|
+
}
|
|
30
|
+
return { type: "firecracker", vmId, hostPath, sshUser, sshPort };
|
|
31
|
+
}
|
|
32
|
+
export async function validateFirecrackerSandbox(config) {
|
|
33
|
+
// Check if fc-agent or firecracker CLI is available
|
|
34
|
+
try {
|
|
35
|
+
await execSimple("fc-agent", ["--version"]);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Try alternative: firecracker
|
|
39
|
+
try {
|
|
40
|
+
await execSimple("firecracker", ["--version"]);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new SandboxError("Error: Firecracker tools (fc-agent or firecracker) not found in PATH", ["Install firecracker: https://github.com/firecracker-microvm/firecracker"]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Check if VM is running using fc-agent
|
|
47
|
+
try {
|
|
48
|
+
const result = await execSimple("fc-agent", ["status", config.vmId]);
|
|
49
|
+
if (!result.includes("running") && !result.includes("Running")) {
|
|
50
|
+
throw new SandboxError(`Error: Firecracker VM '${config.vmId}' is not running.`, [
|
|
51
|
+
`Start it with: fc-agent start ${config.vmId}`,
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof SandboxError) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
// Try alternative: firecracker-ctl or direct check
|
|
60
|
+
try {
|
|
61
|
+
await execSimple("firecracker-ctl", ["status", config.vmId]);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
console.error(`Warning: Could not verify if VM '${config.vmId}' is running.`);
|
|
65
|
+
console.error("Make sure the VM is started before running mama.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Verify host path exists
|
|
69
|
+
try {
|
|
70
|
+
await execSimple("ls", ["-d", config.hostPath]);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw new SandboxError(`Error: Host path '${config.hostPath}' does not exist.`);
|
|
74
|
+
}
|
|
75
|
+
console.log(` Firecracker VM '${config.vmId}' configured with workspace '${config.hostPath}'.`);
|
|
76
|
+
}
|
|
77
|
+
export class FirecrackerExecutor {
|
|
78
|
+
constructor(vmId, hostPath, sshUser = "root", sshPort = 22, env) {
|
|
79
|
+
this.vmId = vmId;
|
|
80
|
+
this.hostPath = hostPath;
|
|
81
|
+
this.sshUser = sshUser;
|
|
82
|
+
this.sshPort = sshPort;
|
|
83
|
+
this.env = env;
|
|
84
|
+
}
|
|
85
|
+
async exec(command, options) {
|
|
86
|
+
if (!this.env || Object.keys(this.env).length === 0) {
|
|
87
|
+
const sshCmd = this.sshPort === 22
|
|
88
|
+
? `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${this.sshUser}@${this.vmId} sh -c ${shellEscape(command)}`
|
|
89
|
+
: `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -p ${this.sshPort} ${this.sshUser}@${this.vmId} sh -c ${shellEscape(command)}`;
|
|
90
|
+
const hostExecutor = new HostExecutor();
|
|
91
|
+
return hostExecutor.exec(sshCmd, options);
|
|
92
|
+
}
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const sshArgs = ["-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10"];
|
|
95
|
+
if (this.sshPort !== 22) {
|
|
96
|
+
sshArgs.push("-p", String(this.sshPort));
|
|
97
|
+
}
|
|
98
|
+
sshArgs.push(`${this.sshUser}@${this.vmId}`, "sh", "-se");
|
|
99
|
+
const child = spawn("ssh", sshArgs, {
|
|
100
|
+
detached: true,
|
|
101
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
102
|
+
});
|
|
103
|
+
let stdout = "";
|
|
104
|
+
let stderr = "";
|
|
105
|
+
let timedOut = false;
|
|
106
|
+
let settled = false;
|
|
107
|
+
const timeoutHandle = options?.timeout && options.timeout > 0
|
|
108
|
+
? setTimeout(() => {
|
|
109
|
+
timedOut = true;
|
|
110
|
+
if (child.pid)
|
|
111
|
+
killProcessTree(child.pid);
|
|
112
|
+
}, options.timeout * 1000)
|
|
113
|
+
: undefined;
|
|
114
|
+
const onAbort = () => {
|
|
115
|
+
if (child.pid)
|
|
116
|
+
killProcessTree(child.pid);
|
|
117
|
+
};
|
|
118
|
+
if (options?.signal) {
|
|
119
|
+
if (options.signal.aborted) {
|
|
120
|
+
onAbort();
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
child.on("error", (error) => {
|
|
127
|
+
if (settled)
|
|
128
|
+
return;
|
|
129
|
+
settled = true;
|
|
130
|
+
if (timeoutHandle)
|
|
131
|
+
clearTimeout(timeoutHandle);
|
|
132
|
+
if (options?.signal) {
|
|
133
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
134
|
+
}
|
|
135
|
+
reject(error);
|
|
136
|
+
});
|
|
137
|
+
child.stdout?.on("data", (data) => {
|
|
138
|
+
stdout += data.toString();
|
|
139
|
+
if (stdout.length > 10 * 1024 * 1024) {
|
|
140
|
+
stdout = stdout.slice(0, 10 * 1024 * 1024);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
child.stderr?.on("data", (data) => {
|
|
144
|
+
stderr += data.toString();
|
|
145
|
+
if (stderr.length > 10 * 1024 * 1024) {
|
|
146
|
+
stderr = stderr.slice(0, 10 * 1024 * 1024);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
child.stdin?.on("error", (error) => {
|
|
150
|
+
stderr += `${error.message}\n`;
|
|
151
|
+
});
|
|
152
|
+
child.stdin?.end(buildRemoteScript(command, this.env));
|
|
153
|
+
child.on("close", (code) => {
|
|
154
|
+
if (settled)
|
|
155
|
+
return;
|
|
156
|
+
settled = true;
|
|
157
|
+
if (timeoutHandle)
|
|
158
|
+
clearTimeout(timeoutHandle);
|
|
159
|
+
if (options?.signal) {
|
|
160
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
161
|
+
}
|
|
162
|
+
if (options?.signal?.aborted) {
|
|
163
|
+
reject(new Error(`${stdout}\n${stderr}\nCommand aborted`.trim()));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (timedOut) {
|
|
167
|
+
reject(new Error(`${stdout}\n${stderr}\nCommand timed out after ${options?.timeout} seconds`.trim()));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
resolve({ stdout, stderr, code: code ?? 0 });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
getWorkspacePath(_hostPath) {
|
|
175
|
+
return "/workspace";
|
|
176
|
+
}
|
|
177
|
+
getSandboxConfig() {
|
|
178
|
+
return {
|
|
179
|
+
type: "firecracker",
|
|
180
|
+
vmId: this.vmId,
|
|
181
|
+
hostPath: this.hostPath,
|
|
182
|
+
sshUser: this.sshUser,
|
|
183
|
+
sshPort: this.sshPort,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function buildRemoteScript(command, env) {
|
|
188
|
+
const exports = env
|
|
189
|
+
? Object.entries(env)
|
|
190
|
+
.map(([key, value]) => {
|
|
191
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
192
|
+
throw new SandboxError(`Invalid environment variable name for firecracker: ${key}`);
|
|
193
|
+
}
|
|
194
|
+
return `export ${key}=${shellEscape(value)}`;
|
|
195
|
+
})
|
|
196
|
+
.join("\n") + "\n"
|
|
197
|
+
: "";
|
|
198
|
+
return `${exports}${command}\n`;
|
|
199
|
+
}
|
|
200
|
+
export const firecrackerSandboxAdapter = {
|
|
201
|
+
type: "firecracker",
|
|
202
|
+
parse: parseFirecrackerSandboxArg,
|
|
203
|
+
validate: validateFirecrackerSandbox,
|
|
204
|
+
createExecutor: (config, env) => new FirecrackerExecutor(config.vmId, config.hostPath, config.sshUser, config.sshPort, env),
|
|
205
|
+
};
|
|
206
|
+
//# sourceMappingURL=firecracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firecracker.js","sourceRoot":"","sources":["../../src/sandbox/firecracker.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"child_process\";\nimport type {\n ExecOptions,\n ExecResult,\n Executor,\n FirecrackerSandboxConfig,\n SandboxAdapter,\n} from \"./types.js\";\nimport { SandboxError } from \"./errors.js\";\nimport { HostExecutor } from \"./host.js\";\nimport { execSimple, killProcessTree, shellEscape } from \"./utils.js\";\n\nexport function parseFirecrackerSandboxArg(value: string): FirecrackerSandboxConfig | undefined {\n if (!value.startsWith(\"firecracker:\")) {\n return undefined;\n }\n\n const arg = value.slice(\"firecracker:\".length);\n // Format: firecracker:<vm-id>:<host-path>[:<ssh-user>[:<ssh-port>]]\n // Example: firecracker:vm1:/home/user/workspace\n // firecracker:vm1:/home/user/workspace:root\n // firecracker:vm1:/home/user/workspace:root:22\n const parts = arg.split(\":\");\n if (parts.length < 2) {\n throw new SandboxError(\n \"Error: firecracker sandbox requires vm-id and host-path\\n\" +\n \"Usage: firecracker:<vm-id>:<host-path>[:<ssh-user>[:<ssh-port>]]\\n\" +\n \"Example: firecracker:vm1:/home/user/workspace\",\n );\n }\n const vmId = parts[0];\n const hostPath = parts[1];\n const sshUser = parts[2] || \"root\";\n const sshPort = parts[3] ? parseInt(parts[3], 10) : 22;\n\n if (!vmId || !hostPath) {\n throw new SandboxError(\"Error: firecracker sandbox requires vm-id and host-path\");\n }\n if (isNaN(sshPort) || sshPort <= 0 || sshPort > 65535) {\n throw new SandboxError(\"Error: invalid SSH port\");\n }\n return { type: \"firecracker\", vmId, hostPath, sshUser, sshPort };\n}\n\nexport async function validateFirecrackerSandbox(config: FirecrackerSandboxConfig): Promise<void> {\n // Check if fc-agent or firecracker CLI is available\n try {\n await execSimple(\"fc-agent\", [\"--version\"]);\n } catch {\n // Try alternative: firecracker\n try {\n await execSimple(\"firecracker\", [\"--version\"]);\n } catch {\n throw new SandboxError(\n \"Error: Firecracker tools (fc-agent or firecracker) not found in PATH\",\n [\"Install firecracker: https://github.com/firecracker-microvm/firecracker\"],\n );\n }\n }\n\n // Check if VM is running using fc-agent\n try {\n const result = await execSimple(\"fc-agent\", [\"status\", config.vmId]);\n if (!result.includes(\"running\") && !result.includes(\"Running\")) {\n throw new SandboxError(`Error: Firecracker VM '${config.vmId}' is not running.`, [\n `Start it with: fc-agent start ${config.vmId}`,\n ]);\n }\n } catch (error) {\n if (error instanceof SandboxError) {\n throw error;\n }\n // Try alternative: firecracker-ctl or direct check\n try {\n await execSimple(\"firecracker-ctl\", [\"status\", config.vmId]);\n } catch {\n console.error(`Warning: Could not verify if VM '${config.vmId}' is running.`);\n console.error(\"Make sure the VM is started before running mama.\");\n }\n }\n\n // Verify host path exists\n try {\n await execSimple(\"ls\", [\"-d\", config.hostPath]);\n } catch {\n throw new SandboxError(`Error: Host path '${config.hostPath}' does not exist.`);\n }\n\n console.log(` Firecracker VM '${config.vmId}' configured with workspace '${config.hostPath}'.`);\n}\n\nexport class FirecrackerExecutor implements Executor {\n constructor(\n private vmId: string,\n private hostPath: string,\n private sshUser: string = \"root\",\n private sshPort: number = 22,\n private env?: Record<string, string>,\n ) {}\n\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n if (!this.env || Object.keys(this.env).length === 0) {\n const sshCmd =\n this.sshPort === 22\n ? `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${this.sshUser}@${this.vmId} sh -c ${shellEscape(command)}`\n : `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -p ${this.sshPort} ${this.sshUser}@${this.vmId} sh -c ${shellEscape(command)}`;\n const hostExecutor = new HostExecutor();\n return hostExecutor.exec(sshCmd, options);\n }\n\n return new Promise((resolve, reject) => {\n const sshArgs = [\"-o\", \"StrictHostKeyChecking=no\", \"-o\", \"ConnectTimeout=10\"];\n if (this.sshPort !== 22) {\n sshArgs.push(\"-p\", String(this.sshPort));\n }\n sshArgs.push(`${this.sshUser}@${this.vmId}`, \"sh\", \"-se\");\n\n const child = spawn(\"ssh\", sshArgs, {\n detached: true,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n let timedOut = false;\n let settled = false;\n\n const timeoutHandle =\n options?.timeout && options.timeout > 0\n ? setTimeout(() => {\n timedOut = true;\n if (child.pid) killProcessTree(child.pid);\n }, options.timeout * 1000)\n : undefined;\n\n const onAbort = () => {\n if (child.pid) killProcessTree(child.pid);\n };\n\n if (options?.signal) {\n if (options.signal.aborted) {\n onAbort();\n } else {\n options.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n\n child.on(\"error\", (error) => {\n if (settled) return;\n settled = true;\n if (timeoutHandle) clearTimeout(timeoutHandle);\n if (options?.signal) {\n options.signal.removeEventListener(\"abort\", onAbort);\n }\n reject(error);\n });\n\n child.stdout?.on(\"data\", (data) => {\n stdout += data.toString();\n if (stdout.length > 10 * 1024 * 1024) {\n stdout = stdout.slice(0, 10 * 1024 * 1024);\n }\n });\n\n child.stderr?.on(\"data\", (data) => {\n stderr += data.toString();\n if (stderr.length > 10 * 1024 * 1024) {\n stderr = stderr.slice(0, 10 * 1024 * 1024);\n }\n });\n\n child.stdin?.on(\"error\", (error) => {\n stderr += `${error.message}\\n`;\n });\n child.stdin?.end(buildRemoteScript(command, this.env));\n\n child.on(\"close\", (code) => {\n if (settled) return;\n settled = true;\n if (timeoutHandle) clearTimeout(timeoutHandle);\n if (options?.signal) {\n options.signal.removeEventListener(\"abort\", onAbort);\n }\n\n if (options?.signal?.aborted) {\n reject(new Error(`${stdout}\\n${stderr}\\nCommand aborted`.trim()));\n return;\n }\n\n if (timedOut) {\n reject(\n new Error(\n `${stdout}\\n${stderr}\\nCommand timed out after ${options?.timeout} seconds`.trim(),\n ),\n );\n return;\n }\n\n resolve({ stdout, stderr, code: code ?? 0 });\n });\n });\n }\n\n getWorkspacePath(_hostPath: string): string {\n return \"/workspace\";\n }\n\n getSandboxConfig(): FirecrackerSandboxConfig {\n return {\n type: \"firecracker\",\n vmId: this.vmId,\n hostPath: this.hostPath,\n sshUser: this.sshUser,\n sshPort: this.sshPort,\n };\n }\n}\n\nfunction buildRemoteScript(command: string, env?: Record<string, string>): string {\n const exports = env\n ? Object.entries(env)\n .map(([key, value]) => {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {\n throw new SandboxError(`Invalid environment variable name for firecracker: ${key}`);\n }\n return `export ${key}=${shellEscape(value)}`;\n })\n .join(\"\\n\") + \"\\n\"\n : \"\";\n return `${exports}${command}\\n`;\n}\n\nexport const firecrackerSandboxAdapter: SandboxAdapter<FirecrackerSandboxConfig> = {\n type: \"firecracker\",\n parse: parseFirecrackerSandboxArg,\n validate: validateFirecrackerSandbox,\n createExecutor: (config, env) =>\n new FirecrackerExecutor(config.vmId, config.hostPath, config.sshUser, config.sshPort, env),\n};\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ExecOptions, ExecResult, Executor, HostSandboxConfig, SandboxAdapter } from "./types.js";
|
|
2
|
+
export declare function parseHostSandboxArg(value: string): HostSandboxConfig | undefined;
|
|
3
|
+
export declare function validateHostSandbox(_config: HostSandboxConfig): Promise<void>;
|
|
4
|
+
export declare class HostExecutor implements Executor {
|
|
5
|
+
exec(command: string, options?: ExecOptions): Promise<ExecResult>;
|
|
6
|
+
getWorkspacePath(hostPath: string): string;
|
|
7
|
+
getSandboxConfig(): HostSandboxConfig;
|
|
8
|
+
}
|
|
9
|
+
export declare const hostSandboxAdapter: SandboxAdapter<HostSandboxConfig>;
|
|
10
|
+
//# sourceMappingURL=host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/sandbox/host.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,QAAQ,EACR,iBAAiB,EACjB,cAAc,EACf,MAAM,YAAY,CAAC;AAGpB,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAKhF;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;AAED,qBAAa,YAAa,YAAW,QAAQ;IACrC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAuEtE;IAED,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzC;IAED,gBAAgB,IAAI,iBAAiB,CAEpC;CACF;AAED,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,iBAAiB,CAKhE,CAAC","sourcesContent":["import { spawn } from \"child_process\";\nimport type {\n ExecOptions,\n ExecResult,\n Executor,\n HostSandboxConfig,\n SandboxAdapter,\n} from \"./types.js\";\nimport { killProcessTree } from \"./utils.js\";\n\nexport function parseHostSandboxArg(value: string): HostSandboxConfig | undefined {\n if (value === \"host\") {\n return { type: \"host\" };\n }\n return undefined;\n}\n\nexport async function validateHostSandbox(_config: HostSandboxConfig): Promise<void> {\n return;\n}\n\nexport class HostExecutor implements Executor {\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n return new Promise((resolve, reject) => {\n const shell = process.platform === \"win32\" ? \"cmd\" : \"sh\";\n const shellArgs = process.platform === \"win32\" ? [\"/c\"] : [\"-c\"];\n\n const child = spawn(shell, [...shellArgs, command], {\n detached: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n let timedOut = false;\n\n const timeoutHandle =\n options?.timeout && options.timeout > 0\n ? setTimeout(() => {\n timedOut = true;\n killProcessTree(child.pid!);\n }, options.timeout * 1000)\n : undefined;\n\n const onAbort = () => {\n if (child.pid) killProcessTree(child.pid);\n };\n\n if (options?.signal) {\n if (options.signal.aborted) {\n onAbort();\n } else {\n options.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n\n child.stdout?.on(\"data\", (data) => {\n stdout += data.toString();\n if (stdout.length > 10 * 1024 * 1024) {\n stdout = stdout.slice(0, 10 * 1024 * 1024);\n }\n });\n\n child.stderr?.on(\"data\", (data) => {\n stderr += data.toString();\n if (stderr.length > 10 * 1024 * 1024) {\n stderr = stderr.slice(0, 10 * 1024 * 1024);\n }\n });\n\n child.on(\"close\", (code) => {\n if (timeoutHandle) clearTimeout(timeoutHandle);\n if (options?.signal) {\n options.signal.removeEventListener(\"abort\", onAbort);\n }\n\n if (options?.signal?.aborted) {\n reject(new Error(`${stdout}\\n${stderr}\\nCommand aborted`.trim()));\n return;\n }\n\n if (timedOut) {\n reject(\n new Error(\n `${stdout}\\n${stderr}\\nCommand timed out after ${options?.timeout} seconds`.trim(),\n ),\n );\n return;\n }\n\n resolve({ stdout, stderr, code: code ?? 0 });\n });\n });\n }\n\n getWorkspacePath(hostPath: string): string {\n return hostPath;\n }\n\n getSandboxConfig(): HostSandboxConfig {\n return { type: \"host\" };\n }\n}\n\nexport const hostSandboxAdapter: SandboxAdapter<HostSandboxConfig> = {\n type: \"host\",\n parse: parseHostSandboxArg,\n validate: validateHostSandbox,\n createExecutor: () => new HostExecutor(),\n};\n"]}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { killProcessTree } from "./utils.js";
|
|
3
|
+
export function parseHostSandboxArg(value) {
|
|
4
|
+
if (value === "host") {
|
|
5
|
+
return { type: "host" };
|
|
6
|
+
}
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
export async function validateHostSandbox(_config) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
export class HostExecutor {
|
|
13
|
+
async exec(command, options) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const shell = process.platform === "win32" ? "cmd" : "sh";
|
|
16
|
+
const shellArgs = process.platform === "win32" ? ["/c"] : ["-c"];
|
|
17
|
+
const child = spawn(shell, [...shellArgs, command], {
|
|
18
|
+
detached: true,
|
|
19
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
20
|
+
});
|
|
21
|
+
let stdout = "";
|
|
22
|
+
let stderr = "";
|
|
23
|
+
let timedOut = false;
|
|
24
|
+
const timeoutHandle = options?.timeout && options.timeout > 0
|
|
25
|
+
? setTimeout(() => {
|
|
26
|
+
timedOut = true;
|
|
27
|
+
killProcessTree(child.pid);
|
|
28
|
+
}, options.timeout * 1000)
|
|
29
|
+
: undefined;
|
|
30
|
+
const onAbort = () => {
|
|
31
|
+
if (child.pid)
|
|
32
|
+
killProcessTree(child.pid);
|
|
33
|
+
};
|
|
34
|
+
if (options?.signal) {
|
|
35
|
+
if (options.signal.aborted) {
|
|
36
|
+
onAbort();
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
child.stdout?.on("data", (data) => {
|
|
43
|
+
stdout += data.toString();
|
|
44
|
+
if (stdout.length > 10 * 1024 * 1024) {
|
|
45
|
+
stdout = stdout.slice(0, 10 * 1024 * 1024);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
child.stderr?.on("data", (data) => {
|
|
49
|
+
stderr += data.toString();
|
|
50
|
+
if (stderr.length > 10 * 1024 * 1024) {
|
|
51
|
+
stderr = stderr.slice(0, 10 * 1024 * 1024);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
child.on("close", (code) => {
|
|
55
|
+
if (timeoutHandle)
|
|
56
|
+
clearTimeout(timeoutHandle);
|
|
57
|
+
if (options?.signal) {
|
|
58
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
59
|
+
}
|
|
60
|
+
if (options?.signal?.aborted) {
|
|
61
|
+
reject(new Error(`${stdout}\n${stderr}\nCommand aborted`.trim()));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (timedOut) {
|
|
65
|
+
reject(new Error(`${stdout}\n${stderr}\nCommand timed out after ${options?.timeout} seconds`.trim()));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
resolve({ stdout, stderr, code: code ?? 0 });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
getWorkspacePath(hostPath) {
|
|
73
|
+
return hostPath;
|
|
74
|
+
}
|
|
75
|
+
getSandboxConfig() {
|
|
76
|
+
return { type: "host" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export const hostSandboxAdapter = {
|
|
80
|
+
type: "host",
|
|
81
|
+
parse: parseHostSandboxArg,
|
|
82
|
+
validate: validateHostSandbox,
|
|
83
|
+
createExecutor: () => new HostExecutor(),
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.js","sourceRoot":"","sources":["../../src/sandbox/host.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"child_process\";\nimport type {\n ExecOptions,\n ExecResult,\n Executor,\n HostSandboxConfig,\n SandboxAdapter,\n} from \"./types.js\";\nimport { killProcessTree } from \"./utils.js\";\n\nexport function parseHostSandboxArg(value: string): HostSandboxConfig | undefined {\n if (value === \"host\") {\n return { type: \"host\" };\n }\n return undefined;\n}\n\nexport async function validateHostSandbox(_config: HostSandboxConfig): Promise<void> {\n return;\n}\n\nexport class HostExecutor implements Executor {\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n return new Promise((resolve, reject) => {\n const shell = process.platform === \"win32\" ? \"cmd\" : \"sh\";\n const shellArgs = process.platform === \"win32\" ? [\"/c\"] : [\"-c\"];\n\n const child = spawn(shell, [...shellArgs, command], {\n detached: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n let timedOut = false;\n\n const timeoutHandle =\n options?.timeout && options.timeout > 0\n ? setTimeout(() => {\n timedOut = true;\n killProcessTree(child.pid!);\n }, options.timeout * 1000)\n : undefined;\n\n const onAbort = () => {\n if (child.pid) killProcessTree(child.pid);\n };\n\n if (options?.signal) {\n if (options.signal.aborted) {\n onAbort();\n } else {\n options.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n\n child.stdout?.on(\"data\", (data) => {\n stdout += data.toString();\n if (stdout.length > 10 * 1024 * 1024) {\n stdout = stdout.slice(0, 10 * 1024 * 1024);\n }\n });\n\n child.stderr?.on(\"data\", (data) => {\n stderr += data.toString();\n if (stderr.length > 10 * 1024 * 1024) {\n stderr = stderr.slice(0, 10 * 1024 * 1024);\n }\n });\n\n child.on(\"close\", (code) => {\n if (timeoutHandle) clearTimeout(timeoutHandle);\n if (options?.signal) {\n options.signal.removeEventListener(\"abort\", onAbort);\n }\n\n if (options?.signal?.aborted) {\n reject(new Error(`${stdout}\\n${stderr}\\nCommand aborted`.trim()));\n return;\n }\n\n if (timedOut) {\n reject(\n new Error(\n `${stdout}\\n${stderr}\\nCommand timed out after ${options?.timeout} seconds`.trim(),\n ),\n );\n return;\n }\n\n resolve({ stdout, stderr, code: code ?? 0 });\n });\n });\n }\n\n getWorkspacePath(hostPath: string): string {\n return hostPath;\n }\n\n getSandboxConfig(): HostSandboxConfig {\n return { type: \"host\" };\n }\n}\n\nexport const hostSandboxAdapter: SandboxAdapter<HostSandboxConfig> = {\n type: \"host\",\n parse: parseHostSandboxArg,\n validate: validateHostSandbox,\n createExecutor: () => new HostExecutor(),\n};\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ImageSandboxConfig, SandboxAdapter } from "./types.js";
|
|
2
|
+
export declare function parseImageSandboxArg(value: string): ImageSandboxConfig | undefined;
|
|
3
|
+
export declare function validateImageSandbox(config: ImageSandboxConfig): Promise<void>;
|
|
4
|
+
export declare const imageSandboxAdapter: SandboxAdapter<ImageSandboxConfig>;
|
|
5
|
+
//# sourceMappingURL=image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/sandbox/image.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAIrE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAUlF;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAOpF;AAED,eAAO,MAAM,mBAAmB,EAAE,cAAc,CAAC,kBAAkB,CAOlE,CAAC","sourcesContent":["import type { ImageSandboxConfig, SandboxAdapter } from \"./types.js\";\nimport { SandboxError } from \"./errors.js\";\nimport { execSimple } from \"./utils.js\";\n\nexport function parseImageSandboxArg(value: string): ImageSandboxConfig | undefined {\n if (!value.startsWith(\"image:\")) {\n return undefined;\n }\n\n const image = value.slice(\"image:\".length);\n if (!image) {\n throw new SandboxError(\"Error: image sandbox requires image name (e.g., image:ubuntu:24.04)\");\n }\n return { type: \"image\", image };\n}\n\nexport async function validateImageSandbox(config: ImageSandboxConfig): Promise<void> {\n try {\n await execSimple(\"docker\", [\"--version\"]);\n } catch {\n throw new SandboxError(\"Error: Docker is not installed or not in PATH\");\n }\n console.log(` Image auto-provisioning enabled. Image: ${config.image}`);\n}\n\nexport const imageSandboxAdapter: SandboxAdapter<ImageSandboxConfig> = {\n type: \"image\",\n parse: parseImageSandboxArg,\n validate: validateImageSandbox,\n createExecutor: () => {\n throw new SandboxError(\"Error: image sandbox must resolve to a concrete container executor\");\n },\n};\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SandboxError } from "./errors.js";
|
|
2
|
+
import { execSimple } from "./utils.js";
|
|
3
|
+
export function parseImageSandboxArg(value) {
|
|
4
|
+
if (!value.startsWith("image:")) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const image = value.slice("image:".length);
|
|
8
|
+
if (!image) {
|
|
9
|
+
throw new SandboxError("Error: image sandbox requires image name (e.g., image:ubuntu:24.04)");
|
|
10
|
+
}
|
|
11
|
+
return { type: "image", image };
|
|
12
|
+
}
|
|
13
|
+
export async function validateImageSandbox(config) {
|
|
14
|
+
try {
|
|
15
|
+
await execSimple("docker", ["--version"]);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new SandboxError("Error: Docker is not installed or not in PATH");
|
|
19
|
+
}
|
|
20
|
+
console.log(` Image auto-provisioning enabled. Image: ${config.image}`);
|
|
21
|
+
}
|
|
22
|
+
export const imageSandboxAdapter = {
|
|
23
|
+
type: "image",
|
|
24
|
+
parse: parseImageSandboxArg,
|
|
25
|
+
validate: validateImageSandbox,
|
|
26
|
+
createExecutor: () => {
|
|
27
|
+
throw new SandboxError("Error: image sandbox must resolve to a concrete container executor");
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=image.js.map
|