Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ REDIS_URL=redis://localhost:6379
# READ_DATABASE_URL is set, reader (default 50).
# BUZZ_DB_POOL_SIZE=50

# Writer-session Postgres timeouts for buzz-db-backed pools and the relay audit
# pool, all in milliseconds; 0 disables. The separately deployed push gateway
# owns its own database and session policy and does not consume these knobs.
# lock_timeout: fail a statement that waits this long on any lock instead of
# parking behind a wedged holder (default 5000).
# BUZZ_DB_LOCK_TIMEOUT_MS=5000
# idle_in_transaction_session_timeout: reap sessions idle inside an open
# transaction — bounds how long a wedged client can hold locks (default 60000).
# BUZZ_DB_IDLE_TXN_TIMEOUT_MS=60000
# statement_timeout: cap any single statement's runtime. Off by default —
# startup migrations/backfills legitimately run long statements. Warning: a
# pathologically low value (e.g. 1) also times out connection setup and can
# prevent any DB connection from establishing.
# BUZZ_DB_STATEMENT_TIMEOUT_MS=0

# -----------------------------------------------------------------------------
# Typesense (search)
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -259,6 +274,10 @@ RUST_LOG=buzz_relay=debug,buzz_datastore=info,buzz_db=debug,buzz_auth=debug,buzz
# app launch while keeping the current identity and relay data.
# VITE_BUZZ_FORCE_FRESH_ONBOARDING=true

# Protected internal builds only: selects the module graph that contains the
# default-off Bestie experiment. Official OSS builds must leave this unset.
# VITE_BUZZ_BESTIE=1

# ── Subscription & filtering ─────────────────────────────────────────────────
# Subscribe mode: "mentions" (default), "all", or "config" (rule-based).
# BUZZ_ACP_SUBSCRIBE=mentions
Expand All @@ -282,6 +301,14 @@ RUST_LOG=buzz_relay=debug,buzz_datastore=info,buzz_db=debug,buzz_auth=debug,buzz
# Set to true to process the agent's own messages (default: ignore self).
# BUZZ_ACP_NO_IGNORE_SELF=false

# ── Session scoping ──────────────────────────────────────────────────────────
# How ACP provider sessions are scoped in channels: "channel" (default) or
# "thread". "channel" keeps one provider session per channel (legacy). "thread"
# gives each canonical channel thread its own isolated provider session; direct
# messages stay conversation-scoped either way. Ships as "channel" so thread
# scoping can be canaried and rolled back without code changes.
# BUZZ_ACP_SESSION_POLICY=channel

# ── Context ──────────────────────────────────────────────────────────────────
# Max context messages fetched for thread replies and DMs (0–100). 0 = disabled.
# BUZZ_ACP_CONTEXT_MESSAGE_LIMIT=12
Expand Down
74 changes: 73 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ jobs:
name: Desktop
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [changes, desktop-core, desktop-smoke-e2e]
needs: [changes, desktop-core, desktop-smoke-e2e, desktop-windows-build]
if: always() && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')
permissions:
contents: read
Expand All @@ -323,6 +323,10 @@ jobs:
echo "Desktop Smoke E2E shards finished with: ${{ needs.desktop-smoke-e2e.result }}"
exit 1
fi
if [ "${{ needs.desktop-windows-build.result }}" != "success" ]; then
echo "Desktop Windows Build finished with: ${{ needs.desktop-windows-build.result }}"
exit 1
fi
echo "Desktop jobs passed"

desktop-e2e-relay:
Expand Down Expand Up @@ -384,6 +388,7 @@ jobs:
-p buzz-relay \
-p buzz-test-client \
--lib \
--bin buzz-relay \
--test e2e_event_reminder \
--archive-file target/ci/backend-integration-tests.tar.zst
- name: Save relay artifacts cache
Expand Down Expand Up @@ -688,6 +693,18 @@ jobs:
VALUES ('00000000-0000-4000-8000-00000000c0de', 'localhost:3000')
ON CONFLICT (lower(host)) DO NOTHING
;"
- name: Workflow message provenance tests
# The relay's workflow_sink suite is not selected by the infra-free
# unit job. Run both its pure tests and ignored PostgreSQL tests here so
# authored-template provenance cannot regress behind a green CI build.
run: |
cargo nextest run \
--archive-file target/ci/backend-integration-tests.tar.zst \
-E 'package(buzz-relay) and test(/workflow_sink/)' \
--run-ignored all
env:
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
TEST_DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
- name: Replaceable persistence PostgreSQL tests
# Transaction, concurrency, and mention-index coverage for the
# replaceable-event store seam. These tests require real Postgres and
Expand All @@ -713,6 +730,31 @@ jobs:
env:
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
TEST_DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
- name: Writer session timeout guardrails
run: |
cargo nextest run \
--archive-file target/ci/backend-integration-tests.tar.zst \
-E 'package(buzz-db) and test(session_timeouts_install_through_db_new_and_bound_lock_waits)' \
--run-ignored ignored-only
env:
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
TEST_DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
- name: Audit writer session timeout guardrails
run: |
cargo nextest run \
--archive-file target/ci/backend-integration-tests.tar.zst \
-E 'package(buzz-relay) and test(audit_writer_pool_installs_timeouts_and_bounds_advisory_lock_waits)' \
--run-ignored ignored-only
env:
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
- name: Audit worker lock-timeout recovery
run: |
cargo nextest run \
--archive-file target/ci/backend-integration-tests.tar.zst \
-E 'package(buzz-relay) and test(audit_worker_retries_lock_timeout_until_original_entry_is_appended_once)' \
--run-ignored ignored-only
env:
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
- name: Start relay
run: |
chmod +x ./target/ci/buzz-relay
Expand Down Expand Up @@ -1107,6 +1149,36 @@ jobs:
-p git-credential-nostr \
-p git-sign-nostr

desktop-windows-build:
name: Desktop Windows Build
runs-on: windows-latest
timeout-minutes: 20
needs: [changes]
if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true'
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24.14.1
package-manager-cache: false
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
with:
version: 11.4.0
- name: Install desktop dependencies
shell: bash
run: pnpm install --frozen-lockfile
- name: Build both protected-feature selections
shell: pwsh
run: |
Remove-Item Env:VITE_BUZZ_BESTIE -ErrorAction SilentlyContinue
pnpm -C desktop build
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$env:VITE_BUZZ_BESTIE = "1"
pnpm -C desktop build
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

windows-rust:
name: Windows Rust (x86_64-pc-windows-msvc)
runs-on: windows-latest
Expand Down
57 changes: 55 additions & 2 deletions .github/workflows/codex-security-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ jobs:
if: needs.prepare-review.outputs.authorized == 'true'
runs-on: ubuntu-latest
environment: codex-review
timeout-minutes: 30
timeout-minutes: 40
concurrency:
group: codex-security-review-${{ needs.prepare-review.outputs.pr_number }}
cancel-in-progress: true
Expand All @@ -228,7 +228,7 @@ jobs:
REVIEW_REPOSITORY: review-target
REVIEW_DIFF_FILE: .git/codex-review.diff
outputs:
review_json: ${{ steps.run_codex.outputs.final-message }}
review_json: ${{ steps.salvage.outputs.review_json }}
steps:
- name: Checkout exact pull request head
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -287,6 +287,12 @@ jobs:
# action's local proxy rather than entering the Codex subprocess.
- name: Review pull request
id: run_codex
# Codex CLI ≥0.149.x can leave a PTY descendant holding inherited stdio
# after the turn completes, stalling the action indefinitely. The output
# file is written before the hang, so a timeout here wastes at most 30
# minutes instead of the full 40, and the salvage step recovers the result.
timeout-minutes: 30
continue-on-error: true
uses: openai/codex-action@86365089eb2b84e0a8fb0717b304f8bdcb13b20e # v1.12
env:
# Checkout and fetch are complete. Remove runner credentials from the
Expand All @@ -306,6 +312,8 @@ jobs:
safety-strategy: drop-sudo
permission-profile: ':read-only'
working-directory: ${{ github.workspace }}/${{ env.REVIEW_CONTEXT }}
# Written before the hang; salvaged below if the step times out.
output-file: ${{ runner.temp }}/codex-review.json
output-schema: |
{
"type": "object",
Expand Down Expand Up @@ -442,6 +450,51 @@ jobs:
assumptions. Review only the authorized PR range and ground every finding
in a changed hunk and a plausible failure or abuse path.

# Salvage the finished review whether the Codex step completed cleanly or
# timed out due to the PTY-shutdown hang. Prefer the action's final-message
# output (set on a clean exit); fall back to the output file written by the
# CLI before the hang. Fail the job only when neither source is available or
# the recovered JSON is not a valid review shape.
- name: Salvage review output
id: salvage
if: always()
env:
FINAL_MESSAGE: ${{ steps.run_codex.outputs.final-message }}
CODEX_OUTPUT_FILE: ${{ runner.temp }}/codex-review.json
run: |
json=""

# Prefer the action output set on a clean exit.
if [ -n "$FINAL_MESSAGE" ]; then
json="$FINAL_MESSAGE"
echo "source=action-output" >> "$GITHUB_STEP_SUMMARY"
elif [ -s "$CODEX_OUTPUT_FILE" ]; then
json="$(cat "$CODEX_OUTPUT_FILE")"
echo "source=output-file" >> "$GITHUB_STEP_SUMMARY"
else
echo "No review output from action or output file." >&2
exit 1
fi

# Minimal shape validation: non-empty JSON object with overall_risk.
if ! echo "$json" | python3 -c "
import sys, json
d = json.load(sys.stdin)
assert isinstance(d, dict), 'not an object'
assert 'overall_risk' in d, 'missing overall_risk'
"; then
echo "Review JSON failed shape validation." >&2
exit 1
fi

# Write as a multiline output (GitHub-safe delimiter).
EOF=$(dd if=/dev/urandom bs=15 count=1 2>/dev/null | base64)
{
echo "review_json<<${EOF}"
echo "$json"
echo "${EOF}"
} >> "$GITHUB_OUTPUT"

post-review:
name: Post Codex Security Review
needs: [prepare-review, security-review]
Expand Down
85 changes: 85 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,91 @@ Additional rules:

---

## Review-Proven Rules

These rules distill the recurring findings from the last 25 PRs' review
threads — 53% of substantive review findings were repeats of the clusters
below, and reviewed PRs averaged ~5 review rounds. A second, independent
mining pass over 71 agent-review rooms (303 findings, Aug 18–29) confirmed
the same clusters and measured how often authors actually fix each class
once flagged: test-seam binding and unbounded-resource findings were fixed
**100%** of the time, swallowed-error findings **90%**, stale-state races
**70%** — these are not style opinions, they are defects authors agree
with on sight. Apply the rules **before writing code**; each cites the
PRs where reviewers litigated it.

1. **Every caught failure must leave a durable retry record or propagate.**
Never catch-log-and-return-success (opt-out revocation permanently
abandoned, PR #6269), never convert a terminal failure into an
authoritative success/empty result (cold-history `error` → `success`
with `[]`, PR #7013), and never delete the durable journal an operation
depends on before its retry has actually succeeded (PR #6269). If a
partial failure can orphan committed state (installations, endpoints),
schedule its cleanup/renewal durably (PRs #6269, #6996, #7013).

2. **Fence async results by generation; clear derived metadata on every
removal path.** A completing in-flight probe or fetch must verify it is
still the newest before writing its result (stale login-shell probe
recached a false-negative PATH, PR #6904). Provenance/ownership metadata
attached to synthetic state must be updated or cleared on *all* paths
that remove or refresh that state — typed deletion, toolbar removal,
profile/name refresh; enumerate the paths and test each (PR #6956 burned
4 rounds on this one class). Backfill and live subscriptions must
overlap — a gap between a finite history REQ and the live subscription
silently drops events (PR #3995); a retired chunk must not keep a stale
scope fence (PR #6996). (PRs #3995, #6904, #6956, #6996)

3. **Regression tests must bind the production seam and be falsifiable.**
See "Review-Proven Test Standards" in [TESTING.md](TESTING.md) for the
full rule — in short: a guard whose removal doesn't fail any test
protects nothing; bind regression tests to the production code path,
not test-only helpers. (PRs #6807, #6980, #6996, #7013)

4. **Bound every resource, loop, and process tree.** Cap captured
output (unbounded discovery temp files exhausted disk and overran the
deadline, PR #6904). Containment failures are errors, not warnings — a
tolerated Job Object creation failure or a `setsid` escape leaks whole
process trees (PR #6904). Retry/re-subscribe loops need backoff and a
terminal state: a persistent failure must not self-amplify into an
unbounded refresh loop (PR #6996), and check zero-delay edge cases
(`remainingMs()==0` selected the wrong fallback window, PR #6996).
(PRs #6904, #6996)

5. **One user action = one atomic persist.** Implementing a single user
commit as N independent durable writes leaves torn state on partial
failure (theme "Set" as three independent notifier persists, PR #6944;
relay-commit vs. local-save recovery gap, PR #6269). Persist one
snapshot, or order the writes so every prefix is consistent and the
remainder is durably retried per rule 1. (PRs #6269, #6944)

6. **A guard that hides the only recovery affordance is a functional
failure.** Before adding a visibility predicate or state fence, ask:
if the state it assumes goes wrong, does the user still have a way
back? A fence that permanently suppresses "jump to latest" after a
bounded correction fails strands the user silently — two reviewers
flagged this independently (PR #6807).

7. **Audit assistive semantics on every new visual component.** The
agent-review lanes flagged accessibility defects on 44 findings across
the Aug 18–29 window — the second-largest cluster — and authors fixed
the concrete ones (duplicate VoiceOver stops on native controls,
actionable labels owned by two widgets at once, PR #6680; missing or
decorative-leaking semantics on new UI, PRs #6611, #6702, #6885, #6905,
#6908). New UI ships with: one owner per actionable label, no duplicate
screen-reader stops, and explicit semantics for every interactive
element. (PRs #6611, #6680, #6702, #6885, #6905, #6908, #6980)

8. **Every input modality is a first-class seam.** Keyboard, pointer, and
hotkey paths must not silently diverge: `Shift+Space` treated as plain
`Space` because the guard omitted `shiftKey` (PR #6862), keyboard
ownership not released on blur, modifier keys dropped on the non-mouse
path (PRs #5958, #6793, #6860, #6908, #7006). When adding an input
handler, enumerate the modalities that can reach it and test the
non-primary ones — that's where the defects were. (PRs #5958, #5972,
#6793, #6860, #6862, #6908, #7006)

---

## Key Patterns

**Nostr-first HTTP surface**: Buzz's primary API is NIP-29 over WebSocket. The relay also exposes a narrow HTTP surface: NIP-11/NIP-05 metadata, `POST /events`, `POST /query`, `POST /count`, workflow webhooks at `/hooks/{id}`, Blossom media, git smart HTTP, git policy hooks, and health probes. These HTTP paths all preserve the same host-derived community boundary.
Expand Down
9 changes: 9 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,15 @@ This prevents a race where a non-member receives live fan-out events from a priv

After registering, the REQ handler queries Postgres for stored events matching the filters (up to 500 per filter, hard cap). These are sent as `["EVENT", sub_id, event]` frames before `["EOSE", sub_id]`. New events arriving after EOSE are delivered via the fan-out path.

**Client consumption invariant.** A client rebuilding channel state must
open its live subscription before (or overlapping) the finite history
REQ — a gap between the last backfill page and live delivery silently
drops events and rebuilds stale state (PR #3995). When the relay sends a
terminal CLOSED, the subscription is removed server-side; any client-side
ownership tied to it (chunk/scope fences) must be released in the same
step, or live delivery stops permanently while the client believes it is
subscribed (PR #6996).

---

## 6. Crate Reference
Expand Down
5 changes: 5 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ check: fmt-check clippy desktop-check desktop-tauri-fmt-check desktop-tauri-clip
security-review-check:
node --check .github/scripts/codex-security-review.js
node --test .github/scripts/codex-security-review.test.js
actionlint .github/workflows/codex-security-review.yml

# Run the repository-wide differential file-size ratchet and its policy tests.
# The ratchet inspects only files changed from the merge base, so this stays
Expand Down Expand Up @@ -455,6 +456,10 @@ test-unit:
# disabled_mode_still_requires_the_correct_host / _a_matching_origin.
cargo nextest run -p buzz-relay --lib \
-E 'test(/^api::admin::/) - test(=api::admin::tests::disabled_mode_allows_unauthenticated_requests_on_the_admin_host) - test(=api::admin::tests::nip98_mode_unrostered_signer_does_not_consume_a_replay_slot)'
# ACP author-gate and queue tests protect the trust boundary between
# relay events and agent prompts. They are infra-free; ignored lifecycle
# tests remain excluded and run in their dedicated integration lanes.
cargo nextest run -p buzz-acp --lib
else
./scripts/run-tests.sh unit
fi
Expand Down
Loading
Loading