Skip to content

2a: 订阅 = 成员资格(namespace_membership)— core 语义变更 - #50

Merged
Windsander merged 5 commits into
mainfrom
feat/namespace-membership
Sep 18, 2026
Merged

Windsander merged 5 commits into
mainfrom
feat/namespace-membership

Conversation

@Windsander

Copy link
Copy Markdown
Owner

2a · 订阅 = 成员资格(namespace_membership)— core 语义变更

把「订阅」从瞬时 hello 声明升格为持久、签名的图上成员资格,为 2b 的「退订=成员资格退出 + 继任者全量 ack 门禁」提供「谁是成员」的可复现答案。破坏性协议变更,走封板纪律:矩阵/设计先行 → 实现 → harness → 跨端 E2E。

设计(M1–M5)

  • M1 新记录 namespace_membership { member, namespace, active, issuedAt, note? }__policy__);两态:在册/注销。同 (ns,member)R-c 最新记录的 active
  • M2 生效成员 = 图上被采纳成员 ∧ 该设备对该分区的生效授权(授权默认拒绝不变;成员记录不放宽授权)。采纳无条件(不做 R-a)→ 与不动点无循环依赖;受 R-b(签发者/成员被吊销 → 不采纳)。
  • M3 裁剪链对端授权 ∩ 对端成员资格 ∩ 本机订阅声明;hello 订阅声明降级为活跃性/一致性校验,不一致 → 显式拒绝/告警sync-completed.membershipRejected,非静默)。
  • M4 公共 API/可观测Mebular.declareNamespaceMembership / getNamespaceMembership / getNamespaceMembers;fleet member/members CLI;doctor namespace 成员资格
  • M5:本轮 fleet 各 verify 脚本 temp 目录均在 finally 清理(已核对)。
  • 兼容规则(legacy-empty):某分区无任何被采纳成员记录时视为未启用成员资格,沿用 hello 订阅裁剪(不放松授权默认拒绝);一旦出现成员记录即成为该分区强制闸门。→ 既有 695 用例(无成员记录)零改动全绿,新语义 opt-in。

主张 → 证据

# 主张 证据
1 成员持久/确定/顺序无关/幂等 M1 ①·⑥ 同键最新·harness 排列/幂等
2 生效成员 = 成员 ∧ 授权(默认拒绝) M4 getNamespaceMembers·③ 成员无授权不流动
3 裁剪链用成员资格 + 显式拒绝 ② 非成员声明订阅 → membershipRejected
4 注销后新事件不再到达 (旧数据保留;清理属 2b)
5 伪造/非主密钥链成员记录忽略 ⑤ GraphNamespacePolicy 忽略
6 成员轴独立于授权 ⑥ 独立轴·harness 扰动 d residualD=0
7 跨端 E2E verify:fleet:membership 10/10
8 不回归 707 ×6、覆盖阈值全过、lint/build 无输出、CI 三 job 绿

红 → 绿(断言原文)

不变量 破坏点 锚点 结果
成员推导 deriveMembership 返回空 ① 采纳 active 成员… ✕ Expected true Received false → 还原 ✓
成员闸门 membership.members.includes(peer)true ② 非成员声明订阅… ✕ Expected ["nsA"] Received undefined → 还原 ✓
脚本类(S9,先 npm run build 同上「闸门恒开」+ rebuild E2E ④ …B 未收到新事件 FAIL {"before":3,"after":5} → 还原 10/10

harness 日志

[policy-invariants] scenarios=300 nonConverged=1 residualA=0 residualB=0 residualC=0 residualD=0

(随机含成员记录;扰动 d:删全部成员记录 → authorized/revoked 完全不变且成员集合清空。)

跨端 E2E 原始输出(verify:fleet:membership,10/10)

PASS  A 声明成员(自证 + B)
PASS  members 查询:生效成员(∩ 授权)含 device-B
PASS  doctor:namespace 成员资格 PASS(本机在册)  {"members":[device-A,device-B],"self":device-A}
PASS  ① 成员 ∧ 授权 → B 执行 N 次  {"lines":3}
PASS  ① A 收齐结果 resultsMatch
PASS  ④ A 注销 device-B  {"active":false}
PASS  ④ 注销后 members 不再含 device-B  {"members":[]}
PASS  ④ A 重新在线(新 multiaddr)
PASS  ④ A 在线且已完成派发等待,但 B 未收到新事件(exec 不增)  {"before":3,"after":3}
FLEET_SUMMARY {"total":10,"passed":10,"failed":[],"skipped":[]}

SEALING / 矩阵 diff 要点

  • POLICY-INVARIANTS.mdnamespace_membership 记录行(采纳/注销/最新胜出/R-b/伪造);跨切不变量 9(无循环、确定、成员∧授权、独立轴、legacy-empty、显式拒绝);harness 扰动 d
  • SEALING.md:§3 事件类型 + M1–M3 语义 + 破坏性说明(旧节点忽略 → 行为不变/少收,安全方向);裁剪链 bullet 改为 授权 ∩ 成员资格 ∩ 本机订阅;§4 把「订阅=成员资格」改为「退订交接/重订阅恢复(2b/2c)」。

给 2b 的接口/语义清单

  • Mebular.getNamespaceMembership(ns) → { active, members[] }active=false = 该分区未启用成员资格;members = 图上在册(未 ∩ 授权)。
  • Mebular.getNamespaceMembers(ns) → string[]生效成员 = 在册 ∩ 生效授权(2b 的“继任者集合”)。
  • Mebular.declareNamespaceMembership({ member, namespace, active, note? }):入/离的状态钩子(本轮只改集合)。
  • SyncResult.membershipRejected?: string[]:成员不一致的显式信号;事件 sync-membership-rejected
  • GraphNamespacePolicy.getNamespaceMembership(ns)NamespaceMembershipPolicy 接口(供 SyncManager 注入)。
  • 语义边界:注销 ≠ 数据清理--leave 仅移出成员集合);退订交接/重订阅恢复、继任者全量 ack 门禁留 2b

未做项 / 风险

  • 2b/2c 未做:注销后的数据清理、继任者全量 ack 门禁、重订阅恢复(本轮不声称已实现退订交接)。
  • 旧节点混跑:旧端把新分区始终视为「未启用成员资格」→ 行为不变或少收(安全方向,不 fail-open);全端升级后成员资格严格生效。
  • legacy-empty 取舍:分区由「第一条成员记录」激活为强制闸门;这是可观测的确定性状态(收敛后各端一致),但第一跳存在短暂不一致(安全方向为少收)。已在报告中论证并保持默认拒绝。
  • 成员资格不参与授权推导(独立轴),无越权放宽;伪造记录被主密钥信任链过滤。

测试方式

npm ci && npm run build
npm run test:coverage
npm run verify:fleet:all
node scripts/verify-fleet-membership.mjs

POLICY-INVARIANTS:新记录行、跨切不变量 9(无循环/独立轴/legacy-empty/显式拒绝)、扰动 d、harness 含成员轴。
SEALING §3 事件类型 + M1–M3 语义 + 破坏性说明;§4 改「退订交接」为 2b/2c。
grantPolicy:NamespaceMembershipRecord/NAMESPACE_MEMBERSHIP_EVENT;deriveMembership(无条件采纳、R-b、R-c 最新胜出);PolicyState.members/membershipNamespaces;GraphNamespacePolicy.getNamespaceMembership。
namespacePolicy:NamespaceMembershipPolicy 接口。
SyncManager:membershipPolicy 槽;裁剪链改用对端成员资格(legacy-empty 退化为对端订阅声明);成员不一致显式 membershipRejected。
Mebular:declareNamespaceMembership/getNamespaceMembership/getNamespaceMembers(∩ 授权)。
BREAKING CHANGE: 分区一旦有成员记录,非成员不再收到该分区数据。
membership.test:成员∧授权同步、非成员显式拒绝、成员无授权不流动、注销后新事件不到、伪造忽略、独立轴/最新胜出。
harness:随机含成员记录;扰动 d(成员轴独立于授权);日志 residualD=0。
onboard:setNamespaceMembership/namespaceMembers/namespaceMembership。
cli:fleet member --to [--namespace] [--leave] / fleet members。
doctor:namespace 成员资格(未启用 SKIP / 在册 PASS / 否则 FAIL)。
E2E:A 成员 B + B 成员 A → 派发成功;A 注销 B(换端口重挂在线的 A)→ B 不再收新事件。
doctor skipped 期望补 namespace 成员资格;fleet 单测补成员 API/doctor 分支。
@Windsander
Windsander merged commit 186528a into main Sep 18, 2026
6 checks passed
@Windsander
Windsander deleted the feat/namespace-membership branch September 19, 2026 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant