B5 residue: inbound-event security hardening + persona→agent copy sweep#1688
Conversation
… 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>
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Team review — 3/3 APPROVE, no blocking defects.
Both #1667 security findings are correctly closed:
- Sig verification (
parse_verified_inbound_event) runs atmod.rs:386, first line ofreconcile_inbound_persona_event_blocking, before any store mutation.event.verify()atmod.rs:485-487propagates on failure. - Tombstone owner scoping (
parse_deletion_coordinate) checksowner != event.pubkey.to_hex()atmod.rs:510, returnsNoneon 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.
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>
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 reviewreconcile_inbound_persona_eventnow verifiesevent 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_jsonin-tree, independently verified in review); the observerdecrypt path already verified, the archive pipeline is a separate
store-only trust domain.
parse_deletion_coordinatenowrequires the coordinate's owner segment to match the verified
event.pubkey— a validly signed kind:5 naming another owner's coordinateno-ops instead of deleting their record.
coordinate tests migrated to self-owned fixtures.
schnorr verifies at ~100µs each, negligible.
2. Copy sweep (
8d5619ca0) — Category-C persona→agent stringsUser-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 andteam 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:
shipped behavior; recorded, zero diff.
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.