Circle
This content is not available in your language yet.
0. 规范语言
本文中的规范关键字(MUST / SHOULD / MAY 等)按 conformance/normative-language.md 解释;仅大写形式具规范约束力。
1. 目标
Circle(ak:circle:)是 Realm 内被父 Realm 包裹的子事件 / 子消息边界:拥有独立 membership、独立 history visibility、独立投递 / 查询 / projection 裁剪规则,且 Circle.members ⊆ Realm.members;但不持有 federation identity 或 capability registry。Circle 表达”窄于 Realm 的协作圈”。
Realm 与 Circle 分工正交:Realm 承担 federation / identity boundary,Circle 承担 intra-Realm scoped event boundary。一对象一 effective scope 是协议级硬不变量——任何对象 MUST 只属于一个 effective scope(Realm-default 或某个 Circle)。
Circle 在自己的 ak.mls.genesis 被接受之前是 plaintext delivery-only scope;该 Genesis 的 accepted RealmCommit 把 Circle 不可逆激活为 MLS-backed cryptographic scope(见 §7)。未激活的 Circle 仍然必须执行 Circle membership / history / delivery 裁剪。
非目标:Circle 不是 principal Group 的新名字,也不是 Realm 的”默认子圈”。若实现只需要把某个 capability 授给一组 principal,而不需要独立事件历史、投递裁剪或 Strand scope,应使用 realm-and-space.md §4 的 Group / capability constraint / resource selector;不得声明 Circle。Realm 自身仍拥有 Realm-default scope;Circle 表示 Realm 内更窄的 scoped event boundary。
2. 设计原则
- 平面化,不嵌套:Circle 不允许
parent_circle_ref。需要交叉成员关系时,actor 同时属于多个 Circle 即可;不需要 hierarchy。这条沿用realm-links.md§2 “link graph not tree” 的教训。 - 真子集 membership:
Circle.members ⊆ Realm.members,reducer 硬约束。 - 加密激活不可逆:Circle 的 MLS 激活由本 Circle 自己的 accepted
ak.mls.genesis决定,且不可撤销;协议不提供把已激活 Circle 退回明文的分支。 - MLS 独立,不可派生:Circle 激活后,其 MLS group 是独立 epoch 链,MUST NOT 从 Realm-default MLS group key 派生 Circle key。
- 独立历史 ratchet:Circle 的
history_access由 create 初始化,之后仅允许all_history_for_current_members → since_join;不继承也不受父 Realmhistory_accesscap。父 Realm 只提供 current membership intersection。 - Circle ≠ Group:
realm-and-space.md§4 的 Group 表达 principal/actor 集合(capability subject)。Circle 表达资源 / 事件 scope。两个概念正交,不可混淆。
3. Circle 对象
Circle 的 canonical schema 为 circle.schema.json。与历史/加密相关的
author input 与 materialized projection 分离:
Schema id: ak.schema.circle.v1
| 字段 | 规则 |
|---|---|
schema | 固定为 ak.schema.circle.v1 |
realm_id | 父 Realm 的内容寻址 ID,create 后不可变 |
title | Circle 的规范标题 |
display | §4 定义的规范视觉身份 |
directory_visibility | Circle 目录可见性策略 |
join_rule | Circle 加入规则 |
history_access | Circle 自有的单向治理 ratchet;不动态继承父 Realm |
mls_group_id | 仅 materialized MLS Circle;reducer 派生,create payload 禁止 |
id | 仅 materialized;由 create EventId retype |
state | materialized lifecycle,初始为 active |
created_by | create Event 的 producer actor |
created_at | create Event 的规范时间 |
Schema 的两个 closed branch 是:create branch 同时禁止 id/mls_group_id;materialized branch 要求 id,且 MLS
时要求 derived mls_group_id,其值按 realm-and-space.md §2.2 的唯一派生式从该 Circle 的 circle 分支 effective scope 算出(realm_id 与 circle_id 都进入 key bytes)。Plaintext、standard MLS、exporter MLS 的
closed union 见 history-visibility.md §2。
self-surface 的 circle_view(circle-operations.schema.json#/$defs/circle_view)
不携 member_count:成员数由同载体 required 的 member_ids.length 派生,能拿到 circle_view 的 caller 自行计数;
隐私阈由 circle_view 本身的可见性承担。circle_preview 不携 member_ids,只携本节 §9.3 定义的
member_count_bucket;这个 Circle bucket 与公开 Realm Directory 没有共同字段或推断关系。
4. display 字段(标准化视觉身份)
跨客户端一致的 UI 表达是 Circle 安全模型的必要条件(见 §11 UX 论证):
| 子字段 | 必填 | 类型 | 约束 |
|---|---|---|---|
short_name | yes | string | ^[A-Z][A-Za-z0-9 _-]{0,23}$;在 (realm_id, short_name) 上 reducer 强制唯一(case-insensitive)。冲突 MUST failed_precondition(reason=circle_short_name_taken,见 error-code-registry.json)。Circle 名称空间不为 Sidecar 保留前缀。 |
color_token | yes | string | 从 circle.schema.json#/$defs/display/properties/color_token/enum 的受控 palette 选;该 schema enum 是 canonical 机器真源。客户端 MUST 映射 token → 主题颜色(浅/深/高对比),不得自行重分配 token。gray_high_contrast 是承载无障碍/高对比语义的特例 token,并非纯色相;客户端 MUST 把它映射为高对比中性灰主题色。 |
symbol | yes | object | {emoji?: string, glyph?: enum};二选一。glyph 取 circle.schema.json#/$defs/display/properties/symbol/properties/glyph/enum 的受控 snake_case 枚举;该 schema enum 是 canonical 机器真源。客户端 MUST 把 glyph token 映射为本地 icon,MUST NOT 自行扩展未注册 token。 |
颜色 token 与 symbol 必须在 spec 受控集中,目的是同一 Circle 在 Alice 与 Bob 的客户端上呈现一致视觉,否则跨设备社会工程攻击成立。
5. Event 家族
本表为说明视图;完整集合与 wire_scope / reducer_input / projection 属性以 event-kind-registry.json 为准。
| event kind | reducer_input | payload 形态 | 说明 |
|---|---|---|---|
ak.circle.create | yes | full object | 创建 Circle。Circle 的 MLS group 由随后被接受的 ak.mls.genesis 单独建立,create 不初始化 group。 |
ak.circle.history_access | yes | {circle_id,from,to,reason?} | 专用单向 FSM;create 后仅允许 all_history_for_current_members → since_join。 |
ak.circle.update | yes | ak.schema.patch.v1(path 不含 realm_id / profile_ref / history_access) | 改 title / summary / display / directory_visibility / join_rule。 |
ak.circle.archive | yes | object_lifecycle_payload | active → archived。 |
ak.circle.restore | yes | object_lifecycle_payload | archived → active。 |
ak.circle.tombstone | yes | object_lifecycle_payload | terminal;触发 §8 cascade。 |
ak.circle.member.state | yes | {circle_id, member_id, membership: join|knock|leave|ban, ...} | 与 ak.member.state 复用同一 membership_state 四态枚举(join / knock / leave / ban),仅 scope 限定到 Circle;Invite 是独立 pending workflow,不是 membership state。reducer 先校验完整 member_id: ActorId 已是父 Realm join 成员;knock 仅在 join_rule=knock 下允许(见 §9.1)。 |
typed current result 归属与 subject 语义(normative):
circle_create(append-only projection,result_selector=null)是本 Realm 的 Circle 创建日志:一个 Realm 内每创建一个 Circle 追加一条 entry,typed current result 本身由 Event envelope 的realm_id定位。它不是 per-Circle 的 genesis singleton,因此 MUST NOT 把circle_id编进 typed current result subject;null subject 的 canonical wire 形态见../conformance/encoding.md§4。append-only projection的合并不产生冲突值,本 family 也不定义额外的领域冲突语义;Circle 身份唯一性由circle_id的 typed-id 唯一性与 §3 的 create 校验在 admission 阶段保证,不由状态模型冲突表达。circle_tombstone(commit-ordered projection)是 per-Circle 终态槽位,result_selector={"kind":"coalesce","fields":["payload.circle_id","payload.target_ref"]},与ak.circle.archive/ak.circle.restore写入的circle_lifecycle采用同一 subject 形态。coalesce 的第二项是必需的:三个 Circle lifecycle kind 的 payload class 是object_lifecycle_payload(§5 表),它以target_ref作为目标对象的唯一来源、不携带circle_id,因此只声明payload.circle_id的 subject 在该 payload 上不可派生。本行给出的是 Circle 自己的 subject 形态,不是对既有登记行的引用:ak.morph.*/ak.space.*/ak.strand.*的同类终态槽位至今没有result_writes[],而ak.relation.tombstone明确不走这个形态——relation.md§2 把必填relation_id定为唯一目标字段,并要求target_ref出现时返回schema_violation。realm-and-space.md§2.5.1 禁止正文对尚未登记的 kind 引用其result_writes[]。它 MUST NOT 使用 null subject——per-Realm 单例槽位只能容纳一个 Circle 的 tombstone,第二个 Circle 会错误复用第一个的安全槽位,导致错误的前置拒绝或覆盖归属。
6. 对象 scope 表达
引入字段(跨多个现有对象):
Strand.scope_circle_id : id:circle | null # null = Realm-default scopeMessage.effective_scope : reducer-derived read projection,必须等于创建 Event.scope_refEvent.scope_ref : producer-signed, immutable tagged security scopeSpace.scope_circle_id : id:circle | null # Space 自身 metadata / scoped structural relation 的可见性 scopeSpace.child_scope_policy : object # 子资源 placement 约束,见 §7Morph.scope_circle_id : id:circle | nullRelation.scope_circle_id : id:circle | nullRelation.effective_scope : reducer-derived read projection,必须等于创建 Event.scope_ref关键约束:Strand 永远只有一个 effective scope。不存在 per-track scope —— 整个 Strand(synthesis、discussion、其他 track)共享同一事件 / 投递 / history 边界,要么都在 Realm-default,要么都在某个 Circle。
6.1 Reducer 规则
scope_circle_id引用的 Circle MUSTrealm_id与对象realm_id一致;否则schema_violation(reason=circle_realm_mismatch)。scope_circle_id引用的 Circle MUSTstate=active;否则failed_precondition(reason=circle_not_active)。scope_circle_id=null表示 Realm-default scope,对应签名scope_ref={kind:"realm", realm_id}。scope_circle_id=ak:circle:...对应签名scope_ref={kind:"circle", realm_id, circle_id}。- Reducer 在接受每个 Event 时 MUST 从 payload 与 accepted references 派生 scope,并与 producer-signed
scope_ref逐字段比较。后续对象 rebind 不得重解释旧 Event。 - Message 与 Relation 的 read projection MAY 物化顶层
effective_scope,但它必须逐字段等于创建 Event 的scope_ref。该 projection 不是可写真相源。 - Effective history access 恰等于 Circle 当前
history_access。父 Realm history facet 不进入该值;父 Realm 当前 membership intersection 仍独立生效。Circle 的明文/密文判定只看该 Circle scope 自己是否已有 acceptedak.mls.genesis,父 Realm 的激活状态不传递。 - 改绑
scope_circle_id默认 reducer 拒绝(failed_preconditionreason=scope_rebind_forbidden);profile MAY 允许,但 MUST audit-paired high-risk update。所有已存在 Message / 子内容保留其写入时的effective_scope与旧 scope 的 history / key eligibility;新内容才进新 scope。客户端 MUST 把切分前后历史分段展示。 - Structural Relation / position typed current result 的
effective_scopeMUST be no broader than 参与端点中最窄的 scope(取参与端点 scope 集合中最严格者作为关系事实自身的 scope)。具体例:public Board (Realm-default)包含private Strand (Circle=HR-Conf)时,contains关系事实与其 position typed current result 的effective_scope = Circle:HR-Conf,不是 Realm-default;非 Circle 成员看不到该 containment 关系、看不到 private Strand 的 rank/position,也看不到 board 上”此处有隐藏项”的可枚举元数据。 - 当参与端点分别落在同一 Realm 的两个不同 Circle,且没有 Realm-default 端点可作为共同公开侧时,这两个 scope 在 v1 中是不可比较的并列 scope。Reducer MUST NOT 选择任一 Circle 作为”更窄者”,MUST NOT 取并集,也 MUST NOT 自动把关系提升到 Realm-default。Structural Relation、position、parent、cascade 或任何会产生 target-side reverse projection 的事实 MUST
failed_precondition(reason=scope_incomparable)。弱语义 reference 若 profile 显式允许,producer MUST 选择单一 source-sidescope_circle_id,且 projection 对该 scope 外 caller 返回locked/ none,不得创建目标侧反向边或可枚举空洞。
Circle lifecycle 求值基线(normative):普通 Event 的 Circle 身份、active 授权实例与 scope 由当前治理 Station 在接纳事务内从已提交 typed state 解析,允许未知撤销的传播窗口;producer 不携带、也不得替换该授权状态。安全命令从 expected_revision 派生确切 revision,并在唯一确认执行位置重验所有实际读取的安全 typed current result。不得合并多个同 Realm RealmCommit 为权限 view。
每条 accepted 普通消息都由其 Circle stream 的 RealmCommit 接纳并推进一个 position。分区的非 authority Station 只能耐久排队或转发,不能按缓存暂时接纳、更新共享 projection 或向成员 fanout;current governance Station 在接纳位置按 committed archive/tombstone 与授权状态裁决。缺必要依赖时保持 queued/retryable unavailable,不以到达时间或旧收据保留永久资格。restore 产生新的授权 generation,不能复活旧 generation 中被关闭排除的 Event;作者必须绑定新授权重新签发后继。
对应 conformance vector 是 ak.vector.circle.lifecycle_admission_barrier.v1。
6.2 scope_ref wire shape 与对象 projection
Event wire 只有一个安全作用域字段 scope_ref:
- 对带
scope_circle_id的对象,producer 同时提交对象字段和由它确定的 Eventscope_ref; - Message 等不带独立
scope_circle_id的 payload,producer 从引用对象的已接受 projection 得到scope_ref; - reducer 独立派生并比较;不一致返回
scope_ref_mismatch,不得替 sender 盖章或修正; - 对象 read projection 中的
effective_scope只能从创建 Eventscope_ref物化。
Realm scope Event:
{ "scope_ref": { "kind": "realm", "realm_id": "ak:realm:Ac1aCK8aQdnkYImvdH3DFjq4jDCP198pXYWCGzGuVyj5" }}Circle scope Event:
{ "scope_ref": { "kind": "circle", "realm_id": "ak:realm:Ac1aCK8aQdnkYImvdH3DFjq4jDCP198pXYWCGzGuVyj5", "circle_id": "ak:circle:AUD2WOhX-Xh47vBHtRJPMRfXRQXGiOWQqOrJGJnE8CaI" }}Reducer 校验顺序(MUST):
- schema 校验 Event 必有
scope_ref,且scope_ref.realm_id == realm_id。 - 若
scope_circle_id非 null:在 §6.1 规定的 authority-commit 基线中解析对应 Circle——普通 Event 用治理 Station 接纳时的 current authorization,state-changing Event 用领域 payload 的expected_revision——校验realm_id一致 +state=active;不得读取 receiver 当前 projection 代替事件基线。 - 从 payload/accepted target projection 派生预期 scope,与签名
scope_ref逐字段比较。 - authorization、fanout、history 与 E2EE 只使用已验证的签名
scope_ref;对象 projection 可复制该值但不得反向覆盖 Event。
Conformance fixture 见 artifacts/fixtures/circle-scope-fixture.json,覆盖 None→None / 同scope→同scope / None→Some / Some→None / Some(A)→Some(B) 五种 rebind transition 与 schema-violation negative case。
6.3 Space 两个 scope 相关字段语义辨析
| 字段 | 影响对象 | 强制性 | 说明 |
|---|---|---|---|
Space.scope_circle_id | Space 对象自身的 metadata 与 structural relation facts | reducer-enforced | Space 自身的 title / parent / rank / contains 事实落在该 Circle scope;不使 Space 成为独立 Realm 边界,Space 仍是 authorization-transparent 容器,只是它的 metadata 被该 Circle 的投递 / history / encryption profile 约束。 |
Space.child_scope_policy | 任何 placement / move 进入该 Space 的子对象 | reducer-enforced | allow_any / require_e2ee / require_same_scope / require_scope_circle_id 之一,见 §7。是真正的”该 Space 只接受这种 scope 的子对象”硬约束。 |
实现必须区分 scope_circle_id(Space 自身 metadata scope)与 child_scope_policy(子对象 placement 硬约束),二者不可互相替代。
7. Realm-default scope、Circle scope 与加密覆盖范围
Circle 是独立 effective scope。父 Realm 只提供创建/管理授权与 current
membership intersection;不得提供 history_access、MLS group、epoch、secret、snapshot 或 counter fallback。
Circle 的加密激活点是本 Circle 自己的 accepted ak.mls.genesis,与 Realm-default scope 的激活互相独立且均不可逆。
已激活 Circle 的 current tree 仍含按 §9.1 已 effective-invalid 的成员 leaf 时(父 Realm 资格失效),按
../crypto-media/encryption-and-audit.md §2.4.1 处于 epoch_update_required,
停止新的 application send 与 Add,直到移除这些 leaf 的本 Circle winning Commit 生效;父 Realm Event 不推进本 Circle 的
key_access_revision。未激活 Circle 立即按 §9.1 effective Circle membership 拒绝 read/write,不生成 MLS transition。
7.1 Space child scope policy
Space 不拥有 membership / MLS group;Space.scope_circle_id 只是让 Space 自身 metadata 与 structural relation facts 落入某个 existing scope。为了表达”这个 Space 下不允许 plaintext Strand”或”这个 List 只能放 HR Circle 对象”,Space MAY 声明 placement policy:
| field | enum / type | 说明 |
|---|---|---|
child_scope_policy.kind | allow_any / require_e2ee / require_same_scope / require_scope_circle_id | 子资源 scope 约束。 |
child_scope_policy.scope_circle_id | id:circle | kind=require_scope_circle_id 时必填。 |
Reducer MUST 在 ak.strand.create、ak.strand.move、ak.space.parent、structural contains projection 写入时检查 effective child scope policy:
allow_any:不额外限制。require_e2ee:子资源effective_scope必须 MLS-backed。require_same_scope:子资源effective_scope必须等于 Space 自身effective_scope。require_scope_circle_id:子资源scope_circle_id必须等于指定 Circle。
客户端创建子资源时必须显式选择 scope_circle_id;需要强制约束时使用 child_scope_policy 表达,不存在 reducer 无法验证的 Space 级默认 hint。
该 policy 是一个已登记的 typed current result family,不是 create-locked 的对象成员:family 名为 space_child_scope_policy,以 SpaceId 为 subject(special form space_child_scope_policy:<space_id>),result schema 见 typed-current-result.schema.json#/$defs/space_child_scope_policy_result。值是本节那个封闭 policy 对象或 null;null 是「未声明」状态,与 allow_any 同样「不额外限制」,但 reducer MUST NOT 把缺席的成员合成成 {"kind": "allow_any"} 对象——投影只能写签名 Event 或 envelope 提供的值,null 是缺席状态的唯一登记写法。
写入方恰好两处:
ak.space.create.result_writes[]:签名object.child_scope_policy存在时写该对象,缺席时写null。ak.space.update.result_writes[]:专用的非 patch 写,条件化在space_patch_payload顶层的child_scope_policy成员上。该 payload 用propertyNames.not禁止通用patch触及这条路径(含带点路径),并用anyOf: [{required: ["patch"]}, {required: ["child_scope_policy"]}]把二者分成两条不同的准入路径而不是同一件事的两种写法:policy 成员选择 security execution,合并事件是原子的且要求 security finality。成员缺席表示这条 Event 走 metadata patch 分支、在本 family 上零写入,不表示allow_any。
reducer-managed-path-registry.json 里 child_scope_policy 一行的 owner_kind: result_family / owner: space_child_scope_policy 指向的就是上面这两条写。
7.2 “宽 synthesis + 窄 discussion” 场景如何表达
需要”公开锚 + 私密讨论”组合时,MUST 用 两个 Strand + Relation 表达;Strand 永远单一 scope,不存在 per-track 安全边界:
Strand F_public (scope_circle_id = null) ← 公开 authority commit Strand,承载 metadata.title / metadata.summary / stage / metadata.fieldsStrand F_private (scope_circle_id = ak:circle:AUD2WOhX-Xh47vBHtRJPMRfXRQXGiOWQqOrJGJnE8CaI; short_name=HR-Conf) ← Circle 内 Strand,承载敏感讨论与决策细节F_private --confidential_discussion_of--> F_public客户端 UI MAY 把这两个 Strand 在视觉上”组合显示”(同卡片标题区 + 切换 tab),但协议层它们是两个独立对象,各自有独立的:
- 时间线、消息历史
- 成员、history visibility、投递 / 查询裁剪;若对应 scope 为 MLS-backed,则各自使用对应 MLS group(F_public 用 Realm-default,F_private 用 Circle MLS)
- watch typed current result、stage、生命周期
- 投影裁剪规则(无 Circle 成员的 Realm 成员只看到 F_public,看不到 F_private 的存在或活动元数据,符合 §9.1 投递不变量)
confidential_discussion_of 是标准 weak-semantic Relation kind(详见 relation.md),关系事实 MUST 存放在 F_private 的 Circle scope 内。这样 private 成员能从 private Strand 回到 public authority commit;非 Circle 成员不会在 public Strand 上看到”存在一个私密讨论”的可枚举边。
8. Capability 与授权评估
Capability actions:
| action | risk_tier | target event kinds | 说明 |
|---|---|---|---|
ak.circle.create | medium | ak.circle.create | 创建 Circle。默认不在普通成员 bundle 中(防止 Circle 滥用稀释 UX)。 |
ak.circle.manage | medium | ak.circle.update, ak.circle.archive, ak.circle.restore, ak.circle.tombstone | 管理已存在 Circle。 |
ak.circle.member.add | low | ak.circle.member.state(payload.member_id == envelope.actor_id,且 transition 合法) | 加入 join_rule=public 的 Circle 或自助退出;不得自助解除 ban。v1 join_rule=invite 由持有 ak.circle.member.add.others 的管理员显式加入,不存在 Circle pending invite / accept workflow。 |
ak.circle.member.manage | medium | ak.circle.member.state(payload.member_id != envelope.actor_id) | 邀请/移除他人;Circle admin 持有。 |
ak.circle.member.add.others | high | 同上 + 强制带 ak.audit.accessed 配对(与 ak.strand.watch.set.others 同模式) | 跨成员代写(罕用),审计配对。 |
ak.circle.audit | high | 空(read-only),配对 ak.audit.accessed | 不属于 Circle 的 Realm admin 读取 Circle 元数据 / activity rollup 的审计权。 |
授权评估两层 AND:
authorized(actor, action, object) ⇔ capability_grant(actor, action) ∧ (effective_scope(object).kind == "realm" ∨ actor ∈ Circle(effective_scope(object).circle_id).members[at object.causal_checkpoint])其中 effective_scope(object) 对 durable Event 使用 immutable signed scope_ref,对 materialized object 使用创建 Event scope 或当前 scope_circle_id 的规范派生。capability 决定能否执行,Circle membership 决定作用域资格;任一不满足都拒绝。
Circle 管理类 grant MUST 显式约束到 allowed_circle_ids / circle_id selector,或由 Circle 自身的 admin typed current result 派生;不得把无约束的 Realm-wide ak.circle.manage 当作普通管理权限发放。Realm admin 需要读取 Circle 正文或成员细节时 MUST 走 ak.circle.audit + ak.audit.accessed 配对路径;MLS-backed Circle 中还不能获得历史解密 key,除非被正式加入该 Circle。Plaintext Circle 不存在历史解密 key,但仍不得绕过 Circle membership / audit gate 直接投递或查询。
Realm 管理权交接与 Circle 隔离(normative):Realm ownership / admin capability transfer 只转移 Realm 治理能力,MUST NOT 隐式创建任何 ak.circle.member.state、MUST NOT 把接手管理员加入既有 Circle、MUST NOT 赋予既有 Circle 的历史读取 / 解密资格,也不是交接前必须完成的前置条件。若产品希望新管理员继续创建新的 Circle,应在交接 bundle 中显式授予 ak.circle.create(或等价的产品管理员角色中显式包含该 action);这不影响任何既有 Circle。若需要新管理员接管某个既有 Circle 的 lifecycle / membership 管理,必须对该 Circle 显式签发带 allowed_circle_ids 的 ak.circle.manage / ak.circle.member.manage grant;若需要其参与内容讨论,则必须按 §9.1 写入明确的 Circle membership transition。实现 MAY 在交接向导中提示“可选移交哪些 Circle 的管理/成员资格”,但 MUST NOT 要求“把目标管理员加入所有 Circle”作为 Realm admin transfer 的协议条件。
9. Membership 与 Lifecycle
9.1 Membership 拓扑
硬不变量:
-
Circle.members ⊆ Realm.members,且每个 Circlejoin绑定一个确切的父 Realm join 实例。ak.circle.member.state的membership="join"payload MUST 携带 producer 签名的parent_membership_revision:同一完整member_id在父 Realmmember_statetyped current result 的 exactrevision({commit_id, stream_position}),即接纳建立该 member 当前父 Realmjoin的 Event(ak.member.state{join}或ak.invite.accept)的那条 Realm stream RealmCommit。其它 transition MUST NOT 携带该字段;缺失或多带均为schema_violation。治理 Station 在接纳事务内于同一 cut 读取父 Realm 该 member 的 currentmember_state:值不是join,或其revision与 payload 的parent_membership_revision不逐字段相等时,MUSTfailed_preconditionreason=circle_member_must_be_realm_member并零写入。该字段是 producer 签名输入,不是 reducer 派生成员:replica 与 snapshot 消费方无法重算跨 stream 的接纳基线,只能读取签名值。它取自任何父 Realm 成员可读的 typed current,producer 无须读取可能位于自身 readable floor 之下的父 join Event 原文。 -
Effective Circle membership(normative):actor 在 Circle C 为 effective member,当且仅当在同一 durable cut 上同时满足:(a) C 的 canonical
circle_member_statecurrent 为join;(b) 父 Realm 该 member 的 currentmember_state为join,其source_stream_ref是父 Realm stream,且revision逐字段等于 (a) 值中的parent_membership_revision;(c) 父 Realm effective membership 的其余条件(例如 Agent 的 controller binding、lifecycle 与 provision/accountability 绑定)成立。所有 Circle 授权、投递、history、MLS material 与 send gate 中的「Circle member」均指此判定。父 Realm
leave/ban一经接纳,父 current 的 revision 即改变,该 actor 在该 Realm 全部 Circle 的旧join从同一 Realm Commit 起同时 effective-invalid;该门不等待任何清理 Event,也不依赖缓存。父 Realm 之后的 rejoin 产生新的 RealmCommit 与新 revision,旧 Circlejoin永远不因此复活。只有显式写入携带新parent_membership_revision的新 Circlejoin才恢复资格;canonical Circle 值仍为旧join时,因join -> join非法,恢复先由本人或 Circle 管理者写显式leave,再写新的join。reducer、Station 与数据库 trigger MUST NOT 合成 Circleleave,MUST NOT 改写 canonical Circle 行或其 revision;Circle Event 只在 Circle stream 提交,父 Realm Commit 的 position 不得冒充 Circle stream position。已激活 MLS 的 Circle 如何移除失效 leaf 见 §7 与../crypto-media/encryption-and-audit.md§2.4.1;未激活 Circle 无 MLS transition。接纳与复制边界(normative):current governance Station 在每个 Circle stream 接纳事务中以已 committed 的父 Realm current 求值上式,并立即阻止 effective-invalid actor 的新 live 投递、读取与写入;分区的非 authority Station 只能排队/转发,不能按缓存临时接纳。判定比较的是 Commit 身份:
commit_id是完整 RealmCommit body 的内容寻址摘要,已承诺 Event、stream 与 position,它与父 Realm stream 上的 position 一起逐字段比较;Realm stream 与 Circle stream 各自从 0 编号,数值相同的 position 互不相关,MUST NOT 互认,也不得用跨 stream 的 position 大小、墙钟或到达顺序推断父资格。本地 Realm 副本是否已覆盖parent_membership_revision可以按同一 Realm stream 的 position 判断,这是同流比较。Snapshot、committed replication 与冷启动(normative):该判定只依赖同一 durable cut 的两条已有 typed current——父 Realm
member_state与含parent_membership_revision的circle_member_state;不存在也不需要另一份持久派生关闭事实。签名 Snapshot 的current_state_entries取自同一 cut,消费方直接比较;committed replication 与冷启动 hydrate 装入同一组 typed current 后按同一式子比较,不需要读取父 join Event 原文。replica 的 Realm 副本尚未覆盖该 revision(Circle 先到、Realm 滞后),或本地父 current 已是另一 revision 时,该 actor 判定为 effective-invalid 并失败关闭:本地不据此授权 Circle 读取、投递、MLS material 或写入,直到 Realm 副本推进后重新求值;已 accepted 的 Circle Event 与 canonical 行照常保存,不改写、不丢弃。成员站以 Circle join 开流的 bootstrap 规则见../sync/federation.md§4.1.1。历史 cut 的成员连续性见../governance/history-visibility.md§3.1。对应 conformance vector 是
ak.vector.circle.parent_membership_revision.v1。
Circle membership 使用 common-fields.md §4.5 的共享 materialized membership FSM,完整 member_id: ActorId 是 typed current result key。申请正文 MUST NOT 进入 member-state Event;部署若需附加私密材料,必须通过独立的加密扩展通道传输。
Circle 与 Realm 共用 $defs/membership_state 单一枚举真源和同一 transition graph;差异只由 scope guard 表达,不再维护第二张转换表。join -> join 与其余 same-state transition 一样非法。membership 与物理 lifecycle state 正交,不受 common-fields.md §5.1 的 lifecycle same-state 规则覆盖。需要幂等重试的 producer MUST 基于当前 membership state 重新提交合法 transition,而非重放 same-state 写入。
expected_membership 的三态语义(normative):ak.circle.member.state 的可选
expected_membership 是并发写入下的乐观保护,与 transition guard 正交,且三种 wire 形态互不等价:
- 省略:不施加 CAS。合法性完全由本节 FSM transition guard 判定。这与
strand-and-message.md§2 的expected_default_strand_id不同—— 那里的 typed current result 是无 FSM guard 的current-value projection,省略必须归一为expected_revision null;这里的非法转移 已由 guard 拒绝,因此省略不会导致无条件覆盖。 - 显式
null:断言该 actor 当前在本 Circle 没有任何 membership 记录,即这是首次写入。 已存在任意 membership 时 reducer MUSTfailed_precondition。 - 具体枚举值:断言当前 membership 逐字等于该值,不等时 MUST
failed_precondition。
因此实现 MUST NOT 把”省略”与”显式 null”折叠为同一状态:前者放弃 CAS,后者是一个会失败的断言。
producer 类型系统 MUST 保留 Missing / Null / Value 三态,普通二态 optional 会丢失该区分。
9.2 Lifecycle cascade
Circle lifecycle 的转换与 reason_code 见本文件 Circle lifecycle 合同入口。
Circle 不定义 ak.circle.freeze 或 ak.circle.destroy;父 Realm 的 freeze / destroy 在父边界统一生效,Circle 不持有独立 federation identity 或 successor 语义。
因为对象只有单一 scope,lifecycle cascade 简单:
| 场景 | Realm-level / 未 scope 对象 | scope_circle_id 指向该 Circle 的对象 |
|---|---|---|
| 父 Realm tombstone | 按 Realm lifecycle 停止 | Circle 的 canonical lifecycle typed current result 保持原值,但 effective lifecycle 由父 Realm terminal Event 派生为 realm_terminal;不得合成 ak.circle.tombstone 或未登记的 Circle typed current result write。Circle 与其对象停止,后续写入统一拒绝 active failed_precondition;tombstone 到 successor Realm 时不会自动迁移 Circle membership / MLS key / history grant |
| 父 Realm freeze | 所有非豁免新写入按 Realm §2.6.0 拒绝 realm_frozen | Circle-scoped 新写入同样按 realm_frozen 拒绝;Circle 本身不定义独立 freeze,也不得用 Circle capability 绕过父 Realm freeze |
| 父 Realm archive | 按 Realm 默认隐藏 / 只读投影,可由 Realm restore 恢复 | Circle 与其对象遵循父 Realm archive 的默认隐藏 / 只读投影;不额外 tombstone、不改 membership / MLS eligibility,Realm restore 后恢复到 Circle 自身 lifecycle 决定的状态 |
| Circle archive | 不受影响 | receiver 获知 archive 关闭后,新写入 MUST fail closed(failed_precondition, reason=circle_not_active),含新建以该 archived Circle 为 scope_circle_id 的对象;此前暂时接纳的 Event 按关闭集合重算历史资格。既有对象保持历史可读/可审计投影,但不得继续追加 Message / Morph / structural Relation / position update,直到 ak.circle.restore 使 Circle 恢复 active |
| Circle tombstone | 不受影响 | receiver 获知 tombstone 关闭后,新对象写入 MUST circle_not_active;此前接纳的历史按关闭集合重算。projection 显示 scope unavailable;scope_circle_id 不会被自动 rewrite |
| 父 Realm 修改 history access | 不改 Circle history access | Circle 保持自身当前 facet;仅父 Realm current membership intersection 继续生效,Circle 的 MLS 激活状态独立 |
| Circle history visibility 收紧 | 不受影响 | 投影、watch、message read/write 按新状态重新裁剪 |
scope_circle_id 改绑 | — | 默认拒;profile 允许时 audit-paired,新旧历史分段展示(见 §6.1) |
每个对象有唯一 scope,lifecycle 只需在该 scope 与父 Realm 两层间做判定,不存在跨双 scope 的组合表。
父 Realm terminal gate 的判定优先于 Circle 自身 lifecycle gate。因此父 Realm已
tombstone 时,即使 Circle canonical state 仍为 active,receiver 也 MUST 返回
failed_precondition,不得输出 reserved realm_terminal_state reason 或 circle_not_active;该优先级保证所有
实现对同一父 Realm terminal basis 产生相同错误形态。
ak.circle.restore(archived → active)后置条件(normative):archived 是可逆中间态,restore 的 membership / MLS 后置条件如下:
- archive 期间父资格失效仍生效:archived Circle 不冻结 membership。archive 期间父 Realm 被接纳的
ak.member.state -> leave/banMUST 照常按 §9.1 硬不变量 2 使该 actor 在该 archived Circle 的旧join立即 effective-invalid(canonical 行不被改写);restore 后该 Circle 的 effective membership 仍按 §9.1 判定式求值,不存在「archive 期间漏掉的 leave/ban 在 restore 后才补」的窗口。 - MLS-backed Circle 的 epoch 与 rotate:archive 期间 Circle 的 MLS group 不暂停成员变更语义——失效 leaf 使 send gate 按
../crypto-media/encryption-and-audit.md§2.4.1 进入epoch_update_required,移除它们的 repair Commit 照常可提交(与 plaintext Circle 只做 delivery 裁剪相对)。ak.circle.restore本身不强制引入额外 MLS rotate:若 archive 期间已有 winning Commit 移除全部失效 leaf,则 restore 不重复 rotate;否则 restore 后 send gate 仍保持epoch_update_required,恢复加密写入前 MUST 先由该 repair Commit 移除这些 leaf,保证 forward secrecy / post-compromise security 不因 archive→restore 出现空洞。 - restore 不改变既有对象的
effective_scope与历史 key eligibility;restore 只解除 §9.2 表「Circle archive」行的写入冻结(circle_not_active),使新 Message / Morph / structural Relation / position update 可继续追加。
9.3 Sync / 投递不变量
Scope 投递不变量:对任意事件
E满足E.effective_scope.kind="circle"且E.effective_scope.circle_id=C,Station sync surface MUST NOT 向在E的 committed Circle-stream position 处不属于C.members(按 §9.1 effective Circle membership 求值)的 actor 投递E的 envelope 或 payload。订阅 Realm R 等价于订阅 (R 的 Realm-level events) ∪ (∀C ∈ R.circles, 若 actor ∈ C.members 则 C 的 scoped events,否则 ∅)。
特例:
ak.circle.create的 authorization shell 是 Realm-level event,但 projection MUST 按directory_visibility裁剪。GET /_arkret/self/circles/{circle_id}的成功载体是闭合circle_read_view=oneOf(circle_view,circle_preview);list 的唯一数组键是circles,元素使用同一 union。只有同一 accepted cut 下同时属于父 Realm 与 Circle 的 caller 才得到完整circle_view。directory_visibility=members时,其他 caller 的 GET 与不存在 Circle 一律返回现有not_found错误 envelope,list 不列该项;不发可枚举的成功 locked stub。原生 Sidecar 的独立存在性隐私仍由ak.vector.sidecar.existence_privacy.v1覆盖。directory_visibility=realm_members且 Circlestate=active时,属于父 Realm 但不属于该 Circle 的 caller 只得到闭合circle_preview:circle_id、realm_id、visibility="realm_members"、display={color_token,symbol}、member_count_bucket、join_rule、opaque_commitment,不得携带 title、summary、short_name、member_ids、成员 DID、created_by、join history 或私有 Event 引用。非父 Realm 成员、未授权 caller、不可见/不存在对象的 GET 同一not_found错误代码和字段集合;list 均省略。archived/tombstoned的非成员预览也省略。当前 v1 未登记 Circle search operation。member_count_bucket按同一读事务中 §9.1 effective Circle membership 成立的人数计算(只计effective_at <=本次读 cut 的有效成员),固定区间为0、1、2-3、4-7、8-15、16-31、32-63、64-127、128+;不输出原始人数。opaque_commitment是 64 位小写十六进制 SHA-256,输入字节精确为 UTF-8ak.circle.preview.v1、单字节0x00、wirerealm_idUTF-8、单字节0x00、wirecircle_idUTF-8。它只承诺预览中已公开且不可由标题、短名或成员枚举的高熵 ID,跨 caller/读次稳定;不得用它证明私有 Circle Event 内容。- 不可见与不存在的 GET 必须使用同一代码、字段集合与
circle_locked_v1处理类别;实现不得依据存在性设置不同延时、重试或缓存响应类别。此处理类别不是固定毫秒值或密码学恒时承诺;验收比对完整错误 envelope 与路径处理类别,并对显著可区分的延时分支作负向检查。list 对这两者均省略且不得输出计数或占位。普通 Circle 的这两项要求分别由ak.vector.circle.directory_visibility_members_indistinguishable.v1和ak.vector.circle.directory_visibility_realm_members_indistinguishable.v1覆盖。 ak.circle.member.state仅投递给该 Circle 的成员 + 完成ak.circle.audit/ak.audit.accessed配对的 audit reader。
10. 加密 / RealmCommit 集成
每个 Circle 都有独立的 authority commit stream,Circle Event 只由该 stream 的 RealmCommit 排序与确认;父 Realm stream 与 Circle stream 之间不存在 predecessor 关系。未激活 Circle 不存在 MLS group。已激活 Circle 拥有自己的 canonical group、Genesis/Commit、Welcome delivery、leaf 与 snapshot;key-access revision 固定在自己的 Genesis。history_access 只可由专用
ak.circle.history_access Event 从 all_history_for_current_members 单向收紧到 since_join,立即作用于历史交付,
且不进入 MLS key-access revision;不存在 epoch ceiling 或 activation-time policy snapshot。
MLS secret 只保留在成员设备本地,不提供 backup、network delivery 或恢复密钥。
11. Scope-identity UX safety invariants
Circle 引入的最大实践风险是跨 Circle 上下文混淆:用户在 Circle A 的 Strand 工作,被通知 ping 到 Circle B 的 Strand,回复时误以为仍在 A 圈。这是真实泄露发生的瞬间。因为 Strand 是单 scope 的(§6),所以”我在哪个 Circle”等价于”我在哪个 Strand”,这反而让 UX 清晰:每个 Strand 的视觉身份就是它 scope 的视觉身份,不存在”同一 Strand 内 synthesis 一个色、discussion 另一个色”的混乱。
要让客户端能可靠区分 scope,以下信号 MUST 在 spec 层统一,不留给客户端各自发明:
- 颜色 token:同一 Circle 在 Alice 与 Bob 的客户端上必须呈现一致颜色,否则跨设备 social engineering 攻击成立。
- 短名:
short_name(如HR-Conf)相比裸 Circle ID(如ak:circle:01964...)更易人工识别;客户端 SHOULD 显示short_name以辅助 scope 识别。 - 符号 / glyph:无障碍 / 色盲场景的第二信号。
本节是 client-presentation safety conformance,不是 Circle wire shape 或 headless reducer contract。以下不变量只适用于向人类用户呈现写入、回复、转发、引用、mention、邀请或导航入口的客户端 surface。纯 headless SDK、webhook worker、自动化 agent runtime 若不向人类呈现这些入口,则本节呈现义务不适用;但它们向上层 UI 暴露 Circle 数据时 MUST 原样提供 display 与 effective scope,使实际呈现方能够履行本节。适用的客户端实现 MUST 满足以下可测试不变量。具体控件布局、文案与视觉形式是实现自由。
- 在任何会导致写入、回复、转发、引用、mention 或发送通知的入口,当前 effective scope MUST 可被用户区分;Circle scope 至少呈现
display.color_token、display.symbol与display.short_name中的两个互补信号。 - Plaintext Circle MUST 使用不会暗示 E2EE 的 glyph、标签或披露语义;MLS-backed Circle MAY 使用 lock/shield 类语义,但不得让 plaintext scope 与 E2EE scope 看起来等价。
- 跨 Circle 导航或同一 surface 内切换不同 scope 时,客户端 MUST 让用户感知这是跨 scope 转场;不得表现成同一 Strand 内的普通滚动或普通 tab 内容切换。
- Mention / invite / add-recipient 等候选交互 MUST 区分 Circle member 与非 member;不得暗示非成员会收到 Circle-scoped 内容。
- 跨 Circle 引用必须标识为“另一 Circle / 另一协作圈 / 另一作用域”或等价语义,不得使用“信任圈”措辞,且不得预览调用者无权访问的内容。
- “宽 authority commit Strand + 窄 discussion Strand” 的组合形态(§7.2)在 UI 上 MAY 渲染为同一工作 surface,但两个 Strand 之间的切换 MUST 表现为跨 scope 转场,不得表现为同一 Strand 内不同视图。
11.1 Agent Sidecar 与 Circle 的强制分离
Agent Sidecar 使用 sidecar.md 定义的原生 scope_ref.kind="sidecar"。Circle reducer、
membership、MLS group、目录与 lifecycle 均不得为 Sidecar 创建隐藏或系统管理对象。Sidecar 的 private-view
映射与显式发布规则也不得用于 Circle 内容;Circle scope 的信息只能按本文件定义的 membership/history/
delivery 边界流动。
12. 与既有概念的区分
| 概念 | 含义 | 主要承担 |
|---|---|---|
| Realm | federation/identity boundary | membership 主源、capability registry、federation route、Realm-default MLS group |
| Circle | intra-Realm 子事件 / 子消息边界 | 子集 membership、独立 history visibility、scope 投递 / 查询 / projection 裁剪;可选独立 MLS group |
| Group | principal 集合(capability subject,见 realm-and-space.md §4) | 在 capability grant / policy 中作为主体集合;不持有密钥 |
| Space | navigation 容器 | 导航/分组/Board/List;authorization-transparent;不持有 membership 或 key |
关键不混淆点:
- Group 是 principal 集合(谁能做事);Circle 是 资源 / 事件 scope(哪些事件进入哪个成员圈,必要时由哪组密钥保护)。两者正交。
- Space 是导航,即使
Space.scope_circle_id指向 Circle 也只表示”Space metadata 落在该 Circle scope”,Space 自身不是边界。
13. 规范性引用
- 父 Realm 与 capability 主源:
realm-and-space.md。 - Strand scope 字段定义:
strand-and-message.md§3 / §5。 - 跨 scope Relation 规则:
relation.md§4。 - 历史可见性枚举与 canonical 语义:
../governance/history-visibility.md。 - MLS 加密 / governance binding:
../crypto-media/encryption-and-audit.md。 - Circle schema artifact:
spec/v1/artifacts/schemas/circle.schema.json。
Circle lifecycle 合同入口
circle 的 lifecycle 以 contract registry 中对应 typed current result family 的 transition_contracts 与 Event result_projection 为转换真源;本节只定义对象组合规则,不复制转换表。archive 只从 active、restore 只从 archived 发起;非法源分别返回 circle_not_active / circle_not_archived;终态操作对已终态对象返回 circle_already_terminal。新的 same-state 写入不当作幂等成功,已接受 Event 的 exact replay 仍沿通用幂等合同处理。普通 update 只允许 active,不能隐式恢复对象。对象 redaction/terminal 优先于可逆 archive,restore 不能恢复已清除内容。缺对象或依赖时按 common-fields §5.1 保留 pending/replay。