Context
Split out of #157. The security review (docs/SECURITY_REVIEW.md §4, finding A2) originally read as
"local blocklist ignored in verify_content_op." On closer inspection the finding is re-scoped: the
local-blocklist deny-list is designed (MembershipView.blocklist, step 7 of
derive_valid_members_governed in shared/sync/src/membership.rs) but entirely unwired in production.
Not a live bypass. Every daemon derive site passes MembershipView::default() (empty blocklist):
collab_handler.rs::kb_access (the authz gate)
collab_handler.rs::verify_content_op
collab_handler.rs::verify_relayed_content_op (via the members derivation)
- the strong-removal derivation (
collab_handler.rs ~L862)
There is no DocStore storage for blocked principals and no RPC/command to set one — so the empty
default is consistent everywhere and no "admit a blocked principal" hole exists today. Global op-log removal
(Revoke/Remove) already works and propagates; A2 is the local self-protection override for a principal
you cannot get globally removed (e.g. you lack quorum) but want this daemon to stop trusting.
Goal
A durable, local-only (never propagated to peers — distinct from a signed op-log removal), per-KB
deny-list that is enforced at every membership-derivation point, with a human + AI management surface.
Prior art for "deny-lists must apply at every enforcement point": SPKI/SDSI (RFC 2692), UCAN revocation.
Design / task breakdown
A2a — daemon core
A2b — editor / parity surface (principle #3 human↔AI peer)
A2c — adversarial tests (principle #14)
Open questions
- Per-KB (recommended, matches
kb_anchor granularity + MembershipView derivation) vs global-per-daemon.
- Durability mechanism (local file vs storage-backend special key vs accept in-memory like
kb_anchor).
- Interaction with quorum governance (a local block is orthogonal — it never substitutes for op-log removal).
References
Context
Split out of #157. The security review (
docs/SECURITY_REVIEW.md§4, finding A2) originally read as"local blocklist ignored in
verify_content_op." On closer inspection the finding is re-scoped: thelocal-blocklist deny-list is designed (
MembershipView.blocklist, step 7 ofderive_valid_members_governedinshared/sync/src/membership.rs) but entirely unwired in production.Not a live bypass. Every daemon derive site passes
MembershipView::default()(empty blocklist):collab_handler.rs::kb_access(the authz gate)collab_handler.rs::verify_content_opcollab_handler.rs::verify_relayed_content_op(via the members derivation)collab_handler.rs~L862)There is no DocStore storage for blocked principals and no RPC/command to set one — so the empty
default is consistent everywhere and no "admit a blocked principal" hole exists today. Global op-log removal
(
Revoke/Remove) already works and propagates; A2 is the local self-protection override for a principalyou cannot get globally removed (e.g. you lack quorum) but want this daemon to stop trusting.
Goal
A durable, local-only (never propagated to peers — distinct from a signed op-log removal), per-KB
deny-list that is enforced at every membership-derivation point, with a human + AI management surface.
Prior art for "deny-lists must apply at every enforcement point": SPKI/SDSI (RFC 2692), UCAN revocation.
Design / task breakdown
A2a — daemon core
DocStore: durable per-KB blocklist (kb_blocklist(kb_id) -> BTreeSet<String>,add_kb_block,remove_kb_block). Durability decision required:kb_anchors/kb_metasare currently in-memoryonly (
RwLock<HashMap>) — a self-protection deny-list that evaporates on restart is weak, so prefer alocal persisted store (NOT inside the shared
kbc:CRDT collection, or it would sync to peers). Capturethe chosen mechanism in the PR.
membership_view_for(doc_store, kb_id) -> MembershipViewhelper that loads the blocklist; replace allfour
MembershipView::default()sites with it (complete mediation — a blocked principal must be deniedat the gate AND the content paths AND the removal derivation).
kb/block_principal+kb/unblock_principalRPC (params:kb_id,fingerprint). Operator-local,key-blind (no content). Authz: local operator (mirror the existing management-RPC principal handling;
decide owner-only vs any-authenticated — a local self-protection action is arguably any local operator).
A2b — editor / parity surface (principle #3 human↔AI peer)
kb-block-member/kb-unblock-member(+ leader binding under the KB-sharing prefix).(kb-block-member)/(kb-unblock-member).kb_block_member/kb_unblock_member.*KB Sharing*management buffer +kb_sharing_statusintrospection.A2c — adversarial tests (principle #14)
kb_accessandverify_content_op/verify_relayed_content_op(selective oracle: a non-blocked member's op still applies).kbc:collection and is NOT observed by apeer that syncs the same KB.
as self-lock.
Open questions
kb_anchorgranularity +MembershipViewderivation) vs global-per-daemon.kb_anchor).References
docs/SECURITY_REVIEW.md§4 (A2), §5shared/sync/src/membership.rs(MembershipView,derive_valid_members_governedstep 7)