dsh-scratch-vault
把操作腳本的寫入導出工作區、改放各 session 的暫存根,並把每一支收進工作區外的可檢視收納庫,讓工作區只留下真正的交付物。
安装
dsh plugin --profile web add dsh-scratch-vault
沙箱测试
这个插件还没测过。提交测试 →
README
dsh-scratch-vault
讓 DeepSeek Harness 以 Codex 的方式執行操作腳本——腳本跑在工作區之外,並被插件收納在內部。
[English](README.md) | 中文
長程任務裡那些「只跑一次」的探針、轉換器、診斷腳本,不再落到你的倉庫裡:它們從一個位於工作區之外的 session scratch 根執行,並且每一份都被歸檔進插件自有的收納庫,可以列出、檢視、促升、清理。
之前 之後 repo/ repo/ ├── src/ ├── src/ ├── _probe_models.py ← 雜訊 ├── package.json ├── _recon_api.py ← 雜訊 └──(只剩交付物) ├── tmp_convert.mjs ← 雜訊 ├── apply-paths.ps1 ← 雜訊 vault/ └── package.json └── entries/20261012…-a1b2c3/script
為什麼需要它
Agent 執行長程任務會不斷產生中間檔。檔案本身在磁碟上不花錢,真正的代價是:之後每一次 glob、grep、列目錄,都要在這些檔案裡翻找一遍,把無關內容推進上下文,逼 agent 重新理解它自己剛做過的事。
DSH 其實已經有了大半機制。它的 workspace-write 沙箱允許寫入工作區根目錄或平台臨時目錄(writableRoots 回傳工作區根、/tmp、os.tmpdir()),而工作區變更摘要本來就排除臨時根(isTemporaryPath——註解原文是 "scratch paths that never enter the change summary")。缺的是兩件事:把草稿導向那裡的機制,以及把它記下來的帳。
本插件就是這兩件事。
安裝
dsh plugin --profile desktop add github:TINGOD2006/dsh-scratch-vault
裝完重啟 DSH。從本機檢出安裝:
dsh plugin --profile desktop add link:C:/path/to/dsh-scratch-vault
Windows 路徑務必用正斜線,否則 pnpm 會把磁碟代號拼進 profile 目錄,插件裝上卻不啟用。
需求:Node ≥ 22,以及一個有掛載 tools 服務的 DSH。本插件完全不宣告任何依賴——連 @deepseek-ai/dsh-tools 都沒有。以 link: 安裝的外掛,其 bare specifier 是按真實路徑往上解析的,永遠到不了 profile 的 node_modules;只要有一個宿主 import,整個插件就會以一句 failed to import 收場。因此工具定義在 lib/tools.js 裡手工組出,形狀與 defineTool 的產物完全相同:不必安裝任何東西,也沒有需要同步的版本。
改動 lib/ 底下任何檔案後,都必須完整重啟 DSH。載入器在開機時建好模組圖,並以解析後的 URL 為 key 快取每個模組;停用再啟用只會重跑快取模組的 apply(),不會重新 import 檔案。
它做什麼
L2 —— 紀律、環境、內聯執行
- 一段系統提示詞:告訴模型操作腳本該去 scratch 根,不是工作區。
$DSH_SCRATCH與$DSH_VAULT:透過ctx.shellEnv注入每一次 shell 呼叫。該命名空間每次呼叫都重建,所以值永遠是當前 session 自己的目錄。run_script({ language, code, description }):「不寫檔就能跑」的那個工具。程式被寫到工作區外的 scratch 檔,並經ctx.shell執行,因此跑在 session 的沙箱策略之下。這才是「我只是想跑點東西」的正解。
L3 —— 攔截
tools/execute 是唯一環繞派發的接縫,因此也是唯一能改變工具本體收到什麼的接縫:註冊表把可變的 execution 傳進瀑布,而 dispatchToolBody 之後才讀 exec.arguments。(tools/pre-execute 做不到——它的決策型別是 allow | deny | cancel | ask,沒有放引數的地方。)
當 write 或 edit 指向工作區內的一個草稿腳本時,它的 file_path 會被改寫到 session scratch 根,工具結果則告訴模型真實的絕對路徑與收納庫條目 id。引用該腳本的 shell 命令也會一併改寫。
兩個性質保證這件事安全:
- 映射是路徑的純函式(
scratchPathFor)。DSH 以平行群組派發工具呼叫,write與緊接執行它的pwsh可能同時在飛;任何有順序依賴的作法都會競態。 - 只有當工作區檔案不存在時才改寫 shell token。 真的躺在磁碟上的歷史殘留,照原樣從磁碟執行。
shell 改寫刻意保守,因為漏改一次只是少收一個檔,誤改一次會弄壞一條本來能跑的命令。以下情形一律不動:緊鄰 glob 萬用字元的 token、更長檔名的前綴(x.py.bak)、shell 變數參照的一部分($env:X\a.py、$DSH_SCRATCH/a.py、%TEMP%\a.py),以及相對路徑而它的工作目錄無法靜態得知(cd $env:WORK、cd %WORK%、裸 cd 回到家目錄、cd ~/proj)。
出現在 cd/Set-Location/pushd 之後的相對 token,會依那個目錄切換實際把 shell 帶到的位置解析,而不是一律當成相對於工作區根目錄:cd <工作區> 之後 python _probe.py 是最常見的形狀,仍會被改寫成 write 產生的那個 scratch 路徑;而 cd …\lib 之後的 Get-ChildItem notes.js 指的是真實存在的檔案,會被放過。絕對路徑沒有歧義,一律改寫(磁碟機字母完整保留)。
改寫只發生在程式位置:直譯器/runner(python、node、pwsh、bash、ruby、lua…)的引數位置,或命令字本身(.\_probe.ps1)。出現在 # 註解、commit message、grep pattern 或單引號 payload 裡的檔名是資料——命令不是執行它,就是在另一個工作目錄下執行它——改寫這些地方只會弄壞本來能跑的命令,或把某台機器的暫存路徑烙進產出檔案。
L4 —— 收納庫
每一份被攔下的腳本都會帶 JSONL 帳本歸檔到 vault 目錄,並提供 scratch_list、scratch_show、scratch_promote(複製回工作區成為真正的交付物)、scratch_purge。
scratch_import 用來事後收攏:把插件還不存在時就已經躺在工作區裡的殘留收進 vault。它會逐檔比對收納副本的位元組,確認一致之後才刪除原檔(keepOriginal: true 保留原檔,dryRun: true 兩邊都不動)。這裡有兩道護欄:只收「能在 UTF-8 下逐位元組往返」的檔案——有損解碼會被存成 U+FFFD,而拿同一份解碼結果去比對永遠會過——以及絕不刪除分類器判定受保護的檔案,src/、node_modules/ 或 excludeGlobs 白名單裡的路徑會被回報為 refused 並原地不動。
所有對 vault 的寫入都發生在插件自己的行程裡,這點是結構性的:<vaultDir> 位於 DSH_HOME 之下,在工作區之外,因此也在沙箱可寫根之外——agent 透過 shell 執行的程式無法寫入 vault,沙箱內的任何工具也不行。收攏只可能在邊界的宿主這一側成立,而這些工具正好就跑在那一側。
帳本本身是 append-only;唯二必須整檔改寫的操作——定期清理與 maxVaultEntries 裁切——都先寫暫存檔再 rename 就位,所以改寫中途被打斷也不會截斷歷史。讀取由一份以帳本 stat 為鍵的記憶體快取供應;任何其他寫入者(包括共用同一個 vault 的另一個 DSH 視窗)都會改變那個簽章,下一次讀取就重新載入。ledger.lock 讓跨視窗的改寫互斥:鎖被持有時,清理會拒絕(busy: true)而不是先刪掉條目目錄、再讓另一個行程把那些條目重新寫回帳本;鎖會記下持有者的 pid,所以死行程留下的鎖會被搶,而只是慢、但還活著的持有者不會被硬搶。
有兩種失效模式是明確處理的,不靠運氣。讀不出來的帳本不會被當成空帳本:回報空、不快取、也拒絕改寫——從「讀不到」重寫出去,等於把空的內容蓋掉真實歷史。而被截斷的最後一行(append 途中斷電唯一能損壞的東西)會在下一次 append 前先補上換行,而不是讓兩筆記錄黏成一行無法解析的內容。
清理會在每次掛載時套用一次(autoPurge):保留期限是政策,不是待辦事項,收納庫不會再一直長大直到有人想起 scratch_purge。要完全手動就設 autoPurge: false。
設定
刻意不宣告 Cordis Config schema。 若 profile 解析不到 schema 庫的 peer 依賴,整個插件會掛載失敗——而一個載不起來的衛生插件,比一個沒有設定表單的插件更糟。設定改用純 JSON 檔:
<vaultDir>/scratch-vault.json # 預設 ~/.dsh/scratch-vault/scratch-vault.json $DSH_SCRATCH_VAULT_CONFIG # 或指向任意 JSON 檔
每個鍵都可省略,缺的走內建預設。
鍵 · 預設 · 說明
enabled · true · 總開關。
interceptMode · "redirect" · "redirect" 搬走寫入;"deny" 直接拒絕並說明替代作法;"report" 什麼都不改,只記錄「本來會被攔截」的檔案。
scratchRoot · "" → <os.tmpdir()>/dsh-scratch · 草稿檔所在。必須在工作區之外。
vaultDir · "" → <DSH_HOME>/scratch-vault · 收納庫目錄。
interceptWrites · true · 是否檢查 write/edit。
rewriteShell · true · shell 命令中的引用是否跟隨被改寫的寫入。
injectPrompt · true · 是否注入紀律段落。
exposeEnv · true · 是否注入 DSH_SCRATCH/DSH_VAULT。
registerRunScript · true · 是否提供 run_script。
interpreters / extensions · {python,node,powershell,bash} · 各語言的直譯器與草稿副檔名。
scriptExtensions · 27 項 · 哪些副檔名可能是草稿腳本。
scriptNamePatterns · 保守三條 · 標記草稿腳本的檔名 regex。
scratchDirNames · _scratch、tmp 等 · 這些目錄底下的一切都算草稿。
protectDirs · src、lib、test 等 · 絕不觸碰——專案真正的原始碼。
excludeGlobs · [] · 交付物白名單(工作區相對路徑)。
treatBareRootScriptsAsScratch · true · 無專案 manifest 的工作區,根目錄的腳本檔算草稿。
autoPurge · true · 插件掛載時先套用一次保留期限清理。
retainDays · 14 · 清理門檻。
maxVaultEntries · 2000 · 帳本上限,超出丟最舊。
maxEntryBytes · 4194304 · 超過則記帳但不存內容。
設定檔的搜尋順序與文件一致:<vaultDir>/scratch-vault.json 優先,其次是 `$DSH_SCRATCH_VAULT_CONF
…(truncated)
以上内容来自 npm 上的 README,我们只做格式化,不改内容。
版本
npm 上共发布:5,最后更新 2026-10-21