# 安全模型

## Localhost 綁定

HTTP server 只監聽 loopback 位址 `127.0.0.1:17989` 與 `[::1]:17989` — 區網 client 無法連到 daemon。Web 儀表板內嵌於執行檔，由同一個 daemon 在 `/` 提供，因此一般情況下瀏覽器來源就是你自己的機器。CORS middleware 只接受 loopback 來源（`localhost`、`127.0.0.1`、`::1`），並以 `Access-Control-Allow-Private-Network` 回應 Chrome Private Network Access。原本另外接受的兩個託管來源 `https://web.agenvoy.com` 與 `https://agenvoy-board.pardn.workers.dev` 已於 v1.0.17 移除 —— 請使用 daemon 在 `/` 提供的儀表板。

碰到憑證、設定檔或行程狀態的端點，在綁定之外另有 `localhostOnly()` 守門。它檢查的是連線的 remote address（`127.0.0.1` / `::1`），不是 `Origin` header。

CORS 只擋頁面讀取回應，擋不住送出請求，因此 v1.3.11 起 daemon 另外拒絕跨站寫入：`POST`、`PUT`、`PATCH`、`DELETE` 的 `Origin` header 若不是 `localhost`、`127.0.0.1` 或 `::1`，在進入 handler 前即回 403 `[Denied] origin: ...`（沒有 `Origin` 的請求，例如 `curl`，不受影響）。`POST /v1/send` 與 `POST /v1/chat/completions` 另要求 `Content-Type: application/json`，其餘一律回 415，堵住免 preflight 的 `text/plain` simple request。`/v1/send` 的 `enable_sandbox:false`（`agen -p --skip-sandbox`）只接受 loopback 連線，其他來源回 403。

自 v1.2.0 起 TUI 不再自行執行 agent，而是經 Unix socket `~/.config/agenvoy/daemon.sock` 與 daemon 溝通；該 socket 以 `0600` 權限建立，只有擁有者能連線。

## 沒有提權模式

不存在 `/sudo` 指令，也沒有 session 層級的提權。`/dangerous` 指令已移除；請改用下述的逐次確認或 TUI 的直接操作。兩者皆由逐請求授權取代：越過安全邊界的呼叫只為那一次呼叫升起確認，授權範圍限於該 session 加上該特定路徑。

`$HOME` 一律可寫，無需設定（`auto` 下寫入 `$HOME` 內但在工作目錄外的路徑會詢問一次，見[權限與確認](/zh/security-permissions)）。以下情況落在其外：

| 情況 | 行為 |
|---|---|
| `$HOME` 內的敏感路徑 | `boundary.Restricted` 收集後升起一般確認（不需密碼），任何權限模式皆然。核准後該路徑授權給該 session |
| `$HOME` 之外的路徑 | `boundary.Restricted` 收集後升起確認，且要求作業系統密碼，任何權限模式皆然（v1.3.7 之前 `auto` 會直接拒絕、不跳提示）。核准後該路徑授權給該 session。v1.3.10 起 `read_files` 與 `find_files` 讀取可讀清單（內建系統與套件位置加上 `readable_path`）中的路徑不跳確認 |
| 需要寫到 `$HOME` 之外的指令 | Agent 在該次 `run_command` 或 `background_command` 附上 `write_paths`；這些路徑要等同一道密碼確認通過後才綁進 sandbox |
| 被沙箱擋下、且 `network` 與 `write_paths` 都解決不了的指令 | 自 v1.3.0 起該次呼叫可設定 `sandbox: false`，在沙箱外以使用者的完整權限執行。這種呼叫一律跳出需要密碼的確認 —— allow 規則、`auto` 與 allow-all Skill 都不能略過 —— 密碼驗證通過後才執行；確認標示為 `outside sandbox: <cmd>` |
| 需要 root 權限的指令 | 自 v1.3.2 起該次呼叫傳入 argv `['sudo', '<binary>', ...]`。它會發起標示為 `sudo: <cmd>` 的確認，一律要求系統密碼，任何權限模式（含 allow-all）皆然，allow 規則也不能略過；核准後以 sudo ticket 在沙箱外執行 `sudo -n <binary> ...` |

密碼檢查集中在 `internal/sudo`：`sudo -n -v` 探測 sudo ticket 是否仍在快取（上限 3 秒），`sudo -S -v` 驗證輸入的密碼（上限 30 秒）。sudo ticket 是唯一的計時器——沒有第二層 TTL。ticket 仍在快取時，提示不會要求輸入密碼；以 root 執行的 daemon 則直接略過檢查。

TUI 與本機 web 儀表板可以收集密碼；web 確認端點只接受來自 loopback 連線的密碼。自 v1.2.0 起 TUI 不再自行執行 `sudo -v`：密碼輸入在 TUI 的密碼欄位，隨回覆送到 daemon，由 daemon 以 `sudo -S -v` 驗證；確認只送到發起該次執行的 TUI 視窗。無法收集密碼的通道不會提權：自 v1.3.3 起 Telegram 與 Discord 的受限確認不提供 **Yes**，並引導至本機的 TUI 或 Web UI；遠端 HTTP 核准回 403，儀表板保留確認卡片並顯示錯誤（v1.3.3 之前這類通道可以核准，呼叫會以 **skipped** 回傳）。自 v1.3.3 起密碼錯誤時提示保留可再輸入，受限確認也不再提供 **don't ask again** 與 **allow this turn**。

自 v1.3.2 起 `run_command` 與 `background_command` 只接受上述 argv 形式的 `sudo`：不接受 sudo 旗標、不可巢狀 `sudo` / `su`，目標必須是不在 `denied_command` 上的裸 binary。`sh -c` 內的 `sudo` 仍會被拒絕，錯誤訊息會指向 argv 形式。`sudo rm` 會真的刪除，一般的 `rm` 則仍移入垃圾桶。`pkg_manage` 已移除（移除於 v1.3.2）：系統套件改走 `run_command`，Linux 上用 `['sudo', <套件管理器>, ...]`，macOS 上用 `brew` 並設定 `sandbox: false`（`brew` 絕不搭配 `sudo`）。

`run_command` 的 sandbox 約束的是寫入而非讀取，因此指令只有在作業系統本身拒絕時才會在某路徑失敗。檔案工具（`read_files`、`find_files`、`open_file`、`file_history`）讀取 `$HOME` 以外的路徑前仍會跳出需要密碼的確認（v1.3.10 起 `read_files` 與 `find_files` 讀取可讀清單中的路徑不跳確認），讀取敏感路徑前則跳出一般確認，`denied_path` 則讀寫一律拒絕。已核准的路徑以 session 為單位存在 daemon 記憶體中，授權持續到 daemon 重啟為止。

## 永久拒絕

`config.json` 的 `denied_path` 是硬性拒絕清單：這些路徑讀寫皆不可，任何提示都無法核准，重試同一路徑會以相同方式失敗。項目必須是絕對路徑或以 `~/` 開頭；檔案系統根目錄會被拒收。`denied_command` 是其指令側的對應——清單上的 binary 一律拒絕，`sh -c` 內亦然。

## 敏感檔案防護

符合敏感集合的檔案——SSH keys、`.pem`、`.key`、`.env` 與憑證檔——讀寫皆受限：`read_files`、`edit_file`、`remove_file`、`restore_file`、`open_file`、`find_files`、`file_history` 在碰觸前都會升起確認——`$HOME` 內為一般確認，`$HOME` 外則需密碼驗證。自 v1.3.7 起此集合另涵蓋 `.git` 與 `.claude` 目錄、shell 啟動檔（`.bashrc`、`.bash_profile`、`.profile`、`.zshrc`、`.zprofile`、`.zshenv`、`.zlogin`）與 `.mcp.json`，因此用 `read_files` 讀 `.git/config` 或 `~/.zshrc` 現在會詢問，argv 碰到其中任一項的唯讀白名單指令也不再命中。該集合為 `sensitive_path`（`dirs` / `files` / `prefixes` / `extensions`），v1.3.10 起內建於 go-pkg `filesystem`（先前為 `configs/jsons/sensitive_path.json`），並以 `config.json` 中的使用者項目擴充。此防護由 Go 強制，非基於 prompt。

## 密鑰遮蔽

v1.3.11 起，工具輸出中的密鑰在進入模型、log、action history 或 channel 之前即被遮蔽（`internal/utils/secret`，規則在 `configs/jsons/secret_patterns.json`）。偵測只靠格式比對——私鑰區塊、URL 內的憑證、`Bearer` / `Basic` / `token` 值、netrc 密碼，以及 AWS access key、GitHub、GitLab、Slack、Stripe、npm、Hugging Face token 與 LLM API key 等供應商格式——不交給 LLM 判斷是否該遮。

| 項目 | 說明 |
|---|---|
| 格式 | 前 3 字元 + `********` + 後 3 字元；短於 9 字元的值只留前 3 字元，3 字元以內整段變成 `********`。PEM 私鑰區塊整段算一個值，不逐行遮蔽 |
| 工具結果與錯誤 | 在 `toolregister.Dispatch` 遮蔽，agent 迴圈與對外 MCP server 共用 |
| 串流進度 | 經 `secret.Stream` 逐 chunk 遮蔽（`runToolExec` 與 `run_command` 的 progress writer）；跨 chunk 的私鑰區塊整段丟棄 |
| 背景報告 | 存入前在 `FinishBackground` / `UpdateBackground` 遮蔽 |
| 寫回遮罩值 | `edit_file` 的 `content` / `new_string` 與 `run_command` 的 argv 含本 daemon 產生過的遮罩值時，以 `[Invalid]` 拒絕，避免遮罩值覆蓋真正的密鑰。只記遮罩後的字串，不記原文 |

需要金鑰的指令在同一個 `run_command` 內讀取並使用，例如 `['sh', '-c', 'set -a; . ./.env; set +a; curl ...']` 或 `security find-generic-password`，讓值不經過工具結果。
