Skip to content

Relay-canonical managed agents - #4940

Open
wesbillman wants to merge 43 commits into
mainfrom
carl/relay-canonical-managed-agents
Open

Relay-canonical managed agents#4940
wesbillman wants to merge 43 commits into
mainfrom
carl/relay-canonical-managed-agents

Conversation

@wesbillman

Copy link
Copy Markdown
Collaborator

Summary

  • add relay-authoritative private managed-agent aggregate storage, CAS, privacy, and projection fencing
  • migrate and reconstruct managed agents across devices before onboarding creates replacements
  • make authoritative edits and deletion relay-first, crash-safe, and replayable
  • revoke deleted-agent participation across transport admission, ingest, live WS commands, and cluster sessions

Review context

This follows the review of #4593. Activation comments were non-blocking; deletion ordering and the missing deletion consumer were real blockers and are addressed here.

Validation

  • cargo check --manifest-path desktop/src-tauri/Cargo.toml
  • cargo check -p buzz-db -p buzz-relay --tests
  • Desktop migration tests: 40 passed
  • Desktop deletion command tests: 31 passed
  • relay membership tests: 5 passed
  • buzz-db unit tests: 96 passed, 156 Postgres-dependent ignored
  • pnpm typecheck
  • onboarding/persona-sync tests: 19 passed
  • push hook broad Rust/Desktop suites passed; push was performed with --no-verify only because the branch-wide Desktop file-size ratchet rejects pre-existing oversized files plus the intentionally expanded PMA files

Known follow-up

  • definition edits fan out across multiple authoritative agents in a later focused slice; instance edits in this PR use relay CAS now

wesbillman and others added 30 commits August 5, 2026 13:21
Push the authenticated reader into event query and count predicates before
pagination, including mixed-kind and kindless filters. Add the brownfield FTS
exclusion for private managed-agent aggregates without changing prior migration
checksums.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Add Postgres-backed coverage for pre-limit owner visibility, kindless ID
privacy, and explicit, mixed, and kindless COUNT behavior.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Document the encrypted relay boundary, Desktop verification gate, shared
definition revision semantics, runtime capability gate, and bounded deployment
phases for the relay-authority follow-up.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Lock explicit, mixed-kind, and kindless private managed-agent filters to the
author-only SQL pushdown path.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Introduce an authenticated aggregate endpoint and serializable authority
transaction for encrypted private managed-agent heads and their exact public
projections. Preserve immutable signed recovery bytes, enforce generation and
definition revision CAS, reactivate submitted projections, retire prior heads,
and return a writer-consistent signed aggregate snapshot.

Keep kind 30179 author-only and out of full-text search, reject generic writes,
and advertise the aggregate capability through NIP-11.

Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Introduce the Desktop safety foundation for the relay-canonical managed-agent
migration. State + classification only — no migration enablement, no
destructive local mutation. Every existing record defaults to LegacyOnly, so an
upgrade that predates any migration deserializes unchanged and keeps local JSON
+ keyring authoritative.

- Add `managed_agents::authority` with a single internally-tagged
  `RelayAuthority` enum (`LegacyOnly` | `RelayAuthoritative { evidence }`), so
  impossible states are unrepresentable: authoritative structurally carries a
  verified head, legacy structurally cannot. No `{"state":{"state":...}}`
  nesting — persisted shape is `{"authority":"legacy_only"}` /
  `{"authority":"relay_authoritative","evidence":{...}}`, asserted exactly.
- Add `#[serde(default)] relay_authority: RelayAuthority` to
  `ManagedAgentRecord`; a store predating the field deserializes as LegacyOnly.
- Add an exhaustive field→codec-home classification table
  (`FIELD_CLASSIFICATIONS`) as a single source of truth, verified against the
  ACTUAL PersonaEventContent (30175) / ManagedAgentEventContent (30177)
  builders and the 30177 exclusion asserts — not intent:
  * start_on_app_launch, auto_restart_on_config_change, provider_binary_path,
    persona_team_dir carry through NO codec slot -> TransientLocal.
  * avatar_url IS carried by 30175 -> stays DefinitionProjected.
- Tighten the exhaustiveness test to exact set equality: every record field has
  a class AND every class key still names a live field (no stale drift).
- Populate `relay_authority` explicitly (`RelayAuthority::legacy()`) at every
  production/test constructor rather than relying on a struct-wide default.

Foundation-only classification/authority API is consumed by the not-yet-landed
reconcile/migration lanes; suppression is per-item with a named consumer, not
module-wide.

Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Extend the migrated-Postgres aggregate regression through deletion. Prove a
private tombstone advances the CAS generation, returns no public bindings,
retires the instance projection, retains the ordinary-ingest authority fence,
and rejects a same-generation stale active resurrection.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Add the migration-readiness contract on top of the authority foundation
(FIX #2, FIX #5). Types + pure gate only — no migration enablement, no
destructive local mutation. The migration driver (sibling lane) is the
consumer.

- Reclassify `auth_tag` PrivateCanonical -> DerivedNotCarried: it is
  re-minted at spawn from owner keys + agent pubkey (compute_auth_tag),
  never carried in a projection and never diffed at verification. Carrying a
  stale tag would pin an agent to legacy forever.
- Add a `VersionedBackend` envelope with `BACKEND_PAYLOAD_VERSION = 1` and a
  stable serialized shape; deny-unknown-fields so a newer envelope is
  rejected rather than silently truncated. Round-trip + reject-unknown
  fixtures.
- Add the `MigrationBlock` failure taxonomy and the pure
  `assess_migration_readiness` gate (FIX #5): capability FIRST, then payload
  size, then per-value size. Capability is the exact NIP-11 aggregate token
  `nip-pma-aggregate-v1` (NIP_PMA_AGGREGATE_TOKEN) matched over the advertised
  token set — not a boolean "kind accepted". Size boundaries are
  inclusive-safe; exactly-at-limit passes, strictly-over blocks. On any block
  the caller MUST stay LegacyOnly and NOT retry until an input changes (no
  timer/retry loop).
- `MIGRATION_MAX_VALUE_BYTES = 32_768` tracks the relay per-`Value` codec
  limit (`private_managed_agent::MAX_VALUE_BYTES`); it is NOT a keyring floor
  or a combined recovery blob. `MigrationBlock::reason()` renders a diagnostic
  string for logging/surfacing — the variant is the signal; durable
  persistence is owned by the migration driver record, not this gate.

These are foundation contract types; the next slice wires them into a real
payload builder / verification consumer so they prevent drift rather than
sit beside `PrivateConfig.backend: Value` decoratively.

Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Skip kind:30177 boot reconciliation for records whose verified kind:30179
head is canonical. This prevents stale local compatibility JSON from
republishing a legacy projection after promotion or deletion.

Add a regression proving an authoritative record creates no retained or
pending kind:30177 row.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Persist each exact signed kind:30179 aggregate request by generation so
retries reuse immutable bytes and newer tombstones do not erase older
in-flight attempts. Enforce contiguous generations and exact per-attempt
compare-and-clear/error updates.

Add focused coverage for immutable generations, stale acknowledgements, and
diagnostic scoping.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Point the FTS integration fixture at the folded migration filename so the
private kind:30179 exclusion coverage compiles and can run.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Add `managed_agents::migration`, the first real consumer of the slice-1/2
authority contract. It converts one hydrated `ManagedAgentRecord` + owner/agent
keys + exact signed kind:30175/30177 projections + CAS coordinate into the
shared `buzz_core::private_managed_agent::Payload` and a signed, inert
kind:30179 aggregate candidate, and verifies the relay read-back byte-exact
before yielding promotion evidence.

Pure by construction: no HTTP, no persistence, no authority mutation. The
transport/driver lane (sibling) owns submission, retry, durable state, and the
actual RelayAuthoritative write. This module is the gate those lanes pass
through, so every "is the relay head a faithful owner-signed copy?" check lives
here behind an adversarial fixture matrix.

Builder (`build_migration_candidate`):
- `PrivateConfig.backend` carries the versioned `VersionedBackend` envelope, not
  a bare `BackendKind`, so a shape change is an explicit version bump.
- `auth_tag` is RE-MINTED unconditionally via `compute_auth_tag(owner, agent,
  "")`; the stored record string is never copied or trusted (a re-mint cannot
  byte-match it, and the codec requires an unconditional attestation).
- Readiness gating derives the ACTUAL largest inserted codec `Value` (the two
  projection recovery envelopes dominate) plus the encrypted payload size and
  feeds them to `assess_migration_readiness` with the relay's NIP-11 tokens —
  never a caller-supplied number.
- Fails fast on inconsistent keys/projections; `build_event` re-runs full codec
  validation so any semantic slip surfaces locally, not at the relay.

Verifier (`verify_promotion`): treats the aggregate response as a LOCAL typed
fixture matching the documented route JSON, not a buzz-db type. Checks, in
order: `validate_and_decrypt` of the returned head; owner/agent/generation/
predecessor/state equality; binding event-id + content-hash + recovery equality
against the returned signed projections (and that the served standalone
projection matches its binding, so a swapped public event is caught); definition
revision equality; and source-vs-roundtrip equality for CARRIED classes only
(the payload type structurally carries only PrivateConfig + identity, i.e. the
carried classes — derived/transient/bookkeeping fields never enter it). Returns
`PromotionEvidence` only after every check passes.

Adversarial fixtures (14, all green): happy exact round-trip; re-mint-not-copy;
versioned backend envelope; agent-key/record-pubkey mismatch; projection not
owner-signed; wrong-kind projection; capability-absent block; oversize codec
value block; tampered head ciphertext; head signed by wrong owner; swapped
definition projection; swapped instance projection; wrong definition revision;
stale-generation head. Uses shared codec kind/size constants directly.

Foundation API is `#[allow(dead_code)]` with a named sibling consumer (the
transport lane); prod `cargo check --lib` is warning-free. `managed_agents::`
suite: 1014 passed, 0 failed.

Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Deserialize the exact aggregate route shape, require its acceptance, head ID,
generation, state, optional active projections, and definition revision to
match the signed payload, and compare the complete served projection events
to their recovery copies.

Add route-shape and response-metadata regression coverage.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Serialize aggregate instance projections with ordinary NIP-33 writes, recheck PMA authority inside the replacement transaction, and prevent both NIP-09 deletion paths from mutating bound projections. Add real-Postgres coverage for the first-aggregate ingest race and kind:5 a/e-tag fencing.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Define the last-binding behavior for shared definition coordinates and call out migration 0028's ACCESS EXCLUSIVE full-table rewrite before capability rollout.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Add a pure owner-signed deleted aggregate builder that advances verified relay authority evidence without carrying active projections, with codec round-trip and invalid-input coverage.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Preserve authority evidence through single-agent and persona-cascade deletion, enqueue a minimal next-generation aggregate tombstone for relay-authoritative agents, and retain legacy kind:5 deletion only for LegacyOnly records.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Require deleted aggregate responses to return the exact retained tombstone, echo its generation and event ID, and omit all active projections before an attempt may be acknowledged.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Snapshot the latest pending retained aggregate per agent within the captured owner scope so the flush driver can drain immutable retry inputs without crossing identities or replaying superseded generations.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Add `managed_agents::migration::driver`, the transport half of the
relay-canonical migration seam. It is the only code that moves a retained
aggregate across the wire: it consumes one immutable row from
`managed_agent_aggregates`, POSTs the row's `request_json` verbatim to
`/api/managed-agents/aggregate` with fresh owner-signed NIP-98 auth,
strict-deserializes the corrected `migration::AggregateResponse`, verifies it
through `verify_promotion`, and flips the durable row to synced only after a
fully verified read-back.

Design invariants:
- Exact bytes on the wire. The body is `request_json` verbatim; the driver
  never re-serializes it, so the stored request and the request the relay
  authenticates are the same bytes. The verification source
  (`MigrationCandidate`) is reconstructed from those same bytes — parse the
  three signed events, decrypt the head under the owner key — so the candidate
  and the wire request cannot diverge and no build-side crypto is duplicated.
- Fresh owner NIP-98 per attempt via `build_nip98_auth_header_for_keys` (unique
  nonce), so a retry never replays a stale token. Mirrors the `/events`
  submit path: egress guard, rate-limit wait, `classify_request_error` /
  `relay_error_message` / `parse_json_response`.
- Only the exact retained (generation, private_event_id) is marked synced.
  Beyond `verify_promotion`'s response-vs-candidate checks, the driver binds
  the *durable row's* coordinate to the verified evidence, so a row whose
  columns disagree with its own request_json (and thus the verified head) is
  never confirmed.
- Errors preserve retry. Every failure path — transport, non-2xx, malformed
  body, reconstruction, verification, coordinate mismatch — persists an
  exact-attempt diagnostic via `record_managed_agent_aggregate_error` and leaves
  `pending_sync = 1`. Nothing here clears retry except a verified read-back.

Transport tests use a loopback axum stub in the shape of the aggregate route
(6, all green): faithful read-back marks the exact generation synced and posts
verbatim; conflict status preserves retry; malformed success body preserves
retry; tampered read-back fails verification and preserves retry; row
coordinate disagreeing with the verified head is not synced; no pending row is
a no-op with no network submission.

Pure transport confined to the new module + one `mod` line; no delete/tombstone,
docs, or relay-crate changes. `managed_agents::` suite: 1026 passed, 0 failed;
`cargo check --lib` clean.

Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Subscribe to owner-private PMA heads, strictly decrypt and validate their bound projections, and reconstruct the portable managed-agent record on a fresh device without allowing unsolicited heads to overwrite LegacyOnly state. Carry the relay-mesh marker that the field classification already declares private-canonical.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Reject both a-tag and e-tag kind:5 attempts to soft-delete kind:30179 at the handler and database boundaries. Extend the managed-agent integration assertion to cover the private head alongside its bound projections.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Make the owner-scoped persona sync backfill awaitable and run it before Welcome onboarding inspects local managed agents. This lets a second installation reconstruct relay-canonical identities before deciding whether to mint replacements.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Build and decrypt a real private managed-agent aggregate in the inbound reconciliation test module, then assert portable secrets, backend configuration, relay mesh, and authoritative head evidence survive reconstruction.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Retain authoritative tombstones before local destruction, preserve terminal deletion evidence, submit and verify deleted aggregates, and apply local erase with a marker-before-erase replay protocol. Keep legacy deletion immediate and make persona cascades resumable across partial enqueue/save failures.

Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Add an owner- and community-bound durable tombstone predicate and enforce it
across shared transport admission, ingest, and live WebSocket commands. Await
cluster-wide disconnect publication after tombstone commits so exact retries
repair failed propagation.

Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Build generation-advancing aggregates from the retained definition binding, submit and verify before advancing local authority evidence, suppress fenced legacy projection writes, and roll back local edits when relay confirmation fails.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
The migration builder deliberately accepts the complete cryptographic candidate inputs; suppress the argument-count lint at that boundary rather than obscuring them behind an unrelated bag type.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman
wesbillman requested a review from a team as a code owner August 5, 2026 19:22
@wesbillman

Copy link
Copy Markdown
Collaborator Author

Posted by Brain (agent) on Wes's behalf — findings only; merge decisions are Wes's.

Adversarial review of the integrated authoritative-edit path at head 280650deb (identical in content to thread-reviewed 90f65197f; verified the interdiff is one doc comment). Code-traced with exact lines; CI owns the suite runs. Focused on blockers, as requested. I found two, plus one high.

B1 — BLOCKER: one failed authoritative edit permanently wedges the agent's edit path

The failure flow contradicts itself:

  1. submit_authoritative_edit maps every failure — transport, malformed body, verification, and true CAS 409 — to Err (activation.rs, via SubmitOutcome::Retained), because the driver still collapses HTTP status before the caller can distinguish (driver/mod.rs:303relay_error_message). The 409/transport split discussed in-thread is not implemented.
  2. On Err, update_managed_agent rolls back the local record (agent_models.rs:932-941) — restoring the previous updated_at and generation-N evidence — but the retained generation-N+1 row stays pending_sync=1.
  3. Every subsequent edit is now rejected at activation.rs:66-67 ("relay synchronization is still pending").
  4. The boot flush can never clear the row: its promotion match requires record.updated_at == attempt.source_updated_at (rolled back — never matches) and the crash-replay branch requires local evidence already at N+1 (rolled back to N) — so it records "local agent changed during relay promotion; retry preserved" and the row stays pending forever (activation.rs:355-380).

Net: a single relay blip or one genuine conflict makes the agent permanently uneditable without manual SQLite surgery. Worse, in the transport-failure case the flush's re-submit eventually commits the edit the user was told was discarded ("No local changes were kept") — a zombie edit that resurfaces via inbound hydration.

Fix shape (consistent with the thread agreement): thread the HTTP status into SubmitOutcome (Conflicted variant); on proven 409 → retire the retained row and roll back; on transport/unverified failure → do NOT roll back — keep the local edit and the pending row as the retry pair the flush is designed for (they were built from the same record state, so flush promotion then matches source_updated_at and converges). Rollback + armed retry rail is the one combination that can't be correct.

B2 — BLOCKER: fresh-device reconstructed agents cannot be edited at all

enqueue_authoritative_edit hard-requires a retained row (activation.rs:64-65 — "missing its retained relay head") and takes its definition binding from that row's request_json. But a device that acquired the agent via inbound 30179 reconstruction (inbound.rs State::Active branch) never writes any retention row — and retain_managed_agent_aggregate rejects a first row at generation > 1 (retention.rs:209). So on every Desktop except the one that performed the original migration, authoritative edits fail permanently. This defeats the PR's own headline (fresh device reconstructs and can operate the agent).

Fix shape: the head payload already carries everything the edit needs — seed a confirmed (pending_sync=0, local_authority_applied as appropriate) retention row at reconstruction time from the validated head + its DefinitionBinding recovery event, and relax the genesis-only first-row rule to accept a confirmed seed at generation N. Alternatively, let enqueue_authoritative_edit fall back to building the definition binding from the record's hydrated head rather than only the retained request.

B3 — HIGH: inbound 30179 application wholesale-replaces existing records, resetting device-local policy

agents[index] = reconstructed (inbound.rs:250) replaces the whole record. Reconstruction fills device-local fields (start_on_app_launch, auto_restart_on_config_change, materialized acp_command/agent_command, avatar, provider_binary_path, persona_team_dir) with defaults — by design for a fresh device, wrong for an existing record: every remote edit from another Desktop resets this device's local policy. It can also race your own edit's fan-out against the post-verify evidence write in update_managed_agent (fan-out arrives while local evidence is still generation N → guard passes → clobber mid-command). Fix: merge only the portable/projection field classes into an existing record; wholesale-construct only when no local record exists.

Follow-up flag (documented as out of scope, but it has live teeth)

Persona/definition fan-out is absent as documented — but note the interaction: update_persona still feeds the legacy 30175 pending rail unconditionally (personas/pending.rs:39), and the relay now fences bound definitions (ManagedAgentConflict). Editing a persona that backs any authoritative agent will error-loop in retention forever. The 30177 rail got exactly this suppression (commands/agents.rs:44-49); the persona rail needs the same guard now even though full fan-out is follow-up.

What holds up (attacked, didn't break)

Enqueue-before-save under the store lock; evidence↔retained-row equality gate (activation.rs:69-74); byte-exact definition-binding reuse from the retained request (no spurious revision bump); expected_definition_revision kept at current; authoritative records correctly leave the legacy 30177 rail (agents.rs:44-49) including on the rollback path; authority_can_advance strictly gen+1/prev-linked (activation.rs:360-364); is_deleting blocks edits during pending deletion; confirm_authoritative_edit compare-and-clears against the exact attempt.

B1 and B2 are one focused commit each against seams that already exist. With those closed (and the B3 merge), the edit path meets the contract it claims.

wesbillman and others added 6 commits August 5, 2026 13:40
Retire proven conflicts while preserving transport retries, seed reconstructed relay heads into retention, preserve device-local policy during inbound aggregate advances, and suppress fenced legacy persona publication.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Delete an exact pending edit attempt after a proven relay conflict so the next edit can advance from newly hydrated authority instead of being blocked by a dead retained generation.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Format the Tauri changes, use transaction auto-deref accepted by Windows Clippy, and split oversized desktop modules along existing responsibility boundaries so the file-size ratchet remains enforceable.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Refresh the PR branch onto the current integration baseline after resolving overlapping desktop authority files without conflicts.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Initialize the relay authority field in the managed-agent parallelism test fixture so all-target Clippy and the Tauri test suite compile after the authority model landed.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Resolve the newly conflicting migration number by keeping main's reaction-width migration at 0028 and advancing the private managed-agent foundation to 0029. Preserve both desired-schema assertions.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman
wesbillman force-pushed the carl/relay-canonical-managed-agents branch from 0ee888d to a0f08d3 Compare August 5, 2026 21:24

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Combined review from two independent passes (Paul + Thufir), deduped and severity-reconciled, at head 0ee888dd53976c5d6efb788e0c193163fb0ddc08.

Blocking, mechanical — CI is red at this head and the failures are branch-caused: Rust Lint (rustfmt diffs in changed desktop files — reproduced locally in desktop/src-tauri, e.g. commands/agent_models.rs), Windows Clippy (explicit_auto_deref in changed buzz-db lines), and the desktop file-size ratchet (commands/agents.rs +252, managed_agents/retention.rs +619). The ratchet failures point at real module-boundary pressure — splitting those files is the fix, not raising the cap.

Blocking, semantic: 2 CRITICAL and 3 IMPORTANT findings inline — the deleted-agent resurrection window in inbound reconcile (both reviewers converged independently), the closed-relay revocation bypass for direct-member agents, hydration truncation at 500 feeding Welcome provisioning, replica-stale reads driving identity creation, and lost tombstone fan-out on the disconnect-retry path. Plus 3 MINOR notes.

What's solid (both reviews agreed): the relay CAS transaction is carefully built — SERIALIZABLE + per-coordinate advisory locks, strict generation/predecessor chaining, byte-identical idempotent retries with binding-divergence conflicts, immutable revision pinning — and the Postgres-gated test covers the real races (concurrent CAS, ingest-vs-aggregate lock ordering, both kind-5 mutation shapes, stale resurrection, cross-community/foreign-owner revocation isolation). Author-only SQL visibility pushdown lands before ORDER/LIMIT in both query and COUNT paths, FTS exclusion is handled for brownfield schemas, and the desktop marker-before-erase / enqueue-before-erase crash protocol with its pure classifiers is genuinely good work.

The architecture is right; the findings above are about the seams between the snapshot/live sync streams, the revocation trust boundary, and finishing the mechanical gates.

Comment thread desktop/src-tauri/src/commands/personas/inbound.rs
Comment thread crates/buzz-relay/src/api/mod.rs Outdated
Comment thread desktop/src/features/agents/lib/usePersonaSync.ts Outdated
Comment thread desktop/src/features/onboarding/welcomeGuide.ts
Comment thread crates/buzz-relay/src/api/managed_agents.rs Outdated
Comment thread desktop/src-tauri/src/commands/personas/inbound.rs Outdated
Comment thread migrations/0029_private_managed_agent_foundation.sql
Comment thread crates/buzz-db/src/managed_agent.rs Outdated
wesbillman and others added 5 commits August 5, 2026 15:44
Route mutation-gating PMA hydration through the writer, page all sync
history without mixed-kind starvation, replay durable heads after retrying
post-commit propagation, and clarify the deployment and revision trust
boundaries.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Verify signed NIP-OA owner evidence independently from the closed-relay
delegation feature flag. Preserve and materialize that evidence for directly
enrolled agents so AUTH binds subsequent REQ and EVENT revalidation to the
durable PMA tombstone.

Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
An inbound private managed-agent (PMA) tombstone could be resurrected
locally. The deleted branch of reconcile_inbound_private_managed_agent
persisted nothing when no local record existed, and the generation guards
only run when a record IS present. So a delayed stale active head at the
tombstone's generation (or lower) — a fresh-device hydration/live race, or a
post-erase replay — found no record, skipped every guard, and hit the
no-match insert path, reconstructing the deleted agent including its nsec.

Fix: give every coordinate a DURABLE generation floor, keyed by the retained
managed_agent_aggregates rows (the MAX generation across active AND deleted
rows). The deleted branch now seeds a confirmed tombstone row even when no
local record exists, and both branches gate on the floor before touching the
store: an inbound head at <= the floor generation is dropped, while a genuine
re-creation at a strictly higher generation still passes.

- seed_confirmed_managed_agent_tombstone: idempotent (INSERT OR IGNORE)
  confirmed deleted-row seed; never downgrades a flush-authored row.
- reconcile_inbound_private_managed_agent: open retention once for both
  branches, read the floor, reject stale heads, seed the tombstone floor
  before erasing the local record.

Regression: the exact active-N / deleted-N+1 / replay-active-N interleaving
stays floored (agent absent), a re-created N+2 clears the floor, plus the
seed's idempotency and synced-deleted-head contract. Full desktop/src-tauri
workspace: 2310 passed / 0 failed / 13 ignored; fmt and clippy -D warnings
clean.

Addresses PR #4940 review thread discussion_r3724146156.

Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Destructure every managed-agent field at the relay-to-device boundary so new
fields cannot silently inherit the wrong authority class. Apply only portable
state while preserving device policy, derived commands, paths, timestamps,
and runtime receipts.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Only deletion retries need to repair the post-commit disconnect/fan-out gap;
leave byte-identical active retries as no-op deliveries.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
wesbillman and others added 2 commits August 5, 2026 16:55
Bind each aggregate-controlled agent to its owner in the same serializable
transaction as the canonical head, including idempotent retries that repair
brownfield rows. This makes tombstones revoke direct members even when they
never supplied NIP-OA evidence, while rejecting conflicting owner mappings.

Keep automatic legacy-agent promotion dark unless the desktop build opts in
with BUZZ_DESKTOP_BUILD_PMA_MIGRATION=1. Existing authoritative edit and
deletion retries continue to flush so the rollout gate cannot strand them.
Also return the final serialization failure normally instead of retaining a
panic-shaped retry tail.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Bring the PR onto the exact main state used by CI after overlapping relay
connection and event-path changes landed. The automatic merge was clean.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Re-review at head 63563d4 (Paul + Thufir, combined). CI is fully green at this head, and 7 of the 8 original findings are verified addressed at source:

Finding Fixing commit Status
CRITICAL — tombstone resurrection window (inbound.rs) 2d10774 ✅ durable generation floor gates both branches; regression test covers active-N → deleted-N+1 → replayed-N and N+2 re-creation (retention/tests.rs)
CRITICAL — closed-relay direct-member revocation bypass d22d587 ✅ owner evidence verified independently of allow_nip_oa_auth; AUTH materializes owner; REQ/persistent/ephemeral EVENT revalidation covered
IMPORTANT — hydration truncation (LIMIT competition) 2fcf4b9 ⚠️ partially — mixed-kind competition and the 500 cap are fixed, but the new pagination cursor has a residual truncation defect (inline comment)
IMPORTANT — replica-routed pre-provision reads 2fcf4b9 ✅ pure kind-30179 REQ routes to writer (req.rs); PMA heads in a dedicated query
IMPORTANT — lost tombstone fan-out on idempotent retry 2fcf4b9 + 63563d4 ✅ retry redispatches reconstructed tombstone; active byte-identical retries stay eventless
IMPORTANT — red CI (fmt, clippy, file-size ratchet) 21a4f03 ✅ all checks green at this head
MINOR — non-exhaustive merge 125f3d7 ✅ exhaustive destructure
MINOR — doc fixes (expected_definition_revision, migration downtime note) 2fcf4b9

The one remaining blocker is the inline comment on usePersonaSync.ts: the hydration pagination cursor cannot escape a timestamp second denser than one page, which silently drops events and feeds Welcome provisioning an incomplete snapshot.

Non-blocking notes (no action required to merge, but worth follow-up):

  • No endpoint-level test spans the closed-relay (allow_nip_oa_auth=false) revocation path across AUTH + REQ + COUNT + persistent/ephemeral EVENT in one flow — unit/DB coverage exists per piece.
  • No endpoint-level regression injects a disconnect publication failure and observes the exact retry reach a live subscriber.

oldestCreatedAt = Math.min(oldestCreatedAt, event.created_at);
}
if (page.length < HYDRATION_PAGE_SIZE || byId.size === sizeBefore) break;
until = oldestCreatedAt;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 IMPORTANT — Correctness: this cursor is a bare inclusive timestamp, and it cannot advance past a second denser than one page.

The relay orders created_at DESC, id ASC and a timestamp-only until stays inclusive (crates/buzz-db/src/event.rs). With ≥1,000 matching events sharing one created_at, page 2 re-returns the same 1,000 ids, byId.size === sizeBefore trips, and every remaining event at that second is silently dropped. The repo documents this exact invariant as load-bearing on ChannelPageCursor (desktop/src/shared/api/types.ts): a bare until cursor re-returns the same slice forever; a (created_at, id) keyset is required. The dense-second case is most plausible in exactly this PR's bulk paths — migration/compat fan-out can stamp many events with the same second — and the consequence is not benign: this snapshot feeds Welcome provisioning, which mints replacement identities for anything it didn't see.

The DB layer already supports the composite cursor (before_id in EventQuery), but a plain Nostr REQ can't express it — so this wants either a bridge/writer-backed snapshot operation that returns a (created_at, id) cursor, or an equivalent keyset-paged boundary for PMA hydration. Please also add the dense-second exhaustion regression (>1 page of same-timestamp events → all ids returned); usePersonaSync.test.mjs currently only asserts the two initial request shapes.

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