moongate

    Dual-point policy enforcement and secret stand-ins for MCP tools, in one MoonBit core.

    mcp
    agent
    security
    gateway
    policy
    Download zip
    Version
    0.7.1
    License
    MIT
    Last updated
    17 days ago
    Downloads
    62

    No dependencies

    #MoonGate(月门)

    CI Mooncakes

    零改造的 MCP 执行边界。 不改服务端一行代码:moongate serve --policy p.json -- node <server entry> 启动现有 MCP 服务端,同一套 MoonBit 策略就在服务端进程内、工具函数执行前生效。客户端不需要指向新地址,服务端不需要引入新依赖。

    密钥替身。 敏感值在进入模型上下文之前被替换成 «mg:…» 占位符;只有策略显式允许的目的地,网关才会在转发前换回真值。模型可以“使用”密钥,但从未看见它。

    • 零改造适配现有服务端:用官方 @modelcontextprotocol/server-filesystem 验证,拦截发生在服务端进程内,副作用没有发生
    • stdio 与 Streamable HTTP 两种传输,同一套判定
    • 一个网关可带多个 MCP 服务端,跨进程血缘成立
    • 策略、值级血缘、会话配额、一次性审批、哈希链账本,判定全部发生在执行之前
    • 核心用 MoonBit 写,已发布到 Mooncakes;可编译到 JavaScript / Wasm


    #先说清楚:与现有方案的关系

    MCP 安全网关在 2026 年已经是一个拥挤的赛道。以下项目都在做类似的事,功能上有明显重合:

    项目已有能力
    MCP Shield策略、密钥脱敏、跨工具外泄链、人工审批、哈希链审计
    MCPKernel / MCPGuard策略、污点扫描、沙箱、签名审计
    mcp-firewall策略、审批、哈希链、仪表盘
    taintmcp输出污点标记、来源追踪、首匹配策略
    AgentPerms录制真实会话后推断最小权限,再代理执行
    Authsome / OneCLI凭据代理:Agent 只拿到占位符,代理在出口替换

    Mooncakes 上已有 yadidyc/moonpod(通用策略核心,含预算/审批/审计)、eisem/moon_policy(RBAC)等模块。MoonGate 不是它们的扩展,也不复用它们的代码;完整对照、检索记录与差异见 docs/OVERLAP.md。

    如果你要的是通用策略语言或 RBAC,MoonPod / MoonPolicy 更合适;MoonGate 只做 MCP 工具调用这一层边界。

    MoonGate 不是第一个策略网关,也不声称发明这些机制。 我们的差异是结构性的:

    1. 零改造的服务端内执行:上面这些方案都是一个代理进程,必须把客户端指过去;代理被绕过(客户端直连服务端、配置写错)就没有保护。MoonGate 用 Node loader 在服务端进程内注入判定,服务端源码不变,工具函数执行前就拦下。
    2. 在工具调用边界做密钥替身:凭据代理通常在 HTTP 出口替换 header;MoonGate 在 MCP 的 tools/call 参数与工具结果里做替换,占位符可以跨工具流转,替换目标由策略逐条授权。模型上下文里始终没有明文。
    3. MoonBit 单核多后端:网关与服务端 guard 共用一套 .mbt;同一份代码发布为 Mooncakes 库,可编译到 JS/Wasm。

    如果只用一个代理,MoonGate 和上面几个项目能力接近;零改造的服务端内执行与密钥替身是它存在的理由。这一点写在 REVIEW-RESPONSE 里,没有回避。

    #两个执行点

    Agent ──stdio──▶ MoonGate 网关 ──stdio──▶ MCP Server ──▶ 文件 / 网络 / 数据库 │ │ │ 判定 1 │ 判定 2(内嵌 guard) │ 策略 + 血缘 + 审批 │ 同一份策略,硬拒绝 └────────────────────────┘

    演示里有一个专门的场景:绕过网关,直接用客户端连服务端,调用 fs.delete 和 reports/../../etc/passwd。两次都被服务端自己的 guard 拒绝,文件仍然存在。

    网关负责有状态的部分:跨调用的数据血缘、人工审批、密钥替身、账本。服务端 guard 负责无状态的硬规则:禁用工具、路径逃逸、默认拒绝、未解析的占位符。

    #密钥替身

    工具结果: API_KEY=sk-live-9f3a2b7c1d4e │ 网关提取、指纹、存入会话 vault ▼ 模型看到: API_KEY=«mg:8b201fda70f4dd1b» │ ├─ 发往未授权地址 → 拒绝(secret-reveal),明文不离开网关 └─ 发往授权地址 → 转发前替换回真值,上游收到真实密钥

    • vault 只在内存里,随会话结束消失;账本只记录指纹、4 字符前缀和替换次数。
    • 替换规则与工具规则分开:允许调用某个工具,不等于允许它看见密钥。
    • 服务端 guard 遇到未解析的占位符直接拒绝,避免绕过网关后把占位符当参数执行。

    它不试图理解语义,但对拆分、改大小写与常见编码做归一化匹配;边界写在 SEMANTICS.md。

    #五分钟运行

    只需要 Node.js 22+,仓库带 dist/engine.js。

    git clone https://github.com/zhangbowen2006/moongate.git cd moongate node demo/run.mjs

    演示启动四个真实进程(客户端、网关、文件服务端、网络服务端),写真实文件、发真实 HTTP 请求,10 个场景逐项断言:

    场景可观测证据
    读取凭据模型只看到 «mg:…»,明文没有进入上下文
    替身发往未授权地址secret-reveal 拒绝;接收端 0 次
    替身发往已授权地址网关替换回真值;接收端收到真实密钥,1 次
    reports/../../etc/passwd规范化后命中 deny-escape,上游未收到请求
    写报告审批台出现请求;批准后文件才落盘
    换内容再写参数哈希变化,必须重新审批;拒绝后文件不变
    删除文件工具未出现在 tools/list,强行调用也被拒绝
    绕过网关直连服务端服务端 guard 拒绝删除与路径穿越,文件仍在
    账本verify 逐条重算哈希链

    node bin/moongate.mjs verify --ledger runs/demo.jsonl node bin/moongate.mjs report --ledger runs/demo.jsonl

    #从源码构建

    需要 MoonBit 0.10.16+。

    moon check --deny-warn moon test --deny-warn # 47 个单元测试 node scripts/build.mjs node --test test/*.test.mjs # 21 个 Node 测试

    #作为 MoonBit 库使用

    moon add zhangbowen2006/moongate@0.7.1

    import {
    "zhangbowen2006/moongate" @moongate,
    }

    fn main {
    let policy = @moongate.parse_policy(policy_json) catch {
    @moongate.PolicyError(message) => abort(message)
    }
    // 服务端第二执行点:只做硬拒绝,approval 交给网关
    let verdict = @moongate.guard_decision(policy, "fs.delete", args_json)
    println(@moongate.json_render(verdict))
    }

    网关侧使用 new_session、client_line、server_line、resolve_approval,接口与 pkg.generated.mbti 一致。

    #多个服务端

    一个网关可以同时带多个 MCP 服务端。客户端看到合并后的工具清单,网关按工具归属把调用路由到对应进程,判定仍由同一个引擎完成:

    // examples/servers.demo.json { "servers": [ { "name": "fs", "command": "node", "args": ["demo/server.mjs"], "env": { "MOONGATE_ROLE": "fs" } }, { "name": "net", "command": "node", "args": ["demo/server.mjs"], "env": { "MOONGATE_ROLE": "net" } } ] }

    node bin/moongate.mjs run --policy examples/policy.workspace.json \ --servers examples/servers.demo.json --ledger runs/multi.jsonl

    工具名冲突时自动加 服务端名. 前缀。跨进程血缘是重点:文件服务端读到的密钥被替换成占位符,网络服务端拿到占位符时同样受 reveal 约束。

    当前多服务端模式只合并 tools;resources/prompts 路由到第一个服务端。

    #复用 MoonPod 策略(可选)

    MoonPod 是 MoonBit 的通用策略核心。MoonGate 提供一个兼容层,把 MoonPod 的 JSON 策略子集转换后在 MCP 边界执行:

    node bin/moongate.mjs import-policy --format moonpod \ --in examples/policy.moonpod.json --out runs/imported.json node bin/moongate.mjs check --policy runs/imported.json

    支持的字段、明确的失败关闭项与语义差异见 docs/MOONPOD.md。这不是依赖或 fork:MoonGate 不引用 MoonPod 代码,输出仍是普通的 MoonGate 策略。

    #两种接入方式

    #1. 作为网关(任何 MCP 客户端)

    把客户端配置里的启动命令换成 moongate run ... -- <原命令>:

    { "mcpServers": { "workspace": { "command": "node", "args": [ "/abs/path/moongate/bin/moongate.mjs", "run", "--policy", "/abs/path/policy.json", "--ledger", "/abs/path/runs/session.jsonl", "--", "npx", "-y", "@modelcontextprotocol/server-filesystem", "/abs/path/workspace" ] } } }

    被拒绝的调用返回 -32001,带规则、原因和血缘证据;被保护的返回值是占位符。

    #2. 作为服务端内的守卫(第二执行点)

    import * as gate from 'moongate/dist/engine.js'; import { readFileSync } from 'node:fs'; const guard = JSON.parse(gate.guard_create(readFileSync('policy.json', 'utf8'))).handle; const verdict = JSON.parse(gate.guard_check(guard, 'fs.delete', JSON.stringify(args))); if (verdict.decision === 'deny') { return { isError: true, content: [{ type: 'text', text: `blocked by ${verdict.rule}` }] }; }

    approval 在 guard 里是 allow:审批有状态,归网关管。策略加载失败时服务端拒绝所有工具调用(失败关闭)。

    #策略与语义

    规则按声明顺序匹配,第一条命中生效,未命中落到 default_decision(默认拒绝)。参数条件支持 glob、前缀、取反和路径规范化:

    { "id": "deny-escape", "tool": "fs.*", "decision": "deny", "when": [{ "arg": "path", "normalize": "path", "prefix": ["..", "/", "C:/", "~"] }] }

    normalize: "path" 先解析 . 与 ..,所以 reports/../../etc/passwd 不会被 reports/* 误放行。

    密钥替身另有两段配置:

    { "mask_secrets": true, "sensitive": [{ "tool": "fs.read", "when": [{ "arg": "path", "glob": ["**/*.env"] }] }], "reveal": [{ "id": "internal", "tool": "net.post", "when": [{ "arg": "url", "glob": ["**/internal/**"] }] }] }

    会话配额独立于频率限制,按整场会话累计:

    { "budget": { "max_calls": 80, "max_arg_bytes": 262144, "max_reveals": 5 } }

    max_reveals 限制密钥被换回真值的次数——即使目的地都合法,也不允许无限取用。审批与 (工具名, 参数) 的规范化 SHA-256 绑定,换参数、重放、超时都会失效。

    #账本

    每行 JSONL 包含 prev 与 hash:

    hash_n = SHA-256(prev_hash + "\n" + canonical(event_without_hash))

    canonical 递归排序键,任何一行被改动都会让 verify 失败并指出序号。账本只记录工具名、参数键名、摘要、值指纹和替换次数,不记录参数值或明文。

    #工程证据

    • moon test --deny-warn:47 个单元测试(策略顺序、血缘误报、路径规范化、guard、替身替换、审批一次性、账本篡改、变形外泄)
    • node --test:21 个 Node 测试(10 个进程测试 + 2 个 HTTP 传输 + 2 个 MoonPod 兼容层测试 + 6 个材料校验测试 + 1 个版本一致性测试)
    • interop/:官方 SDK 客户端 + 官方 filesystem 服务端,4/4 通过
    • Mooncakes:moon add zhangbowen2006/moongate@0.7.1,已在空工程中安装并调用 guard_decision 验证
    • 申报材料校验:npm run check:submission -- SUBMISSION.md,6 个测试覆盖格式/披露/场景数量
    • CI:Linux / macOS / Windows 三平台,含演示与互操作

    #边界

    • 服务端没有嵌入 guard 时,绕过网关就失去保护;不是沙箱
    • 替身只覆盖工具结果里的字符串叶子;如果明文从别的通道进入上下文,不会被替换
    • 替换目标由策略决定,策略写错就会把密钥发给错误的人
    • 血缘对拆分、改大小写、Base64/URL-safe 编码做了归一化匹配;任意加密、压缩或语义改写仍可绕过
    • 路径规范化是词法的,不解析符号链接
    • 审批台无身份认证,只适合单机
    • HTTP 传输实现了 Streamable HTTP 的 POST/GET,未覆盖旧版 HTTP+SSE;多服务端模式只合并 tools

    完整威胁模型与失败模式:docs/SEMANTICS.md。

    #许可

    MIT。MoonBit 核心库以 Apache-2.0 分发,许可证与 NOTICE 随 dist/ 保留,见 THIRD_PARTY_NOTICES.md。项目为独立新项目,无既有项目代码复用、无第三方项目移植;AI 辅助范围见 PROVENANCE。

    PolicyError

    pub suberror PolicyError {
    PolicyError(String)
    }

    ArgCheck

    pub struct ArgCheck {
    arg : String
    globs : Array[String]
    prefixes : Array[String]
    negate : Bool
    normalize_path : Bool
    }

    Policy model.

    A policy is a small, declarative document with five independent controls:

    1. rules decide per tool call (allow / deny / approval), first match wins.
    2. egress blocks outbound tools whose arguments carry values that earlier tool results marked sensitive.
    3. sensitive declares which tool results should be fingerprinted.
    4. rate caps how many tool calls a session may attempt per window.
    5. advertise decides whether tools the policy denies are hidden from tools/list (so the agent never plans around them).

    Rule order is meaningful on purpose: a firewall-style first-match model is easier to reason about than "most specific wins", and the linter in docs/POLICY.md shows how to order rules so broad grants do not shadow narrow denials.

    Budget

    pub struct Budget {
    max_calls : Int
    max_arg_bytes : Int
    max_single_bytes : Int
    max_reveals : Int
    }

    Session-wide quotas. Zero disables a quota; a missing budget should not lock a session, so zero never means "allow nothing".

    Inflight

    type Inflight

    The gateway state machine.

    The engine is deliberately transport-agnostic: it consumes one protocol line at a time and returns a list of actions for the host to execute. In production the host is a Node process that owns the two pipes; in tests the same engine is driven with plain strings. That split keeps every security decision in MoonBit while the host only performs I/O.

    Invariants the engine maintains:

    • A denied tools/call is never forwarded, so the upstream server cannot perform the side effect.
    • An approval is bound to the canonical digest of the exact tool name and arguments, expires, and is consumed once.
    • Values from sensitive results are fingerprinted and matched against the arguments of egress tools before any rule is applied.
    • Every decision produces exactly one hash-chained ledger event.

    PendingApproval

    type PendingApproval

    Policy

    pub struct Policy {
    name : String
    digest : String
    default_decision : String
    rules : Array[Rule]
    sensitive : Array[SensitiveRule]
    reveal : Array[RevealRule]
    mask_secrets : Bool
    egress_tools : Array[String]
    on_taint : String
    rate : RateLimit
    budget : Budget
    approval_ttl_ms : Int
    min_secret_len : Int
    hide_denied : Bool
    }

    RateLimit

    pub struct RateLimit {
    window_ms : Int
    max_calls : Int
    }

    RawToken

    pub struct RawToken {
    value : String
    fingerprint : String
    preview : String
    }

    A raw secret value plus its identifiers. The raw value is only ever kept in the session vault; ledger entries and errors carry the fingerprint and a four-character preview.

    RevealRule

    pub struct RevealRule {
    id : String
    tool : String
    checks : Array[ArgCheck]
    reason : String
    }

    A destination that is allowed to receive a protected value. Reveal rules are intentionally separate from rules: allowing a tool to run is not the same as allowing it to see a secret.

    RpcMessage

    pub struct RpcMessage {
    raw : Json
    id : Json?
    has_id : Bool
    verb : String?
    params : Json?
    result : Json?
    error : Json?
    }

    JSON-RPC 2.0 envelope handling for the MCP stdio transport.

    MCP's stdio transport delimits messages with newlines (a JSON message may not contain a literal newline), so the transport adapter only splits lines and the engine owns everything else: envelope validation, request/response classification, identifier matching and error synthesis.

    Rule

    pub struct Rule {
    id : String
    tool : String
    decision : String
    reason : String
    checks : Array[ArgCheck]
    approval_ttl_ms : Int
    }

    RuleHit

    pub struct RuleHit {
    id : String
    decision : String
    reason : String
    ttl_ms : Int
    }

    SecretEntry

    type SecretEntry

    A protected value kept in memory for the lifetime of one session. It is never written to the ledger; only preview and the fingerprint leave the process.

    SecretToken

    pub struct SecretToken {
    fingerprint : String
    preview : String
    }

    A token that survived the shape filter. preview is safe to log: four leading characters are enough for a human to recognise which credential is involved, and not enough to reuse it.

    SensitiveRule

    pub struct SensitiveRule {
    id : String
    tool : String
    checks : Array[ArgCheck]
    }

    Session

    pub struct Session {
    id : String
    policy : Policy
    seq : Int
    prev_hash : String
    call_seq : Int
    inflight : Map[String, Inflight]
    lists : Map[String, Bool]
    pending : Map[String, PendingApproval]
    taint : Map[String, TaintSource]
    vault : Map[String, SecretEntry]
    attempts : Array[Int64]
    allowed : Int
    denied : Int
    approval_requests : Int
    forwarded : Int
    tainted_values : Int
    total_calls : Int
    arg_bytes : Int64
    reveal_count : Int
    }

    TaintHit

    pub struct TaintHit {
    fingerprint : String
    preview : String
    source : TaintSource
    }

    One taint hit: the fingerprinted value, plus the tool call that introduced it. This is the evidence the gateway returns to the agent and writes to the ledger.

    TaintSource

    pub struct TaintSource {
    call : String
    tool : String
    preview : String
    }

    Value lineage.

    The gateway does not trust labels the agent attaches to data, and it does not try to understand natural language. It does something narrower and verifiable: when a tool result is declared sensitive by policy, every secret-shaped value in that result is fingerprinted, and any later outbound call whose arguments contain the same value is refused.

    This is exact-value taint, not semantic classification. It catches the common and damaging case — a credential read in one step and shipped out in the next — and it deliberately fails closed: a value that looks like a secret and appears in both places blocks the call even if the agent's intent was benign.
    fn advertise_tool(policy : Policy, tool : String) -> Bool

    Whether a tool should appear in tools/list.

    Hiding is conservative: a tool is hidden only when an unconditional deny rule matches it, or when nothing matches and the default is deny. Tools governed by conditional rules stay visible, because those rules can allow some arguments.

    any_glob

    fn any_glob(patterns : Array[String], text : String) -> Bool

    True when at least one pattern matches. An empty pattern list matches nothing, which keeps "no rule configured" fail-closed.

    base64_encode

    fn base64_encode(text : String) -> String

    Standard Base64 for the ASCII token shapes the scanner accepts.

    base64_urlsafe

    fn base64_urlsafe(text : String) -> String

    bool_field

    fn bool_field(value : Json, key : String, fallback? : Bool) -> Bool

    call_digest

    fn call_digest(tool : String, args : Json) -> String

    Canonical digest of a call. Approvals and ledger entries use this value so that a grant for write reports/a.md cannot be replayed for write reports/b.md.

    capability_args

    fn capability_args(verb : String, params : Json?) -> Json

    Arguments used for policy matching. Tool calls carry params.arguments; resource and prompt requests carry their own parameter object.

    capability_name

    fn capability_name(verb : String) -> String

    Capability name used for policy matching when a client requests a resource or a prompt instead of a tool.

    checks_match

    fn checks_match(checks : Array[ArgCheck], args : Json) -> Bool

    client_line

    fn client_line(session : Session, line : String, now : Int64) -> Array[Json]

    Drive one line coming from the agent (client side).

    close_session

    fn close_session(session : Session, now : Int64) -> Json

    Final ledger event: the counters the report reconciles against. Writing them into the chain makes truncation detectable even without replaying the whole run.

    collapse_separators

    fn collapse_separators(text : String) -> String

    Drop separators and fold case, so sk-abc_123, SK ABC 123 and skabc123 all collapse to the same string. A secret that is split or re-cased before leaving still matches the collapsed view.

    collect_strings

    fn collect_strings(value : Json) -> Array[String]

    Collect every string leaf of a JSON value. Used on tools/call arguments so taint matching sees values, not JSON syntax (keys, punctuation, escapes).

    error_response

    fn error_response(id : Json?, code : Int, message : String, data : Json?) -> Json

    expire_approvals

    fn expire_approvals(session : Session, now : Int64) -> Array[Json]

    Expire pending approvals. The host calls this on a timer; expired requests get a policy error so the agent is never left waiting forever.

    extract_raw

    fn extract_raw(text : String, min_len : Int) -> Array[RawToken]

    Extract raw secret-shaped values for masking. Callers must not log value.

    extract_tokens

    fn extract_tokens(text : String, min_len : Int) -> Array[SecretToken]

    Extract secret-shaped values from one string.

    The scanner accepts runs of [A-Za-z0-9_.-] at least min_len long that contain a digit or an uppercase letter. Requiring a digit or uppercase keeps ordinary prose ("application", "workspace") out of the taint set while still admitting API keys, tokens, connection strings and long identifiers. Names on the left of = or : are excluded on purpose: they are the most common false positive, and a blocked call that only contains a variable name erodes trust in the gateway.

    find_placeholders

    fn find_placeholders(text : String) -> Array[String]

    Fingerprints referenced by placeholders in one string.

    find_taint

    fn find_taint(taint : Map[String, TaintSource], texts : Array[String], min_len : Int) -> Array[TaintHit]

    fingerprint_of

    fn fingerprint_of(text : String) -> String

    Short fingerprint used for value lineage and lease digests. 16 hex characters (64 bits) keep log lines compact; the full digest is available wherever a stronger binding is required.

    glob_match

    fn glob_match(pattern : String, text : String) -> Bool

    Whole-string glob matching.

    * matches any run of characters (including path separators), ? matches exactly one character, everything else is literal. There is no escape syntax and no path normalization: reports/*.md is compared against the argument exactly as the agent sent it. Backtracking is linear in the text because only the most recent * is ever revisited.

    guard_decision

    fn guard_decision(policy : Policy, tool : String, args : Json) -> Json

    Stateless policy check for the in-process guard.

    The guard runs inside the MCP server, where there is no session, no lineage table and no approval queue. It therefore enforces only hard denials: approval is reported as allow because the gateway decides whether a human approved the call. This is the second enforcement point — if the gateway is bypassed, forbidden tools still do not run.

    hex_uint

    fn hex_uint(value : UInt, width? : Int) -> String

    Lowercase hex, zero-padded to width characters.

    id_key

    fn id_key(id : Json?) -> String

    Stable key for correlating responses with requests. JSON allows string and number identifiers; both are prefixed so 1 and "1" cannot collide.

    import_moonpod

    fn import_moonpod(source : String) -> Json raise PolicyError

    Importer for MoonPod (yadidyc/moonpod@0.2.0) JSON policies.

    MoonPod is a transport-neutral policy core; MoonGate is the MCP enforcement runtime. This adapter lets a MoonPod policy be reused at the MCP boundary. It is a compatibility layer, not a fork and not a dependency: MoonGate keeps its own evaluator, and the output is an ordinary MoonGate policy.

    Supported subset: schema_version, allowed_tools, read_roots, write_roots, protected_paths, network_rules, approval_required_tools, max_calls, max_operation_bytes.

    Unsupported fields fail closed with a field name instead of being ignored: command_rules, resource_rules, tool_quotas, max_io_bytes.

    int_field

    fn int_field(value : Json, key : String) -> Int?

    is_egress

    fn is_egress(policy : Policy, tool : String) -> Bool

    Whether a tool can carry data outside the trust boundary.

    is_passthrough_method

    fn is_passthrough_method(verb : String) -> Bool

    Methods the gateway forwards without a policy decision: the MCP handshake, capability discovery and notifications. Anything that can read or change state goes through tools/call, resources/read or prompts/get and is evaluated.

    is_sensitive

    fn is_sensitive(policy : Policy, tool : String, args : Json) -> Bool

    Whether a tool result must be fingerprinted. Sensitive sources are matched against the same arguments that were sent to the tool.

    json_arr

    fn json_arr(items : Array[Json]) -> Json

    json_as_array

    fn json_as_array(value : Json) -> Array[Json]?

    json_as_bool

    fn json_as_bool(value : Json) -> Bool?

    json_as_int

    fn json_as_int(value : Json) -> Int?

    json_as_object

    fn json_as_object(value : Json) -> Map[String, Json]?

    json_as_string

    fn json_as_string(value : Json) -> String?

    json_bool

    fn json_bool(value : Bool) -> Json

    json_canonical

    fn json_canonical(value : Json) -> String

    json_depth

    fn json_depth(value : Json) -> Int

    Depth of an object/array tree, used by the policy parser to reject pathological nesting before the engine keeps it in memory.

    json_get

    fn json_get(value : Json, key : String) -> Json?

    json_i64

    fn json_i64(value : Int64) -> Json

    json_int

    fn json_int(value : Int) -> Json

    json_null

    fn json_null() -> Json

    json_obj

    fn json_obj(pairs : Array[(String, Json)]) -> Json

    json_parse_opt

    fn json_parse_opt(text : String) -> Json?

    Parse a JSON document, returning None instead of raising so protocol callers can answer a malformed line with a JSON-RPC error.

    json_render

    fn json_render(value : Json) -> String

    json_str

    fn json_str(value : String) -> Json

    JSON helpers shared by the policy parser, the protocol layer and the ledger. Two serializers are provided on purpose:

    • json_render keeps insertion order and is used for wire messages, where field order is irrelevant but readability helps debugging.
    • json_canonical sorts object keys recursively and is used for digests and for the hash chain, so semantically identical messages produce identical bytes regardless of how the caller built them.

    json_strings

    fn json_strings(items : Array[String]) -> Json

    map_json_strings

    fn map_json_strings(value : Json, transform : (String) -> String) -> Json

    Rewrite a JSON value by passing every string leaf through transform.

    match_reveal

    fn match_reveal(policy : Policy, tool : String, args : Json) -> RevealRule?

    Whether a destination may receive the real value behind a placeholder. Absence of a matching rule means the placeholder stays unresolved and the call is refused by the engine.

    match_rule

    fn match_rule(policy : Policy, tool : String, args : Json) -> RuleHit?

    First matching rule wins. Returns None when no rule matches, leaving the decision to default_decision.

    new_session

    fn new_session(id : String, policy : Policy) -> Session

    normalize_path

    fn normalize_path(input : String) -> String

    open_session

    fn open_session(session : Session, now : Int64) -> Json

    First ledger event of a run: which policy governed it, identified by content hash, so a report can be tied to an exact policy revision.

    parse_policy

    fn parse_policy(text : String) -> Policy raise PolicyError

    Parse and validate a policy document. Every rejection carries the JSON path of the offending field; no input value is echoed back, because policy files may contain internal tool names that should not end up in logs.

    parse_rpc

    fn parse_rpc(value : Json) -> RpcMessage?

    placeholder_for

    fn placeholder_for(fingerprint : String) -> String

    Placeholder syntax used in tool results and arguments: «mg:<fingerprint>». The delimiters are non-ASCII on purpose so that model-generated prose is unlikely to collide with ordinary content.

    placeholders_in

    fn placeholders_in(value : Json) -> Array[String]

    Distinct placeholder fingerprints anywhere in a JSON value.

    report_ledger

    fn report_ledger(lines : Array[String]) -> Json

    resolve_approval

    fn resolve_approval(session : Session, call : String, approved : Bool, now : Int64) -> Array[Json]

    Resolve a pending approval. Approving forwards the original request, so the upstream server sees exactly one call; denying answers the client with a policy error. Either way the pending entry is removed, which is what makes a grant single-use.

    result_response

    fn result_response(id : Json?, result : Json) -> Json

    reveal_placeholders

    fn reveal_placeholders(text : String, vault : Map[String, String]) -> (String, Int)

    Replace placeholders with their real values. Returns the rewritten string and how many distinct placeholders were substituted. Unknown placeholders are left alone.

    scan_fingerprints

    fn scan_fingerprints(texts : Array[String], min_len : Int) -> Array[SecretToken]

    Fingerprints of every secret-shaped value in the given strings.

    scan_raw

    fn scan_raw(texts : Array[String], min_len : Int) -> Array[RawToken]

    Raw tokens across several strings, deduplicated by fingerprint.

    server_line

    fn server_line(session : Session, line : String, now : Int64) -> Array[Json]

    Drive one line coming from the upstream MCP server.

    sha256_hex

    fn sha256_hex(text : String) -> String

    Hash a UTF-8 string.

    sha256_hex_bytes

    fn sha256_hex_bytes(data : BytesView) -> String

    Hash raw bytes and return 64 lowercase hex characters.

    string_array

    fn string_array(value : Json) -> Array[String]

    string_field

    fn string_field(value : Json, key : String) -> String?

    taint_source

    fn taint_source(call : String, tool : String, preview : String) -> TaintSource

    tool_name_of

    fn tool_name_of(params : Json?) -> String?

    Tool name for a tools/call request, or None when the request is malformed. The engine answers malformed calls with a JSON-RPC error instead of forwarding them, so an upstream server never sees an invalid request.

    variant_values

    fn variant_values(value : String) -> Array[String]

    Encoded forms a secret can take on the way out. The gateway stores the fingerprint of each form when the secret is first seen, so a later call that carries any of them is still recognised as the same value.

    verify_ledger

    fn verify_ledger(lines : Array[String]) -> Json

    Ledger verification and reporting.

    The ledger is the gateway's evidence: a JSONL stream where every entry commits to the previous one. verify_ledger recomputes the chain, so a reviewer can check that a run was not edited after the fact. report_ledger turns the same stream into the aggregate a human reads: what ran, what was blocked, and why.