Skip to content

feat(desktop): publish signed launch bundles for buzz-waker - #28

Merged
yjc801 merged 4 commits into
mainfrom
claude/waker-desktop-publish
Aug 12, 2026
Merged

yjc801 merged 4 commits into
mainfrom
claude/waker-desktop-publish

Conversation

@yjc801

@yjc801 yjc801 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Summary

sign_launch_bundle (desktop/src-tauri/src/managed_agents/waker_bundle.rs) has existed since PR #22/#24/#25 landed the daemon-side bundle transport, but had zero production callers — so a real wake attempt still ended in DeployFailed, just because no bundle ever reached the daemon rather than because the daemon couldn't act on one. This PR wires the desktop side up so a signed bundle actually gets published.

  • ManagedAgentRecord gains an opt-in waker_enabled: bool (default false). A bundle carries the agent's private_key_nsec (NIP-44-encrypted to the agent, but published under the owner's real key to a public relay), so publishing it unconditionally for every Provider-backend agent was the wrong default — flagged this decision in #buzz-dev before implementing, no objection.
  • New set_managed_agent_waker_enabled command flips the flag, refusing a Local backend. Turning it on is the enrolment moment (PLANS/BUZZ_WAKER_DESIGN.md §11).
  • retain_managed_agent_pending — the existing chokepoint every real config-change command already calls, and that already skips runtime-only churn (start/stop) — now also issues and retains a fresh signed kind:30180 bundle when waker_enabled is set. It's retained through the same pending_sync row the existing 30s flush loop (persona_events::flush_active_pending_events) already drains for persona/team/managed-agent writers, reusing that retry path instead of adding a second one.
  • New logic lives in agents_waker.rsagents.rs is already past desktop/scripts/check-file-sizes.mjs's ratchet and may not grow, so retain_managed_agent_pending moved there wholesale and is re-exported for its existing call sites.

Out of scope, called out explicitly rather than guessed at:

  • The generation-nonce substitution contract (crates/buzz-waker/src/effects.rs's own doc note) — daemon-side execution logic, not desktop's signing/publish path.
  • A UI toggle for the new flag — this PR is the backend plumbing; set_managed_agent_waker_enabled is ready for a settings-panel toggle to call.

Test plan

  • cargo fmt --manifest-path desktop/src-tauri/Cargo.toml -- --check
  • cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --workspace --all-targets -- -D warnings
  • cargo test --workspace from desktop/src-tauri — 2453 passed, 0 failed (plus sub-crate suites)
  • node desktop/scripts/check-file-sizes.mjs — ratchet passes
  • pnpm check (desktop TS/lint) — passes (pre-existing unrelated warnings only)
  • New unit tests in agents_waker.rs round-trip a retained bundle through the exact decrypt+verify path buzz-waker's own bundle tap runs, and assert version reissue never repeats

Known pre-existing failure, unrelated to this change: just gate's desktop-test step fails on RootErrorBoundary.test.mjs expecting the string "Buzz failed to start" against the app's actual rendered "Waggle failed to start" — fallout from the app-rename commit (PR #23), reproduces on main with zero changes from this branch. Confirmed via git log that PR #23 touched RootErrorBoundary.tsx without updating its test's expected copy. Left untouched as out of scope for this PR.

🤖 Generated with Claude Code

yjc801 and others added 4 commits August 11, 2026 15:44
sign_launch_bundle existed and was tested but had zero production
callers, so a real wake attempt still ended in DeployFailed because no
bundle ever reached the daemon. This wires the desktop side up:

- ManagedAgentRecord gains an opt-in `waker_enabled` flag (default
  off) — a bundle carries the agent's private_key_nsec, so publishing
  it by default for every Provider-backend agent is the wrong default
  for a security-sensitive value.
- A new set_managed_agent_waker_enabled command flips it on only for
  a Provider backend; that flip is the enrolment moment.
- retain_managed_agent_pending (agents.rs) — the existing chokepoint
  that already fires on every real config change, never on runtime-
  only churn (G3) — now also issues and retains a fresh signed
  kind:30180 bundle when the flag is set. It's retained through the
  same pending_sync row the existing 30s flush loop already drains for
  persona/team/managed-agent writers, reusing that retry path instead
  of adding a second one.
- The new logic lives in agents_waker.rs, split out because agents.rs
  is already past the file-size ratchet and may not grow.

Out of scope: the generation-nonce substitution contract (daemon-side
execution logic, tracked separately) and a UI toggle for the new flag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Junchao Yan <yjc801@gmail.com>
Addresses Alex's PR #28 round-1 findings:

- [P1] Disabling waker, or migrating an enabled agent off a Provider
  backend, now enqueues a NIP-09 tombstone for the retained kind:30180
  launch bundle instead of only stopping future issuance. A daemon that
  reconnects or restarts after this can no longer recover the previous
  authorization from the relay. Does not reach a daemon that already
  holds the bundle in its live BundleState -- that needs daemon-side
  revocation delivery, which PLANS/BUZZ_WAKER_DESIGN.md SS3 names as a
  still-unbuilt implementation gate, documented as a known follow-up.

- [P2] set_managed_agent_waker_enabled's false->true (enrolment)
  transition now propagates a bundle-issuance failure as a command
  error and rolls waker_enabled back to false, instead of silently
  swallowing it and reporting success with no bundle to show for it.

Signed-off-by: Junchao Yan <yjc801@gmail.com>
Addresses Alex's round-2 finding on PR #28: the prior NIP-09 tombstone
only closed relay recovery for a daemon that reconnects after the
disable transition. An already-connected daemon never subscribes to
kind:5 and kept its admitted bundle in BundleState, usable for up to
the 90-day validity window -- exactly the duplicate-harness state the
backend migration guard exists to prevent.

Replace the tombstone with a `revoked` flag on LaunchBundleBody,
published at the *same* kind:30180 NIP-33 coordinate a config-change
reissue already uses. Because the daemon's bundle tap holds that
coordinate's filter open live for real-time reissues, a revocation
reaches an already-connected daemon the same way a reissue does -- no
new relay change, no new event kind.

- crates/buzz-waker/src/bundle.rs: `revoked: bool` field, inside the
  signature like every other clamp.
- crates/buzz-waker/src/bundle_feed.rs: decrypt_verify_and_admit now
  returns a BundleOutcome (Delivered/Revoked); a revoked delivery
  raises FloorStore's revocation floor and BundleState::clear() drops
  the cached bundle.
- desktop/src-tauri/src/commands/agents_waker.rs: tombstone_waker_bundle_pending
  replaced by revoke_waker_bundle_pending, which republishes a revoked
  bundle at the same coordinate instead of a kind:5 delete.

Signed-off-by: Junchao Yan <yjc801@gmail.com>
Addresses Alex's round-3 (escalated) findings on PR #28:

- revoke_waker_bundle_pending now returns Result instead of logging
  and swallowing its own failure. Both callers (waker_enabled(false)
  and a Provider->Local backend migration) now call it BEFORE mutating
  or saving the record, and propagate Err on failure -- so a caller
  can no longer report success while the old bundle stays authorized
  for up to 90 days. Failing before any mutation needs no rollback,
  since nothing was persisted yet.

- sign_and_retain_waker_bundle_at now bumps the outer Nostr event's
  created_at past the previously retained head via the existing
  monotonic_created_at helper (same rule reconcile::retain_agent_record
  already applies to the sibling kind:30177 record), instead of using
  raw wall-clock seconds. NIP-33 replacement breaks same-created_at
  ties by lowest event id, so a same-second issue-then-revoke could
  otherwise leave the relay serving the older, non-revoked bundle even
  though local retention had already moved on.

Signed-off-by: Junchao Yan <yjc801@gmail.com>
@yjc801
yjc801 merged commit 1c41e2d into main Aug 12, 2026
19 of 25 checks passed
@yjc801
yjc801 deleted the claude/waker-desktop-publish branch August 12, 2026 03:13
yjc801 pushed a commit that referenced this pull request Aug 26, 2026
## Why

Database pressure currently collapses several distinct delays into one
symptom. This adds the evidence layer needed to distinguish pool
acquisition wait, logical database operation time, advisory-lock wait,
and selected transaction duration before changing timeout or retry
policy.

This is the phase 2 Lane A observability bundle for
[#26](TheSentinel454#26),
[#28](TheSentinel454#28), and
[#33](TheSentinel454#33). It is stacked
on block#6668.

## What

- Record explicit reader/writer checkout wait and acquisition outcomes
with `buzz_db_pool_acquire_wait_seconds` and
`buzz_db_pool_acquisitions_total`.
- Extend the compile-time `#[datastore_span(name = "...")]` seam with
`buzz_db_operation_duration_seconds`, so operation labels remain static
source literals instead of request data.
- Route correctness-critical replacement, membership, push-gate,
deletion, and migration/schema-safety advisory locks through one
observer without changing their SQL, order, scope, or blocking behavior.
- Measure six internally owned transaction lifetimes with
`buzz_db_transaction_duration_seconds`, starting after `BEGIN` succeeds
and ending after explicit commit/rollback or scope exit.
- Emit root slow-operation warnings at 500 ms, logging the first slow
completion and then 1/100 per call site with only `operation`,
`outcome`, and `elapsed_ms`.
- Document names, units, fixed label vocabularies, measurement
boundaries, and blind spots in this PR description.

Fixed labels are deliberately small:

- `pool_role`: `writer`, `reader`
- `lock_type`: `replacement`, `membership`, `push_gate`, `deletion`,
`migration_schema_safety`
- `outcome`: `success`, `error`, `timeout` where SQLx/PostgreSQL can
distinguish it accurately
- `operation`: compile-time datastore names plus the six closed
transaction operation names documented in the runbook

No metric or slow warning contains community IDs, event IDs, event
kinds, coordinates, d-tags, SQL/query text, query IDs, returned errors,
or event content.

## Coverage boundaries

- Operation duration is the complete annotated logical function body,
not pure SQL execution; it may include implicit checkout, lock wait,
nested operations, and application work. Cancelled futures do not reach
its completion hook.
- Pool timing covers explicit helper checkouts, including proved-reader
routing and selected writer-owned transactions. Implicit SQLx checkout
through `&PgPool` remains folded into operation duration.
- Lock timing covers application-side blocking locks in the five named
families. Trigger/stored-procedure locks, channel-TTL locking, the usage
try-lock, and the audit service session lock remain outside this slice.
- Transaction timing covers only the six wholly owned boundaries
documented in the runbook. It excludes pool wait, `BEGIN`, asynchronous
rollback cleanup after an early return, and caller-owned
`Db::begin_transaction` lifetime.

## Relationship to block#6229

block#6229 is the incident-driven timeout precursor. This PR does not add or
change `statement_timeout`, `lock_timeout`,
`idle_in_transaction_session_timeout`, retries, audit durability, or
client-visible conflicts. It provides the missing distributions needed
to evaluate those policies later and intentionally leaves block#6229's open
audit retry/durability finding untouched.

The branches overlap in `crates/buzz-db/src/lib.rs` and
`crates/buzz-db/src/migration.rs`, so a later rebase may need textual
conflict resolution, but the behavior is complementary rather than
duplicated.

## Risk assessment

Moderate-low. The primary risk is instrumentation overhead and added
static series. Cardinality is source-bounded, slow logs are
sampled/redacted root events, and the lock/transaction changes wrap
existing awaits without changing policy or ordering.

## Verification

Author workstation: `buzz-tornquist-db-pressure-observability`
(`2010927`), exact head `d7cf833e26c528adfcde3917ded80daf6f4ddac9`,
parent `6f50e6b2b2a996349149af61d35bdd6a355f77fd`.

- `cargo fmt --all --check` — passed
- `cargo clippy -p buzz-datastore-tracing -p buzz-db -p buzz-audit -p
buzz-search -p buzz-relay --all-targets -- -D warnings` — passed
- `cargo test -p buzz-datastore-tracing --quiet` — 4 passed
- `cargo test -p buzz-db --quiet` — 109 passed, 200 ignored
- `cargo test -p buzz-audit -p buzz-search --quiet` — 16 passed, 25
ignored
- `cargo test -p buzz-relay --lib --quiet -- --test-threads=1` — 906
passed, 48 ignored
- Native PostgreSQL focused tests for pool success/timeout/error, lock
success/contention/timeout/error, replacement, membership serialization,
push ordering, deletion fencing, migration/schema exclusion, and reader
fallback — 8 passed

The default-parallel relay run passed once; subsequent runs exposed the
existing load-sensitive
`api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo` 504 at
the end of the suite. That test passes in isolation and the full relay
suite passes serially.

Independent exact-head review workstation:
`buzz-tornquist-db-pressure-observability-review` (`2013067`).
Formatting, the same all-target clippy command, datastore
instrumentation tests, DB unit tests, source privacy guards, and
diff/non-goal audits passed; no review findings.

Generated with Codex

---------

Signed-off-by: tornquist <tornquist@squareup.com>
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.

1 participant