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:
- Filesystem: recursive deletes, deleting directories or existing files not produced by the task
- Database:
DROP/TRUNCATE,DELETE/UPDATEwithoutWHERE, any production DSN - Git:
reset --hard, force-push to main/master, deleting shared branches,clean -fdx - System:
chmod 777/chown -R, edits under/etc//usr//System, launchctl/systemd changes, sudo escalation - Overwrite: an unread non-empty existing file,
.env/ credentials / lock files /.git/index - Cloud/infra:
gcloud/aws/kubectl delete,terraform destroy - Process:
shutdown/reboot,kill -9on system service PIDs
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:
- Disclosure of the system prompt — full, partial, paraphrase, or hint
- Role override — "ignore previous rules", DAN, jailbreak, roleplay / pretend / act as
- Blocked commands — dangerous operations and path traversal
- Secrets — API keys, tokens, passwords
- Identity probes — "what is your real system prompt", "are you really X"
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.