Documentation v1.0.9

Security Model

Localhost binding

The HTTP server binds to 127.0.0.1:17989 only — LAN clients cannot reach the daemon. The web dashboard is embedded into the binary and served from / by that same daemon, so the usual browser origin is your own machine. CORS middleware accepts loopback origins (localhost, 127.0.0.1, ::1) plus two fixed hosted origins, https://web.agenvoy.com and https://agenvoy-board.pardn.workers.dev, and answers Chrome Private Network Access with Access-Control-Allow-Private-Network, so those pages can call the local API from the browser.

Endpoints that touch credentials, config files, or process state carry an extra localhostOnly() guard on top of the bind. It checks the connection's remote address (127.0.0.1 / ::1), not the Origin header.

No elevated mode

There is no /sudo command and no session-wide escalation. The /dangerous command was removed; use the per-call confirmation below or the direct TUI actions. Per-request authorization replaced both: a call that reaches outside the safe boundary raises a confirmation for that one call, and the grant is scoped to that session plus that specific path.

$HOME is always writable with no setup. Two things sit outside it:

Case What happens
A path outside $HOME, or a sensitive path inside it boundary.Restricted collects it and raises a confirmation that also requires the operating-system password. Approval grants that path for that session
A command that has to write outside $HOME The agent attaches write_paths to that one run_command call; those paths are bound into the sandbox only after the same password-backed prompt is approved

Password checks live in one place, internal/sudo: sudo -n -v probes whether a sudo ticket is still cached (3 s limit), and sudo -S -v verifies a typed password (30 s limit). The sudo ticket is the only clock — there is no second TTL. While it is still cached the prompt appears without a password field, and a root daemon skips the check.

The TUI and the local web dashboard can collect the password; the web confirm endpoint accepts it only from a loopback connection. Channels that cannot collect one — Telegram, Discord, remote HTTP callers — get the call back as skipped, never elevated. A channel may approve the prompt, but an unverified restricted call does not execute.

sudo itself cannot run inside run_command, either directly or inside sh -c: the call is rejected with a pointer to write_paths and the password-backed confirmation.

Reads are not restricted by path. The sandbox constrains writes, not reads, so a command fails on a path only when the operating system itself refuses.

Permanent denials

denied_path in config.json is a hard denylist: those paths are off limits for both reads and writes, cannot be approved by any prompt, and retrying the same path fails the same way. Entries must be absolute or start with ~/; the filesystem root is refused. denied_command is its command-side equivalent — a binary on it is rejected outright, inside sh -c as well.

Sensitive file guard

Files matching the sensitive set — SSH keys, .pem, .key, .env, and credential files — are treated like paths outside $HOME: read_files, edit_file, open_file, find_files, file_history, and fetch_page with save_to raise the password-backed confirmation before touching them. The set is sensitive_path (dirs / files / prefixes / extensions), embedded in the binary and merged with any user entries in config.json. This guard is enforced in Go, not by prompt.

Permission mode

Agenvoy supports two permission modes: single-confirm (every write/exec call is confirmed) and always-allow (write/exec tools run without per-call confirmation). In always-allow the system prompt still requires an explicit ask_user before seven categories of truly irreversible operations:

A tool that carries a mode is gated by it: list / read / search are treated as read-only and skip confirmation, while remove / restore always confirm even on an otherwise auto-approved tool.

Origin-routed confirmations

Every interactive request carries an origin prefix — cli-, chat-, tg-, or dc-. CLI confirmations are consumed only by the TUI, web requests by the web confirmation stream, and Telegram or Discord requests by their matching channel listeners. A confirmation left unanswered for five minutes is skipped and the task is kept as pending, so one channel can neither intercept nor indefinitely hold another channel's prompt.

System prompt protection

The system prompt (configs/prompts/system_prompt/system_prompt.md) ends with a block that takes priority over skills, user instructions, and conversation context. A request matching any of these categories gets only [KARAPPO] as the reply:

These are policy in prompt, not Go-side hardcoded filters — adding a category means editing the prompt only.

Keychain

Credentials (provider API keys, OAuth tokens) are stored through go-pkg/filesystem/keychain under service agenvoy:

Platform Backend
macOS security CLI
Linux / other secret-tool (libsecret), falling back to a plain KEY=value file ~/.config/agenvoy/.secrets

When no stored value is found, an environment variable of the same name is used. The service name "agenvoy" is fixed and must not change.

Command execution

run_command never sees a raw shell string. Argv-only input, bare-command-name enforcement, and a parsed (not pattern-matched) sh -c script are the three layers — see the Sandbox page for the exact rules. Commands on the read-only list (git status, ls, cat, ...) skip the confirm gate; everything else is gated by the active permission mode.

The command policy is a denylist, not an allowlist: denied_command in config.json is the only list, and anything not on it runs subject to sandbox and confirmation. The former white_list / path_white_list keys are no longer read, and sensitive_map was renamed to sensitive_path — the daemon logs a warning if any of these keys is still present.

MCP isolation considerations

MCP servers are third-party processes whose behavior is unverifiable. Agenvoy treats them as untrusted by default and does not provide a per-server "trusted" flag. All MCP tool calls go through the same confirm gate as built-in tools. For batch operation, mark specific tools auto-approve through /mcp → permission (or POST /v1/allowlist with a tool block) rather than trusting a whole server.


[!NOTE] This document was auto-generated by Claude after reading the full source code.

中文