@engenty/wizards-runtime 0.7.5 → 0.7.7
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/apps/runtime/dist/authoring/guide.js +5 -1
- package/apps/runtime/dist/cli/environment.js +2 -0
- package/apps/runtime/dist/cli/main.js +10 -1
- package/apps/runtime/dist/cli/sandbox.js +77 -0
- package/apps/runtime/dist/cli/status.js +9 -0
- package/apps/runtime/dist/debug.js +2 -1
- package/apps/runtime/dist/docs/dev/index.md +6 -1
- package/apps/runtime/dist/docs/dev/integration/channels.md +11 -0
- package/apps/runtime/dist/docs/dev/integration/claude-code-plugin.md +10 -0
- package/apps/runtime/dist/docs/dev/integration/embed.md +10 -0
- package/apps/runtime/dist/docs/dev/integration/index.md +36 -0
- package/apps/runtime/dist/docs/dev/integration/link-and-share.md +10 -0
- package/apps/runtime/dist/docs/dev/integration/mcp-apps.md +10 -0
- package/apps/runtime/dist/docs/dev/integration/mcp.md +10 -0
- package/apps/runtime/dist/docs/dev/integration/meta.json +20 -0
- package/apps/runtime/dist/docs/dev/integration/openui.md +181 -0
- package/apps/runtime/dist/docs/dev/integration/own-ui.md +10 -0
- package/apps/runtime/dist/docs/dev/integration/results.md +11 -0
- package/apps/runtime/dist/docs/dev/integration/wordpress.md +10 -0
- package/apps/runtime/dist/docs/dev/meta.json +1 -1
- package/apps/runtime/dist/docs/user/command-line.md +3 -0
- package/apps/runtime/dist/docs/user/server.md +1 -1
- package/apps/runtime/dist/engine/steps.js +2 -1
- package/apps/runtime/dist/env.js +10 -26
- package/apps/runtime/dist/routes/runs.js +14 -1
- package/apps/runtime/dist/sandbox/agentos-package.js +73 -0
- package/apps/runtime/dist/sandbox/agentos.js +162 -0
- package/apps/runtime/dist/sandbox/docker.js +32 -62
- package/apps/runtime/dist/sandbox/index.js +37 -8
- package/apps/runtime/dist/sandbox/install.js +122 -0
- package/apps/runtime/dist/sandbox/mastra.js +71 -0
- package/apps/runtime/dist/services/spaces.js +2 -1
- package/apps/runtime/dist/tools/code.js +133 -0
- package/apps/runtime/dist/tools/connector.js +6 -0
- package/apps/runtime/dist/tools/index.js +14 -3
- package/apps/web/dist/assets/{AboutDialog-D4mvc-zu.js → AboutDialog-BP8-Ugxf.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-DLqpJsF6.js → ChangelogDialog-CD6CQNW9.js} +1 -1
- package/apps/web/dist/assets/{ChatView-3laypRcE.js → ChatView-4T0QarEr.js} +1 -1
- package/apps/web/dist/assets/{EditorPage-BalA5U2Y.js → EditorPage-CWdV7Xdn.js} +3 -3
- package/apps/web/dist/assets/{FlowPreview-CusP7ci9.js → FlowPreview-Dj2gAfuf.js} +1 -1
- package/apps/web/dist/assets/{Harness-iYLtOLaK.js → Harness-SLbsntQF.js} +1 -1
- package/apps/web/dist/assets/{NewWizardChannels-CbwJ9A56.js → NewWizardChannels-CsDMlji7.js} +1 -1
- package/apps/web/dist/assets/{NewWizardPage-BOBYiB9i.js → NewWizardPage-_bFYjlgh.js} +1 -1
- package/apps/web/dist/assets/{OnboardingPage-CUwCiVIr.js → OnboardingPage-DObQeCDg.js} +1 -1
- package/apps/web/dist/assets/{ProjectPage-Bb16MdYG.js → ProjectPage-DX-oNLWy.js} +1 -1
- package/apps/web/dist/assets/{PublicRunner-BCt7Q6C-.js → PublicRunner-Bn9rVNEb.js} +1 -1
- package/apps/web/dist/assets/{ReadOnly-kIHX2TEy.js → ReadOnly-CK0piLUV.js} +1 -1
- package/apps/web/dist/assets/{RunDrawer-hZ9oeSXu.js → RunDrawer-CVMTLse_.js} +1 -1
- package/apps/web/dist/assets/{RunnerView-DUS9fPmx.js → RunnerView-Cjhuu9ua.js} +1 -1
- package/apps/web/dist/assets/{SettingsPage-BzwR50fi.js → SettingsPage-DTzuiSAj.js} +1 -1
- package/apps/web/dist/assets/{SetupPage-gAp7rPxp.js → SetupPage-D9jlhgiP.js} +1 -1
- package/apps/web/dist/assets/{SharePage-DqZWHva_.js → SharePage-Cc3Opj2F.js} +1 -1
- package/apps/web/dist/assets/changelog-CJNkd1_L.js +1 -0
- package/apps/web/dist/assets/{esm-DXeMu8Lb.js → esm-CfnFE_jE.js} +1 -1
- package/apps/web/dist/assets/{index-C5Sm7tTR.js → index-B5F1_qA_.js} +4 -4
- package/apps/web/dist/assets/{lock-R_CfU0Xo.js → lock-DAvRJY4B.js} +1 -1
- package/apps/web/dist/assets/oss-credits-X2JPiMOR.js +1 -0
- package/apps/web/dist/assets/{phone-CgrIS4_-.js → phone-BWOB67tX.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-2IMOOlLl.js → settings-menu-CEQlVs1O.js} +1 -1
- package/apps/web/dist/assets/{ui-dzGvc3xk.js → ui-BmR_4eL2.js} +1 -1
- package/apps/web/dist/assets/{upload-BSuTilFF.js → upload-pBKn2EFp.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/apps/web/dist/mcp/flow.html +2 -2
- package/node_modules/@engenty/wizards/dist/openui/Wizard.d.ts +20 -0
- package/node_modules/@engenty/wizards/dist/openui/Wizard.js +62 -0
- package/node_modules/@engenty/wizards/dist/openui/index.d.ts +9 -0
- package/node_modules/@engenty/wizards/dist/openui/index.js +9 -0
- package/node_modules/@engenty/wizards/dist/openui/message.d.ts +11 -0
- package/node_modules/@engenty/wizards/dist/openui/message.js +69 -0
- package/node_modules/@engenty/wizards/dist/openui/run.d.ts +8 -0
- package/node_modules/@engenty/wizards/dist/openui/run.js +137 -0
- package/node_modules/@engenty/wizards/dist/openui/server.d.ts +37 -0
- package/node_modules/@engenty/wizards/dist/openui/server.js +77 -0
- package/node_modules/@engenty/wizards/package.json +4 -1
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +4 -1
- package/node_modules/@engenty-wizards/shared/dist/definition.js +1 -0
- package/node_modules/@engenty-wizards/shared/dist/fit.js +7 -2
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +14 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +2 -0
- package/package.json +4 -3
- package/apps/web/dist/assets/changelog-DugmCNSf.js +0 -1
- package/apps/web/dist/assets/oss-credits-cZ9NUaLZ.js +0 -1
package/apps/runtime/dist/env.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import { randomBytes } from "node:crypto";
|
|
3
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { hostname } from "node:os";
|
|
5
4
|
import { join, resolve } from "node:path";
|
|
6
5
|
import { findChrome } from "./render/browser-path.js";
|
|
6
|
+
import { enginesDir } from "./sandbox/install.js";
|
|
7
7
|
/** Load `.env.local` then `.env` without overriding the real environment. */
|
|
8
8
|
function loadEnvFiles() {
|
|
9
9
|
for (const name of [".env.local", ".env"]) {
|
|
@@ -22,29 +22,6 @@ function loadEnvFiles() {
|
|
|
22
22
|
}
|
|
23
23
|
loadEnvFiles();
|
|
24
24
|
const str = (key, fallback = "") => process.env[key]?.trim() || fallback;
|
|
25
|
-
/**
|
|
26
|
-
* Without `SANDBOX`: Docker when the sandbox image is on this machine, else none.
|
|
27
|
-
* The image is not published, so a fresh install would otherwise offer a tool that cannot start.
|
|
28
|
-
*/
|
|
29
|
-
function sandboxEngine(image) {
|
|
30
|
-
const engine = str("SANDBOX");
|
|
31
|
-
if (str("SANDBOX_ENABLED", "1") !== "1" || engine === "off") {
|
|
32
|
-
return "off";
|
|
33
|
-
}
|
|
34
|
-
if (engine) {
|
|
35
|
-
return "docker";
|
|
36
|
-
}
|
|
37
|
-
return dockerHasImage(image) ? "docker" : "off";
|
|
38
|
-
}
|
|
39
|
-
function dockerHasImage(image) {
|
|
40
|
-
try {
|
|
41
|
-
execFileSync("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 5000 });
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
25
|
/** A comma-separated list. */
|
|
49
26
|
const list = (key) => str(key)
|
|
50
27
|
.split(",")
|
|
@@ -217,9 +194,16 @@ export const env = {
|
|
|
217
194
|
: "/usr/bin/chromium")),
|
|
218
195
|
/** Encodes widget animations to MP4; without it widgets offer no video. */
|
|
219
196
|
ffmpegPath: str("FFMPEG_PATH", "ffmpeg"),
|
|
220
|
-
/**
|
|
221
|
-
|
|
197
|
+
/**
|
|
198
|
+
* The engine that runs an agent's shell and code (sandbox/index.ts): `off`, an engine's name,
|
|
199
|
+
* or empty for the first one that is ready here.
|
|
200
|
+
*/
|
|
201
|
+
sandbox: str("SANDBOX_ENABLED", "1") === "1" ? str("SANDBOX").toLowerCase() : "off",
|
|
222
202
|
sandboxImage,
|
|
203
|
+
/** Where `wizards sandbox add` installs the engines that aren't part of the runtime. */
|
|
204
|
+
sandboxEngines: enginesDir(),
|
|
205
|
+
/** Hosts an agentOS sandbox reaches besides the npm and PyPI registries: `api.example.com`. */
|
|
206
|
+
sandboxHosts: list("SANDBOX_ALLOW_HOSTS"),
|
|
223
207
|
/** Assets and workspace files in an S3-compatible bucket (R2); empty = the data folder. */
|
|
224
208
|
s3: {
|
|
225
209
|
endpoint: str("S3_ENDPOINT").replace(/\/$/, ""),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FORMATS, isLang, LANG_CODE, LANGS, LIST_FORMATS, TEXT_CLASSES, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
2
2
|
import { TableColumnValueError } from "@engenty-wizards/shared/engenty/data-tables";
|
|
3
|
-
import { noteText } from "@engenty-wizards/shared/run";
|
|
3
|
+
import { noteText, } from "@engenty-wizards/shared/run";
|
|
4
4
|
import { machineTranslated, translateDefinition, } from "@engenty-wizards/shared/translation";
|
|
5
5
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
6
6
|
import { and, eq } from "drizzle-orm";
|
|
@@ -220,6 +220,18 @@ async function publishedWizard(token) {
|
|
|
220
220
|
function wantsTranslation(w, def, lang) {
|
|
221
221
|
return w.translate && lang !== wizardLang(def);
|
|
222
222
|
}
|
|
223
|
+
/** What the wizard's pages ask, in order (`PublicWizard.fields`). */
|
|
224
|
+
export function pageFields(def) {
|
|
225
|
+
return def.steps.flatMap((step) => step.type === "page"
|
|
226
|
+
? step.fields.map((f) => ({
|
|
227
|
+
id: f.id,
|
|
228
|
+
label: f.label,
|
|
229
|
+
kind: f.kind,
|
|
230
|
+
...(f.options?.length ? { options: f.options } : {}),
|
|
231
|
+
...(f.required ? { required: true } : {}),
|
|
232
|
+
}))
|
|
233
|
+
: []);
|
|
234
|
+
}
|
|
223
235
|
/** What a wizard's page shows before a run: in the viewer's language where `as` is given. */
|
|
224
236
|
async function publicBody(c, w, original, as) {
|
|
225
237
|
const translated = as ? translateDefinition(original, as.memory) : null;
|
|
@@ -245,6 +257,7 @@ async function publicBody(c, w, original, as) {
|
|
|
245
257
|
machine: machineTranslated(as.memory, original),
|
|
246
258
|
}
|
|
247
259
|
: null,
|
|
260
|
+
fields: pageFields(def),
|
|
248
261
|
};
|
|
249
262
|
}
|
|
250
263
|
export const publicRoutes = new Hono()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* What `wizards sandbox add agentos` installs (install.ts): agentOS's core, about 340 MB, which
|
|
5
|
+
* only the computers and servers that run the sandbox with it need.
|
|
6
|
+
*/
|
|
7
|
+
export const AGENTOS_VERSION = "0.4.1";
|
|
8
|
+
export const AGENTOS_CORE = "@rivet-dev/agentos-core";
|
|
9
|
+
/** The helper agentOS ships for each platform, beside the core: `<package>/agentos-native-sidecar`. */
|
|
10
|
+
const SIDECAR_PACKAGE = "@rivet-dev/agentos-runtime-sidecar-";
|
|
11
|
+
/** Where the core keeps its programs, compiled to WebAssembly. */
|
|
12
|
+
const COMMANDS = join("node_modules", "@rivet-dev", "agentos-runtime-core", "commands");
|
|
13
|
+
/**
|
|
14
|
+
* Programs agentOS ships but leaves off the PATH; the install packs them so every VM has them.
|
|
15
|
+
* The ones from the `@agentos-software` registry are in a format 0.4 no longer reads.
|
|
16
|
+
*/
|
|
17
|
+
export const AGENTOS_EXTRA_COMMANDS = ["curl", "git", "jq", "sqlite3", "unzip", "zip"];
|
|
18
|
+
/** Dependencies of the core that its code never imports: Google and AWS clients, about 140 MB. */
|
|
19
|
+
const UNUSED = ["googleapis", "googleapis-common", "@aws-sdk", "@smithy", "better-sqlite3"];
|
|
20
|
+
/** agentOS ships its helper for macOS and glibc Linux, on arm64 and x64. */
|
|
21
|
+
export function agentOsSupported(platform = process.platform, arch = process.arch) {
|
|
22
|
+
return ["darwin", "linux"].includes(platform) && ["arm64", "x64"].includes(arch);
|
|
23
|
+
}
|
|
24
|
+
/** The package of extra programs every VM gets (AGENTOS_EXTRA_COMMANDS). */
|
|
25
|
+
export const agentOsExtras = (dir) => join(dir, "extras");
|
|
26
|
+
/** The helper with its routine lines filtered out, or null where there is no bash. */
|
|
27
|
+
export const agentOsSidecar = (dir) => join(dir, "sidecar.sh");
|
|
28
|
+
/** An agentOS package is a folder with a manifest and its programs in `bin/`. */
|
|
29
|
+
function packExtras(dir) {
|
|
30
|
+
const bin = join(agentOsExtras(dir), "bin");
|
|
31
|
+
mkdirSync(bin, { recursive: true });
|
|
32
|
+
for (const command of AGENTOS_EXTRA_COMMANDS) {
|
|
33
|
+
const target = join(bin, command);
|
|
34
|
+
copyFileSync(join(dir, COMMANDS, command), target);
|
|
35
|
+
// Without the mode, the VM finds the program and refuses to run it.
|
|
36
|
+
chmodSync(target, 0o755);
|
|
37
|
+
}
|
|
38
|
+
writeFileSync(join(agentOsExtras(dir), "agentos-package.json"), `${JSON.stringify({ name: "engenty-wizards-extras", version: AGENTOS_VERSION })}\n`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The helper prints a line on stderr for every engine it keeps warm, a few per command, and
|
|
42
|
+
* agentOS passes its stderr on to the server's. The wrapper leaves those lines out.
|
|
43
|
+
*/
|
|
44
|
+
function writeSidecarWrapper(dir) {
|
|
45
|
+
const bash = ["/bin/bash", "/usr/bin/bash"].find((path) => existsSync(path));
|
|
46
|
+
const modules = join(dir, "node_modules", "@rivet-dev");
|
|
47
|
+
const platform = readdirSync(modules).find((name) => `@rivet-dev/${name}`.startsWith(SIDECAR_PACKAGE));
|
|
48
|
+
const helper = platform && join(modules, platform, "agentos-native-sidecar");
|
|
49
|
+
if (!bash || !helper || !existsSync(helper)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const quoted = `'${helper.replaceAll("'", `'\\''`)}'`;
|
|
53
|
+
writeFileSync(agentOsSidecar(dir), `#!${bash}\n` +
|
|
54
|
+
"# agentOS's helper without the lines it prints for every engine it keeps warm.\n" +
|
|
55
|
+
`exec ${quoted} "$@" 2> >(grep --line-buffered -v -e '^agentos-v8-runtime: ' -e 'vfs::posix::tar_fs' >&2)\n`);
|
|
56
|
+
chmodSync(agentOsSidecar(dir), 0o755);
|
|
57
|
+
}
|
|
58
|
+
export const AGENTOS_PACKAGE = {
|
|
59
|
+
label: "agentOS",
|
|
60
|
+
version: AGENTOS_VERSION,
|
|
61
|
+
packages: [`${AGENTOS_CORE}@${AGENTOS_VERSION}`],
|
|
62
|
+
size: "about 340 MB",
|
|
63
|
+
unsupported: () => agentOsSupported()
|
|
64
|
+
? null
|
|
65
|
+
: `agentOS runs on macOS and Linux (arm64, x64), not on ${process.platform} ${process.arch}.`,
|
|
66
|
+
prepare(dir) {
|
|
67
|
+
for (const name of UNUSED) {
|
|
68
|
+
rmSync(join(dir, "node_modules", name), { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
packExtras(dir);
|
|
71
|
+
writeSidecarWrapper(dir);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, posix } from "node:path";
|
|
3
|
+
import { env } from "../env.js";
|
|
4
|
+
import { AGENTOS_CORE, AGENTOS_EXTRA_COMMANDS, agentOsExtras, agentOsSidecar, } from "./agentos-package.js";
|
|
5
|
+
import { importFromEngine, installedVersion } from "./install.js";
|
|
6
|
+
const WORKDIR = "/workspace";
|
|
7
|
+
const EXEC_TIMEOUT_MS = 120_000;
|
|
8
|
+
/**
|
|
9
|
+
* Python starts in $HOME, whatever directory the shell is in. Before each command, `python3`
|
|
10
|
+
* becomes a function that hands it the shell's directory as HOME.
|
|
11
|
+
*/
|
|
12
|
+
const PYTHON_FOLLOWS_CD = 'python3() { HOME="$PWD" command python3 "$@"; }; python() { python3 "$@"; }\n';
|
|
13
|
+
/** Where npm and Python packages come from: every VM reaches them. */
|
|
14
|
+
const REGISTRIES = ["registry.npmjs.org", "pypi.org", "files.pythonhosted.org"];
|
|
15
|
+
/**
|
|
16
|
+
* What an agentOS VM runs: a Linux userland compiled to WebAssembly (bash, coreutils and the
|
|
17
|
+
* extra programs the install packs), Node in a V8 isolate and Python 3.13 as Pyodide. No native
|
|
18
|
+
* programs: no ffmpeg, ImageMagick or LibreOffice.
|
|
19
|
+
*/
|
|
20
|
+
export function agentOsCapabilities(hosts = []) {
|
|
21
|
+
return {
|
|
22
|
+
commands: [
|
|
23
|
+
"bash",
|
|
24
|
+
"ls",
|
|
25
|
+
"cat",
|
|
26
|
+
"head",
|
|
27
|
+
"sort",
|
|
28
|
+
"grep",
|
|
29
|
+
"sed",
|
|
30
|
+
"awk",
|
|
31
|
+
"find",
|
|
32
|
+
"diff",
|
|
33
|
+
"tar",
|
|
34
|
+
"gzip",
|
|
35
|
+
...AGENTOS_EXTRA_COMMANDS,
|
|
36
|
+
"node",
|
|
37
|
+
"npm",
|
|
38
|
+
"python3",
|
|
39
|
+
],
|
|
40
|
+
network: hosts.length
|
|
41
|
+
? `only the npm and PyPI registries and ${hosts.join(", ")}`
|
|
42
|
+
: "none, except the npm and PyPI registries",
|
|
43
|
+
notes: "python3 is Pyodide: numpy and pandas load offline; install other pure-Python packages with " +
|
|
44
|
+
`python3 -c "import micropip; await micropip.install('openpyxl')" (no pip command, no ` +
|
|
45
|
+
"pillow, matplotlib or scipy). node runs JavaScript, not TypeScript.",
|
|
46
|
+
uses: "calculations, tables or file conversion",
|
|
47
|
+
workdir: WORKDIR,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
let runtime = null;
|
|
51
|
+
/** One native helper process hosts the VMs of every run; the first sandbox starts it. */
|
|
52
|
+
function agentOsRuntime(dir) {
|
|
53
|
+
runtime ??= (async () => {
|
|
54
|
+
// agentOS reads the helper's path from the environment when it starts the helper.
|
|
55
|
+
if (!process.env.AGENTOS_SIDECAR_BIN && existsSync(agentOsSidecar(dir))) {
|
|
56
|
+
process.env.AGENTOS_SIDECAR_BIN = agentOsSidecar(dir);
|
|
57
|
+
}
|
|
58
|
+
const mod = (await importFromEngine("agentos", dir, AGENTOS_CORE));
|
|
59
|
+
return { agentOs: mod.AgentOs, sidecar: await mod.AgentOs.createSidecar() };
|
|
60
|
+
})().catch((err) => {
|
|
61
|
+
runtime = null;
|
|
62
|
+
throw err;
|
|
63
|
+
});
|
|
64
|
+
return runtime;
|
|
65
|
+
}
|
|
66
|
+
/** Stops the helper process. The next sandbox starts a new one. */
|
|
67
|
+
export async function stopAgentOs() {
|
|
68
|
+
const current = runtime;
|
|
69
|
+
runtime = null;
|
|
70
|
+
await (await current?.catch(() => null))?.sidecar.dispose();
|
|
71
|
+
}
|
|
72
|
+
function toResult(result) {
|
|
73
|
+
const stdout = result.stdout ?? "";
|
|
74
|
+
let stderr = result.stderr ?? "";
|
|
75
|
+
if (result.outcome === "timed_out") {
|
|
76
|
+
stderr = `${stderr}\ntimed out`.trim();
|
|
77
|
+
}
|
|
78
|
+
else if (result.outcome !== "succeeded" && !stdout && !stderr) {
|
|
79
|
+
stderr = result.error?.message ?? result.outcome;
|
|
80
|
+
}
|
|
81
|
+
return { exitCode: result.exitCode ?? (result.outcome === "succeeded" ? 0 : 1), stdout, stderr };
|
|
82
|
+
}
|
|
83
|
+
/** Where `wizards sandbox add agentos` put it. */
|
|
84
|
+
export const agentOsDir = () => join(env.sandboxEngines, "agentos");
|
|
85
|
+
/** Ready once `wizards sandbox add agentos` installed it. */
|
|
86
|
+
export const agentOsEngine = {
|
|
87
|
+
name: "agentos",
|
|
88
|
+
ready: () => installedVersion(agentOsDir()) !== null,
|
|
89
|
+
capabilities: () => agentOsCapabilities(env.sandboxHosts),
|
|
90
|
+
create: () => new AgentOsSandbox(),
|
|
91
|
+
};
|
|
92
|
+
/** One agentOS VM per run, in the server's helper process: no Docker, no container. */
|
|
93
|
+
export class AgentOsSandbox {
|
|
94
|
+
dir;
|
|
95
|
+
hosts;
|
|
96
|
+
capabilities;
|
|
97
|
+
machine = null;
|
|
98
|
+
constructor(dir = agentOsDir(), hosts = env.sandboxHosts) {
|
|
99
|
+
this.dir = dir;
|
|
100
|
+
this.hosts = hosts;
|
|
101
|
+
this.capabilities = agentOsCapabilities(hosts);
|
|
102
|
+
}
|
|
103
|
+
vm() {
|
|
104
|
+
this.machine ??= (async () => {
|
|
105
|
+
const { agentOs, sidecar } = await agentOsRuntime(this.dir);
|
|
106
|
+
const vm = await agentOs.create({
|
|
107
|
+
sidecar: { kind: "explicit", handle: sidecar },
|
|
108
|
+
software: [{ packagePath: agentOsExtras(this.dir) }],
|
|
109
|
+
// Anything not listed is refused, the host's own loopback and private networks included.
|
|
110
|
+
permissions: {
|
|
111
|
+
fs: "allow",
|
|
112
|
+
childProcess: "allow",
|
|
113
|
+
process: "allow",
|
|
114
|
+
env: "allow",
|
|
115
|
+
network: {
|
|
116
|
+
default: "deny",
|
|
117
|
+
rules: [
|
|
118
|
+
{
|
|
119
|
+
mode: "allow",
|
|
120
|
+
operations: ["*"],
|
|
121
|
+
patterns: [...REGISTRIES, ...this.hosts].flatMap((host) => [
|
|
122
|
+
`dns://${host}`,
|
|
123
|
+
`tcp://${host}:443`,
|
|
124
|
+
]),
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
await vm.filesystem.mkdir(WORKDIR, { recursive: true });
|
|
131
|
+
return vm;
|
|
132
|
+
})();
|
|
133
|
+
this.machine.catch(() => {
|
|
134
|
+
this.machine = null;
|
|
135
|
+
});
|
|
136
|
+
return this.machine;
|
|
137
|
+
}
|
|
138
|
+
async exec(command, options = {}) {
|
|
139
|
+
const vm = await this.vm();
|
|
140
|
+
return toResult(await vm.process.exec(PYTHON_FOLLOWS_CD + command, {
|
|
141
|
+
cwd: options.cwd ?? WORKDIR,
|
|
142
|
+
env: { HOME: WORKDIR, ...options.env },
|
|
143
|
+
timeoutMs: options.timeoutMs ?? EXEC_TIMEOUT_MS,
|
|
144
|
+
output: { capture: "all" },
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
async writeFile(path, data) {
|
|
148
|
+
const vm = await this.vm();
|
|
149
|
+
const target = posix.resolve(WORKDIR, path);
|
|
150
|
+
await vm.filesystem.mkdir(posix.dirname(target), { recursive: true });
|
|
151
|
+
await vm.filesystem.writeFile(target, data);
|
|
152
|
+
}
|
|
153
|
+
async readFile(path) {
|
|
154
|
+
const vm = await this.vm();
|
|
155
|
+
return vm.filesystem.readFile(posix.resolve(WORKDIR, path));
|
|
156
|
+
}
|
|
157
|
+
async destroy() {
|
|
158
|
+
const machine = this.machine;
|
|
159
|
+
this.machine = null;
|
|
160
|
+
await (await machine?.catch(() => null))?.dispose();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { DockerSandbox } from "@mastra/docker";
|
|
3
|
+
import { env } from "../env.js";
|
|
4
|
+
import { MastraRunSandbox } from "./mastra.js";
|
|
2
5
|
/** The image's own working directory, the one its unprivileged user may write to. */
|
|
3
6
|
const WORKDIR = "/sandbox";
|
|
4
|
-
/** Bytes per append when a file is written through the shell; stays far below ARG_MAX. */
|
|
5
|
-
const WRITE_CHUNK = 48 * 1024;
|
|
6
7
|
/** What the engenty-sandbox image carries. The container sits on Docker's default bridge. */
|
|
7
8
|
export const DOCKER_CAPABILITIES = {
|
|
8
9
|
commands: ["bash", "python3", "pip", "uv", "node", "npm", "bun", "jq", "curl", "git", "unzip"],
|
|
@@ -10,66 +11,35 @@ export const DOCKER_CAPABILITIES = {
|
|
|
10
11
|
uses: "calculations, charts or file conversion",
|
|
11
12
|
workdir: WORKDIR,
|
|
12
13
|
};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
pidsLimit: 256,
|
|
31
|
-
capDrop: ["ALL"],
|
|
32
|
-
securityOpt: ["no-new-privileges"],
|
|
33
|
-
workingDir: WORKDIR,
|
|
34
|
-
labels: { "engenty-wizards.run": this.runId },
|
|
35
|
-
});
|
|
36
|
-
await sandbox.start();
|
|
37
|
-
return sandbox;
|
|
38
|
-
})();
|
|
39
|
-
return this.container;
|
|
40
|
-
}
|
|
41
|
-
async exec(command, options = {}) {
|
|
42
|
-
const sandbox = await this.start();
|
|
43
|
-
const result = await sandbox.executeCommand(command, [], {
|
|
44
|
-
timeout: options.timeoutMs,
|
|
45
|
-
cwd: options.cwd,
|
|
46
|
-
env: options.env,
|
|
47
|
-
});
|
|
48
|
-
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
49
|
-
}
|
|
50
|
-
async writeFile(path, data) {
|
|
51
|
-
const target = quote(path);
|
|
52
|
-
const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
|
|
53
|
-
await this.must(`mkdir -p "$(dirname ${target})" && : > ${target}`);
|
|
54
|
-
for (let at = 0; at < bytes.length; at += WRITE_CHUNK) {
|
|
55
|
-
const chunk = bytes.subarray(at, at + WRITE_CHUNK).toString("base64");
|
|
56
|
-
await this.must(`printf %s ${chunk} | base64 -d >> ${target}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
async readFile(path) {
|
|
60
|
-
const result = await this.must(`base64 -w0 ${quote(path)}`);
|
|
61
|
-
return Buffer.from(result.stdout.trim(), "base64");
|
|
62
|
-
}
|
|
63
|
-
async must(command) {
|
|
64
|
-
const result = await this.exec(command);
|
|
65
|
-
if (result.exitCode !== 0) {
|
|
66
|
-
throw new Error(result.stderr.trim() || `exit code ${result.exitCode}`);
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
14
|
+
/** One container per run through the host's Docker, with Mastra's Docker sandbox. */
|
|
15
|
+
export function dockerSandbox(runId, image) {
|
|
16
|
+
return new MastraRunSandbox(DOCKER_CAPABILITIES, () => new DockerSandbox({
|
|
17
|
+
id: `wizards-${runId.toLowerCase()}`,
|
|
18
|
+
image,
|
|
19
|
+
memory: 1024 * 1024 * 1024,
|
|
20
|
+
pidsLimit: 256,
|
|
21
|
+
capDrop: ["ALL"],
|
|
22
|
+
securityOpt: ["no-new-privileges"],
|
|
23
|
+
workingDir: WORKDIR,
|
|
24
|
+
labels: { "engenty-wizards.run": runId },
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
export function dockerHasImage(image) {
|
|
28
|
+
try {
|
|
29
|
+
execFileSync("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 5000 });
|
|
30
|
+
return true;
|
|
69
31
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.container = null;
|
|
73
|
-
await (await container?.catch(() => null))?.destroy();
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
74
34
|
}
|
|
75
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Ready where Docker has the sandbox image (`SANDBOX_IMAGE`). The image is not published, so
|
|
38
|
+
* without the check a fresh install would offer a tool that cannot start.
|
|
39
|
+
*/
|
|
40
|
+
export const dockerEngine = {
|
|
41
|
+
name: "docker",
|
|
42
|
+
ready: () => dockerHasImage(env.sandboxImage),
|
|
43
|
+
capabilities: () => DOCKER_CAPABILITIES,
|
|
44
|
+
create: (runId) => dockerSandbox(runId, env.sandboxImage),
|
|
45
|
+
};
|
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
import { env } from "../env.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { agentOsEngine } from "./agentos.js";
|
|
3
|
+
import { DOCKER_CAPABILITIES, dockerEngine } from "./docker.js";
|
|
4
|
+
/**
|
|
5
|
+
* The engines, in the order one is picked when `SANDBOX` names none. Another engine joins
|
|
6
|
+
* here: one of Mastra's sandboxes through MastraRunSandbox (mastra.ts), its package installed
|
|
7
|
+
* with `wizards sandbox add <name>` when it isn't a dependency of the runtime (install.ts).
|
|
8
|
+
*/
|
|
9
|
+
export const ENGINES = [dockerEngine, agentOsEngine];
|
|
10
|
+
/**
|
|
11
|
+
* `off`: none. An engine's name: that one, ready or not; it says what is missing when a run
|
|
12
|
+
* uses it. Empty: the first engine that is ready here.
|
|
13
|
+
*/
|
|
14
|
+
export function pickEngine(engines, requested) {
|
|
15
|
+
if (requested === "off") {
|
|
6
16
|
return null;
|
|
7
17
|
}
|
|
8
|
-
|
|
18
|
+
if (requested) {
|
|
19
|
+
const engine = engines.find((e) => e.name === requested);
|
|
20
|
+
if (!engine) {
|
|
21
|
+
console.warn(`SANDBOX=${requested} is no engine (${engines.map((e) => e.name).join(", ")} or off): the sandbox is off.`);
|
|
22
|
+
}
|
|
23
|
+
return engine ?? null;
|
|
24
|
+
}
|
|
25
|
+
return engines.find((e) => e.ready()) ?? null;
|
|
26
|
+
}
|
|
27
|
+
let picked = null;
|
|
28
|
+
/** The engine of this server, picked once at the first question. */
|
|
29
|
+
export function sandboxEngine() {
|
|
30
|
+
picked ??= { engine: pickEngine(ENGINES, env.sandbox) };
|
|
31
|
+
return picked.engine;
|
|
32
|
+
}
|
|
33
|
+
/** What this server's sandbox runs, or null when it is switched off. */
|
|
34
|
+
export function sandboxCapabilities() {
|
|
35
|
+
return sandboxEngine()?.capabilities() ?? null;
|
|
9
36
|
}
|
|
10
37
|
export function createSandbox(runId) {
|
|
11
|
-
|
|
38
|
+
const engine = sandboxEngine();
|
|
39
|
+
if (!engine) {
|
|
12
40
|
throw new Error("The sandbox is switched off on this server.");
|
|
13
41
|
}
|
|
14
|
-
return
|
|
42
|
+
return engine.create(runId);
|
|
15
43
|
}
|
|
16
44
|
export function shellToolDescription(caps) {
|
|
17
|
-
|
|
45
|
+
const notes = caps.notes ? ` ${caps.notes}` : "";
|
|
46
|
+
return `Run a shell command in a private Linux sandbox (${caps.commands.join(", ")}). Working directory ${caps.workdir}. Network: ${caps.network}.${notes}`;
|
|
18
47
|
}
|
|
19
48
|
/** The authoring guide's line about the sandbox tool. */
|
|
20
49
|
export function sandboxGuideLine() {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { wizardsHome } from "../cli/home.js";
|
|
5
|
+
import { AGENTOS_PACKAGE } from "./agentos-package.js";
|
|
6
|
+
export const ENGINE_PACKAGES = {
|
|
7
|
+
agentos: AGENTOS_PACKAGE,
|
|
8
|
+
};
|
|
9
|
+
/** Written last: a folder without it is an install that did not finish. */
|
|
10
|
+
const MARKER = "engine.json";
|
|
11
|
+
/** Where the engines go: `SANDBOX_ENGINES_DIR`, else `~/.engenty/wizards/engines`. */
|
|
12
|
+
export function enginesDir(env = process.env) {
|
|
13
|
+
return resolve(env.SANDBOX_ENGINES_DIR?.trim() || join(wizardsHome(env), "engines"));
|
|
14
|
+
}
|
|
15
|
+
export const engineDir = (name, env = process.env) => join(enginesDir(env), name);
|
|
16
|
+
/** The version installed in `dir`, or null when nothing usable is there. */
|
|
17
|
+
export function installedVersion(dir) {
|
|
18
|
+
try {
|
|
19
|
+
const { version } = JSON.parse(readFileSync(join(dir, MARKER), "utf8"));
|
|
20
|
+
return typeof version === "string" ? version : null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** The file an export map's entry names for `import`: conditions may nest (`import.default`). */
|
|
27
|
+
function importTarget(target) {
|
|
28
|
+
if (typeof target === "string") {
|
|
29
|
+
return target;
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(target)) {
|
|
32
|
+
return target.map(importTarget).find(Boolean);
|
|
33
|
+
}
|
|
34
|
+
if (target && typeof target === "object") {
|
|
35
|
+
for (const condition of ["import", "node", "default"]) {
|
|
36
|
+
const found = importTarget(target[condition]);
|
|
37
|
+
if (found) {
|
|
38
|
+
return found;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/** A package of an engine's folder, for `import()`: its export map's `.`, else its main. */
|
|
45
|
+
export function packageEntry(dir, name) {
|
|
46
|
+
const root = join(dir, "node_modules", name);
|
|
47
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
48
|
+
const { exports } = pkg;
|
|
49
|
+
const dot = exports && typeof exports === "object" && "." in exports ? exports["."] : exports;
|
|
50
|
+
return join(root, importTarget(dot) ?? pkg.main ?? "index.js");
|
|
51
|
+
}
|
|
52
|
+
/** Loads a package an engine installed; says how to install it when it isn't there. */
|
|
53
|
+
export async function importFromEngine(engine, dir, name) {
|
|
54
|
+
if (!installedVersion(dir)) {
|
|
55
|
+
throw new Error(`${engine} isn’t installed in ${dir}: wizards sandbox add ${engine}`);
|
|
56
|
+
}
|
|
57
|
+
return import(pathToFileURL(packageEntry(dir, name)).href);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Installs an engine into `dir` with the given npm, replacing what was there. Install scripts do
|
|
61
|
+
* not run; an engine that needs one does its work in `prepare`.
|
|
62
|
+
*/
|
|
63
|
+
export async function installEngine(name, dir, npm) {
|
|
64
|
+
const engine = ENGINE_PACKAGES[name];
|
|
65
|
+
if (!engine) {
|
|
66
|
+
throw new Error(`There is no engine “${name}” to install. ${engineList()}`);
|
|
67
|
+
}
|
|
68
|
+
const unsupported = engine.unsupported?.();
|
|
69
|
+
if (unsupported) {
|
|
70
|
+
throw new Error(unsupported);
|
|
71
|
+
}
|
|
72
|
+
rmSync(dir, { recursive: true, force: true });
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
writeFileSync(join(dir, "package.json"), '{ "private": true }\n');
|
|
75
|
+
const result = await npm([
|
|
76
|
+
"install",
|
|
77
|
+
"--prefix",
|
|
78
|
+
dir,
|
|
79
|
+
"--no-audit",
|
|
80
|
+
"--no-fund",
|
|
81
|
+
"--ignore-scripts",
|
|
82
|
+
"--loglevel=error",
|
|
83
|
+
...engine.packages,
|
|
84
|
+
]);
|
|
85
|
+
if (result.code !== 0) {
|
|
86
|
+
throw new Error(`npm couldn’t install ${name}: ${result.tail.trim().split("\n").at(-1)}`);
|
|
87
|
+
}
|
|
88
|
+
engine.prepare?.(dir);
|
|
89
|
+
writeFileSync(join(dir, MARKER), `${JSON.stringify({ name, version: engine.version }, null, 2)}\n`);
|
|
90
|
+
return engine.version;
|
|
91
|
+
}
|
|
92
|
+
export function removeEngine(dir) {
|
|
93
|
+
rmSync(dir, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
export const engineList = () => `The engines to install: ${Object.keys(ENGINE_PACKAGES).join(", ")}.`;
|
|
96
|
+
/** `node apps/runtime/dist/sandbox/install.js agentos …`: an image or a checkout installs so. */
|
|
97
|
+
if (import.meta.main) {
|
|
98
|
+
const { spawn } = await import("node:child_process");
|
|
99
|
+
const npm = (args) => new Promise((resolve) => {
|
|
100
|
+
const child = spawn("npm", args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
101
|
+
child.on("error", (err) => resolve({ code: 127, tail: err.message }));
|
|
102
|
+
child.on("close", (code) => resolve({ code: code ?? 1, tail: "" }));
|
|
103
|
+
});
|
|
104
|
+
const names = process.argv
|
|
105
|
+
.slice(2)
|
|
106
|
+
.flatMap((arg) => arg.split(","))
|
|
107
|
+
.filter(Boolean);
|
|
108
|
+
if (!names.length) {
|
|
109
|
+
console.error(`Name the engines to install. ${engineList()}`);
|
|
110
|
+
process.exitCode = 2;
|
|
111
|
+
}
|
|
112
|
+
for (const name of names) {
|
|
113
|
+
try {
|
|
114
|
+
const version = await installEngine(name, engineDir(name), npm);
|
|
115
|
+
console.log(`${name} ${version} is installed in ${engineDir(name)}.`);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.error(err.message);
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|