Widen run_identity_transition, import_identity_blocking, and their two
drain/compensate helpers to <R: tauri::Runtime>, plus the shared
drain_scope_runtimes on the apply_workspace path. Monomorphization makes
the existing Wry call sites (import_identity, import_recovered_identity,
apply_workspace) compile to identical code — behavior-neutral. This lets
the P26-C1 coordinator schedules be driven under mock_builder()'s
App<MockRuntime> in the forthcoming §7 fixtures.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The coordinator exposed a single pre-commit validity check reaching only the
late (fenced) boundary, but P26-C1 mandates TWO gates: an early gate under the
held transition lock — before Mesh preflight and drain — so a task superseded
while queued on the locks is rejected having done zero disruptive work, and the
existing late gate held across the durable commit. A single FnOnce cannot cover
both; without the early gate a cancelled phone-recovery would drain runtimes and
revoke owner-identity durable capabilities before the late check rejected it,
churning the fail-closed side uncompensated.
Take two explicit checks (early_validity_check + late_validity_check). Normal
import supplies always-Ok for both; pairing supplies two clones of the
idempotent task-currency read. Collapse the mesh/non-mesh fork onto one manual
workspace_transition acquisition and gate the mesh preflight call so the early
gate sits at the same point under both feature configs.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The identity-transition coordinator now drains owner-identity egress between
the runtime drain and the durable B dispatch, closing the split-state class
where an already-issued lease or durable capability could transmit under A
after B commits. Because the coordinator body holds two std-mutex Layer-2
guards it cannot carry across .await, the drain is synchronous: a new
`wait_egress_drain_blocking` parks on a Condvar beside the egress registry,
notified from both lease Drop impls. Both guards stay held continuously across
the barrier — releasing either permits A-runtime resurrection before B commits
(the three-phase guard-release shape was ruled UNSAFE). Deadlock-freedom rests
on leases taking only the egress mutex; a tree sweep found zero lease-vs-guard
sites.
The persist stage is now classified against durable fact (P27-C1) under the
retained commit fence, after a fsync'd IdentityTransitionPending journal
(P28-C1): Committed finishes the in-memory swap and clears the journal;
DefinitelyUnchanged compensates the drain; Indeterminate latches fail-closed
and leaves the journal for boot reconciliation. `commit_imported_identity` no
longer performs the durable persist — it takes the proven storage and does
only the in-memory swap, so it runs on the Committed arm alone.
Retires the dead_code allows the coordinator now consumes across the egress,
persistence, and journal modules. An RAII test guard resets the egress
registry on drop so a latched Indeterminate state cannot leak into crate tests
that read the indeterminate-gated key accessors without the registry lock.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
When the keyring is unreachable at boot and an identity-transition journal
row survives, the durable-B candidate cannot be reconciled this boot. Loading
the A-valued identity.key would resume fully-signable A over a possibly-canonical
B — the exact split-state the journal exists to prevent. Fail closed to the
recovery-blocked posture (ephemeral key, signing disabled) the base
migration-marker branch already uses, never RecoveryState::None; a later boot
with a reachable keyring reconciles from durable fact.
Retires pending_exists's allow(dead_code) now that startup consumes it. Pins an
allow on PersistenceOutcome::Committed's storage field (consumed by the not-yet-
wired P29/P30 barrier body) to keep the tree clippy -D warnings clean; retired
in the sub-commit F zero-allow sweep.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
run_identity_transition previously sampled has_active_scope before acquiring
workspace_transition and skipped the lock entirely on the no-scope path. That
left a None -> Some activation race: a concurrent apply_workspace could commit
a new active scope between the pre-branch sample and the durable identity
commit, so the import would drain nothing while a live scope existed.
Route both the mesh-llm and non-mesh paths through workspace_transition
unconditionally, and move the active/no-scope decision inside
import_identity_blocking, derived from a scope snapshot taken under the held
guard. Sampling under the guard closes the race: apply_workspace and identity
import now serialize on workspace_transition, so no activation can slip between
the sample and the commit. No deadlock — apply_workspace takes only
workspace_transition (never identity_mutation), so the global order
identity_mutation -> workspace_transition has no cycle. Drops the now-redundant
has_active_scope bool from import_identity_blocking's signature.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
An in-memory Indeterminate latch does not survive a restart: a process
that returned an ambiguous persistence outcome and then crashed would
boot fully-signable A even though a durable B may exist, and later
resolve to B when the keyring returns.
Add identity_transition_journal: an fsync'd JSON row recording
{from_pubkey, to_pubkey}, written before the coordinator dispatches the
durable B write and cleared only at the two proven exits. read_pending /
pending_exists let startup honor a surviving row, and a present-but-
corrupt row reads as pending (fail closed) rather than absent. The C5
coordinator writes/clears it in the barrier commit and startup honors it
in the recovery-route commit.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The base persistence kernel returns a binary Result, but it is not
transactional: a returned Err does not prove no durable key write
happened. A coordinator that compensates on any Err can leave durable
identity B on disk beside live in-memory identity A, and a later
reachable-keyring restart activates B (the P25/P26 split-state class).
Add persist_imported_identity_classified, which wraps the proven kernel
and derives a three-valued PersistenceOutcome from durable fact:
Committed (B proven canonical, compensation forbidden),
DefinitelyUnchanged (B never landed, the only compensable outcome), and
Indeterminate (neither proven, fail closed). On a kernel Err it re-reads
the keyring and identity.key under the caller's held transition guards
and classifies from what they hold; a keyring unreachable on re-read
fails closed. The C5 coordinator consumes this in the barrier commit.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- preserve the WebKit-required `Window` receiver when scheduling and
clearing presence subscription retries
- add a receiver-sensitive regression test covering both retry creation
and disposal
## Impact
When subscription opening failed, WebKit rejected the detached timer
call before `retryTimer` could be set. The reconciler's `finally` block
then immediately started another reconciliation because demand was still
unsatisfied and no retry appeared pending. This bypassed the intended
exponential backoff and could repeatedly reopen subscription work during
startup, so the impact was more than console noise.
With the timer receiver fixed, a failed open schedules one bounded retry
at a time (1s exponential backoff, capped at 30s), and disposal cancels
it correctly.
## Validation
- `cd desktop && node --import ./test-loader.mjs
--experimental-strip-types --test
src/features/presence/lib/presenceSubscriptionReconciler.test.mjs` (11
passed)
- `cd desktop && pnpm test` (4,993 passed)
- `cd desktop && pnpm typecheck`
- `cd desktop && pnpm check` (passes with existing unrelated warnings)
- pre-push hook on `578b9a0b5c851b21de0b8746238b04dc69032e78` (desktop
check/typecheck/tests, Rust tests, Tauri checks, mobile tests all
passed)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary
- make the differential file-size ratchet a first-class repository gate
- run the same unfiltered gate from pre-push, `just check`, and CI
- remove hidden file-size coupling from Desktop, Web, and Mobile lint
commands
- isolate ratchet Git subprocesses from hook-exported repository state
## Why
The Desktop ratchet grew to govern `desktop/src-tauri/crates/**`, but
the pre-push `desktop-check` command remained path-filtered to non-Tauri
files. That contract drift allowed a Tauri Rust file-size regression
through local validation.
The ratchet already computes its own merge-base diff, so duplicating
governed paths in Lefthook and CI adds drift risk without meaningful
runtime savings. One root gate owns the policy now.
## Testing
- `just file-size-check`
- oversized untracked probe under `desktop/src-tauri/crates/**` fails
with the 1,000-line ceiling
- file-size core tests with hook-style `GIT_DIR` / `GIT_WORK_TREE`
environment
- `lefthook dump` confirms the unfiltered pre-push command
- mandatory pre-push suite passed on
`3217003db10c84d8a0c5f636ec4c92936777a002`
- `git diff --check`
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
The C5 coordinator additions (`run_identity_transition`, `commit_under_fence`)
pushed `commands/identity.rs` past the 1000-line desktop file-size ratchet.
Move the coordinator cluster — the drain helper, `run_identity_transition`,
`commit_under_fence`, and `import_identity_blocking` — into a `#[path]` sibling
`identity_transition.rs`, mirroring the `agents_scoped.rs` split. The two
`pub(crate)` entry points are re-exported so external call paths (`import_identity`,
phone recovery, the fence tests) are unchanged. No behavior change.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Phone-recovery identity swap (`import_recovered_identity`) bypassed the
managed-agent runtime drain, active-scope clear, and scope-generation bump
that `import_identity` performs (P25-C1): it acquired only `identity_mutation`
and committed via `commit_imported_identity` directly. A recovery in an active
workspace left stale agent runtimes bound to the prior identity.
Extract the shared `run_identity_transition` coordinator so both callers route
through the same lock-ordering and drain path. The pre-commit boundary is
generalized to a `commit_under_fence` primitive: it locks the supplied fence,
runs the validity check, and runs the durable commit under the same held guard,
so a racing supersession can neither interleave nor slip between check and
commit (P26-C1). Normal import passes no fence and an always-Ok check; recovery
passes the pairing `generation_fence` and the task-currency check.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
Live 0.5.16 review (Royal Court thread) traced the sustained 20–25%
WebContent CPU burn in any large mounted channel with a working agent to
three defects, in descending impact:
1. **Shimmer animation forces per-frame style + compositing walks.**
`.buzz-shimmer` animated `background-position` under
`-webkit-background-clip: text`, which WebKit cannot run on the
compositor: every frame did a full document style resolve plus a
recursive compositing-hierarchy update over the timeline's layer tree.
The highlight now lives on an `aria-hidden` overlay child duplicating
the label text and animates **opacity only** (compositor-accelerated); a
real element is used instead of `::after` generated content so screen
readers never see the duplicate text. Visual: the moving sweep becomes a
gentle pulse. `prefers-reduced-motion` removes the overlay entirely —
static muted label, exactly as before.
2. **Observer journal whole-journal dedup + re-sort per append.** Every
one-per-second observer frame rebuilt a dedup Set over up to 3000
retained events and re-sorted the whole journal with a
`Date.parse`-per-comparison comparator. In-order batches (the ordinary
live path, same condition as the existing incremental transcript fold)
now dedup within the batch and concat; out-of-order/replayed arrivals
keep the full path.
3. **Redundant disk reads in the 5s agent-list poll.**
`build_managed_agent_summary` re-read the global config from disk per
call despite receiving it as a parameter, and re-read the teams store
per tracked pair — 2N redundant reads per poll tick for N agents. Both
are now caller-supplied; one-shot command paths use a
`summarize_from_disk` helper. Same stores, same `unwrap_or_default`
failure posture, read once.
## Evidence (mechanism attribution, live 0.5.16-block, mounted
~1500-event channel)
- True-idle mounted view (working-state UI live): **25.08% mean / 24.15%
median** WebContent CPU; post working-state decay: **6.33% / 3.50%**.
- Reduce Motion A/B during a live agent turn (isolates the shimmer,
working UI still mounted): **19.50% mean → 5.39% mean** (72% collapse).
Native 10s samples: `Document::resolveStyle` 379 samples → 1;
`updateCompositingLayersAfterStyleChange` 378 → 0; recursive
`updateBackingAndHierarchy` 363 → 6.
- The shimmer mechanism predates 0.5.16 (CSS unchanged since #3151);
current multi-agent workloads exposed and amplified it. The mention
regression itself was fixed separately in #6182.
## Testing
- `desktop` observer store suites: 55/55 pass, including 5 new tests
pinning the fast-path invariants (equal-timestamp seq ordering,
duplicate-batch redelivery, intra-batch duplicates, overlapping late
arrival takes the slow path, transcript-equals-replay on both paths).
- `cargo test --lib managed_agents` (1016 passed) and `--lib commands`
(727 passed); `cargo clippy` clean; `pnpm typecheck` + biome clean.
- Release gate for the patched build (per Mongo): active-turn mounted
CPU must collapse from the ~21–25% baseline with no per-frame
style/compositing walk in a native sample — measured on Wes's workspace
once a build with this branch is running.
Findings and raw samples:
`RESEARCH/LIVE_0_5_16_WEBKIT_ATTRIBUTION_2026_08_18.md`,
`RESEARCH/RENDERER_STEADY_STATE_LOOP_AUDIT_2026_08_18.md`,
`.scratch/live-app-review/` (Carl/Donut/Mongo/Brain, Royal Court thread
01a7fe75).
---
*Opened by Brain (agent) via @wesbillman's account on his behalf —
coordinated in Buzz channel agent-mention-policy-royal-court, thread
01a7fe75.*
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
h2 0.4.14 is vulnerable to unbounded memory growth from empty DATA
frames (RUSTSEC-2026-0258, patched in 0.4.16). cargo-deny fails the
Security gate on the advisory ingest. Bump is lockfile-only; 224 other
dependencies unchanged.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The production adapters unwrap .value from the StampedArtifact wire
shape, but the Playwright bridge mocked sign_event, create_auth_event,
nip44_encrypt_to_self, nip44_decrypt_from_self, and
sign_nostr_identity_binding with bare returns. At runtime the app under
test hit JSON.parse(undefined) at every sign/encrypt site, cascading
into membership-subscribe, read-state, and badge failures across the
smoke suite. Add a local stamped() helper and route every owner-identity
artifact mock (including the C4 nsec/backup cases) through it.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
get_nsec and create_ncryptsec_backup admit an owner-identity egress
lease BEFORE reading/deriving the secret and return their value as a
StampedArtifact carrying the issuing lease's generation. The NIP-49
constructor sits outside the sign/encrypt method sweep, so the export
class is stamped constructor-agnostically. Adapters unwrap .value; the
reveal/copy generation-compare defers to C6/C7. Closed-world
enumeration and the ncryptsec source allowlist updated in the same
commit.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Compose main's post-C3 changes with the workspace-scoped agent store
(WSA) refactor, keeping BOTH sides' semantics. Non-mechanical resolutions
(per Paul's cluster ruling, thread eb3246b2):
- Pollen rename → scoped-pipeline step 1.5 (NOT global pre-scope): the
live store on HEAD is scopes/<id>/managed-agents.json, so a global-only
rename never reaches an adopted scope and orphans the reconcile queue.
READY_MARKER_VERSION bumped v1→v2 so scopes marked ready under v1 re-run
the two new idempotent steps.
- Team-membership repair → scoped step 4.5 (before the step-5 detach; the
clean-repair gate is preserved by construction via fatal-on-Err) PLUS a
per-apply repair-only call in apply_workspace, preserving main's
every-boot cadence upstream of the superseding-head write.
- Event-sync fatal team leg + run_event_sync_blocking awaited in
apply_workspace, keyed off the scoped active_retention_scope +
definitions_dir; spawn_pending_profile_reconciliations after apply.
main's migrate_legacy_retention_into is dropped as subsumed by
scope-init's pre-Ready migrate_legacy_retention_db (Step A).
- inbound.rs apply composed HEAD's §2.8 linkage-freeze with main's
access-policy runtime-refresh into InboundAgentApply { linkage,
access_changed }.
- P29-C1 owner-identity egress lease ported to net-new main sign sites
(project owner announcement, relay/get.rs, sign_project_issue_assignee
operation) to compile against submit_signed_event_with_keys(&lease).
Merge fallout resolved: #5682's local-spawn idle-pool-sleep env
(idle_pool_sleep_env / IDLE_POOL_SLEEP_SECS) is subsumed — its only call
site was the local direct-env block HEAD's ff16a80b deleted, and the
remote-deploy policy_env path never carried it; the orphaned symbols are
dropped. spawn_event_sync removed (its sole caller became
run_event_sync_blocking). mesh_llm_tests.rs import fixed for main's
readiness→mesh_readiness rename (its symbols' coverage now lives in
mesh_readiness.rs's own inline tests). Test call sites updated for main's
new prospective_spawn_config_snapshot enforced_owner_only arg,
ManagedAgentRecord::provider_policy_pending field,
AgentUpdateRollback::new preserve_access_policy arg, private AppState.keys
(via identity_lifecycle_keys_guard), and submit_event_with_keys(&lease).
File-size ratchet (base 978e585e): four files crossed the 1000-line cap
after the merge and were split, never trimmed. team_snapshot.rs →
team_snapshot/retain.rs (retain_agent_pending); personas/inbound.rs →
inbound/tombstone.rs (parse_deletion_coordinate + reconcile_inbound_
tombstone); inbound/inbound_tests.rs → inbound/team_tests.rs (kind:30176
team-inbound tests); managed_agents/retention.rs → retention/tests.rs
(inline test module).
Desktop test mocks stamped: main-authored identity-command mocks
(sidebarSyncTestHelpers.mjs installTauriMock; communityThemeSync.test.mjs
onboarding-fetch decrypt) returned bare values, which the C3 owner-
identity adapters unwrap as .value → undefined. Wrapped in the
{value, artifact:{id,generation}} stamped shape (inert stamp in C3),
matching the resolution already applied to the file's other mocks.
Gates at merge HEAD: cargo fmt --check clean; cargo clippy --workspace
--all-targets -D warnings clean (default features); cargo test --lib
2710 passed / 0 failed; desktop-check (file-size ratchet) clean;
desktop-typecheck clean; desktop-test 4991 passed / 0 failed; mesh-llm
leg 90 passed / 0 failed.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Extend the durable owner-identity capability registry with ArtifactPolicy
(NIP-AP, P30/P31/P32-C1): a generation-stamped owner-key-derived value that
leaves its producing lease and is applied later. Unlike Session/Bearer,
an artifact owns no side-effecting teardown, so its only revocation is the
exercise-time / application-site generation compare — the transition bump
IS the invalidation (shape (ii), acked by Paul). revoke_durable_capabilities_before
performs no per-entry artifact work; the barrier reaches artifacts via the bump.
The seven commands/identity producers admit a bounded lease before the owner-key
sign/encrypt/decrypt and return the value wrapped as StampedArtifact<T>
({value, artifact:{id,generation}}), stamping the issuing lease's generation
(the F1 lesson). The wire shape is locked on both sides; a serialization test
pins it so C6/C7 inherits a stable contract.
The 7 TS adapters unwrap .value at the boundary with a named C6/C7 deferral —
outward threading to application sites + the generation-compare lands in C6/C7
coupled to the code that reads the stamp (supersedes condition 2 of the
stamp-at-boundary ruling, revised on the measured 87-file transitive radius).
StampedArtifact type and the four identity adapters split into small modules to
respect the desktop file-size ratchet.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Addresses Paul's four C2 findings before C3.
F1 (barrier bypass): register_owner_session/register_owner_bearer re-read the
current generation at registration, so a capability derived from a losing
identity could be stamped with the winning generation and survive the C5
barrier. Both now take &OwnerIdentityEgressLease as a compile-enforced witness
and stamp lease.generation() via a shared register_durable() helper — a bump
between admission and registration leaves a stale stamp that the first
admit_exercise refuses (fail-closed). Huddle registers the session while the
auth lease is still held (before the joined await, not spanning it). Corrects
the DurableRegistry "same lock" and media.rs "no bump can slip" doc claims.
Adds a red-then-green control: admit under gen N, drain bumps to N+1, register,
exercise refuses.
F3 (doctrine): the upload legacy retry re-attached the signed header without a
second admit_exercise. Revalidate before the retry dispatch — two
transmissions, two validations.
F4 (scope): create_auth_event (frontend relay WS) now signs under a per-send
bounded lease, gating reconnection against an in-flight drain. Frontend session
REGISTRATION (capability + native-WS teardown) is explicitly deferred to C6/C7
with the frontend identity store.
F2 (gates): split six files under the desktop file-size ratchet — never
trimmed. owner_identity_egress.rs → directory module with the durable substrate
in durable.rs; relay.rs/media.rs/identity.rs move test modules to sibling
_tests.rs files; card.rs extracts the card-archive cluster to card/archive.rs
and messages.rs the feed-item projection to messages/feed_item.rs.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Buzz Desktop release v0.5.16
- **Frozen main:** `ee992ff0822f44d1c308822f116cb9d26f9a3386`
- **Reviewed candidate:** `a6211b0e285600a6f08d6592261e44ecc4a6917b`
- **Previous desktop release:** `desktop-v0.5.15`
- **Proposed immutable tag:** `desktop-v0.5.16`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
Bring the owner-derived Blossom bearers into the durable-capability world so
a header minted under identity A cannot be attached after a transition to B.
mint_media_get_auth and the do_upload t=upload mint now sign under a bounded
egress lease (issuance is an ordinary leased operation, spec L4569-4570) and
return/hold an OwnerIdentityCapability<BearerPolicy> registered with its
revocation handle. The four get-auth attach sites (media_download,
personas::card, media_proxy x2) and the upload dispatch validate the bearer
via admit_exercise() immediately before the HTTP send — a stale capability
attaches nothing (get-auth stays fail-open) or uploads zero bytes.
mint_media_get_auth becomes async; the ripple is a mechanical .await through
its four already-async callers. Removes the register_owner_bearer
allow(dead_code) now that C2b consumes it. Substrate coverage is unchanged:
the stale-bearer zero-bytes and registration-completeness controls already
pin the exercise behavior these guards depend on.
2438 lib tests pass, clippy + fmt clean.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- preserve OSS relay-agent mentions under shared channel and agent
policy
- restrict owner-only release builds to relay agents with
cryptographically verified ownership matching the current user
- remove the remote policy replay loop that repeatedly rebuilt the relay
directory, while retaining focused polling and send-time revalidation
- query relay profiles and managed policies by exact author coordinates
to prevent noisy events from crowding out valid agents
## Diagnosis
The packaged Block release compiles
`BUZZ_DESKTOP_BUILD_AGENT_ACCESS_OWNER_ONLY`, while ordinary OSS/dev
builds do not. Relay-discovered agents were filtered as if all remote
agents were outside that owner-only boundary, so a same-owner agent
running on another machine disappeared in the release even though the
OSS path could look healthy.
The fix uses the NIP-OA-authenticated owner from the relay directory as
the cross-machine proof. Internal builds admit only verified same-owner
agents and fail closed for missing, mismatched, stale/revoked, or
unavailable ownership evidence. OSS builds retain shared channel/policy
behavior.
## Validation
- desktop focused unit coverage: 39 tests passed
- desktop typecheck and focused static checks passed
- focused Tauri Rust policy/directory tests passed
- production-style E2E build succeeded
- targeted Playwright mention scenarios passed:
- owner-only release hides other-owned relay agent
- owner-only release shows verified same-owner relay agent
- OSS build shows shared `anyone` agent
- repository pre-push hook passed on
`4d40b6e5bb032f2c0755127172c50dee213f65a3`:
- branch skew
- desktop check and typecheck
- desktop tests
- Rust tests
- Tauri checks
- mobile tests
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Add OwnerIdentityCapability<P> over the existing egress registry: a
generation-stamped, registry-tracked handle for authority that outlives the
bounded lease that derived it. Two policies land: Session (authenticated
connections — the huddle audio socket, later the frontend relay WS) and
Bearer (pre-minted Blossom headers, threaded in a follow-up). Each capability
is registered with its revocation handle (the session's cancellation token;
the bearer's registry id) so the C5 coordinator barrier only invokes what C2
registered — it never retrofits the registry schema.
admit_exercise() validates BOTH current egress admission AND
capability_generation == current identity-persistence generation immediately
before each transmission, so a stale capability sends zero bytes. Issuance
runs under a bounded lease (the signing that derives the capability is an
ordinary leased operation).
The huddle audio socket is threaded: the NIP-42 auth signs under a bounded
lease (dropped before the joined-await), the session capability is registered
with the connection's cancel token, and the send task validates it before
every frame batch — a frame cannot ride the established peer after an identity
transition supersedes it.
C2 builds substrate only: the coordinator revocation barrier
(revoke_durable_capabilities_before) and drain wiring defer to C5 with the
egress drain, gated behind the same generation bump C5 introduces. Per-item
allow(dead_code) with the C5-consumer comment; C5's zero-allow confirmation
extends to these. generation never bumps until C5, so this is
behavior-preserving.
8 new unit tests (2438 lib pass): generation-stamp, exercise admits when
live+current, stale-capability zero-bytes controls (generation bump, drain,
latch) for both kinds, barrier revokes old-generation only, registration-
completeness + deregister-on-drop, and a no-transition control.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Managed-agent sink #4 was labeled a reaction publish; the actual site is
send_managed_agent_channel_message (the managed-agent channel message send).
add_reaction/remove_reaction route through the self-admitting owner wrappers
and are not ManagedAgentKeyed sites. Also scope the eight-site count to the
ManagedAgentKeyed construction set, which is what it enumerated — the owner
side has its own disjoint, larger set. Resolves the P29-C1 MINOR from C1
close.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Route every owner and managed-agent relay send through the
owner_identity_egress admission layer so no code path can publish
under an owner identity without a rate-limit-witnessed EgressLease.
Privatize AppState.keys behind latch-gated checked accessors
(signing_keys/current_pubkey), thread the EgressLease witness through
all six explicit-key funnels plus the four git-workflow and four
managed-agent sink sites, and rewrite the huddle STT task to
re-resolve keys per send with a mid-huddle recovery-latch break.
The C5 P25/P28 coordinator (journal + three-valued commit outcome +
Indeterminate latch) and its egress-drain wiring stay deferred: each
unreachable item carries a per-item allow(dead_code) naming C5 as its
consumer, with semantics pinned by the owner_identity_egress unit
tests until C5 makes them reachable from production paths.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- retain explicit regression coverage for the exact 128-channel relay
request limit
- cover the 129-channel split into 128 + 1 filters
The workflow-listing implementation originally carried by this PR landed
through #6009. This branch is now rebased onto current `main`, so the
remaining diff is only the boundary test that #6009 did not include.
Fixes#6116
## Test plan
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
workflow_queries_respect_relay_explicit_channel_limit`
- pre-push hook: Desktop checks, Desktop tests, Desktop Tauri checks,
and path-scoped Rust tests
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.15
- **Frozen main:** `7f61cf431af1d8f0480a0baf525881a12f2be7f2`
- **Reviewed candidate:** `7ad30276d05c39ccd8699ca2521e761fd285ea49`
- **Previous desktop release:** `desktop-v0.5.14`
- **Proposed immutable tag:** `desktop-v0.5.15`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
KeyStore::write_and_verify confirmed a write by calling load(), which
returns the in-process cache that store() itself just advanced — proving
the cache was updated, not that the OS keyring durably holds the value.
mint_bound_identity relies on this before returning a commit-ready binding,
so a backend that acks a write without persisting it could let Phase 4b
commit an identity binding whose only secret dies with the process,
violating the §2.5 invariant that no binding exists with an unverified key.
Route the production confirmation through SecretStore::verify_stored_raw,
which bypasses the cache and reads the OS backend directly — the same
primitive the identity path already uses. The other caller
(migrate_inline_key) is upgraded for free; its Ok/Err contract is
unchanged, only strengthened to mean durably-verified.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
**Category:** fix
**User Impact:** Workflow listings reliably include every accessible
channel, including for users with more than 128 memberships and when
connected to older relays.
**Problem:** Multi-value `#h` filters could lose live delivery, apply
channel scoping after SQL limits, mishandle partial authorization or
revocation, and permit unbounded membership work. Desktop also submitted
every channel in one request, exceeding the relay's new 128-value safety
bound.
**Solution:** Preserve NIP-01 OR semantics across relay query, count,
and live-subscription paths while enforcing authorization and bounded
explicit-channel work before database or Redis operations. Desktop keeps
the older-relay-compatible one-channel-per-filter shape, sends filters
in bounded batches, combines responses, and deduplicates signed events
by event ID.
<details>
<summary>File changes</summary>
**crates/buzz-db/src/event.rs**
Distinguishes authorization channel scopes from explicit `#h` scopes in
list and count SQL so requested channels are applied before limits
without implicitly including global rows.
**crates/buzz-relay/src/handlers/req.rs**
Shares explicit-channel scope extraction and limits, preserves valid OR
siblings when malformed branches cannot match, repairs request-local
membership misses, and registers authorized live subscriptions per
channel.
**crates/buzz-relay/src/handlers/count.rs**
Applies the same bounded explicit-channel authorization to COUNT and
preserves channel scope when a multi-channel request narrows to one
authorized channel.
**crates/buzz-relay/src/api/bridge.rs**
Brings HTTP query and count behavior in line with WebSocket semantics
before SQL execution and rejects over-limit explicit-channel requests
before membership I/O.
**crates/buzz-relay/src/subscription.rs**
Indexes multi-channel subscriptions by every authorized channel and
shrinks, rather than destroys, their scope when one channel is revoked.
**crates/buzz-relay/src/handlers/side_effects.rs**
Releases only revoked channel topics and sends terminal closure only
when no authorized channel remains.
**crates/buzz-test-client/tests/e2e_relay.rs**
Adds ignored relay integration coverage for multi-channel delivery and
valid historical/live behavior with malformed or empty OR siblings.
**desktop/src-tauri/src/commands/workflows.rs**
Builds one single-channel filter per membership, submits at most 128 per
relay request, combines batches, and deduplicates by immutable signed
event ID.
**desktop/src-tauri/src/commands/workflows_tests.rs**
Covers filter compatibility, malformed input, 129-channel batching, and
cross-batch event-ID deduplication.
</details>
## Reproduction steps
1. Join multiple channels containing workflows, open **Workflows**, and
confirm workflows from every accessible channel appear.
2. Repeat with more than 128 memberships and confirm the listing remains
complete rather than failing the relay request.
3. Send a multi-value `#h` query/count and confirm only requested
authorized channels affect SQL limits and counts.
4. Subscribe to channels A and B, revoke A, and confirm B continues
delivering live events.
5. Subscribe with a valid channel branch plus a malformed or empty `#h`
sibling and confirm valid history, EOSE, and post-EOSE live delivery
still occur.
## Validation
At pushed head `c419a923f05e483ab26c006a0b3a80cfb3c73844`:
- Relay request tests: 53 passed.
- Desktop full Rust unit suite: 2,468 passed, 17 ignored.
- Relay E2E target compiled with `--no-run`.
- Strict relay clippy passed.
- Desktop Tauri clippy/check passed.
- Pre-push Rust tests and Desktop Tauri checks passed.
- Rust formatting and `git diff --check` passed.
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
## Why
Managed channel sessions already receive authoritative per-turn context.
The old startup recovery checklist told every new session to scan the
global feed
## What
- Remove `Startup Recovery` with concise channel and heartbeat turn
contracts.
## Risk Assessment
Low. This changes prompt guidance and its test only; routing and runtime
behavior are unchanged.
Generated with Codex
---------
Signed-off-by: Salman Mohammed <smohammed@squareup.com>
## Summary
- apply the inactive primary-navigation opacity treatment to every
sidebar destination, including Pulse, Projects, and Workflows
- remove the duplicated Inbox and Agents conditionals so future gated
rows inherit the same hierarchy
- add E2E coverage for all inactive rows and restoration to full opacity
when selected
## Validation
- `pnpm --dir desktop build:e2e`
- `pnpm --dir desktop exec playwright test badge.spec.ts --grep "primary
navigation rows share the same inactive emphasis" --project=smoke`
- pre-push hook: desktop check, typecheck, and 4,984 unit tests
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Two membership-propagation defects let an agent team silently lose
members — both observed live on Will's store (Sietch Tabr), not
hypothetical.
**Stale `persona_ids` dropped on save.** Team records written before
persona ids were namespaced hold bare slugs (`thufir`) instead of the
namespaced id (`sietch-tabr:thufir`). Nothing rewrites them, and the
interactive save path (`ensure_persona_ids_are_active`) *drops* any id
it cannot resolve — so the next in-app save shrinks the team. This nuked
four of five Sietch Tabr members.
**`team_id` drifts from team membership.** Team instructions are
injected at spawn by matching `record.team_id`
(`spawn_snapshot::effective_team_instructions`), so an instance's
binding must track its persona's membership. It drifts two ways: adding
a persona to a team leaves the persona's already-running instances at
`team_id: null` (a member in the roster but not in behavior — seen
twice, Gurney and Hayt), and removing a persona while keeping its agents
leaves the kept instance bound to a team that no longer lists it (still
drawing that team's instructions at spawn).
## Fix
A boot migration (`migration/team_membership.rs`) heals existing stores
in one pass over `teams.json` + `managed-agents.json`:
- **Rewrite stale ids.** A stale id is one no definition slug resolves.
Its target is the definition whose `source_team_persona_slug` equals the
bare slug, scoped to the team's source team (via `source_dir` for a
directory-backed team, or the unique `source_team` among resolvable
members for a detached one). Rewrite only when exactly one candidate
matches; zero or many leave the id in place — strictly safer than the
save path, which drops it.
- **Repair `team_id`.** Backfill an instance whose persona is a team
member but whose own binding is unset, and heal a stale binding whose
team no longer lists the persona (re-point when exactly one *other* team
claims it, otherwise unbind). Both directions gate on single-team
evidence — a persona spanning several teams has none (JSON team order is
not ownership), so it is left as-is and logged. A binding whose team
still lists the persona is authoritative and never touched.
Runs BEFORE `detach_directory_backed_teams` (so a not-yet-detached team
can still be scoped by its `source_dir`) and before any UI save can drop
an id. Rewrite-or-leave converges to a fixed point, so a second boot is
a no-op; the store is backed up once before either write.
The edit path (`commands/teams.rs`) propagates a membership change to
live instances immediately, without waiting for the next boot, scoped to
the delta between the pre-edit and post-edit rosters:
- **Added personas** (on the team now, not before) backfill `team_id` on
their unbound instances. An explicit add is legitimate binding evidence
even for a persona shared across teams — unlike the order-blind boot
case.
- **Removed personas** (on the team before, not now) clear `team_id` on
instances bound to *this* team (bindings to other teams are untouched),
so a "keep agents" removal stops feeding a kept instance the old team's
instructions.
- **Delta-scoping keeps a metadata-only edit inert:** with no roster
change, no instance is re-pointed — a shared unbound persona is never
silently bound to whichever team was edited last.
Propagation is best-effort after the authoritative `save_teams`
(mirroring `retain_team_pending`): the team already exists on disk, and
boot repair is the designed retry for a stale/unset binding, so a
secondary `managed-agents.json` write failure no longer fails a command
whose team write succeeded — which would otherwise let a UI retry mint a
duplicate team.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The §3.5 binding-retirement finalizer in v1 is permanently conservative
(P15-I2/P16-I1/P17-C1): it OBSERVES retirement-due entries at recovery
points and discharges nothing, because library metadata alone cannot
prove a process-global key or a community-visible identity is unused —
an unrelated plain carrier of the bound pubkey may live in an inactive
scope no deleting authority may read. The keyring entry, deferred rows,
and tombstoned binding record persist as permanent journaled markers.
Add LibraryEntry::is_retirement_due (derived condition: at least one
projection, all terminal, a binding key or deferred row still names it)
and LoadedLibrary::retirement_due_entries (the pure observer selector).
The recovery-point wiring is Phase 4b.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The mint orchestrator leaves a durable orphan row whenever a crash lands
before the atomic step-4 commit; nothing reclaimed the dangling keyring
secret. Add reap_unreferenced_orphans: for each orphan with no live
binding, delete its keyring entry THEN drop its journal row, persisting
the trimmed document once. Delete-before-drop and drop-only-on-success
make the sweep idempotent across recovery points, and a backend delete
failure keeps that row for a later retry.
store_all only merges, so a new KeyStore::delete seam (delegating to
SecretStore::delete, absent-entry = Ok) is required; the fakes mirror
that contract. Also folds in Paul's read-back-miss mint crash point:
load returning None after a verified write yields Err, no binding, and a
surviving orphan row for the reap.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The §2.5 fresh-key mint protocol requires a durable orphan-journal
checkpoint before the secret reaches the keyring, so every persisted
secret has a prior durable coordinate and no crash can strand an
un-journaled key. build_identity_binding covered construction; this adds
the ordering: generate in memory, durably journal the pubkey, keyring
write+read-back verify, then build the binding from the read-back nsec.
The durable step-2 write is an injected persist seam so the ordering is
unit-testable at every crash point without real disk IO; the atomic
step-4 binding commit + orphan removal stays with the insertion
transaction (Phase 4b). KeyStore/agent_keyring_name are lifted to
pub(crate) so the library key protocols share the keyring seam.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Fixes#5665.
`next_timestamp` in `crates/buzz-cli/src/commands/projects.rs` computed
a replacement's `created_at` as `head.created_at + 1`. The relay's
ingest path rejects events more than ±900s from server time
(`MAX_TIMESTAMP_DRIFT_SECS` in
`crates/buzz-relay/src/handlers/ingest.rs`), so:
- `projects update` on any project whose head is older than 15 minutes
fails with `relay error 400: invalid: event timestamp too far from
server time` (live repro in #5665);
- inside the window, replacements are recorded at `head+1` —
seconds-to-minutes in the past — so a concurrent wall-clock writer
silently wins LWW and audit timestamps misstate when the write happened.
## Change
`next_timestamp` now returns `max(now, head.created_at + 1)`: strictly
after the observed head (preserving the dominate-the-head guarantee for
skewed/future heads), never behind the wall clock. This mirrors the
relay's own replacement-authoring pattern (`now.max(head+1)` in
`side_effects.rs`).
## Testing
- `cargo test -p buzz-cli --lib` — 344 passed; adds
`next_timestamp_uses_wall_clock_when_head_is_stale`, and the existing
far-future-head test still holds (`head+1` wins when head > now)
- `cargo clippy -p buzz-cli --all-targets` / `cargo fmt --check` — clean
- Live before/after on a self-hosted relay: vanilla CLI fails on a
2h-aged head; with this change the same update is accepted and the head
lands at wall clock.
Same failure family as #2876 (`repos protect` vs the drift window) —
that path is not touched here.
---------
Signed-off-by: Ika Minami <ika@infiniteidol.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Co-authored-by: Ika Minami <ika@infiniteidol.com>
Co-authored-by: Ravneet Arora <rarora@squareup.com>
Phase-3 §2.5 crash-safe mint protocol construction half (P4-I3, P8-C2).
build_identity_binding is the commit-time inverse of validate_entry_bindings:
derive agent_pubkey from the read-back nsec (not the stored record — a stored
auth_tag proves nothing about this binding, and a pre-NIP-OA seed has none),
then compute a fresh auth tag with the current owner keys so the constructed
entry passes read-side validation with no legacy-None special case. Passes
nostr types straight into buzz-sdk exactly as the validator does.
LibraryDocument orphan-journal methods (journal_orphan_pubkey idempotent,
remove_orphan_pubkey, unreferenced_orphans) implement §2.5 step 2/4 plus the
recovery sweep selection: a pubkey journaled before its secret is persisted,
dropped in the same atomic write that commits its binding, and reaped if a
crash left it uncommitted. unreferenced_orphans scans for a LIVE binding only
— a deferred-archive marker does not keep an uncommitted orphan alive.
Factors entry_binds_agent out of entry_names_agent for the live-binding test
shared by both. Keyring I/O and full transaction wiring remain DEFER-to-4b.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The runtime lifecycle scope-match fix pushed runtime_commands.rs past the
1000-line file-size ratchet. Rather than trim the file to the exact ceiling
again, move the cohesive observer-lifecycle unit (observer_lifecycle_key
plus put_managed_agent_runtime_lifecycle[_for]) into a sibling
runtime_commands_observer.rs, #[path]-included like the existing seams
module. Mechanical move only — no behavior change.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Removes the promise in `SECURITY.md` to publish a GitHub Security
Advisory after every security fix is released.
The disclosure policy continues to state that Buzz follows coordinated
disclosure and credits reporters unless they request anonymity.
Checked with `git diff --check`.
Signed-off-by: Jordan Mecom <jm@squareup.com>
put_managed_agent_runtime_lifecycle_for validated pair key, start_nonce,
and process liveness but never required the tracked runtime's scope_id to
equal the current active scope — the same cross-scope leak class killed on
the sibling put_agent_session_config (P23-C1). A same-pair/same-nonce
lifecycle frame from a drained workspace was accepted and mutated the
runtime after a workspace rotation.
Capture the active scope before the runtime-map lock and reject any frame
whose runtime.scope_id differs, mirroring the sibling command's shape. Add
a capability test proving a stale-scope frame is rejected with the runtime
lifecycle unmutated (no status emit).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Phase-3 chunk 1 of the cross-workspace agent library ($2.5). Two keyring-free
pure functions with no production callers yet — the crash-safe mint protocol and
their insertion/removal transaction wiring land in Phase 4b per the resequencing
ruling:
- key_archive_protected(agent_pubkey): the deletion-safety predicate (P13-C1,
widened by P17-C1). Scans RAW library.json entries so a quarantined entry that
ever bound the pubkey still protects it; over-protecting only leaves a secret
resident, while mis-deleting a live global key is irreversible.
- select_binding_seed: deterministic seed selection (P3-I2) — earliest
created_at, ties broken by lowest pubkey; None only when zero instances exist.
Tests live in a new binding_tests submodule to keep the Phase-1 tests.rs suite
clear of the 1000-line file ratchet.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Agent profiles resolve through one shared selector (`pickProfileAgent`)
at every entry point — the persona card, the profile panel, and library
grouping. That selector ranked instances only by active/name, with no
archive awareness, so a relay-archived instance early in file order
could hijack the persona card and the profile panel. The persona card
also recorded a durable pubkey target, which could strand the panel on
an archived identity when the click landed during the archive-snapshot
fail-open window. The profile panel's Runtime → Instances roster had the
same blind spot: it rendered every persona instance raw, so archived
instances appeared mixed in with live ones as if active.
This makes the shared resolution path archive-aware via the existing
fail-open `useIsArchivedPredicate`:
- `pickProfileAgent` filters archived instances before ranking and
returns `undefined` when every instance is archived (persona-only mode).
- `buildUnifiedGroups` drops archived agents from the standalone `Custom
agents` and `Unknown agents` buckets; matched persona groups keep their
full list and rely on the selector's persona-only fallback.
- `useCanonicalManagedAgentProfile` resolves through a pure
`resolveCanonicalManagedAgent` helper that applies the target-provenance
rules: a deliberately requested archived pubkey stays exact (so its
archive controller can unarchive it, even when a live sibling exists),
`preserveRequestedInstance` still pins a Runtime → Instances selection,
and non-archived historical navigation keeps its canonicalization.
- The persona card's main click records a persona target that
re-resolves every render, so it self-corrects to a live sibling after
hydration. Deliberate instance navigation and the runtime-error
affordance keep their explicit-pubkey path.
- The Runtime → Instances roster (`ProfileInstancesSection`) buckets
instances off the same predicate via `bucketPersonaInstances`: live rows
render as before, and archived rows move under a labeled `Archived`
subsection. The instance count reflects both buckets, and archived rows
keep their explicit-pubkey click so unarchive stays UI-reachable (the
deliberate-navigation path above).
The predicate is fail-open (treats every identity as live while the
relay archive snapshot loads) and self-exempt, so a cold start never
hides an identity and a user is never folded from their own client.
While the snapshot is loading, every instance renders in the live list —
nothing hidden, nothing labeled.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Problem
Scheduled workflow `send_message` actions fire and land in the channel
with correct `p` tags for the mentioned agents — but the agents never
wake. The wake-up is silently dropped.
**Root cause:** workflow messages are signed by the **relay keypair**
(`workflow_sink.rs` signs with `state.relay_keypair`), so `event.pubkey`
is the relay's pubkey, not the workflow owner. In `buzz-acp`, the
inbound author gate (`author_allowed`) runs **before** the `p`-tag
mention check. Under the default `respond_to = owner-only`, the relay
pubkey is neither the owner nor a sibling, so every workflow wake-up
dies at the gate with a debug-level `"inbound author gate — dropping
event"`.
The relay-side comment even says the mention `p` tags exist *"so
mentioned agents are woken (wake is p-tag gated)"* — but wake is also
author-gated, and that path was missed.
## Fix
Gate relay-signed workflow messages on their **attributed author** — the
pubkey that created the workflow — instead of the relay pubkey:
- **Relay:** `workflow_sink.rs` now emits an explicit
`buzz:workflow-owner` tag carrying `workflow.owner_pubkey` (the workflow
creator, which the executor already passes as `author_pubkey` and whose
channel access the relay verifies before emitting). Ownership is never
inferred from `p`-tag order; mention `p` tags play no role in
attribution.
- **Harness:** at startup, `buzz-acp` fetches the relay's NIP-11 `self`
pubkey (new `RestClient::fetch_relay_self`, public `/info` endpoint).
Best-effort: fetch failure just logs a warning and preserves pre-fix
behavior.
- **Gate:** an event that is (a) authored by the relay `self` key, (b)
tagged `buzz:workflow`, and (c) carries a well-formed
`buzz:workflow-owner` pubkey is gated on that owner, through the exact
same owner/sibling/allowlist policy as a direct author.
## Security notes (all fail closed)
- No NIP-11 `self` pubkey → no exemption.
- `buzz:workflow` / `buzz:workflow-owner` tags on a non-relay-signed
event → ignored (a member cannot forge the exemption; the relay verifies
signatures on submission and only the relay holds its key).
- Relay-signed event without the tags, or with a malformed owner value
(not 64-hex) → plain author gate.
- Who is @mentioned in the message has no bearing on whose authority is
evaluated.
- A workflow owned by a random channel member still cannot wake an
owner-only agent — the owner's pubkey must pass the same policy.
## Testing
- 7 unit tests (`workflow_attributed_author_tests`) covering
attribution, fail-closed paths, p-tag independence, malformed owner
values, and the forgery case.
- Extended the PG-gated `workflow_send_message_p_tags_mentioned_member`
integration test to assert the `buzz:workflow-owner` tag.
- `cargo test -p buzz-acp`: 785 passed, 0 failed. `cargo test -p
buzz-relay --lib workflow_sink`: 17 passed. Clippy + fmt clean. (9
pre-existing `buzz-relay` failures in unrelated
`api::media`/`api::admin` tests fail identically on the base commit
without this change.)
Found while debugging scheduled automations in a Buzz review-pipeline
channel: two cron workflows fired daily @mentions at agents that never
responded, while direct human @mentions woke them instantly.
---------
Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Co-authored-by: Fizz <3a9f8a30fbb462abec1e2977b2280a7ae50c7ff794433790be15bd48bfd52d0b@buzz.block.builderlab.xyz>
The process-global session_config_cache was keyed by {pubkey, relay_url}
with no owner, scope, or generation, and put_agent_session_config gated
only on a same-pubkey record in the then-active store. A delayed
session_config_captured frame from workspace A could survive an A->B
drain, repopulate B's colliding key, and surface A's live session as
B's. Lifecycle frames already carried the missing capability
(startNonce + tracked-live-runtime check); session-config frames did not.
Move the cache onto ManagedAgentPairRuntime (session_config), making an
ownerless entry unrepresentable and destroyed atomically with the runtime
on drain/removal/exit-prune. session_config_captured now carries
startNonce; no-nonce old-harness frames are dropped with no fallback.
Admission requires the frame's exact {pubkey, relay_url, start_nonce} to
match a still-live tracked runtime whose scope_id equals the current exact
scope, validated under the runtime-map lock immediately before the sole
mutation; the store read is demoted to enrichment. get_agent_config_surface
consumes the cache only through the same still-current capability.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Why
Buzz currently appends its managed prompt to Goose's native prompt, so
managed agents receive both instruction sets instead of the intended
Buzz-only system prompt.
## What
- Send Goose's custom session system-prompt request with `mode: "set"`
- Lock the replacement contract in the ACP request test
## Risk Assessment
Low — the change is limited to Goose session setup; adapters that do not
implement Goose's custom method keep the existing method-not-found
fallback behavior.
## References
Goose v1.46.0 routes `set` to `override_system_prompt`, and its prompt
builder selects that override instead of rendering the native
`system.md`: [ACP
handler](98c11ce2ee/crates/goose/src/acp/server/manage_sessions.rs (L57-L93)),
[prompt
builder](98c11ce2ee/crates/goose/src/agents/prompt_manager.rs (L153-L191)).
Validated end to end against the official Goose v1.46.0 binary with a
local OpenAI-compatible capture server: the provider request contained
the exact Buzz replacement prompt and did not contain Goose's native
base-prompt marker.
---
**Update Aug 15, 13:17 CDT:** Added the [Terra-high prompt-ablation
comparison](4492f76349/data/prompt-ablation/20260815-terra-high/comparison.md).
The Goose conditions used GPT 5.6 Terra at high effort on the same 11
Terminal-Bench 2.1 tasks, with two attempts per task and concurrency
four. The matched `append-full` and `set-full` runs used the same
persona and included the same Buzz platform prompt; Active-h is the
primary measure because it excludes Buzz lifecycle overhead.
| Goose condition | Pass | Active-h | Median active | Agent-h | Wall-h |
Tool calls |
|---|---:|---:|---:|---:|---:|---:|
| Native prompt + Buzz prompt (`append-full`) | 21/22 | 0.3042 | 0.85
min | 0.3974 | 0.1496 | 234 |
| Native prompt + persona only (`append-persona-only`) | 22/22 | 0.3050
| 0.75 min | 0.3990 | 0.1498 | 204 |
| Buzz prompt replaces native prompt (`set-full`) | 22/22 | 0.3340 |
0.87 min | 0.4296 | 0.1551 | 275 |
Replacing instead of appending produced one additional passing attempt,
but it was not an efficiency improvement in this small sample: versus
`append-full`, `set-full` increased Active-h by 9.8%, median active by
2.0%, Agent-h by 8.1%, Wall-h by 3.7%, and tool calls by 17.5%. It was
faster on only two of eleven per-task active-time medians
(`distribution-search` and `prove-plus-comm`). With two attempts per
task, these are directional results rather than confidence intervals;
they support this change as an instruction-isolation/correctness fix,
not a performance optimization, and argue against Goose's appended
native prompt being the main source of active-time cost.
Generated with Codex
Signed-off-by: Atish Patel <atishpatel2012@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
**Category:** improvement
**User Impact:** Users can scan what each workflow does and trigger,
edit, duplicate, enable, disable, or delete it directly from the
library.
**Problem:** The workflow list buried common actions and did not expose
each automation's trigger-to-action shape at a glance.
**Solution:** Add a responsive workflow library with a persistent create
tile, compact trigger/action diagrams, prominent workflow titles with
supporting descriptions, and shared card actions while preserving
existing detail, editor, and run-history entry points. Card toggles
refresh both list and open-detail caches so status and definition stay
consistent.
<details>
<summary>File changes</summary>
**desktop/src/features/workflows/ui/WorkflowActionsMenu.tsx**
Adds a shared card menu for trigger, edit, duplicate, enable/disable,
and delete actions.
**desktop/src/features/workflows/ui/WorkflowCard.tsx**
Reworks cards around the prototype's visual hierarchy: color-coded
trigger, action flow, sentence-case eyebrow, prominent title, supporting
description, status, channel, and update date without a footer clock
icon.
**desktop/src/features/workflows/ui/WorkflowsView.tsx**
Adds the responsive grid, create tile, mutation wiring, and list/detail
cache invalidation. Container breakpoints keep cards two-across at
medium widths and three-across in the 1280px desktop layout.
**desktop/src/features/workflows/ui/workflowDefinition.ts**
Adds immutable enabled-state updates plus narrow trigger and
first-action readers used only to select card icons.
**desktop/src/features/workflows/ui/workflowDefinition.test.mjs**
Covers neutral icon selection, enabled-state immutability, and status
presentation.
**desktop/tests/e2e/workflows.spec.ts**
Covers the create tile, title/description hierarchy, selected-card
enable/disable consistency, and deterministic narrow/medium/wide
captures while retaining existing action coverage.
</details>
## Reproduction steps
1. Open **Workflows** and confirm the create tile stays first as cards
flow from one to three columns with available width.
2. Confirm each card shows a sentence-case trigger eyebrow, prominent
workflow title, supporting description when present, status, channel,
and update date without a clock icon.
3. Open a card's overflow menu and trigger, edit, duplicate,
enable/disable, or delete the workflow.
4. Leave the detail panel open while toggling and confirm its badge and
JSON definition update with the card.
## Screenshots
Real built E2E UI with representative workflow data at three viewport
sizes.
### Narrow — 800 × 720

### Medium — 1024 × 720

### Wide — 1280 × 720

### Card actions

---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
## Summary
- discover shared managed agents from authenticated relay directory
records instead of treating channel membership as sufficient proof
- publish and refresh access-policy changes immediately so running
clients converge across machines without a restart or five-minute poll
- route profile edits through the exact managed instance and
stop/restart runtimes around access changes so unrelated edits cannot
silently widen access
- keep mention send-time revalidation and Block owner-only build
enforcement fail closed
- explain invalid custom provider/model configuration instead of leaving
Save silently disabled
### Related issue
Fixes#3204
### Known residuals
- a brand-new remote agent's first policy record can wait for the
bounded directory poll when no authenticated directory coordinate exists
yet; send-time mention revalidation remains fail closed
- a failed remote-provider policy redeploy is recorded but cannot
undeploy the older provider instance until the provider protocol gains
the destructor tracked by #5570
### Testing
- full Desktop unit suite: 4,961 tests passed
- focused profile editor Playwright workflow passed, including Customize
access edits and prompt-only edits after tightening an instance
- Desktop TypeScript, Biome formatting, file-size ratchet, Tauri checks,
and pre-push suites passed
- independently reviewed for authenticated directory trust, live
subscription teardown, runtime revocation ordering, fail-open edit
paths, and per-agent provider deployment serialization
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
Co-authored-by: diegorumo <diegorumo@gmail.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>