Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
18 changes: 18 additions & 0 deletions bin/anthropic-first-acp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -u

anthropic_file="${ANTHROPIC_CREDENTIALS_FILE:-$HOME/Downloads/jarvis-keys.env}"
openai_file="${EKO_CREDENTIALS_FILE:-$HOME/.config/eko/credentials.env}"

if [[ -r "$anthropic_file" ]]; then
anthropic_value=$(sed -n 's/^ANTHROPIC_API_KEY=//p' "$anthropic_file" | head -1)
[[ -n "$anthropic_value" ]] && export ANTHROPIC_API_KEY="$anthropic_value"
fi

export PATH="/home/mrmoe28/.npm-packages/bin:/home/mrmoe28/.local/bin:$PATH"
export CODEX_HOME="${CODEX_HOME:-$HOME/.buzz-codex}"
mkdir -p "$CODEX_HOME"

# Run Anthropic directly. Provider failover requires a multi-agent ACP change;
# switching executables inside one ACP stream is not protocol-safe.
exec claude-agent-acp
22 changes: 22 additions & 0 deletions bin/codex-acp-buzz
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail

# Keep Buzz's existing OPENAI_API_KEY untouched. Codex ACP needs the separate
# OpenAI credential managed by the local Eko tooling.
credentials_file="${EKO_CREDENTIALS_FILE:-$HOME/.config/eko/credentials.env}"
if [[ -r "$credentials_file" ]]; then
saved_openai_api_key="${OPENAI_API_KEY-}"
openai_value=$(sed -n 's/^OPENAI_API_KEY=//p' "$credentials_file" | head -1)
[[ -n "$openai_value" ]] && export OPENAI_API_KEY="$openai_value"
if [[ -z "${OPENAI_API_KEY-}" ]]; then
export OPENAI_API_KEY="$saved_openai_api_key"
fi
fi

# Buzz may launch agents in a restricted environment where the normal Codex
# state directory is read-only. Keep a separate writable state directory for
# this ACP child.
export CODEX_HOME="${CODEX_HOME:-$HOME/.buzz-codex}"
mkdir -p "$CODEX_HOME"

exec codex-acp "$@"
36 changes: 36 additions & 0 deletions bin/youtube-browser-mcp.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env node
import readline from "node:readline";
import { chromium } from "/home/mrmoe28/Downloads/eko-capture/node_modules/playwright/index.mjs";

let browser, page;
const tools = [
{ name: "browser_tabs", description: "List open browser tabs", inputSchema: { type:"object", properties:{} } },
{ name: "browser_navigate", description: "Navigate the active tab to a URL", inputSchema: { type:"object", properties:{ url:{type:"string"} }, required:["url"] } },
{ name: "browser_snapshot", description: "Read the active page URL, title, and visible text", inputSchema: { type:"object", properties:{} } },
{ name: "browser_click", description: "Click a visible element by accessible role/name or text", inputSchema: { type:"object", properties:{ text:{type:"string"}, role:{type:"string"} }, required:["text"] } },
{ name: "browser_type", description: "Type into an input or contenteditable element using real keyboard events", inputSchema: { type:"object", properties:{ selector:{type:"string"}, text:{type:"string"}, slowly:{type:"boolean"} }, required:["selector","text"] } },
{ name: "browser_evaluate", description: "Run a small JavaScript expression in the active page and return its JSON-safe result", inputSchema: { type:"object", properties:{ expression:{type:"string"} }, required:["expression"] } },
{ name: "browser_upload", description: "Set a file on a file input", inputSchema: { type:"object", properties:{ selector:{type:"string"}, path:{type:"string"} }, required:["path"] } },
{ name: "browser_screenshot", description: "Save a screenshot of the active page", inputSchema: { type:"object", properties:{ path:{type:"string"} }, required:["path"] } }
];
async function active() {
if (!browser) browser = await chromium.connectOverCDP(process.env.CDP_URL || "http://127.0.0.1:9222");
const ctx = browser.contexts()[0];
page ||= ctx.pages().find(p => /^https?:/.test(p.url())) || await ctx.newPage();
return page;
}
const result = (text) => ({ content:[{type:"text",text}] });
async function call(name, a) {
if (name === "browser_tabs") { await active(); const pages=browser.contexts()[0].pages(); const tabs=[]; for (let i=0;i<pages.length;i++) tabs.push({index:i,url:pages[i].url(),title:await pages[i].title().catch(()=>"")}); return result(JSON.stringify(tabs,null,2)); }
const p = await active();
if (name === "browser_navigate") { await p.goto(a.url,{waitUntil:"domcontentloaded"}); return result(`Navigated to ${p.url()}`); }
if (name === "browser_snapshot") return result(JSON.stringify({url:p.url(),title:await p.title(),text:(await p.locator("body").innerText()).slice(0,20000)},null,2));
if (name === "browser_click") { const loc=a.role ? p.getByRole(a.role,{name:a.text}).first() : p.getByText(a.text,{exact:true}).first(); await loc.click({timeout:10000}); return result(`Clicked ${a.text}`); }
if (name === "browser_type") { const loc=a.selector ? p.locator(a.selector).first() : p.locator("textarea,input,[contenteditable=true]").first(); await loc.click(); await loc.press(process.platform === "darwin" ? "Meta+A" : "Control+A"); await loc.press("Backspace"); await loc.pressSequentially(a.text, {delay:a.slowly ? 35 : 0}); return result("Typed input"); }
if (name === "browser_evaluate") { const value = await p.evaluate((expression) => { const fn = new Function(`return (${expression})`); return fn(); }, a.expression); return result(JSON.stringify(value ?? null)); }
if (name === "browser_upload") { await p.locator(a.selector || "input[type=file]").first().setInputFiles(a.path); return result(`Uploaded ${a.path}`); }
if (name === "browser_screenshot") { await p.screenshot({path:a.path,fullPage:false}); return result(`Saved screenshot to ${a.path}`); }
throw new Error(`Unknown tool ${name}`);
}
const rl=readline.createInterface({input:process.stdin});
rl.on("line", async line=>{ try { const q=JSON.parse(line); let r; if(q.method==="initialize") r={protocolVersion:"2024-11-05",capabilities:{tools:{}},serverInfo:{name:"buzz-youtube-browser",version:"0.1.0"}}; else if(q.method==="notifications/initialized") return; else if(q.method==="tools/list") r={tools}; else if(q.method==="tools/call") r=await call(q.params.name,q.params.arguments||{}); else r={}; process.stdout.write(JSON.stringify({jsonrpc:"2.0",id:q.id,result:r})+"\n"); } catch(e){ process.stdout.write(JSON.stringify({jsonrpc:"2.0",id:JSON.parse(line).id,error:{code:-32603,message:e.message}})+"\n"); }});
144 changes: 144 additions & 0 deletions buzz-app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
# Buzz on-demand launcher.
#
# Opening the app: brings up the self-host stack (Postgres/Redis/MinIO + friends
# via docker compose), applies migrations/seed if needed, starts the buzz-relay
# release binary, then launches the packaged Buzz desktop app.
#
# Closing the app: stops the relay and tears the Docker stack down (named
# volumes are preserved, so your data/schema survive between sessions).
set -uo pipefail

REPO="/home/mrmoe28/Desktop/repos/buzz"
cd "$REPO" || { echo "[Buzz] repo not found at $REPO"; exit 1; }

# Pinned toolchain (just / cargo / node / pnpm) — needed for migrations + seed.
. ./bin/activate-hermit >/dev/null 2>&1 || true
export PATH="$REPO/bin:$PATH"

# Load .env into this script's own environment so every subprocess we launch
# directly (buzz-relay release binary) sees real credentials, not the
# hardcoded buzz_dev/localhost fallbacks baked into config.rs defaults.
if [[ -r "$REPO/.env" ]]; then
set -a
# shellcheck disable=SC1091
source "$REPO/.env"
set +a
fi

# Installed desktop app binary (resolved at package-install time).
APP_BIN="/usr/bin/buzz-desktop"

# WebKitGTK on NVIDIA segfaults inside its GPU compositing path (crash traced to
# libwebkit2gtk-4.1 / libjavascriptcoregtk). Disabling DMABUF alone is not enough;
# compositing mode must also be off or the app SIGSEGVs on launch every time.
export WEBKIT_DISABLE_COMPOSITING_MODE=1
export WEBKIT_DISABLE_DMABUF_RENDERER=1

RELAY_PID=""
ACP_PID=""
CHROME_PID=""
teardown() {
echo
echo "[Buzz] Shutting down stack..."
[[ -n "$ACP_PID" ]] && kill "$ACP_PID" 2>/dev/null || true
[[ -n "$CHROME_PID" ]] && kill "$CHROME_PID" 2>/dev/null || true
[[ -n "$RELAY_PID" ]] && kill "$RELAY_PID" 2>/dev/null || true
pkill -f "target/release/buzz-relay" 2>/dev/null || true
docker compose down 2>/dev/null || true # keeps named volumes (data persists)
echo "[Buzz] Stopped."
}
trap teardown EXIT INT TERM

echo "[Buzz] Bringing up self-host services + migrations..."
if ! just _ensure-migrations; then
echo "[Buzz] Service/migration setup failed — see output above." >&2
exit 1
fi

# Clear any stale relay still holding the relay/health/metrics ports (e.g. a
# leftover from a crashed session) so we always run our own fresh relay and
# never bind against a stale process. Kill by exact PID (pkill -f self-matches).
for port in 3000 8080 9102; do
holder=$(ss -ltnp 2>/dev/null | grep -E ":${port} " | grep -oP 'pid=\K[0-9]+' | head -1)
if [ -n "$holder" ]; then
echo "[Buzz] Clearing stale process $holder on :$port"
kill -9 "$holder" 2>/dev/null || true
fi
done

echo "[Buzz] Starting relay (ws://localhost:3000)..."
nice -n 5 ./target/release/buzz-relay > "$REPO/relay-runtime.log" 2>&1 &
RELAY_PID=$!

echo -n "[Buzz] Waiting for relay"
for _ in $(seq 1 40); do
if ! kill -0 "$RELAY_PID" 2>/dev/null; then
echo " FAILED — relay exited early. See relay-runtime.log:" >&2
tail -20 "$REPO/relay-runtime.log" >&2
exit 1
fi
# Ready only when OUR relay pid owns :3000 (not some other process).
if ss -ltnp 2>/dev/null | grep -E ":3000 " | grep -q "pid=${RELAY_PID}"; then
echo " ready"; break
fi
echo -n "."; sleep 1
done

echo "[Buzz] Ensuring Chrome CDP bridge is available..."
if ! curl -fsS --max-time 2 http://127.0.0.1:9222/json/version >/dev/null 2>&1; then
CHROME_BIN="$(command -v google-chrome || command -v google-chrome-stable || command -v chromium || true)"
if [[ -n "$CHROME_BIN" ]]; then
"$CHROME_BIN" \
--user-data-dir="$HOME/.eko-chrome" \
--remote-debugging-port=9222 \
--no-first-run --no-default-browser-check \
>/tmp/buzz-chrome-cdp.log 2>&1 &
CHROME_PID=$!
for _ in $(seq 1 20); do
curl -fsS --max-time 1 http://127.0.0.1:9222/json/version >/dev/null 2>&1 && break
sleep 1
done
if ! curl -fsS --max-time 2 http://127.0.0.1:9222/json/version >/dev/null 2>&1; then
echo "[Buzz] Chrome CDP failed to start — see /tmp/buzz-chrome-cdp.log" >&2
CHROME_PID=""
fi
else
echo "[Buzz] Chrome/Chromium not found; browser automation unavailable." >&2
fi
fi

echo "[Buzz] Starting Codex ACP agent..."
if [[ -r "$REPO/.env" ]]; then
set -a
# shellcheck disable=SC1091
source "$REPO/.env"
set +a
fi
if [[ -n "${BUZZ_PRIVATE_KEY:-}" && -n "${BUZZ_ACP_AGENT_COMMAND:-}" ]]; then
"$REPO/target/release/buzz-acp" > "$REPO/buzz-acp-runtime.log" 2>&1 &
ACP_PID=$!
sleep 1
if ! kill -0 "$ACP_PID" 2>/dev/null; then
echo "[Buzz] Codex ACP failed to start — see buzz-acp-runtime.log:" >&2
tail -30 "$REPO/buzz-acp-runtime.log" >&2
ACP_PID=""
fi
else
echo "[Buzz] ACP skipped: BUZZ_PRIVATE_KEY or BUZZ_ACP_AGENT_COMMAND is missing."
fi

echo "[Buzz] Launching desktop app..."
if [[ ! -x "$APP_BIN" ]]; then
echo "[Buzz] Packaged app binary not found at: $APP_BIN" >&2
exit 1
fi
# BUZZ_PRIVATE_KEY must NOT reach the desktop app. `.env` is sourced above with
# `set -a` for the relay and the ACP agent (which genuinely need that key), but
# the app treats BUZZ_PRIVATE_KEY as a dev/CI identity override that takes
# precedence over the OS keyring (see app_state.rs `identity_from_env`). Leaving
# it exported makes every launch boot as the .env identity instead of the
# owner's, which drops the app into the "enter your private key" re-import
# screen on each restart.
env -u BUZZ_PRIVATE_KEY "$APP_BIN"
# When the app window closes, control returns here and the EXIT trap tears down.
6 changes: 6 additions & 0 deletions crates/buzz-acp/src/base_prompt.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
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
26 changes: 26 additions & 0 deletions crates/buzz-conformance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,27 @@ pub trait Tracer: Send + Sync {
/// Record one trace step. Implementations MAY be no-ops in production
/// builds and write to JSONL in tests.
fn record(&self, step: TraceStep);

/// Whether recorded steps are actually observed.
///
/// Emitters on hot paths MUST consult this before doing work whose
/// *only* consumer is the trace — most importantly extra database
/// reads that project row labels independently of the fetch query
/// (the read-seam's `communities_of_channels` lookup). With a
/// discarding tracer that work is pure overhead.
///
/// This is the `log.isDebugEnabled()` of the trace seam. It exists to
/// let callers skip *building emit inputs*, never to let them skip an
/// emit they would otherwise have made: when this returns `true`
/// every seam must behave exactly as it did before the gate existed,
/// so the coverage-breach guard stays non-vacuous.
///
/// Defaults to `true` — a new tracer is assumed to observe steps until
/// it says otherwise. Wrappers that delegate to an inner tracer MUST
/// forward this method rather than inherit the default.
fn enabled(&self) -> bool {
true
}
}

/// A no-op tracer for production. Zero cost: the build can omit emission
Expand All @@ -324,4 +345,9 @@ pub struct NoopTracer;

impl Tracer for NoopTracer {
fn record(&self, _step: TraceStep) {}

/// Nothing is observed, so emitters should skip building inputs.
fn enabled(&self) -> bool {
false
}
}
27 changes: 24 additions & 3 deletions crates/buzz-db/src/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ mod tests {
use super::*;
use std::collections::BTreeSet;

const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz";
const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz"; // sadscan:disable np.postgres.1

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ConstraintKind {
Expand Down Expand Up @@ -561,7 +561,7 @@ mod tests {
let mut migrations: Vec<_> = MIGRATOR.iter().collect();
migrations.sort_by_key(|migration| migration.version);

assert_eq!(migrations.len(), 26);
assert_eq!(migrations.len(), 27);
assert_eq!(migrations[0].version, 1);
assert_eq!(&*migrations[0].description, "initial schema");
assert!(migrations[0]
Expand Down Expand Up @@ -919,6 +919,27 @@ mod tests {
assert!(heartbeat.contains("epoch"));
assert!(heartbeat.contains("INSERT INTO replica_heartbeat (id) VALUES (1)"));
assert!(heartbeat.contains("_operator_global_tables"));

// Channel-id lookup index (0027): serves the tenant-independent
// `channels` lookups that carry no community_id predicate, which no
// community_id-leading index can satisfy. Covering + partial so the
// planner can go index-only; asserted NOT UNIQUE because `id` alone is
// not unique in this table (the same channel id may exist under more
// than one community), so a unique index would encode a false
// constraint and fail to build on such a database.
assert_eq!(migrations[26].version, 27);
let channel_id_index = migrations[26].sql.as_str();
assert!(channel_id_index.contains("idx_channels_id_live"));
assert!(channel_id_index.contains("INCLUDE (community_id)"));
assert!(channel_id_index.contains("WHERE deleted_at IS NULL"));
assert!(
!channel_id_index.contains("CREATE UNIQUE INDEX"),
"channels.id is not unique across communities — index must not be UNIQUE",
);
assert!(
desired_schema.contains("idx_channels_id_live"),
"desired-state schema must carry the channel-id lookup index",
);
}

#[test]
Expand Down Expand Up @@ -1161,7 +1182,7 @@ mod tests {
run_migrations(&pool)
.await
.expect("retry succeeds after operator repair");
assert_eq!(applied_versions(&pool).await.last().copied(), Some(26));
assert_eq!(applied_versions(&pool).await.last().copied(), Some(27));
}

#[tokio::test]
Expand Down
56 changes: 56 additions & 0 deletions crates/buzz-relay/src/conformance/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,16 @@ impl Tracer for CountingTracer {
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.inner.record(step);
}

/// Delegate, never inherit the `true` default. This wrapper is
/// transparent: whether emits are observed is a property of the
/// tracer underneath it. Returning `true` over a `NoopTracer` would
/// reintroduce the overhead the gate exists to remove; returning
/// `false` over a real tracer would suppress the emits whose absence
/// the `EmitGuard` reports as a coverage breach.
fn enabled(&self) -> bool {
self.inner.enabled()
}
}

impl EmitGuard {
Expand Down Expand Up @@ -455,6 +465,52 @@ mod tests {
}
}

/// Discarding tracer that reports `enabled() == false`, standing in
/// for the production `NoopTracer`.
#[derive(Debug, Default)]
struct DisabledTracer;

impl Tracer for DisabledTracer {
fn record(&self, _step: TraceStep) {}
fn enabled(&self) -> bool {
false
}
}

/// `CountingTracer` must forward `enabled()` to the tracer it wraps
/// rather than inherit the trait's `true` default. Both directions
/// matter, and getting either wrong is silent:
///
/// - over a disabled tracer, answering `true` would keep the hot-path
/// read-seam `channels` lookup running in production — the overhead
/// the gate exists to remove;
/// - over a live tracer, answering `false` would make gated emitters
/// skip emits during conformance runs, so the `EmitGuard` would
/// report `ImplBug` for seams that are in fact correct (or, worse,
/// mask a real breach behind an expected one).
#[test]
fn counting_tracer_delegates_enabled_to_inner() {
let (_guard, counting) = EmitGuard::arm(
Arc::new(DisabledTracer),
dummy_state(),
"delegates_disabled",
);
assert!(
!counting.enabled(),
"CountingTracer must report disabled when wrapping a discarding tracer"
);

let (_guard, counting) = EmitGuard::arm(
Arc::new(VecTracer::default()),
dummy_state(),
"delegates_live",
);
assert!(
counting.enabled(),
"CountingTracer must report enabled when wrapping an observing tracer"
);
}

fn dummy_state() -> AbstractState {
AbstractState {
resolved_community: CommunityLabel::from_uuid(Uuid::from_u128(0xA)),
Expand Down
Loading
Loading