Skip to content
Closed
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
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -274,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 @@ -297,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
48 changes: 47 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,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 @@ -319,6 +319,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 @@ -685,6 +689,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 Down Expand Up @@ -1121,6 +1137,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
85 changes: 85 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,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
45 changes: 45 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = [
"crates/buzz-relay",
"crates/buzz-core",
"crates/buzz-conformance",
"crates/buzz-nip-fi-seal-test",
"crates/buzz-push-gateway",
"crates/buzz-db",
"crates/buzz-pubsub",
Expand Down
4 changes: 4 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,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
15 changes: 15 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ just test # unit + integration (starts Docker if needed)
cargo test -p buzz-test-client -- --ignored
```

### Review-Proven Test Standards

Mined from the last 25 PRs' review threads (see Review-Proven Rules in
[AGENTS.md](AGENTS.md)); this is the test-quality rule reviewers litigated
most:

**Regression tests must bind the production seam and be falsifiable.**
A guard whose removal doesn't fail any test protects nothing — mutations
survived the full mobile suite twice (PRs #6996, #7013). Don't bind a
regression test to a test-only helper instead of the production code
path (PR #7013). Give pure predicates a table test over the full input
combination space (PR #6807). Scope Playwright locators — unscoped
`getByText` in a required smoke test is a strict-mode flake (PR #6980).
(PRs #6807, #6980, #6996, #7013)

---

## Live Local Relay
Expand Down
22 changes: 18 additions & 4 deletions crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,31 @@ Controls which authors' events the harness forwards to the agent. Events from di
| `anyone` | Forward all events (no author filtering). |
| `nobody` | Drop all inbound events. Agent only acts on heartbeat prompts. |

Relay-signed workflow messages delegate to their recorded owner only when they
explicitly target this agent with authenticated workflow-mention provenance.
The owner tag means that owner scheduled the workflow; it does not claim that
the owner authored every word after template rendering. ACP verifies the
provenance against the relay's NIP-11 `self` key, then evaluates the owner under
the same author policy as ordinary messages. Legacy workflow messages and
workflow output without an explicit agent mention remain attributed to the relay
signer. `nobody` remains absolute.

The gate applies to **all** inbound events — @mentions, DMs, thread replies, and any event delivered by the relay. Owner control commands are checked **before** the gate, so the owner can still manage the harness regardless of mode:

| Command | Effect |
|---------|--------|
| `!shutdown` | Gracefully exits the harness. |
| `!cancel` | Cancels the current in-flight turn for that channel, if any. |
| `!rotate` | Rotates the ACP session for that channel. If a turn is in-flight, it is cancelled and the channel session is invalidated when the task returns; otherwise the cached idle session is invalidated immediately. The next queued/received event starts a fresh session. |
| `!cancel` | Cancels the current in-flight turn for the command's resolved session scope, if any. |
| `!rotate` | Rotates the ACP session for the command's resolved session scope. If a turn is in flight, it is cancelled and that scoped session is invalidated when the task returns; otherwise the cached scoped session is invalidated immediately. The next queued/received event in that scope starts a fresh session. |

Under the default `channel` policy, a session scope is the whole channel, so these commands retain their channel-wide behavior. Under the `thread` policy, post the command as a reply in the target thread so `!cancel` or `!rotate` affects only that thread. DMs remain one conversation scope. `!cancel` is a no-op when its scope is idle.

Use `!cancel` to stop only the current turn; it is a no-op when the channel is idle. Use `!rotate` when you want the next turn in the channel to start from a fresh ACP session, even if the channel is currently idle.
Owner control commands must be kind:9 stream messages from the owner, must have body exactly `!cancel`, `!rotate`, or `!shutdown` after trimming, and must mention this agent with a separate `p` tag. They are consumed by the harness instead of being forwarded to the agent. An inline `@Name` changes the body and does not match. With the Buzz CLI, target a thread while preserving the exact command body by passing the mention separately:

Owner control commands must be kind:9 stream messages from the owner, must mention this agent with a `p` tag, and are consumed by the harness instead of being forwarded to the agent.
```bash
buzz messages send --channel <channel-id> --reply-to <thread-root-id> \
--mention <agent-pubkey> --content '!cancel'
```

> **Note:** The default mode is `owner-only`. Agents without a registered `agent_owner_pubkey` will not respond to any events until the owner is resolved. Set `--respond-to anyone` to disable the gate entirely.

Expand Down
6 changes: 0 additions & 6 deletions crates/buzz-acp/src/base_prompt.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
You are operating inside the Buzz platform — a Nostr-based messaging platform for human-agent collaboration. The buzz-acp harness routes channel events to your session.

## Session Model

You are one per-channel session of your agent identity — not the only copy. Each channel gets its own independent conversation context, and multiple sessions of the same agent may be active in different channels at the same time. Sessions share your core memory, your workspace on disk, and the relay. They do NOT share conversation context, in-progress reasoning, or in-context task state.

When a human references work "you" are doing in another channel, that work belongs to a different session of you. Unless the human asks you to take it over or coordinate it from this channel, leave execution with the owning session — answer from what you can verify (core memory, workspace files, relay messages) and assume the owning session has it handled.

## Buzz CLI

The `buzz` CLI is your primary interface. Auth env vars: `BUZZ_RELAY_URL`, `BUZZ_PRIVATE_KEY`, `BUZZ_AUTH_TAG`. Exit codes: 0 ok, 1 user error, 2 network, 3 auth, 4 other. Output is structured JSON.
Expand Down
Loading
Loading