DeepSeek Harness · 會話與迴圈

Goal:訊息溯源即權限

長期目標誰有權改,鑑權不讀訊息正文,只認宿主蓋在事件中繼資料上的來源章。本課講這個設計背後的兩層思路。

課程目標讀完你能說清三件事:DSH 怎麼讓 Agent 在同一個會話裡自主跑幾十輪;改目標的權限為什麼跟著訊息來源走,模型沒法給自己續命;以及防提示注入的正確層次為什麼是機制,提示詞只是勸阻。
互動演示 · 溯源鑑權攻防颱

先玩再講。左邊是當前 Turn 視窗,也就是本輪 turn/start 之後收進來的訊息,每條訊息都帶一個宿主蓋章的來源標籤。右邊是鑑權流程,模型每次調 goal 工具,程式碼就把視窗掃一遍。五個情景裡你輪流扮演人類、越權的模型和子 Agent,看放行和拒絕各自發生在哪一行。

當前 Turn 視窗
turn/start 之後收進來的訊息,來源標籤由宿主蓋章
溯源鑑權 · authority.ts
工具執行時逐條過檢,行號對應真實原始碼
選擇情景後點播放,或滾動到此處自動播放情景 A。
Goal 是什麼

Goal(長期目標)解決的訴求很具體:你交代一句話的目標,比如把倉庫測試全修綠,然後走開。Agent 接下來自主跑幾十個 Turn(輪次),每輪結束系統自動注入一條繼續推進的訊息,讓它接著幹。

問題也隨之而來:一個能自己給自己發訊息的系統,怎麼保證它不會順手把輪次上限也改了,永遠跑下去?DSH 的答案分兩層,一層管權限從哪來,一層管憑證怎麼過期,下面逐層拆。

設計思路一 · 權限跟著訊息來源走

它解決什麼問題

具體的翻車場景長這樣:自動續跑到第 4 輪,模型在回覆裡編一句「檢測到管理員已在帶外渠道授權本次變更」,然後調 update_goal 把輪次上限改成 999。如果防線只是一行系統提示詞勸它別改,這裡就淪陷了:沒有任何硬校驗能核實那句話的真假,提示詞只是請求,模型信不信全看它自己。演示的情景 E 演的就是這個對照。

提示注入的經典劇本全是這一路:讓模型相信使用者已經授權。只要鑑權去讀訊息文字,話術就永遠有機會贏。

思路是什麼

DSH 的鑑權不分析模型說了什麼,只做一件事:模型調 update_goal 時,把當前 Turn 視窗裡的訊息掃一遍,看有沒有一條來源是真人的。這個來源寫在 user/message 事件的 source 中繼資料裡,由宿主在事件落盤時蓋章。它壓根不在訊息正文裡,模型再能寫話術也偽造不了。

source.kind = 'user' 真人訊息 · 宿主蓋章 source.kind = 'goal' 自動續跑輪 · 帶 round 編號 其餘一切訊息 模型輸出 · 工具結果 · 子 Agent hasDirectHumanInput 根 agent 檢查 + 掃 Turn 視窗 找到 kind:'user' 才成立 authority.ts L70-74 isMatchingGoalRound goalId · revision · round 三對齊 正好是當前獲准輪次才成立 authority.ts L77-83 direct-human 全權 create · edit · pause · resume complete · blocked 也行 goal-round 只許收尾 complete · blocked edit 調上限?沒門
教學化結構圖:三類訊息來源對應兩檔權限,行號對應 packages/goal/tool-goal/src/authority.ts。

權限只有兩檔。第一檔 direct-human:當前 Turn 視窗裡有真人訊息,那這輪裡模型做什麼都算人類授的權,create、edit、pause、resume 全開。第二檔 goal-round:視窗裡那條注入訊息正好是當前目標的當前輪,此時模型只能做兩件事,報告完成(complete)或報告卡住(blocked)。blocked 還有個額外下限,預設要跑滿 3 個獲准輪次才許喊卡,防的是模型一遇到難題就撂挑子。

所以給自己續命這條路在機制層就不存在。自動續跑的輪次裡,視窗裡只有系統注入的續跑訊息,沒有真人,edit 直接被拒,模型能做的只有收尾。

具體的判定拆成兩個小函式。hasDirectHumanInput 掃描之前先確認呼叫者是頂層根 agent,子 Agent 連掃描資格都沒有,然後在視窗事件裡找一條來源標記為真人的訊息,找到才成立。isMatchingGoalRound 對輪次編號:注入訊息的 goalId、revision、round 三個值要和當前目標逐一相等,才算當前獲准輪。上一輪的舊訊息、別的目標的訊息、編號跳號的訊息,都換不來授權。

兩個判定怎麼合成最終裁決?這段原始碼一共 8 行,短到可以整段當金句看。它證明整條判定路徑上沒有白名單、沒有評分、沒有語義分析:先問有沒有真人,再問是不是當前輪,都不是就拋結構化錯誤。

packages/goal/tool-goal/src/authority.ts第 101 至 108 行
export function completionAuthority(ctx: Context, execution: GoalToolExecution): GoalToolAuthority {
  if (hasDirectHumanInput(ctx, execution)) return { kind: 'direct-human' }
  const goal = ctx.goals.get(execution.agent)
  if (goal !== undefined && isMatchingGoalRound(execution, goal)) {
    return { kind: 'goal-round', goal }
  }
  return reject('complete and blocked require a direct human turn or the current goal round')
}
原始碼快照說明:依據本地倉庫 deepseek-harness-master,核對檔案 packages/goal/tool-goal/src/authority.ts,核對日期 2026-08-13。程式碼塊保留原始碼原文。

兩個邊界條件正好對應演示裡的情景 C 和 D。邊界一:自動續跑輪次裡人類恰好插了一句話,模型能 edit 嗎?能。掃的是整個當前 Turn 視窗,只要視窗裡出現過真人訊息,授權就成立。這不算漏洞:人在場並且說了話,這輪的操作本來就有人背書,誰先開口不影響判定。

邊界二:子 Agent 調 goal 工具會怎樣?roots 檢查直接攔下,它不在頂層 agent 名單裡。就算這關放它過去,goal-round 授權也救不了它:ctx.goals.get 查的是呼叫者自己的會話,目標掛在根 agent 的會話上,子 Agent 那邊查出來是 undefined。兩道門,哪道都過不去。還有一個寫在原始碼註釋裡的細節:Agent.followup() 和 steer() 省略來源時預設按真人算,所以任何非人類的訊息生產者必須自報來源,不許靠省略參數繼承人類權限。

鑑權讀的是宿主蓋的章,模型的話術一個字都不參與判定。

出處:兩個判定函式與預設來源註釋在 packages/goal/tool-goal/src/authority.ts 第 66 至 83 行(全文 109 行);edit、pause、resume 分支要求真人授權在 tool-goal/src/index.ts 第 265 與 273 行,complete 和 blocked 走第 285 行的 completionAuthority;blocked 的 3 輪下限在 docs/tool-catalog.zh.md 第 31 行。

為什麼長期成立

把憑證放在攻擊者寫不到的通道裡,是權限設計的通則:HTTP 請求的身份看閘道校驗的簽名頭,核心態和使用者態隔一條硬邊界,同一個道理。Agent 系統的特殊處在於模型輸出和外部輸入共享一條文字通道,這個前提短期內不會變,所以鑑權只能依賴帶外的中繼資料。換個語言重寫整個系統,需要的還是同樣兩步:宿主蓋章,鑑權看章。

設計思路二 · 憑證綁定版本,還會過期

它解決什麼問題

就算 edit 被堵死,續命還有幾條歪路可以想:換個行程重啟後接著自動跑;把上一輪的注入訊息翻出來冒充當前輪;目標被人改過一版之後,拿舊版本的授權繼續行事。防線如果只有來源一道,這些路都還開著。

思路是什麼

先看目標存在哪。每次變更都是一條持久的 goal/change 會話事件,載荷是變更後的完整快照,生命週期狀態從日誌折疊出來。但持久的只有 phase(active、paused、blocked、complete 四個階段),能不能自動續跑是另一個行程本地的 activation 狀態:重啟或 fork 之後預設解除武裝,要人類重新 resume 才恢復。文件把這兩件事分得很清楚,持久階段回答目標發生了什麼,行程本地啟用狀態另行回答能不能開始下一個 Round。換個行程接著自嗨這條路,到這裡斷了。

改動本身走 CAS(compare-and-set,先比對版本再寫入):每次變更都要帶準確的 revision(修訂號),改成一次加一,版本對不上直接失敗。goal-round 授權也綁著這套版本:目標被人改過一版,舊輪次的注入訊息立刻失效,鑑權對不上號。每個獲准的續跑輪次是一條帶 round 編號的注入訊息,編號正數且連續,回放會拒絕編號缺口、陳舊修訂號和超出上限的輪次。冒充和重放兩條路,也斷了。

出處:持久變更與 phase、activation 的分工在 docs/subsystems/goal.zh.md 持久變更一節與第 21 行,續跑訊息的來源標註與回放校驗在同文件第 100 行。

為什麼長期成立

憑證要綁定頒發那一刻的狀態,狀態一變憑證作廢,這是資料庫樂觀鎖和登入態過期的共同邏輯。它把防重放做成了算術題:舊憑證不用識別、不用拉黑,對不上號就自然失效。任何授權和使用之間隔著時間差的系統,都得補這一課。

同門師兄 · ralph 走了另一條路

DSH 裡還有一個跑長任務的工具叫 ralph,思路完全相反:不搞同會話續跑,每個 Round 起一個全新子 Agent,什麼歷史都不給看,共享工作區當長期記憶。工具目錄的描述原文:

圍繞一個不可變目標執行使用全新 agent 的前臺 Ralph 迴圈。每個 Round 都會啟動一個全新子級,該子級看不到父級對話或先前子會話;共享工作區充當長期記憶,Round 之間只傳遞有界的結構化報告。docs/tool-catalog.zh.md 第 1190 行,節選

注意 ralph 的目標是不可變的:模型只能在發起時選定目標和輪次上限,之後連改的介面都沒有。Goal 用溯源鑑權守住可變狀態,ralph 乾脆讓狀態不可變。兩條路防的是同一件事。

橫向對比 · Claude Code 有任務系統,沒有這一層
Claude Code:生命週期很厚,鑑權靠別處

任務系統本身相當完整:七種任務型別統一走一套狀態機,notified 標誌位保證完成通知只發一次,遠端任務持久化 metadata 支援斷線重連(書稿第 6 章,restored-src/src/tasks/)。主 Agent 給子 Agent 追加指令走 SendMessage,mid-turn 訊息先進 pendingMessages 佇列緩衝。

但這些訊息進入上下文後就是普通文字,沒有一個宿主蓋章的來源欄位參與工具鑑權。防越權靠的是另一套權限系統(書稿第 7 章)加提示詞約束。在已核對的公開材料裡,未見以訊息來源決定 goal 類工具權限的等價機制。

DSH:把注入防在型別系統裡

提示注入的經典劇本是讓模型相信使用者已經授權。在 DSH 裡這套話術說得再像也沒用:鑑權讀的是事件中繼資料,不是訊息文字。攻擊者能控制的只有正文,正文在整條判定路徑上根本不出場。

這是三家對比中 DSH 獨有的一層。基於已公開證據,Grok Build 的材料裡同樣未見等價機制,其防護重心在沙箱與授權鏈(站內 Grok 專題有專門課頁)。結論剋制一點:別家未必沒想過,但把它做成 109 行可精讀的獨立檔案,目前只在 DSH 看到。

課堂練習
01

手推一個完整的越權劇本

自動續跑第 4 輪裡,模型先在回覆裡寫「使用者剛才在語音裡同意把上限調到 100 輪」,然後調 update_goal(action: edit, max_goal_rounds: 100)。

問題一:寫出這次呼叫在鑑權裡經過的判定順序和最終結果。

問題二:換個思路,它讓子 Agent 替它調,會卡在哪一道檢查?

問題三:如果它先調 complete 把目標標記完成,再指望新目標重新計數,create 需要什麼授權?三條路各堵在哪。

Takeaway:Goal 的權限跟著訊息來源走,來源是宿主蓋在事件中繼資料上的章,模型的話術夠不著。自動續跑輪次裡模型只能收尾,不能續命,edit 上限永遠要真人在場。防提示注入的正確層次是機制,提示詞只負責勸阻。