From 130b49f75db5ad22cb630cc0e03bfdc6d80f05b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o?= Date: Wed, 23 Sep 2026 11:09:48 +0200 Subject: [PATCH] docs(security): document the tool approval gate and its default The post-external-context gate is off unless ODYSSEUS_TOOL_APPROVAL_GATE is set, but THREAT_MODEL.md described only the untrusted-context wrapper. A reader auditing the prompt-injection posture would reasonably assume the gate was active. State the default, what it blocks when enabled, the two deliberate exemptions, and note in Known Gaps that the compensating control for the missing shell sandbox is off by default. --- THREAT_MODEL.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index ee656087c..1dc4d4c20 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -60,6 +60,19 @@ External content that reaches the LLM is treated as untrusted via `src/prompt_se **Untrusted surfaces that must go through this wrapper:** web search results, fetched URLs, emails (read), saved memories, skill text, notes, and any tool output sourced from outside the server. Injecting untrusted content directly into the system role is a security bug. +### Post-external-context tool approval gate — off by default + +`src/tool_capabilities.py` carries a second layer: once untrusted content has entered a run, `ToolRunSecurityContext.decision_for()` blocks tools that execute code, mutate state, or cause external side effects until the user authorises the action separately. + +**It is disabled unless `ODYSSEUS_TOOL_APPROVAL_GATE` is set** (`1`/`true`/`yes`/`on`). The default is off because the gate is conservative enough to interrupt ordinary agent work. That is a deliberate usability trade, and it means a default deployment relies on the wrapper above — not on the gate — to contain injected instructions. + +Operators who run the agent against untrusted web or email content with side-effecting tools enabled should turn it on. With the gate off, a successful injection can reach `bash`, `host_shell`, `send_email` and `delete_email` without a separate confirmation; with it on, each of those is refused until approved. + +Two exemptions apply even when the gate is on, both deliberate: + +- Sources in `_CONTROL_PLANE_CONTEXT_SOURCES` (skills, runtime descriptors, the open editor document, the open email, uploaded files) are treated as control-plane metadata and still permit read-only tools. +- A TUI run that advertises an authenticated host bridge and declares `unattended_mode` exempts the local execution set in `TUI_CLIENT_TOOL_NAMES`. Personal, network and deployment-local tools are never exempted. + ## Security Headers `core/middleware.py:SecurityHeadersMiddleware` sets headers on every response: @@ -72,7 +85,7 @@ External content that reaches the LLM is treated as untrusted via `src/prompt_se These are open, acknowledged, and contributor help is welcome: -1. **No shell/filesystem sandbox.** The agent `bash` and `read_file`/`write_file` tools run as the app process user with no network egress filtering or filesystem confinement. A successful prompt-injection reaching a shell-enabled admin session can make outbound requests to internal services. See #1058 for the sandbox proposal. +1. **No shell/filesystem sandbox.** The agent `bash` and `read_file`/`write_file` tools run as the app process user with no network egress filtering or filesystem confinement. A successful prompt-injection reaching a shell-enabled admin session can make outbound requests to internal services. See #1058 for the sandbox proposal. The tool approval gate above is the compensating control, and it is off by default — so on a default deployment this gap is unmitigated beyond the untrusted-context wrapper. 2. **SSRF via `/api/v1/chat` `base_url` parameter.** A chat-scoped API token can supply an arbitrary `base_url`; the server forwards the LLM request to that host without validating the scheme or address. PR #1039 fixes this.