feat(waker): dynamic per-agent supervisor (build order step 3) - #50
Merged
Conversation
Phase 1 of docs/waker-agent-enrolment.md / PLANS/BUZZ_WAKER_DESIGN.md §12: the pure, network-free half of replacing hand-edited WAKER_AGENTS_CONFIG_PATH JSON with credentials delivered over the relay. Adds crates/buzz-waker/src/enrolment.rs: - RosterBody/SignedRoster: which agent pubkeys are currently enrolled with an owner, republished in full on every add/remove/rotate. This is the discovery primitive that closes the design review's P2 finding — a single coordinate per (owner, waker) pair gets the same small-bounded-query completeness the bundle tap already has, instead of needing history pagination or a relay guarantee the non- replaceable kind-1059 envelope doesn't provide. - CredentialBody/SignedCredential: one agent's nsec/auth_tag, once the roster says that pubkey exists. Reuses the bundle tap's exact per-agent delivery shape. - parse_authorized_owners: WAKER_OWNER_PUBKEYS parsing/validation — the daemon-level trust anchor for a brand-new agent's first admission, before any per-agent FloorStore exists to pin an owner into. Mirrors main.rs's existing parse_owner_pubkey. Both signed types mirror bundle.rs's SignedLaunchBundle shape and verification order (identity before cryptography, cryptography before parsing) with their own domain separators, and redact secret fields from Debug the same way LaunchBundleBody/SignedLaunchBundle do. Wire I/O (a roster/credential tap mirroring bundle_feed.rs) and the dynamic per-agent supervisor main.rs needs to act on any of this are later phases — this crate has zero production callers of the new module yet, matching how bundle issuance itself shipped unwired before its own daemon-side plumbing landed. Testing: - cargo test -p buzz-waker: 216 lib tests + 8 main tests pass (13 new in enrolment::tests) - cargo clippy -p buzz-waker --all-targets -- -D warnings: clean - cargo fmt -p buzz-waker -- --check: clean - cargo doc -p buzz-waker --no-deps with -D warnings surfaces 4 pre-existing private-doc-link errors in bundle_feed.rs/cursor.rs/ feed.rs/attempt.rs, none in enrolment.rs; reproduces identically on main with this diff stashed out, and cargo doc is not part of just ci's gate set Signed-off-by: Junchao Yan <yjc801@gmail.com>
…ries Addresses Alex's two review findings on PR #48: - [P1] SignedCredential::verify accepted any owner-signed {agent_pubkey, nsec} pair without proving the nsec actually derives agent_pubkey. A signed-but-self-inconsistent credential (agent_pubkey: A, nsec: key-for-B) would verify successfully, and later phases key durable state (floor, roster membership, state directory) by A while a spawned connection authenticates as B. Fixed by parsing nsec after signature verification and requiring its derived pubkey to equal the claimed agent_pubkey, mirroring the identical binding crates/buzz-core/src/private_managed_agent.rs already enforces (validate_active_definition). The check (and the new auth_tag shape check) is skipped for a revoked credential, since CredentialBody's own doc already establishes nsec/auth_tag as unused issuer placeholders there, same as LaunchBundleBody::revoked. - [P2] SignedRoster::verify accepted a roster with malformed agent pubkeys or the same agent listed more than once with disagreeing credential_version, leaving Phase 2/3's diff/fold order-dependent and letting an invalid coordinate reach durable state. Fixed by validating and normalizing the roster as one semantic unit after signature verification: every agent_pubkey must parse as a canonical Nostr public key, duplicates are refused outright, and a new MAX_ROSTER_ENTRIES (256, comfortably under the ~65KB practical NIP-44 ceiling bundle_feed.rs's own NIP44_CONTENT_LEN_RANGE already enforces) bounds the roster's serialized size. Testing: - cargo test -p buzz-waker: 224 lib tests + 8 main tests pass (9 new in enrolment::tests covering both findings) - cargo clippy -p buzz-waker --all-targets -- -D warnings: clean - cargo fmt -p buzz-waker -- --check: clean Signed-off-by: Junchao Yan <yjc801@gmail.com>
Updates PR #48's schema to match docs/waker-agent-enrolment.md's now- approved multi-tenant delta (PR #47): a live CredentialBody can carry the agent owner's provider deploy credential, so one daemon can deploy on behalf of several owners. Adds ProviderCredential, a typed per-provider enum (Sprites { sprite_token } today) rather than an arbitrary environment map — the design doc explains why: Command::env() doesn't distinguish a credential from any other process-control variable, so an unconstrained map would let a tenant set LD_PRELOAD/PATH/proxy flags in the trusted provider subprocess. Actually spawning from a sanitized environment (daemon-controlled baseline plus this schema) is deploy-wiring, a later phase — this is schema only, matching Phase 1's scope. Validation mirrors nsec/auth_tag: checked for a live credential, skipped for a revocation (an issuer's placeholder value must not need to be well-formed). Debug redacts provider_credential the same way it already redacts nsec. WAKER_OWNER_PUBKEYS's fail-closed contract (round-2 finding on PR #47) needs no code change here: main.rs has no call site for parse_authorized_owners yet (Phase 2/3 wires that in), and the existing doc comment already states the correct contract for when it does. cargo test -p buzz-waker: 38 lib + 8 main pass (8 new tests). clippy -D warnings and fmt --check clean. Signed-off-by: Junchao Yan <yjc801@gmail.com>
Addresses Alex's REQUEST-CHANGES on PR #48 (c549d54): ProviderCredential derived Debug, so formatting it directly — not nested inside CredentialBody, whose manual formatter only redacts the enclosing-body case — printed sprite_token verbatim. Any future error, assertion, or log line in deploy wiring that formats a bare ProviderCredential would have leaked the tenant's token. Replaces the derive with a manual redacting Debug, matching the existing pattern for CredentialBody and buzz-backend-sprites::Credential. CredentialBody's own formatter now just defers to it instead of a separate ad hoc redaction. Added a test that formats ProviderCredential directly, per the finding. cargo test -p buzz-waker: 230 lib + 8 main pass (1 new test). clippy -D warnings and fmt --check clean. Signed-off-by: Junchao Yan <yjc801@gmail.com>
Implements build order step 2 from PLANS/BUZZ_WAKER_DESIGN.md §12: the wire I/O half of agent enrolment, on top of Phase 1's schema (PR #48). roster_feed.rs — one connection for the whole daemon, authenticated as the waker's own identity rather than any watched agent's: a roster's whole job is telling the daemon which agents exist before it can authenticate as any of them. RosterState tracks the latest roster *per owner* (WAKER_OWNER_PUBKEYS may list several, and one owner's roster says nothing about another's membership), applying a delivery only if its roster_version exceeds what's already tracked for that owner — correct regardless of relay delivery order, not just the common created_at DESC case. run_roster_tap refuses to open a connection at all when given no authorized owners, enforcing the design's fail-closed contract (docs/waker-agent-enrolment.md, approved) itself rather than trusting main.rs's future wiring alone. credential_feed.rs mirrors bundle_feed.rs closely — same connect/ backoff/idle-timeout loop, same decrypt-verify-admit split against a per-agent FloorStore (bundle_feed's NIP44_CONTENT_LEN_RANGE constant is now pub(crate) and reused directly rather than duplicated) — with the two changes the design calls for: it also connects and decrypts as the waker identity, since the daemon doesn't have the target agent's key yet, and the query adds #d pinned to the target agent's pubkey, since #p is now the waker's shared identity rather than agent-specific. Both taps re-check their own #d value per received frame (roster: the fixed sentinel; credential: the specific agent pubkey) as defense in depth against a filter bug crossing the two streams, per the design's own instruction. Not wired into main.rs — diffing RosterState against the daemon's watch list and spawning/cancelling credential taps is step 3, the dynamic supervisor, deliberately not started here. cargo test -p buzz-waker: 251 lib + 8 main pass (21 new tests). clippy -D warnings and fmt --check clean. Signed-off-by: Junchao Yan <yjc801@gmail.com>
Alex's Phase 2 review (PR #48, head 008bcd6) found two real gaps in the roster tap: - RosterState only remembered the highest roster_version seen this process's lifetime. Because the envelope kind isn't replaceable, a relay can replay an owner's old-but-still-validly-signed roster after a restart, resurrecting an agent a newer roster already removed. Each authorized owner now gets its own FloorStore (crate::floors), opened lazily under roster_floor_dir and re-read from disk on every delivery, so the anti-replay floor survives a restart the same way it already does for bundle versions. - The REQ filter combined every authorized owner into one authors array with a single limit, but a NIP-01 filter's limit applies to the whole filter, not once per author. A burst of reissues from one owner could crowd another owner's current roster out of the response entirely. roster_filters now builds one filter per owner (buzz-relay runs each filter in a multi-filter REQ as its own independently-limited query), still under one subscription. cargo test/clippy/fmt -p buzz-waker all clean. `just gate`'s only failure is an unrelated pre-existing file-size ratchet trip in desktop/src-tauri/src/managed_agents/runtime.rs, a file this diff never touches. Signed-off-by: Junchao Yan <yjc801@gmail.com>
Alex's re-review of the durable-floor fix (PR #48, head 6c93cd1) found the P2 fix itself was incomplete: roster_filters emits one filter per authorized owner, but buzz-relay refuses any REQ with more than MAX_FILTERS_PER_REQ (10, advertised via NIP-11 as max_filters). An 11-owner configuration would open no roster subscription at all and recover no tenants' rosters. roster_req is replaced by roster_reqs, which chunks authorized_owners into batches of at most ROSTER_MAX_FILTERS_PER_REQ and opens one REQ frame per batch, each under its own roster_subscription_id (always suffixed, even for the common single-batch case, so roster_frame has one shape to match against instead of two). run_roster_tap now sends every batch's REQ after connecting and tracks the full set of subscription ids for frame routing; a failure partway through subscribing reconnects the whole connection rather than leaving a partial subscription set running. cargo test/clippy/fmt -p buzz-waker all clean (255 lib + 8 main, +2 new tests: one proving an 11th owner opens a second batch rather than being dropped or refusing to start, one proving a delivery on the second batch's subscription id is still recognized). Signed-off-by: Junchao Yan <yjc801@gmail.com>
Stacks on claude/waker-enrolment-schema (PR #48, approved, not yet merged) — needs its roster_feed/credential_feed taps. Extracts the per-agent spawn block (presence tap, bundle tap, wake loop) into spawn_agent_watch, called both by the static WAKER_AGENTS_CONFIG_PATH startup loop (unchanged behavior) and by a new reconciliation loop that diffs every authorized owner's roster against a supervised map, spawning a per-agent credential tap to fetch a newly-listed agent's nsec before it can be watched, then calling spawn_agent_watch once that credential arrives. A statically configured pubkey always wins a collision against a roster-discovered one and is never touched by the roster diff in either direction (compute_desired_roster_agents). Each watched agent's three tasks share one CancellationToken::child_token of the daemon's global token, tracked in a SupervisedAgent map keyed by pubkey. An unsolicited exit is classified (classify_exit): fatal for the whole daemon if the agent was statically configured (preserves today's exact behavior when WAKER_OWNER_PUBKEYS is unset), but tears down only that one agent if it was roster-discovered — a single tenant's agent misbehaving must not take down a daemon serving several. confirm_author_not_known_agent's baseline (this daemon's watch list) moves from a frozen Arc<[String]> snapshot taken once at startup to a new live WatchList (Arc<Mutex<HashSet<String>>>) updated as agents are added or removed. A frozen snapshot would let a roster-added agent's own mention wake another agent undetected, defeating the no-agent-to-agent-wake-loop invariant the guard exists to enforce. New env vars, both optional: WAKER_OWNER_PUBKEYS (comma-separated authorized owners; empty/unset disables dynamic enrolment, matching parse_authorized_owners' existing fail-closed contract) and WAKER_IDENTITY_NSEC (required only when WAKER_OWNER_PUBKEYS is set — the roster/credential taps' own connecting identity, never a watched agent's). Deliberately deferred, not implemented this round: a WAKER_MAX_AGENTS capacity bound (open tuning value in the design doc, not part of this step's own build-order text) and reacting to a credential rotation/ revocation for an already-running dynamically watched agent (the credential tap keeps running and would log one, but only the first delivered credential bootstraps identity). WAKER_AGENTS_CONFIG_PATH still requires at least one entry — a pure roster-only daemon with zero static agents isn't possible yet. cargo test -p buzz-waker: 261 lib + 17 main pass (12 new: 6 for WatchList, 6 for compute_desired_roster_agents/classify_exit). clippy -D warnings and fmt --check clean. Signed-off-by: Junchao Yan <yjc801@gmail.com>
Alex's review of the dynamic supervisor (PR #50, head 7b149cd) found three real P1s: 1. RosterEntry.credential_version was discarded, and an agent was promoted the moment CredentialState held *any* delivery — a stale or replayed version could bootstrap the wrong identity, and once running, nothing ever re-checked a later rotation or revocation. Fixed: SupervisedAgent now tracks expected_credential_version and keeps its CredentialState wired for the agent's whole supervised lifetime, not just bootstrap. reconcile_roster gained two new passes: one tears down a still-listed roster agent (pending or already running) the moment the roster's own claimed version moves, so re-bootstrap starts fresh under the new version in the same tick; promotion (and a running agent's continued operation) now requires an exact version match against the live delivery, and a revocation (CredentialState flipping to None while running) tears the agent down too. 2. CredentialBody.provider_credential was decoded but never used — provider_deploy_pinned ran with no tenant environment, so a dynamically enrolled owner's deploy would use the daemon's own inherited credentials or fail outright. buzz-provider-deploy gained an additive env: Option<&HashMap<String, String>> parameter on every entry point (None preserves every existing caller's behavior exactly, including desktop's), applied as an overlay via Command::envs — not isolation, and the module doc says so plainly, since no daemon-baseline spec for that ever existed anywhere in this codebase to reuse. ProviderCredential::to_env() derives the overlay (SPRITE_TOKEN today); RealWakeEffects/WakeLoopConfig/spawn_agent_watch now carry it from the delivered credential through to the actual deploy call. 3. Dynamic enrolment had no admission cap — an authorized owner's roster, however large, was adopted in full. New required-alongside- WAKER_IDENTITY_NSEC env var WAKER_MAX_AGENTS: a refuse-not-evict ceiling counting every supervised pubkey (config, pending, and running together), checked before each new roster adoption. cargo test -p buzz-waker -p buzz-provider-deploy: 262 lib + 18 main + 31 (provider-deploy, +2 new proving the env overlay actually reaches the child and overrides an inherited variable) all pass. clippy -D warnings and fmt --check clean across both crates plus desktop/src-tauri (checked and clippy'd standalone; backend.rs's two call sites needed a trailing None each). Signed-off-by: Junchao Yan <yjc801@gmail.com>
…atic baseline Alex's re-review of PR #50 (head ef8172b) found three more real gaps in the dynamic supervisor: 1. A roster credential-version change tore down and re-adopted the same pubkey within one reconcile_roster pass. The predecessor generation's presence/bundle/wake/credential tasks kept running until their own cancellation propagated, and when they finally exited, the join loop looked up `supervised` by pubkey and found the *replacement* generation's fresh, uncancelled token — misclassifying an expected predecessor exit as the replacement's unsolicited failure and tearing the new generation down too, so rotation could never converge. Fixed: TaskExit now carries a clone of the exact CancellationToken the task was spawned under, captured at spawn time; the join loop checks that token's own is_cancelled() directly instead of re-deriving cancellation status from whatever currently occupies the pubkey in `supervised`. 2. The `env` overlay onto a tenant's deploy subprocess only ever added SPRITE_TOKEN on top of this daemon's own inherited environment — the child still saw WAKER_IDENTITY_NSEC and everything else the daemon's process carries. Since the tenant's own bundle authorizes which provider binary/digest runs, an unisolated child could read and exfiltrate secrets across the shared-daemon boundary. Fixed: buzz-provider-deploy now clears the child's environment before adding back a small fixed baseline (HOME, PATH — both required by buzz-backend-sprites's own credential resolution and provisioning code) plus the tenant's own credential, whenever `env` is `Some`. `None` (every non-multi-tenant caller, including desktop) is unaffected. 3. WAKER_MAX_AGENTS is documented as a total ceiling over config, pending, and running agents together, but reconcile_roster's refuse-not-evict check only ever guarded roster *additions* — a static WAKER_AGENTS_CONFIG_PATH baseline already past the ceiling started unchecked and stayed that way for the daemon's whole life. Fixed: ensure_static_agent_count_fits_cap fails startup before any agent is spawned or gets per-agent state on disk when the static baseline alone exceeds WAKER_MAX_AGENTS and dynamic enrolment is enabled. cargo test -p buzz-waker -p buzz-provider-deploy: 262 lib + 21 main (4 new) + 32 (1 new) pass. clippy -D warnings and fmt --check clean across both crates plus desktop/src-tauri (unaffected call sites verified). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Junchao Yan <yjc801@gmail.com>
PR #48 squash-merged into main after this branch's base was claude/waker-enrolment-schema, so GitHub auto-retargeted PR #50's base to main and reported it as conflicting. Both conflicts were mechanical: lib.rs's module doc comment on main still described the supervisor as unimplemented, and enrolment.rs's add/add conflict was this branch's own to_env()/test addition versus main's pre-existing content that this branch's copy already contains in full. This branch's version is a strict superset of main's for both files (verified by diff before resolving) — no logic changed by this merge. Signed-off-by: Junchao Yan <yjc801@gmail.com>
This was referenced Aug 13, 2026
yjc801
added a commit
that referenced
this pull request
Aug 13, 2026
* feat(desktop): issue waker enrolment rosters and credentials #48 and #50 built the daemon half of enrolment — roster and credential taps, per-owner deploy, a reconciling supervisor — but nothing anywhere published what those taps read. A grep of desktop/ for SignedRoster or CredentialBody found only MeshLLM's unrelated roster, so the daemon had subscriptions with nothing to receive and enrolment was inert end to end. This is the issuing side. Two payloads, both owner-signed and NIP-44-encrypted to the waker identity rather than to the agent, riding the same envelope and the same pending_sync row the launch bundle already uses — so publishing, retries, and the WebSocket routing #38 added come for free. A roster of every enrolled agent at the fixed coordinate the daemon queries, and one credential per agent carrying its nsec, auth_tag, and the owner's own provider credential. The retention store made this less mechanical than it looks. Its primary key is (kind, pubkey, d_tag) with no p column, and on the relay a credential is distinguished from that agent's launch bundle *only* by the p tag — so a credential keyed by the bare agent pubkey lands on the bundle's own row and displaces it, leaving whichever was written second as the only one ever published. Remote wake would have broken for exactly the agents that enrolled. Rows are namespaced (`credential:<pubkey>`) while the published event keeps the real d tag, which is sound because the flush loop publishes raw_event verbatim. A test asserts the bundle survives issuing a credential and still decrypts as the agent. Ordering is credential first, roster second, never the reverse: the roster names the credential version each agent is expected to have, so that order leaves a partial failure with a credential no roster mentions — inert — rather than a roster promising a credential that does not exist. For the same reason the roster lists only agents whose credential was actually issued. Revocation does both halves. Roster omission is what stops the daemon watching an agent; the revoked credential is what reaches a daemon already holding the old one and raises its durable floor. Not best-effort, matching revoke_waker_bundle_pending: a caller must refuse to persist the disable rather than report success with the agent still enrolled. Deliberately incomplete in one place, and it does not regress anything: provider credentials resolve from SPRITE_TOKEN/SPRITES_TOKEN only. The production path for a Finder-launched desktop is the keychain arm in buzz-backend-sprites, but that crate is bin-only, so reaching it means either restructuring it into a library or duplicating its ~/.sprites walk — neither belongs here. An absent credential means the daemon deploys with its own, which is what it does today for every statically configured agent. The waker identity comes from WAGGLE_WAKER_IDENTITY_PUBKEY over a compiled-in default that is empty, since only whoever deploys the daemon knows its pubkey. Unset means enrolment is a silent no-op, which is every install that does not use a remote waker. Found while writing the tests: the daemon rejects a live credential whose nsec does not parse or does not derive the claimed agent_pubkey, so placeholder keys prove nothing. Tests use real keys and run the daemon's own verifier rather than a re-implementation. Verified: 2505 desktop unit tests pass, clippy clean, fmt clean, file-size ratchet clean. Not yet exercised against a live daemon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Junchao Yan <yjc801@gmail.com> * fix(desktop): drop per-user provider credentials from enrolment There are two roles, not three: a service provider who runs the waker and holds the one Sprites token it deploys with, and users who toggle Remote wake and configure nothing. The design's "per-owner provider credentials" reads as the provider's, but "owner" is this codebase's word for the key that signs an agent's events — the user's own desktop key. Under the two-role model a user has no Sprites token to send, so the field could never be filled: `sprite login` is not part of using a hosted waker. So the resolver goes rather than growing a keychain arm to feed it. The credential now carries the agent's nsec and auth_tag and nothing else — exactly what the daemon needs to connect *as* that agent. Better wire, not just less code: no provider token ever transits the relay, and no user can choose which token the daemon spends. `org` still travels per agent in the signed provider_config, which is where per-agent provider targeting already belonged. The prior commit's env-only resolver would have read SPRITE_TOKEN from whatever environment the desktop happened to launch with and encrypted it to the waker. For a Finder-launched app that is empty, so it was mostly dead; where it did fire it sent a secret that did not need to travel. Terminology fixed throughout: "service provider" for whoever runs the daemon, "user" for whoever toggles the switch, and a note at the top of the commands module that "owner" means the signing key rather than either of them. 2504 desktop unit tests pass, clippy clean, fmt clean, ratchet clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Junchao Yan <yjc801@gmail.com> * fix(desktop): issue enrolment on enable, commit-gate the roster, and don't strand a revoked disable Three round-1 review findings on the waker enrolment path: - set_managed_agent_waker_enabled's false-to-true branch only issued a launch bundle, bypassing retain_waker_enrolment_pending entirely. A freshly enabled agent published no credential or roster entry, so the daemon could never discover it until an unrelated later edit happened to go through the generic retain helper. Issue enrolment in that path too, with the same fail-and-roll-back semantics as the bundle half. - IssuanceLedger::reserve durably burns a version before signing, encrypting, or retaining a credential, so a failure after reservation (invalid waker key, retention error) left the roster reading a version nothing was ever published to. Add a committed counter recorded only once a credential is actually retained, and build roster entries from that instead of the raw reservation counter. - revoke_waker_enrolment_pending retained the credential revocation before the roster. If the roster write then failed, the caller reported the disable as aborted while the already-queued revocation could still publish and tear the agent down — local state and the external effect claiming opposites. Once the credential revocation is retained the security effect is committed and cannot be un-queued, so treat that as a completed disable and make the roster retract best-effort from there; a roster that still names the agent self-corrects the next time any other agent's enrolment is retained. just gate: fmt, clippy -D warnings, desktop-tauri tests (2505 passed), desktop typecheck/lint/build all clean. Signed-off-by: Junchao Yan <yjc801@gmail.com> * fix(desktop): don't fail a disable after the revocation is queued sign_and_retain_credential_at reserved a version, signed, retained the encrypted credential envelope (the durable, irreversible queue point for the flush loop), and then made a separate fallible ledger write to record the committed version. A failure in that trailing write propagated as an Err even though the credential — including a revocation — was already retained and would still publish. For revoke_waker_enrolment_pending this meant a caller could report the disable as aborted ("waker remains enabled") while the already-queued revocation went on to tear the agent down regardless, contradicting the reported result. Treat record_committed's failure as it's treated elsewhere in this ledger: log and continue once the thing it is bookkeeping for has actually landed. Signed-off-by: Junchao Yan <yjc801@gmail.com> * fix(desktop): distinguish live issuance from revocation on a lost commit marker The round-3 fix made every record_committed failure non-fatal once the credential envelope was retained. That's correct for a revocation, whose irreversible effect is the thing that must commit — but wrong for a live issuance: swallowing the failure there let the enable command report success while committed() stayed at 0 and retain_roster_at silently omitted the agent, leaving Remote wake enabled locally but undiscoverable by the daemon. Gate the behavior on the existing `revoked` parameter instead of applying it unconditionally: a revocation logs and continues (the disable must persist regardless), a live issuance still propagates the failure (the existing enable-path caller already rolls back waker_enabled on Err, which is the correct outcome here). Signed-off-by: Junchao Yan <yjc801@gmail.com> --------- Signed-off-by: Junchao Yan <yjc801@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacks on #48 (approved, not yet merged) — base is
claude/waker-enrolment-schemarather thanmainbecause this needs its roster/credential taps. Once #48 merges, this PR's base should be retargeted tomain.Implements
PLANS/BUZZ_WAKER_DESIGN.md§12 build order step 3, the "highest risk" step: making the daemon's watched-agent set dynamic instead of a fixed startup snapshot.spawn_agent_watch, shared by both the staticWAKER_AGENTS_CONFIG_PATHstartup path (unchanged behavior) and a new reconciliation loop that diffs every authorized owner's roster against asupervisedmap, spawns a per-agent credential tap to fetch a newly-listed agent'snsec, then callsspawn_agent_watchonce that credential arrives. A statically configured pubkey always wins a collision against a roster-discovered one (compute_desired_roster_agents).CancellationToken::child_token. An unsolicited exit is classified (classify_exit): fatal for the whole daemon if the agent was statically configured (preserves today's exact behavior whenWAKER_OWNER_PUBKEYSis unset), but tears down only that one agent if it was roster-discovered — one tenant's agent misbehaving must not take down a daemon serving several.confirm_author_not_known_agent's baseline moves from a frozenArc<[String]>snapshot to a new liveWatchList. A frozen snapshot would let a roster-added agent's own mention wake another agent undetected, defeating the no-agent-to-agent-wake-loop invariant.WAKER_OWNER_PUBKEYS(empty/unset disables dynamic enrolment) andWAKER_IDENTITY_NSEC(required only when owners are configured).Deliberately deferred (flagged, not silently dropped): a
WAKER_MAX_AGENTScapacity bound (open tuning value in the design doc, not part of this step's own build-order text) and reacting to a credential rotation/revocation for an already-watched agent (the tap keeps running and would log one, but only the first delivered credential bootstraps identity).WAKER_AGENTS_CONFIG_PATHstill requires at least one entry — a pure roster-only daemon with zero static agents isn't possible yet.Test plan
cargo test -p buzz-waker: 261 lib + 17 main pass (12 new tests)cargo clippy -p buzz-waker --all-targets -- -D warningscleancargo fmt -p buzz-waker -- --checkcleangit diff --checkcleanjust gate/just ci— not run against this diff standalone (base isn'tmainyet); ran clean against the equivalent buzz-waker-only scope🤖 Generated with Claude Code