Security & the trust model
protoAgent's security posture is deliberately simple and honest: it sandboxes what is genuinely untrusted, trusts what you deliberately installed, and tells you plainly that running arbitrary code needs a container — it does not pretend otherwise. There are three actors, three postures.
1. Code you installed → trusted (by your choice)
A plugin's backend Python runs in-process, with full access — the filesystem and network as the server user. This is intentional and it's the same deal as pip install / npm install / a VS Code extension / a ComfyUI custom node: installing a plugin runs its code, so installing it is trusting it. Only install plugins you'd trust with your shell.
Because the backend already runs as you, gating a plugin's frontend (its UI) behind extra "trust" ceremony would be theater — it'd be harder on the browser-React of code whose Python you already let run as your user. So plugin UIs are simply sandboxed iframes (the plugin serves its own page; see building a plugin view) — uniform, no special "this plugin is trusted" mode. (Earlier versions had a Module Federation in-process-React path behind a trust gate; ADR 0038 retired it as inconsistent with this model.)
2. Code the agent generated → untrusted (always)
This is the boundary that genuinely matters. Even with a trusted operator and trusted plugins, the model's output is untrusted — prompt injection, a hostile page it fetched, a hallucinated snippet. So anything the agent generates and renders is sandboxed:
- Artifacts (
show_artifact— generated HTML/SVG/Mermaid/React) render in a nestedsandbox="allow-scripts"iframe with no same-origin — they run, but can't touch the console, its cookies, or its APIs. Same model as Claude Artifacts / Open WebUI.
This is the one place the sandbox isn't ceremony — it's the line between "code you installed" and "code the model emitted."
3. The agent running code → opt-in, honestly partial
execute_code and the ACP coding-agent runtimes run real code. We don't pretend this is sandboxed: execute_code's own docs say it is "isolation, not a true sandbox" — a subprocess with a hard timeout and a scrubbed env, but it can still touch the FS/network as the server user. The real controls are:
- Opt-in —
execute_codeis off unless enabled. - Egress allowlist (ADR 0008) — outbound HTTP is deny-by-default for private IPs, and a configured
egress.allowed_hostsmakes it a strict allowlist. - The honest guidance: enable it for trusted-model output, or run inside a hardened container.
The real isolation boundary is the container
Putting it together: if you want protection from genuinely untrusted third-party plugins (not "I trust what I installed"), the lever is not any per-feature toggle — it's the deployment boundary. Run protoAgent in a container (the recommended isolation boundary, see multi-instance). Inside that boundary:
- installed plugins are trusted (you chose them),
- the agent's generated output is sandboxed (iframe),
execute_codeis opt-in and egress-gated.
That's the whole model, stated plainly: trust what you install, sandbox what the model generates, contain the blast radius. No security theater, no pretending a plugin's React was more dangerous than its Python.
An instance with no token
With no bearer token (the desktop default, bound to loopback), the operator API is open to anything that can reach the port. On a shared machine that includes every web page open in a local browser. Three hardening rules narrow what a browser page can do (#3668):
Host allowlist. Every request and WebSocket must be addressed to a name the instance is served under: an IP literal,
localhost/*.localhost,*.ts.net, this machine's<name>.local, a named bind, the hosts ofA2A_ALLOWED_ORIGINS, orPROTOAGENT_TRUSTED_HOSTS. Anything else gets403. This is the same allowed-hosts check Jupyter, VS Code and Vite apply to their local servers.Cross-site requests. A request that can change state (anything but
GET/HEAD) and every WebSocket must come from the console itself. That means one of:- the same origin, including the console's plugin views;
- the desktop app;
- a loopback origin the server's CORS policy already admits;
- an
A2A_ALLOWED_ORIGINSentry; - a client that isn't a browser (no
Origin, noSec-Fetch-Site: cross-site, as with curl or an SDK).
Anything else gets
403.JSON only on
/a2aand/v1/*. A request with a body must sayContent-Type: application/json(or a+jsontype). Anything else gets415.
These rules narrow the exposure but aren't authentication. Any local process, and any other page on a loopback origin, can still use an open instance. If other people or programs share the machine, or the instance is reachable beyond loopback, set a token. A token-gated instance skips these rules: its credential is a header the client attaches (never a cookie), so a page on another origin has nothing to send. The server refuses a non-loopback open bind unless PROTOAGENT_ALLOW_OPEN=1.
See also
- Building a plugin view — the sandboxed-iframe plugin UI model.
- ADR 0008 (egress allowlist), ADR 0027 (git-URL plugin install), ADR 0038 (why the in-process React trust gate was retired), ADR 0004 / multi-instance (containers as the isolation boundary).