Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions packages/core/src/agent-os.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1172,7 +1172,16 @@ const KERNEL_POSIX_BOOTSTRAP_DIRS = [
"/etc/agentos",
] as const;

const NODE_RUNTIME_BOOTSTRAP_COMMANDS = ["node", "npm", "npx"] as const;
// Runtime commands that get a `/bin/<cmd>` stub at bootstrap so the guest shell
// resolves them on PATH (e.g. `sh -c "python ..."`, pipelines). The sidecar
// intercepts these by name and routes them to the embedded V8 / Pyodide runtime.
const RUNTIME_BOOTSTRAP_COMMANDS = [
"node",
"npm",
"npx",
"python",
"python3",
] as const;
const KERNEL_COMMAND_STUB = "#!/bin/sh\n# kernel command stub\n";
const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
const SIDECAR_BINARY = join(REPO_ROOT, "target/debug/agentos-sidecar");
Expand Down Expand Up @@ -2806,7 +2815,7 @@ export class AgentOs {
options?.rootFilesystem,
[
...collectBootstrapWasmCommands(preparedCommandDirs.commandDirs),
...NODE_RUNTIME_BOOTSTRAP_COMMANDS,
...RUNTIME_BOOTSTRAP_COMMANDS,
...toolBootstrapCommands,
],
);
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/runtime-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ const S_IFDIR = 0o040000;
const S_IFLNK = 0o120000;
const MAX_SYMLINK_DEPTH = 40;
const KERNEL_COMMAND_STUB = "#!/bin/sh\n# kernel command stub\n";
const NODE_RUNTIME_BOOTSTRAP_COMMANDS = ["node", "npm", "npx"] as const;
const NODE_RUNTIME_BOOTSTRAP_COMMANDS = [
"node",
"npm",
"npx",
"python",
"python3",
] as const;
const KERNEL_POSIX_BOOTSTRAP_DIRS = [
"/dev",
"/proc",
Expand Down
18 changes: 16 additions & 2 deletions packages/core/src/sidecar/rpc-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,12 @@ function canUseDirectExec(
driver: string | undefined,
commandName: string | undefined,
): boolean {
return driver === "wasmvm" || (driver === "node" && commandName === "node");
return (
driver === "wasmvm" ||
(driver === "node" && commandName === "node") ||
(driver === "python" &&
(commandName === "python" || commandName === "python3"))
);
}

function shellSingleQuote(value: string): string {
Expand Down Expand Up @@ -777,7 +782,12 @@ export class NativeSidecarKernelProxy {
processId,
command: spawnCommand,
args: spawnArgs,
driver: spawnCommand === "node" ? "node" : "wasmvm",
driver:
spawnCommand === "node"
? "node"
: spawnCommand === "python" || spawnCommand === "python3"
? "python"
: "wasmvm",
cwd: options?.cwd ?? this.cwd,
env: {
...(options?.env ?? {}),
Expand Down Expand Up @@ -2427,6 +2437,10 @@ function buildCommandMap(
["node", "node"],
["npm", "node"],
["npx", "node"],
// `python` / `python3` are served by the embedded Pyodide runtime,
// mirroring how `node` is served by the embedded V8 runtime.
["python", "python"],
["python3", "python"],
]);
for (const name of commandGuestPaths.keys()) {
commands.set(name, "wasmvm");
Expand Down
3 changes: 2 additions & 1 deletion packages/core/tests/agentos-base-filesystem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ describe("AgentOs base filesystem", () => {
expect(await vm.exists("/boot")).toBe(true);
expect(await vm.exists("/usr/bin/env")).toBe(true);
expect(await vm.exists("/bin/node")).toBe(true);
expect(await vm.exists("/bin/python")).toBe(false);
expect(await vm.exists("/bin/python")).toBe(true);
expect(await vm.exists("/bin/python3")).toBe(true);
await expect(vm.writeFile("/tmp/blocked.txt", "blocked")).rejects.toThrow(
"EROFS",
);
Expand Down
135 changes: 135 additions & 0 deletions packages/core/tests/python-cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { AgentOs } from "../src/index.js";
import { hasRegistryCommands, REGISTRY_SOFTWARE } from "./helpers/registry-commands.js";

// End-to-end coverage for the Pyodide-powered `python` / `python3` CLI exposed
// by secure-exec, driven through the public AgentOs API. `python` resolves as a
// runtime command (like `node`): `vm.exec()` / `vm.execArgv()` / `spawn()` route
// it directly to the embedded Pyodide runtime, and a `/bin/python` stub also
// makes it resolvable on the guest shell `PATH`. The Python runtime bridges the
// whole VM filesystem into Pyodide, so scripts and file I/O work anywhere on the
// VM (these tests use `/tmp`).
describe("python CLI (Pyodide runtime)", () => {
let vm: AgentOs;

beforeAll(async () => {
vm = await AgentOs.create();
}, 120_000);

afterAll(async () => {
await vm?.dispose();
});

test(
"python -c runs inline code",
async () => {
const result = await vm.execArgv("python", ["-c", "print(1 + 1)"]);
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe("2");
},
120_000,
);

test(
"python via vm.exec (agent command path) runs inline code",
async () => {
const result = await vm.exec('python -c "print(2 + 3)"');
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe("5");
},
120_000,
);

test(
"python runs a script file anywhere on the VM filesystem with sys.argv",
async () => {
// /tmp is bridged into Python via the whole-root mount (not just /workspace).
await vm.writeFile(
"/tmp/argv.py",
"import sys\nprint(','.join(sys.argv))\n",
);
const result = await vm.execArgv("python", [
"/tmp/argv.py",
"alpha",
"beta",
]);
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe("/tmp/argv.py,alpha,beta");
},
120_000,
);

test(
"python writes to the VM filesystem, visible to the host (cross-runtime)",
async () => {
const write = await vm.execArgv("python", [
"-c",
"open('/tmp/from-python.txt','w').write('written-by-python')",
]);
expect(write.exitCode, write.stderr).toBe(0);
// The write landed in the kernel VFS, so the host sees it too.
const contents = await vm.readFile("/tmp/from-python.txt");
expect(Buffer.from(contents).toString("utf8")).toBe("written-by-python");
},
120_000,
);

test(
"python -m runs a module",
async () => {
const result = await vm.execArgv("python", ["-m", "this"]);
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout).toContain("Beautiful is better than ugly");
},
120_000,
);

test(
"python3 alias runs inline code",
async () => {
const result = await vm.execArgv("python3", ["-c", "print(6 * 7)"]);
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe("42");
},
120_000,
);

test(
"python - reads the program from stdin",
async () => {
const chunks: string[] = [];
const { pid } = vm.spawn("python", ["-"], {
onStdout: (data) => chunks.push(Buffer.from(data).toString("utf8")),
});
vm.writeProcessStdin(pid, "print('from stdin program')\n");
vm.closeProcessStdin(pid);
const exitCode = await vm.waitProcess(pid);
// Native-sidecar process_output can lag the exit notification by a turn.
await new Promise((resolve) => setTimeout(resolve, 0));
expect(exitCode).toBe(0);
expect(chunks.join("")).toContain("from stdin program");
},
120_000,
);

// The guest-shell path needs the WASM `sh` from the registry. `python` resolves
// on the shell PATH via its `/bin/python` stub, so `sh -c`/pipelines work.
test.runIf(hasRegistryCommands)(
"python runs through the guest shell and pipelines",
async () => {
const shellVm = await AgentOs.create({ software: REGISTRY_SOFTWARE });
try {
const direct = await shellVm.exec('sh -c "python -c \'print(2 + 3)\'"');
expect(direct.exitCode, direct.stderr).toBe(0);
expect(direct.stdout.trim()).toBe("5");

const piped = await shellVm.exec('echo "print(6 * 7)" | python -');
expect(piped.exitCode, piped.stderr).toBe(0);
expect(piped.stdout.trim()).toBe("42");
} finally {
await shellVm.dispose();
}
},
120_000,
);
});
12 changes: 6 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ catalog:
'@agentos-software/unzip': 0.3.0-rc.2
'@agentos-software/yq': 0.3.0-rc.2
'@agentos-software/zip': 0.3.0-rc.2
'@secure-exec/core': 0.3.2
'@secure-exec/google-drive': 0.3.2
'@secure-exec/core': 0.0.0-python-cli.48bf1fc
'@secure-exec/google-drive': 0.0.0-python-cli.48bf1fc
'@secure-exec/nodejs': 0.2.1
'@secure-exec/s3': 0.3.2
'@secure-exec/sandbox': 0.3.2
'@secure-exec/s3': 0.0.0-python-cli.48bf1fc
'@secure-exec/sandbox': 0.0.0-python-cli.48bf1fc
# <<< secure-exec catalog <<<
3 changes: 2 additions & 1 deletion website/docs.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export const siteConfig = {
{ slug: "docs/networking", label: "Networking & Previews", attrs: { "data-icon": "globe" } },
{ slug: "docs/cron", label: "Cron Jobs", attrs: { "data-icon": "clock" } },
{ slug: "docs/sandbox", label: "Sandbox Mounting", badge: { text: "Beta", variant: "caution" }, attrs: { "data-icon": "hardDrive" } },
{ slug: "docs/js-runtime", label: "JavaScript Runtime", attrs: { "data-icon": "nodejs" } },
{ slug: "docs/nodejs-runtime", label: "Node.js Runtime", attrs: { "data-icon": "nodejs" } },
{ slug: "docs/python-runtime", label: "Python Runtime", attrs: { "data-icon": "python" } },
{ slug: "docs/permissions", attrs: { "data-icon": "key" } },
{ slug: "docs/resource-limits", label: "Resource Limits", attrs: { "data-icon": "gauge" } },
],
Expand Down
2 changes: 1 addition & 1 deletion website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@astrojs/sitemap": "^3.2.0",
"@astrojs/starlight": "^0.36.0",
"@astrojs/tailwind": "^6.0.0",
"@rivet-dev/docs-theme": "github:rivet-dev/docs-theme#v0.2.2",
"@rivet-dev/docs-theme": "github:rivet-dev/docs-theme#v0.2.3",
"astro": "^5.18.2",
"framer-motion": "^12.0.0",
"lucide-react": "^0.469.0",
Expand Down
2 changes: 1 addition & 1 deletion website/src/content/docs/docs/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ The kernel is the single chokepoint. Each kind of guest operation is serviced by

The executor is the untrusted half of the VM. It runs the guest code and reaches the kernel for everything else.

- **JavaScript Acceleration.** Guest JavaScript runs on a native V8 runtime (the same engine in Chrome and Node.js, with the full JIT compiler) inside an isolate. This is what we call **JavaScript Acceleration**: the guest's JavaScript executes at native speed, not through an interpreter or a translation shim. It is genuinely fast, and it presents normal Node.js semantics. See [JavaScript Runtime](/docs/js-runtime).
- **JavaScript Acceleration.** Guest JavaScript runs on a native V8 runtime (the same engine in Chrome and Node.js, with the full JIT compiler) inside an isolate. This is what we call **JavaScript Acceleration**: the guest's JavaScript executes at native speed, not through an interpreter or a translation shim. It is genuinely fast, and it presents normal Node.js semantics. See [JavaScript Runtime](/docs/nodejs-runtime).
- **WASM alongside it.** The shell (`sh`) and the coreutils behind process execution ship as WebAssembly modules, and you can run your own WASM too. See [POSIX Syscalls](/docs/architecture/posix-syscalls) and the [Compiler Toolchain](/docs/architecture/compiler-toolchain).
- **Native binaries.** Tools mounted into the VM run inside the same boundary as everything else.
- **No host fallthrough.** The executor holds no capability of its own. For every file read, process spawn, or socket open, it issues a syscall and blocks for the kernel's reply.
Expand Down
41 changes: 0 additions & 41 deletions website/src/content/docs/docs/js-runtime.mdx

This file was deleted.

52 changes: 52 additions & 0 deletions website/src/content/docs/docs/nodejs-runtime.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: "Node.js Runtime"
description: "Run Node.js in agentOS: native V8 acceleration, the node CLI, installing packages, and Node.js compatibility."
skill: true
---

agentOS runs **Node.js** (`process.version` `v22.0.0`), fully isolated from the host. `node`, `npm`, and `npx` are on the `PATH`.

## JavaScript Acceleration

Normally, JavaScript running inside WebAssembly is exceptionally slow. In agentOS, JavaScript runs inside a native V8 isolate (powered by [Secure Exec](https://secureexec.dev)) for native runtime speeds:

- **Native V8 speed, no overhead** — guest JS runs on V8's full JIT, not a WASM translation layer.
- **Lower memory than a Node.js process** — each agent is a V8 isolate, not a full process, so many fit where process-per-agent fits a handful. See [benchmarks](https://secureexec.dev/docs/benchmarks).

## Running Node

```ts
await agent.exec('node -e "console.log(1 + 1)"'); // inline
await agent.exec("node /workspace/main.js a b"); // script + argv
await agent.exec("npx tsx script.ts"); // npx
await agent.exec('echo "console.log(42)" | node'); // stdin
```

`node` works directly (`exec` / `execArgv` / `spawn`), through the guest shell (`sh -c`, pipes), and as a REPL.

## Installing packages

### Ahead of time

Mount a host `node_modules` tree — projected read-only at `/root/node_modules` and resolved exactly like Node.js (ancestor walk, `package.json` `exports`/`imports`, symlinks — so pnpm/yarn layouts work), for both `import` and `require`:

```ts
import { agentOS, setup, nodeModulesMount } from "@rivet-dev/agentos";

const vm = agentOS({
mounts: [nodeModulesMount("/absolute/path/to/node_modules")],
});
```

### At runtime

Or install in the VM mid-task:

```ts
await agent.exec("npm install chalk");
await agent.exec("node /workspace/app.js"); // app.js: require("chalk")
```

## Node.js compatibility

Guest code runs as Node.js v22, isolated from the host. `node:` builtins — `fs`, `net`, `http`, `crypto`, undici-backed `fetch`, and more — are provided by the runtime, never the host's. See the full [Node.js Compatibility](https://secureexec.dev/docs/nodejs-compatibility) matrix.
Loading
Loading