Skip to content

feat(editor): block/unblock action + parity surface for the local blocklist (#162 A2b) - #187

Merged
cuttlefisch merged 1 commit into
mainfrom
feat/162-blocklist-editor-surface
Jun 28, 2026
Merged

feat(editor): block/unblock action + parity surface for the local blocklist (#162 A2b)#187
cuttlefisch merged 1 commit into
mainfrom
feat/162-blocklist-editor-surface

Conversation

@cuttlefisch

Copy link
Copy Markdown
Owner

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_principal RPCs + 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-policy chain)

  • Intent/command plumbing: CollabIntent::KbSetBlock + KbCollabAction::SetBlock (mae-core) → CollabCommand::KbBlockPrincipal (collab_bridge) → JSON-RPC kb/block_principal / kb/unblock_principal (fire-and-forget, like set_policy). Disconnected → the usual "Not connected — cannot manage KB" feedback.
  • Ex-commands: :kb-member-block <kb> <fp> / :kb-member-unblock <kb> <fp> (empty fp opens the KB Sharing picker).
  • At-point: b on 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).
  • Scheme: (kb-block-member KB FP) / (kb-unblock-member KB FP).
  • MCP: kb_block_member / kb_unblock_member (descriptions stress LOCAL-only, never propagated — distinct from the global kb_remove_member).
  • Keybinding + help: b in the kb-sharing keymap; concept:kb-sharing help node documents the local-self-protection commands.

Naming

Follows the established split (memory gotcha): ex-commands use kb-member-* (kb-member-block), Scheme primitives use kb-*-member (kb-block-member), MCP tools use kb_*_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 warnings clean (incl. GUI).
  • cargo test --workspace green (incl. GUI).

Part of #162.

🤖 Generated with Claude Code

…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 cuttlefisch added the release:none Skip version bump on merge label Jun 28, 2026
@cuttlefisch
cuttlefisch merged commit 1e011b9 into main Jun 28, 2026
14 checks passed
@cuttlefisch
cuttlefisch deleted the feat/162-blocklist-editor-surface branch June 28, 2026 23:14
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:none Skip version bump on merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant