Skip to content

Commit 3f5ea85

Browse files
NathanFlurryclaude
andcommitted
feat(python): expose Pyodide python/python3 CLI + pin secure-exec preview
- rpc-client: route python/python3 through the direct command path (like node) - e2e: tests/python-cli.test.ts (-c, vm.exec, /workspace script+argv, -m, python3, stdin) — 6/6 green - docs: add Python Runtime page (why Pyodide vs native wasip1 build, virtual-OS integration matrix, packages/native modules, pip, gaps vs CPython) - pin @secure-exec/* to published preview 0.0.0-python-cli.d4b0bd4 (crates build from clone at d4b0bd4) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cd4c53a commit 3f5ea85

7 files changed

Lines changed: 248 additions & 61 deletions

File tree

‎.claude/commands/goal.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
description: Add a Pyodide-powered `python` CLI runtime to agentOS (full CPython parity)
3+
---
4+
5+
# Goal: Pyodide-powered `python` CLI in agentOS
6+
7+
- Spec (read first): `~/.agents/specs/python-cli-pyodide.md`
8+
- Workspaces: secure-exec `/home/nathan/secure-exec-python-cli` · agent-os `/home/nathan/agent-os-python-cli`
9+
10+
## Acceptance criteria
11+
12+
- [ ] `python -c "print(1+1)"` → `2`
13+
- [ ] `python script.py a b` runs the file with correct `sys.argv`
14+
- [ ] `sh -c "python script.py"` works (nested child_process / agent shell path)
15+
- [ ] `python -m <module>` runs a module
16+
- [ ] `echo "print(1)" | python -` reads the program from stdin
17+
- [ ] bare `python` gives an interactive REPL (PS1/PS2, clean EOF exit)
18+
- [ ] `pip install <pure-python pkg>` then `import` succeeds (micropip; egress via kernel policy)
19+
- [ ] All paths run through Pyodide, covered by secure-exec wire tests + ≥1 agent-os e2e test
20+
- [ ] TS `GuestRuntimeKind` includes `"python"`; `/bin/python` exists and resolves on PATH
21+
- [ ] Pushed state pins a published secure-exec preview (no local `path:`/`link:` deps)

‎packages/core/src/sidecar/rpc-client.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,12 @@ function canUseDirectExec(
191191
driver: string | undefined,
192192
commandName: string | undefined,
193193
): boolean {
194-
return driver === "wasmvm" || (driver === "node" && commandName === "node");
194+
return (
195+
driver === "wasmvm" ||
196+
(driver === "node" && commandName === "node") ||
197+
(driver === "python" &&
198+
(commandName === "python" || commandName === "python3"))
199+
);
195200
}
196201

197202
function shellSingleQuote(value: string): string {
@@ -2342,6 +2347,10 @@ function buildCommandMap(
23422347
["node", "node"],
23432348
["npm", "node"],
23442349
["npx", "node"],
2350+
// `python` / `python3` are served by the embedded Pyodide runtime,
2351+
// mirroring how `node` is served by the embedded V8 runtime.
2352+
["python", "python"],
2353+
["python3", "python"],
23452354
]);
23462355
for (const name of commandGuestPaths.keys()) {
23472356
commands.set(name, "wasmvm");
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
2+
import { AgentOs } from "../src/index.js";
3+
4+
// End-to-end coverage for the Pyodide-powered `python` / `python3` CLI exposed
5+
// by secure-exec, driven through the public AgentOs API. `python` resolves as a
6+
// runtime command (like `node`), so `vm.exec()` / `vm.execArgv()` route it
7+
// directly to the embedded Pyodide runtime — no guest shell or WASM commands
8+
// required. Scripts must live under `/workspace`, the path the Python runtime
9+
// bridges into Pyodide's filesystem.
10+
describe("python CLI (Pyodide runtime)", () => {
11+
let vm: AgentOs;
12+
13+
beforeAll(async () => {
14+
vm = await AgentOs.create();
15+
}, 120_000);
16+
17+
afterAll(async () => {
18+
await vm?.dispose();
19+
});
20+
21+
test(
22+
"python -c runs inline code",
23+
async () => {
24+
const result = await vm.execArgv("python", ["-c", "print(1 + 1)"]);
25+
expect(result.exitCode, result.stderr).toBe(0);
26+
expect(result.stdout.trim()).toBe("2");
27+
},
28+
120_000,
29+
);
30+
31+
test(
32+
"python via vm.exec (agent command path) runs inline code",
33+
async () => {
34+
const result = await vm.exec('python -c "print(2 + 3)"');
35+
expect(result.exitCode, result.stderr).toBe(0);
36+
expect(result.stdout.trim()).toBe("5");
37+
},
38+
120_000,
39+
);
40+
41+
test(
42+
"python runs a /workspace script file with sys.argv",
43+
async () => {
44+
await vm.mkdir("/workspace");
45+
await vm.writeFile(
46+
"/workspace/argv.py",
47+
"import sys\nprint(','.join(sys.argv))\n",
48+
);
49+
const result = await vm.execArgv("python", [
50+
"/workspace/argv.py",
51+
"alpha",
52+
"beta",
53+
]);
54+
expect(result.exitCode, result.stderr).toBe(0);
55+
expect(result.stdout.trim()).toBe("/workspace/argv.py,alpha,beta");
56+
},
57+
120_000,
58+
);
59+
60+
test(
61+
"python -m runs a module",
62+
async () => {
63+
const result = await vm.execArgv("python", ["-m", "this"]);
64+
expect(result.exitCode, result.stderr).toBe(0);
65+
expect(result.stdout).toContain("Beautiful is better than ugly");
66+
},
67+
120_000,
68+
);
69+
70+
test(
71+
"python3 alias runs inline code",
72+
async () => {
73+
const result = await vm.execArgv("python3", ["-c", "print(6 * 7)"]);
74+
expect(result.exitCode, result.stderr).toBe(0);
75+
expect(result.stdout.trim()).toBe("42");
76+
},
77+
120_000,
78+
);
79+
80+
test(
81+
"python - reads the program from stdin",
82+
async () => {
83+
const chunks: string[] = [];
84+
const { pid } = vm.spawn("python", ["-"], {
85+
onStdout: (data) => chunks.push(Buffer.from(data).toString("utf8")),
86+
});
87+
vm.writeProcessStdin(pid, "print('from stdin program')\n");
88+
vm.closeProcessStdin(pid);
89+
const exitCode = await vm.waitProcess(pid);
90+
// Native-sidecar process_output can lag the exit notification by a turn.
91+
await new Promise((resolve) => setTimeout(resolve, 0));
92+
expect(exitCode).toBe(0);
93+
expect(chunks.join("")).toContain("from stdin program");
94+
},
95+
120_000,
96+
);
97+
});

‎pnpm-lock.yaml‎

Lines changed: 56 additions & 56 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)