2026-09-23·by Sijie Wang#software#project#standmeet

subjectivity-record-notes-acl

Subjectivity 记录笔记:一个 ACL 粒度问题

(原名为 "Subjectivity owner-visibility (gate-one tightening)"——现改名,因为"owner"从一开始就是错误的框架。owner 的可见性没有任何变化:整个语料库本来就是属主的。真正会变化的是访客能否读到某条记录笔记,而这就是普通的 ACL 问题。)

结论(2026-07-16): subjectivity 与 wiki/output 是同一类东西——都走普通的语料库 ACL(角色 glob)。它多出来的唯一一根轴是 show_as_source:agent 读过之后,是否可以引用它。是两个旋钮,不是三个:

knoblives ongoverns
role glob角色agent 能否读取
show_as_source笔记读过之后,能否引用它——subjectivity 自 017c37d23(2026-09-05)起默认为 false:此前 corpus.create / corpus.update 沿用了 wiki/output 那个 nil→true 的默认值(showAsSourceForSubjectivitybackend/internal/corpus/ops/corpus_write_args.go:89

下文的"owner 层级"是在 glob 之外又加了一个能不能读的旋钮——是同一根轴,所以它没有增加任何能力。已经实现过(eacc06c736fced08),又被回退(e9241618)。它指出的缺口是真实的;答案是 glob 的粒度问题,不是要一个新概念——而这个答案里"按 code"的那一半同一天就上线了,见"该怎么做"。

触发事件: subjectivity/ 里出现了第一篇记录类笔记——简历:真实姓名、教育经历、雇主、城市。PII 进入了一个 StandMeet 会提供给访客的语料库。

已实现的两道门 (这部分成立——是准确的描述)

  • 门一——进入上下文。 语料库的准入是角色级别的路径 glob 正向白名单role_corpus_urispath_acl.go 里的 MatchesAnyCorpusGlob):命中即通过,raw://** 硬编码拒绝,空集即全拒,签发时冻结进 RoleSnapshot。access code 只能收窄它所承袭的角色(纯 AND 的层级关系,见 capability-acl-hierarchy.md);code 级别的语料库收窄——写这篇时作为决定 A.2 被推迟——已于 2026-07-16 以 code_corpus_denials 上线(6395374b0 后端,9a08ab6a2 role + code 上的 GUI;backend/db/schema.sql:1043)。
  • 门二——呈现给访客。 subjectivity 是一个私有层级:读取只用来给 agent 的语气打底,除非笔记自己用 show_as_source: true 开了口子,否则永不引用给访客(由服务端按笔记逐条判定,权威实现在 subjectivity_cite.go)。

缺口(真实,尚未关闭): 门二藏起来的是出处,不是信息本身——读过简历的 agent,仍然可以在回答里直接说出雇主名字。而且一次 subjectivity://** 授权,会把记录类笔记和立场类笔记一起放进来。

由此得出的错误结论:"门一需要一个笔记级别的排除机制"。门一手上早就有这个手段——不授予这个 glob 就是了。简历暴露的是一个粒度问题(subjectivity://** 太粗,而扁平命名空间又提供不出更细的东西可授予),不是缺了一道门。

方案:笔记级别的 owner 层级 (已否决——留档)

在笔记 frontmatter 里加 visibility: owner;同步时带进数据库;服务端把这类笔记从每一个访客会话的可达集合里剔除,不管角色 glob 怎么写:

readable(note) = MatchesAnyCorpusGlob(role_globs, uri)   <- existing gate 1 (role, frozen)
              ∧ NOT note.owner_only                       <- new AND term (note, live, owner-unilateral)

为什么它是错的,按反对意见出现的顺序:

  1. visibility本来就有两层含义,而且它们不是一回事。 visibilitywritings上的 public | private,说的是前端怎么渲染(private → 渲染出 locked_body 的预览)。它从来不是访问控制——visibility.go 里白纸黑字写着。给它加上 owner,就让同一个键既要管渲染模式,要管准入规则:F-L-8 又犯了一次——一个 flag,两个正交的概念。D.1 当时就点出了这个冲突,却还是倾向"复用"。
  2. "owner"没有所指对象。 它标记一篇笔记对 owner 可见——可整个语料库按定义就对 owner 可见。它标注的是一个常量。真正会变化的只有访客的可达性,所以错的是框架本身,不只是措辞:一个诚实的名字,主语应该是 visitor
  3. 所以它是多余的。 一旦这根轴是"访客可达性",那就正是角色 glob 已经在管的那根轴。同一根轴上装两个旋钮,不是一个功能。
  4. 简历要的不是隐藏。 它要的是定向开放:默认不公开,再对该看到它的角色/code 逐一授权——招聘者的 loop 能看到,路人看不到。这正是普通 ACL 存在的意义

该怎么做

简历是一个普通的 ACL 问题:不公开,按角色/code 逐一开放。

  • 现状: 该看到它的角色会授予一个覆盖它的 glob;不该看到的角色就不授予。但 subjectivity 扁平的命名空间,把真实的选择压缩成了 subjectivity://**(全部)或者逐条枚举每一篇笔记(也就是下文被否决的"纯靠自觉")。今天诚实的状态是:subjectivity 只能要么全给,要么全不给。
  • 这篇笔记本该问的问题: 要授予部分 subjectivity,就得有个东西可以拿来授予——一个子路径、一套命名约定,或者逐笔记枚举。这与"subjectivity 保持扁平"的 vault 规则相冲突。被否决方案 #2 拒绝"让文件系统决定语义",转头又发明了一套机制来绕开这条约束。如果 ACL 的最小单位是路径 glob,那么扁平命名空间本身就是一个"不要粒度"的决定——所以要么扁平规则让步,要么 subjectivity 就该是有意识地选择全有全无。
  • 按 code 定向是另外那一半——2026-07-16 已上线,形状正是下面"重新打开"所论证的那个。 code_corpus_denials6395374b0e2e/test/code-corpus-narrowing.spec.ts)是一个只做减法的层:readable = role 的 glob 命中 AND 没被本码的 deny 命中,集合求交、无序,所以 A.2 担心的顺序敏感性根本不会出现。单位是 glob,跟 role 的正列表是同一种语言:subjectivity://cv 从一张邀约上减掉一篇笔记,subjectivity://** 把整个体裁从这张码上收回(schema.sql:1034-1042)。仍然全有全无的是扁平命名空间在 role 这一侧。

被否决的方案 (原文如此;#2 的推理现在反而是最有意思的部分)

  • 在 glob 列表里加拒绝行(先匹配者胜) ——正是 A.2 推迟掉的那种顺序敏感式收窄;会改变门一的代数和角色编辑器的 UX;而且真正要保护的单位是"这篇笔记是一条记录",不是一种路径形状。
  • 给记录类笔记单开一个子路径或独立体裁 ——按 vault 规则 subjectivity 必须保持扁平(分组只存在于索引里);为了满足 ACL 而重组内容,等于让文件系统来决定语义。(← 重新打开这一条:ACL 的单位本来就是路径。)
  • 纯靠自觉(不用通配符,逐条枚举允许项) ——依赖 owner 永远不偷懒;只要有任何通配符存在,新的记录类笔记默认就会泄露。作为习惯可以接受,作为机制是失败的。

悬而未决的决定点 (已作废——留下来展示当初错在哪)

  • D.1 字段命名:复用 visibility: 还是新开一个 owner_only: true倾向复用,靠体裁区分歧义。 两个都错——这套机制本就不该存在。留下的教训:status: seed 笔记里的"倾向 X"不是一个决定。 但它当时被当成了决定,并被直接上线了。
  • D.2 范围 / D.3 Meili / D.4 默认方向:作废。

否决之后还站得住的部分

  • 缺口是真实的,关了一半。 subjectivity://** 加一篇记录类笔记 = 该角色就能读到这份记录;一张码现在可以把它减掉(code_corpus_denials),但在 role 这一侧,今天的安全性仍然来自手工收窄授权,不是结构性的保证。
  • 一个索引过期的隐患,值得记下来,供未来任何笔记级 ACL 事实参考。 语料库搜索里走 Meili 的那条腿,信任的是该标志位的索引副本。索引是尽力而为、异步写入的,所以它手上的副本可能是过期的、缺失的,或者是孤儿数据——而这三种情况读出来都是放行。任何笔记级的 ACL 事实都必须回表核对(数据库才是权威,索引只是候选来源),否则就会失效开放。线上实测发现:corpus_read 正确地扣下了这篇笔记,而 corpus_search 却把它整篇返回——路径、标题、雇主都出现在摘要里。
  • 原始笔记的结论句在写下的那一刻就已经是错的: "目前没有任何角色 glob 会放行 subjectivity/cv.md" ——审计用的 subj-verify 角色授予了 subjectivity://**,而它恰好放行了这篇笔记。之所以没有泄露,只是因为 cv.md 晚于已同步的 vault 副本而已。安全性来自简历还没被同步,而不是来自 glob 本身——这正是这篇笔记自己那句"只要有任何通配符存在,新的记录类笔记默认就会泄露"的现场应验。
about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

subjectivity-record-notes-acl