InsForge 02:當操作者是 agent,權限、錯誤與文件要怎麼重新設計
「The agent’s intelligence was never the bottleneck. Its context was.」(Tony Chang,CTO,〈Three Levels of Trust in AI Coding Agents〉,2026-07)
上一篇從架構看 InsForge:後端的第一操作者換成 coding agent 之後,狀態、錯誤、設定、權限的設計都跟著改。這篇往下走一層,看一個真的相信「瓶頸在 context」的團隊,會在後端留下什麼設計。
我挑了權限、錯誤、文件三件事。每一件都從同一個角度看:agent 當操作者會遇到什麼問題,InsForge 做了什麼決定,以及為什麼這樣決定。
讀完這篇,你會理解:
- 權限:為什麼 InsForge 讓 agent 在自己的 schema 裡全權操作,邊界卻交給資料庫的角色來守
- 錯誤:為什麼錯誤回傳要多寫一句給 agent 讀的下一步,這句話又是怎麼長出來的
- 文件:為什麼文件綁住部署版本還不夠,還要讓 agent 能回報文件寫錯的地方
MCP 和 CLI 都只是 adapter,規則只寫在一處
agent 碰得到後端的入口有兩個,MCP 和 CLI。入口一多,最怕的是規則跟著分散:同一件事從 MCP 進來會被擋,從 CLI 進來卻不會。
InsForge 的決定是讓兩個入口都只當 adapter。拿 agent 建一張 orders 表來看:InsForge 沒有「建表」這個 tool,agent 先照官方流程讀一次 metadata,確認還沒有這張表,再送一段 CREATE TABLE 的 SQL。MCP 那一側負責執行 SQL 的 tool,拆開來就是一次 fetch,帶著 admin key 打一個 REST endpoint;CLI 的 db query 打的也是同一個 endpoint。
兩個 adapter 在進後端的第一關就匯流。route 先驗身分、再驗參數,最後留一筆 audit,記下是誰執行、SQL 的開頭、影響幾列。參數的格式定義只有一份,MCP、CLI 和後端 route 三處共用。驗證、audit、護欄都寫在 adapter 後面那一層,只寫一次。
這樣決定的理由很直接:adapter 裡沒有邏輯,就不會有哪條路漏掉檢查。之後要加第三個入口,也只是再寫一個轉接,規則不用跟著複製一份。
REST 後面接著三種東西
agent 只打 InsForge 自己的 REST API,從來不直接碰資料庫或任何 vendor。REST 後面的 service 層,再往下接三種東西:
- Postgres。 資料庫的基礎操作,例如執行 SQL、建表、讀 metadata,都由 service 用 pg 驅動直接連 Postgres。這裡沒有 provider,Postgres 是平台本體,不是可以替換的零件。
- PostgREST。 app 在 SDK 裡查詢或寫入資料表,例如查 orders 的所有訂單,會轉給 PostgREST。它把每張表自動變成一組 REST API,InsForge 不用替每張表寫 handler。
- Provider adapter。 需要外部服務的能力,例如物件儲存、容器運算、寄信、模型閘道、前端部署,各自經過一個 provider,接到 S3、Fly、Docker、SMTP、OpenRouter、Vercel 這些 vendor。
建 orders 表那條路,就是從 agent 一路走到 Postgres 那一格。
換一個 storage,agent 那端什麼都不用改
拿一個具體的變更來看:如果要讓 InsForge 支援一種新的儲存後端,例如一個不走 S3 協定的儲存服務,各層要動的地方是這樣:
| 層 | 要不要動 | 做什麼 |
|---|---|---|
| provider | 要動 | 新寫一個 provider,實作 storage 的介面,包含上傳、讀取、建立與刪除 bucket 這類方法。 |
| service | 動一行 | 選擇 provider 的邏輯多加一個分支。 |
| route | 不動 | 建 bucket 的 endpoint 照舊。 |
| MCP tool 與 CLI 命令 | 不動 | 建 bucket 的 tool 打同一個 endpoint,agent 不知道底下換了。 |
| 文件與 skills | 不動 | agent 學過的用法照舊有效。 |
agent 那一端不用註冊任何新東西。
如果要加的是一個全新的能力,情況就完全不同。假設 InsForge 本來沒有 queue,要加上去,就得從 route、service 一路寫到 MCP tool、CLI 命令、文件與 skills,每一層都要動。InsForge 在 2026 年加上 Compute、Sites、Payments,走的就是這條路。
分層換到的,和付出的
這樣分層換到四件事:
- 介面穩定。 換 vendor 只動 provider,agent 呼叫的 tool 和命令都不變。
- 護欄只寫一處。 驗身分、audit、SQL guard、換身分執行都在 REST 後面,兩個入口都會經過。
- 給 agent 的語意也只加一處。 metadata 把六個 service 的狀態彙整成一次回傳,錯誤在 middleware 統一翻成下一步。這些都掛在 service 和錯誤處理那一層,provider 不需要知道 agent 存在。
- 同一套 code 可以跑在不同環境。 InsForge 自己的雲和使用者自架的版本共用同一份程式,差異收在選哪個 provider。
代價是 InsForge 必須替每一個能力,親手寫一遍給 agent 用的操作介面。從 route、service、錯誤提示到 metadata,整合層本身就是它的產品,光後端就有約 72,000 行、24 個 route 模組。工這麼重,就不可能什麼都接。所以 InsForge 只用 Postgres,每個能力通常也只接一家 vendor:模型閘道只接 OpenRouter,前端部署只接 Vercel。
換來的是一個 agent 學一次就能用到底的後端,付出的是平台必須自己扛下每一個整合。這個取捨下一篇會再從另一個角度談。
權限邊界用身分來畫,不靠猜哪句 SQL 危險
agent 要能全權操作自己的後端:建表、改欄位、刪掉不要的表,都是日常工作。但它不能拿到整台資料庫,不能動到 Postgres 的權限設定,也不能碰平台自己的系統表。
一種做法是列出危險的語句,一句一句擋。InsForge 選的是另一條路:每次執行前,先把連線換成 project_admin 這個角色,做完再切回來。agent 能做什麼、不能做什麼,交給資料庫的角色權限去判斷。
async executeRawSQL(query, params) {
const sql = this.sanitizeQuery(query); // 擋逃逸路徑
const client = await pool.connect(); // 連線池用的是 superuser
const result = await withAdminContext(client, // SET ROLE project_admin
() => client.query(sql, params)); // 執行完一定 RESET ROLE
if (isDDL(sql)) {
await client.query(`NOTIFY pgrst, 'reload schema'`);
}
return result;
}
連線池裡的連線本身是 superuser,但 agent 的 SQL 從來不是用這個身分執行的。它建出來的表,owner 是 project_admin;它想動 project_admin 權限以外的東西,資料庫會直接拒絕。執行完切回原本的角色,是為了不讓帶著別人身分的連線回到池裡。最後那行通知是讓 PostgREST 重新載入 schema,新表才會出現在 REST API 上。
開頭那道 guard 只負責一件事:擋住會讓身分這條線失效的語句,例如自己切換 role、改 search_path、自己控制交易。其餘的 SQL,包括 DROP TABLE,都交給身分去判斷。
為什麼這樣畫?危險的語句列不完,SQL 的寫法變化太多;但「這個角色能碰什麼」是資料庫本來就會嚴格執行的規則。把邊界交給身分,平台只需要守住「不准換身分」這一條。
錯誤要寫給一個會重試的程式讀
人看到錯誤會停下來想,agent 則傾向直接再試一次。錯誤訊息如果只描述發生了什麼,agent 就只能猜下一步。
InsForge 的決定是在錯誤回傳裡多放一個欄位,直接寫出下一步:
{
"error": "DATABASE_DUPLICATE",
"message": "duplicate key value violates unique constraint \"users_email_key\"",
"statusCode": 409,
"nextActions": "A record with this field(email) already exists. You can query the existing record by using the query tool and then try again."
}
前三個欄位是傳統的錯誤格式,給程式比對用。第四個 nextActions 是寫給 agent 讀的:email 重複了,就先去查已經存在的那一筆。一般 API 不寫這句,因為人會自己想到;InsForge 把這段推理直接放進回傳。
回到 orders 這張表,一個典型回合大概是這樣(依 code 推演的示意):
| 回合 | agent 做了什麼 | 後端回了什麼 |
|---|---|---|
| 1 | agent 直接插入一筆訂單。 | 後端說表不存在,並提示先用 metadata 查表。 |
| 2 | agent 照提示讀 metadata。 | metadata 顯示沒有 orders,但有 users。 |
| 3 | agent 建表後再插入一次。 | 後端說外鍵對不上,並提示去查被參照的那筆資料。 |
| 4 | agent 查 users,發現使用者不存在。 | agent 先建使用者,再插入訂單,這次成功。 |
沒有這個欄位,agent 在第 1 步可能盲目重試、懷疑自己把表名拼錯,或跑去翻文件。有了它,每個錯誤都指向一個明確的下一步。資料庫的錯誤不必一個個手寫,Postgres 的錯誤碼是標準的,InsForge 用一張映射表把常見的錯誤碼自動翻成下一步。
這個欄位不是看到 agent 一再失敗才補上的。2025-08-02,repo 裡第一個有後端程式的 commit,錯誤格式就已經有它,比正式上線早了三個月。官方在〈How We Made Engineering Agent Native〉裡這樣描述:
「Critical-path errors return a
nextActionhint so the agent can route itself to the right next step instead of stalling on a stack trace.」
關鍵路徑上的錯誤要附一句提示,讓 agent 自己找到下一步,不要卡在 stack trace 前面。
之後目錄慢慢長到 21 條,新加的條目都對應一次真實的卡死。compute 的機器被平台回收後,資料庫還記著它,後續呼叫就一直失敗,commit 訊息把這個狀況寫成「opaque 5xxs forever」,修法是偵測到這種情況就回一句「重新部署這個服務」。email 驗證開著、卻沒設定寄信服務時,註冊會卡住,修法是直接告訴 agent 先設定 SMTP 再重試。兩次修正都同時加了 error code 和測試。
為什麼從第一天就這樣做?因為 agent 是第一讀者。錯誤格式一開始就預留這個欄位,每寫一個新功能,工程師都會被問「這個錯誤的下一步是什麼」;每修一個讓 agent 卡住的 bug,也會順手補上那句話。
文件會過期,所以讓 agent 自己回報
agent 寫錯 code,很常是因為照著訓練資料裡的舊 API 寫。InsForge 一開始的解法是讓文件跟著部署版本走:文件由 server 端回傳,內容就是那台 instance 正在跑的版本,並規定 agent 寫 code 前一定要先拉文件。後來給 agent 的文件從一份 131 行的說明,長成一套約 12,000 行、隨 CLI 安裝的 skills。
但綁住版本只解決一半的問題。它保證 agent 拿到的是當前版本的文件,卻保證不了文件本身寫得對。文件可能漏寫一個步驟,可能跟實際行為不一致,也可能根本沒寫到 agent 碰上的狀況。
InsForge 的決定是讓 agent 自己回報。skill 裡規定,agent 碰到 InsForge 自己的問題時,要用 feedback 命令回報,附上它用的繞道方法,然後繼續完成使用者的任務。官方說這類回報「often becomes the doc fix」,常常直接變成文件的修正。
這些 skills 本身也寫滿了只有 agent 需要的規則。例如 CLI 的第一條規則:
「Keep npx’s
-y: without it, npx asks “Ok to proceed?” before installing the package and blocks forever in a TTY-attached agent shell.」(insforge-skills,insforge-cli SKILL.md)
人看到提示會按 y,agent 的 shell 則會永遠卡在那裡。這種坑,只有真的看著 agent 卡住的人才寫得出來。
為什麼要讓 agent 回報?因為 agent 是最常、也最早撞到文件錯誤的使用者。它每天照著文件做事,文件跟現實一有落差,它最先發現。給它一條回報的路,agent 就從文件的讀者,變成文件的修正來源。
設計啟示
- 入口是 adapter,規則只寫一處,代價是平台要替每個能力寫一遍整合。 驗證、紀錄與護欄放在所有入口後面共用的那一層,任何一條路都漏不掉;但每多一個能力,平台就多扛一份從介面到錯誤提示的工。
- 權限用身分來畫,不用語句來猜。 危險的語句列不完,角色權限卻是資料庫本來就會嚴格執行的規則。平台只要守住「不准換身分」,其餘交給身分判斷。
- 錯誤的讀者是一個會重試的程式。 把下一步寫進錯誤回傳,並讓它成為每個新功能都要回答的題目,agent 的重試才會往對的方向走。
- 文件要能被 agent 回報修正。 綁住版本只保證拿到的是當前文件;讓最常讀文件的 agent 能回報落差,文件才跟得上現實。
下一篇會退一步看這些設計背後的取捨:InsForge 放棄了什麼,模型越來越強之後這類「橋」還有沒有價值,以及公司為什麼一路從後端走向運算地基。