Inspiration
AI agents are only as useful as the tools you give them, and every new tool is third-party code running next to your files, your API keys, and a model that can be talked into things by a malicious web page. Most agent frameworks load tools straight into the host process with ambient access to everything. One buggy plugin crashes the session; one compromised plugin reads ~/.ssh.
We wanted the opposite: an agent where nothing is trusted by default, every capability is an add-on you consent to, and one plugin failing can never take everything else down. Think of a browser's extension model, but for AI agents.
What it does
Helm is a local AI agent harness where every capability is a plugin: the model provider, the tools, the safety policies, even the UI panels. The host only knows how to run a conversation, supervise processes, and enforce permissions.
- CLI (
helm) and local web console (helm serve) built on one shared core - Six bundled plugins in TypeScript and Python: files, git, web search, an OpenRouter model provider, an offline mock model, and Guard, a policy plugin that adds no tools at all. Instead it intercepts other plugins' tool calls to block
.envreads and redact secrets - Declare → consent → enforce → revoke: plugins list their permissions in a manifest, you approve them at install, and you can narrow or revoke them at any time, taking effect on the very next call
- Full audit log of every security-relevant decision
- One-command developer experience:
helm plugin newscaffolds a working plugin with a passing test, andhelm plugin devhot-reloads it inside the real sandbox
How we built it
Each plugin runs as its own OS process and talks to the host over JSON-RPC 2.0 on stdio. Because the host never loads plugin code into its own process, plugins can be written in any language, and crash isolation comes built in.
Security is layered, so a plugin can only do something every layer allows. If $L_i(p)$ is the set of actions layer $i$ permits plugin $p$, its effective capability is
$$ \text{Allowed}(p) = \bigcap_{i=1}^{4} L_i(p) $$
- macOS Seatbelt sandbox: a profile generated per launch from the plugin's current grants. No sockets, no reads outside granted paths, no spawning other programs
- Node permission model (
node --permission) as a second layer for TypeScript plugins - Permission broker: all file, network, process, and secret access goes through host APIs that canonicalize paths (resolving symlinks before scope checks), block SSRF to private IPs at every redirect hop, and record an audit entry
- Scrubbed environment and resource limits: V8 heap caps, a memory watchdog, and call timeouts
For stability, a supervisor handles handshakes, timeouts, and cancellation. A plugin is quarantined once it crashes $\geq 3$ times in a sliding 5-minute window:
$$ \text{quarantine}(p) \iff \big|{\, c \in \text{crashes}(p) : t_{\text{now}} - t_c \le 300\,\text{s} \,}\big| \ge 3 $$
When that happens, its tools are hidden from the agent, with a plain-language explanation, until you reset it.
The stack is Node 22+ running TypeScript directly (no build step), a stdlib-only Python SDK, and a Preact/htm web console. Model access goes through OpenRouter, with a configurable blockedModels policy checked both before each request and against the model actually routed to.
Challenges we faced
- Nested sandboxes.
sandbox-execfails withOperation not permittedwhen Helm itself runs inside another sandbox, such as an IDE or agent terminal. Silently falling back would have been a security hole, so untrusted plugins now refuse to start with anISOLATION_UNAVAILABLEerror that explains the two ways forward. - Proving the host is truly generic. It's easy to accidentally hard-code "if git plugin, do X". We added a test that fails if any host file mentions a bundled plugin, tool, or provider by name.
- Path and network edge cases. Symlinks (
link-to-home/.ssh/id_rsa), DNS names that resolve to127.0.0.1, and redirect chains all had to be caught by the broker. We wrote adversarial fixture plugins (py-escape,sh-raw,bad-hook) whose only job is to try to break out. - Failing safely. A crashed policy plugin shouldn't mean "no policy". Intercept hooks can be declared fail-closed, so a broken Guard blocks calls rather than letting them through.
What we learned
- Process isolation is the decision everything else depends on: crash containment, enforceable permissions, and language freedom all follow from it.
- Defense in depth matters. Node's own docs call its permission model a seat belt, not a security boundary, so we treat it as one layer of four, never the only one.
- Good error messages are a security feature. Every failure in Helm says what happened, why, and the exact command to fix it. Users who understand a block don't go looking for ways around it.
- Treat the model as untrusted input. Prompt injection means every tool call is suspect, so approvals and audit trails belong in the core, not as an afterthought.
Built With
- git
- json-rpc
- manus
- node.js
- python
- typescript