Skip to content

Sync upstream block/buzz main (97e1f6a0) into the Kiingo fork — DO NOT MERGE until main's go - #202

Merged
RossHartmann merged 410 commits into
mainfrom
sync/upstream-60de1db0
Oct 18, 2026
Merged

RossHartmann merged 410 commits into
mainfrom
sync/upstream-60de1db0

Conversation

@RossHartmann

@RossHartmann RossHartmann commented Oct 16, 2026 •

Copy link
Copy Markdown

Caution

Do not merge. This is a planned merge: Ross approved the sync to land only after phone push is verified end to end on the current fork, and main will give the go. Draft until then. Cutover follows the separate runbook (summary at the end).

Summary

This PR merges upstream block/buzz main into Kiingo/buzz, as real merges with no rebase and no force-push.

Commit
Fork base b743da9a (#196), then main merged in again twice: 599d88ec (#198) and efc7fb7d (#197, #200)
Upstream merged 60de1db0 (main sync, 394 commits), then 97e1f6a0 (2 follow-up commits; neither adds a migration)
Merge base eed74bde
Inventory upstreamSnapshot 97e1f6a0828d97d0031e2a31402554b679bf1146, the live upstream tip at the final fetch

The branch name keeps the first pinned SHA (60de1db0).

Commits, in first-parent order:

  1. a38968f6 merges upstream 60de1db0, with every conflict resolved and every Kiingo delta re-ported. Push-gateway files take upstream's version in this commit.
  2. 0333f34c re-ports APNs .p8 token auth onto upstream's single-application gateway. This commit is for C's review.
  3. c886e4ba merges upstream 97e1f6a0. fix(db): validate channel roster snapshots in the app writer block/buzz#8316's roster guard runs in the single fenced insert path.
  4. d371fab8 merges fork main feat(mobile): create and manage hosted agents from the phone #198 (mobile hosted agents), re-ported onto upstream's new settings page and Tabler icons.
  5. 008f86c4 re-baselines the inventory: new snapshot, migration ledger, reclassification, and an exact budget amendment.
  6. f37f21eb merges fork main feat(mobile): manage agent guest access from Settings #197 and chore(ios): build TestFlight with hosted-agent creation enabled #200. The guest-access Settings section is re-ported onto upstream's settings page, and its Lucide icons are moved to BuzzIcons/GuestAccessIcons. Budgets are unchanged.

Every commit is signed off (DCO) and carries Co-Authored-By. Upstream history and authorship are preserved.

Migrations (decided: renumber upstream, never the fork)

  • Upstream 0041–0065 are renamed byte for byte to 0046–0070 with git mv. sqlx's SHA-384 checksums are unchanged.

  • The fork's production-applied 0041–0045 keep their numbers and checksums.

  • Production validates 1–45 and applies 46–70. Fresh databases apply the same order.

  • The per-file map lives in docs/kiingo-fork-inventory.json under migrationLedger, with lastProductionVersion: 45.

  • New static guard: scripts/check-kiingo-migration-ledger.mjs runs from check-kiingo-fork-boundary.mjs in the existing "Kiingo fork replay, footprint and migration-ledger boundary" CI step. It needs no database and no tests. It fails on any of:

    • a duplicate version prefix;
    • an unversioned or unmapped file;
    • a mapped file that is not byte-identical to upstreamSnapshot:<upstreamPath>;
    • an unmapped upstream migration;
    • recorded SHA-384 drift (fork-owned production versions are frozen);
    • a gap or ordering change;
    • an include_str! of a missing migration.

    Negative cases were verified locally: a duplicate 0041 and a one-byte edit to 0050 both fail.

  • Upstream test literals naming upstream versions are shifted by +5. That covers migration.rs (65 to 70, migrations[N], run_to, version ==), follow_up_retirement_tests.rs, thread_window_postgres_tests.rs, runtime/tests.rs, deletion.rs, and two include_str! paths. docs/thread-window-deployment.md and docs/push-gateway-deployment.md use the fork numbers.

  • The policy is documented in docs/kiingo-fork-maintenance.md (Migration ledger): next free number, and the ledger is the source of truth.

Migration mapping (upstream name → fork name, SHA-384 prefix)
Version Fork file Upstream file SHA-384 (prefix)
41 0041_managed_publication_fences.sql fork-owned, applied in prod df79c95f3ad3b3c1
42 0042_agent_status_thread_counters.sql fork-owned, applied in prod 0a3b105e82542f89
43 0043_user_stop_events.sql fork-owned, applied in prod 417047e6d862c0df
44 0044_channel_event_sequence.sql fork-owned, applied in prod 8ab6ad125a952895
45 0045_push_guest_notification_kinds.sql fork-owned, applied in prod 3ddca2a5c7cabd42
46 0046_nip_fi_identity_foundation.sql 0041_nip_fi_identity_foundation.sql ad6359d4b86fc85d
47 0047_nip_fi_authorization_foundation.sql 0042_nip_fi_authorization_foundation.sql 702326df04517e53
48 0048_push_gateway_dogfood_profile.sql 0043_push_gateway_dogfood_profile.sql ed55cbda5e3de7e5
49 0049_drop_nip_fi_ledger.sql 0044_drop_nip_fi_ledger.sql a852862896862d0c
50 0050_retain_push_revocation_tombstones.sql 0045_retain_push_revocation_tombstones.sql 7c8c13d6e6e7dec2
51 0051_storage_accounting_snapshots.sql 0046_storage_accounting_snapshots.sql e02ae30344987441
52 0052_relay_admin_action_target.sql 0047_relay_admin_action_target.sql 1e676435f2013364
53 0053_audit_hash_version.sql 0048_audit_hash_version.sql 2b040688d14216cd
54 0054_thread_window_index.sql 0049_thread_window_index.sql 35b707ad2a8ad28b
55 0055_operator_listener_mentions.sql 0050_operator_listener_mentions.sql 08c1b91010b969db
56 0056_owner_community_deletion_admission.sql 0051_owner_community_deletion_admission.sql fe6d3068a6512587
57 0057_channel_artifacts.sql 0052_channel_artifacts.sql dcf66d35143fbd83
58 0058_owner_deletion_auto_approval.sql 0053_owner_deletion_auto_approval.sql 9a1914fcb2df1622
59 0059_owner_deletion_quota_reservation.sql 0054_owner_deletion_quota_reservation.sql ef0f783e356c61a9
60 0060_relay_admin_direct_actions.sql 0055_relay_admin_direct_actions.sql 4a6dff9da91abd69
61 0061_personal_read_state.sql 0056_personal_read_state.sql f9b6824b395e1a12
62 0062_personal_read_started_at.sql 0057_personal_read_started_at.sql 9d5038ffc8e9cbc2
63 0063_personal_read_through_message.sql 0058_personal_read_through_message.sql 32b6817d8e773102
64 0064_push_rollback_gate.sql 0059_push_rollback_gate.sql ce3afd86480ded9a
65 0065_push_single_application.sql 0060_push_single_application.sql 10149f8eeda12893
66 0066_channel_labels.sql 0061_channel_labels.sql d584af4ed17070c2
67 0067_system_channel_type.sql 0062_system_channel_type.sql f154368d05b20959
68 0068_retire_push_match_trigger.sql 0063_retire_push_match_trigger.sql a4156e039f95b5bc
69 0069_profile_name_fields.sql 0064_profile_name_fields.sql 5a618efc012b8fca
70 0070_retire_channel_ttl_trigger.sql 0065_retire_channel_ttl_trigger.sql 2440f69b41932348

Conflict-resolution log

The rehearsal found 74 content conflicts; #196 added 4 more mobile ones. All 78 are resolved. The policy was to keep upstream behaviour unless an inventory delta requires otherwise. Re-ports land in fork-added files behind small hooks where possible.

Relay / DB / pubsub (deltas 1, 2, 5, 11, 12, 13, 20)

  • handlers/event.rs (D13, privacy). Upstream fix(relay): gate owner-only kinds in shared fan-out access filter block/buzz#8006 moved owner-only live fan-out into filter_fanout_by_access, hard-coded to two kinds. Taking upstream's location as-is would have let 46040–46042 reach non-owners. The predicate there is now RESULT_GATED_KINDS.contains(&kind) (event.rs:183). That one check covers local delivery, Redis cross-pod delivery and kindless ids subscriptions. Hunk 2 keeps upstream's NIP-43 leave exception, with the fork's invocation-route scope check inside it.

  • handlers/req.rs. Upstream imports and upstream's system-channel exclusion (feat(relay): leave system channels out of reads that do not name them block/buzz#8181/feat(relay): let channel-state filters opt in to system channels block/buzz#8182). The fork's RESULT_GATED_KINDS p-gated read check is kept (req.rs:1663).

  • Push (D13). Migrations 0064 and 0068 (upstream 0059 and 0063) retire the trigger that carried 46040/46042.

    • event_follow_up::PUSH_MATCH_KINDS is now [9, 40002, 45001, 45003, 46040, 46042]. This file had no conflict.
    • From the code: global events with no h tag still go through record_push_event, and the matcher skips the membership check when there is no channel.
    • push_lease.rs keeps the fork's 6-kind PUSH_KINDS. The test that pinned the 0045 trigger is replaced by push_match_kinds_match_advertised_push_kinds, a set-equality check.
    • push.rs is upstream's version. fix(push): stop backfilling messages on lease activation block/buzz#8178 removed the activation backfill.
    • NIP-11 push_kinds still advertises 46040/46042 under buzz-push-v1.
  • store/event.rs (D11, publication fence). Upstream's AdmittedTx structure is taken (refactor(db): type-level admitted event-write transactions (AdmittedTx) block/buzz#8130/BUZZ-175: Route serving event writes through the tenant lock block/buzz#7828/buzz-db: admit every fenced-table writer in app code block/buzz#8219). The re-introduced insert_event_on is removed again, and insert_event_in_transaction delegates to insert_event_with_thread_metadata_tx. Every insert path, including the canvas head (insert_canvas_head_checked), therefore passes managed_publication::apply_event_tx; the fence cannot be bypassed.

  • deletion.rs. EXPECTED_SCOPED_TABLES and PURGE_SCOPED_TABLES take the union of fork and upstream tables. Upstream's purge loop still iterates them.

  • dm.rs (D12). seal_dm_if_opener_inactive now runs inside begin_community_write_transaction. No conflict marker.

  • api/bridge.rs (D1/D2/D11).

  • ingest.rs. Upstream's target_readable_in_channel, then the fork's "operational status is immutable" rejection. The fork's drift-check placement is kept.

  • router.rs / state.rs / main.rs / pubsub (D20).

    • Upstream's process-local readiness (Make relay readiness process-local block/buzz#7341) is kept. transport_continuity moves to /_status and is never in readiness.
    • The new restriction_cache and serving_active_cache are added to requalification and to the continuity bypass. The new observer_owner_cache readers are gated.
    • Upstream's raw Redis paths now go through the fork's ordered transport/writer: NIP-FI disconnect publisher/subscriber, set_presence_many/clear_presence_many, and the NIP-FI replay SET NX.
    • The deletion transport erasure survived on upstream's drain.
  • buzz-admin storage-snapshot (D5). It uses MediaStorage::from_runtime_env, so Azure metrics keep working.

  • schema/. The fork schema delta is re-ported into upstream's per-object layout (refactor(schema): fan desired-state schema out to one file per table block/buzz#8131):

    • managed_* tables, user_stop_events, channel_event_heads;
    • the events.channel_sequence column, CHECK and index;
    • functions and the two events triggers;
    • per-table fence attachments.

    The fork's NEW.kind IN (… 46040, 46042) desired-schema assertion is replaced by one that 0045 keeps its history and 0068 drops the trigger. The reconcile script's partition trigger drops merged cleanly.

ACP / CLI / SDK (deltas 2, 3, 12, 14)

  • Trust lane. It is now part of upstream's per-thread key: a new SessionScope::Guest { channel_id, requester, root_event_id }, applied by SessionScope::in_lane right after scope derivation. Owner and guest lanes, and any two guests, never share a provider session, queue partition, in-flight slot, steer target or typing entry. The owner lane is unchanged upstream behaviour.

  • Per-requester conversations. These are a refinement inside one scope (queue/conversation.rs): take_conversation, flush_cancelled_only, joins_in_flight_turn before upstream's steer/interrupt decision.

  • lib.rs re-port onto InboundAuthorGate. The trust tier comes from listener_trust::admit. Also re-ported:

    • DM allowlist;
    • recipient-only invocations and the self-event filter;
    • verified user cancel;
    • DM addressing;
    • publication recovery;
    • failure notices as agent-status events;
    • the reaction state machine.

    pool.rs keeps upstream context targeting and thread hydration, plus fork invocation-only prompts, the guest context filter, [your owner] marking and prompt authors.

  • base_prompt.md. The fork's "Loyalty and Confidentiality" section sits above upstream's "Incoming Turn Contract".

  • CLI messages.rs. Upstream NIP-30 emoji resolution plus the fork's provenance/handoff tags, through the new buzz_sdk::build_message_with_emoji_and_extra_tags. build_message_with_extra_tags keeps its signature.

  • First t tag. Fork DM detection (dm_mentions.rs, dms.rs) uses the first t tag, matching feat(relay): channel labels block/buzz#8175.

  • Upstream-compat wrappers. session_cancel (test-only) and cancel_with_cleanup_grace delegate to the fork's *_with_origin variants.

Desktop (deltas 6, 8, 10, 12, 14, 15, 21)

Mobile (deltas 11, 16, 23, 24, 25)

CI, hooks, misc (deltas 0, 7, 9, 19)

  • ci.yml. Upstream split CI into _ci-{rust,desktop,desktop-macos,relay,clients,security}.yml (ci: split CI into reusable workflows block/buzz#7168). The Kiingo Spot-only, test-free policy is re-applied in ci.yml, _ci-rust.yml, _ci-desktop.yml, _ci-clients.yml and _ci-security.yml. _ci-relay.yml and _ci-desktop-macos.yml are left untouched; their domains are skipped on Kiingo/buzz. The re-applied policy:
    • Spot runs-on, plus Windows Spot with the custody step;
    • contract and test steps skipped;
    • test matrix lanes not scheduled;
    • mold/Tauri dependency qualification;
    • the frozen-snapshot file-size base;
    • the boundary and ledger step;
    • test-only gates skipped.
  • minio-image.yml. This new upstream workflow runs on a hosted runner and smoke-tests. It is skipped on Kiingo/buzz.
  • docker.yml. Fork runs-on plus upstream's needs: [push-gateway-build, qualify].
  • lefthook.yml. The fork's test-free static hooks, wrapped in upstream's without-git-env.sh, with upstream's extended globs.
  • Justfile. Upstream's create-if-missing sidecar stub with the fork's ${EXE}.
  • docker-compose.yml. Upstream Silo (Use Silo for bundled object storage block/buzz#8023); D0 is dropped as upstream-present.
  • Cargo.lock. Upstream versions win for shared packages: ed25519-dalek 3.0.0, iroh 1.0.3. Fork-only crates are kept.
  • ed25519-dalek pin. The fork's pins in buzz-core, buzz-acp and buzz-db go from =3.0.0-rc.0 to =3.0.0, matching upstream chore(mesh): upgrade MeshLLM to 0.76.2 block/buzz#7559 (main's decision). The rc.0 → 3.0.0 API is verified only by cargo check/clippy --all-targets over the fork's signing, verifying and key-parsing call sites.

Needs C review (push gateway, chart, docs)

These are best-effort resolutions. The tree builds, but C should finalize them. They are mostly in commit 0333f34c, which can be reviewed on its own. C's #199 (Xcode 27 upstream picks b270437a, 46159fcf, 73962435) becomes upstream-present after this sync; once main merges it, main gets merged into this branch again.

  1. crates/buzz-push-gateway/src/config.rs. Upstream's single ApplicationConfig, with apns_auth: ApnsAuthConfig replacing apns_cert_path. parse_application calls apns_token::parse_auth. The fork's BUZZ_PUSH_PUBLIC_DELIVERY_URL host relaxation is dropped, because feat(push): support configurable HTTP(S) delivery URLs block/buzz#7877 moved the delivery URL to the relay (BUZZ_PUSH_GATEWAY_DELIVERY_URL) and the gateway no longer reads it. The fork's delivery-URL tests went with it.
  2. crates/buzz-push-gateway/src/main.rs. The certificate/token transport match is re-applied on upstream's application runtime, next to AppAttestVerifier::with_environment.
  3. crates/buzz-push-gateway/src/apns_token.rs (no conflict). The env vars are renamed from BUZZ_PUSH_DOGFOOD_APNS_{AUTH,CERT_PATH,KEY_PATH,KEY_ID,TEAM_ID} to BUZZ_PUSH_APNS_*.
  4. deploy/charts/buzz-push-gateway/templates/deployment.yaml. The token branch is now under application.apnsAuth/application.apnsToken: env vars, plus the auth-key.p8 mount on upstream's apns volume. The fork's int64 render fix is dropped as a duplicate of fix(mobile-infra): render push grant lifetimes as decimal in chart 0.3.2 block/buzz#7820.
  5. values.yaml / values.schema.json. application.apnsAuth: certificate by default. apnsToken has keyId/teamId matching ^([A-Z0-9]{10})?$. The schema uses if/then/else, so certificate requires apnsCert and token requires apnsToken. publicDeliveryUrl is gone, as upstream removed it.
  6. deploy/charts/buzz-push-gateway/tests/render.sh. Upstream's script, plus the fork's token assertions under the new names. Not run: there is no helm here, and it is a test.
  7. docs/push-gateway-deployment.md. Upstream's env table with the token rows, and the token section with application.* chart keys. Relay migration numbers are shifted to the fork numbering.
  8. Relay push side (C should also sign off):
    • handlers/push_lease.rs PUSH_KINDS and the new parity test;
    • store/event_follow_up.rs PUSH_MATCH_KINDS;
    • store/push.rs, now upstream's version;
    • the D13 fan-out fix in handlers/event.rs;
    • the mobile dev_push_lease_test.dart union.
  9. Kiingo/kiingo follow-up. This needs C or main; I did not change Kiingo/kiingo. infrastructure/buzz/kubernetes/push-gateway-values.yaml still uses profiles.dogfood.* and publicDeliveryUrl. The upstream chart ignores those and falls back to the default TEAMID.com.example.buzz app id, so every enrollment would fail attestation. Move them to application.* before the cutover deploy. The relay needs BUZZ_PUSH_GATEWAY_DELIVERY_URL when push is enabled.

Risk summary

This is adapted from the read-only risk analysis, with an addendum for this branch.

Per migration (46–70 apply in production; 41–45 already applied)

New (upstream) Change Reversible Online-safe on production (~125 MB DB)
0046–0047 (0041–0042) NIP-FI identity and authorization ledger tables and guards dropped by 0049 in the same run yes (new empty tables)
0048 (0043) deletes relay-DB gateway rows with app_profile <> 'buzz-ios-dogfood', then narrows the CHECK no (deletes rows); expected 0 rows yes; pre-check the count
0049 (0044) drops the NIP-FI ledger and restores the 0030 fence-exclusion body destructive, but the tables are empty yes
0050 (0045) partial unique indexes on push_gateway_installations yes yes (small table)
0051 (0046) storage accounting snapshot table yes yes
0052 (0047) 2 nullable columns on relay_admin_actions yes short AEL on a small table
0053 (0048) audit_log.hash_version NOT NULL DEFAULT 1 CHECK yes AEL plus a full audit_log scan with no lock_timeout; seconds at this size
0054 (0049) idx_thread_metadata_window, with lock_timeout 1s and statement_timeout 5s yes only if prebuilt CONCURRENTLY (runbook)
0055 (0050) operator-listener outbox tables; exclusion function extended yes yes
0056, 0058, 0059 (0051, 0053, 0054) owner deletion admission, auto-approval, quota index yes lock_timeout 5s; tiny tables
0057 (0052) artifact tables (fenced) yes yes
0060 (0055) direct relay-admin actions partly small scan
0061–0063 (0056–0058) personal read-state tables; drops a column it added itself yes yes
0064 (0059) push rollback gate; resets the trigger allowlist to 4 kinds (drops 46040/46042 from SQL) forward only yes; app-path PUSH_MATCH_KINDS carries them
0065 (0060) single push application; refuses if any active push_leases row or relay-DB gateway installation exists; drops app_profile no AEL on push_leases (small); retire leases first
0066 (0061) channels.labels with CHECK yes AEL plus a channels scan with no lock_timeout; sub-second
0067 (0062) channel_type gains system no; old images cannot decode it catalog only; image rollback unsupported after this
0068 (0063) drops the push-match trigger and function forward only AEL on events and every partition, lock_timeout 5s; catalog-only hold
0069 (0064) profile_name_fields() yes yes
0070 (0065) drops the channel-TTL trigger and function forward only same AEL as 0068

Production runs migrations as a separate step. Kiingo prod has migrate.autoMigrate: false. deploy.sh runs the buzz-database-migrate Job (buzz-admin migrate, new image) before the gateway and relay rollouts. Use deploy-production, never deploy-relay, which runs no migrations.

Relay behaviour changes for existing clients

These assume NIP-FI stays off and BUZZ_V1_ENABLED/BUZZ_OPERATOR_LISTENERS stay unset.

  • Wire. Refusals are correlated: OK false for EVENT, CLOSED for REQ/COUNT. Ephemeral kinds no longer count toward the message quota. Query deadlines are terminal CLOSED. Tag pushdown matches by position. #h membership is checked in one query, with the cap raised to 2048. All three clients already handle CLOSED.
  • Targets the sender can't read look missing (fix(relay): reject unreadable write targets as missing block/buzz#8249). Reacting to or reporting an event you can't read now fails with invalid:. Deletion denial text changed; no client parses it.
  • Channel tags. Channels carry t type first, then labels and P creator. Desktop and mobile read the first t. Labels and system channels are new. System channels are excluded from every read that doesn't name them, and 39002 #p:[me] doesn't list them. Identity rotation would therefore skip system-channel memberships once those exist.
  • Readiness. /_readiness is process-local and the dependency report is on /_status. The fork's transport continuity moved there. Azure probes lose the shared-dependency signal, by design (Make relay readiness process-local block/buzz#7341).
  • Bans. Bans cascade from an owner to its agents and close live sockets. Restriction and serving caches have a 30 s / 10 s TTL and are bypassed when transport continuity is lost.
  • Push. NIP-11 advertises buzz-push-v1 (no nip-pl, no app_profiles) and always emits artifacts. The current TestFlight build stops registering for push against this relay. Chat is unaffected.

Desktop compatibility

0.5.22-kiingo.x stays compatible, and no desktop release is needed for the cutover. It does degrade in three places:

  • it still caps the client at 5 communities;
  • reacting to an event the user can't read now fails visibly;
  • identity rotation skips system-channel memberships (none exist today).

Rebuild the harness and hosted images soon after the cutover. The old buzz-acp and CLI read the last t tag, which misreads labeled channels.

NIP-PL / push / auth / membership

  • Push changes protocol. It moves from NIP-PL to buzz-push-v1 (refactor(push): one app profile per gateway block/buzz#8206). Every enrollment made before the sync is discarded, and the gateway needs a fresh database: its bootstrap guard refuses a populated pre-v5 DB. Before 0065 can run, push must be disabled on the relay and every active push_leases row retired.
  • 46040/46042. Owner-only for live delivery, Redis and reads (RESULT_GATED_KINDS), and push-eligible through PUSH_MATCH_KINDS. They are untested at runtime and belong on Spot/E2E.
  • Auth. NIP-FI is off. A NIP-98 payload tag, when present, must be 64 lowercase hex characters; all clients already comply. NIP-OA time bounds use the signed timestamp.
  • Membership. Removing a member disconnects their live sockets. 46040–46042 read gating is unchanged.

Decisions / open questions (conservative choices, keeping Kiingo behaviour)

  1. Migration renumbering follows main's decision (renumber upstream). The future rule is next-free-number, documented in the maintenance doc.

  2. Budget amendment upstream-sync-97e1f6a0.

    • Modified upstream files: 262 to 277.
    • Production-source files: 153 to 156.
    • Changed lines: 10588 to 9577, ratcheted down.
    • Hunks: 1167 to 1163, ratcheted down.

    The business-logic budget stays zero. This needs Ross's explicit policy approval with the PR; the maintenance doc requires it.

  3. Transport continuity is reported in /_status, not readiness. Putting a shared Redis dependency back into readiness would contradict Make relay readiness process-local block/buzz#7341.

  4. Owner !cancel also stops guest lanes in the same thread or conversation, keeping the fork rule that owner authority wins. A member's !cancel only stops turns triggered by that member's own events.

  5. ACP terminal failures, including model-not-found, are reported as agent-status events (40098), not chat. This replaces upstream's chat notice, and upstream's five notice tests were rewritten.

  6. D8 onboarding. Only the surviving identity routes to continueWithExistingIdentity; any other selected key follows upstream's flow. The desktop owner should confirm. The F8 onboarding e2e coverage needs a new spec.

  7. Upstream Postgres tests that expect cache hits for the restriction and serving caches (ephemeral_gates_*, relay_admin restriction cache) will fail on Spot while transport continuity is false in fixtures. This is the same class of failure as the fork's existing membership-cache bypass. They need a buzz-pubsub test hook; they are test-only.

  8. Upstream thread windows (39007) don't include agent status (40098). Left as upstream.

  9. buzz-deletion's drain still builds S3-only MediaStorage::new, as before. The storage-accounting cronjob in the Kiingo overlay needs the Azure storage env vars and workload identity.

  10. Branch name keeps the first pinned upstream SHA (60de1db0). The merged and snapshotted upstream is 97e1f6a0.

Checks (static only; no tests were executed on the Mac)

All heavy commands were serialized one at a time, with a load gate.

Check Result
cargo fmt --all -- --check; desktop cargo fmt -- --check pass
cargo check --workspace --all-targets; cargo clippy --workspace --all-targets -- -D warnings pass
cargo check / clippy -D warnings --all-targets (--manifest-path desktop/src-tauri/Cargo.toml) pass
desktop pnpm install --frozen-lockfile, tsc --noEmit, biome check . pass (3 pre-existing biome warnings)
mobile flutter pub get, dart format --set-exit-if-changed ., flutter analyze pass, no issues
swift build --build-tests for BuzzPushKit (compile only) pass
actionlint on ci.yml, _ci-*.yml, docker.yml, minio-image.yml pass
node scripts/check-kiingo-fork-boundary.mjs, including the migration ledger pass: 604 divergent paths, 277 / 156 / 9577 / 1163, contamination 0
CHECK_FILE_SIZES_BASE=$(git rev-parse origin/main) node {desktop,web,mobile}/scripts/check-file-sizes.mjs pass

Not run, by policy: every test suite, including render.sh, Postgres-backed migration tests and E2E. These run on Spot after deployment. Specifically recommended:

  • a fresh-DB migrate of 1–70;
  • a production-shaped migrate of 1–45 to 70;
  • e2e_guest_routing;
  • the D13 fan-out and push tests.

CI status on Spot

The Kiingo public runner admission (gha-runner-controller public-buzz.cjs) refuses any job whose workflow file differs from main's (public_workflow_not_current). This PR changes these workflows:

  • ci.yml, plus the _ci-*.yml files, which come from upstream;
  • docker.yml, push-gateway-helm-chart.yml, sprig.yml, codex-security-review.yml, mesh-lifecycle.yml and minio-image.yml.

Their Spot jobs fail admission by design. Examples: Docker image / Build (linux/amd64) and push gateway helm chart / validate. That is not a code failure. Workflows that are unchanged against main do run: helm chart, CodeQL, Desktop Release Candidate, and the personal-dev gateway build all passed or are queued.

Full CI signal on this tree will only exist once the workflow changes are on main, or once main or Ross runs an admitted workflow at this revision. The static checks listed above were run locally instead.

Cutover

~/Projects/buzz-upstream-sync-20261016-reports/cutover-runbook.md is local to the operator. In order:

  1. Read-only preflight.
  2. On-demand backup, PITR time T0 and pg_dump on psql-buzz-prod-wjymjvi3lf6gy.
  3. Push disabled; active leases retired.
  4. 0054 index prebuilt CONCURRENTLY.
  5. Fresh buzz_push_gateway DB.
  6. Kiingo/kiingo gateway values renamed.
  7. deploy-production: migrate Job, then gateway, then relay rolling 2 pods one at a time.
  8. Push re-enabled.
  9. New TestFlight build from the merged tree released; phones re-enroll.
  10. Verification matrix.
  11. Forward-fix, or PITR restore to a new server.

🤖 Generated with Claude Code

kalvinnchau and others added 30 commits September 23, 2026 12:01
## Summary

Add an opt-in, newest-first thread window to the authenticated HTTP
query path.

A filter with `thread_window: true` returns:

- reply rows ordered by `(created_at DESC, id ASC)` with an exact
composite cursor;
- optional bounded reactions, edits, deletions, and delete-of-aux
closure;
- one relay-signed `kind:39007` bounds event bound to the normalized
host, authenticated reader, channel, root, request cursor, kinds, depth,
limit, and aux choice.

Filters without the flag keep the existing oldest-first thread behavior.
Channel windows and `kind:39006` are unchanged. No client opts into this
mode in this PR.

### Why extend NIP-CW

NIP-CW already defines relay-computed, cursor-paged conversation views
over extended NIP-01 filters. Thread windows use the same protocol
ideas: ordinary signed rows, composite keyset pagination, optional aux
closure, and relay-signed bounds. Adding a thread mode keeps those
related wire contracts together instead of creating a second NIP with
duplicated terminology and compatibility rules.

Extending a draft, optional NIP with an additive mode is normal protocol
evolution. Existing fields and meanings are not reinterpreted:

- `top_level: true` remains channel mode;
- `kind:39006` remains channel-window bounds;
- `thread_window: true` selects the new mode;
- `kind:39007` has a distinct identity and root/request binding;
- extension-unaware clients never send the selector;
- legacy thread queries remain unchanged.

NIP-CW documents only the wire contract and relay semantics. Database
plans, replica internals, and rollout procedures remain implementation
concerns.

### Why thread windows use independent modules

The implementation deliberately keeps `thread_window.rs` separate in
`buzz-core`, `buzz-db`, and `buzz-relay` rather than adding branches
throughout the existing channel-window path.

The modes share pagination concepts, but their invariants differ:

- channel mode selects top-level rows; thread mode requires one root and
depths `1..=depth_limit`;
- thread mode accepts only conversation row kinds and rejects
unknown/conflicting fields;
- thread bounds bind the root and authenticated reader because
cross-channel aux visibility can differ by reader;
- thread aux includes the root and requires complete two-hop closure;
- thread mode refreshes writer authorization before selection and before
signing;
- historical cursor pages may use a proved replica transaction, but
fallback must restart both aux hops on the writer;
- legacy thread traversal is oldest-first and must not inherit
descending cursor semantics.

Forcing these rules into channel-window code would create a mode flag
across parsing, SQL predicates, bounds construction, authorization,
replica fallback, and tests. That abstraction would not remove
complexity. It would distribute it. Separate modules keep each contract
local and leave the established channel and legacy paths untouched.

### Technical design

- Strict request parsing normalizes UUIDs, event IDs, kinds, defaults,
and cursors before computing the response binding.
- SQL applies community, root, channel, depth, kind, deletion, and
cursor predicates before `limit + 1`.
- Ordered metadata scanning plus exact event-key lookup avoids the
stale-statistics plan that previously exceeded the four-second SQL
budget on a 10k-reply thread.
- Aux work is bounded by target batches, query count, raw candidate
count, response bytes, SQL timeouts, and one shared HTTP deadline.
- Fresh authorization-set comparison catches both grants and revocations
during closure. The relay returns retryable `503` rather than signing an
incomplete page.
- Replica failure permanently degrades the request to the writer and
restarts closure from its original targets without resetting budgets.
- Migration `0049_thread_window_index.sql` adds the mixed-direction
index used by newest-first scans. It validates a prebuilt index and
bounds startup lock/build time.

### Tradeoffs

- The stale-statistics-safe plan is intentionally less clever. Measured
warm 100k deep-cursor execution increased from roughly 0.30 ms to 4.80
ms, plus about 8 ms planning, in exchange for avoiding a reproduced
multi-second timeout after rapid 10k-thread insertion.
- Bounds are authoritative only for the served page. There is no
cross-page snapshot guarantee, and replica insertion coverage does not
imply latest edit/deletion/aux visibility.
- Strict parsing rejects unsupported constraints instead of silently
serving a different row set.
- Required aux closure fails closed. The relay does not omit aux and
sign plausible-but-incomplete bounds.
- This PR ships relay capability only. Client rendering, retry UX,
live-history overlap, and compatibility fallback remain separate work.

### Related issue

No existing issue found. This work follows the thread-loading design and
review discussion.

### Testing

- `cargo test -p buzz-core`: 258 unit tests and 2 doctests passed.
- `cargo check --workspace --all-targets`: passed at the rebased
implementation head.
- `cargo test -p buzz-db`: 131 non-PostgreSQL tests passed; 275
infrastructure tests were ignored as declared.
- Focused PostgreSQL migration/runtime regressions: 3/3 passed against
an isolated PostgreSQL container, including both corrected
migration-ledger tests.
- `cargo fmt --all --check`: passed.
- Pre-rebase feature validation passed 398/398 PostgreSQL tests,
signed-wire multi-tenant/auth/pagination/aux/revocation probes, clean
Compose boot/reboot, and full core tests.
- Independent HA validation exercised three relay pods and a physical
PostgreSQL standby on the same runtime logic before the final
documentation-only cleanup.
- Independent final-delta review confirmed exactly two changed files,
byte-identical executable migration SQL after comments/diagnostic
normalization, matching prebuild/catalog/recovery instructions, and `git
diff --check`; no runtime receipts were relabeled to the new SHA.
- The pre-push hook ran repository checks but its Rust unit lane exited
100 without exposing an individual failed test in the captured
multiplexed output. The independently run affected-package checks above
passed; current-head repository CI remains required.

Known unrelated baseline behavior remains: the mesh-demo echo test can
return 504, and same-second archive → unarchive can deduplicate its
membership notification. Neither path is changed here.

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: cid <d9f92a72922bf45c17379a47d64dae84b6020397c2d5a52b5317d512068cd9d3@buzz.block.builderlab.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: cid <d9f92a72922bf45c17379a47d64dae84b6020397c2d5a52b5317d512068cd9d3@buzz.block.builderlab.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Fix Databricks V2 Unity Catalog Claude service routing without erasing
verified model capabilities.

- preserve exact provider-qualified record precedence for
`data_workflow_tools.goose.goose-claude-opus-5-5`, including adaptive
thinking, explicit `output_config.effort` (default medium), and signed
native tool continuation
- route other boundary-matched `claude-*` service components to
Anthropic Messages while preserving the full FQN and advertising no
unverified effort controls
- keep GPT-5+ service components on OpenAI Responses and other uncurated
FQNs on MLflow Chat Completions; catalog/schema text does not affect
routing
- keep Rust and TypeScript resolver precedence aligned and regenerate
the 168-vector normative corpus

Validation at `1204f1eacff17ebe05eb2bba06b5eee210e21c25`:
- `cargo test -p buzz-agent`
- `pnpm -C desktop test` — 6,628 passed
- `pnpm -C desktop typecheck`
- `cargo fmt --all -- --check`
- `cargo clippy -p buzz-agent --all-targets -- -D warnings`
- `just test-unit` — passed on isolated rerun
- pre-push file-size, desktop check, desktop typecheck, desktop tests,
and Tauri checks passed; the repository-wide Rust lane exited without
identifying a failing test in two concurrent hook runs, while its
isolated `just test-unit` rerun passed

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
…ammar (block#7844)

New Databricks endpoints (Claude Opus 5.5, GPT-6 Astra/Sol, etc.) showed
as raw FQNs in the model picker until someone added a curated corpus
record for each. This adds a strict token-class grammar as a third label
tier inside the Databricks label helpers (Rust `model_capabilities.rs` +
TS `modelCapabilities.ts`): exact record → unique alias → **generated
label** → raw id. In TS it fires only for an explicit `databricks_v2`
provider; providerless ids stay raw.

## Grammar

Takes the last component of a UC FQN (or strips one wrapper:
`databricks-`, `goose-`, `kgoose-`, `builderbot-`; an id that is neither
stays raw), splits on `-`, and classifies each token as version,
letter+digit version, size, trailing date (dropped), or word. Labels are
built only from the id's own text, and the grammar never reinterprets a
version. When in doubt it returns raw:

- **Known families only.** The family (leading letters of the first
token) must be in the manifest's existing `label_family_tokens`. Custom
endpoints such as `prod.ai.rag-agent-3` or `builderbot-pr-reviews-v2`
stay raw. Supporting a new vendor means adding one entry to that list,
not a record per model.
- **Attached digits stay whole** (`gpt10` → `GPT10`). If the family
already carries digits and the next token is a version
(`goose-qwen3-5-122b-a10b`, `system.ai.llama3-1-8b`,
`system.ai.claude3-5-sonnet`), the id stays raw; this is checked before
any reorder.
- A third consecutive number (`gemini-3-7-1-flash`) returns raw rather
than splitting off a patch version.
- Any non-ASCII input returns raw, checked before trimming or case
folding.
- Any unclassifiable token, or no version-bearing token
(`builderbot-pr-reviews`), returns raw. `(latest)`/`Preview` never
appear unless an exact record supplies them.

Special cases, each justified by an unseen-id shape rather than the
corpus score:

- `meta-llama-` → `Llama`: a namespace convention.
- Claude numeric-first reorder: Goose names Claude endpoints
`claude-4-7-opus`. Only 1–2 version tokens followed by
`opus`/`sonnet`/`haiku` reorder (`claude-5-6-opus` → `Claude Opus 5.6`);
anything else keeps its order (`claude-5-6-thinking` → `Claude 5.6
Thinking`). Both current numeric-first ids also have exact records.
- Qwen compact decimal: Qwen's own naming `qwen35` → `Qwen3.5` when the
second digit is nonzero.

## Colliding labels in the picker

The same model is often served from several sources, so humanized labels
collide (four rows are all `GPT-5.5`). `disambiguateModelLabels` in
`formatAgentModelLabel.ts` suffixes only colliding Databricks labels
with their source:

- UC FQN: its `catalog.schema`, e.g. `GPT-6 Astra (system.ai)` / `GPT-6
Astra (sandbox.public)`.
- Workspace endpoint: its wrapper, e.g. `GPT-5.5 (databricks)` /
`GPT-5.5 (goose)` / `GPT-5.5 (kgoose)`.
- Still colliding after that (e.g. a date variant in the same schema):
the full id, e.g. `Gemini 3.7 Flash (system.ai.gemini-3-7-flash-0925)`.

Unique labels, raw-id rows, the default row, and other providers are
unchanged. It runs in `getDiscoveredPersonaModelOptions` (covering
`AgentDropdownSelect`, `PersonaModelCombobox`, `PersonaDropdownField`,
whose closed triggers read the option label) and in `ModelPicker`. Model
search in `AgentDropdownSelect` and `PersonaModelCombobox` also matches
the raw id, so searching `system.ai` finds those rows. Rust
`ModelEntry.name` is unchanged.

## What labels drive

Picker values, persistence, routing, catalog dedup, `resolve()` and
capabilities all stay keyed on the raw id; two ids sharing a label both
remain selectable. Labels do drive picker search (alongside the raw id),
the workspace catalog's equal-time sort, and `TeamIdentityCard`'s "Mixed
models" summary, which compares lowercased base labels, so `goose-` and
`system.ai` GPT-6 Astra read as one model there (correct, since they are
the same model).

## Tests

Shared fixtures in `scripts/databricks-label-fixtures.json` are replayed
by both Rust and TS. Each row asserts its tier: `exact` rows match a
record, `alias` rows resolve with generation off, `generated` rows
return nothing with generation off (so a future alias or record cannot
mask them), and `raw` rows return nothing either way. A curator guard
requires that across all 57 exact records, the ids the grammar does not
reproduce equal exactly the 10 curator-only ids.
`disambiguateModelLabels` has unit tests for namespace, wrapper, and
full-id-fallback collisions, untouched rows, other providers, and
case-insensitive grouping. The defaults-picker e2e checks the suffixed
rows and trigger, raw-id search, and that choosing the second uncurated
FQN by its visible name persists its raw id and restores its label. An
edit-agent e2e covers the existing-agent model dropdown
(`#edit-agent-model`): both suffixed rows render, the `system.ai` row is
chosen by name, and after save and reopen the agent stores the
`system.ai` id and shows `GPT-6 Astra (system.ai)`.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary

Upgrade Buzz from MeshLLM `v0.76.0-rc9` to the latest stable [v0.76.2
release](https://github.com/Mesh-LLM/mesh-llm/releases/tag/v0.76.2) (via
v0.76.1).

- Update all eight Mesh git dependencies across desktop and relay smoke
examples; both lockfiles resolve all 47 Mesh packages to
`a0c1e66b0ac037dd56544b9e2d94969ea43d694f` (tag `v0.76.2`). All non-Mesh
lock records are unchanged by the 0.76.1 → 0.76.2 bump.
- Match Mesh's `ed25519-dalek = 3.0.0` and `rusqlite = 0.40`
dependencies.
- Explicitly enable rusqlite's `fallible_uint` feature, preserving
Buzz's existing `u64` SQL conversions when **mesh is disabled**.
Mesh-enabled builds previously supplied this transitively, so testing
both configurations matters.
- Temporarily pin **only** the optional `sqlx-sqlite` package to the
merged upstream compatibility fix
[transact-rs/sqlx#4402](transact-rs/sqlx#4402),
revision `94aafe3a68884d923b0798a767c8d7f6cfda89d2`. SQLx 0.9.0 caps
`libsqlite3-sys` below 0.38, while Mesh 0.76 requires 0.38. Cargo
resolves the conflict even with SQLite disabled. The fix raises that
upper bound; remove the patch once released. Buzz's active PostgreSQL
driver stays on crates.io SQLx 0.9.0.
- Preserve the explicit Linux keyring 3 `crypto-rust` feature so Secret
Service sessions retain DH encryption independently of Mesh keyring 4.
- Preserve the dedicated examples-only `buzz-mesh-smoke` crate and
lifecycle workflow wiring, keeping Mesh/native-runtime dev dependencies
out of relay CI builds.
- v0.76.1 → v0.76.2 upstream delta: self-updater fix for composed
product-v2 bundles (Mesh-LLM/mesh-llm#1844) plus release plumbing — no
API changes affecting Buzz.
- Merged `origin/main` (`7c789dee0`) without rewriting shared PR
history.

### Validation at `ae4adba15` (local macOS ARM64, Hermit toolchain)

- **Passed:** `cargo test --locked -p buzz-mesh-smoke --all-targets`
(all six example harnesses compile; no live harness execution).
- **Passed:** desktop Tauri workspace tests, both default and
`--features mesh-llm` graphs (3,296 tests with mesh enabled, 0
failures).
- **Passed:** `just desktop-tauri-clippy` (both feature graphs, `-D
warnings`), root workspace clippy `-D warnings`, `cargo fmt --all
--check`.
- **Passed:** `--locked` metadata resolution for both workspaces;
lockfile diff audited to touch only Mesh package records.
- **Passed:** full pre-push hook suite on the final push (branch-skew,
rust-tests, desktop-tauri-checks, file-size-check) — no hooks bypassed.
- **Not run:** live Mesh inference/admission smoke or installed-app UI
exercise. CI lifecycle smoke coverage applies.

Originating Buzz conversation:
buzz://message?channel=dffbc117-845a-4c02-bc3f-eaeeefc4deaa&id=670f9c262e1ec0a4df5600b3173a0c5de2abe17e08ba9ccfdd8eff109fadf1a8

---------

Signed-off-by: Thinker <75d8a808fa21bb8d1812e080cf471db601c6e8e6a62dcd516d37531e83a7bb77@meshllm.communities.buzz.xyz>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Thinker <75d8a808fa21bb8d1812e080cf471db601c6e8e6a62dcd516d37531e83a7bb77@meshllm.communities.buzz.xyz>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Galadriel <b81c841ed20a8e17caf4e6fac2f6f3bc61d304c84b8394720f6d00f37538c47e@meshllm.communities.buzz.xyz>
Co-authored-by: Gu <gu@buzz>
)

Mobile heart and warning reactions and message text render as monochrome
symbols because bundled Inter supplies U+2764 and U+26A0. Remove only
those two character mappings from both regular and italic Inter fonts so
Flutter uses native font fallback. No production Dart or Markdown
parsing changes are needed.

The reproducible font-processing script verifies that every other
mapping and font table is unchanged, apart from the required checksum.
Glyphs, metrics, language coverage and symbols such as © ® ™ ↑ ↓ − ∕ are
preserved; the bundled Inter license is retained.

Validated on an actual iPhone 17 Pro simulator running iOS 26.5.
Reaction hearts and warnings remain centered within 0.33 logical pixels
of the pill center. Native screenshot checks cover reactions and plain,
emoji, explicit text-style, bold, italic, and enlarged emoji-only
message hearts and warnings, plus pixel-identical preserved text
samples.

**Fallback behavior:** on iOS 26.5, plain ❤ / ⚠ and explicit text-style
❤︎ / ⚠︎ also render as color emoji. Native font metrics add one logical
pixel to each of the three mixed-message lines in this fixture. Android
visual behavior has not been validated.

### Reactions

| Before | After |
| --- | --- |
| ![Monochrome hearts and warnings in
reactions](https://raw.githubusercontent.com/block/buzz/cf60b78c79fbdc4ac314ba74f3fd27b76c823755/pr-7842--before-reactions.png)
| ![Centered native color hearts and warnings in
reactions](https://raw.githubusercontent.com/block/buzz/cf60b78c79fbdc4ac314ba74f3fd27b76c823755/pr-7842--after-reactions.png)
|

### Message text

| Before | After |
| --- | --- |
| ![Monochrome message hearts and warnings with preserved
symbols](https://raw.githubusercontent.com/block/buzz/cf60b78c79fbdc4ac314ba74f3fd27b76c823755/pr-7842--before-messages.png)
| ![Native color message hearts and warnings with preserved
symbols](https://raw.githubusercontent.com/block/buzz/cf60b78c79fbdc4ac314ba74f3fd27b76c823755/pr-7842--after-messages.png)
|

Images are cropped native simulator screenshots of production widgets
with synthetic content. Before uses the original bundled fonts; after
uses the processed fonts.

Added tests: the existing mobile CI test lane now checks both bundled
fonts against the verified generated artifacts, preventing restoration
of the old mappings or unintended font changes.

Validation: the two asset tests, font-table verification, byte-identical
regeneration, native screenshot regression checks, and Flutter analysis
pass. The screenshot test fails against the original rendering. Pre-push
checks, including the full mobile suite, pass. Full repository
validation also passes. The enlarged emoji-only screenshot check passes
and rejects the old normal-sized fixture. GitHub CI, Codex review, and
the security review pass for commit 538167a. Both prior Codex
findings are addressed and resolved.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
🤖
## Summary
Scripts, scheduled jobs, and other automation need to give an agent one
prepared task and wait for a result without starting a long-running chat
listener. This adds `buzz-acp run`: it reads a task from a local file or
stdin, runs one fresh agent session with the configured tools and
instructions, cleans up, and exits with a machine-readable status.

```sh
buzz-acp run --task ./task.json
cat task.json | buzz-acp run --task -
```

- The versioned JSON task specifies a task ID, intended agent public
key, prepared prompt, and execution time limit. Invalid input or an
identity mismatch is rejected before agent launch. Credentials,
executable, tools, and permissions remain controlled by trusted launch
settings, not the task document.
- Each invocation uses the normal agent equipment: base/persona/team
instructions, enabled memory, tools, model and effort settings,
permissions, and Git identity, credentials, and signing. Both task and
conversational modes obtain these capabilities from one shared runtime
owner; new shared setup belongs there rather than in separate entry
points.
- Launch configuration reads have a separate ten-second bound and can be
cancelled even when a prompt file blocks. A deadline covers adapter
startup through the task turn. Interrupts and termination signals
trigger bounded cleanup. Stdout contains one terminal JSON record;
diagnostics stay on stderr. A normal turn ending is not proof that the
task achieved its goal.
- Normal invocation remains the conversational service. Task mode does
not start chat subscriptions, presence, heartbeat turns, or a persistent
agent pool. Tasks are trusted executable instructions, not sandboxed
input. Reusing a task ID does not prevent duplicate execution, and
uncertain outcomes must not be blindly retried.

See [the task contract](crates/buzz-acp/TASKS.md) for the document
format, exit codes, limits, and examples. URL retrieval, a task server,
and a persistent execution ledger are outside this change.

### Related issue
None found in the existing issue and PR search.

### Testing
- At `4e86bbc97`: 964 unit tests and 22 integration tests passed, with
three existing ignored tests. The package suite ran serially with
inherited harness/Git options cleared. A final test-only shutdown-result
assertion was then checked with the focused memory test and all-target
Clippy.
- All eight real-binary task tests independently passed on the exact
candidate, including blocked configuration reads, file/stdin validation,
session equipment, deadlines, cancellation, and cleanup. The memory test
passed 20 repeated runs. Removing the oversized-input guard fails the
new error assertion; restoring it passes.
- Package all-target Clippy, formatting, file-size checks, and pre-push
Rust/Tauri checks passed. All executed hosted checks passed on the
updated head (54 successful, 28 skipped). The separate Codex review
comment still requests authorization for this exact range.
- Earlier revisions passed the release build and a real Goose 1.51.0
tool-use smoke test. No new release artifact or installed Janet runtime
is claimed for this feedback revision.
- Repository-wide `just ci` was previously attempted; workspace Clippy
passed, then the desktop dependency check stopped on a local pnpm fetch
failure. Hosted checks cover the applicable PR lanes.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
…lock#7850)

Pre-push hook failures were hard to diagnose in two ways that cost
agents repeated reruns.

**nextest printed no failures.** lefthook runs the `rust-tests` lane
under a pty, and with an agent shell's `TERM=dumb` nextest picks its
progress-bar reporter, which then swallows every FAIL line, panic, and
summary — the lane only says `error: test run failed`. An orphaned
nextest (left behind when a push is killed) also spins at ~100% CPU
polling the dead pty for keyboard input. `rust-tests` now sets
`NEXTEST_SHOW_PROGRESS=counter` and `NEXTEST_NO_INPUT_HANDLER=1` via
lefthook's per-command `env:`.

**Stale `node_modules` looked like a code error.** After merging main
moved dependencies, `desktop-typecheck` failed with `Cannot find module`
and no hint to reinstall. `desktop-check`, `desktop-typecheck`, and
`desktop-test` now set `pnpm_config_verify_deps_before_run: warn` (a
shared YAML anchor), so pnpm prints `Your node_modules are out of sync
with your lockfile …` ahead of the script output. It is scoped to these
lanes and set to `warn` because the check also gates `pnpm exec`, and
`just bump-desktop-version` leaves the workspace flagged until the next
install, so `error` would block desktop release pushes. pnpm uses
recorded workspace state and manifest timestamps and is not a complete
lockfile-content check, so this is a hint rather than a guarantee.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
🤖 I’m Larry.

## Summary
Managed-agent definitions can now choose which installed ACP transport
will launch the agent. The choice sits beside **Agent harness** while
creating or editing an agent, so it is made before first deployment and
remains visible afterward.

Buzz discovers executable commands named `buzz-*-acp`, keeps **Buzz ACP
(default)** as the safe default, and preserves existing custom command
values. Discovery and launch share the same resolver, so a command
offered in the picker is the command Buzz will execute.

## UX walkthrough

### 1. The ACP command picker appears while creating the agent
The default is explicit, and the helper text explains that this
selection controls deployment.

![Create-agent form with the ACP command picker beside Agent
harness](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/01-persona-create-acp-selector.png)

### 2. Installed wrapper commands are offered by name
Here Buzz has discovered `buzz-janet-acp` alongside the stock transport.

![ACP command menu offering Buzz ACP and
buzz-janet-acp](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/02-discovered-wrapper-dropdown.png)

### 3. The wrapper is selected before deployment

![Create-agent form with buzz-janet-acp
selected](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/03-wrapper-selected-before-deploy.png)

### 4. The deployed profile reports the effective command

![Deployed agent Runtime profile showing
buzz-janet-acp](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/04-deployed-profile-effective-acp.png)

### 5. Editing the deployed agent reopens its definition with the choice
preserved

![Edit-agent form retaining
buzz-janet-acp](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/05-edit-shows-persisted-wrapper.png)

## Details
- Definitions carry the selected ACP command through relay publication,
snapshots, imports, team adoption, deployment, and later edits.
- Existing definitions without this field continue to use `buzz-acp`.
- Unknown persisted commands remain visible but unavailable rather than
being silently replaced.
- Discovery is read-only and never executes candidates. It filters
non-files and non-executables, supports Windows `.exe`, `.cmd`, and
`.bat` shims, deduplicates aliases, and returns a stable sorted list.
- Command names remain portable; Buzz does not persist machine-specific
absolute paths.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@nostr>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Upstream MinIO image pulls are blocking CI (block#7867). Add a separate
publisher for `ghcr.io/block/buzz-minio:latest`, so the reusable image
can be bootstrapped before CI adopts it in block#7870.

- Build the checksummed official MinIO/mc release binaries from block#7868 on
a digest-pinned Alpine base.
- Build only when image/workflow/Compose inputs change or on manual
dispatch. PRs build and smoke-test; only `block/buzz` main can publish.
- Run the real Compose healthcheck and initializer, object
upload/read/delete, and anonymous-access denial before publishing
`latest` plus a unique `sha-<commit>-run-<id>-<attempt>` tag.
- Manual dispatch disables Docker's layer cache to refresh Alpine
packages. Normal image builds retain caching; each maintenance rebuild
keeps its own tag.

### Rollout

This PR contains only the publisher and opt-in Compose override. **It
does not switch ordinary CI consumers**, so its checks do not require
the unpublished image. This is a separate PR from adoption, as required
by Buzz's squash-only merge rules.

1. Merge this PR; its main-branch workflow builds, tests, and publishes
the image.
2. Make the `buzz-minio` GHCR package public and verify an anonymous
pull of `ghcr.io/block/buzz-minio:latest`. New GHCR packages default to
private.
3. Rebase draft block#7870 onto current main, retarget it to main, run its
integration checks, and merge it to switch all MinIO-backed CI jobs to
the published image.

### Validation

- Updated hosted [MinIO image build and smoke
test](https://github.com/block/buzz/actions/runs/36018177877) passed at
`700ab6d5346b37a64c9defde0016dc65c98aa790`; publication correctly
skipped on the PR.
- Hosted [ordinary
CI](https://github.com/block/buzz/actions/runs/36018178123) passed at
the same head. Its actual Git comparison detected only the five
publisher files, all application path filters were false, and the
MinIO-dependent integration checks were correctly skipped. Synced
current main before this run to eliminate an earlier stale-base
comparison that included unrelated ACP changes.
- Actual five changed paths were passed through the repo's pinned
`dorny/paths-filter`: all application domains were false. All 12
existing path-selection cases and the required-context isolation
contract also passed.
- Publisher actionlint, smoke-script shellcheck, and commit/push hooks
passed.
- Fresh local rebuild of `700ab6d5346b37a64c9defde0016dc65c98aa790`
passed with `docker buildx build --no-cache --platform linux/amd64
--load`. Logs confirm new Alpine index fetches and package installation
rather than a cached install layer. The committed smoke script passed
under local AMD64 emulation using the current merged Compose services
with isolated resource names and no published ports: healthcheck,
initializer/private bucket, object upload/read/delete, and anonymous
HTTP 403. Temporary containers, network, and volume were removed. Both
pinned binary checksums match the official release SHA-256 files.
- Full `just ci` was previously attempted: workspace formatting/Clippy,
desktop checks, and Tauri formatting passed; the run was stopped during
Tauri Clippy. No full local application-suite pass is claimed; hosted
application suites correctly skip this publisher-only change.

---------

Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
…ead (block#7806)

Mobile half of the sidebar stale-reader fix (desktop: block#7805). It makes
the channel sections, sort, stars and mutes managers converge on the
latest relay head.

## Changes

- **Tie-break:** sections, stars and mutes compare `(created_at, id)`
with `id < cursor`, which matches the relay's `ORDER BY created_at DESC,
id ASC`. Sort already did this.
- **Coherent cursor:** `_publish()` records the signed event ID through
`SignedEventRelay.submit(onSigned:)` and advances
`(_lastRemoteCreatedAt, _lastRemoteEventId)` together. It only advances
when the pair is after the current cursor, so a newer head learned
during the await is kept. This applies to sections, stars and mutes.
- **Stars/mutes startup retry:** one serialized `_syncWithRelay()`
(`_syncInFlight`/`_syncAgain`) with 2s→30s backoff. It retries until the
history head is applied and the live subscription is settled. The
stars/mutes managers no longer re-subscribe on CLOSED:
`RelaySessionNotifier` owns retryable and rate-limited CLOSED, and a
terminal CLOSED is never re-sent. Reconnect works through session replay
and provider rebuild. Sections and sort keep their existing manager
CLOSED retry.
- **Stars/mutes durability:** storage `write()` returns the
`setString()` result. A decode or apply error, or a persist that returns
false or throws, fails the attempt. The accepted head only advances once
it is durable, so the same head is retried.
- **Stars/mutes pending fence:** each read snapshots `_localRevision`
and `_publishPending`. If a local edit or a publish happened while the
read was in flight, the result is discarded. Startup recovery retries
it; a resume read does not. Per-entry arbitration is unchanged.
- **Sections publish commits content with its cursor:** the submitted
blob is committed together with its cursor, so a same-second loser
applied during the OK wait can't strand the device. A local edit made
during the wait is kept.
- **Stars/mutes undecodable head:** an undecodable head is skipped
without holding startup open. Only a persist failure keeps the head
unapplied and retried.
- **Resume re-read:** all four providers listen to
`appLifecycleProvider`. On `resumed`, each manager runs one guarded
history read through its existing fetch/apply path. It is one-shot:
skipped when a local edit or publish is pending as it starts, not
retried on failure, and it starts no timers or poll loop. The result is
discarded if a mutation lands before it applies, and a disposed or
superseded manager (sort generation fence) never persists it.

## Residuals

- If the app stays in the foreground the whole time and a healthy socket
drops an EVENT, the missed head is picked up on the next resume or
reconnect.
- Sections: a live event can still overwrite a pending local edit. This
is pre-existing, and the resume read adds no new path to it.
- Sort: a greater remote timestamp wins, by policy.
- Stars/mutes: entries with equal `updatedAt` can still resolve
differently on different clients, under the existing merge policy.
- Stars/mutes: publish reads first and keeps the entry with the later
`updatedAt`, so a peer whose clock runs fast can override a pending
local entry. This rule already exists on main and matches desktop.
- Sections and sort persistence failures are pre-existing and not
redesigned here.

## Size

Production: 10 files, +495/−162. Tests: 3 files, +718. The new
`sidebar_sync_fixture.dart` runs over the real `RelaySessionNotifier`
seam and is shared by `channel_reader_recovery_test.dart`. Plus a
sections tie-break case.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
Switch every CI job that starts MinIO to the public
`ghcr.io/block/buzz-minio:latest` image published by block#7869. Integration
jobs pull the image; image builds stay in the separate, path-filtered
publisher workflow.

- Apply the CI Compose override to both desktop integration shards,
backend integration, relay E2E, and mesh lifecycle, including
script-mediated Compose calls. Both the server and bucket initializer
use the Buzz image.
- Override both MinIO images in Helm's quickstart CI values, covering
the server, bucket initializer, and relay's wait-for-bucket init
container in the gated kind installation.
- Select integration and mesh checks when either Compose file changes,
with regression coverage using the real pinned `dorny/paths-filter`
action.

### Publication verified

block#7869 is merged, and [the first main-branch
publication](https://github.com/block/buzz/actions/runs/36021389137/job/107706874954)
succeeded. An anonymous pull using an empty Docker configuration
returned the publisher's digest:
`sha256:b8470bbeafbf57b20c86cf63804682b714bdcfdbb517f3770247e321e623f48f`.
This PR is rebased onto main and ready for consumer integration checks.

### Validation

- Ran the repository's MinIO smoke script against the actual published
AMD64 image: healthy startup, bucket initialization, object
upload/read/delete, and rejection of anonymous reads all passed. Used
the rendered CI Compose services with isolated local ports and resource
names.
- Verified `mc` runs and writes its configuration as Helm's non-root
UID/GID 65532 with dropped capabilities and no privilege escalation.
- Helm 3.16.4 lint and quickstart rendering passed; all three rendered
MinIO container references use the Buzz image.
- All 14 CI selection cases and the required-context isolation check
passed. Mesh workflow passes actionlint; shared workflows pass with
shellcheck disabled for pre-existing findings.
- Fresh repository-wide review traced workflow and script consumers and
found no remaining CI image omissions. Full application integration
checks and a real kind installation have not been run locally.

---------

Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
## Buzz Desktop release v0.5.25

- **Frozen main:** `797012ff01a6d499959b45ed2e56f7927c6a4d6b`
- **Reviewed candidate:** `c8f73213089cbd5a0f1e675d3193558280d46e10`
- **Previous desktop release:** `desktop-v0.5.24`
- **Proposed immutable tag:** `desktop-v0.5.25`

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>
## Why

Buzz server components need one typed feature-flag contract without
forcing the open-source build to depend on LaunchDarkly. A staging or
production relay must not be able to choose the wrong provider through
runtime configuration.

## What

- Adds typed boolean and signed `i64` flags with static, environment,
and compile-gated LaunchDarkly evaluators.
- Defines an executable relay artifact contract that compiles exactly
one provider.
- Rejects zero or multiple relay provider features at compile time.
- Runs the LaunchDarkly feature tests and the provider compile matrix in
the unit CI lane and fallback test script.

## How

`buzz-feature-flags` stays provider-neutral. The future relay
composition root will expose one build path per artifact feature:
`static-feature-flags`, `environment-feature-flags`, or
`launchdarkly-feature-flags`. Runtime configuration carries only
settings for the selected provider. It never carries a provider name,
fallback, or precedence rule.

The crate still has no relay consumer. A locked compile fixture proves
each singleton builds and every zero or multi-provider combination fails
until the relay adopts this contract.

This removes the proposed runtime `ProviderMode` and leaves one
provider, one construction path, and one evaluator per artifact.

## Risk

This PR still has no relay or app consumer, so it changes no
user-visible behavior. Risk is limited to the new feature-flag crate,
its compile contract, and its unit-test lane.

## Testing

An external Rust consumer at `30cf968bfe85e82980fda7e63f5bebe649ca939f`
exercised six environment-evaluator scenarios and 59 assertions. The
compile-time correction adds no live consumer to exercise manually.

## Bigger picture

When `buzz-relay` adopts this crate, its build manifests must map each
distribution to one provider feature. That consumer PR must verify live
Relay Proxy reachability, secret ingestion, lifecycle ownership, and
user-visible behavior.

Generated with Codex

---------

Signed-off-by: tornquist <tornquist@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ock#7873)

Follow-up to block#7806.

- **A retired sections manager never writes the shared prefs cache.**
`_persist()` now returns once the manager is disposed. That covers every
path a torn-down manager could take after an account or relay switch,
including a late OK restoring its submitted layout and the `dispose()`
flush's read-before-write merging a head. Before this change, both could
overwrite the successor's cache on the same pubkey key.
- **Coverage:**
- a retired late OK and a retired flush pre-read both leave the
successor's cache untouched
- an edit made while an OK is held still publishes after the OK is
released (`_lastPublishedStore` is built from the submitted store)
- a same-second losing head adopted after a local edit still converges
UI, cache and relay once our OK wins. The adopted layout's pending
debounce is deliberately left armed, because its republish is what
re-converges this race.

**Residual:** an edit made just before our own echo arrives can be
replaced by that echo. This is existing behaviour on main and on
desktop.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
…face (block#4768)

Adds a relay admin console under Settings — the single surface for
relay-level operator work — replacing the previously unreachable
moderation section. Operators authenticate with their own Nostr key over
NIP-98 — no browser extension, no bearer token — to view deployment-wide
moderation reports and product feedback, resolve/dismiss/escalate/reopen
reports, update feedback status, and manage the operator/moderator
staffing roster.

## Nav reachability

The old `moderation` section id was defined in `settingsSections` but
never wired into any group in `settingsNavGroups`, so the sidebar never
rendered it — unreachable since block#1617. This PR replaces it with a
`relay-admin` section wired into the Communities nav group, pointing at
the relay admin console, and drops the separate `admin-console` section
id so a single surface owns relay-level trust & safety.

The Relay admin nav entry is always visible. Auth gates the panel
itself; the nav entry is a door, not a credential — hiding it behind
discovery state would strand an operator whose relay transiently fails
NIP-11 discovery with no path back to the connection settings.

## Discovery and connection

On mount the console reads the connected relay's NIP-11 document for the
optional `admin_api` field. When a valid origin is discovered **and its
host matches the connected relay's host** (case-insensitive, host
identity only), it is automatically saved and probed — the panel renders
immediately with no Save step required. A cross-host advertisement is
pre-fill only: it populates the manual origin field under Advanced and
is never saved or probed without an explicit Save, so a relay cannot
direct an unconsented NIP-98 signature at a third-party origin; residual
exposure on the same-host path is bounded by NIP-98's URL/method/payload
binding. Saving is only required for manual origin changes. The
advertised origin is untrusted input: revalidated through
`AdminOrigin::parse`, falling back to manual entry when absent or
invalid. A saved manual origin always takes precedence over discovery.

## Auth and transport (Rust, `src-tauri/src/commands/admin/`)

- `AdminOrigin` value object: validates scheme + host + optional port,
rejects credentials/path/query/fragment; `http://` only for loopback.
The relay side normalizes its configured admin host at config load and
compares inbound `Host`/`Origin` hosts case-insensitively.
- `AdminRoute` closed enum: no IPC surface accepts arbitrary URLs, so
the NIP-98-signed URL is byte-identical to the fetched URL.
- Dedicated no-redirect `reqwest` client as an SSRF guard: a relay `3xx`
is an error and the NIP-98 header never crosses origins.
- `admin_probe` signs `GET /probe` and returns a typed state
(`nip98Authorized` / `nip98Denied` / `disabled` / `notAdminApi` /
`networkOrIntercepted`). The response body is validated against the full
relay contract — `nip98Authorized` only when the complete NIP-98
invariant holds, `disabled` only on a coherent disabled-mode body; any
incoherent or unrelated JSON classifies as `notAdminApi`, so the console
never fails open at the network boundary. Unknown fields are tolerated
for forward compatibility.
- NIP-98 signing via `AppState::signing_keys()`, one retry on `401` with
a fresh signed event; response size bounded by a `Content-Length`
preflight and a streaming byte counter; per-pubkey origin storage with
atomic `0o600` writes.

## Relay admin console

The console is gated behind the relay `OPERATOR`/`MODERATOR` roles from
block#3777; every `/api/admin/v1/*` route returns `403` to anyone else.
Community self-administration (`community-members` / Invites) and
in-channel enforcement (the 9040–9043 signed commands) are a separate
axis and are untouched. The previously-duplicate community report queue
(`ModerationQueueCard`, `moderationQueue.ts`, and three dead hooks) was
unreachable dead code and is deleted in its own commit;
`useBanMemberMutation` and the rest of `features/moderation/hooks.ts`
remain in use by the members sidebar and message menu.

## Console UI (TypeScript, `src/features/admin-console/`)

- `AdminConsoleSettingsCard`: discovery + probe flow, honest copy for
every probe state, manual origin under an `Advanced` disclosure at the
bottom. The role (operator/moderator) is rendered in the connection
status line as plain text ("Connected as operator") — no badge-shaped
elements above the tab row. The origin provenance (relay config /
database) appears inside the Advanced card as small muted text. Section
title changed to `Admin`.
- `AdminConsolePanel` with Reports / Feedback / Staffing tabs (Staffing
gated on `role === "operator"`); reports and feedback grouped by
community for cross-community triage.
- **Reports**: always requests `scope=all` so the existing
resolve/cancel/reopen controls can reach every status (`open`,
`processing`, `resolved`, `dismissed`, `escalated`). The relay's
omitted-scope default is `escalated`-only (the platform-safety
backstop); `scope=all` is explicit and scoped to this console. Full
action matrix per `target_kind` (event →
delete/kick/ban/timeout/dismiss/escalate; pubkey →
ban/timeout/dismiss/escalate; blob → dismiss/escalate); kick is
suppressed when the report carries no `channelId`. A `processing` row
stays navigable — enforcement state lives in the detail view. A `failed`
action (always pre-mutation) offers a single **Cancel & reopen** via
`POST /reports/{id}/cancel` with `{actionId}` fencing; there is no
client-side retry. `pending`/`enforcing` actions belong to the relay
recovery worker and offer no button; a `409` reloads detail. Lists
refetch on back-navigation after a mutation.
- **Report failure UX**: `reportErrorMessage()` strips 4xx/5xx status
prefixes and surfaces the relay's own error reason in the toast (e.g.
relay returns `"400: you cannot report this content"` → toast shows
`"you cannot report this content"`). Self-report blocking UI removed —
the relay has no self-report gate, and the failure was purely
client-side error surfacing.
- **Reason audience disclosure**: the Reason (optional) field shows
exact copy under the input based on selected action — Delete discloses
that reason is sent verbatim to the affected user and posted publicly in
the room; Kick/Ban/Timeout disclose affected-user only; Dismiss/Escalate
disclose reporter only. These mirror the actual relay notice paths.
- **Enforcement history**: the report detail carries the governing
action even after the report leaves `processing` — a report enforced
then reopened is `open` yet still shows its `succeeded` action as
executed history alongside the resolve form; a reopen does not un-happen
a ban. The resolve form gates on `open` status alone.
- **Reopen**: terminal reports (`resolved`/`dismissed`/`escalated`) can
be returned to `open` for re-triage via `POST /reports/{id}/reopen`.
Reopen never reverses applied enforcement, and the copy says so.
- **Feedback**: list, detail, and status control
(`new`/`reviewed`/`archived`) with a generation-fenced attachment
viewer. A purged source community severs provenance to `null` rather
than deleting the row; severed rows bucket under a "source community
removed" heading.
- **Staffing**: Add is create-only with display names resolved via
`useUsersBatchQuery` and npub cross-fade on hover
(`HoverStaffingIdentity`). An entered key that already appears in the
authoritative roster (config, owner-fallback, or DB) is rejected locally
with an inline duplicate-key message and no request is issued; a `409
Conflict` from add, role change, or remove surfaces the relay's parsed
error body via `adminErrorMessage(e)` — distinguishing config-backed key
conflicts from last-operator conflicts without HTTP-status inference.
Roster with `config`/`owner_fallback`/`db` source labels; config-backed
entries disable remove client-side. In-place role change via `<select>`
calling `putAdminOperator`; the relay responds with the effective
`OperatorEntry` (`pubkey`, `effectiveRole`, `sources`) in the same shape
as the roster list. Read-only badge for config-managed entries.
- **Timeout expiry**: `useTimeoutState` clears the store when derived
state is `INACTIVE` but the store is still active; the 1s tick is scoped
to entries with a known `expiresAtMs` and owned by
`ComposerTimeoutBanner` (mounted only while a timeout is active), while
`ChannelPane` subscribes to the boolean `useTimeoutActive()` — the pane
re-renders when a timeout starts or ends, not every second.
`useMembersSidebarModeration` ticks `nowMs` only while the sidebar is
open, the viewer can moderate, and some member has a future
`mutedUntil`; the tick after the last expiry stops the interval.
- Staffing refreshes the principal after a self-demotion or
self-removal: `AdminConsolePanel` passes `onSelfMutation` to
`StaffingTab`, which re-probes the saved origin so the role badge and
tab visibility update without a manual re-probe.
- Staffing mutation failures, including both 409 variants (config-backed
key vs. last operator), surface the relay's parsed error via
`adminErrorMessage`, so the last-operator recovery guidance is shown.
- Mutations confirm via `sonner` toasts; failures surface the relay's
parsed error `message`, not raw JSON. The `disabled` auth mode renders
read-only.

## Mutation idempotency

Resolve and reopen carry a per-attempt `requestId`. On first submit the
whole command (`{requestId, action, reason, expirationSecs}`) is frozen;
after an ambiguous failure (409, 5xx, transport error, truncated
response) the snapshot is retained, action/reason/duration controls are
locked, and the retry sends the exact same payload byte-for-byte. A
definitive pre-commit rejection (non-409 4xx with `bodyComplete: true`)
discards the snapshot, unlocks controls, and the corrected submit uses a
fresh payload. On success the toast derives from the authoritative
`AdminReportResolution` (`activeAction.action` when present; otherwise
`status` for decision-only outcomes) — never from the mutable form
selection.

The native mutation commands — including operator delete — reject with a
typed `AdminMutationError` carrying the relay's HTTP status
(`relayStatus`, `null` when no verdict exists) and a `bodyComplete` flag
that is `true` only when the full response body was read.

## Restriction routes contract change

`GET /members/restrictions` and `DELETE /members/{pubkey}/ban|timeout`
now take `communityHost` instead of `communityId`; the relay resolves
the tenant itself and returns `400 unknown_community_host` for an
unmapped host. This is a breaking contract change with no compatibility
shim: the relay must run this build before a desktop that uses it,
scripts sending `communityId` must switch, and rolling the relay back
breaks the desktop Restrictions UI. `docs/admin/README.md` documents the
three routes in the route inventory and carries the migration note.
Restriction calls also carry the relay the list was loaded from, and the
native side rejects a mismatch before building any URL.

## Hardening

- Saved admin origins are stored per pubkey and per relay host. The host
slug parses the relay URL, keeps the port-drop for DNS names, and
encodes IPv6 hosts canonically, so two IPv6 relays never share a file
and no IPv6 slug can match a DNS slug.
- NIP-11 admin discovery has its own 10s request deadline and reads
`/info` success and error bodies through the bounded reader with a 64
KiB cap (`Content-Length` is only a pre-check).
- `PUT /operators` builds its `OperatorEntry` response from the
validated body (`effectiveRole = role`, `sources = ["db"]`) instead of
re-reading the roster, so a concurrent change can't turn a committed
write into a `403`.

Related: block#3777 (relay `OPERATOR`/`MODERATOR` role model + NIP-98 auth +
`admin_api` NIP-11 advertisement — provides the runtime and the
discovery field this console consumes)
Related:
[builderbot-platform-core-infrastructure#140](squareup/builderbot-platform-core-infrastructure#140)
(prod admin ingress allowlist and NIP-98 rollout)


## Screenshots

Captured headless at `197f0ba0c` against the mock bridge (1280×720).

### Setup

**Origin setup — Advanced disclosure open, idle state**
![Relay admin panel with Advanced: admin origin disclosure open before
probe](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/02-origin-setup.png)

**Authorized console — all three tabs**
![Authorized relay console showing Connected as operator status and
Reports/Feedback/Staffing
tabs](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/03-origin-authorized.png)

### Reports

**Reports queue — open, processing, resolved across two relay groups**
![Reports tab showing community-grouped list with spam/open,
harassment/processing, illegal/resolved
rows](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/04-reports-queue.png)

**Open report detail — reporter, target, message snapshot, resolve
form**
![Open spam report detail with reported message content and
Delete/Kick/Ban/Timeout/Dismiss/Escalate action
buttons](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/05-report-detail-open.png)

**Processing report — PROCESSING/HARASSMENT, ban enforcing state**
![Processing harassment report detail showing enforcement state with ban
action in enforcing
status](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/06-report-enforcement.png)

**Resolve form — Delete selected, audience disclosure copy**
![Resolve form with Delete action selected showing "Sent verbatim to the
affected user and posted publicly in the room" audience
copy](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/07-resolve-form.png)

**Resolved report — enforcement history block**
![Resolved/illegal report detail showing Resolved by, Resolved at, and
Act ID fields from completed
enforcement](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/08-resolved-history.png)

### Feedback

**Feedback list — community grouping with status badges**
![Feedback tab showing relay.example.com group with new/reviewed items
and source-community-removed
group](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/09-feedback-list.png)

**Feedback detail — mutable status control (operator mode)**
![Feedback detail with new/reviewed/archived status tab control visible
for
mutation](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/10-feedback-mutable.png)

**Feedback detail — passive status badge (read-only/disabled mode)**
![Feedback detail in disabled mode showing passive "Status NEW" badge
with no mutation
control](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/11-feedback-readonly.png)

### Staffing

**Staffing roster — config/owner\_fallback/db source labels**
![Staffing tab showing three operators with config, owner_fallback, and
db source
labels](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/12-staffing-roster.png)

**Add form — new pubkey filled, ready to submit**
![Staffing add form with a new 64-hex pubkey entered and moderator role
selected](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/13-staffing-add.png)

**Duplicate rejection — inline error for existing principal**
![Staffing add form showing "Already an operator: operator. Use Remove
to revoke before re-adding with a different role." error for an existing
config-backed
operator](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/14-staffing-dup.png)

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
…on leases under row lock (block#7853)

## What

Two fixes to `crates/buzz-db/src/store/relay_admin_actions.rs` and one
cleanup in `crates/buzz-db/src/store/thread.rs`.

### 1. Thread counter atomicity

The admin delete action now calls the canonical
`soft_delete_event_and_update_thread_in_tx` body shared with NIP-29 and
NIP-09 event-ID deletion. The delete, parent `reply_count` decrement,
root `descendant_count` decrement, and step marker commit or roll back
together in one transaction. A second action against an already-deleted
target is a no-op and does not double-decrement. After a committed
delete, `emit_live_thread_summary` refreshes the live thread count the
same way `DELETE_EVENT` does.

### 2. Lock-first lease expiry fence (all five admin-action paths)

On main, the final marker `UPDATE` in every admin-action path
(`execute_ban_with_marker`, `execute_timeout_with_marker`,
`execute_kick_with_marker`, `execute_delete_with_marker`,
`record_failure`) checks `clock_timestamp()` in the `WHERE` clause.
PostgreSQL can evaluate that predicate before waiting to lock the target
row. If another transaction holds the action row and releases it
unchanged after the lease expires, the update commits under the stale
timestamp.

On this branch, each path acquires the action row with `SELECT … FOR
UPDATE` inside the same transaction before any domain write. The
wall-clock expiry check in the marker `UPDATE` runs after that lock is
held, not before. The pre-entry ownership `SELECT EXISTS` checks remain
— they are early-rejection optimizations that are safe because the
correct final fence rolls back any provisional work.

`record_failure` on main uses a single-statement `execute(pool)`. On
this branch it begins a transaction, acquires the row lock with `SELECT
… FOR UPDATE` and an explicit expiry check, then writes `state =
'failed'` — matching the four mutation functions.

**Lease validity is checked at the protected ownership check under the
action-row lock, not at physical `COMMIT` completion.**

| Function | Change |
|---|---|
| `execute_ban_with_marker` | `SELECT … FOR UPDATE` added after
pre-entry check, before domain write |
| `execute_timeout_with_marker` | same |
| `execute_kick_with_marker` | same |
| `execute_delete_with_marker` | same |
| `record_failure` | wrapped in transaction; `SELECT … FOR UPDATE` +
clock check before state update |

### 3. Unused standalone counter helpers removed

`increment_reply_count`, `decrement_reply_count` (both in `thread.rs`),
and `Db::decrement_reply_count` have no production callers outside their
forwarding wrappers. Both perform unguarded autocommit writes that
bypass the deletion-transition/atomicity invariant. Removed to prevent
future misuse.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
…ck#7881)

The staging recipe pointed at the retired
`sprout-oss.stage.blox.sqprod.co` relay.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…lock#7879)

Adds a short paragraph to the Protocol section of `VISION.md` about
`/buzz/v1` endpoints, which are backend-for-frontend (BFF) reads.

- **Dividing line:** every write is a signed Nostr event. Nostr reads
must cover all of that state. `/buzz/v1/...` can add reads shaped for
specific use cases (thread catch-up, sync) over the same store.
- We start with this rule and refine it as more exceptions come up.
## Why

Data backfills need a durable contract outside schema migrations. The
contract must survive crashes and support automatic or manual operation
without adding version-selection machinery.

## What

- Define one PostgreSQL-backed lifecycle for finite data repair
- Treat the stable backfill ID as the only durable execution and
readiness identity
- Use eventually consistent, idempotent registration; compatible workers
compete for one exclusive claim
- Require bounded batches, atomic mutation and checkpoint commits,
generation fencing, and validation before completion
- Define all four migration and automatic-backfill configurations
- Define the authorized admin API and honest client projection

## How

A backfill captures one immutable upper bound, processes finite batches,
and commits each target mutation with its checkpoint. PostgreSQL owns
lifecycle state, claims, progress, and completion.

Registration order and build metadata do not choose a definition.
Compatible processes are interchangeable. A material behavior change
requires a new stable ID. The orchestrator does not resolve different
behavior registered under one active ID. Diagnostic validation on
completed rows records only its bounded outcome and audit; it does not
create an execution claim.

The spec now starts with a six-step mental model and follows the
operator flow. It removes repeated requirements and cuts the document
from 614 lines and 4,835 words to 474 lines and 3,656 words.

## Risk

Low. This PR changes documentation only. It commits Buzz to a design
contract, but it does not add runtime, schema, configuration, or
protocol behavior.

## Testing

No manual runtime testing. A fresh full-spec review checked the
lifecycle, safety rules, four configuration cells, admin contract, and
production-seam conformance requirements. `git diff --check` and the
repository file-size policy pass at
`13681664674d4667b181a6211f012090fa40a2cf`.

## Bigger picture

The design keeps the relay as orchestrator, a focused backfill crate as
domain owner, and `buzz-db` as narrow transaction infrastructure. It
removes deployment generations and definition precedence from the
implementation plan.

Generated with Codex

---------

Signed-off-by: tornquist <tornquist@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…7897)

Adds a "Before opening a PR" checklist and "Reviewing" guidance to
`AGENTS.md`, and makes the Review-Proven Rules the shared reviewer
checklist.

PRs stay in draft until an agent review has run, an agent has exercised
the change on the affected surface, and a human has tested it. The
review-completed passphrase goes in the description only once that
checklist holds for the current change, and comes out if later edits
change behavior. Docs-only changes use the light path: content, link,
and diff checks plus human confirmation.

Reviews read `VISION.md` and the surface's `VISION_*.md` first, check
the change against the Review-Proven Rules, recommend blocking only for
concrete correctness, security, or contract defects with a realistic
failure, label everything else optional, and put all findings in the
first pass. Agents comment and never use Request Changes; humans decide
which findings must be fixed.

Related:
[block/buzz-app#268](block/buzz-app#268)

---------

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…lock#7900)

`api::admin::postgres_tests::disabled_mode_allows_unauthenticated_requests_on_the_admin_host`
falls through to a DB-backed read route but was not `#[ignore]`d. The
`test-unit` exclusions meant to skip it still named
`api::admin::tests::…`, which stopped matching when block#6730 moved the
admin tests into `mod postgres_tests`. As a result the test ran in the
infra-free unit job and only passed after waiting out the sqlx acquire
timeout (~150s in hosted CI), while the ignored-only PostgreSQL lane
never ran it against a real database.

- The test is now `#[ignore = "requires PostgreSQL"]`, like its
DB-backed peers, so the PostgreSQL lane runs it.
- The two dead exclusions are removed, and the `api::admin` clause is
just `test(/^api::admin::/)`.
`nip98_mode_unrostered_signer_does_not_consume_a_replay_slot` was
already ignored and runs in the PostgreSQL lane.
- The `Justfile` comment now states the convention (DB-backed admin
tests are ignored; non-ignored ones reject before touching the DB),
drops the stale reference to a ci.yml step that no longer exists, and
keeps the timeout-trap warning.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
…lock#7264)

Implements NIP-FI HTTP ingress enforcement per the merged spec (PR
block#7254). Every protected HTTP surface in enforce mode requires a valid
`Nostr-Federated-Identity: Bearer <JWS>` assertion alongside its NIP-98
authorization, bound to the same key. The NIP-98 pubkey (proven actor)
must equal the assertion's `nostr_pubkey`; absent, mismatched, or
unverifiable assertions are denied fail-closed with byte-exact
text/plain responses per the spec's rejection table.

This is S5 of the `buzz-enterprise-identity` program. WS enforcement
(S3) is separate.

## What changed

**New `nip_fi_http` module:** `admit_nip_fi_http()` covers the full
admission sequence — `Nostr-Federated-Identity` extraction, offline JWKS
verification, NIP-98 key pairing [FI-INV-05], issuer-qualified deny-map
check via `(iss, pubkey, now)` [FI-INV-14], and byte-exact denial for
all four `DenialClass` variants. `admit_nip_fi_http_on_state()` is the
single call-site wrapper for all surfaces [FI-TRACE-AUTHORITY-UNIFORM].

**Protected surfaces** (all gated via the uniform authority):
- HTTP bridge: `POST /events`, `/query`, `/count`
- GIF proxy: `POST /gifs/search`, `/gifs/share`
- Workflow reads: `GET /workflows/{id}/runs`, `GET
/workflows/{id}/runs/{id}/approvals`
- Moderation reads: `GET /moderation/reports`, `/moderation/audit`,
`/moderation/restricted`
- Media/Blossom: `PUT /upload`, `/media/upload`
- Git smart-HTTP: `info/refs`, `upload-pack`, `receive-pack`
- Git settings control plane: `GET`/`POST
/git/:owner/:repo/default-branch` (now routed through
`admit_nip_fi_http_on_state` for full assertion key-pairing)
- Invites: `POST /api/invites` (mint only; claim exempt as
pre-membership enrollment door)

**NIP-98 strictness in NIP-FI mode:**
- When NIP-FI is not Off, bridge POST handlers force `require_auth_token
= true` — the `X-Pubkey` dev-mode fallback cannot satisfy the pairing
requirement [NIP-FI.md:547-567]
- POST bodies on `/events`, `/query`, `/count` require a matching
`payload` tag in enforce mode [NIP-FI.md:579-597]
- A payload tag present with no content (one-element `["payload"]` or
empty string) is rejected as a structurally invalid claim — NIP-98
requires the content to be a valid 64-char lowercase hex SHA-256 digest.
Malformed payload diagnostics use a static error message (no
attacker-controlled content in logs).
- Off mode preserves pre-NIP-FI behavior per [FI-INV-15]

**Off-mode compatibility changes:** Admission now runs before request
validation on every protected surface, so Off mode differs from
pre-NIP-FI behavior in these ways:
- **GIF routes:** NIP-98 authentication happens before the upstream
provider lookup. An unauthenticated request on a tenant with no GIF
provider now returns 401 instead of 404. The 401 goes through the legacy
`api_error()` path, so the body is JSON `{"error":"..."}` with no
`WWW-Authenticate` header.
- **Workflow runs** (`GET /workflows/{id}/runs`): the query string is
parsed after admission. A request with a missing credential and a
malformed query (for example `?limit=abc`) now returns 401 instead of
400.
- **Authorized malformed query:** once admitted, a malformed query
returns a JSON 400 `{"error":"invalid query: ..."}` from
`parse_query_or_400` instead of axum's `text/plain` `Query` rejection.
- **Moderation reads** (`/moderation/reports`, `/moderation/audit`):
same extraction change as workflow runs. Admission comes first, then a
JSON 400 for a malformed query.
- **Malformed `payload` tag:** shared NIP-98 verification now rejects a
malformed `payload` tag in every mode, including on bodyless requests.
That means a one-element `["payload"]`, an empty digest, or anything
other than 64 lowercase hex characters. Before this change, a bodyless
request ignored the tag.

**Auth header cardinality:** Enforce mode rejects requests with more
than one `Authorization` header with 403 `evidence rejected\n`.
DenyProtected returns 503 `authorization unavailable\n` before this
check (and before NIP-98 or assertion verification) runs. The
cardinality gate is enforced at the NIP-FI admission boundary
(`admit_nip_fi_http`), not via legacy auth boolean flags, so it applies
uniformly to all Enforce-mode surfaces including Git. Off mode passes
through the first header value unchanged, preserving pre-NIP-FI behavior
[FI-INV-15].

**Denial-class remapping:** In Enforce mode, NIP-98
extraction/verification failures from the closure are mapped to the
appropriate `DenialClass`: `MissingEvidence` when no `Authorization`
header is present, `EvidenceRejected` when the header is present but
invalid. Off mode propagates legacy responses unchanged [FI-INV-15]; for
example, the Git missing-Authorization and wrong-scheme 401s keep their
legacy bytes with no `content-type` header.

**JWKS refresh scheduling:** The refresh timer's `*last` checkpoint is
set after the fetch completes, not from the pre-fetch timestamp,
preventing interval drift under network latency. The loop is extracted
to `nip_fi_jwks_refresh_loop` for unit-testability.

**Log hygiene [NIP-FI.md:777-779]:** Issuer identity is never written to
logs or error messages. All startup/refresh diagnostics use an ordinal
index (`issuer_index = N`). Config parse errors for malformed issuers
use classify()/line/column codes rather than raw config values.

**Admin SPA document routes:** `/reports`, `/reports/{id}`, and
`/feedback` are exempt from the NIP-FI assertion guard only on the admin
host (host-qualified via `is_admin_host`). These paths are not added to
the broad `NIP_FI_EXEMPT_PREFIXES` list — tenant hosts remain fully
protected.

**Deny-map seam (`HttpDenyMap` trait):** signature is `(issuer, pubkey,
now)` matching `NipFiDenyMap::is_denied` from S4 (PR block#7265) so
integration is a one-liner. Stub is `AlwaysAdmitStubDenyMap` — named to
accurately describe stub-phase semantics (fail-open intentionally
pending S4).

**Route inventory:** every authenticated HTTP route is classified as
Protected or Exempt with justification. Documented in the F4 seam-test
comment in `bridge.rs`.

**Outer-guard tests:** `#[ignore = "requires Postgres"]` tests in
`bridge.rs` drive the full router (`build_router`) in Enforce mode with
valid NIP-98 but no `Nostr-Federated-Identity` header. These exercise
the **outer `nip_fi_assertion_guard` middleware** in `router.rs` — the
layer that fires before any handler runs. They prove the guard's exact
response bytes (401 `authentication required\n`, `text/plain;
charset=utf-8`, `WWW-Authenticate: Nostr`) for each protected route.
Per-handler `admit_nip_fi_http_on_state` wiring is proven separately by
the cardinality test and the media/settings/git handler-level suites.

## Seams

**S3 seam:** `nip_fi_config.rs` is duplicated (S3 not landed;
session-deadline parts stripped). Removed when S5 rebases onto S3.

**S4 seam:** `AlwaysAdmitStubDenyMap` until S4 provides the real map.
Integration is one line at the `admit_nip_fi_http_on_state` wrapper.

## Evidence

At head `ee338ea9c` (branch `hayt/nip-fi-http-enforcement`), hosted CI
run
[36167450853](https://github.com/block/buzz/actions/runs/36167450853):
- **Rust / Unit Tests** ([job
108179338458](https://github.com/block/buzz/actions/runs/36167450853/job/108179338458)):
passed. The `buzz-relay` `test-unit` step ran 171 tests with 171 passes.
Its PASS lines include all 57 non-ignored tests this PR adds to the
relay: `nip_fi_http::tests` (25), `nip_fi_config::tests` (8),
`router::tests` (8), `api::parse_query_tests` (5),
`api::git::transport::off_mode_precedence_tests` (5),
`tests::jwks_refresh_*` (3), and `composition_tests` (3). The same job
ran the `buzz-auth` suite, which includes
`nip_fi::jwks::tests::key_set_serves_live_snapshot_under_reader_and_refresh_overlap`.
- **Rust / Rust Lint**: passed.
- **Relay and PostgreSQL / Backend Integration (relay e2e)**: passed.
- **PostgreSQL Domain / PostgreSQL Tests** ([job
108183303436](https://github.com/block/buzz/actions/runs/36167450853/job/108183303436)):
passed. 542 tests run, 542 passed.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Signed-off-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary
Fix the upload/edit smoke-test race observed on main at
`6410e685a80d42db0645fadc9bbe710559ef911e`.

- Hold the mock upload at the Tauri IPC boundary and release it
explicitly after checking edit rejection, rather than assuming menu
interaction finishes within one second.
- Wait for the Radix menu to unmount: edit dispatch happens in
`onCloseAutoFocus`, after the click. Without that wait, the negative
assertion can pass before the edit callback actually runs.
- Keep the existing attachment-preservation and subsequent
successful-edit assertions. No production code or CI coverage is
changed.

## Evidence and scope
- [Main
failure](https://github.com/block/buzz/actions/runs/36189213149/job/108250341388):
`opening edit during an immediate photo upload preserves the draft`,
including an upload-progress timeout on retry.
- Investigated from block#7791, which changes only `docs/nips/NIP-AR.md`. Its
PostgreSQL failures and video-menu timeout are separate; PostgreSQL
passed on this main run, and the video spec passed locally. This PR does
not claim to fix those failures or the separate pointer-interception
failure seen in the main job.
- block#7843 already addresses unrelated runtime suites being selected for
docs-only PRs.

## Validation
- Full desktop `pnpm test`: 6,676 Node tests + 92 jsdom tests passed.
- Upload/edit regression repeated 5 times: passed. Holding the upload
without waiting for menu teardown failed all 5 runs, demonstrating the
second race.
- Complete file-attachment and video-attachment Playwright specs: 30
passed.
- Pre-push desktop lint, typecheck, full desktop tests, and file-size
gate passed at `46e3d4392e5b56dcb74ea864aa19aa10287f27d3`.
- `just ci` attempted: initial formatting issue corrected; second run
exceeded the 5-minute local limit during Tauri clippy. Not claiming full
repository CI green.

## Review / human verification
Self-reviewed the diff against the test contract; no production behavior
changes. Draft pending human verification and CI.

To verify: activate Hermit, then `cd desktop && pnpm test:e2e:smoke
file-attachment.spec.ts --grep "opening edit during" --repeat-each=5`.
Expect all five runs to pass, retaining the upload in the draft and
allowing edit only after upload release.

Originating conversation:
buzz://message?channel=aa7f2b48-d367-4ca1-be09-116a2eb45b99&id=1b68585ba4483a87f5a453fd7bbdd11d434e332dcb4aff8acfe1f5d2601b6561

Signed-off-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz>
Co-authored-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz>
A documentation-only update to block#7809 selected desktop builds and E2E
tests because path detection compared an old PR base SHA with GitHub's
newer synthetic merge commit. The [failing run's path-detection
log](https://github.com/block/buzz/actions/runs/35789957773/job/106955810630)
includes four unrelated desktop files from `main`; `VISION_MOBILE.md`
did not match a runtime filter.

Use GitHub's PR file list for pull requests so unrelated changes in the
synthetic merge commit cannot select runtime suites. The existing
directory filters are unchanged: root and docs/ Markdown do not select
runtime suites, while Markdown under runtime directories still selects
its affected suites. Mixed code/documentation changes retain normal
coverage. Always-on security, policy, and source-contract checks and
full push-to-main coverage are unchanged.

Added regression coverage runs the pinned paths-filter action against
real fixture repositories, including a synthetic merge containing
unrelated upstream desktop code. All 35 selection scenarios and 26
required-check scenarios pass; the new regressions failed before the
fix. Existing required-context isolation, file-size policy, and
security-review contract checks pass, as do script lint and workflow
syntax validation. Full workflow lint reports the same two pre-existing
shell-quoting findings in the untouched dead-token guard. A desktop E2E
build was run to diagnose the unrelated required smoke failure.

Related: block#7809 (incident, unchanged) and block#5756 (shared-input path
coverage, separate scope). No matching issue found.

PRs with at least 3,000 changed files now select every runtime suite,
avoiding GitHub's PR-file-list ceiling. Boundary tests cover 2,999,
3,000, and 3,001 files, including a runtime file omitted beyond the API
cap; disabling the safeguard makes the latter two regressions fail.

Required checks now run and fail when path selection fails, is
cancelled, or is skipped. Regression coverage exercises the workflow
conditions and shell checks for all 13 required wrappers, plus an
API-denial case against the pinned action. Mutating either the
scheduling guard or the result check makes all 13 failure regressions
fail.

The required mention-settings smoke test expected a pin after explicitly
disabling automatic mentions. Waiting for its old avatar to exit
reproduced the CI failure 3/3; the test now checks that subsequent
mentions remain manual and retains outgoing recipient-tag assertions.
Corrected browser regression: 20/20 repeated runs; related picker unit
tests: 18/18. No desktop production behavior changed. The always()
requirement is now pinned in required-wrapper regression tests; deleting
it fails all 26 gate cases.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
The relay can use a configurable HTTP(S) gateway delivery URL and signs
the same URL it sends. The gateway verifies NIP-98 against the received
request URL, including authority, port, path and query. App Attest,
scoped grants, replay checks and delivery quotas are unchanged.

Forwarding proxies must preserve Host and supply the original request
scheme through X-Forwarded-Proto. The NIP relay-delivery wording now
defines the configured HTTP(S) URL as the signed URL and labels the
public URL as an example.

<!-- buzz-review-completed -->

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
## Summary

Adds a short, proposal-only **NIP-AR: Channel Artifacts** in
`docs/nips/NIP-AR.md`, as a simpler alternative to the closed block#7771.

- Defines complete-snapshot artifact events with stable identity and
atomic, `prev`-based revisions.
- Uses channel access and posting permissions for reads, edits, moves,
and writer-authorized deletion.
- Keeps client-defined content opaque to the relay, with exact tag
filtering over current accessible state and bounded queries.
- Covers durable source removal on moves, terminal deletion, and safe
client fallbacks without defining task/project schemas or changing
application behavior.

### Remaining design work

This is a compact draft, not a complete implementation-ready wire
contract. Concrete query request/pagination/error/limit-discovery
formats, removal/replay wire details, and numeric query limits remain
follow-up work. Prefer extending shared WebSocket `REQ` / HTTP `/query`
surfaces rather than adding plugin-specific endpoints. No database
layout or scale guarantees are proposed.

### Related issue

Closest prior proposal: block#7771 (closed). Searched existing PRs and issues
for channel artifacts; no matching issue found.

### Testing

- Verified the committed document is byte-for-byte identical to the
reviewed local draft.
- Parsed the example JSON and its embedded JSON content; checked unique
two-element example tags.
- Reviewed the entire one-file diff and ran `git diff --check`.
- Commit and push hooks passed (including DCO sign-off and differential
file-size checks).
- Full application CI was not run locally: this changes only a Markdown
proposal, with no implementation or UI changes.

---------

Signed-off-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz>
Signed-off-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz>
Signed-off-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz>
Co-authored-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz>
Co-authored-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz>
Co-authored-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz>
We are introducing a companion service to the relay that needs pubkey
event mentions for pubkeys it manages. This PR introduces an outbox for
pubkey mentions that will be sent to these "operator listener" services.

## Summary

Deliver mentions of registered pubkeys to deployment-global operator
listeners.

- Configure listeners with `BUZZ_OPERATOR_LISTENERS`; listeners manage
target pubkeys through NIP-98-authenticated `POST`/`DELETE
/operator/listener/pubkeys` requests.
- During supported event ingestion, match `p` tags to registrations and
insert one outbox row per listener in the event transaction.
- A background worker claims up to 10 rows, sends NIP-98-signed
notifications, and retries transient failures with exponential backoff
for up to 9 attempts (idle polling backs off from 250 ms to 2 s).
- Outbox rows are ineligible for delivery after 15 minutes and are
reaped every 5 minutes. Registered pubkeys live for 30 days;
re-registering renews them, and a daily reaper removes expired
registrations.
- Keep persistence app-managed: no database triggers, functions, foreign
keys, or separate match queue.

## Changed behavior

Listener outbox rows are enqueued inside the shared event-insert and
thread-metadata transactions; enqueue failure rolls back the event.
`event_mentions` behavior is unchanged: ordinary event/reaction mention
indexing remains post-commit best effort with warning-only failures.

## Testing

- `cargo fmt --all`
- `cargo test -p buzz-db --lib`
- `cargo test -p buzz-relay --lib
config::tests::operator_listener_routes`
- `cargo check -p buzz-db -p buzz-relay`
## Summary

- Keep non-listener-mention event writes on the plain insert path
without opening a transaction.
- Preserve the transaction around listener mention event insertion and
outbox enqueue.

## Validation

- `cargo fmt --all -- --check` (Hermit)
- Tests not run.

Follow-up to Baxen’s review of block#7793, point 1.

Signed-off-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz>
Co-authored-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz>
)

Addresses point 3 of [previous
comment](block#7793 (review))
"3. Fail fast on a bad BUZZ_OPERATOR_LISTENERS (config.rs:857)"

## Summary
- Propagate malformed `BUZZ_OPERATOR_LISTENERS` values from
`Config::from_env` as `ConfigError`, including invalid UTF-8.
- Update startup config tests to assert returned errors and restore the
original admin warning capture helper, removing the shared helper
introduced only for listener log assertions.
- Cover valid and absent listener values through `Config::from_env`.

## Context
Follow-up to the review of block#7793 specifically [reverting some config
logging
parts](https://github.com/block/buzz/pull/7793/changes#diff-eaa0eeb209ff1ad6212ffa56d3a983bb732858647c9fb227ef7483a3363b5a71).
Invalid listener configuration currently logs an error and silently
disables listener delivery.

## Verification
- `cargo fmt --all -- --check` passed with Hermit.

Signed-off-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz>
Co-authored-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz>
…#7912)

This removes implementation references and implementation status from
`docs/nips/NIP-FI.md` so the spec holds normative text only. It drops
the Blossom compliance note, the known-gap notes, crate/file paths and
PR numbers, roadmap "supersedes" sentences, doc-history lines, and the
design-rationale note on session-only vs deny-until-TTL (its restart
re-push SHOULD remains stated normatively elsewhere).

Two wording changes carry normative weight: `PUT /media/upload` is
listed as a plain kind-24242 upload route instead of a temporary alias,
and the JWKS fetcher's SSRF controls become a MUST instead of a
description.

It doesn't depend on any enforcement PR and can merge at any time.

Related: block#7265

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
- `upstreamSnapshot` is now 97e1f6a, the exact upstream commit this branch
  merges.
- The migrationLedger maps upstream 0041-0065 to fork 0046-0070, with each
  file's SHA-384.
- Paths that upstream now ships are dropped and recorded per delta as
  `upstreamPresentAtSync`: D0, D8's harness-management spec, D12's
  respondToAllowlist/RespondToField, D13's push.rs.
- New re-port paths are classified, and a `kiingo-migration-ledger` delta
  is added.
- Exact budget amendment `upstream-sync-97e1f6a0`. Modified upstream files
  go from 262 to 277 and production-source files from 153 to 156. Changed
  lines (10588 to 9577) and hunks (1167 to 1163) ratchet down. The forbidden
  business-logic budget stays zero.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com>
Comment thread .github/workflows/_ci-clients.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
Brings in mobile agent guest-access management (#197) and the TestFlight
hosted-agent build flag (#200).

- The Settings guest-access section hook is re-ported onto upstream's
  restructured settings page, next to the hosted-agents row.
- lucide_icons_flutter is gone upstream (block#8109), so the new guest-access
  settings pages use BuzzIcons (Tabler). Four glyphs that BuzzIcons does not
  name (ban, calendar, calendar-month, history) are added to the fork-owned
  GuestAccessIcons. keyRound maps to BuzzIcons.key.
- The inventory takes main's new guest-access settings delta. Budgets are
  unchanged (277 / 156 / 9577 / 1163).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com>
…go/buzz

Kiingo/buzz runs no GitHub-hosted jobs (Spot-only policy); this upstream
job ran on a hosted runner in the sync PR's CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com>
… limits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com>

@RossHartmann RossHartmann left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Review from C (push pipeline owner) of 0333f34: re-port of APNs token auth onto the single-application config, plus the relay guest-kind push path. The re-port looks correct. There are two must-dos outside this PR, listed under "Cutover items in Kiingo/kiingo" below.

I read the source and rendered the chart. I ran no tests.

Gateway: matches the #189 design

  • ApplicationConfig.apns_auth replaces the certificate path.
  • parse_auth keeps the certificate default and requires all three token variables. Key and team ids must be ten uppercase characters.
  • main.rs builds the token or certificate transport.
  • apns.rs on the branch still has the provider-token signer, the bearer header, and the ExpiredProviderToken → retry path. Every other 403 stays a configuration fault.
  • The BUZZ_PUSH_APNS_{AUTH,CERT_PATH,KEY_PATH,KEY_ID,TEAM_ID} names fit upstream, which already used BUZZ_PUSH_APNS_CERT_PATH. 👍
  • Dropping the two fork fixes is right:
    • The template now renders maxGrantLifetimeSeconds | int64 | quote.
    • The gateway no longer takes a public delivery URL, so the host relaxation has nothing to apply to. The audiences stay the fixed push.buzz.xyz constants.
  • App Attest defaults to production when BUZZ_PUSH_APP_ATTEST_ENVIRONMENT is unset, which is what the TestFlight build uses.

Chart: rendered with helm 4.3.1 against Kiingo's draft values

  • The schema's if/then/else requires apnsToken in token mode and apnsCert otherwise.
  • Token mode renders BUZZ_PUSH_APNS_AUTH=token, KEY_PATH=/run/buzz/apns/auth-key.p8, the quoted key and team ids, and mounts only auth-key.p8 (0400). It renders no certificate env or mount.
  • required also catches the empty-string defaults of keyId and teamId.
  • Minor, optional: apnsToken.keyId and teamId allow "" in the schema. The template catches that, but the schema could make them ^[A-Z0-9]{10}$ under the then branch so helm lint reports it earlier.

Relay: guest-kind push looks right

  • PUSH_KINDS (descriptor) and buzz_db::PUSH_MATCH_KINDS (app-code enqueue) both list 9/40002/45001/45003/46040/46042. push_match_kinds_match_advertised_push_kinds pins them equal, now that the fork's 0045 SQL trigger is retired.
  • The enqueue runs from after_admitted_insert. Db::insert_event sets the producer, and channel_id is None there, so global, h-less 46040/46042 events are staged.
  • handlers/event.rs: the live fan-out filters RESULT_GATED_KINDS, which includes 46040–46042, through reader_authorized_for_event. Only the p-tagged owner gets them, even through a kindless ids:[…] subscription. That matches the pull paths.
  • push_runtime has the owner-only wake test, guest_approval_request_wakes_only_the_addressed_owner.
  • The relay keeps BUZZ_PUSH_GATEWAY_DELIVERY_URL, so prod-values stays valid.

Cutover items in Kiingo/kiingo

  1. push-gateway-values.yaml must switch to application.* in the same cutover.
    • The sync chart's schema has profiles: false, so the current values fail with at '/profiles': false schema.
    • My draft is ready and not pushed: Kiingo/kiingo e7f1ac2e8a09 on branch buzz-push-gateway-single-application-20261016. It moves to application.{appAttestAppId, apnsTopic, apnsEnvironment, apnsAuth: token, apnsToken} and drops publicDeliveryUrl.
  2. buzz-production.yml also needs the change in that draft.
    • "Require the push gateway sources" greps the chart for BUZZ_PUSH_DOGFOOD_APNS_KEY_PATH. With this branch it would fail the deploy. The draft accepts either name.
    • The host-consistency check required publicDeliveryUrl: https://push.chat.kiingo.com/... in the gateway values. It now reads the relay's BUZZ_PUSH_GATEWAY_DELIVERY_URL in prod-values.yaml.
  3. Leases and clients.
    • Upstream 0060 refuses to migrate while push leases are active. Relay push is enabled in prod now.
    • TestFlight build 1 speaks nip-pl, so keep enrollment to Ross and testers. Expect either to clear leases before the cutover or to cut over before anyone enrolls.
    • Phones need a build from this branch (buzz-push-v1) after the cutover.

Comment thread crates/buzz-acp/src/git.rs Dismissed
Comment thread crates/buzz-acp/src/git.rs Dismissed
Comment thread crates/buzz-media/src/upload.rs Dismissed
Comment thread crates/buzz-media/src/upload.rs Dismissed
Ross Hartmann and others added 2 commits October 17, 2026 12:14
…am-60de1db0

Conflict resolutions:
- iOS Xcode 27 picks (#199) and the block#7745 backport (#208) are upstream
  commits already in the 97e1f6a snapshot: take the sync side for every
  #199 path and drop both drop/upstream-present deltas.
- deny.toml: keep upstream's ignores and main's RUSTSEC-2026-0367/0368/0369;
  the sync tree pulls zip 8.6.0 via mesh-llm v0.76.2, re-audited: plugin
  manager and autoupdate still never call extract() and write plain files.
- Message Details (#205): desktop composes the host ref with the right-click
  root ref; mobile adds Details to upstream's grouped action sheet (the
  popover was removed upstream in block#8280) with a Tabler info-circle icon;
  retired-notice filtering passes agentPubkeys at upstream's new
  formatTimeline call sites.
- Hosted agents (#201): Lucide icons -> BuzzIcons (lucide removed upstream).
- Inventory: sync inventory plus main's new deltas (remote signer, guest
  alert Review, Message Details) and the merged hosted-agents delta paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the #209 media cache delta and records the merged budgets
(modified upstream files 282, production-source files 158, lines 9674,
hunks 1183) as amendment sync-merge-main-20261017, pending approval
above Ross's 2026-10-16 limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RossHartmann added a commit that referenced this pull request Oct 17, 2026
Kiingo's Spot runner admission only runs a job whose workflow file matches
main (public_workflow_not_current), so the upstream sync PR (#202), which
replaces ci.yml with upstream's split _ci-*.yml, cannot run its own CI.
Land the workflow files and their CI-support scripts, hermit pins and
nextest config first, unchanged from the sync branch: Kiingo's Spot-only,
no-test policy is re-applied and upstream-only GitHub-hosted jobs are
skipped on Kiingo/buzz. Inventory: a temporary drop/upstream-present delta
and a four-file budget amendment, both removed by the sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com>
@RossHartmann

Copy link
Copy Markdown
Author

Taken over by C (2026-10-17), from the mobile-parity agent, which has moved to the Assistant reliability fixes. This is still a draft. Don't merge until main gives the go.

Refreshed against Kiingo/buzz main f5e365e (#199, #201, #203 and #205–#209):

- buzz-acp relay.rs: keep upstream's RestClient::relay_self() and add
  #210's cached relay_self_key()/read_relay_self_key() beside it.
- ingest.rs: #210's kind:9007 dm refusal precedes upstream's
  visibility/labels parsing.
- side_effects.rs: #210's DM-visibility guard on upstream's
  get_channel_for_event_write read.
- channel_members.rs: #210's DM add guard calls get_active_role_tx with
  tx.conn() (upstream's AdmittedTx).
- Inventory: #210's channel_members delta and Ross's 2026-10-17 ceilings
  (600/400/40000/4000); the pending sync-merge amendment is dropped.

cargo check (acp, relay, db, all targets) passes; no tests run locally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RossHartmann added a commit that referenced this pull request Oct 17, 2026
Kiingo's Spot runner admission only runs a job whose workflow file matches
main (public_workflow_not_current), so the upstream sync PR (#202), which
replaces ci.yml with upstream's split _ci-*.yml, cannot run its own CI.
Land the workflow files and their CI-support scripts, hermit pins and
nextest config first, unchanged from the sync branch: Kiingo's Spot-only,
no-test policy is re-applied and upstream-only GitHub-hosted jobs are
skipped on Kiingo/buzz. Inventory: a temporary drop/upstream-present delta
and a four-file budget amendment, both removed by the sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com>
@RossHartmann

Copy link
Copy Markdown
Author

Merged main 765e8e8 (#210) in, now at 192627b. Resolutions:

cargo check passes on acp, relay and db across all targets (no tests run locally). The PENDING amendment is dropped in favour of Ross's ceilings: measured 283 files, 159 production files, 10027 lines and 1201 hunks, against ceilings of 600, 400, 40000 and 4000. The migration ledger is ok.

RossHartmann added a commit that referenced this pull request Oct 17, 2026
Lands #202's workflow files, CI-support scripts, hermit pins and nextest config on main, so the sync PR's workflows match main and Kiingo's Spot runner admission can run its CI. Content is upstream's from the sync branch, with Kiingo's Spot-only, no-test policy and hosted-job skips. Its own changed workflows could not run here by design (admission public_workflow_not_current at Set up runner); unchanged workflows (CodeQL, Desktop Release Candidate) passed. Inventory: temporary drop/upstream-present delta upstream-ci-prelanding, which #202 removes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Workflow files already match; the temporary upstream-ci-prelanding
delta is dropped because the sync moves upstreamSnapshot past those
files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RossHartmann

Copy link
Copy Markdown
Author

CodeQL triage for the upstream sync (C, 2026-10-17, approved by main). Alerts were checked mechanically against upstream block/buzz 97e1f6a0828d. An alert was dismissed only if:

  • its file at the alert commit is byte-identical to upstream, and
  • its line is test-only (a test path, a *_tests.rs file, or inside a #[cfg(test)] mod), or it is one of the two reviewed exceptions below.

The next upstream sync should re-check that these are still upstream-only.

Dismissed (52)

# reason severity rule location
143 false positive high js/xss-through-dom desktop/src/features/agents/ui/AgentCreationPreview.tsx:743
156 used in tests high rust/cleartext-logging crates/buzz-acp/src/git.rs:557
157 used in tests high rust/cleartext-logging crates/buzz-acp/src/git.rs:615
158 used in tests high rust/cleartext-logging crates/buzz-agent/src/databricks_tests.rs:762
39 used in tests high rust/cleartext-logging crates/buzz-auth/src/nip98_replay.rs:173
40 used in tests high rust/cleartext-logging crates/buzz-auth/src/nip98_replay.rs:205
100 used in tests high rust/cleartext-logging crates/buzz-auth/src/rate_limit.rs:281
42 used in tests high rust/cleartext-logging crates/buzz-auth/src/rate_limit.rs:325
43 used in tests high rust/cleartext-logging crates/buzz-backend-kubernetes/src/observe.rs:562
159 used in tests high rust/cleartext-logging crates/buzz-cli/src/commands/repo_default_branch.rs:301
44 used in tests high rust/cleartext-logging crates/buzz-core/src/pairing/qr.rs:508
77 used in tests critical rust/hard-coded-cryptographic-value crates/buzz-core/src/pairing/session.rs:1145
160 used in tests high rust/cleartext-logging crates/buzz-db/src/store/artifact_postgres_tests.rs:572
144 used in tests high rust/cleartext-logging crates/buzz-db/src/store/personal_read/arrival_postgres_tests.rs:70
145 used in tests high rust/cleartext-logging crates/buzz-db/src/store/personal_read/participation_postgres_tests.rs:111
146 used in tests high rust/cleartext-logging crates/buzz-db/src/store/personal_read/threads_postgres_tests.rs:86
147 used in tests high rust/cleartext-logging crates/buzz-db/src/store/personal_read/threads_postgres_tests.rs:182
148 used in tests high rust/cleartext-logging crates/buzz-media/src/upload.rs:962
149 used in tests high rust/cleartext-logging crates/buzz-media/src/upload.rs:1083
66 used in tests critical rust/hard-coded-cryptographic-value crates/buzz-pair-relay/tests/integration.rs:213
67 used in tests critical rust/hard-coded-cryptographic-value crates/buzz-pair-relay/tests/integration.rs:250
68 used in tests critical rust/hard-coded-cryptographic-value crates/buzz-pair-relay/tests/integration.rs:265
69 used in tests critical rust/hard-coded-cryptographic-value crates/buzz-pair-relay/tests/integration.rs:398
70 used in tests critical rust/hard-coded-cryptographic-value crates/buzz-pair-relay/tests/integration.rs:433
162 used in tests critical rust/hard-coded-cryptographic-value crates/buzz-pair-relay/tests/integration.rs:647
72 used in tests critical rust/hard-coded-cryptographic-value crates/buzz-pair-relay/tests/integration.rs:753
73 used in tests critical rust/hard-coded-cryptographic-value crates/buzz-pair-relay/tests/integration.rs:801
74 used in tests critical rust/hard-coded-cryptographic-value crates/buzz-pair-relay/tests/integration.rs:848
75 used in tests critical rust/hard-coded-cryptographic-value crates/buzz-pair-relay/tests/integration.rs:1085
163 used in tests critical rust/hard-coded-cryptographic-value crates/buzz-push-gateway/src/app_attest/testing.rs:331
150 used in tests high rust/cleartext-logging crates/buzz-relay/src/audio/handler.rs:7705
151 used in tests high rust/cleartext-logging crates/buzz-relay/src/audio/handler.rs:9964
152 used in tests high rust/cleartext-logging crates/buzz-relay/src/audio/handler.rs:10286
153 used in tests high rust/cleartext-logging crates/buzz-relay/src/audio/handler.rs:10459
154 used in tests high rust/cleartext-logging crates/buzz-relay/src/audio/handler.rs:10464
155 used in tests high rust/cleartext-logging crates/buzz-relay/src/audio/handler.rs:11138
47 used in tests high rust/cleartext-logging crates/buzz-test-client/tests/conformance_multitenant.rs:334
48 used in tests high rust/cleartext-logging desktop/src-tauri/src/commands/agent_discovery/install_report_redaction_tests.rs:95
49 used in tests high rust/cleartext-logging desktop/src-tauri/src/commands/agent_discovery/install_report_redaction_tests.rs:150
50 used in tests high rust/cleartext-logging desktop/src-tauri/src/commands/agent_discovery/install_report_redaction_tests.rs:239
51 used in tests high rust/cleartext-logging desktop/src-tauri/src/commands/agent_discovery/install_report_redaction_tests.rs:271
52 used in tests high rust/cleartext-logging desktop/src-tauri/src/commands/agent_discovery/install_report_redaction_tests.rs:296
53 used in tests high rust/cleartext-logging desktop/src-tauri/src/commands/agent_discovery/install_report_redaction_tests.rs:336
54 used in tests high rust/cleartext-logging desktop/src-tauri/src/commands/agent_discovery/install_report_redaction_tests.rs:363
57 used in tests high rust/cleartext-logging desktop/src-tauri/src/commands/agent_discovery/install_report_redaction_tests.rs:381
76 used in tests critical rust/hard-coded-cryptographic-value desktop/src-tauri/src/commands/identity_key_backup_tests.rs:39
140 used in tests high js/incomplete-url-substring-sanitization desktop/src/app/contentSecurityPolicy.test.mjs:144
141 used in tests high js/incomplete-sanitization desktop/src/features/workflows/ui/workflowFormTypes.test.mjs:371
142 used in tests high js/incomplete-multi-character-sanitization desktop/src/shared/ui/markdownMentionDisplay.test.mjs:35
58 won't fix high rust/cleartext-logging crates/buzz-pairing-cli/src/main.rs:131
59 won't fix high rust/cleartext-logging crates/buzz-pairing-cli/src/main.rs:267
60 won't fix high rust/cleartext-logging crates/buzz-pairing-cli/src/main.rs:297

Reviewed exceptions:

  • won't fix: buzz-pairing-cli prints the pairing URI and SAS with println! to the local developer's terminal. The crate has no tracing or telemetry.
  • false positive: AgentCreationPreview's <img src={avatarUrl}> is a React-escaped attribute.

Still open (17), routed to main

# severity rule location why it stays open
161 high rust/cleartext-logging crates/buzz-mesh-smoke/examples/mesh_admission_smoke.rs:178 upstream-identical, but not provably test code
139 medium actions/missing-workflow-permissions .github/workflows/ci.yml:285 fork-modified file
138 medium actions/missing-workflow-permissions .github/workflows/ci.yml:275 fork-modified file
137 medium actions/missing-workflow-permissions .github/workflows/ci.yml:265 fork-modified file
136 medium actions/missing-workflow-permissions .github/workflows/ci.yml:254 fork-modified file
135 medium actions/missing-workflow-permissions .github/workflows/ci.yml:244 fork-modified file
134 medium actions/missing-workflow-permissions .github/workflows/ci.yml:233 fork-modified file
133 medium actions/missing-workflow-permissions .github/workflows/ci.yml:222 fork-modified file
132 medium actions/missing-workflow-permissions .github/workflows/ci.yml:212 fork-modified file
131 medium actions/missing-workflow-permissions .github/workflows/ci.yml:202 fork-modified file
130 medium actions/missing-workflow-permissions .github/workflows/ci.yml:192 fork-modified file
129 medium actions/missing-workflow-permissions .github/workflows/_ci-clients.yml:157 fork-modified file
64 high rust/cleartext-logging crates/buzz-backend-kubernetes/src/reconcile.rs:274 upstream-identical, but not provably test code
63 high rust/cleartext-logging crates/buzz-backend-kubernetes/src/reconcile.rs:270 upstream-identical, but not provably test code
62 high rust/cleartext-logging crates/buzz-backend-kubernetes/src/reconcile.rs:218 upstream-identical, but not provably test code
56 high rust/cleartext-logging desktop/src-tauri/src/secret_store.rs:1072 upstream-identical, but not provably test code
55 high rust/cleartext-logging desktop/src-tauri/src/extensions/identity_rotation/crypto.rs:417 fork-modified file

…bus advisories

- Workflows now match main again (M-4108 read-only token defaults).
- Inventory: classify docs/implementation-plans/M-4108-workflow-token-permissions.md
  with the other fork implementation docs (#211 did not add it).
- deny.toml: ignore RUSTSEC-2026-0370/0371/0372 (zvariant, zbus_names,
  zvariant_derive unmaintained; merged into zbus 6), transitive via zbus 5.19
  for the desktop Linux keyring with no compatible upgrade yet. These were
  published today and failed #202's Dependency policy step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirrors the temporary Windows Rust guard (M-4109); workflows stay
byte-identical to main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RossHartmann

Copy link
Copy Markdown
Author

Merging with Desktop Windows Build treated as non-blocking, per main's decision on 2026-10-18.

Every other required check is green at ff08b24. That includes Rust Lint, the cross-compiles, Web, Mobile, Desktop JS and Tauri core, Security, the helm and push-gateway charts, Docker, Desktop RC, and CodeQL.

Desktop Windows Build has been queued since 01:28Z because the Windows Spot pool hit its failed-purchase budget: 2 of 2 in a 6-hour window, after two cold VMs were dropped. One went when #212 removed the Rust demand; the other went in the 01:05Z controller deploy. No Windows VM can be bought until 06:42:51Z. This is not an admission regression. Ross has no Windows users and has paused Windows work. Tracked in M-4109.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.