ss1738/qedra

GitHub: ss1738/qedra

qedra 是一个 MCP server 形式的运行时安全闸门,在编码 agent 执行 tool call 前拦截破坏性操作,并通过签名凭证提供可独立验证的合规审计追踪。

Stars: 1 | Forks: 0

# qedra [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/ss1738/qedra/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

A hijacked coding agent tries to force-push main, rm -rf the repo, leak a secret and wipe CI; qedra blocks every one while the real fix goes through

一个用于自主编码 agent 的运行时闸门。它在 agent 和你的 repo 之间的 tool-call 路径中作为 [MCP](https://modelcontextprotocol.io) server 运行,因此每一次 `run_shell`、`write_file` 和 `git` 调用在执行前都会被检查。它会拦截那些会破坏仓库或机器的操作(强制推送 `main`、在工作树上执行 `rm -rf`、窃取 secret、清空 CI、从 metadata endpoint 窃取云凭证、开启反向 shell,或者——需主动开启——拆除云/数据库基础设施),并让正常的构建和提交工作顺利进行,而无需信任 agent 会自觉守规。 它的 git-branch 子策略由 z3 进行机器检查,因此该策略能够发现自身的漏洞。这是几乎所有防护栏都不具备的特性。其他的部分则是高精度的启发式规则,本 README 会明确说明哪些是哪些。 编码 agent(Copilot Workspace、SWE-agent、OpenHands)现在会自动开启 PR、运行 shell 并重写 git 历史。如今它们的安全性依赖于模型自身的表现:提示词防护栏和对齐。这是一种概率性行为,且依赖于具体模型。一个对齐良好的模型可能会拒绝提示词注入;但越狱或较弱的模型则不会。qedra 是一个不依赖于模型的确定性层级。它会在每次 tool call 运行前进行检查,因此无论受感染 agent 发起操作的原因是什么,都会被拦截。 ## 超越闸门:可验证凭证(Agent Control Plane)

An agent's destructive commands are blocked, then a signed session receipt is produced and independently verified

闸门能够阻止恶意操作。Agent Control Plane 则添加了依赖方所需的另一半内容:一份任何人都可以在不信任你或你的服务器的情况下进行验证的签名凭证。将 agent 身份绑定到一个已命名的、已提交的策略上,对其 tool call 进行拦截,并导出凭证。该凭证包含 agent ID、策略承诺、每个被拦截的操作及其裁决结果,以及基于防篡改哈希链的 Ed25519 签名。审计员、银行或保险公司仅凭公钥即可验证它: ``` qedra-verify receipt.json # VERIFIED: 已验证:6 个操作,策略 acme-prod-agent-policy,未被篡改且完好无损 ``` `verify_receipt` 运行四项独立检查:(1) 凭证指明了验证者所持有的策略(承诺匹配),(2) 公共哈希链完好无损(无插入、删除、重排或篡改),(3) Ed25519 签名有效,(4) 它会在执行轨迹上重新运行已提交的策略。第 4 项检查才是关键所在:即使操作员使用自己的密钥重新构建哈希链并重新签名,对于策略本该 `BLOCK` 的操作,伪造的 `ALLOW` 也会被识破。仅凭凭证和公共策略,任何人都能以密码学手段区分出真实的凭证与谎言。不想运行 Python 的依赖方可以通过 HTTP 运行 `qedra-serve` 进行相同的检查:将凭证 `POST /verify`,即可根据服务器持有的策略获取 JSON 格式的 yes/no 结果。 ``` python3 demo_receipt.py # a gated session -> VERIFIED independently -> operator forges + re-signs -> CAUGHT ``` 凭证自带密钥,因此它自身只能证明某个密钥签署了合规的执行轨迹。在由你控制的注册表中,通过带外方式固定每个 agent 的公钥(`qedra-verify receipt.json --registry agents.json`),这样验证过程就能证明该凭证确实来自该 agent。如果攻击者签署了合规的执行轨迹但冒充了已注册的身份,将会被拒绝。 **隐私:在不破坏证明的前提下进行脱敏。** 哈希链基于每个操作的加盐承诺生成,因此 `receipt.redact()` 可以在保持签名和完整性有效的同时剥离原始命令和文件内容。见证人可以披露任意操作子集,以证明这些裁决是可靠的。你可以向保险公司证明你的 agent 遵守了策略,而无需交出生产环境的命令。验证结果会明确指出披露了多少内容,因此部分脱敏的凭证绝不会与完全可靠的凭证混为一谈。 ``` qedra-redact receipt.json --out redacted.json --witness witness.json qedra-verify redacted.json # integrity + authenticity, no content revealed qedra-verify redacted.json --witness witness.json # + full soundness, from the witness ``` **零知识凭证(实验性功能,仅限 git-branch 策略)。** 选择性披露仍然迫使针对每次操作做出选择:要么透露它,要么失去其可靠性证明。在 zk-mode(`ControlPlane(agent_id, policy, zk=True)`)下,git-branch 操作会携带一个零知识证明,证明它属于被策略归类为所记录裁决的操作,且所基于的承诺与哈希链中的承诺相同。你可以彻底隐藏该操作,而验证者依然能确认裁决是正确的,并且依然能识破伪造的“该操作已被允许”(`demo_zk_receipt.py`)。该证明运行在可插拔的群上:默认是 2048 位 MODP 群,而设置 `zk="ec"` 则会选择 secp256k1,实测速度快约 10 倍,体积小约 8 倍。这属于原型密码学(在随机预言机模型中采用 Fiat-Shamir 启发式的 Sigma OR-proof),在被作为实质保证依赖之前,需要外部审查。其设计、范围和路线图详见 [docs/ZK_ROADMAP.md](docs/ZK_ROADMAP.md)。 **可配置的策略,按内容绑定。** 策略本身就是数据(`PolicySpec`):受保护的分支加上任何特定于组织的 secret 或 shell 黑名单模式,并将内置的威胁模型作为默认值。`Policy.root()` 会提交到规范的内容哈希,而不是版本标签,因此凭证会绑定生成它时所遵循的确切规则集。可以通过 `Policy("prod", spec=PolicySpec(protected_branches=("trunk",)))` 设置你自己的受保护分支,并且 git-branch 可靠性证明(z3)也会基于你的自定义设置运行。验证者持有相同的规范(`qedra-verify --policy-spec spec.json`),其哈希必须与凭证的根节点匹配,因此“验证者持有该策略”是一个字面事实,而非空头承诺。可以使用 `qedra-policy init --protected main,release --out spec.json` 和 `qedra-policy show spec.json --policy-id acme-prod` 来编写和检查规范,这将打印出内容哈希及生成的策略根节点。 **针对拥有真实凭证 agent 的可选预设。** 默认设置已针对 2,836 条真实命令的误拦截进行了验证,因此它保持精简,并涵盖了 git/file/local-shell 威胁模型。但是持有云或数据库凭证的 agent 可能造成的破坏远不止于一个 repo。`devops` 预设为灾难性的基础设施/数据命令(`terraform destroy`、`kubectl delete namespace`、`aws s3 rm --recursive`、`DROP DATABASE` 等)添加了高精度的黑名单,这些命令对于自主 agent 来说几乎从来都不是合法的。它默认处于关闭状态(可通过 `qedra-policy init --preset devops` 或 `Policy("prod", spec=preset_spec("devops"))` 选择开启),因此除非你将其开启,否则经过验证的默认设置不会受到影响。与所有 shell 规则一样,这些也是启发式的:对于直接形式具有高精度,但可通过混淆绕过,因此请将其与最小权限凭证和沙箱结合使用。 **持久化审计日志 + 告警。** 在生产环境中,你不能指望进程能在最后存活下来以导出凭证。传入 `ControlPlane(agent_id, policy, audit_log=AuditLog("audit.jsonl"))`,每一次被拦截的操作都会在发生时实时流式传输到磁盘,因此即使 agent 崩溃或被杀死,也会留下一道防篡改的踪迹。`verify_log()` 会在无密钥的情况下重新计算文件上的哈希链(完整性),而 `receipt_from_log()` 则能从中重构出带有签名的凭证(真实性)。当闸门拦截某项操作时,会触发 `on_block=` 回调;现成的 `webhook_alert(url)` 会发送兼容 Slack 的告警,且其失败绝不会破坏闸门的运行。在 MCP server 中,可以设置 `GUARDRAIL_AUDIT_LOG` 和 `GUARDRAIL_ALERT_URL` 以同时启用这两项功能。参见 `demo_audit.py`。 在 MCP server 中,`session_receipt` 工具会为该 session 导出凭证。范围在此明确声明:凭证证明了所记录的执行轨迹在已提交的策略下得到了可靠的拦截。它并不证明模型的意图,并且其强制执行力完全等同于规则集和闸门处于进程外的强度(与下方“假设”中所述的闸门假设相同)。`VERIFIED` 凭证能够保证什么,不能保证什么,在 [docs/CONTROL_PLANE_TRUST_MODEL.md](docs/CONTROL_PLANE_TRUST_MODEL.md) 中有明确说明,这是安全审查员或保险公司应该阅读的文档。 ## 已证明的特性 vs. 启发式特性 在开头声明这一点,因为这是该项目的核心所在: | 威胁 | 执行方式 | 保证级别 | |---|---|---| | 对受保护分支的强制推送 / 硬重置 / 变基 | 结构化 git 策略 | **由 z3 进行机器检查**(见下文) | | 对 repo / home / root 执行 `rm -rf`,fork bomb,裸块设备写入 | 基于命令的正则匹配 | 启发式,高精度,可通过混淆绕过 | | 将 secret 写入磁盘或通过网络 **或 DNS**(包括 DNS 隧道)外泄;涵盖现代提供商密钥(`sk-proj-`、`sk-ant-`)、AWS、GitHub、私钥 | 正则匹配(secret 形态 + 外泄点) | 启发式 | | 云元数据 (IMDS) 凭证盗窃(`curl 169.254.169.254/...`) | 正则匹配,针对 fetch-context 限定范围 | 启发式,高精度 | | 反向 shell / 后门持久化(`>& /dev/tcp`、`nc -e`、`authorized_keys`、`sudoers`、cron) | 正则匹配 | 启发式,高精度 | | 灾难性的基础设施/数据破坏 —— 云/k8s 拆除、DB drops、缓存刷新(**需开启 `devops` 预设**) | 可选的正则黑名单 | 启发式,高精度,默认关闭 | | 清空 CI workflow 文件 | 路径 + 内容检查 | 启发式 | | 构建/测试 agent 会执行的其他所有操作 | 默认允许 | 不予监管(设计使然) | “启发式”意味着它能捕捉直接的形式,并故意允许坚定的混淆者(`rm -r -f`、`$IFS`、base64、别名)绕过。它提高了意外事故或简单注入的成本。它不是沙箱。要实现严格隔离,请将其与 seccomp/gVisor 配合使用(参见“如何对比”)。 ## 演示(已测量,可复现) 被劫持的 agent 发出的同样具有破坏性的 tool-call 序列,在真实的 repo 上分别在有和没有闸门的情况下运行(`demo_compare.py`): | | `.git` 完好 | 磁盘上的 secret | CI 配置完好 | 合法修复依然被应用 | |---|:---:|:---:|:---:|:---:| | **无闸门** | 已销毁 | 已泄露 | 已清空 | 是 | | **有闸门** | 是 | 无 | 是 | 是 | 在真实执行路径中,6 项破坏性操作被全部拦截,2 项合法操作被全部允许。 ## 机器检查的核心(这是增加可信度的细节,而非主要防线) 这涵盖了最窄的受保护面——受保护分支的历史,server 端的 `pre-receive` 钩子对此也有防护,因此这并不是其主要价值所在。它出现在这里是因为其与众不同之处:人工编写的白名单无法告诉你它是否存在漏洞,而此策略可以。使用 z3,它在符号化的 git 操作类别上证明了: ``` guard_allows(a) implies not mutates_protected_history(a) ``` 其安全规范是独立于防护代码编写的,并非从其中复制而来,因此结果不是循环论证的。它会返回 `PROVED`,或者一个具体的反例。删除一条规则,它就会找到确切的漏洞: ``` full policy : PROVED (no admitted action rewrites protected history) drop the rebase rule : HOLE op=rebase branch=main ``` 此保证仅涵盖结构化的 git tool-call 路径。它不验证 Python 实现、正则规则或 shell 解析器。 ## 如何对比 | 工具 | 层级 | 捕捉内容 | 区别所在 | |---|---|---|---| | git server 端钩子(`pre-receive`) | git server | 错误的推送,且仅在到达 server 时 | qedra 在操作本地运行前就进行拦截,并且还涵盖 file/shell,而不仅仅是推送 | | seccomp / gVisor | syscall / 内核 | syscall 级别的隔离 | 强隔离,但没有“受保护分支”或“这是 secret”的概念;起互补作用,而非替代品 | | OPA / 策略引擎 | 请求 / API | 结构化的策略决策 | 通用策略,没有对策略本身进行形式化的自检,且开箱即用时缺少编码 agent 的操作模型 | | CodeQL / Copilot Autofix | 静态分析 | 代码中的漏洞 | 分析静态代码;对 agent 在运行时会对 repo 做什么毫不知情 | | 钩子 + shell 白名单 | 本地 | 一些相同的模式 | 无法证明策略没有漏洞;而这正是 z3 增加的部分 | 简而言之:这不是沙箱,也不是 linter。它是针对编码 agent 的操作级闸门,针对唯一一个证明可行的子策略拥有形式化检查的核心。请将其置于沙箱之前使用,而不是取而代之。 ## 针对误摩擦进行验证(而非针对攻击检测) 一个阻碍合法工作的闸门在两次 pull request 后就会被关闭,因此值得衡量的是它阻碍真实开发者命令的频率。它被在来自 **12 个受信任 repo(`rust-lang/rust`、`tokio`、`denoland/deno`、`ripgrep`、`cargo`、`ruff`)的 2,836 条真实 `run:` 命令**上运行(`realworld_test.py`),采用只读模式,没有执行任何操作,每个 repo 抽取前三个 workflow 文件: | 版本 | 误拦截 | 升级上报 | 允许 | |---|:---:|:---:|:---:| | v0(遇到任何元字符即失败关闭) | 2(均为合法 CI) | 1854 (65%) | 35% | | v0.1(默认允许,拦截威胁模型) | 0 | 0 | 100% | 随后为了检查选择性偏差,在一个更大、非刻意挑选的样本上重新运行:**来自 9 种语言中获 Star 最多的 49 个 repo(`react`、`next.js`、`tensorflow`、`ollama`、`godot`、`deno`、`rust` 等)的 3,790 条真实命令**,通过 GitHub 搜索排名收集,而非人工挑选,同样每个 repo 抽取前三个 workflow 文件(`scripts/expanded_realworld.py`)。结果:**99.9% 允许,0 次误拦截,2 次正确的升级上报**(一次强制推送和一次变基,其分支是一个 CI 变量,因此闸门无法判断它是否受保护,需请求人工介入)。 那个更大的样本发挥了它的价值:它抓住了**小样本遗漏的四个误报**(`rm -f ../private.key` 和 `rm -rf ./bin/build_deps`,一个是特定文件,一个是子路径,而 `rm` 规则曾过度将其匹配为销毁 repo)。现已修复,并且这四个都成为了回归测试。这就是为什么这个数字值得信赖:它在它未曾选择的代码上进行了测试,并且发现的问题都得到了修复,而不是被掩盖。 直言不讳地说:在默认允许的设计下,合法命令的低误拦截率是目标,而不是意外。语料库的价值在于它暴露出的另一种失误(v0 采用失败关闭机制,拦截了三分之二的正常 CI;rm 规则过度匹配了真实的子路径删除),并且这些问题都得到了修复。这个语料库**不能**衡量攻击检测;它不包含对抗性命令。攻击行为已由演示和测试涵盖,而不是由这个数字涵盖。 ## 假设与局限性 - **闸门必须独立于 agent 进程之外。** 如果 agent 可以编辑防护栏或读取 HMAC 密钥,它就可以绕过或伪造。只有在密钥不被 agent 访问的前提下(通过包装进程或独立服务,而不是 agent 控制的进程内导入),审计链才是防篡改的。参见 [THREAT_MODEL.md](THREAT_MODEL.md)。 - **Shell 覆盖尽力而为。** 坚定的攻击者会通过混淆绕过正则表达式。应将 shell 拦截视为纵深防御,而非安全边界。 - **良好对齐的 agent 通常会自行拒绝注入。** 在 `demo_hijack.py` 中,一个真实的 Claude agent 读取了植入的提示并拒绝了它。闸门的价值在于,当对齐失效时,它作为确定性的底线。 - **集成是通过 MCP 或 function-calling 循环实现的**(见下文)。无论闸门在哪里运行,形式化保证仍然仅涵盖 git-branch 策略。 ## 在 Claude Code 中使用它(一个钩子,零代码) Claude Code 会在你的机器上运行 shell 命令,这完全属于其威胁模型。它具有原生的 `PreToolUse` 钩子,可以将每次 tool call 交给外部命令并允许其拦截该调用,因此 闸门可以直接插入,无需框架代码: ``` pip install qedra qedra-hook --print-config # prints the .claude/settings.json block to paste ``` 将该块粘贴到 `.claude/settings.json` 中,每一次 `Bash` 调用都会被凭证所证明的同一个 闸门归类:强制推送到 `main`、`rm -rf ~`、通过管道将 secret 发送到网络都会被拒绝, 并且 Claude 会被告知原因;在未命名分支上进行历史重写需要询问人类;其他一切操作 则不受影响地继续进行。策略从环境中读取,因此一次安装即可服务所有 repo: ``` export GUARDRAIL_PRESET=devops # opt-in cloud/DB kill-command denylist export GUARDRAIL_AUDIT_LOG=~/.qedra/session.jsonl # a real session leaves a verifiable trail ``` 设置了 `GUARDRAIL_AUDIT_LOG` 后,每个调用(一个独立的钩子进程)都会追加到一条持久化的哈希链 踪迹中,因此活跃的 Claude Code session 会留下可验证的记录。只需一条命令,即可将该踪迹转变为单次签名的、可由第三方验证的凭证: ``` qedra-attest ~/.qedra/session.jsonl --agent-id my-agent --out receipt.json # 已证明:12 个条目(2 个被阻止),agent 'my-agent' -> receipt.json # public key: 302780f5... 使用以下命令验证: qedra-verify receipt.json --pin-key 302780f5... qedra-verify receipt.json --pin-key 302780f5... # VERIFIED: untampered and sound ``` `qedra-attest` 会首先检查踪迹的完整性(被篡改的日志永远无法被重新签名生成有效的 凭证),并在 `~/.qedra/signing_key`(权限模式 600,首次使用时生成)管理 Ed25519 密钥,并 打印出公钥,以便依赖方可以将 agent 的身份固定在该公钥上。该 钩子在发生内部错误时默认允许放行:任何内部错误都会让调用继续执行,因为破坏了 agent 运行的安全钩子 会被卸载。它的职责是针对已定义的威胁模型进行高精度的拦截,其强度完全等同于闸门是 agent 的唯一路径(拥有不受限制的第二 shell 的 agent 会绕过它,这与下文假设相同)。 ## 与任何 MCP agent 配合使用 该闸门作为 [MCP](https://modelcontextprotocol.io) server 运行,这是 Claude Desktop、Cursor、Copilot 和 Windsurf 都支持的 tool-call 接口。将 MCP client 指向它,agent 发出的每一次 `run_shell`、`write_file` 或 `git` 调用都会在协议路径中 被拦截,然后才会触及 repo。无需信任 agent。 只有当 MCP server 是 agent 访问 shell、git 和 文件的唯一路径时,这才成立。如果 agent 还拥有不受限制的原始 shell,或者可以编辑 server 文件,就会绕过闸门。请在进程外运行它,并且不给 agent 其他 执行路径。参见 [THREAT_MODEL.md](THREAT_MODEL.md)。 在 `pip install 'qedra[mcp]'` 之后,该 server 就是 `qedra-mcp` 命令: ``` // in your MCP client config (Claude Desktop, Cursor, ...) "qedra": { "command": "qedra-mcp", "env": { "GUARDRAIL_WORKSPACE": "/path/to/your/repo", "GUARDRAIL_AUDIT_LOG": "/path/to/audit.jsonl", // optional: durable trail on disk "GUARDRAIL_ALERT_URL": "https://hooks.slack.com/services/..." // optional: alert on every block } } ``` `integrations/demo_mcp.py` 进行了端到端的演示:它启动 server 并 通过 stdio(那些应用使用的相同传输方式)作为真实的 MCP client 连接,然后 发出一系列合法和破坏性的 tool call。破坏性的操作会被拦截在 协议内部: ``` Tools exposed to the agent via MCP: run_shell, write_file, git, audit_log > write_file('calc.py') wrote calc.py (the fix) > git('add calc.py') ok x git('push origin main --force') BLOCK force-push to protected branch 'main' x run_shell('rm -rf .git') BLOCK recursive delete of the repo x write_file('config.py' w/ token) BLOCK writing a secret to disk x run_shell('curl ... -d ghp_...') BLOCK secret exfiltration over the network x write_file('.github/workflows/...') BLOCK emptying CI config > run_shell('cargo build --release') allowed blocked: 5 allowed: 4 .git intact, no secret, CI intact, fix applied, audit chain verifies ``` ## 在任何 function-calling agent 中使用(无需 MCP) 大多数 agent 并不是 MCP。它们运行 OpenAI 或 Anthropic 的 tool-calling 循环。同样的闸门可以在 不依赖 SDK 的情况下插入该循环,因为 tool call 只是一个 `(name, arguments)` 对。注册 工具,然后通过 `ToolGate` 路由每一个调用: ``` from qedra.tool_gate import ToolGate, openai_tools # or anthropic_tools() from qedra.control_plane import ControlPlane, Policy gate = ToolGate("/path/to/repo", control_plane=ControlPlane("my-agent", Policy("prod"))) # ... 向 model 注册 openai_tools() ... for call in response.tool_calls: # your normal loop result = gate.handle(call.function.name, json.loads(call.function.arguments)) # ... feed result back to the model as the tool message ... receipt = gate.receipt() # signed, independently verifiable ``` 每个调用在运行前都会被拦截。未知的工具和格式错误的参数会以失败关闭的状态被拒绝,因此 被劫持的模型无法导致循环崩溃或溜过闸门。`integrations/demo_function_calling.py` 在真实的 repo 上运行了整个端到端循环。只有当 `ToolGate` 是 agent 访问 shell、git 和 文件的唯一路径时这才成立(与 MCP 路径的进程外假设相同)。 ## 运行它 ``` pip install -e . # installs the package + the `qedra-verify` command # (或者,要在不使用 CLI 的情况下仅运行以下脚本:pip install z3-solver cryptography "mcp") python3 tests/test_guardrail.py # 11 tests (the gate) python3 tests/test_control_plane.py # 34 tests (verifiable receipts + 0/500 forgeries caught) python3 tests/test_zk.py # 12 tests (zero-knowledge receipts) python3 tests/test_zk_ec.py # 10 tests (secp256k1 group) python3 tests/test_zk_receipt.py # 14 tests (zk receipts, end-to-end) python3 tests/test_tool_gate.py # 7 tests (the function-calling adapter) python3 tests/test_policy_spec.py # 11 tests (configurable, content-addressed policies) python3 tests/test_policy_cli.py # 6 tests (the policy authoring CLI) python3 demo_compare.py # with and without the gate, on a real repo python3 demo_receipt.py # a verifiable receipt: issued, verified, and a forgery caught python3 integrations/demo_mcp.py # the gate in a real MCP tool-call path python3 integrations/demo_function_calling.py # the gate in a plain OpenAI/Anthropic tool-calling loop python3 realworld_test.py # the 2,836-command friction check (needs gh) ANTHROPIC_API_KEY=... python3 demo_hijack.py # a real Claude agent meets a planted injection: it declines, and the gate stands behind it ``` 实际的拦截过程(真正阻止攻击的行为)由 `demo_compare.py` 和 `integrations/demo_mcp.py` 展示。`demo_hijack.py` 展示了另一种情况:一个表现良好的真实 agent,在此情况下闸门不会增加任何摩擦,而是作为底线静待时机。 ## 状态 早期阶段,单人开发,对范围有坦诚的说明。欢迎提供反馈和指出策略中的漏洞,尤其是漏洞。参见 [THREAT_MODEL.md](THREAT_MODEL.md) 和 [SECURITY.md](SECURITY.md)。 MIT 许可证。
标签:AI编程智能体, Lerna, MCP服务器, StruQ, Z3求解器, 安全防护网关, 数据泄露, 策略执行, 逆向工具, 防御机制