DeepSeek Harness 安全 Plugin 真正要驗收的,不是「工具有沒有跑」,而是「路徑繞出去、Plugin 被卸載、程序突然 crash 時,下一個副作用還會不會發生」。這篇是進階故障注入實驗;如果你還沒跑過 Web、四種 Mode 與第一個 Plugin,先完成《DeepSeek Harness 完整教學》。本文不重複安裝導覽,而是用固定官方 source 的 acceptance protocol,拆解 permission boundary、cooperative cancellation、config replacement 與 event projection;文中的程式碼是可移植的關鍵 excerpt,481 項測試則是 AlphaLab 對固定 checkout 的實測結果,不是可下載的完整 lab。
截至 2026 年 8 月 19 日,官方 master 與最新 prerelease 都是 dsh-v0.1.0-rc.7、commit 99f6f02fecdb7dff40c3fbc9470f5907c29f74ca;本文以這個 snapshot 實測。source license 是 MIT,LICENSE SHA-256 為 ebb4f099…6be。官方 package.json 宣告 pnpm@11.7.0 與 Node ^22.19.0 || >=24.0.0;本次 shell 另行核對後,實際使用 pnpm 11.7.0、Node v24.15.0。驗收只涵蓋這份官方 Git checkout 與這個環境,沒有宣稱任何 npm tarball 與它 byte-for-byte 相同。官方仍標示 developer preview,並明示會有 compatibility-breaking changes;未來版本請先重跑本文測試,不要只改版本字串。
MIT 是 DeepSeek Harness 本身的 license,不代表所有 dependency 都是 MIT。這個 commit 的 THIRD_PARTY_NOTICES.md 分列 vendored source、runtime npm dependencies 與其他 disclosed payload;各專案仍受自己的 license 約束,完整 transitive closure 另由 pnpm-lock.yaml 與 python/sdk/uv.lock 固定。部署或再散布前要一併審查,不能只看根目錄 LICENSE。
DeepSeek Harness 安全 Plugin 先說結論:安全是三道閘門
🐋 記憶把手:安全 Plugin = canonical allowlist(只能寫哪裡)+ cooperative cancellation(何時停止)+ append-only log projection(出事後辨認未知結果)。
少任何一層,都可能出現「畫面顯示取消,第二個檔案卻照樣寫完」或「重啟後把未知結果的操作再做一次」。
- 空間閘門:把 model-controlled path 解析成 canonical target,再要求它位於 Plugin 專用 root 且命中 exact allowlist。
- 時間閘門:把每次 tool call 的
exec.signal與 Plugin lifetime signal 合併,並在每個副作用前再次檢查。 - 復原閘門:從 append-only event log 重建 model-visible history;遇到已記錄 call、沒有 result 的 crash window,先查外部狀態,不盲目重跑。

為什麼「workspace-write 已開」仍不夠?
官方 filesystem sandbox 會 canonicalize target,workspace-write 允許 workspace 與平台 temp 下的寫入;讀取、網路與程序可見性不在這個 mode 的限制範圍。更容易踩雷的是:bare fs-local 的 cwd 只負責相對路徑解析,並不是 containment boundary。
所以本文不是取代官方 dsh-fs-sandbox,而是在它上面再縮一層:整個 Agent 可以寫 workspace,但這支 Plugin 只准碰 .dsh-safe-output/phase-1.txt 與 phase-2.txt。這是 trusted-code policy fence,不是 kernel boundary;若要執行不受信任程式碼,外層仍應使用隔離帳號、VM/container。可對照《把 AI coding agent 關進 microVM》理解兩層責任差異。
DeepSeek Harness 安全 Plugin 實作:先固定版本與測試世界
先在拋棄式目錄準備官方 source checkout。packageManager 欄位只是期望值,不保證你目前 shell 叫到的 pnpm 正確,因此安裝前要逐項核對 commit、tag、pnpm、Node 與 license hash;不要在含生產 API key 的日常 workspace 做故障注入。
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
git checkout --detach dsh-v0.1.0-rc.7
test "$(git rev-parse HEAD)" = "99f6f02fecdb7dff40c3fbc9470f5907c29f74ca"
test "$(git describe --exact-match --tags HEAD)" = "dsh-v0.1.0-rc.7"
node --version # 必須符合 ^22.19.0 || >=24.0.0
pnpm --version # 本文驗收值:11.7.0
shasum -a 256 LICENSE # ebb4f09972...55c240dec2ecfa16ea6be
pnpm install --frozen-lockfile
mkdir -p tmp/alphalab-safety-lab
核心 Plugin 如下。除了 canonical identity 與合併 signal,還有三個不能省的細節:必須注入並逐 call 解析 sandboxPolicy;用 createIfAbsent 防止盲目覆寫;tool registration 與 abort/drain disposer 必須放在同一個 composite effect,利用反向 disposal 先停止、等待 in-flight 工作收斂,最後才 unregister。分成兩個頂層 effect 會並行卸載,不能證明這個順序。
import { setTimeout as delay } from 'node:timers/promises'
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { ToolRunContext } from '@deepseek-ai/dsh-tools'
import type { FsTarget } from '@deepseek-ai/dsh-fs'
import type {} from '@deepseek-ai/dsh-fs'
import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox'
import type {} from '@deepseek-ai/dsh-sandbox-policy'
export const name = 'alphalab-safe-writer'
export const inject = ['tools', 'fs', 'sandboxPolicy']
declare module '@deepseek-ai/cordis' {
interface Events {
/** Test/diagnostic boundary only: emitted after dispatch, before any filesystem operation. */
'alphalab-safe-writer/body-start'(callId: string): void
/** Test/diagnostic boundary only: emitted after an atomic phase write commits. */
'alphalab-safe-writer/phase-committed'(callId: string, phase: 1 | 2): void
}
}
export interface Config {
outputRoot: string
allowedFiles: string[]
}
export const Config: Schema<Config> = Schema.object({
outputRoot: Schema.string().required(),
allowedFiles: Schema.array(String).required(),
})
function sameTarget(ctx: Context, a: FsTarget, b: FsTarget): boolean {
return ctx.fs.contains(a, b) && ctx.fs.contains(b, a)
}
function policyRequest(exec: ToolRunContext): Parameters<Context['sandboxPolicy']['resolve']>[0] {
return exec.agent === undefined ? {} : { session: exec.agent.session }
}
/**
* A deliberately narrow demo tool. Its allowlist constrains model-controlled
* names; the injected filesystem and per-call policy remain the enforcing
* capability seam. This is trusted-process containment, not a kernel sandbox.
*/
export async function apply(ctx: Context, config: Config): Promise<void> {
if (ctx.fs.sandboxMode === undefined) {
throw new Error('alphalab-safe-writer requires a confining ctx.fs backend')
}
if (config.allowedFiles.length === 0) {
throw new Error('alphalab-safe-writer requires at least one allowed file')
}
const root = await ctx.fs.resolve(config.outputRoot)
const slots = await Promise.all(config.allowedFiles.map(
path => ctx.fs.resolve(path, { cwd: config.outputRoot }),
))
for (let index = 0; index < slots.length; index += 1) {
const slot = slots[index]!
if (sameTarget(ctx, root, slot) || !ctx.fs.contains(root, slot)) {
throw new Error(`allowed file is not a child of outputRoot: ${config.allowedFiles[index]}`)
}
if (slots.slice(0, index).some(prior => sameTarget(ctx, prior, slot))) {
throw new Error(`duplicate canonical allowlist target: ${config.allowedFiles[index]}`)
}
}
const lifetime = new AbortController()
const inFlight = new Set<Promise<unknown>>()
let closing = false
const confinedPolicy = async (exec: ToolRunContext, signal: AbortSignal): Promise<SandboxExecutionPolicy> => {
const standing = ctx.sandboxPolicy.resolve(policyRequest(exec))
if (standing.mode === 'read-only') {
throw new Error('safe_write_pair denied by the calling session read-only policy')
}
if (standing.mode === 'workspace-write') {
const standingRoot = await ctx.fs.resolve(standing.workspaceRoot, { signal })
if (!ctx.fs.contains(standingRoot, root)) {
throw new Error('safe_write_pair outputRoot is outside the calling session workspace')
}
}
// Narrow danger-full-access or a broader workspace to this plugin root.
// fs-sandbox still documents platform-temp allowance and a residual
// canonicalize-to-syscall race; the exact canonical slot check below is a
// second trusted-code fence, not a claim of kernel isolation.
return {
...standing,
mode: 'workspace-write',
workspaceRoot: ctx.fs.processPath(root),
}
}
const run = async (args: { first_path: string; second_path: string; pause_ms: number }, exec: ToolRunContext): Promise<string[]> => {
const signal = AbortSignal.any([exec.signal, lifetime.signal])
ctx.emit('alphalab-safe-writer/body-start', String(exec.callId))
signal.throwIfAborted()
const sandboxPolicy = await confinedPolicy(exec, signal)
const write = async (path: string, content: string, phase: 1 | 2): Promise<string> => {
signal.throwIfAborted()
const target = await ctx.fs.resolve(path, { cwd: config.outputRoot, signal })
if (!ctx.fs.contains(root, target) || !slots.some(slot => sameTarget(ctx, slot, target))) {
throw new Error(`write denied outside canonical allowlist: ${path}`)
}
signal.throwIfAborted()
// Guarded create: never overwrite a pre-existing target and re-check the
// backend's per-call policy immediately before atomic publication.
await ctx.fs.writeText(target, content, { kind: 'createIfAbsent' }, signal, sandboxPolicy)
ctx.emit('alphalab-safe-writer/phase-committed', String(exec.callId), phase)
return target.displayPath
}
const first = await write(args.first_path, 'phase-1\n', 1)
await delay(args.pause_ms, undefined, { signal })
const second = await write(args.second_path, 'phase-2\n', 2)
return [first, second]
}
const tool = defineTool({
name: 'safe_write_pair',
description: 'Create two canonical-allowlisted files with one cooperative cancellation point between them.',
parameters: {
first_path: { type: 'string', required: true },
second_path: { type: 'string', required: true },
pause_ms: { type: 'number', required: true },
},
output: {
schema: {
type: 'array',
items: { type: 'string' },
},
render: (_args, value) => [{ type: 'text', text: value.join(', ') }],
},
async execute(args, exec) {
if (closing) throw new Error('safe-writer is unloading')
const operation = run(args, exec)
inFlight.add(operation)
try {
return await operation
} finally {
inFlight.delete(operation)
}
},
})
// One composite effect is essential: yielded disposers run in reverse order,
// so unload first closes admission, aborts, and drains; only then does it
// unregister the tool. Separate top-level effects would unload concurrently.
ctx.effect(function* () {
yield ctx.tools.register(tool)
yield async () => {
closing = true
lifetime.abort(new Error('safe-writer unmounted'))
while (inFlight.size > 0) {
await Promise.allSettled([...inFlight])
}
}
}, 'alphalab-safe-writer:abort-drain-unregister')
}
白話說,allowlist 比「字串開頭是這個資料夾」更嚴格。../、絕對路徑或 workspace 內指向外面的 symlink,在 canonical identity 上都不會同時通過 root 與 exact slot;每次 mutation 還會帶入 calling session 的 policy。這仍是 trusted-code fence:fs-sandbox 明載 canonicalize 到 syscall 之間仍接受一個 ancestor-symlink TOCTOU,且 workspace-write 仍允許平台 temp,不能包裝成 kernel isolation。
驗收 ①:先攻擊 path traversal 與 symlink
測試不要只餵正常路徑。這裡分清兩個時點:若 allowlisted slot 在 Plugin 啟動前已是外連 symlink,apply() 必須直接拒絕 boot;若 mount 後才把 slot 換成外連 symlink,該次 tool call 必須在真正 write 前失敗。兩種情況都要求 0 次底層 write,outside canary 完全不變:
it('fails boot when an allowlisted slot resolves outside its root', async () => {
await writeFile(join(outside, 'escaped.txt'), 'outside\n')
await symlink(join(outside, 'escaped.txt'), join(outputRoot, 'phase-1.txt'))
await expect(boot()).rejects.toThrow('allowed file is not a child of outputRoot')
expect(fs.writeCount).toBe(0)
expect(await readFile(join(outside, 'escaped.txt'), 'utf8')).toBe('outside\n')
})
it('denies a runtime symlink replacement without changing its target', async () => {
await boot()
await writeFile(join(outside, 'escaped.txt'), 'outside\n')
await symlink(join(outside, 'escaped.txt'), join(outputRoot, 'phase-1.txt'))
const result = await call('phase-1.txt', 'phase-2.txt')
expect(result.isError).toBe(true)
expect(fs.writeCount).toBe(0)
expect(await readFile(join(outside, 'escaped.txt'), 'utf8')).toBe('outside\n')
})
Traversal case 另外先 await boot(),再確認 ../escape.txt 回傳 error、fs.writeCount === 0,且 phase 2 不存在。既有 allowlisted 檔則用 createIfAbsent 測試:底層雖收到 1 次 guarded write,sentinel 不得被覆寫,phase 2 也不得出現。只看到錯誤訊息不算通過;必須檢查 call count 與真實 filesystem state。
驗收 ②:Caller cancel 與 unmount 必須擋住下一個副作用
官方 Cordis lifecycle 會在卸載時回收 tool registration 與 effects,但「新 call 看不到工具」不等於「已開始的 Promise 被硬殺」。Tool runtime contract 明確採 cooperative cancellation:已開始工作仍會被 drain,也可能已留下副作用。
先把 caller cancellation 的三個窗口分開:dispatch 前取消必須 body=0、write=0;body 已進入、第一次 write 前取消必須 body=1、write=0;phase 1 commit 後取消則保留 phase 1,但 write count 只能是 1,phase 2 不得出現。這不是回滾,而是阻止下一個副作用:
await boot()
const controller = new AbortController()
let bodyCount = 0
ctx!.on('alphalab-safe-writer/body-start', () => {
bodyCount += 1
controller.abort(new Error('cancel at body boundary'))
})
const result = await call('phase-1.txt', 'phase-2.txt', 1, controller.signal)
expect(result.isError).toBe(true)
expect(bodyCount).toBe(1)
expect(fs.writeCount).toBe(0)
expect(existsSync(join(outputRoot, 'phase-1.txt'))).toBe(false)
Unload 還要多驗一件事:disposer 必須等待已 commit、但故意不理 cancellation 的 provider boundary 收斂,等待期間 tool 仍已被 composite effect 擁有,不能提早 unregister。測試用的 CountingSandboxedFileSystem 是純本地 gate,不是 production provider:
const fiber = await boot()
const block = fs.blockNextWriteAfterCommit()
const running = call('phase-1.txt', 'phase-2.txt', 10_000)
let resultSettlements = 0
void running.then(() => { resultSettlements += 1 })
await block.entered.promise
let unloadSettled = false
const unload = fiber.dispose().then(() => { unloadSettled = true })
await Promise.resolve()
await Promise.resolve()
expect(unloadSettled).toBe(false)
expect(ctx!.tools.get('safe_write_pair')).toBeDefined()
block.release.resolve()
await unload
const result = await running
expect(result.isError).toBe(true)
expect(unloadSettled).toBe(true)
expect(resultSettlements).toBe(1)
expect(ctx!.tools.get('safe_write_pair')).toBeUndefined()
expect(fs.writeCount).toBe(1)
expect(existsSync(join(outputRoot, 'phase-2.txt'))).toBe(false)
await new Promise(resolve => setTimeout(resolve, 25))
expect(resultSettlements).toBe(1)
expect(fs.writeCount).toBe(1)
另一個 unload case 在 phase 1 event 後立即 dispose,同樣必須留下 phase-1\n、沒有 phase 2。這也回答「半寫入」的歧義:官方 local FS 對單一 target 採 atomic publication,signal 會在 publication 前檢查;但兩個檔案是兩個已提交副作用,不是 transaction。若 phase 1 代表扣款、phase 2 代表記帳,你需要 idempotency、對帳或補償,不是再多一個 abort check。
驗收 ③:Crash 後做 event projection;未知結果先對帳
DeepSeek Harness 的 Session 是 append-only typed event log;deriveMessages() 才把 user/assistant/tool result 投影成模型下一次看見的 history。所謂 deterministic projection,應限定為:同一個相容版本、同一段已驗證 event prefix,得到相同 model-visible projection。它不是讓程序回到 crash 前的記憶體,也不是重新執行工具。
import { createHash } from 'node:crypto'
import { expect } from 'vitest'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
const digest = (value: unknown) =>
createHash('sha256').update(JSON.stringify(value)).digest('hex')
function expectStableProjection(original: Session): void {
const before = digest(original.deriveMessages())
const projected = Session.create(SessionId('projection-copy'), [...original.events])
expect(digest(projected.deriveMessages())).toBe(before)
}
Standalone opId fixture:只教決策,不冒充 production Plugin
本文的 durable-op.ts 是獨立、單 writer、raw node:fs 的教學 fixture;它不是 Harness Plugin、不是可讓模型呼叫的 tool,也不是 production journal。它把 provider 換成完全本地的 inert fake,只驗收下列 decision model:
- 第一次操作:先 append 並 file-fsync
intent(opId, fingerprint),才准呼叫 fake provider;拿到結果後再 appendresult。 - 已有相同 result:回傳
reused-result,純modelVisibleProjection(records)可重建輸出;provider lookup=0、apply=0。 - 只有相同 intent:一定先
lookup(opId)。若 provider 能證明已提交,記reconciled;查不到或無法確認就記UNKNOWN,絕不把舊 intent 當成再次 apply 的授權。 - opId 重用:相同 fingerprint 去重;不同 fingerprint 拋
OperationConflictError。 - journal 損壞:先驗證所有完整 frame,才可截掉最後一段 torn tail;已換未來 schema 或完整行 JSON 壞掉則 fail closed,原 bytes 不動、provider 不得被碰。
Fixture 的 afterIntent/afterEffect hook 只模擬 process window,不是 OS hard crash。它只 fsync journal file、沒有 fsync parent directory;fake provider 會原地 truncate/rewrite,非 atomic store;也沒有 cross-process lease。因此它只能證明「intent 不授權盲目重做」這個決策,不能證明 exactly-once 或真正 crash durability。
這篇刻意不把 AlphaLab 既有四篇混成一篇;四條路徑各自回答不同問題:
- /deepseek-harness:入門安裝、Web/CLI 與四種 Mode,不做本文的 source-pinned 故障矩陣。
- /code-implemented-tool-calls:production executor、durable journal 與暫停/恢復,不把 raw-fs toy 當正式實作。
- /deepseek-api-cost-control:用 claim/lease/outbox 協調 worker、重試與成本,不處理這支 Plugin 的 path boundary。
- /agentic-transaction:外部副作用的 idempotency、對帳與補償,不宣稱 abort 可以回滾。
真正的 hard-crash 證據只來自官方 subprocess e2e。官方 persistence contract 會把「已記錄 call、沒有 result」修成 TOOL_OUTCOME_UNKNOWN;給模型的指示是:只讀或 idempotent 工作才考慮 retry,有外部副作用就先驗證狀態或詢問使用者。
量化驗收:官方 crash-recovery.e2e.ts 的 2 個 subprocess hard-crash tests 全綠:一個涵蓋 model dispatch 前的 durable request,另一個涵蓋副作用前的 durable tool intent;結果遺失時恢復成 UNKNOWN,不會自動宣告成功或再跑一次。這 2 個才是本文所有「hard crash」字樣的證據。
驗收 ④:Config-only HMR、API key 與 sub-agent teardown
先分清 HMR 的範圍。rc.7 shipped Web 的 packages/bundle/web-app/cordis.patch.yml 明確把 shared-module hmr row 設為 disabled;CLI/profile boot 才會在沒有 HMR service 時掛上一個 { root: [] } 的 watch-only instance,監看 live profile 與 home 的 cordis.patch.yml。因此本文能談的是長生命週期 CLI surface 的 config-only HMR,不是 stock Web source HMR,也不是改任意 Plugin 原始碼都會自動 reload。
Patch row 必須靠穩定 id 命中既有 entry;non-insert patch 缺 id、或 id 不存在時,composer 會 warning 並跳過,不會猜目標。DeepSeek Harness 的 patch 也不是 nested deep merge。官方 CLI reference 說得很精確:命中同一 row 的後層 patch 會替換完整 config,所以每次都要重述所有必要 key。若原本使用 apiKeyEnv: TEAM_DEEPSEEK_KEY,新 patch 只留下 maxTokens,schema 會把 credential ref 回填為預設 DEEPSEEK_API_KEY;若只有團隊變數有值,下一個 request 便得到 MISSING_CREDENTIAL。Plugin 不一定在 load 時就失敗,因為 key 是 per request 解析。
import { expect } from 'vitest'
import { resolveAdapterOptions } from '@deepseek-ai/dsh-llm-deepseek'
expect(resolveAdapterOptions({ apiKeyEnv: 'TEAM_DEEPSEEK_KEY' }).apiKeyEnv)
.toBe('TEAM_DEEPSEEK_KEY')
// 模擬整個 config 被 { maxTokens: 8192 } 取代
expect(resolveAdapterOptions({ maxTokens: 8192 }).apiKeyEnv)
.toBe('DEEPSEEK_API_KEY')
Config-only update 會保留同一個 fiber identity,但預設 Fiber.update() 仍會 restart:先卸載 effects,再用新 config 重跑 Plugin callback;invalid candidate rollback 也會以舊 config 重新啟動。因此 ordinary config update 同樣可能有 availability gap,且不保留 Plugin-local in-memory state。name/inject/group 變動則走 replacement path:先 dispose 舊 fiber、再啟動新 fiber;新 instance 失敗時,是把舊 Plugin重新啟動成 fresh instance。兩條路徑都不能宣稱 atomic swap。固定版本實測涵蓋 valid、invalid、parse failure、修復與移除,但 User-patch 與 registry 測試分別通過,仍不足以證明這支自製 Plugin 在每一個 HMR failure window 都絕無 duplicate registration。
Credential gates 的固定版本實測是 core 17 tests,加上只篩選 DeepSeek keyless 行為的 2 tests:空 ambient variable 視為沒有 key,request 以 MISSING_CREDENTIAL 失敗,而且 rejected dummy key 不會出現在錯誤輸出。這些測試沒有真實 API key,也沒有送出 provider request。
Sub-agent 也要驗收「失敗不冒充成功」。依 rc.7 的 one-shot result contract,只有 completed 能成為成功輸出;aborted 映射 killed,error、max-tokens、refusal 與未知 stop reason 都是 failed,非 completed 的 partial output 不能當成功答案。One-shot settleRun() 會先 await result、再 await dispose(),完成 child reap 後才回報 completed/killed/failed;SDK child 在 handshake/startup 失敗時也會先 close() 並 reap,才 reject。本文跑的 39 tests 同時涵蓋 run-settlement.spec.ts 與 subagent-dsh-sdk.spec.ts:後者真的啟動本地 fake-runtime stdio child,走 startup failure、caller cancel、dispose 與 provider unregister 路徑。
環境變數也有兩層語意:底層 SDK client 若收到明確 env,那就是 child 的完整環境;subagent-dsh-sdk provider 則主動組成 { ...scrubbedParentEnv(), ...spec.env }。其中 scrubber 一律移除 DSH_*,並依變數名稱是否符合 KEY|PASSWORD|SECRET|TOKEN 的正規式移除 ambient value,再疊加明列的 spec.env。這是名稱啟發式,不是 credential allowlist;名稱沒命中的秘密仍可能被繼承,而且 stock spec.env 只能加值、不能刪掉已保留的 ambient name。敏感部署應以最小 ambient env 啟動 parent Harness,或改用能提供完整最小 env 的自訂 wrapper/provider,再加外層隔離。取消沒有 wire-level prompt-cancel;它先在本地 settle,再由 close() 做 bounded protocol shutdown,接著走 stdin EOF → POSIX SIGTERM → SIGKILL 的 teardown ladder,並等 child exit。測試涵蓋這些 teardown path,但沒有在 dispose 後獨立檢查 PID,也不能證明所有可能的 late callback 都不存在。

固定版本實測:9 suites/481 tests
AlphaLab 把前述自製安全測試與官方 regression tests 綁在同一個 99f6f02 checkout:16 個 test files 分成 9 組,每組先核對固定 source、Vitest config 與最低通過數,再用移除 credential-like 與 proxy 變數的環境執行。Custom tests 使用獨立 lab config,官方 unit suites 使用 root vitest.config.ts,只有真正的 subprocess crash suite 使用 vitest.e2e.config.ts。
可執行性邊界:本文公開的是可移植的核心 Plugin 與關鍵 test excerpts,不是一份可下載、checksum-bound 的完整 lab bundle;因此讀者可以照著建立自己的驗收,但不能只靠本文重建 AlphaLab 當次完全相同的測試環境。
2026 年 8 月 19 日的固定版本實測結果是 overall: PASS,9 suites、16 個 test files 共 481 tests:
custom-safety-lab:19 passed(allowlist、guarded create、cancel/unload、standalone opId 決策)hmr-and-tool-registry:152 passed(valid/invalid patch recovery、整份 patch list replacement、registry ownership)cooperative-cancellation:43 passed(agent-loop cancel、timeout 等待 cooperative settlement)filesystem-policy:25 passed(canonical containment、per-call policy fence)credential-scrubbing-core:17 passed(rejected credential 不洩漏)deepseek-missing-credential:2 passed(keyless request 的MISSING_CREDENTIAL)session-repair-and-projection:182 passed(torn tail、UNKNOWN/NOT_STARTED、projection version mismatch refold)checkpoint-hard-crash:2 passed(官方 subprocess hard-crash fixtures)subagent-settlement-and-sdk-process:39 passed(settlement、fake-runtime env/cancel/dispose)- 合計:481 passed;每組 status 0,沒有命中 forbidden output sentinel。
481 tests 不是「rc.7 沒有 bug」的證明,只表示本文明列的 gates 在這份 pinned checkout 通過。換 commit、Node、pnpm、lockfile 或測試輸入,都應視為另一份證據重新執行,不能沿用這次結果。
這 481 tests 明確不能證明的 8 件事
- 不是 Web source-HMR 證明:shipped Web 的 shared-module HMR 是 disabled;這次實測只綁 CLI/profile config-only watcher 與當前 source。
- 不是所有 HMR window 的 zero-duplicate 證明:user-patch suite 與 registry suite 各自通過,不等於這支 custom Plugin 在每種失敗交錯都沒有重複註冊。
- 不是 Cordis 自動取消任意 body:unload 本身不會硬殺 arbitrary tool Promise;本文結果來自 custom Plugin 自己 fuse lifetime signal、停止 admission 並 drain。
- 不是 kernel sandbox:
fs-sandbox是 trusted-code path containment;workspace-write仍允許平台 temp,且有文件明載的 ancestor-symlink TOCTOU。 - 不是 exactly-once:JSONL/session repair 只平衡 history 並標記 unknown;它既不證明外部 effect exactly-once,也不授權盲目 rerun。
- 不是 production opId store:custom fixture 是 standalone raw-fs、simulated-window、single-writer;沒有 parent-directory fsync、atomic provider store 或 cross-process lease。
- 不是 PID 級完整 teardown 證明:SDK subprocess tests 走過 teardown,但未在 disposal 後獨立檢查 child PID,也沒有證明每一種 late callback 都不存在。
- 不是線上整合驗收:這次實測沒有真實 API key、provider call、npm artifact、WordPress action 或 external network access。
最常見的 6 個錯誤
- 只檢查字串 prefix:
/work/a與/work/ab、..、symlink 都可能欺騙 lexical check;使用 provider 的 canonical target 與contains()。 - 把 unregister 當 cancel:它阻止未來查到工具,不代表 started body 停止;Plugin 必須擁有 lifetime controller。
- 只在函式開頭查 signal:取消可能發生在 await 之後;每個不可逆副作用前都要再查,並把 signal 傳入底層 API。
- 把 atomic file 當 multi-step transaction:單檔不出現半份內容,不代表兩檔一起成功或一起失敗。
- 看到 unknown 就 retry:先用 idempotency key 或外部查詢確認 phase 1 是否已發生。
- 以為 patch 會保留沒寫的欄位:先用
--dump-config保存基線,替換整個 row 時明列自訂 credential ref 與必要 expression。
FAQ:DeepSeek Harness 安全 Plugin 常見問題
1. 有 workspace-write 就安全了嗎?
不夠。它是較寬的 filesystem policy,還允許平台 temp,且不限制所有讀取、網路或 process visibility;敏感 Plugin 應再加自己的 canonical allowlist。
2. unmount 會回滾已寫完的檔案嗎?
不會。它會清理 lifecycle effects;已完成的外部副作用需要補償或人工處理。
3. AbortSignal 能硬殺任何 Plugin 嗎?
不能。這是 cooperative signal;registry/timeout path 會等 started Promise settle,才產生最終 cancelled/timeout result。忽略 signal 的同程序 Promise 可能繼續、卡住或留下副作用,不能把 timeout 當 preemption。
4. atomic write 代表 crash 後一定落盤嗎?
不等於。atomic publication 解決讀者看到半份 target 的問題;durability、fsync、兩個檔案的一致性是不同問題。
5. Session recovery/projection 會自動再執行工具嗎?
不會。Persistence load 只修復可修的 torn tail/open turn,projection 從相容 events 推導 model-visible history;未知 tool outcome 反而要求先查外部狀態。
6. API key 不見會讓 DeepSeek Plugin 載入失敗嗎?
不一定。native adapter 的 route 可先註冊;沒有可解析 key 的 request 會以 MISSING_CREDENTIAL 失敗。
7. sub-agent 有部分文字就能算成功嗎?
不能。One-shot path 只有 completed 是成功。Foreground tool 會把 non-completed partial text 留在錯誤內容供診斷;background Jobs 的 settleRun() 則丟棄 partial output,只回 failed/killed。兩條路徑都不能把部分文字當成功答案。
8. stock Web 會熱更新任何 Plugin 原始碼嗎?
不會。rc.7 shipped Web 明確停用 shared-module HMR;CLI/profile 只補上一個 root: [] 的 config-only watcher。改 Plugin source 後應重新啟動並重跑驗收,不能用 config reload 的結果外推 source reload。
給開發者的 5 個最後檢查
- 固定 release、commit、Node/pnpm 與 lockfile。
- 用外部 canary 驗證 denied write,而不是只看 log。
- 把 caller signal 與 Plugin lifetime signal 都傳到底層副作用。
- 保存 event log,並用 projection digest 驗證 crash 前後 history。
- 把 unknown、max-tokens、refusal、credential failure 都列為非成功。
接著閱讀
左右滑動查看更多推薦
結語:先定義不能發生什麼,再寫 Plugin
DeepSeek Harness 安全 Plugin 的重點不是多包一層 try/catch,而是把三個世界接起來:canonical allowlist 決定「只能去哪裡」,lifetime cancellation 決定「卸載後不能再做什麼」,append-only log projection 決定「重啟後知道哪些結果仍未知」。記住本文的把手:安全 Plugin = canonical allowlist + cooperative cancellation + append-only log projection/UNKNOWN。測試先把它打破,正式任務才有資格信任它。






