@engenty/wizards-runtime 0.7.5 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/user/command-line.md +3 -0
- package/apps/runtime/dist/docs/user/server.md +1 -1
- package/apps/runtime/dist/env.js +10 -26
- 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/web/dist/assets/{AboutDialog-D4mvc-zu.js → AboutDialog-DYgXPagX.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-DLqpJsF6.js → ChangelogDialog-BXqBW8j3.js} +1 -1
- package/apps/web/dist/assets/{ChatView-3laypRcE.js → ChatView-CK2--tsE.js} +1 -1
- package/apps/web/dist/assets/{EditorPage-BalA5U2Y.js → EditorPage-jvcwyXbU.js} +1 -1
- package/apps/web/dist/assets/{Harness-iYLtOLaK.js → Harness-CV9uYzbv.js} +1 -1
- package/apps/web/dist/assets/{NewWizardChannels-CbwJ9A56.js → NewWizardChannels-BakW5q6s.js} +1 -1
- package/apps/web/dist/assets/{NewWizardPage-BOBYiB9i.js → NewWizardPage-BoA-LJuW.js} +1 -1
- package/apps/web/dist/assets/{OnboardingPage-CUwCiVIr.js → OnboardingPage-DaD1XnOe.js} +1 -1
- package/apps/web/dist/assets/{ProjectPage-Bb16MdYG.js → ProjectPage-BFgk1DEv.js} +1 -1
- package/apps/web/dist/assets/{PublicRunner-BCt7Q6C-.js → PublicRunner-C9MwEvxy.js} +1 -1
- package/apps/web/dist/assets/{ReadOnly-kIHX2TEy.js → ReadOnly-DC2Kp7l8.js} +1 -1
- package/apps/web/dist/assets/{RunDrawer-hZ9oeSXu.js → RunDrawer-CY6NZSrI.js} +1 -1
- package/apps/web/dist/assets/{RunnerView-DUS9fPmx.js → RunnerView-BT26XgcY.js} +1 -1
- package/apps/web/dist/assets/{SettingsPage-BzwR50fi.js → SettingsPage-AFt9IDLt.js} +1 -1
- package/apps/web/dist/assets/{SetupPage-gAp7rPxp.js → SetupPage-DtFYAbwJ.js} +1 -1
- package/apps/web/dist/assets/{SharePage-DqZWHva_.js → SharePage-t2xwNS61.js} +1 -1
- package/apps/web/dist/assets/changelog-CATQFxCQ.js +1 -0
- package/apps/web/dist/assets/{index-C5Sm7tTR.js → index-Drw7FMTQ.js} +3 -3
- package/apps/web/dist/index.html +1 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/changelog-DugmCNSf.js +0 -1
|
@@ -8,6 +8,7 @@ import { CLIENTS, nodeIsCurrent } from "./machine.js";
|
|
|
8
8
|
import { mcp } from "./mcp.js";
|
|
9
9
|
import { menu } from "./menu.js";
|
|
10
10
|
import { logsCommand, reportCommand } from "./report.js";
|
|
11
|
+
import { sandboxCommand } from "./sandbox.js";
|
|
11
12
|
import { setup } from "./setup.js";
|
|
12
13
|
import { open, start, studio } from "./start.js";
|
|
13
14
|
import { autostart, restart, status, stop } from "./status.js";
|
|
@@ -40,6 +41,8 @@ ${line(`${command()} restart`, "restart it, e.g. to switch to a newly installed
|
|
|
40
41
|
${line(`${command()} update`, "install the latest version")}
|
|
41
42
|
${line(`${command()} autoupdate`, "updates by themselves: add on or off (WIZARDS_DISABLE_AUTOUPDATE=1 is off too)")}
|
|
42
43
|
${more("(downloaded in the background, used from the next start; never during a run)")}
|
|
44
|
+
${line(`${command()} sandbox`, "the sandbox where AI steps run code, without Docker: add or remove")}
|
|
45
|
+
${more("(agentOS unless you name another engine, about 340 MB; used from the next start)")}
|
|
43
46
|
${line(`${command()} autostart`, "start it when you log in to this computer: add on or off")}
|
|
44
47
|
${more("(macOS: a LaunchAgent; Linux: a systemd user service)")}
|
|
45
48
|
${line(`${command()} connect`, "add your wizards to your AI apps: every app found, or the ones you name:")}
|
|
@@ -70,6 +73,7 @@ function parse(argv) {
|
|
|
70
73
|
const args = {
|
|
71
74
|
command: "",
|
|
72
75
|
value: "",
|
|
76
|
+
engine: "",
|
|
73
77
|
apps: [],
|
|
74
78
|
paths: [],
|
|
75
79
|
json: false,
|
|
@@ -116,10 +120,13 @@ function parse(argv) {
|
|
|
116
120
|
else if (arg.startsWith("-")) {
|
|
117
121
|
return `Unknown option ${arg}.`;
|
|
118
122
|
}
|
|
119
|
-
else if (["autostart", "autoupdate", "logs", "account"].includes(args.command) &&
|
|
123
|
+
else if (["autostart", "autoupdate", "logs", "account", "sandbox"].includes(args.command) &&
|
|
120
124
|
!args.value) {
|
|
121
125
|
args.value = arg;
|
|
122
126
|
}
|
|
127
|
+
else if (args.command === "sandbox" && !args.engine) {
|
|
128
|
+
args.engine = arg;
|
|
129
|
+
}
|
|
123
130
|
else if (["connect", "disconnect", "features"].includes(args.command)) {
|
|
124
131
|
args.apps.push(arg);
|
|
125
132
|
}
|
|
@@ -199,6 +206,8 @@ async function main(argv) {
|
|
|
199
206
|
return autoUpdateCommand(paths, args.value);
|
|
200
207
|
case "account":
|
|
201
208
|
return accountCommand(paths, args.value, { yes: args.yes });
|
|
209
|
+
case "sandbox":
|
|
210
|
+
return sandboxCommand(paths, args.value, args.engine);
|
|
202
211
|
case "connect":
|
|
203
212
|
case "disconnect":
|
|
204
213
|
return connectCommand(paths, args.apps, args.command === "disconnect");
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { delimiter, dirname, join } from "node:path";
|
|
2
|
+
import { ENGINE_PACKAGES, engineList, installEngine, installedVersion, removeEngine, } from "../sandbox/install.js";
|
|
3
|
+
import { runtimePath } from "./environment.js";
|
|
4
|
+
import { runLogged } from "./exec.js";
|
|
5
|
+
import { findOnPath } from "./machine.js";
|
|
6
|
+
import { busy, cmd, no, ok, tilde } from "./ui.js";
|
|
7
|
+
/** What `sandbox add` installs when no engine is named. */
|
|
8
|
+
const DEFAULT_ENGINE = "agentos";
|
|
9
|
+
/** Where an engine goes: `SANDBOX_ENGINES_DIR/<name>`, else `engines/<name>` in the install's folder. */
|
|
10
|
+
export const engineDirOf = (paths, name) => join(process.env.SANDBOX_ENGINES_DIR?.trim() || join(paths.home, "engines"), name);
|
|
11
|
+
/** The engines installed here, with their versions and the versions this release installs. */
|
|
12
|
+
export function installedEngines(paths) {
|
|
13
|
+
return Object.entries(ENGINE_PACKAGES).flatMap(([name, engine]) => {
|
|
14
|
+
const version = installedVersion(engineDirOf(paths, name));
|
|
15
|
+
return version ? [{ name, label: engine.label, version, current: engine.version }] : [];
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `wizards sandbox [add|remove] [engine]`: the sandbox where AI steps run code, without Docker.
|
|
20
|
+
* The engines are not part of engenty wizards; `add` downloads one beside the install.
|
|
21
|
+
*/
|
|
22
|
+
export async function sandboxCommand(paths, value, engine = "") {
|
|
23
|
+
const name = engine.toLowerCase() || DEFAULT_ENGINE;
|
|
24
|
+
if ((value === "add" || value === "remove") && !ENGINE_PACKAGES[name]) {
|
|
25
|
+
console.error(`${no} There is no engine “${name}”. ${engineList()}`);
|
|
26
|
+
return 2;
|
|
27
|
+
}
|
|
28
|
+
if (value === "add") {
|
|
29
|
+
return add(paths, name);
|
|
30
|
+
}
|
|
31
|
+
if (value === "remove") {
|
|
32
|
+
removeEngine(engineDirOf(paths, name));
|
|
33
|
+
console.log(`${ok} The sandbox ${ENGINE_PACKAGES[name].label} is removed. It goes away with the next ${cmd("restart")}.`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
if (value) {
|
|
37
|
+
console.error(`Use ${cmd("sandbox add")} or ${cmd("sandbox remove")}.`);
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
const installed = installedEngines(paths);
|
|
41
|
+
if (!installed.length) {
|
|
42
|
+
console.log(`${no} No sandbox is installed. AI steps can’t run code unless Docker has the engenty-sandbox image. To add one: ${cmd("sandbox add")}`);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
for (const { name, label, version } of installed) {
|
|
46
|
+
console.log(`${ok} The sandbox ${label} ${version} is in ${tilde(engineDirOf(paths, name))}.`);
|
|
47
|
+
}
|
|
48
|
+
console.log(`AI steps run code in ${installed.length === 1 ? "it" : "the first one"}, unless Docker has the engenty-sandbox image or SANDBOX names an engine.`);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
async function add(paths, name) {
|
|
52
|
+
const engine = ENGINE_PACKAGES[name];
|
|
53
|
+
const unsupported = engine.unsupported?.();
|
|
54
|
+
if (unsupported) {
|
|
55
|
+
console.error(`${no} ${unsupported}`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
const npm = findOnPath("npm", [dirname(process.execPath), process.env.PATH ?? ""].join(delimiter));
|
|
59
|
+
if (!npm) {
|
|
60
|
+
console.error(`${no} npm wasn’t found next to this Node.js.`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
const env = { ...process.env, PATH: runtimePath(paths, process.env) };
|
|
64
|
+
const logFile = join(paths.logs, "install.log");
|
|
65
|
+
const dir = engineDirOf(paths, name);
|
|
66
|
+
const spinner = busy(`Installing the sandbox (${engine.label} ${engine.version}, ${engine.size})…`);
|
|
67
|
+
try {
|
|
68
|
+
await installEngine(name, dir, (args) => runLogged(npm, args, { env, logFile }));
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
spinner.fail(`The sandbox couldn’t be installed: ${err.message}`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
spinner.stop(`${ok} The sandbox is installed in ${tilde(dir)}.`);
|
|
75
|
+
console.log(`AI steps run code in it from the next start: ${cmd("restart")}`);
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
@@ -6,6 +6,7 @@ import { autoUpdatePolicy, updateNotice } from "./autoupdate.js";
|
|
|
6
6
|
import { installedByScript, isCheckout, packageRoot, packageVersion } from "./home.js";
|
|
7
7
|
import { detectClients, findChrome, findFfmpeg, nodeIsCurrent } from "./machine.js";
|
|
8
8
|
import { findWizardsProcesses, stopProcesses } from "./processes.js";
|
|
9
|
+
import { installedEngines } from "./sandbox.js";
|
|
9
10
|
import { entryUrl, runningRuntime, settings, startInBackground } from "./start.js";
|
|
10
11
|
import { bad, badge, cmd, cyan, dim, no, ok, tilde } from "./ui.js";
|
|
11
12
|
/**
|
|
@@ -45,6 +46,14 @@ export async function status(paths, checks) {
|
|
|
45
46
|
}
|
|
46
47
|
row("Browser", chrome ? `${ok} ${dim(tilde(chrome))}` : `${no} not found (needed for PDF and PNG)`);
|
|
47
48
|
row("ffmpeg", ffmpeg.path ? `${ok} ${ffmpeg.version ?? ""}` : `${no} not found (needed for MP4)`);
|
|
49
|
+
const engines = installedEngines(paths);
|
|
50
|
+
row("Sandbox", engines.length
|
|
51
|
+
? engines
|
|
52
|
+
.map(({ label, version, current }) => version === current
|
|
53
|
+
? `${ok} ${label} ${version}`
|
|
54
|
+
: `${no} ${label} ${version} is outdated (${cmd("sandbox add")} installs ${current})`)
|
|
55
|
+
.join(", ")
|
|
56
|
+
: `${no} not installed (${cmd("sandbox add")} lets AI steps run code)`);
|
|
48
57
|
const { updated, pending } = updateNotice(paths, packageVersion());
|
|
49
58
|
if (updated) {
|
|
50
59
|
console.log(`\n ${ok} Updated to ${updated.to}. What changed: ${cyan(updated.notes)}`);
|
|
@@ -8,6 +8,7 @@ import { logState, startedAt, tail } from "./log.js";
|
|
|
8
8
|
import { managed } from "./manage.js";
|
|
9
9
|
import { modelSettings } from "./models.js";
|
|
10
10
|
import { loadedPlugins } from "./plugins/registry.js";
|
|
11
|
+
import { sandboxEngine } from "./sandbox/index.js";
|
|
11
12
|
import { runIndexOf, tenantOfRun } from "./tenants/control.js";
|
|
12
13
|
import { LOCAL_TENANT } from "./tenants/tenant.js";
|
|
13
14
|
/**
|
|
@@ -41,7 +42,7 @@ export async function status() {
|
|
|
41
42
|
appUrl: env.appUrl,
|
|
42
43
|
manageUrl: env.manage.url || null,
|
|
43
44
|
gatewayUrl: (managed ? env.manage.gatewayUrl : env.local.gatewayUrl) || null,
|
|
44
|
-
sandbox:
|
|
45
|
+
sandbox: sandboxEngine()?.name ?? "off",
|
|
45
46
|
browser: env.browserCdpUrl ? "cdp" : "local",
|
|
46
47
|
files: env.s3.bucket ? "s3" : "data folder",
|
|
47
48
|
database: env.databaseUrl ? "libsql" : "file",
|
|
@@ -21,6 +21,7 @@ version: 0.5.0
|
|
|
21
21
|
| `wizards stop` | Stops it, with any other copy of it still running in the background. Your data is kept. AI apps that use your wizards start it again when you use one there |
|
|
22
22
|
| `wizards restart` | Stops it and starts it again; after an update, that is the new version. If it isn’t running, it starts in the background |
|
|
23
23
|
| `wizards update` | Installs the latest version |
|
|
24
|
+
| `wizards sandbox add` / `remove` | Adds the sandbox where AI steps run code, or removes it. It is agentOS, about 340 MB, and needs no Docker. Without a sandbox, steps can’t run code. `wizards sandbox` alone shows whether it is there. Name an engine to add another one: `wizards sandbox add agentos` |
|
|
24
25
|
| `wizards autostart on` / `off` | Starts it when you log in, or stops doing so. It is a LaunchAgent on macOS and a systemd user service on Linux |
|
|
25
26
|
| `wizards connect [app …]` | Adds your wizards to your [AI apps](./ai-apps.md): every app found, or the ones you name |
|
|
26
27
|
| `wizards disconnect [app …]` | Takes them out again |
|
|
@@ -51,6 +52,7 @@ running, `wizards start` opens the running one instead of starting a second.
|
|
|
51
52
|
| `~/.engenty/wizards/.env` | The settings file |
|
|
52
53
|
| `~/.engenty/wizards/plugins` | [Plugins](./plugins.md) on this computer |
|
|
53
54
|
| `~/.engenty/wizards/logs` | Logs: `install.log`, `update.log`, `runtime.log` when it runs in the background, `autostart.log` when a Mac starts it at login |
|
|
55
|
+
| `~/.engenty/wizards/engines` | The sandbox, once `wizards sandbox add` put it there: one folder per engine |
|
|
54
56
|
| `~/.engenty/wizards/tools`, `runtime` | Its own Node.js, and engenty wizards itself |
|
|
55
57
|
| `~/.engenty/wizards/bin/wizards` | The command; `~/.local/bin/wizards` links to it |
|
|
56
58
|
|
|
@@ -65,6 +67,7 @@ the studio doesn’t: one `NAME=value` per line. Restart engenty wizards after a
|
|
|
65
67
|
| `API_PORT` | The port | `24368`; if it is taken, the next free one up to `24387` |
|
|
66
68
|
| `CHROME_PATH` | The browser for PDF and PNG exports and steps on websites: Chrome, Edge, Chromium or Brave | The first one found: on a Mac Google Chrome, Microsoft Edge, Chromium or Brave in `/Applications` or `~/Applications`; on Linux `google-chrome`, `chromium`, `chromium-browser` or `microsoft-edge` on the PATH. `wizards status` names it |
|
|
67
69
|
| `FFMPEG_PATH` | ffmpeg for MP4 videos | `ffmpeg` |
|
|
70
|
+
| `SANDBOX_ALLOW_HOSTS` | Addresses the sandbox reaches besides the npm and PyPI registries, comma-separated: `api.example.com`. It reaches nothing else | not set |
|
|
68
71
|
| `OLLAMA_URL` | Where Ollama answers | `http://127.0.0.1:11434/v1` |
|
|
69
72
|
| `PLUGINS_DIR` | Folders with plugins, comma-separated. If you set it, it replaces the default | `~/.engenty/wizards/plugins` |
|
|
70
73
|
| `PLUGINS_WATCH` | With `0`, plugins no longer load again when their files change | `1` |
|
|
@@ -35,7 +35,7 @@ The logs show a link (“Open the studio: …”). Open it once: it signs your b
|
|
|
35
35
|
| Data | Databases and files are on the `/data` volume. Back that volume up |
|
|
36
36
|
| Models | Your own API keys or Ollama. The subscriptions of AI clients live on your own computer, not on a server |
|
|
37
37
|
| PDF, PNG, browser steps, MP4 | Chromium and ffmpeg are in the image |
|
|
38
|
-
| Code & shell | The sandbox is off in this image, so steps can’t use that tool. `compose.yaml` shows how to switch it on |
|
|
38
|
+
| Code & shell | The sandbox is off in this image, so steps can’t use that tool. `compose.yaml` shows how to switch it on: with Docker, or with agentOS built into the image |
|
|
39
39
|
| Plugins | Put them in a folder that you mount into the container and name in `PLUGINS_DIR` |
|
|
40
40
|
|
|
41
41
|
## Update
|
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(/\/$/, ""),
|
|
@@ -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
|
+
};
|