Skip to content

Commit dcaf589

Browse files
author
Duncan
committed
Merge remote-tracking branch 'origin/main' into duncan/nip-fi-verifier-contracts
* origin/main: Remove public relay signing key fallback (#6729) docs(nest): make commit attribution policy-neutral (#6707) fix(desktop-messages): preserve inline agent mentions with persistent addressing (#6793) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2 parents 58a2603 + ee6ca5f commit dcaf589

22 files changed

Lines changed: 595 additions & 96 deletions

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ TYPESENSE_URL=http://localhost:8108
5151
BUZZ_BIND_ADDR=0.0.0.0:3000
5252
# Public WebSocket URL — used in NIP-42 auth challenges
5353
RELAY_URL=ws://localhost:3000
54-
# Stable relay signing key. Set this in dev if you want REST-created forum posts
55-
# to keep resolving to the original author across relay restarts.
54+
# Stable relay signing key (required). `just bootstrap` generates a random key in
55+
# the gitignored .env file. Preserve that value across restarts and backups.
5656
# BUZZ_RELAY_PRIVATE_KEY=<32-byte hex private key>
5757
# Optional: path to the web UI dist directory. When set, the relay serves
5858
# the web frontend at / for browser requests. Leave unset for local dev

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,7 @@ jobs:
529529
REDIS_URL=redis://localhost:6379 \
530530
RELAY_URL=ws://localhost:3000 \
531531
BUZZ_BIND_ADDR=0.0.0.0:3000 \
532+
BUZZ_RELAY_PRIVATE_KEY="$(openssl rand -hex 32)" \
532533
BUZZ_REQUIRE_AUTH_TOKEN=false \
533534
BUZZ_RECONCILE_CHANNELS=true \
534535
BUZZ_RATE_LIMIT_HUMAN_MESSAGES_PER_MIN=100000 \
@@ -714,6 +715,7 @@ jobs:
714715
REDIS_URL=redis://localhost:6379 \
715716
RELAY_URL=ws://localhost:3000 \
716717
BUZZ_BIND_ADDR=0.0.0.0:3000 \
718+
BUZZ_RELAY_PRIVATE_KEY="$(openssl rand -hex 32)" \
717719
BUZZ_REQUIRE_AUTH_TOKEN=false \
718720
BUZZ_RECONCILE_CHANNELS=true \
719721
BUZZ_GIT_PROBE_WRITERS=8 \

Justfile

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ bootstrap:
4343
cp .env.example .env
4444
echo "Created .env from .env.example — review it before running just dev."
4545
fi
46+
./scripts/ensure-local-relay-key.sh .env
4647
4748
# Start Docker services, run migrations, install desktop deps
4849
setup: bootstrap
@@ -307,6 +308,7 @@ test:
307308
test-unit:
308309
#!/usr/bin/env bash
309310
set -euo pipefail
311+
./scripts/test-ensure-local-relay-key.sh
310312
if command -v cargo-nextest &>/dev/null; then
311313
cargo nextest run -p buzz-core -p buzz-auth --lib
312314
# buzz-auth NIP-FI verifier integration + doctests. The verifier's
@@ -434,13 +436,19 @@ relay: bootstrap _ensure-migrations
434436
#!/usr/bin/env bash
435437
set -euo pipefail
436438
export PATH="{{justfile_directory()}}/bin:$PATH"
439+
set -o allexport
440+
source .env
441+
set +o allexport
437442
cargo run -p buzz-relay
438443
439444
# Start the relay with the built web UI served from it
440445
relay-web: bootstrap _ensure-migrations
441446
#!/usr/bin/env bash
442447
set -euo pipefail
443448
export PATH="{{justfile_directory()}}/bin:$PATH"
449+
set -o allexport
450+
source .env
451+
set +o allexport
444452
[[ -d node_modules ]] || pnpm install
445453
pnpm -C web build
446454
BUZZ_WEB_DIR=./web/dist cargo run -p buzz-relay
@@ -450,6 +458,9 @@ admin: bootstrap _ensure-migrations
450458
#!/usr/bin/env bash
451459
set -euo pipefail
452460
export PATH="{{justfile_directory()}}/bin:$PATH"
461+
set -o allexport
462+
source .env
463+
set +o allexport
453464
[[ -d node_modules ]] || pnpm install
454465
pnpm -C admin-web build
455466
export BUZZ_ADMIN_HOST="${BUZZ_ADMIN_HOST:-admin.localhost:3000}"
@@ -470,7 +481,12 @@ admin-check: fmt-check
470481
pnpm -C admin-web exec playwright test
471482

472483
# Start the relay server in release mode
473-
relay-release: _ensure-migrations
484+
relay-release: bootstrap _ensure-migrations
485+
#!/usr/bin/env bash
486+
set -euo pipefail
487+
set -o allexport
488+
source .env
489+
set +o allexport
474490
cargo run -p buzz-relay --release
475491
476492
@@ -479,6 +495,9 @@ dev *ARGS: bootstrap _ensure-sidecar-stubs _ensure-migrations
479495
#!/usr/bin/env bash
480496
set -euo pipefail
481497
export PATH="{{justfile_directory()}}/bin:$PATH"
498+
set -o allexport
499+
source .env
500+
set +o allexport
482501
bind_addr="${BUZZ_BIND_ADDR:-0.0.0.0:3000}"
483502
relay_port="${bind_addr##*:}"; [[ -n "$relay_port" ]] || relay_port=3000
484503
health_port="${BUZZ_HEALTH_PORT:-8080}"

TESTING.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ CLI signs every request with NIP-98, so you don't need `nak` or hand-rolled
2929

3030
```bash
3131
. ./bin/activate-hermit # activate pinned toolchain
32-
cp .env.example .env # one-time
32+
just bootstrap # create .env and its stable relay key once
3333
just setup # start Docker services, run migrations
3434
```
3535

@@ -70,6 +70,9 @@ Rebuild after any code change — the steps below use the release binaries.
7070
In a separate terminal (it runs in the foreground):
7171

7272
```bash
73+
set -o allexport
74+
source .env # includes the key generated by just bootstrap
75+
set +o allexport
7376
buzz-relay # release binary from step 2, serves ws://localhost:3000
7477
# alternatives:
7578
# cargo run --release -p buzz-relay # rebuild + run in release
@@ -89,9 +92,9 @@ curl -s http://localhost:8080/_readiness # → {"status":"ready"}
8992
> `BUZZ_HEALTH_PORT`) so K8s probes bypass auth middleware. The main app
9093
> port also exposes `/health` for convenience.
9194
92-
The relay starts in dev mode (`BUZZ_REQUIRE_AUTH_TOKEN=false`). The startup
93-
log emits a WARN about this — that's expected for local testing. See the env
94-
vars table at the bottom if you need to lock it down.
95+
The relay starts in dev mode (`BUZZ_REQUIRE_AUTH_TOKEN=false`) with the stable
96+
relay identity generated in `.env`. See the env vars table at the bottom if
97+
you need to lock it down.
9598

9699
> **Already running Buzz Desktop (or another relay) on `:3000` / `:8080` /
97100
> `:9102`?** Buzz binds three ports — main, health, metrics — and any of

crates/buzz-relay/src/main.rs

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ fn buzz_auto_migrate_enabled(value: Option<&str>) -> bool {
3535
})
3636
}
3737

38+
fn relay_keypair_from_config(relay_private_key: Option<&str>) -> anyhow::Result<nostr::Keys> {
39+
let hex = relay_private_key.ok_or_else(|| {
40+
anyhow::anyhow!(
41+
"BUZZ_RELAY_PRIVATE_KEY must be set. Run `just bootstrap` for local \
42+
development or configure a stable 32-byte hex private key."
43+
)
44+
})?;
45+
nostr::Keys::parse(hex).map_err(|e| anyhow::anyhow!("invalid BUZZ_RELAY_PRIVATE_KEY: {e}"))
46+
}
47+
3848
/// Controls how many per-community gauge series the usage poller emits.
3949
///
4050
/// Datadog cost is proportional to the number of unique time-series. With ~25
@@ -143,6 +153,7 @@ async fn main() -> anyhow::Result<()> {
143153
error!("Invalid configuration: {e}");
144154
anyhow::anyhow!("Configuration error: {e}")
145155
})?;
156+
let relay_keypair = relay_keypair_from_config(config.relay_private_key.as_deref())?;
146157
info!(
147158
bind_addr = %config.bind_addr,
148159
relay_url = %config.relay_url,
@@ -422,29 +433,6 @@ async fn main() -> anyhow::Result<()> {
422433
let workflow_config = buzz_workflow::WorkflowConfig::default();
423434
let workflow_engine = Arc::new(WorkflowEngine::new(db.clone(), workflow_config));
424435

425-
let relay_keypair = if let Some(hex) = &config.relay_private_key {
426-
nostr::Keys::parse(hex)
427-
.map_err(|e| anyhow::anyhow!("invalid BUZZ_RELAY_PRIVATE_KEY: {e}"))?
428-
} else if !config.require_auth_token {
429-
// Dev mode: use a deterministic keypair so addressable events (kind:39000/39001/39002)
430-
// replace correctly across restarts. Without this, each restart generates a new pubkey
431-
// and replace_addressable_event inserts duplicates instead of replacing.
432-
const DEV_RELAY_PRIVKEY: &str =
433-
"0000000000000000000000000000000000000000000000000000000000000001";
434-
let keys = nostr::Keys::parse(DEV_RELAY_PRIVKEY).expect("hardcoded dev key is valid");
435-
tracing::warn!(
436-
pubkey = %keys.public_key().to_hex(),
437-
"Using hardcoded dev relay keypair (BUZZ_REQUIRE_AUTH_TOKEN=false). \
438-
Set BUZZ_RELAY_PRIVATE_KEY for production."
439-
);
440-
keys
441-
} else {
442-
panic!(
443-
"BUZZ_RELAY_PRIVATE_KEY must be set when BUZZ_REQUIRE_AUTH_TOKEN=true. \
444-
A stable relay identity is required for production."
445-
);
446-
};
447-
448436
config
449437
.media
450438
.validate()
@@ -2037,8 +2025,8 @@ mod tests {
20372025

20382026
use super::{
20392027
buzz_auto_migrate_enabled, dropped_in_memory_keys, idle_timeout_secs,
2040-
refresh_legacy_active_gauge_recency, run_periodic_until_cancelled, EmissionScope,
2041-
InMemoryMetricKey,
2028+
refresh_legacy_active_gauge_recency, relay_keypair_from_config,
2029+
run_periodic_until_cancelled, EmissionScope, InMemoryMetricKey,
20422030
};
20432031
use metrics::GaugeFn;
20442032
use metrics_util::{
@@ -2086,6 +2074,23 @@ mod tests {
20862074
assert!(buzz_auto_migrate_enabled(Some("on")));
20872075
}
20882076

2077+
#[test]
2078+
fn configured_relay_identity_is_preserved() {
2079+
let configured = nostr::Keys::generate();
2080+
let secret = configured.secret_key().to_secret_hex();
2081+
2082+
let selected = relay_keypair_from_config(Some(&secret)).expect("configured key");
2083+
2084+
assert_eq!(selected.public_key(), configured.public_key());
2085+
}
2086+
2087+
#[test]
2088+
fn missing_relay_identity_is_rejected() {
2089+
let result = relay_keypair_from_config(None);
2090+
2091+
assert!(result.is_err());
2092+
}
2093+
20892094
#[test]
20902095
fn test_emission_scope_off_disallows_every_community() {
20912096
assert!(EmissionScope::All.allows(&Uuid::new_v4()));

desktop/src-tauri/src/managed_agents/nest.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const BUZZ_CLI_SKILL_MD: &str = include_str!("nest_skill.md");
4848
/// Template content version for AGENTS.md static content (above managed markers).
4949
/// Bump this when changing `nest_agents.md` to trigger refresh on existing installs.
5050
/// Version 1 is implicitly "before this mechanism existed" (no version file).
51-
const NEST_AGENTS_VERSION: u32 = 4;
51+
const NEST_AGENTS_VERSION: u32 = 5;
5252

5353
/// Template content version for SKILL.md.
5454
/// Bump this when changing `nest_skill.md` to trigger refresh on existing installs.

desktop/src-tauri/src/managed_agents/nest/tests.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ fn nest_skill_contains_safe_mention_workflow() {
4141
assert!(BUZZ_CLI_SKILL_MD.contains("never changes membership automatically"));
4242
}
4343

44+
#[test]
45+
fn nest_agents_template_separates_commit_attribution_claims() {
46+
assert_eq!(AGENTS_MD.matches("## Git Commit Attribution").count(), 1);
47+
assert!(AGENTS_MD.contains(
48+
"Git authorship, co-authorship, DCO sign-off, and cryptographic signing are separate claims"
49+
));
50+
assert!(AGENTS_MD
51+
.contains("Request, approval, review, or accountability alone is not co-authorship"));
52+
assert!(AGENTS_MD.contains("A sign-off is not an approval marker"));
53+
assert!(AGENTS_MD.contains("Never use another person's signing key"));
54+
assert!(AGENTS_MD.contains("inspect every outgoing commit against the actual upstream or base"));
55+
assert!(AGENTS_MD.contains("An agent-owned repository may use the agent as author"));
56+
assert!(!AGENTS_MD.contains("every commit MUST include a `Signed-off-by`"));
57+
}
58+
4459
#[test]
4560
fn ensure_nest_creates_all_dirs_and_agents_md() {
4661
let tmp = tempfile::tempdir().unwrap();
@@ -431,6 +446,34 @@ fn refresh_agents_md_writes_version_file() {
431446
assert_eq!(version.trim(), NEST_AGENTS_VERSION.to_string());
432447
}
433448

449+
#[test]
450+
fn refresh_agents_md_upgrades_attribution_and_preserves_owned_content() {
451+
let tmp = tempfile::tempdir().unwrap();
452+
let root = tmp.path().join(".buzz");
453+
ensure_nest_at(&root).unwrap();
454+
455+
let agents_md = root.join("AGENTS.md");
456+
fs::write(
457+
&agents_md,
458+
"# Buzz Nest\n\n## Git Commit Identity\n\n\
459+
- **Human sign-off (required):** every commit MUST include a `Signed-off-by`.\n\n\
460+
<!-- BEGIN BUZZ MANAGED — regenerated automatically, do not edit below -->\n\
461+
## Active Agents\n\n| Name | Persona | How to address |\n\
462+
|------|---------|----------------|\n| Kit | Builder | @Kit |\n\
463+
<!-- END BUZZ MANAGED -->\n\n## Local Notes\n\nKeep me.\n",
464+
)
465+
.unwrap();
466+
fs::write(root.join(".nest-agents-version"), "4\n").unwrap();
467+
468+
ensure_nest_at(&root).unwrap();
469+
470+
let content = fs::read_to_string(&agents_md).unwrap();
471+
assert_eq!(content.matches("## Git Commit Attribution").count(), 1);
472+
assert!(!content.contains("**Human sign-off (required):**"));
473+
assert!(content.contains("| Kit | Builder | @Kit |"));
474+
assert!(content.contains("## Local Notes\n\nKeep me."));
475+
}
476+
434477
#[test]
435478
fn refresh_skill_md_writes_version_file() {
436479
let tmp = tempfile::tempdir().unwrap();

desktop/src-tauri/src/managed_agents/nest_agents.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,18 @@ created: 2026-01-15
4444
- **`.scratch/` is disposable** — don't rely on it across sessions
4545
- **Stay on task** — only stage files relevant to your current work
4646

47-
## Git Commit Identity
47+
## Git Commit Attribution
4848

49-
The human operator signs off for accountability.
49+
Git authorship, co-authorship, DCO sign-off, and cryptographic signing are separate claims. Follow repository-local rules and the authorizing human's explicit directions; do not infer attribution from repository ownership or from who requested, approved, or reviewed the work.
5050

51-
- **Human sign-off (required):** every commit MUST include a `Signed-off-by` trailer for the human operator who is responsible for the agent's work. Add via `git commit --trailer "Signed-off-by: Human Name <human@email>"`. One blank line must separate trailers from the commit body.
52-
- **Human credit (`Co-authored-by`):** every commit MUST also include a `Co-authored-by` trailer for the same human operator, with identical name and email to the `Signed-off-by` line. GitHub parses `Co-authored-by` for contribution-graph credit; `Signed-off-by` alone does not grant it. Add via `git commit --trailer "Co-authored-by: Human Name <human@email>"`. Place `Co-authored-by` before `Signed-off-by` in the trailer block.
53-
- **Discovering the human's identity:** read `git config user.name` and `git config user.email` from the working repository. These reflect the human operator's configured identity for that repo (which may differ from their global config). Use these exact values for both trailers. Do NOT hardcode, guess, or prompt for the email — the repo config is the source of truth. If `git config user.email` returns empty, STOP and ask the human operator for their name and email before committing.
54-
- **Signing:** if the agent has a registered signing key, sign commits. If not, commits will land unverified — this is acceptable until agent SSH keys are provisioned. Do NOT use the human's signing key.
55-
- **Verify before pushing:** `git log -1` should show the human's `Signed-off-by` trailer.
51+
- **Author:** use the person or agent required by the applicable policy. If no policy specifies an author, use the identity that actually authored the change.
52+
- **Co-authors:** add `Co-authored-by` only for other people or agents who materially authored the change. Request, approval, review, or accountability alone is not co-authorship.
53+
- **DCO:** add `Signed-off-by` only when repository policy requires that identity's DCO certification. A sign-off is not an approval marker.
54+
- **Identity:** resolve required identities from trusted local configuration or explicit verified direction; never hard-code or guess them. A managed runtime may make effective `git config user.*` values identify the agent. Stop and ask if a required identity cannot be established.
55+
- **Signing:** use only the signing key configured for the committing identity. Never use another person's signing key.
56+
- **Verify before pushing:** inspect every outgoing commit against the actual upstream or base and confirm its attribution matches the applicable policy.
57+
58+
A repository may require an accountable human as author and the implementing agent as co-author. An agent-owned repository may use the agent as author and require no human trailer. In both cases, repository-local policy controls.
5659

5760
<!-- BEGIN BUZZ MANAGED — regenerated automatically, do not edit below -->
5861
## Active Agents

desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,25 @@ globalThis.localStorage = {
99

1010
const preference = await import("./autoPinMentionedAgentsPreference.ts");
1111

12-
test("defaults missing and invalid values to keeping mentioned agents pinned", () => {
13-
assert.equal(preference.parseKeepMentionedAgentsPinned(null), true);
14-
assert.equal(preference.parseKeepMentionedAgentsPinned("invalid"), true);
12+
test("defaults missing and invalid values to one-time agent mentions", () => {
13+
assert.equal(preference.parseKeepMentionedAgentsPinned(null), false);
14+
assert.equal(preference.parseKeepMentionedAgentsPinned("invalid"), false);
1515
assert.equal(preference.parseKeepMentionedAgentsPinned("true"), true);
1616
assert.equal(preference.parseKeepMentionedAgentsPinned("false"), false);
1717
});
1818

1919
test("persists changes to the post-mention pinning preference", () => {
20-
preference.setKeepMentionedAgentsPinned(false);
21-
assert.equal(preference.getKeepMentionedAgentsPinned(), false);
20+
preference.setKeepMentionedAgentsPinned(true);
21+
assert.equal(preference.getKeepMentionedAgentsPinned(), true);
2222
assert.equal(
2323
values.get(preference.KEEP_MENTIONED_AGENTS_PINNED_STORAGE_KEY),
24-
"false",
24+
"true",
2525
);
2626

27-
preference.setKeepMentionedAgentsPinned(true);
28-
assert.equal(preference.getKeepMentionedAgentsPinned(), true);
27+
preference.setKeepMentionedAgentsPinned(false);
28+
assert.equal(preference.getKeepMentionedAgentsPinned(), false);
2929
assert.equal(
3030
values.get(preference.KEEP_MENTIONED_AGENTS_PINNED_STORAGE_KEY),
31-
"true",
31+
"false",
3232
);
3333
});

desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from "react";
22

33
export const KEEP_MENTIONED_AGENTS_PINNED_STORAGE_KEY =
44
"buzz.messages.keepMentionedAgentsPinned";
5-
export const DEFAULT_KEEP_MENTIONED_AGENTS_PINNED = true;
5+
export const DEFAULT_KEEP_MENTIONED_AGENTS_PINNED = false;
66

77
const listeners = new Set<() => void>();
88
let keepMentionedAgentsPinned = readStoredPreference();

0 commit comments

Comments
 (0)