门面的方向——两个信任平面
上级:structure · 邻居:facade-parity · owner-facade-from-registry · acl-and-quota-granularity
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)。