门面的方向——两个信任平面

更新于 · 在 sijie.xyz 查看原条目 ↗

facade-parity 让每一个门面都成为唯一那份注册表的投影。但那个模型没有方向这根轴 —— 它的基础 reach 只有 OwnerRead() / OwnerAction(),接的也就两个门面,两个都朝向 owner。这一篇补上缺的那根轴;而"为什么必须在任何对外门面碰这套机制之前补",值得说白:

天真地把一个对外门面注册进去,会让每一个 OwnerRead() 的 op 在它上面显示为"缺失" —— 棘轮会要求把 admin 控制台的读操作暴露给陌生人。 一个没有方向概念的对等机制,不只是保护不了你,它会主动往错的方向推。

模型

一个门面的身份,是它服务的那个行动者。对等把门面往上归一层,按信任平面分组 —— 持有同一级授权的行动者。平面正好只有两个。

平面 行动者 授权 对等语义
owner admin 控制台、owner 自己的 AI(经 MCP);将来还有 Electron、IM 摄入 登录会话 / 密钥对 完备性 —— 每一个 owner op 出现在每一个 owner 门面上
outward 访客的人(码 / BYOAI)、访客的程序(API key)、匿名读者与爬虫 一个角色,从授权解析出来 角色一致性 —— 一个可授予角色的能力,出现在每一个对外门面上,除非它那一类被豁免

"公开"不是一个平面

匿名访客是那个退化的授权:他的角色就是 owner 的公开角色。这正是 BYOAI 已经限定到的那个角色,也正是 reader 页面上 published 所闸住的东西。于是在对外这个平面内部,一切收敛成一行:

reachable(caller, cap) =
    facade-renderable(cap)      # can this facade physically carry it
  ∧ opened(cap)                 # owner made it an API candidate (API facade only)
  ∧ role(grant).grants(cap)     # code role / key role / public role
  − denials(grant, cap)         # per-code / per-key deny rows

逐行读:这个门面承载得了它吗;owner 把它开成 API 候选了吗(仅 API 面);这次授权解析出的角色授予了它吗;再减去按码 / 按 key 的拒绝行。

这个旋钮从匿名 → 带 key → 带码,走在同一根轴上。把"公开"收进 ACL 的回报是:owner 将来可以直接把某个能力授予公开角色本身(比如匿名只读搜索),不需要任何新机器。

泄露不变量,两处强制

一个 owner 平面的 op,永远不许渲染在任何对外门面上。 这就是为什么方向必须是结构性的,而不是一条约定:

  • manifest 层 —— 一种 leak 违规类型:任何一个暴露里含有"平面跟门面不一致"的 op,就是硬红,两个方向都查,启动时直接失败。不是"不要求",是"禁止"。
  • 注册表层 —— 仅 owner 的能力,其访客绑定本来就返回 ErrHidden,而对外门面走的是同一条装配路径,于是这类能力在结构上不可能出现。一致性测试会往对外暴露里注入一个假的 owner op,断言它变红(a-gate-that-cannot-go-red)。

同一条不变量用两套机制守是刻意的:manifest 抓的是声明错了,注册表抓的是装配错了,两者谁也盖不住对方的失误。

第四个门面:API key

方向这根轴就是为了容纳它才建的。在它之前,一个外人够到这台实例只有两条路:访客对话(要码、有 LLM 在回路里、按轮次计量)或者匿名公开页 —— 没有任何办法让一个程序直接调用 owner 的能力。 API-key 面就是那个位置:像一张访问码,减去脑子和油钱 —— 同样是按角色限定的授权,但形态是"端点进、结构化数据出",用速率限制而不是轮次/会话配额来节流。

它对外、非 agentic、按角色限定;而它之所以立得住,正因为平面模型已经先回答了"它可以承载什么"。已上线,2026-07-10(b42a15e7f):backend/internal/routes/pubapi/pubapi.go —— 只渲染非 agentic 的对外工具,按 key 的固定窗口限流器(rateWindow)负责节流,候选闸门(上面那行 opened(cap))的棘轮已归零;2df6b795b(2026-08-20)让经 key 发起的预约能说明是替谁订的(on_behalf_of.go)。

相关笔记