@nebutra/sandbox-runtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nebutra/debug/sandbox-runtime.jsonl +1 -0
- package/.turbo/turbo-build.log +22 -0
- package/.turbo/turbo-test.log +14 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +676 -0
- package/dist/chunk-UGJKHUL3.js +151 -0
- package/dist/chunk-UGJKHUL3.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +30 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/examples/doctor.ts +6 -0
- package/examples/dry-run-plan.ts +9 -0
- package/examples/local-echo.ts +11 -0
- package/package.json +44 -0
- package/src/cli.ts +22 -0
- package/src/index.test.ts +41 -0
- package/src/index.ts +212 -0
- package/tsconfig.json +12 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { CapabilityError } from "@nebutra/errors";
|
|
5
|
+
function sandboxDebugPath() {
|
|
6
|
+
return join(process.cwd(), ".nebutra", "debug", "sandbox-runtime.jsonl");
|
|
7
|
+
}
|
|
8
|
+
async function appendSandboxDebug(entry) {
|
|
9
|
+
const path = sandboxDebugPath();
|
|
10
|
+
await mkdir(dirname(path), { recursive: true });
|
|
11
|
+
await writeFile(path, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
12
|
+
`, {
|
|
13
|
+
flag: "a"
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function createLocalMacSandbox(options = {}) {
|
|
17
|
+
const endpoint = options.endpoint ?? process.env.SANDBOX_RUNTIME_LOCAL_URL ?? "http://127.0.0.1:8020/api/v1/sandbox/exec";
|
|
18
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
19
|
+
return {
|
|
20
|
+
id: "local_mac",
|
|
21
|
+
async exec(request) {
|
|
22
|
+
const response = await fetchImpl(endpoint, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "content-type": "application/json" },
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
tenantId: request.tenantId ?? "local",
|
|
27
|
+
threadId: request.threadId ?? "local",
|
|
28
|
+
command: request.cmd,
|
|
29
|
+
capabilityPolicy: {
|
|
30
|
+
kind: "external_sandbox",
|
|
31
|
+
network_access: request.hints?.networkAccess ?? false
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const detail = await response.text().catch(() => "");
|
|
37
|
+
throw new CapabilityError(
|
|
38
|
+
"sandbox-runtime",
|
|
39
|
+
`Local sandbox refused execution (${response.status})`,
|
|
40
|
+
{
|
|
41
|
+
suggestion: "Start the Rust sandbox service or route this task to a configured remote provider.",
|
|
42
|
+
metadata: { detail },
|
|
43
|
+
statusCode: response.status
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const result = await response.json();
|
|
48
|
+
await appendSandboxDebug({
|
|
49
|
+
type: "exec",
|
|
50
|
+
provider: "local_mac",
|
|
51
|
+
command: request.cmd,
|
|
52
|
+
result
|
|
53
|
+
});
|
|
54
|
+
return result;
|
|
55
|
+
},
|
|
56
|
+
async doctor() {
|
|
57
|
+
try {
|
|
58
|
+
const result = await this.exec({ cmd: "echo sandbox ok" });
|
|
59
|
+
return { provider: "local_mac", ok: result.exitCode === 0 };
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
provider: "local_mac",
|
|
63
|
+
ok: false,
|
|
64
|
+
suggestion: error instanceof Error ? error.message : "Check local sandbox service reachability."
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function createRemotePlaceholder(id, envKey) {
|
|
71
|
+
return {
|
|
72
|
+
id,
|
|
73
|
+
async exec() {
|
|
74
|
+
throw new CapabilityError("sandbox-runtime", `${id} adapter is not configured`, {
|
|
75
|
+
suggestion: `Set ${envKey} and wire the provider SDK before routing tasks to ${id}.`,
|
|
76
|
+
statusCode: 503
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
async doctor() {
|
|
80
|
+
return process.env[envKey] ? { provider: id, ok: true } : { provider: id, ok: false, suggestion: `Set ${envKey} to enable ${id}.` };
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
var SandboxRuntime = class _SandboxRuntime {
|
|
85
|
+
#providers = /* @__PURE__ */ new Map();
|
|
86
|
+
#routes;
|
|
87
|
+
constructor(config = {}) {
|
|
88
|
+
for (const provider of config.providers ?? [
|
|
89
|
+
createLocalMacSandbox(),
|
|
90
|
+
createRemotePlaceholder("remote_code", "SANDBOX_REMOTE_CODE_TOKEN"),
|
|
91
|
+
createRemotePlaceholder("remote_gpu", "SANDBOX_REMOTE_GPU_TOKEN"),
|
|
92
|
+
createRemotePlaceholder("remote_workspace", "SANDBOX_REMOTE_WORKSPACE_TOKEN")
|
|
93
|
+
]) {
|
|
94
|
+
this.#providers.set(provider.id, provider);
|
|
95
|
+
}
|
|
96
|
+
this.#routes = config.routes ?? [
|
|
97
|
+
{ when: { needsGpu: true }, provider: "remote_gpu" },
|
|
98
|
+
{ when: { durationS: 30 }, provider: "remote_code" },
|
|
99
|
+
{ when: { needsGpu: false }, provider: "local_mac" }
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
static fromConfig(config = {}) {
|
|
103
|
+
return new _SandboxRuntime(config);
|
|
104
|
+
}
|
|
105
|
+
plan(request) {
|
|
106
|
+
const hints = request.hints ?? {};
|
|
107
|
+
for (const rule of this.#routes) {
|
|
108
|
+
const matches = Object.entries(rule.when).every(
|
|
109
|
+
([key, value]) => hints[key] === value
|
|
110
|
+
);
|
|
111
|
+
if (matches && this.#providers.has(rule.provider)) {
|
|
112
|
+
return { provider: rule.provider, reason: "matched static routing rule" };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
provider: this.#providers.has("local_mac") ? "local_mac" : Array.from(this.#providers.keys())[0] ?? "",
|
|
117
|
+
reason: "default route"
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async exec(request) {
|
|
121
|
+
const plan = this.plan(request);
|
|
122
|
+
const provider = this.#providers.get(plan.provider);
|
|
123
|
+
if (!provider) {
|
|
124
|
+
throw new CapabilityError("sandbox-runtime", "No sandbox provider available", {
|
|
125
|
+
suggestion: "Configure at least one sandbox provider before executing commands.",
|
|
126
|
+
metadata: plan
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return provider.exec(request);
|
|
130
|
+
}
|
|
131
|
+
async doctor() {
|
|
132
|
+
return Promise.all(Array.from(this.#providers.values()).map((provider) => provider.doctor()));
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
async function readSandboxDebug(limit = 10) {
|
|
136
|
+
try {
|
|
137
|
+
const raw = await readFile(sandboxDebugPath(), "utf8");
|
|
138
|
+
return raw.trim().split("\n").filter(Boolean).slice(-limit).map((line) => JSON.parse(line));
|
|
139
|
+
} catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export {
|
|
145
|
+
sandboxDebugPath,
|
|
146
|
+
createLocalMacSandbox,
|
|
147
|
+
createRemotePlaceholder,
|
|
148
|
+
SandboxRuntime,
|
|
149
|
+
readSandboxDebug
|
|
150
|
+
};
|
|
151
|
+
//# sourceMappingURL=chunk-UGJKHUL3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { CapabilityError } from \"@nebutra/errors\";\n\nexport interface ExecHints {\n readonly durationS?: number;\n readonly needsGpu?: boolean;\n readonly networkAccess?: boolean;\n}\n\nexport interface ExecRequest {\n readonly cmd: string;\n readonly filesIn?: Record<string, string>;\n readonly hints?: ExecHints;\n readonly tenantId?: string;\n readonly threadId?: string;\n}\n\nexport interface ExecResponse {\n readonly exitCode: number;\n readonly aggregatedOutput: string;\n readonly executedOn: string;\n}\n\nexport interface SandboxHealth {\n readonly provider: string;\n readonly ok: boolean;\n readonly suggestion?: string;\n}\n\nexport interface Sandbox {\n readonly id: string;\n exec(request: ExecRequest): Promise<ExecResponse>;\n doctor(): Promise<SandboxHealth>;\n}\n\nexport interface RouteRule {\n readonly when: Partial<ExecHints>;\n readonly provider: string;\n}\n\nexport interface SandboxRuntimeConfig {\n readonly providers?: readonly Sandbox[];\n readonly routes?: readonly RouteRule[];\n}\n\nexport function sandboxDebugPath(): string {\n return join(process.cwd(), \".nebutra\", \"debug\", \"sandbox-runtime.jsonl\");\n}\n\nasync function appendSandboxDebug(entry: Record<string, unknown>): Promise<void> {\n const path = sandboxDebugPath();\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\\n`, {\n flag: \"a\",\n });\n}\n\nexport function createLocalMacSandbox(\n options: { endpoint?: string; fetch?: typeof fetch } = {},\n): Sandbox {\n const endpoint =\n options.endpoint ??\n process.env.SANDBOX_RUNTIME_LOCAL_URL ??\n \"http://127.0.0.1:8020/api/v1/sandbox/exec\";\n const fetchImpl = options.fetch ?? fetch;\n return {\n id: \"local_mac\",\n async exec(request) {\n const response = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n tenantId: request.tenantId ?? \"local\",\n threadId: request.threadId ?? \"local\",\n command: request.cmd,\n capabilityPolicy: {\n kind: \"external_sandbox\",\n network_access: request.hints?.networkAccess ?? false,\n },\n }),\n });\n if (!response.ok) {\n const detail = await response.text().catch(() => \"\");\n throw new CapabilityError(\n \"sandbox-runtime\",\n `Local sandbox refused execution (${response.status})`,\n {\n suggestion:\n \"Start the Rust sandbox service or route this task to a configured remote provider.\",\n metadata: { detail },\n statusCode: response.status,\n },\n );\n }\n const result = (await response.json()) as ExecResponse;\n await appendSandboxDebug({\n type: \"exec\",\n provider: \"local_mac\",\n command: request.cmd,\n result,\n });\n return result;\n },\n async doctor() {\n try {\n const result = await this.exec({ cmd: \"echo sandbox ok\" });\n return { provider: \"local_mac\", ok: result.exitCode === 0 };\n } catch (error) {\n return {\n provider: \"local_mac\",\n ok: false,\n suggestion:\n error instanceof Error ? error.message : \"Check local sandbox service reachability.\",\n };\n }\n },\n };\n}\n\nexport function createRemotePlaceholder(\n id: \"remote_code\" | \"remote_gpu\" | \"remote_workspace\",\n envKey: string,\n): Sandbox {\n return {\n id,\n async exec() {\n throw new CapabilityError(\"sandbox-runtime\", `${id} adapter is not configured`, {\n suggestion: `Set ${envKey} and wire the provider SDK before routing tasks to ${id}.`,\n statusCode: 503,\n });\n },\n async doctor() {\n return process.env[envKey]\n ? { provider: id, ok: true }\n : { provider: id, ok: false, suggestion: `Set ${envKey} to enable ${id}.` };\n },\n };\n}\n\nexport class SandboxRuntime {\n readonly #providers = new Map<string, Sandbox>();\n readonly #routes: readonly RouteRule[];\n\n constructor(config: SandboxRuntimeConfig = {}) {\n for (const provider of config.providers ?? [\n createLocalMacSandbox(),\n createRemotePlaceholder(\"remote_code\", \"SANDBOX_REMOTE_CODE_TOKEN\"),\n createRemotePlaceholder(\"remote_gpu\", \"SANDBOX_REMOTE_GPU_TOKEN\"),\n createRemotePlaceholder(\"remote_workspace\", \"SANDBOX_REMOTE_WORKSPACE_TOKEN\"),\n ]) {\n this.#providers.set(provider.id, provider);\n }\n this.#routes = config.routes ?? [\n { when: { needsGpu: true }, provider: \"remote_gpu\" },\n { when: { durationS: 30 }, provider: \"remote_code\" },\n { when: { needsGpu: false }, provider: \"local_mac\" },\n ];\n }\n\n static fromConfig(config: SandboxRuntimeConfig = {}): SandboxRuntime {\n return new SandboxRuntime(config);\n }\n\n plan(request: ExecRequest): { provider: string; reason: string } {\n const hints = request.hints ?? {};\n for (const rule of this.#routes) {\n const matches = Object.entries(rule.when).every(\n ([key, value]) => hints[key as keyof ExecHints] === value,\n );\n if (matches && this.#providers.has(rule.provider)) {\n return { provider: rule.provider, reason: \"matched static routing rule\" };\n }\n }\n return {\n provider: this.#providers.has(\"local_mac\")\n ? \"local_mac\"\n : (Array.from(this.#providers.keys())[0] ?? \"\"),\n reason: \"default route\",\n };\n }\n\n async exec(request: ExecRequest): Promise<ExecResponse> {\n const plan = this.plan(request);\n const provider = this.#providers.get(plan.provider);\n if (!provider) {\n throw new CapabilityError(\"sandbox-runtime\", \"No sandbox provider available\", {\n suggestion: \"Configure at least one sandbox provider before executing commands.\",\n metadata: plan,\n });\n }\n return provider.exec(request);\n }\n\n async doctor(): Promise<SandboxHealth[]> {\n return Promise.all(Array.from(this.#providers.values()).map((provider) => provider.doctor()));\n }\n}\n\nexport async function readSandboxDebug(limit = 10): Promise<unknown[]> {\n try {\n const raw = await readFile(sandboxDebugPath(), \"utf8\");\n return raw\n .trim()\n .split(\"\\n\")\n .filter(Boolean)\n .slice(-limit)\n .map((line) => JSON.parse(line) as unknown);\n } catch {\n return [];\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SandboxRuntime,
|
|
3
|
+
readSandboxDebug
|
|
4
|
+
} from "./chunk-UGJKHUL3.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
var command = process.argv[2] ?? "doctor";
|
|
8
|
+
var runtime = SandboxRuntime.fromConfig();
|
|
9
|
+
if (command === "doctor") {
|
|
10
|
+
process.stdout.write(
|
|
11
|
+
`${JSON.stringify({ capability: "sandbox-runtime", results: await runtime.doctor() }, null, 2)}
|
|
12
|
+
`
|
|
13
|
+
);
|
|
14
|
+
} else if (command === "debug") {
|
|
15
|
+
process.stdout.write(
|
|
16
|
+
`${JSON.stringify({ capability: "sandbox-runtime", entries: await readSandboxDebug() }, null, 2)}
|
|
17
|
+
`
|
|
18
|
+
);
|
|
19
|
+
} else if (command === "plan") {
|
|
20
|
+
const task = process.argv.slice(3).join(" ") || "echo sandbox ok";
|
|
21
|
+
process.stdout.write(
|
|
22
|
+
`${JSON.stringify({ capability: "sandbox-runtime", task, plan: runtime.plan({ cmd: task }) }, null, 2)}
|
|
23
|
+
`
|
|
24
|
+
);
|
|
25
|
+
} else {
|
|
26
|
+
process.stderr.write(`Unknown sandbox-runtime command: ${command}
|
|
27
|
+
`);
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { readSandboxDebug, SandboxRuntime } from \"./index\";\n\nconst command = process.argv[2] ?? \"doctor\";\nconst runtime = SandboxRuntime.fromConfig();\n\nif (command === \"doctor\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"sandbox-runtime\", results: await runtime.doctor() }, null, 2)}\\n`,\n );\n} else if (command === \"debug\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"sandbox-runtime\", entries: await readSandboxDebug() }, null, 2)}\\n`,\n );\n} else if (command === \"plan\") {\n const task = process.argv.slice(3).join(\" \") || \"echo sandbox ok\";\n process.stdout.write(\n `${JSON.stringify({ capability: \"sandbox-runtime\", task, plan: runtime.plan({ cmd: task }) }, null, 2)}\\n`,\n );\n} else {\n process.stderr.write(`Unknown sandbox-runtime command: ${command}\\n`);\n process.exitCode = 1;\n}\n"],"mappings":";;;;;;AAEA,IAAM,UAAU,QAAQ,KAAK,CAAC,KAAK;AACnC,IAAM,UAAU,eAAe,WAAW;AAE1C,IAAI,YAAY,UAAU;AACxB,UAAQ,OAAO;AAAA,IACb,GAAG,KAAK,UAAU,EAAE,YAAY,mBAAmB,SAAS,MAAM,QAAQ,OAAO,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,EAChG;AACF,WAAW,YAAY,SAAS;AAC9B,UAAQ,OAAO;AAAA,IACb,GAAG,KAAK,UAAU,EAAE,YAAY,mBAAmB,SAAS,MAAM,iBAAiB,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,EAClG;AACF,WAAW,YAAY,QAAQ;AAC7B,QAAM,OAAO,QAAQ,KAAK,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK;AAChD,UAAQ,OAAO;AAAA,IACb,GAAG,KAAK,UAAU,EAAE,YAAY,mBAAmB,MAAM,MAAM,QAAQ,KAAK,EAAE,KAAK,KAAK,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,EACxG;AACF,OAAO;AACL,UAAQ,OAAO,MAAM,oCAAoC,OAAO;AAAA,CAAI;AACpE,UAAQ,WAAW;AACrB;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
interface ExecHints {
|
|
2
|
+
readonly durationS?: number;
|
|
3
|
+
readonly needsGpu?: boolean;
|
|
4
|
+
readonly networkAccess?: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface ExecRequest {
|
|
7
|
+
readonly cmd: string;
|
|
8
|
+
readonly filesIn?: Record<string, string>;
|
|
9
|
+
readonly hints?: ExecHints;
|
|
10
|
+
readonly tenantId?: string;
|
|
11
|
+
readonly threadId?: string;
|
|
12
|
+
}
|
|
13
|
+
interface ExecResponse {
|
|
14
|
+
readonly exitCode: number;
|
|
15
|
+
readonly aggregatedOutput: string;
|
|
16
|
+
readonly executedOn: string;
|
|
17
|
+
}
|
|
18
|
+
interface SandboxHealth {
|
|
19
|
+
readonly provider: string;
|
|
20
|
+
readonly ok: boolean;
|
|
21
|
+
readonly suggestion?: string;
|
|
22
|
+
}
|
|
23
|
+
interface Sandbox {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
exec(request: ExecRequest): Promise<ExecResponse>;
|
|
26
|
+
doctor(): Promise<SandboxHealth>;
|
|
27
|
+
}
|
|
28
|
+
interface RouteRule {
|
|
29
|
+
readonly when: Partial<ExecHints>;
|
|
30
|
+
readonly provider: string;
|
|
31
|
+
}
|
|
32
|
+
interface SandboxRuntimeConfig {
|
|
33
|
+
readonly providers?: readonly Sandbox[];
|
|
34
|
+
readonly routes?: readonly RouteRule[];
|
|
35
|
+
}
|
|
36
|
+
declare function sandboxDebugPath(): string;
|
|
37
|
+
declare function createLocalMacSandbox(options?: {
|
|
38
|
+
endpoint?: string;
|
|
39
|
+
fetch?: typeof fetch;
|
|
40
|
+
}): Sandbox;
|
|
41
|
+
declare function createRemotePlaceholder(id: "remote_code" | "remote_gpu" | "remote_workspace", envKey: string): Sandbox;
|
|
42
|
+
declare class SandboxRuntime {
|
|
43
|
+
#private;
|
|
44
|
+
constructor(config?: SandboxRuntimeConfig);
|
|
45
|
+
static fromConfig(config?: SandboxRuntimeConfig): SandboxRuntime;
|
|
46
|
+
plan(request: ExecRequest): {
|
|
47
|
+
provider: string;
|
|
48
|
+
reason: string;
|
|
49
|
+
};
|
|
50
|
+
exec(request: ExecRequest): Promise<ExecResponse>;
|
|
51
|
+
doctor(): Promise<SandboxHealth[]>;
|
|
52
|
+
}
|
|
53
|
+
declare function readSandboxDebug(limit?: number): Promise<unknown[]>;
|
|
54
|
+
|
|
55
|
+
export { type ExecHints, type ExecRequest, type ExecResponse, type RouteRule, type Sandbox, type SandboxHealth, SandboxRuntime, type SandboxRuntimeConfig, createLocalMacSandbox, createRemotePlaceholder, readSandboxDebug, sandboxDebugPath };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SandboxRuntime,
|
|
3
|
+
createLocalMacSandbox,
|
|
4
|
+
createRemotePlaceholder,
|
|
5
|
+
readSandboxDebug,
|
|
6
|
+
sandboxDebugPath
|
|
7
|
+
} from "./chunk-UGJKHUL3.js";
|
|
8
|
+
export {
|
|
9
|
+
SandboxRuntime,
|
|
10
|
+
createLocalMacSandbox,
|
|
11
|
+
createRemotePlaceholder,
|
|
12
|
+
readSandboxDebug,
|
|
13
|
+
sandboxDebugPath
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SandboxRuntime } from "../src/index";
|
|
2
|
+
|
|
3
|
+
const runtime = SandboxRuntime.fromConfig();
|
|
4
|
+
const plan = runtime.plan({
|
|
5
|
+
cmd: "echo sandbox ok",
|
|
6
|
+
hints: { needsGpu: false, durationS: 1 },
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SandboxRuntime } from "../src/index";
|
|
2
|
+
|
|
3
|
+
const runtime = SandboxRuntime.fromConfig();
|
|
4
|
+
const result = await runtime.exec({
|
|
5
|
+
cmd: "echo sandbox ok",
|
|
6
|
+
hints: { needsGpu: false },
|
|
7
|
+
tenantId: "local",
|
|
8
|
+
threadId: "example",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nebutra/sandbox-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unified sandbox runtime router for local and remote execution providers",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"nebutra": {
|
|
9
|
+
"featureId": "sandbox-runtime",
|
|
10
|
+
"category": "ai",
|
|
11
|
+
"summary": "Sandbox trait, provider router, doctor, and debug tools"
|
|
12
|
+
},
|
|
13
|
+
"main": "./src/index.ts",
|
|
14
|
+
"types": "./src/index.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.ts"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@nebutra/errors": "0.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.19.15",
|
|
23
|
+
"tsup": "^8.5.1",
|
|
24
|
+
"typescript": "^5.9.3",
|
|
25
|
+
"vitest": "^4.1.4"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/Nebutra/Nebutra-Sailor/tree/main/packages/ai/sandbox-runtime#readme",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/Nebutra/Nebutra-Sailor.git",
|
|
31
|
+
"directory": "packages/ai/sandbox-runtime"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/Nebutra/Nebutra-Sailor/issues"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readSandboxDebug, SandboxRuntime } from "./index";
|
|
2
|
+
|
|
3
|
+
const command = process.argv[2] ?? "doctor";
|
|
4
|
+
const runtime = SandboxRuntime.fromConfig();
|
|
5
|
+
|
|
6
|
+
if (command === "doctor") {
|
|
7
|
+
process.stdout.write(
|
|
8
|
+
`${JSON.stringify({ capability: "sandbox-runtime", results: await runtime.doctor() }, null, 2)}\n`,
|
|
9
|
+
);
|
|
10
|
+
} else if (command === "debug") {
|
|
11
|
+
process.stdout.write(
|
|
12
|
+
`${JSON.stringify({ capability: "sandbox-runtime", entries: await readSandboxDebug() }, null, 2)}\n`,
|
|
13
|
+
);
|
|
14
|
+
} else if (command === "plan") {
|
|
15
|
+
const task = process.argv.slice(3).join(" ") || "echo sandbox ok";
|
|
16
|
+
process.stdout.write(
|
|
17
|
+
`${JSON.stringify({ capability: "sandbox-runtime", task, plan: runtime.plan({ cmd: task }) }, null, 2)}\n`,
|
|
18
|
+
);
|
|
19
|
+
} else {
|
|
20
|
+
process.stderr.write(`Unknown sandbox-runtime command: ${command}\n`);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createLocalMacSandbox, SandboxRuntime } from "./index";
|
|
3
|
+
|
|
4
|
+
describe("SandboxRuntime", () => {
|
|
5
|
+
it("plans deterministic provider routing without asking a model", () => {
|
|
6
|
+
const runtime = SandboxRuntime.fromConfig({
|
|
7
|
+
providers: [createLocalMacSandbox({ fetch: fetch })],
|
|
8
|
+
routes: [{ when: { needsGpu: false }, provider: "local_mac" }],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(runtime.plan({ cmd: "echo sandbox ok", hints: { needsGpu: false } })).toMatchObject({
|
|
12
|
+
provider: "local_mac",
|
|
13
|
+
reason: expect.stringContaining("rule"),
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("delegates execution to the local isolator endpoint", async () => {
|
|
18
|
+
const runtime = SandboxRuntime.fromConfig({
|
|
19
|
+
providers: [
|
|
20
|
+
createLocalMacSandbox({
|
|
21
|
+
fetch: async () =>
|
|
22
|
+
new Response(
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
exitCode: 0,
|
|
25
|
+
aggregatedOutput: "sandbox ok\n",
|
|
26
|
+
executedOn: "local_mac",
|
|
27
|
+
}),
|
|
28
|
+
{
|
|
29
|
+
status: 200,
|
|
30
|
+
},
|
|
31
|
+
),
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await expect(runtime.exec({ cmd: "echo sandbox ok" })).resolves.toMatchObject({
|
|
37
|
+
exitCode: 0,
|
|
38
|
+
executedOn: "local_mac",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { CapabilityError } from "@nebutra/errors";
|
|
4
|
+
|
|
5
|
+
export interface ExecHints {
|
|
6
|
+
readonly durationS?: number;
|
|
7
|
+
readonly needsGpu?: boolean;
|
|
8
|
+
readonly networkAccess?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ExecRequest {
|
|
12
|
+
readonly cmd: string;
|
|
13
|
+
readonly filesIn?: Record<string, string>;
|
|
14
|
+
readonly hints?: ExecHints;
|
|
15
|
+
readonly tenantId?: string;
|
|
16
|
+
readonly threadId?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ExecResponse {
|
|
20
|
+
readonly exitCode: number;
|
|
21
|
+
readonly aggregatedOutput: string;
|
|
22
|
+
readonly executedOn: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SandboxHealth {
|
|
26
|
+
readonly provider: string;
|
|
27
|
+
readonly ok: boolean;
|
|
28
|
+
readonly suggestion?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Sandbox {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
exec(request: ExecRequest): Promise<ExecResponse>;
|
|
34
|
+
doctor(): Promise<SandboxHealth>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RouteRule {
|
|
38
|
+
readonly when: Partial<ExecHints>;
|
|
39
|
+
readonly provider: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SandboxRuntimeConfig {
|
|
43
|
+
readonly providers?: readonly Sandbox[];
|
|
44
|
+
readonly routes?: readonly RouteRule[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function sandboxDebugPath(): string {
|
|
48
|
+
return join(process.cwd(), ".nebutra", "debug", "sandbox-runtime.jsonl");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function appendSandboxDebug(entry: Record<string, unknown>): Promise<void> {
|
|
52
|
+
const path = sandboxDebugPath();
|
|
53
|
+
await mkdir(dirname(path), { recursive: true });
|
|
54
|
+
await writeFile(path, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`, {
|
|
55
|
+
flag: "a",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createLocalMacSandbox(
|
|
60
|
+
options: { endpoint?: string; fetch?: typeof fetch } = {},
|
|
61
|
+
): Sandbox {
|
|
62
|
+
const endpoint =
|
|
63
|
+
options.endpoint ??
|
|
64
|
+
process.env.SANDBOX_RUNTIME_LOCAL_URL ??
|
|
65
|
+
"http://127.0.0.1:8020/api/v1/sandbox/exec";
|
|
66
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
67
|
+
return {
|
|
68
|
+
id: "local_mac",
|
|
69
|
+
async exec(request) {
|
|
70
|
+
const response = await fetchImpl(endpoint, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "content-type": "application/json" },
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
tenantId: request.tenantId ?? "local",
|
|
75
|
+
threadId: request.threadId ?? "local",
|
|
76
|
+
command: request.cmd,
|
|
77
|
+
capabilityPolicy: {
|
|
78
|
+
kind: "external_sandbox",
|
|
79
|
+
network_access: request.hints?.networkAccess ?? false,
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const detail = await response.text().catch(() => "");
|
|
85
|
+
throw new CapabilityError(
|
|
86
|
+
"sandbox-runtime",
|
|
87
|
+
`Local sandbox refused execution (${response.status})`,
|
|
88
|
+
{
|
|
89
|
+
suggestion:
|
|
90
|
+
"Start the Rust sandbox service or route this task to a configured remote provider.",
|
|
91
|
+
metadata: { detail },
|
|
92
|
+
statusCode: response.status,
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const result = (await response.json()) as ExecResponse;
|
|
97
|
+
await appendSandboxDebug({
|
|
98
|
+
type: "exec",
|
|
99
|
+
provider: "local_mac",
|
|
100
|
+
command: request.cmd,
|
|
101
|
+
result,
|
|
102
|
+
});
|
|
103
|
+
return result;
|
|
104
|
+
},
|
|
105
|
+
async doctor() {
|
|
106
|
+
try {
|
|
107
|
+
const result = await this.exec({ cmd: "echo sandbox ok" });
|
|
108
|
+
return { provider: "local_mac", ok: result.exitCode === 0 };
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
provider: "local_mac",
|
|
112
|
+
ok: false,
|
|
113
|
+
suggestion:
|
|
114
|
+
error instanceof Error ? error.message : "Check local sandbox service reachability.",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function createRemotePlaceholder(
|
|
122
|
+
id: "remote_code" | "remote_gpu" | "remote_workspace",
|
|
123
|
+
envKey: string,
|
|
124
|
+
): Sandbox {
|
|
125
|
+
return {
|
|
126
|
+
id,
|
|
127
|
+
async exec() {
|
|
128
|
+
throw new CapabilityError("sandbox-runtime", `${id} adapter is not configured`, {
|
|
129
|
+
suggestion: `Set ${envKey} and wire the provider SDK before routing tasks to ${id}.`,
|
|
130
|
+
statusCode: 503,
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
async doctor() {
|
|
134
|
+
return process.env[envKey]
|
|
135
|
+
? { provider: id, ok: true }
|
|
136
|
+
: { provider: id, ok: false, suggestion: `Set ${envKey} to enable ${id}.` };
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class SandboxRuntime {
|
|
142
|
+
readonly #providers = new Map<string, Sandbox>();
|
|
143
|
+
readonly #routes: readonly RouteRule[];
|
|
144
|
+
|
|
145
|
+
constructor(config: SandboxRuntimeConfig = {}) {
|
|
146
|
+
for (const provider of config.providers ?? [
|
|
147
|
+
createLocalMacSandbox(),
|
|
148
|
+
createRemotePlaceholder("remote_code", "SANDBOX_REMOTE_CODE_TOKEN"),
|
|
149
|
+
createRemotePlaceholder("remote_gpu", "SANDBOX_REMOTE_GPU_TOKEN"),
|
|
150
|
+
createRemotePlaceholder("remote_workspace", "SANDBOX_REMOTE_WORKSPACE_TOKEN"),
|
|
151
|
+
]) {
|
|
152
|
+
this.#providers.set(provider.id, provider);
|
|
153
|
+
}
|
|
154
|
+
this.#routes = config.routes ?? [
|
|
155
|
+
{ when: { needsGpu: true }, provider: "remote_gpu" },
|
|
156
|
+
{ when: { durationS: 30 }, provider: "remote_code" },
|
|
157
|
+
{ when: { needsGpu: false }, provider: "local_mac" },
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static fromConfig(config: SandboxRuntimeConfig = {}): SandboxRuntime {
|
|
162
|
+
return new SandboxRuntime(config);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
plan(request: ExecRequest): { provider: string; reason: string } {
|
|
166
|
+
const hints = request.hints ?? {};
|
|
167
|
+
for (const rule of this.#routes) {
|
|
168
|
+
const matches = Object.entries(rule.when).every(
|
|
169
|
+
([key, value]) => hints[key as keyof ExecHints] === value,
|
|
170
|
+
);
|
|
171
|
+
if (matches && this.#providers.has(rule.provider)) {
|
|
172
|
+
return { provider: rule.provider, reason: "matched static routing rule" };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
provider: this.#providers.has("local_mac")
|
|
177
|
+
? "local_mac"
|
|
178
|
+
: (Array.from(this.#providers.keys())[0] ?? ""),
|
|
179
|
+
reason: "default route",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async exec(request: ExecRequest): Promise<ExecResponse> {
|
|
184
|
+
const plan = this.plan(request);
|
|
185
|
+
const provider = this.#providers.get(plan.provider);
|
|
186
|
+
if (!provider) {
|
|
187
|
+
throw new CapabilityError("sandbox-runtime", "No sandbox provider available", {
|
|
188
|
+
suggestion: "Configure at least one sandbox provider before executing commands.",
|
|
189
|
+
metadata: plan,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return provider.exec(request);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async doctor(): Promise<SandboxHealth[]> {
|
|
196
|
+
return Promise.all(Array.from(this.#providers.values()).map((provider) => provider.doctor()));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function readSandboxDebug(limit = 10): Promise<unknown[]> {
|
|
201
|
+
try {
|
|
202
|
+
const raw = await readFile(sandboxDebugPath(), "utf8");
|
|
203
|
+
return raw
|
|
204
|
+
.trim()
|
|
205
|
+
.split("\n")
|
|
206
|
+
.filter(Boolean)
|
|
207
|
+
.slice(-limit)
|
|
208
|
+
.map((line) => JSON.parse(line) as unknown);
|
|
209
|
+
} catch {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"target": "esnext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"incremental": false
|
|
9
|
+
},
|
|
10
|
+
"include": ["src", "examples"],
|
|
11
|
+
"exclude": ["node_modules", "dist"]
|
|
12
|
+
}
|
package/tsup.config.ts
ADDED