nono:要讓 agent 打 API 又不讓它握著 key,假 token 解法自己踩到的三個 issue
要讓 agent 去打 API,又不想讓它手上握著那把 key,怎麼做?
最順手的答案是環境變數:key 放在 shell 裡,啟動 agent 時帶進去。問題是 agent 讀得到自己的環境變數,它跑的任何一個子行程也讀得到。我之前寫過 IETF 草案怎麼把這種做法判成反模式(那篇在這裡),這篇換個角度:有一個開源工具真的動手解了這題,解完之後自己踩到什麼。
工具叫 nono,repo 是 nolabs-ai/nono,Rust 寫的,Apache-2.0。截至 10 月 22 日,我用 gh api 查到它有 4,608 顆星、317 個 fork,最新版是 10 月 13 日的 v0.80.0。repo 建立在 2026-01-31,README 自己寫著「In the lead-up to a 1.0 release, APIs are stabilizing. API changes may still occur where necessary, but will be kept to a minimum.」,所以還沒到 1.0。
先講我沒做的事。我只讀了文件與 issue 標題、內文,沒有實際把 nono 裝起來跑過,也沒有讀它的原始碼。下面凡是機制,都是文件怎麼說;凡是壞掉的地方,都是 issue 作者怎麼說。
環境變數那條路,文件自己先承認了
nono 的憑證注入頁把做法分成兩種。一種是 --env-credential,文件原文說它「Simpler, but the secret is visible in the process environment.」另一種是 --credential,它推薦拿來放 LLM 的 API key:
Proxy injection (
--credential) - The agent talks to a local reverse proxy that injects real API keys on the fly. The credential never enters the sandbox, not even as an environment variable.
流程是這樣的:沙箱裡的 agent 把請求送到本機 127.0.0.1 的代理,代理去掉路徑前綴,把真正的 key 塞進 HTTP header,再用 TLS 轉給上游。文件舉的例子是 agent 送 POST http://127.0.0.1:PORT/openai/v1/chat/completions,代理轉成對 https://api.openai.com/v1/chat/completions 的請求,Authorization: Bearer sk-... 是代理從 keystore 拿出來注入的。為了讓 SDK 自己繞進代理,nono 會在子行程環境設 OPENAI_BASE_URL=http://127.0.0.1:<port>/openai,文件說大多數 LLM SDK 看到這個變數就會自動改道。
那 agent 手上剩什麼?一張佔位用的假 token。文件把這叫 phantom token(這是 nono 自己的用詞,出處是它的 credential-injection 文件):agent 在請求裡帶 NONO_PROXY_TOKEN 當佔位,代理驗證通過才換成真 key,缺了或不對就回 401。要注意,這一段文件寫的是 url_path 與 query_param 兩種注入模式,其他模式是不是同一套機制,我沒有逐一對過。
這個設計我站它。agent 被提示注入拐走的那一天,攻擊者從 agent 的環境或記憶體裡挖,挖到的只是一張假券。文件也是這樣宣稱的:「Even if the agent is compromised, it cannot extract credentials from its own environment or memory.」不過這句是宣稱,我沒有驗證。我會改變看法的條件很具體:只要有人拿出實際重現,證明沙箱內的行程能從代理或 keystore 摸到真 key,這個「key 不進沙箱」的賣點就得打折。
解法落地之後,卡的地方在 OAuth
假 token 加代理,對「貼一把 API key」的用法很乾淨。但 Claude Code 有一類用法不是貼 key,是用瀏覽器登入的訂閱帳號,那種登入的 token 不是一把固定的 key。nono 的 issue 列表裡有三筆,我覺得值得逐筆看。我沒有讀這三筆的留言串,也沒讀對應的 PR,所以只寫 issue 開出來時的描述,與我取資料時的狀態(2026-10-22 上午)。
#1950,沙箱裡的 Claude Code 說自己沒登入。 2026-09-19 開的,nono 0.78.0、macOS 26.6.2。作者貼出指令 nono run -p claude-code --allow-cwd --allow . -- claude --dangerously-skip-permissions,能力清單裡有 r+w ~/Library/Keychains。進到 Claude Code v2.1.278 輸入 hi,得到 “Not logged in · Please run /login”。作者附了對照組:同一台機器不經 nono 直接跑 claude,只看到「Your login expires in 2 days」的提醒,然後正常對話。這個 issue 現在是 closed。v0.80.0 的 release notes 有一行寫 “fix(cli): keep Claude Code on its default Keychain service (#1950)”,PR 編號是 2085。這一行和 issue 之間的因果細節我沒讀 PR 內文,所以不替它下根因的結論。
#1622,跑了幾個小時之後 401。 2026-08-11 開的,現在也是 closed。內文說多位使用者回報,Claude Code 在 nono 底下無人看管幾個小時後,出現 401 OAuth access token has expired,而且只發生在瀏覽器登入的訂閱帳號上,API key 的用法沒有。其中一個案例是好幾個平行的背景 agent 同時掛掉,掛完立刻在沙箱外直接跑 claude,卻顯示「Your login expires in 1 day」,也就是 token 其實沒過期。作者對此提了一個假設:nono 可能把一份舊的、快取的憑證一直餵給子行程,沒有重新去讀 Claude Code 已經刷新在磁碟上的那份。這是 issue 作者的假設,不是確認過的根因,我沒有查它後來怎麼收場。
#1831,一條「只准讀」的白名單,把憑證路由蓋掉了。 2026-09-09 開的,我取資料時還是 open。標題說 allow_domain 的 endpoint 規則會遮住 oauth_capture 的憑證路由:同一個 host 如果兩邊都有設定,憑證路由不會被選中,假 token 就原封不動轉到上游,上游回 401。觸發的範例是 { "domain": "*", "endpoints": [{ "method": "GET", "path": "/**" }] }。症狀的形狀很怪:萬用字元沒列到的方法不受影響,所以 POST /v1/messages 照常通過,但 Claude Code 所有的 GET 都 401,畫面上看到的是 MCP connector 載不進來。作者在 issue 裡還貼了他讀原始碼的分析。我沒讀原始碼,不轉述那段,也不判斷它對不對。
Methods the wildcard does not list are unaffected, so
POST /v1/messagesstill works while every GET the client makes 401s.
這句是 #1831 的原文,我覺得整串紀錄裡最難纏的就是它:POST 能用、GET 全壞,你從「Claude 能回話」這個表面訊號,根本看不出哪裡錯了。
三筆擺在一起,我自己有個歸納,標明是推測:代理注入對「靜態 key」很乾淨,碰到會自己刷新的 OAuth token,就多了一個「代理手上那份」與「CLI 自己維護那份」要對齊的問題。#1622、#1831、#1950 各自只涵蓋這個推測的一小塊,三筆合起來也證明不了它,所以我把它當成讀 issue 時的一個問法,不當結論。
時間線
nono 這幾筆紀錄的先後
2026-01-31
repo 建立。
2026-08-11
#1622 開出,長時間無人看管後出現 401。2026-10-22 查到時為 closed。
2026-09-09
#1831 開出,allow_domain 規則遮住憑證路由。2026-10-22 查到時為 open。
2026-09-19
#1950 開出,沙箱內 Claude Code 顯示 Not logged in。2026-10-22 查到時為 closed。
2026-10-13
v0.80.0 發佈,release notes 有一行提到 #1950(PR 2085)。
2026-10-21
repo 最近一次 push。
從 #1950 開出(9 月 19 日)到 v0.80.0(10 月 13 日)是 24 天。維護節奏我只看到發版頻率:v0.76.0 到 v0.80.0 五個版本,從 9 月 9 日排到 10 月 13 日,間隔從兩天到兩週不等。
回到最初那個問題
如果今天要我回答開頭那題,我會這樣分:用 API key 的場景,代理加假 token 的方向看起來是對的,文件的流程也講得通;用訂閱帳號的 OAuth 登入,先預期要踩坑,而且至少有一個坑(#1831)在我查的當下還沒關。
我自己會先做的事很小:把你要給 agent 的網路規則寫得越窄越好,別一上來就丟一條全域萬用字元;再拿一個會用到 GET 與 POST 的真實流程,兩種方法都走一遍,只看到「能回話」就收工,是 #1831 作者描述的那種壞法最容易溜過去的地方。這是我讀完 issue 後的推演,我自己沒有照做過。
README 還有幾個我沒看懂也沒驗證的地方:它說自己 “zero latency”,我沒看到任何量測;它說是 “Built by the team that brought you Sigstore”,我沒查證;v0.79.0 的 release notes 列了一則 GHSA 安全公告,我只看到編號,沒打開讀內容。
key 不進沙箱這件事,nono 給的做法是把真東西留在門外、只放一張假券進去。假券要在每個路口都被認得,這才是這個設計真正在賭的地方,而我讀的三筆 issue,症狀都落在被拒絕或認不得這一邊:未登入、401、401。










