Skip to content

B5 residue: inbound-event security hardening + persona→agent copy sweep#1688

Merged
wpfleger96 merged 4 commits into
mainfrom
brain/b5-residue
Jul 9, 2026
Merged

B5 residue: inbound-event security hardening + persona→agent copy sweep#1688
wpfleger96 merged 4 commits into
mainfrom
brain/b5-residue

Conversation

@wesbillman

Copy link
Copy Markdown
Collaborator

B5 residue: inbound-event security hardening + persona→agent copy sweep

Follow-ups from the #1667 review and the unified-agent-model workstream
(PLANS/UNIFIED_AGENT_RESIDUE_BACKLOG.md). Both commits Pinky-reviewed.

1. Security (b9b1869ec) — the two gaps flagged in the #1667 review

  • Inbound signature gate: reconcile_inbound_persona_event now verifies
    event signatures immediately after parse (parse_verified_inbound_event),
    before the store lock, retention rows, and every apply/tombstone branch.
    Everything downstream trusts event.pubkey (ownership routing, behavioral-
    quad application, tombstone scoping), so a forged pubkey dies at the door.
    This is the ONLY inbound agent-store mutation path (sweep of every non-test
    Event::from_json in-tree, independently verified in review); the observer
    decrypt path already verified, the archive pipeline is a separate
    store-only trust domain.
  • Tombstone owner scoping (NIP-09): parse_deletion_coordinate now
    requires the coordinate's owner segment to match the verified
    event.pubkey — a validly signed kind:5 naming another owner's coordinate
    no-ops instead of deleting their record.
  • Tests: tampered-reject, valid-accept, foreign-owner-reject; existing
    coordinate tests migrated to self-owned fixtures.
  • Perf note: the gate runs on every backfill event on first boot — ≤500
    schnorr verifies at ~100µs each, negligible.

2. Copy sweep (8d5619ca0) — Category-C persona→agent strings

User-surfacing strings that still said "persona" after the model unified on
"agent": mention-autocomplete fallback label, and the error-toast family
("agent {id} not found", "Built-in agents cannot be edited/deleted",
"Only built-in agents can be added to or removed from My Agents") across the
persona commands, ensure_persona_is_active (surfaces via agent start and
team create/update), and the catalog gate. The mock bridge throws the
verbatim Rust strings so backend/bridge string parity holds.

Deliberately unchanged (on record from review): internal identifiers
(persona_id, kind:30175 naming, slugify prefix) and three backticked
developer-facing diagnostic strings (env_vars.rs:336, agents_deploy.rs:67,
writeback.rs:79).

3. Decision record (8778cfc82)

Wes's two design calls, decided 2026-07-09 in-channel:

  • Definition-only create keeps requiring a runtime (1a) — already the
    shipped behavior; recorded, zero diff.
  • Avatar is definition-level identity (2a) — the instance dialog's
    read-only avatar now says so honestly instead of claiming the backend
    can't persist it. Companion decision: fix(desktop): persist agent avatar edits #1666 (per-instance avatar edits,
    written against the pre-B5 model) closed with rationale; it becomes the
    starting point if per-instance avatar demand appears later.

Testing

Rust 1121/1121, frontend 2266/2266 (post-rebase onto 46613dc), tsc/biome/check scripts/both fmt passes
clean; agents.spec + persona-env-vars e2e (the specs asserting these exact
strings) 15/15 on --project=integration.

wesbillman and others added 3 commits July 9, 2026 13:58
… their owner

Closes the two security gaps from the #1667 review (flagged follow-up PR
there; elevated because the behavioral quad now routes through this path):

1. `reconcile_inbound_persona_event` verified nothing — any process able
   to reach the command could hand it an event with a forged pubkey, and
   ownership routing, quad application, and tombstone scoping all trust
   that field. `parse_verified_inbound_event` now enforces `event.verify()`
   right after parse; a tampered or forged event dies before any store
   logic runs.

2. `parse_deletion_coordinate` discarded the coordinate's owner segment, so
   a validly signed kind:5 naming ANOTHER owner's coordinate would delete
   that record. NIP-09 scopes deletion to the record's author: the
   coordinate owner must now match `event.pubkey`, else the tombstone
   no-ops.

Tests: tampered-event rejection + validly-signed acceptance for the gate;
foreign-owner rejection for the tombstone; existing coordinate tests
migrated to self-owned fixtures (they now construct events whose signer IS
the coordinate owner, which is the only shape the parser accepts). The
persona-sync e2e specs already sign with the real owner key, so they
exercise the gate as-is.

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Category-C residue from the B5 unification — surfaces that still said
"persona" to users after the model collapsed to one "agent" term:

- Mention autocomplete fallback label (useMentions + mentionRanking):
  a candidate with no display name and no pubkey now labels as "agent".
- Error toasts: "agent {id} not found" (persona commands AND
  ensure_persona_is_active, which surfaces through agent start and team
  create/update), "disappeared unexpectedly", "Built-in agents cannot be
  edited/deleted", and the catalog gate "Only built-in agents can be
  added to or removed from My Agents". Rust command errors surface
  verbatim in UI toasts; the mock bridge throws the same strings so
  backend and bridge stay parity-aligned.

Deliberately unchanged: internal identifiers (persona_id, kind:30175
naming, slugify prefix) and developer-facing internals — this sweep is
user-visible copy only.

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
The instance edit dialog's avatar preview was annotated "backend can't
persist yet" — untrue since B5: the avatar is definition-level identity
by decision (Wes, 2026-07-09), edited on the agent profile and propagated
to linked instances. Say so. Companion to closing #1666, which implemented
per-instance avatar edits against the pre-B5 model.

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman wesbillman requested a review from a team as a code owner July 9, 2026 20:00

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Team review — 3/3 APPROVE, no blocking defects.

Both #1667 security findings are correctly closed:

  • Sig verification (parse_verified_inbound_event) runs at mod.rs:386, first line of reconcile_inbound_persona_event_blocking, before any store mutation. event.verify() at mod.rs:485-487 propagates on failure.
  • Tombstone owner scoping (parse_deletion_coordinate) checks owner != event.pubkey.to_hex() at mod.rs:510, returns None on mismatch. Foreign-owner deletions are silent no-ops.

Copy sweep verified 7/7, deliberately-unchanged strings confirmed 3/3.

Two non-blocking MINORs noted as inline comments below. CI 25/25 green at head 8778cfc82.


Defense-in-depth suggestion (non-blocking, future hardening): could also check event.pubkey == state.keys.public_key() backend-side before applying inbound sync, rather than relying solely on the relay authors filter + TS-side check at usePersonaSync.ts:31-33. The relay filter + sig gate is sufficient for the stated #1667 gap, but a backend check would make future caller drift or stale-subscription races harmless.

Copy residue outside this PR's scope (non-blocking, future sweep): e2eBridge.ts has pre-existing Category-C mock strings ("Core engineering personas" at line 1767, "Imported Persona" at line 6583, "references missing personas" at line 6499) not touched by this diff.

Comment thread desktop/src-tauri/src/commands/personas/inbound_tests.rs
Comment thread desktop/src-tauri/src/managed_agents/personas.rs
Two non-blocking review findings from the #1688 team review:

- personas.rs: "team personas" -> "team agents" in validate_persona_deletion
  error string (same user-facing copy family the PR already swept)
- inbound_tests.rs: add assert_ne! guard after the tamper string-replace so
  a future serializer whitespace change can't silently turn the test into a
  false-green by leaving the event un-tampered

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 merged commit f0e6558 into main Jul 9, 2026
25 checks passed
@wpfleger96 wpfleger96 deleted the brain/b5-residue branch July 9, 2026 21:24
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.

2 participants