feat(editor): block/unblock action + parity surface for the local blocklist (#162 A2b) - #187
Merged
Merged
Conversation
…cklist (#162 A2b) PR2 of the ADR-039 A2 local self-protection blocklist. PR1 (#186) landed the daemon enforcement + RPCs; this wires the human + AI peer surface (CLAUDE.md #3) to SET/CLEAR a local block. A follow-up adds the blocked-list display + introspection and closes #162. Mirrors the existing kb-member-remove / kb-set-policy chain end-to-end: - mae-core: CollabIntent::KbSetBlock + KbCollabAction::SetBlock (+ kb_ops conversion). - collab_bridge: CollabCommand::KbBlockPrincipal → JSON-RPC kb/block_principal | kb/unblock_principal (fire-and-forget like set_policy); disconnected → the usual "cannot manage KB" feedback. - Ex-commands :kb-member-block / :kb-member-unblock (empty fp opens the picker). - At-point `b` on a member row in *KB Sharing*, routed BEFORE the owner guard — blocking is NOT owner-gated (you may block even the owner; local-only to your daemon). - Scheme (kb-block-member)/(kb-unblock-member); MCP kb_block_member/kb_unblock_member (descriptions stress LOCAL-only, never propagated — distinct from kb_remove_member). - `b` kb-sharing keybinding; concept:kb-sharing help node documents the commands. Naming follows the established split: ex-command kb-member-*, Scheme kb-*-member, MCP kb_*_member. Tests (principle #14): block→blocked:true / unblock→blocked:false / missing-fp→no intent; at-point block queues the right intent; and the adversarial inverse — non_owner_can_still_block_self_protection (a viewer CAN locally block, where role/remove are owner-rejected). fmt + clippy --workspace --all-targets clean (incl GUI); cargo test --workspace green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cuttlefisch
added a commit
that referenced
this pull request
Jun 28, 2026
…loses #162) PR3 of the ADR-039 A2 local self-protection blocklist — closes #162. PR1 (#186) landed daemon enforcement; PR2 (#187) the human+AI action surface. This adds the display + introspection. Why a fetch (not the local replica): membership renders from the editor's local `kbc:` replica, but the blocklist is LOCAL-only on the daemon and never in the synced collection — so the editor cannot derive it and must fetch the authoritative state. An optimistic local mirror would miss blocks set in a prior session or by another frontend on a shared daemon. Daemon: - DocStore::all_kb_blocklists() + a read-only `kb/blocklist` query RPC returning `{ blocklist: { kb_id: [fp, ...] } }` (optional kb_id scopes to one). The ONLY way a client learns the local-only blocklist. Editor: - CollabState.kb_blocklists cache; a `send_blocklist_fetch` helper sent ON CONNECT (durable prior-session blocks) and AFTER each block/unblock (the user's own changes); reply → PendingResponseKind::Blocklist → CollabEvent::BlocklistUpdated → cache + repaint. Display-only; the daemon stays authoritative. - KbSharingEntry.blocked: Vec<BlockedView> in build_snapshot (label best-effort from the member replica; a non-member block falls back to the short fingerprint). - *KB Sharing* buffer: a foldable "Blocked (N)" section after Pending; at-point `B` → unblock (kb-sharing-unblock, not owner-gated); diagnostic.error theme key. - Introspection: all three actors share build_snapshot, so `blocked` flows into the kb_sharing_status MCP tool and (kb-sharing-status) Scheme primitive (#3/#8). Tests: daemon block-fence test extended with the kb/blocklist query oracle; editor blocklist_renders_blocked_view_with_member_label (bob stays a MEMBER and is listed Blocked with his label; a non-member block has empty label; the view emits a BlockedHeader + one Blocked row per principal). Daemon 120+51 tests + clippy clean; editor fmt + clippy --workspace + test --workspace green (incl GUI). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cuttlefisch
added a commit
that referenced
this pull request
Jun 28, 2026
…loses #162) (#189) PR3 of the ADR-039 A2 local self-protection blocklist — closes #162. PR1 (#186) landed daemon enforcement; PR2 (#187) the human+AI action surface. This adds the display + introspection. Why a fetch (not the local replica): membership renders from the editor's local `kbc:` replica, but the blocklist is LOCAL-only on the daemon and never in the synced collection — so the editor cannot derive it and must fetch the authoritative state. An optimistic local mirror would miss blocks set in a prior session or by another frontend on a shared daemon. Daemon: - DocStore::all_kb_blocklists() + a read-only `kb/blocklist` query RPC returning `{ blocklist: { kb_id: [fp, ...] } }` (optional kb_id scopes to one). The ONLY way a client learns the local-only blocklist. Editor: - CollabState.kb_blocklists cache; a `send_blocklist_fetch` helper sent ON CONNECT (durable prior-session blocks) and AFTER each block/unblock (the user's own changes); reply → PendingResponseKind::Blocklist → CollabEvent::BlocklistUpdated → cache + repaint. Display-only; the daemon stays authoritative. - KbSharingEntry.blocked: Vec<BlockedView> in build_snapshot (label best-effort from the member replica; a non-member block falls back to the short fingerprint). - *KB Sharing* buffer: a foldable "Blocked (N)" section after Pending; at-point `B` → unblock (kb-sharing-unblock, not owner-gated); diagnostic.error theme key. - Introspection: all three actors share build_snapshot, so `blocked` flows into the kb_sharing_status MCP tool and (kb-sharing-status) Scheme primitive (#3/#8). Tests: daemon block-fence test extended with the kb/blocklist query oracle; editor blocklist_renders_blocked_view_with_member_label (bob stays a MEMBER and is listed Blocked with his label; a non-member block has empty label; the view emits a BlockedHeader + one Blocked row per principal). Daemon 120+51 tests + clippy clean; editor fmt + clippy --workspace + test --workspace green (incl GUI). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cuttlefisch
added a commit
that referenced
this pull request
Jul 2, 2026
…e + parity (#187) (#261) * fix(collab): Checkpoint-3 remediation — dead config + timebox coverage + parity (#187) Findings from the Checkpoint-3 pre-dogfood whole-surface review (3 adversarial review agents, verified per principle #14). No confirmed exploit; the high-value findings are fixed here, the rest triaged to #260 (v0.16). 1. REAL BUG — dead daemon config (#7): daemon.toml [collab.sync] max_update_size_bytes was operator-documented but NEVER read — the per-update gate used a hardcoded const, so raising the limit had no effect (its sibling max_document_size_bytes IS wired). Fixed: DocStore now carries max_update_size (mirrors max_document_size) → wired from config in main.rs → the collab_handler gate reads doc_store.max_update_size() at all 3 sites. DEFAULT_MAX_UPDATE_SIZE (1 MiB) is the documented fallback when unset. Reconciles the E-era comment that wrongly called it a fixed const. Test: max_update_size_honors_config_and_falls_back_to_default. 2. FAIL-OPEN TEST GAP (#14): the derive-cache timebox path (ADR-042 valid_until horizon — the ONLY guard that expires a member without an op-log/blocklist change) had ZERO coverage; the prior test freezes and never crosses an expiry. Added derive_cache_drops_a_timeboxed_out_member_when_wallclock_passes_expiry: admit a member with expires_at=T, derive at T-1 (present, warms cache), derive at T+1 same op-log → member MUST be dropped. A regression (wrong horizon / <= slip) would keep serving a TTL-expired member; this pins it. 3. PARITY (#3): collab-share (had command + collab_share MCP tool but no Scheme prim, asymmetric with kb-share) and collab-sync now have named Scheme prims + scheme_api doc nodes. Manual rebuilt (959 nodes). 4. Rationale comment on STALE_TIMEOUT_SECS (presence liveness bound — cosmetic, stays a const per #7). Deferred to #260 (v0.16, none a blocker): recovery-key supersede unit test, P2P forged-genesis integration test, collab-join/collab-start prims, dedicated MCP tools. Refs #187, #260, umbrella #251. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deny): ignore RUSTSEC-2026-0194/0195 (quick-xml, wayland build-time) Two freshly-published quick-xml DoS advisories fail cargo-deny repo-wide (not caused by this PR). quick-xml reaches us ONLY as a build-time proc-macro dep of wayland-scanner (winit → wayland-client → wayland-scanner), pinned ^0.39 so the >=0.41 fix is unreachable without a winit ecosystem bump. wayland-scanner uses quick-xml to parse the TRUSTED, crate-shipped Wayland protocol XML at build time — never attacker-controlled input — so neither DoS threat model (untrusted XML) applies. Documented ignore, consistent with the existing winit/sled GUI-path advisory ignores. `cargo deny check advisories` → ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
PR 2 of 3 for #162 (ADR-039 A2 local blocklist). PR1 (#186, merged) landed the daemon-side enforcement (storage +
kb/block_principal/kb/unblock_principalRPCs + complete-mediation at all four derive sites). This PR adds the action + parity surface so both the human and the AI peer can set/clear a local block (CLAUDE.md #3). A follow-up PR3 adds the blocked-list display + introspection and closes the issue.Surface (mirrors the existing
kb-member-remove/kb-set-policychain)CollabIntent::KbSetBlock+KbCollabAction::SetBlock(mae-core) →CollabCommand::KbBlockPrincipal(collab_bridge) → JSON-RPCkb/block_principal/kb/unblock_principal(fire-and-forget, likeset_policy). Disconnected → the usual "Not connected — cannot manage KB" feedback.:kb-member-block <kb> <fp>/:kb-member-unblock <kb> <fp>(empty fp opens the KB Sharing picker).bon a member row in*KB Sharing*→ block. Routed before the owner guard — blocking is not owner-gated (you may block any principal, even the owner; the block only affects your own daemon).(kb-block-member KB FP)/(kb-unblock-member KB FP).kb_block_member/kb_unblock_member(descriptions stress LOCAL-only, never propagated — distinct from the globalkb_remove_member).bin the kb-sharing keymap;concept:kb-sharinghelp node documents the local-self-protection commands.Naming
Follows the established split (memory gotcha): ex-commands use
kb-member-*(kb-member-block), Scheme primitives usekb-*-member(kb-block-member), MCP tools usekb_*_member(kb_block_member).Tests (principle #14)
dispatch_kb_member_block_unblock_parse_args— block→blocked:true, unblock→blocked:false, missing fp → no intent.block_on_member_row_queues_local_block— at-point queues the right intent.non_owner_can_still_block_self_protection— the adversarial inverse: a non-owner (viewer) can locally block (self-protection is not owner-gated), where role/remove are rejected.Verification
cargo fmt --check+clippy --workspace --all-targets -D warningsclean (incl. GUI).cargo test --workspacegreen (incl. GUI).Part of #162.
🤖 Generated with Claude Code