From d9e87a9957b3346bae4f19b7160e60d8f22aa293 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Thu, 25 Jun 2026 12:11:59 -0700 Subject: [PATCH] feat(python): Pyodide python/python3 CLI + whole-VM-root filesystem + VFS-persistent pip + file delete/rename - rpc-client: route python/python3 through the direct command path; report the python driver in processTree() - /bin/python + /bin/python3 stubs so the guest shell resolves python on PATH - e2e: tests/python-cli.test.ts (-c, vm.exec, script anywhere on the VM fs, -m, python3, stdin, cross-runtime write->host read, guest shell + pipelines) - docs: Node.js Runtime + Python Runtime pages (running, installing packages, compatibility incl. supported file delete/rename); sidebar python icon - pin @secure-exec/* to the published python-cli preview Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/agent-os.ts | 13 +- packages/core/src/runtime-compat.ts | 8 +- packages/core/src/sidecar/rpc-client.ts | 18 ++- .../tests/agentos-base-filesystem.test.ts | 3 +- packages/core/tests/python-cli.test.ts | 135 ++++++++++++++++++ pnpm-lock.yaml | 12 +- pnpm-workspace.yaml | 8 +- website/docs.config.mjs | 3 +- website/package.json | 2 +- .../src/content/docs/docs/architecture.mdx | 2 +- website/src/content/docs/docs/js-runtime.mdx | 41 ------ .../src/content/docs/docs/nodejs-runtime.mdx | 52 +++++++ .../src/content/docs/docs/python-runtime.mdx | 63 ++++++++ 13 files changed, 300 insertions(+), 60 deletions(-) create mode 100644 packages/core/tests/python-cli.test.ts delete mode 100644 website/src/content/docs/docs/js-runtime.mdx create mode 100644 website/src/content/docs/docs/nodejs-runtime.mdx create mode 100644 website/src/content/docs/docs/python-runtime.mdx diff --git a/packages/core/src/agent-os.ts b/packages/core/src/agent-os.ts index 18aba76c3..6954ea11f 100644 --- a/packages/core/src/agent-os.ts +++ b/packages/core/src/agent-os.ts @@ -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/` 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"); @@ -2806,7 +2815,7 @@ export class AgentOs { options?.rootFilesystem, [ ...collectBootstrapWasmCommands(preparedCommandDirs.commandDirs), - ...NODE_RUNTIME_BOOTSTRAP_COMMANDS, + ...RUNTIME_BOOTSTRAP_COMMANDS, ...toolBootstrapCommands, ], ); diff --git a/packages/core/src/runtime-compat.ts b/packages/core/src/runtime-compat.ts index e2bce3578..161d0de4f 100644 --- a/packages/core/src/runtime-compat.ts +++ b/packages/core/src/runtime-compat.ts @@ -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", diff --git a/packages/core/src/sidecar/rpc-client.ts b/packages/core/src/sidecar/rpc-client.ts index e9833f2b3..a7c2502d4 100644 --- a/packages/core/src/sidecar/rpc-client.ts +++ b/packages/core/src/sidecar/rpc-client.ts @@ -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 { @@ -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 ?? {}), @@ -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"); diff --git a/packages/core/tests/agentos-base-filesystem.test.ts b/packages/core/tests/agentos-base-filesystem.test.ts index 3847c8f10..d02e3803c 100644 --- a/packages/core/tests/agentos-base-filesystem.test.ts +++ b/packages/core/tests/agentos-base-filesystem.test.ts @@ -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", ); diff --git a/packages/core/tests/python-cli.test.ts b/packages/core/tests/python-cli.test.ts new file mode 100644 index 000000000..c4f76cf77 --- /dev/null +++ b/packages/core/tests/python-cli.test.ts @@ -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, + ); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b384fb64b..3521226ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -674,8 +674,8 @@ importers: specifier: ^6.0.0 version: 6.0.2(astro@5.18.2(@types/node@24.13.2)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0)) '@rivet-dev/docs-theme': - specifier: github:rivet-dev/docs-theme#v0.2.2 - version: https://codeload.github.com/rivet-dev/docs-theme/tar.gz/086e29994f5d85c2fcd55232dede611750fb82c2(@astrojs/starlight@0.36.3(astro@5.18.2(@types/node@24.13.2)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)))(astro@5.18.2(@types/node@24.13.2)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) + specifier: github:rivet-dev/docs-theme#v0.2.3 + version: https://codeload.github.com/rivet-dev/docs-theme/tar.gz/ce69ec9955f666bc3622a31666927dcebbc9a4d9(@astrojs/starlight@0.36.3(astro@5.18.2(@types/node@24.13.2)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)))(astro@5.18.2(@types/node@24.13.2)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) astro: specifier: ^5.18.2 version: 5.18.2(@types/node@24.13.2)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) @@ -2353,9 +2353,9 @@ packages: engines: {node: '>=18'} hasBin: true - '@rivet-dev/docs-theme@https://codeload.github.com/rivet-dev/docs-theme/tar.gz/086e29994f5d85c2fcd55232dede611750fb82c2': - resolution: {tarball: https://codeload.github.com/rivet-dev/docs-theme/tar.gz/086e29994f5d85c2fcd55232dede611750fb82c2} - version: 0.2.2 + '@rivet-dev/docs-theme@https://codeload.github.com/rivet-dev/docs-theme/tar.gz/ce69ec9955f666bc3622a31666927dcebbc9a4d9': + resolution: {tarball: https://codeload.github.com/rivet-dev/docs-theme/tar.gz/ce69ec9955f666bc3622a31666927dcebbc9a4d9} + version: 0.2.3 peerDependencies: '@astrojs/starlight': '>=0.36.0' astro: '>=5.0.0' @@ -8952,7 +8952,7 @@ snapshots: - supports-color optional: true - '@rivet-dev/docs-theme@https://codeload.github.com/rivet-dev/docs-theme/tar.gz/086e29994f5d85c2fcd55232dede611750fb82c2(@astrojs/starlight@0.36.3(astro@5.18.2(@types/node@24.13.2)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)))(astro@5.18.2(@types/node@24.13.2)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))': + '@rivet-dev/docs-theme@https://codeload.github.com/rivet-dev/docs-theme/tar.gz/ce69ec9955f666bc3622a31666927dcebbc9a4d9(@astrojs/starlight@0.36.3(astro@5.18.2(@types/node@24.13.2)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)))(astro@5.18.2(@types/node@24.13.2)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0))': dependencies: '@astrojs/starlight': 0.36.3(astro@5.18.2(@types/node@24.13.2)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) '@fortawesome/free-solid-svg-icons': 6.7.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 241001664..19b1eeb06 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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 <<< diff --git a/website/docs.config.mjs b/website/docs.config.mjs index 37b0649f5..9510a50e8 100644 --- a/website/docs.config.mjs +++ b/website/docs.config.mjs @@ -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" } }, ], diff --git a/website/package.json b/website/package.json index 16fdb1dca..27c5cd507 100644 --- a/website/package.json +++ b/website/package.json @@ -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", diff --git a/website/src/content/docs/docs/architecture.mdx b/website/src/content/docs/docs/architecture.mdx index a8c792c88..633e87570 100644 --- a/website/src/content/docs/docs/architecture.mdx +++ b/website/src/content/docs/docs/architecture.mdx @@ -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. diff --git a/website/src/content/docs/docs/js-runtime.mdx b/website/src/content/docs/docs/js-runtime.mdx deleted file mode 100644 index ab5ebd7a7..000000000 --- a/website/src/content/docs/docs/js-runtime.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: "JavaScript Runtime" -description: "How agentOS runs guest JavaScript: native V8 acceleration, low memory overhead, and Node.js compatibility." -skill: true ---- - -The JavaScript runtime is powered by the Rivet [Secure Exec](https://secureexec.dev) project, which provides the isolated V8 runtime that agentOS runs guest code in. Every guest VM executes its JavaScript inside this runtime, fully sandboxed from the host. - -## JavaScript Acceleration - -- **JavaScript is unusually slow as WebAssembly**: unlike most software, JavaScript pays a heavy penalty when compiled to WebAssembly, because so much engineering has gone into JIT-compiling JavaScript directly in V8. -- **Native V8, full JIT**: agentOS therefore runs guest JavaScript on the native V8 engine with its full JIT compiler, not through a WASM translation layer. We call this **JavaScript Acceleration**. -- **Native-speed execution**: guest JavaScript runs at native speed while staying inside the isolation boundary, with normal Node.js semantics. - -## Comparison to Node.js efficiency - -- **Isolate model, not processes**: agentOS runs each agent inside a V8 isolate rather than spawning a full Node.js process per agent. -- **Low memory overhead**: an isolate carries far less per-agent memory overhead than a full Node.js process, so many agents fit in the footprint that a process-per-agent model would spend on a handful. -- **Benchmarks**: see the Secure Exec [benchmarks](https://secureexec.dev/docs/benchmarks) for cold start, warm execution, and reuse measurements. - -## Node.js compatibility - -Guest code runs as Node.js (reporting `process.version` as `v22.0.0`), but it never touches the host runtime. Every `node:` builtin resolves to a kernel-backed bridge or an in-isolate polyfill, never the real host module. For the full builtin matrix (`fs`, `net`, `http`, `crypto`, undici-backed `fetch`, and more), see the Secure Exec [Node.js Compatibility](https://secureexec.dev/docs/nodejs-compatibility) reference. - -### npm packages - -By default the VM has no npm packages installed. Mount a host `node_modules` directory to give guest code access to real packages: the `nodeModulesMount` helper projects it read-only at `/root/node_modules`, and the in-kernel resolver walks it exactly like Node.js does, with no bundling or patching. - -```ts -import { agentOS, setup, nodeModulesMount } from "@rivet-dev/agentos"; - -const vm = agentOS({ - // Project a host node_modules tree into the VM (read-only by default). - mounts: [nodeModulesMount("/absolute/path/to/node_modules")], -}); - -export const registry = setup({ use: { vm } }); -registry.start(); -``` - -Resolution matches naive Node.js over the mounted tree: the ancestor `node_modules` walk, `package.json` `exports`/`imports` and conditions, and `realpath`/symlink following (so pnpm and yarn layouts resolve too). Both ESM `import` and CommonJS `require` work. See the Secure Exec [module loading](https://secureexec.dev/docs/features/module-loading) guide for the full model. diff --git a/website/src/content/docs/docs/nodejs-runtime.mdx b/website/src/content/docs/docs/nodejs-runtime.mdx new file mode 100644 index 000000000..67df0783c --- /dev/null +++ b/website/src/content/docs/docs/nodejs-runtime.mdx @@ -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. diff --git a/website/src/content/docs/docs/python-runtime.mdx b/website/src/content/docs/docs/python-runtime.mdx new file mode 100644 index 000000000..65348d6a2 --- /dev/null +++ b/website/src/content/docs/docs/python-runtime.mdx @@ -0,0 +1,63 @@ +--- +title: "Python Runtime" +description: "Run Python in agentOS: the python CLI, installing packages ahead of time or at runtime, and what's supported." +skill: true +--- + +agentOS runs **CPython 3.13** as a first-class runtime. `python` and `python3` are on the `PATH` and plug into the VM's filesystem, processes, and network — agents use them like any other command. + +## Running Python + +```ts +await agent.exec('python -c "print(1 + 1)"'); // inline +await agent.exec("python /workspace/main.py a b"); // script + sys.argv +await agent.exec("python -m http.server 8000"); // module +await agent.exec('echo "print(40 + 2)" | python -'); // stdin +``` + +`python` works directly (`exec` / `execArgv` / `spawn`), through the guest shell (`sh -c`, pipes), and as an interactive REPL. + +## Installing packages + +`pip install` writes to a persistent spot on the VM filesystem, so a package installed once is importable by every later `python` run in that VM. + +### Ahead of time + +Install once during setup so the agent starts ready — no install cost mid-task: + +```ts +// one-off setup pass on the VM, before handing it to the agent +await agent.exec("pip install requests pandas"); +// requests + pandas now import in every python run on this VM +``` + +### At runtime + +Or let the agent install what it needs, mid-task: + +```ts +await agent.exec("pip install rich"); +await agent.exec('python -c "import rich; print(rich.__version__)"'); +``` + +`pip install ` and `python -m pip install ` both work; downloads obey the VM's network policy (default-deny + allowlist). + +## Compatibility + +CPython 3.13 and the standard library, with a few VM-shaped differences. + +### Supported + +- Full VM filesystem (`/tmp`, `/etc`, `/root`, …) — shared with other processes and `readFile()` +- Reading, writing, creating, deleting, and renaming files anywhere on the VM +- A real process in the tree: stdin/stdout/stderr, signals, `kill` +- `subprocess` launching other VM commands (`node`, etc.) +- Pure-Python packages, plus native packages with a prebuilt wheel — `numpy`, `pandas`, `scipy`, `scikit-learn`, `pydantic`, `cryptography`, `Pillow`, and [many more](https://pyodide.org/en/stable/usage/packages-in-pyodide.html) +- `requests`, `urllib`, and `pip` over HTTP/DNS, under the VM network policy + +### Unsupported + +- OS threads and `multiprocessing` — the runtime is single-threaded +- `os.fork` / `os.exec` +- Some packages with native (C/Rust) extensions — see the [full list of supported packages](https://pyodide.org/en/stable/usage/packages-in-pyodide.html) +- Raw TCP/UDP sockets and listeners