AeroPy
AeroPy is a browser-native Python execution extension for VS Code Web. It runs a pinned Pyodide runtime in a dedicated Web Worker and exposes six capabilities through public VS Code language-model tools plus one focused Agent Skill.
It has no Node.js extension entry point, native binary, server-side Python fallback, AeroCode dependency, workspace mount, or credential bridge.
AeroPy also provides an opt-in AeroPy Notebook editor and Python controller for .ipynb files. Select it from Reopen Editor With… when another notebook provider is installed. Notebook cells execute serially in a dedicated Pyodide environment per open notebook after the existing execution confirmation and workspace-trust checks.
Capabilities
| Public capability |
VS Code tool name |
Effect |
python/run_code |
aeropy_python_run_code |
Approved execution |
python/run_file |
aeropy_python_run_file |
Approved execution and explicit workspace read |
python/environment |
aeropy_python_environment |
Read |
python/install_packages |
aeropy_python_install_packages |
Separately approved execution and browser network |
python/save_artifacts |
aeropy_python_save_artifacts |
Explicitly approved workspace writes |
python/reset |
aeropy_python_reset |
Approved Worker destruction; no workspace mutation |
Other extensions discover these tools through vscode.lm.tools and invoke them with vscode.lm.invokeTool. The contributed python-analysis skill is declared with contributes.chatSkills. AeroPy also returns a versioned public API from activate() for independently approved direct workflows.
When a Python file is active, AeroPy shows a ▶ Run button in the editor title bar. It invokes AeroPy: Run Active Python File, requests execution approval, and streams output to the AeroPy output channel. Choose Run and Don't Ask Again to skip future confirmations for this direct command; the prompt can be restored with the AeroPy: Confirm Before Running an Active Python File setting. Workspace trust requirements still apply.
Use AeroPy: Install Python Packages… in the Command Palette to install packages without chat. Enter exact pins separated by spaces or commas (for example, polars==1.33.1), then confirm the download. Progress and errors appear in Output → AeroPy. This command installs into the environment used by Python files and chat; each notebook has its own separate environment. Packages are temporary and are discarded on reset.
Python tracebacks and early execution failures appear in Output → AeroPy before the completion summary for both file runs and inline code runs.
Effectful language-model tools request a separate VS Code confirmation by default. If the calling agent already provides its own approval UI, set AeroPy: Confirm Agent Tool Invocations to false for that workspace to avoid duplicate prompts. This delegates approval to every extension invoking AeroPy tools in that workspace; it does not weaken AeroPy's trusted-workspace checks.
Python output contract
from browser_python import emit, artifact
print("streamed to the AeroPy output channel")
emit({"answer": 42}, name="summary")
open("/tmp/result.csv", "w", encoding="utf-8").write("value\n42\n")
artifact("/tmp/result.csv", "text/csv", "result.csv")
Staged files are copied below /inputs for one run. python/run_file accepts either an absolute workspace URI or a workspace-relative path; multi-root paths start with the workspace folder name. Artifacts stay in host memory until reset, cancellation, deactivation, or an explicit save through python/save_artifacts, AeroPy: Save Artifact to Workspace…, or AeroPy: Save All Artifacts to Workspace….
Artifact writes are performed by the Web extension host through vscode.workspace.fs, not by Python. The agent supplies each returned artifact ID and a workspace-relative destination, VS Code previews the paths for approval, and existing files are protected unless overwrite=true is explicitly requested.
AeroPy supports saving any artifact with a valid MIME type. Python is responsible for writing the file correctly. Common formats receive additional verification; other formats remain supported with general checks only. An unrecognized format is not rejected merely because AeroPy has no content verifier for it.
| Format |
Additional checks |
HTML (text/html) |
UTF-8 and HTML parser errors; a doctype is optional |
JSON (application/json, *+json) |
UTF-8 and JSON parsing |
| PNG |
PNG signature (not a full image decode) |
| SVG |
UTF-8 and the existing SVG active-content sanitization |
| Text, including CSV and Markdown |
UTF-8 (not CSV/Markdown semantic validation) |
| Other MIME types, including PDF, ZIP, and custom binary formats |
No format-specific verification |
All artifacts still have MIME syntax, relative-name, count, and byte-limit checks. Workspace saves retain destination and overwrite checks. Descriptors report verification.level (format or generic) and the exact verification.checks applied. These checks do not certify content as safe or fully correct. HTML parser checks are not a full HTML conformance validator.
HTML scripts and styles are preserved byte-for-byte; parsing does not execute scripts or fetch resources. HTML is not automatically rendered in tool results or notebooks. Notebooks preserve HTML output with a text notice. Formats without an explicit notebook renderer are preserved as base64 in metadata.aeropy_artifact_data with a text notice. Artifacts generated through python/run_code or python/run_file remain available to python/save_artifacts until reset or cancellation.
from browser_python import artifact
with open("/tmp/system-map.html", "w", encoding="utf-8") as output:
output.write("<!doctype html><html><head><title>System map</title></head>"
"<body><h1>System map</h1></body></html>")
artifact("/tmp/system-map.html", "text/html", "system-map.html")
Save the returned artifact ID with python/save_artifacts to persist it in the workspace.
Architecture
The web extension host owns VS Code APIs, workspace access, approvals, and in-memory artifact handles. A dedicated Worker owns Pyodide and its ephemeral filesystem. Only structured-cloneable protocol messages cross the boundary.
Protocol v1 messages include a run ID, operation, code or staged byte arrays, stream events, structured values, artifact descriptors, package changes, environment data, and terminal completion/error results. Timeout and cancellation terminate the Worker. The next operation creates a clean generation.
No VS Code API, authentication session, GitHub token, extension secret, or workspace credential enters the Worker. Standard input is noninteractive. Python can still use Pyodide's JavaScript bridge and browser fetch, so the Worker is a lifecycle/isolation boundary—not a secure sandbox for hostile code.
Notebook serialization preserves nbformat 4 metadata, unknown fields, source representation, execution counts, and existing stream, error, structured text/JSON, and PNG/JPEG outputs. Generated HTML and SVG artifacts are preserved in notebook output data with a text notice; AeroPy does not render them. Loading a notebook never executes output content. Rich HTML, JavaScript, widgets, comms, magics, and interactive input are preserved in the file but are not rendered or executed by AeroPy. Cancelling or interrupting execution terminates that notebook's Worker, so its Python state and unsaved artifacts are discarded; the next run starts a clean Worker.
Enforced first-slice limits
| Limit |
Value |
| Default / maximum execution timeout |
30 s / 120 s |
| Python source |
1 MiB |
| stdout / stderr |
512 KiB each |
| Staged files |
16 |
| Individual / total staged input |
8 MiB / 16 MiB |
| Artifacts |
16 |
| Individual / total returned artifacts |
8 MiB / 32 MiB |
| Packages per install |
5 |
| Package downloads per install |
25 MiB |
Package requests must use exact name==version pins. AeroPy accepts packages built for Pyodide 314.0.6 and PyPI pure-Python *-none-any.whl files. Implicit dependencies are disabled so every download is explicit and bounded. Native CPython wheels, source builds, arbitrary wheel URLs, and install scripts are rejected.
The runtime is pinned to Pyodide 314.0.6 / Python 3.14.2. Its official package inventory supplies the matching Polars 1.33.1 WebAssembly wheel. Install polars==1.33.1 using the command above or python/install_packages, then run:
import polars as pl
print(pl.DataFrame({"value": [1, 2, 3]}))
The runtime upgrade changes the available distribution pins (for example, NumPy is now 2.4.6). Use packages built for this runtime; wheels compiled for the previous Python/Pyodide ABI are not interchangeable.
Browser networking and CORS rules apply. Subprocesses and raw sockets are unavailable. The filesystem and installed packages do not persist across browser sessions or environment resets.
Public direct API
const extension = vscode.extensions.getExtension('bpcarson.aeropy');
const python = await extension.activate();
const result = await python.runCode(
{ code: 'print(sum(range(10)))' },
{
approve: async ({ operation, effect, input, limits }) => {
// Present and record approval in the consuming extension's normal flow.
return true;
},
onEvent: (event) => console.log(event),
cancellationToken,
},
);
await python.saveArtifacts(
{
artifacts: result.artifacts.map(({ id, name }) => ({
artifactId: id,
workspacePath: `generated/${name}`,
})),
overwrite: false,
},
{ approve: async ({ effect }) => effect.workspaceWrite === true },
);
Effectful direct calls default to denied when no approval callback is supplied. Language-model tool invocations use VS Code's standard prepareInvocation confirmation contract. Artifact writes require a trusted workspace and remain outside the Worker.
Portable CLI and stdio MCP
Local harnesses can discover and call the same canonical tool names and JSON schemas without VS Code:
aeropy tools
aeropy tool aeropy_python_run_code --input '{"code":"print(sum(range(10)))"}'
aeropy mcp
aeropy mcp serves newline-delimited MCP JSON-RPC over stdio and keeps one Pyodide environment alive for the session. One-shot aeropy tool calls end with the process. Both surfaces return the version-1 portable result envelope, including the canonical tool name, success, recoverability, effect outcome, and saved-workspace artifact paths.
The Node adapter relies on the calling harness for approval and is intended only for reviewed code: Pyodide's JavaScript bridge can reach Node. Workspace reads and writes are contained below the current directory, reject symbolic-link traversal, and preserve existing files unless overwrite=true is supplied. Rich VS Code result parts are represented as structured descriptors.
npm run tooling:setup
npm ci
npm run check
npm run test:portable
npm run package:vsix
npm run test:web
test/shared/conformance.js is the unchanged deterministic fixture used by headless contract tests and the real VS Code Web Chromium gate. It verifies discovery, clean environment inspection, stdout/structured/artifact output, hard cancellation and recovery, approved pinned installation, native-package rejection, reset, and pre-Worker denial. The web gate additionally invokes python/environment through the public vscode.lm.invokeTool surface.
npm run test:integration resolves and verifies a compatible stable Marketplace bpcarson.configurable-chat Web extension, loads it into a Playwright-controlled VS Code Web host, discovers AeroPy's public skill and tools through AeroCode, and runs that same fixture through AeroCode's approval and tool UI. Approval, result, and completed-conformance screenshots are retained in the Playwright report.
Non-goals for 0.1
No debugger, terminal, interactive REPL, language server, persistent environment, arbitrary native wheels, subprocesses, sockets, unrestricted networking, hostile-code sandbox guarantee, server-side Python fallback, or AeroCode-private API.
Development quality checks
Use Node 24. Before the first dependency install (and after a shared-tooling pin
update), run npm run tooling:setup with GitHub CLI authenticated to
bpcarson/actions, then npm ci. Shared lint/format configuration is loaded from
the exact commit in .github/shared-tooling.json; CI receives the same package
through its pinned action. .shared-tooling/ is generated and must not be committed.
npm run lint / npm run lint:fix: check or fix JavaScript lint.
npm run format:check / npm run format: check or apply formatting.
npm run test:unit: all existing unit, portable-contract and Node CLI tests.
npm run build: manifest conformance and production web bundle.
npm run test:web: existing browser conformance and notebook tests in the pinned VS Code build.
npm run test:integration: resolve compatible stable AeroCode and run the existing integration suite.
npm run package:vsix / npm run check:vsix: package and check the required product payload.
Install the locked browser with ./node_modules/.bin/playwright install chromium
before local browser tests. Dependency and VS Code host pins are declared in
.github/extension-dependencies.json. The AeroCode compatibility range initially
preserves the supported 0.x line; narrow it when a test requires a newer public
API. CI records the selected release, URL and SHA-256 and passes the verified
local extension directory to the integration harness. Direct test:aerocode
requires that directory to be supplied; use test:integration for normal local use.
The integration command runs the installed shared tooling package. In CI it verifies
the retained dependency bytes without resolving again; integration-only retries use
the artifact ID from the successful dependency job.
The required CI workflow keeps its release-triggering name and dispatch support.
Both callers explicitly enable integration. The separate Prerelease compatibility
workflow runs weekly or manually. AeroPy
remains JavaScript with an explicit type-check exemption; no existing tests were
removed. Production bundling retains the pre-existing Mocha dynamic-dependency
warning. Update branch protection to require quality / quality, quality / web,
quality / dependencies, and quality / integration when adopting this reusable
workflow; replace the old unit-package, vscode-web, and aerocode-web checks.
Both release paths validate the source with the existing shared release-policy
action before materializing tooling and installing dependencies. The version-bump
job keeps that policy's inspect/create operations locally so it can invoke the
private setup action; it does not need a cross-repository token. Keep both setup
action references and their tooling-ref inputs aligned with
.github/shared-tooling.json and the setup revision inside the pinned CI workflow.