Skip to content

feat(sync): daemon-layer auto-grant on subscription (killswitch-gated) - #122

Merged
Windsander merged 2 commits into
mainfrom
feat/auto-grant-subscription
Oct 15, 2026
Merged

Windsander merged 2 commits into
mainfrom
feat/auto-grant-subscription

Conversation

@Windsander

Copy link
Copy Markdown
Owner

守护层自动签发(订阅驱动授权)· 批准版实施

按已批准提案(core-autosign-proposal.md)实现,范围=批准版:core 引擎(策略推导/不变量/存储/同步热路径)零改动;不含 hello 登记表(产品决定第一轮 core 零触碰,legacy 域维持中性态+CLI 兜底)。

改动面(git diff --stat 自证,6 文件 +768/-1)

文件 内容
src/sync/autoGrant.ts(新) 评估器纯函数:规则链(可信→未吊销·红线→已声明订阅→无生效 grant→跳过 self→R-a 签发资格)
src/mebular.ts sync.autoGrant 配置(库默认关/守护默认开,照 antiEntropy 形状)+ 生命周期(首评/周期 30s/会话结束去抖 2s/关停)+ 吊销集合只读转发
packages/mcp/bin/mebular.mjs + packages/mcp/src/config.mjs serve 默认开(init 生成配置 + createMebular 缺省,成对照 antiEntropy 先例);kill-switch=sync.autoGrant.enabled:false
tests/sync/auto-grant.test.ts(新,+16) 规则链/红线/竞态序/非信任链/幂等/多供给端 + 门面集成(订阅→放行→数据;屏蔽→订阅→拒绝;事件接线;开关)
scripts/accept-auto-grant.mjs(新) 真机对抗 15/15(真实守护供给端 × in-process 订阅端)

红线加固(产品特别要求,全部落地)

  1. 未吊销为规则链硬前置(snapshot().revoked 排除)——R-d 恢复语义(grantPolicy.ts:436-437:被采纳 grant 清除吊销)决定不查吊销=屏蔽崩塌;
  2. 签发前对 subject 再查一次吊销(confirmNotRevoked,新鲜推导)——竞态窗口压到最小;
  3. 竞态恢复序单测固定:grant→device_revoke 重新吊销 ✓、device_revoke→grant 清除吊销(红线存在的原因)✓。

验收

  • 单测 16/16;npm test 1150 → 1166(+16 全绿,不降);verify:daemon 43/44(唯一失败=既有环境项,与基线一致);check:docs 7/7、surface-parity 11/11、cleanliness 0/0、deps 4/4;lint 净。
  • 真机对抗 15/15(node scripts/accept-auto-grant.mjs,双守护 loopback 沙箱):
    • ①【红线】屏蔽→订阅→零 grant、生效授权仍 []、屏蔽后新数据不到达;
    • ② 订阅→自动放行→数据到位;留证记录 {"subject":"device-C","namespaces":["work"],"note":"auto-grant: subscription-driven"};幂等(恰一条);
    • ③ 开关关→零自动签发(默认拒绝),CLI 兜底可用;
    • ④ 老订阅端(只 hello)→中性态不自动签+CLI 兜底;老供给端≍开关关。
  • 六类审计事件契约不变(零新增事件类型);auto 标记走既有 note 字段(collectPolicy 已透出,admin 视图可见/可按 note 过滤)。
  • verify-console.mjs:578(rejoin 409)未受影响(评估器跳过 self,rejoin 语义冻结;全量套件含该断言为绿)。

⚠️ 前存缺陷报告(非本分支引入,按纪律未修)

实施中发现:delegated 守护 → root 守护的设备自签事件(membership 等)永不落图。origin/main(9505dbc7)上同一脚本完整复现:C 视 A 的 peerId 为拨号地址里的 libp2p id(12D3KooW…),A 视 C 为握手呈现的 hex 摘要——疑似对端键控非对称使 nudge/常驻会话静默失配;verify-daemon-cluster 只覆盖 A→B 方向故从未暴露。探针实证:facade↔daemon 三个组合均正常,仅 daemon×daemon 此方向失败。修复需动 SyncManager/P2PNode 连接键控(超出本轮批准文件清单),请主控/产品单独立项。本 PR 的真机验收因此采用「真实守护供给端 × in-process 订阅端门面」(真 libp2p/签名/存储;探针已证该方向可用)。

设计偏差说明(提案 → 实现)

事件触发源由提案的 events-applied('__policy__') 改为 sync-completed 去抖:events-applied 只对物化进图存储的事件计数,__policy__ 记录不经 applyRemoteEvent 物化(实测证实不发),在不碰 SyncManager 的约束下 sync-completed 是唯一可靠触发点(membership 只能经同步会话到达)。时延语义不变(会话结束 +2s)。

待 CI 五项绿后由用户合并(我不合并)。

opencode added 2 commits October 15, 2026 19:35
Implements the approved "守护层自动签发" proposal: the daemon evaluates the
policy graph and issues grants through the SAME chain as manual approval
(Mebular.grantNamespaces) for devices that are 图上在册 (subscription =
membership, M1) ∧ not revoked ∧ without an effective grant — the "批准" step
becomes automatic while the policy engine (derivation / invariants / storage /
sync hot path) stays untouched.

- src/sync/autoGrant.ts (new): pure evaluator. Rule chain: trusted (implicit
  via snapshot trust root) →【红线】not revoked (snapshot.revoked hard
  precondition; R-d recovery semantics mean an adopted grant CLEARS
  revocation, so auto-issue must never target revoked devices) → subscription
  declared (active membership) → no effective grant (idempotent) → skip self
  (rejoin semantics frozen this round) → issuer qualification (R-a local
  pre-check: bootstrap issuer or self authorized). Red-line hardening per
  product: re-check revocation per subject right before issuing
  (confirmNotRevoked) to minimize the snapshot→append race window.
- src/mebular.ts: sync.autoGrant config ({enabled, intervalMs}; library
  default OFF, daemon default ON — antiEntropy-shaped) + lifecycle wiring:
  first eval (next tick, init-safe) + periodic sweep (30s) + debounced eval
  on sync-completed. Trigger source note: events-applied does NOT fire for
  __policy__ records (applyRemoteEvent only materializes node/edge/tag
  types), so the debounce hooks sync-completed instead — zero SyncManager
  changes (scope红线). shutdown/rollback stop the evaluator.
- packages/mcp: serve defaults autoGrant on (config.mjs createMebular
  fallback + init-generated config.json), kill-switch via
  sync.autoGrant.enabled=false in config.json.
- tests/sync/auto-grant.test.ts (new, +16): rule chain, red line (incl.
  confirm-veto), race-ordering semantics (grant→revoke re-revokes; revoke
  →grant clears — why the precondition exists), untrusted records, self/
  qualification/idempotence/multi-supplier, facade E2E (subscribe → auto
  release → data; block → subscribe → denied; event wiring; killswitch).
- scripts/accept-auto-grant.mjs (new): real-daemon adversarial acceptance,
  15/15 — red line first, note='auto-grant' evidence, idempotence,
  killswitch, mixed-version neutral + CLI fallback.

Pre-existing finding (NOT introduced here, reproduced on origin/main, NOT
fixed per scope discipline): delegated-daemon → root-daemon device-authored
events never land (peer-id keying asymmetry suspected; verify-daemon-cluster
only covers A→B). Acceptance therefore pairs real supplier daemons with
in-process subscriber facades (real libp2p/signing/storage) — the direction
proven working by probes. Details in the PR body.

npm test 1150 → 1166 (+16, all green); verify:daemon 43/44 (baseline-equal);
check:docs/surface-parity/cleanliness/deps green; lint clean.
@Windsander

Copy link
Copy Markdown
Owner Author

真机对抗验收原始输出(node scripts/accept-auto-grant.mjs,head a195897,CI 五项绿)

PASS  A 守护就绪(root;autoGrant 配置省略=默认开)
PASS  ① A 屏蔽 B(revokeDevice)  {"status":200}
PASS  ①【红线】屏蔽后订阅 → 零 grant(含零 auto-grant)  {"grants":0}
PASS  ① 吊销保持:B 生效授权仍 [](R-d 未被解除)  {"ok":true,"device":"device-B","namespaces":[]}
PASS  ① 屏蔽后新数据不到达(post-block 节点)
PASS  ② A 自动签发 work grant(note=auto-grant 留证)
    留证 auto-grant 记录: {"grantId":"ba14f462-…","subject":"device-C","namespaces":["work"],"issuedAt":1792063672266,"note":"auto-grant: subscription-driven"}
PASS  ② 数据到位:C 收到 work 数据(订阅 → 自动放行)
PASS  ② 幂等:auto-grant 记录恰一条  {"autoCount":1}
PASS  ④a 中性态:legacy-ns 无 membership 记录 → 不自动签发  {"grants":0}
PASS  ④a 默认拒绝保持:D 看不到 legacy-ns 数据
PASS  ④a CLI 兜底:手工 grant 后 D 收到 legacy-ns 数据
PASS  ③/④b A2 守护就绪(autoGrant.enabled=false)
PASS  ③ kill-switch:开关关 → 零自动签发(回默认拒绝)  {"grants":0}
PASS  ③ 数据不放行:E 看不到 work 数据
PASS  ③ CLI 兜底:手工 grant 后 E 收到 work 数据
== accept:auto-grant ==
FLEET_SUMMARY {"total":15,"passed":15,"failed":[],"skipped":[]}

补充:①的「post-block」设计——B 在屏蔽前合法收到的旧数据不动(清理属 2b 轮),断言针对屏蔽后 A 新写的 A-notes-post-block 节点不到达。

@Windsander
Windsander merged commit 351bfc5 into main Oct 15, 2026
10 checks passed
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