vahub gives a language model the ability to act on your home and your accounts. This document says plainly what is trusted, what is not, what the policy gate does about it, and where the gate stops helping. It also says how to report a vulnerability.
security covers the same boundary from the operator's side, with the configuration that goes with each decision. This file is the shorter statement of the model itself.
Report privately through GitHub Security Advisories:
https://github.com/LynnDelpy/vahub/security/advisories/new
Please do not open a public issue for a vulnerability, and please do not post a working exploit before a fix exists.
Useful things to include: the version or commit, the configuration that makes it reachable (with secrets removed), what an attacker gains, and the smallest sequence that demonstrates it. If a module is involved, say which one and where it came from.
What to expect: an acknowledgement within three days, an assessment within two weeks, and credit in the release notes unless you prefer otherwise. This is a volunteer project with no bug bounty. Once a fix is released, the advisory is published.
Supported versions:
| Version | Status |
|---|---|
| 0.1.x | supported |
| older | none exist yet |
Three parts, with a boundary between each:
- An agent that talks to a language model and proposes tool calls.
- A policy gate that authorizes or refuses every call, in code.
- Modules, which are separate processes that speak MCP over stdin and stdout and hold the credentials for whatever they talk to.
The gate sits between the agent and the modules, and the scheduler and a confirmed action go through it as well. There is no path from a proposed action to a module that skips it.
Not trusted: the model's output. Every tool call it proposes is a request, never a decision. The model is a component that can be steered by its input.
Not trusted: anything a tool returns. The title of a calendar event, the body of a notification, the name of a device, the text of a transit alert. All of it enters the model's context, and any of it can contain instructions aimed at the model. This is prompt injection, and it is not solved by prompting. It is contained by refusing the resulting calls at the gate, and by keeping the web page from rendering module output as markup.
Not trusted: the module process. A module runs on your machine, with the configuration you gave it. The hub does not import it, shares no memory with it, and can kill it, but a module is still code you chose to run.
Trusted: vahub.yaml. The configuration file is the operator's word. Anyone who can edit it can grant anything. Protect it accordingly.
Trusted: a signed-in account, or the operator on the host. The hub has a
built-in login (named accounts, on by default) and can instead sit behind an
authenticating proxy. Whoever writes vahub.yaml or runs the CLI on the host can
grant anything; the login only decides who may use the web interface.
An account holds one of two roles, and the difference is worth stating because it is the only privilege boundary inside the web interface:
- admin may install, configure and remove modules, and may create, disable and remove accounts. Installing runs code you chose on the host, and configuring hands that code a credential, so these are the two most consequential things a browser can do here.
- user may talk to the assistant, arrange the dashboard, read a module's read-declared tools, approve a held-back action, and edit the places and schedules the household shares.
The first account is an admin, because otherwise nobody could create the second.
The role is read from the account on every request, so a demotion or a disable
takes effect on the next request rather than when a cookie expires, and the hub
refuses the change that would leave no admin able to sign in. Neither role can
edit the policy or grant the assistant a capability; that is still vahub.yaml.
With web.auth.enabled off there are no accounts, so there are no roles: the
proxy decided who may reach the hub and everyone who does is an operator.
- Default deny. A tool with no rule in
policy.rulesis refused. Adding a module does not grant it anything. - Arguments, not just names. Each rule lists the arguments it permits and
the values they may take (
in,matches,range,max_len). An argument the rule does not describe is refused rather than passed through. This is the part that matters: a Home Assistant long lived token is admin or nothing, so oncelight_turn_onis allowed at all, the only thing standing between the model and every device in the house is the constraint onentity_id. - Per principal. The agent, the scheduler and a person confirming at the console are different principals. A principal has deny patterns and a list of classes it must have confirmed. The scheduler can act unattended and still be denied locks.
- Classes and confirmation. A tool is
read,writeordestructive. A class listed under a principal'sconfirmis not executed. The call becomes a pending confirmation with its arguments frozen, a time to live, and a decision made out of band. Later turns in the conversation cannot alter what the confirmation executes. - Catalog filtering. Tools the principal could never call are not offered to the model, so it does not plan around them.
- Audit. Every call is recorded with its principal, arguments, decision and
result, including refusals. Arguments named in a manifest's
audit.redactare masked before they are written. - Manifests do not grant. A manifest's
toolsblock is what the module claims about itself. It is advisory. Onlyvahub.yamlauthorizes.
- A rule you wrote too widely.
matches: ".*"is an allow rule with extra steps. The gate enforces what you wrote, and cannot tell an intended entity pattern from an accidental one. - What a module does on its own. The gate sees calls the hub makes. It does not sit between the module and the service the module talks to. A module holding an admin token can use it whenever it likes, for anything, without a tool call. This is the reason installing a module is a trust decision and the reason a module gets only the environment variables its manifest declares.
- A malicious module. The hub reduces the damage (minimal environment, no shell, argv lists only, an optional dedicated uid per module, one call in flight at a time, untrusted results guarded rather than parsed hopefully), but a module is a process on your machine. Read what you install and pin it to a revision.
- Exfiltration through permitted tools. A permitted read tool combined with a permitted tool that sends arbitrary text is a general purpose channel out. If a notify module can send any string anywhere, injected instructions can use it to forward whatever the agent has read. Constrain destinations, or class the tool so it needs confirmation.
- The model provider. Prompts, transcripts and tool results go to whichever provider you configure, and speech providers receive your audio. The only configurations that keep this on your machine are a local model endpoint and browser side speech.
- An exposed hub with the login off. Authentication is the built-in login
(
web.auth.enabled, on by default) or a reverse proxy. With it off and the hub bound past loopback, every route is reachable by anyone who can connect.web.auth_subject_headerrecords who a proxy says is acting; it is an audit field, never an authorization input, because a header is trivially forged by anyone who reaches the port directly. - Operator surface on the host, not the web. Module stderr, the tool
catalogue and the audit log are read with the CLI (
vahub doctor,vahub audit,vahub module verify), which needs shell access. Modules and accounts are also manageable from the browser by an admin; the policy is not, and no role can grant the assistant a capability. A plain user's view of a module carries its name, state and tools, not its configuration keys, its last error, or whether policy allows it. - A compromised admin session is a compromised hub. An admin can install a
module, which is arbitrary code on the host. That was already true of the CLI
and of
vahub.yaml; making it reachable from a browser means the admin's session cookie now stands in front of it. That is why an admin's own password change requires the current password, why the role check is re-read from the database on every request, and why plain accounts exist at all: give the householduseraccounts and keepadminfor whoever runs the hub. - Denial of service and cost. The budgets bound one turn (iterations, tool result bytes, tokens, wall clock). They keep a loop from becoming an unbounded bill. They are not a defence against someone who can reach the API and keep asking.
- The state directory. Conversation history, the audit trail and pending
confirmations live in SQLite under
hub.state_dir. It is not encrypted. Anyone who can read the file can read your conversations.
- Keep
web.hoston loopback, or put an authenticating reverse proxy in front and let only the proxy reach the port. - Keep
policy.default: deny. Write the narrowest constraints that let the thing you want actually work, and check them with the audit log rather than by assumption. - Class anything you would not want to happen unattended as
destructive, and give the scheduler a deny pattern for it. - Give every module its own uid in the manifest, and run the hub where dropping privileges is possible.
- Keep secrets out of
vahub.yaml. Use${VAR}or${file:/path}so the values come from systemd credentials, Docker secrets or Kubernetes secrets, and list secret bearing arguments underaudit.redact. - Restrict outbound network access to the endpoints your modules and your model provider actually need.
- Install modules from a pinned tag or commit. The registry refuses a moving branch as a source for this reason.
These are design decisions, not oversights, and issues asking about them are welcome but will get this answer:
- Prompt injection is contained, not prevented. The gate assumes the model can be convinced to propose anything.
- The hub does not authenticate users. Authentication belongs to the proxy.
- Module code is not sandboxed beyond process separation, a minimal environment and an optional uid. Stronger isolation (namespaces, seccomp) is a deployment concern and is not built in.