No description
  • Rust 76.3%
  • JavaScript 13.2%
  • Svelte 6%
  • Shell 2.9%
  • CSS 1%
  • Other 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Steffen Rörtgen 4a9c87471c
All checks were successful
AppImage / appimage (push) Successful in 8m56s
ci: never leave a release without its asset
Upload under a temporary name, retry up to three times, and only then
delete the old asset and rename the new one. The previous order deleted
first, so a slow upload that hit a 502 from the reverse proxy left the
v0.1.0 release empty.
2026-10-02 00:15:23 +02:00
.forgejo/workflows ci: upload-artifact@v3 — v4 is not supported on Forgejo Actions 2026-10-01 18:06:26 +02:00
.vscode chore: scaffold Edufeed Agent (Tauri 2 + Svelte 5, nix dev shell, pinned Buzz sidecars) 2026-09-30 22:44:47 +02:00
ci ci: build the AppImage with docker build, no bind mounts 2026-10-01 11:16:34 +02:00
docs docs: Forgejo mirror is edufeed/edufeed-agent-connect 2026-10-01 17:38:29 +02:00
scripts ci: never leave a release without its asset 2026-10-02 00:15:23 +02:00
src fix(ui): bridge banner above the tabs, re-attaching terminals, clearer runtime cards 2026-10-01 11:21:05 +02:00
src-tauri fix(supervisor): a group joined at runtime clears the no-group hint 2026-10-01 11:44:17 +02:00
static chore: scaffold Edufeed Agent (Tauri 2 + Svelte 5, nix dev shell, pinned Buzz sidecars) 2026-09-30 22:44:47 +02:00
.dockerignore ci: build the AppImage with docker build, no bind mounts 2026-10-01 11:16:34 +02:00
.envrc chore: scaffold Edufeed Agent (Tauri 2 + Svelte 5, nix dev shell, pinned Buzz sidecars) 2026-09-30 22:44:47 +02:00
.gitignore ci: build the AppImage with docker build, no bind mounts 2026-10-01 11:16:34 +02:00
flake.lock chore: scaffold Edufeed Agent (Tauri 2 + Svelte 5, nix dev shell, pinned Buzz sidecars) 2026-09-30 22:44:47 +02:00
flake.nix chore: scaffold Edufeed Agent (Tauri 2 + Svelte 5, nix dev shell, pinned Buzz sidecars) 2026-09-30 22:44:47 +02:00
jsconfig.json chore: scaffold Edufeed Agent (Tauri 2 + Svelte 5, nix dev shell, pinned Buzz sidecars) 2026-09-30 22:44:47 +02:00
package.json feat(ui): runtimes screen with install, sign-in and embedded terminal 2026-10-01 09:16:22 +02:00
pnpm-lock.yaml feat(ui): runtimes screen with install, sign-in and embedded terminal 2026-10-01 09:16:22 +02:00
README.md docs: Forgejo mirror is edufeed/edufeed-agent-connect 2026-10-01 17:38:29 +02:00
svelte.config.js chore: scaffold Edufeed Agent (Tauri 2 + Svelte 5, nix dev shell, pinned Buzz sidecars) 2026-09-30 22:44:47 +02:00
vite.config.js feat(ui): setup, pairing and agents screens 2026-10-01 02:09:32 +02:00

Edufeed Agent

A small desktop companion (Tauri 2 + Svelte 5) that runs Block's buzz-acp harness so a user's own Claude Code / Codex login (or an API key) can drive an AI agent that participates in edufeed NIP-29 groups. The agent is only online while this computer is — there is no server-side runtime; closing the app takes the agent offline.

See docs/design.md for the full design spec (spans this repo, edufeed-app, and pyramid-edufeed).

Install

Linux AppImage releases are published to this repo's Forgejo releases by .forgejo/workflows/appimage.yml on every vX.Y.Z tag:

https://git.edufeed.org/edufeed/edufeed-agent-connect/releases/download/vX.Y.Z/edufeed-agent_X.Y.Z_amd64.AppImage

(substitute the actual release version for X.Y.Z). Download it, make it executable, and run it:

chmod +x edufeed-agent_X.Y.Z_amd64.AppImage
./edufeed-agent_X.Y.Z_amd64.AppImage

First run: When you start the companion for the first time, you will be asked to enter the web app URL (e.g., https://edufeed.org). Alternatively, you can click "Add agent" on the web app's Agents page — the deep link edufeed-agent://setup?app=<url> will open the running companion (or start it) with the app URL pre-filled.

The web app's AGENT_DOWNLOAD_URL env var is set to this same pattern with the current release version baked in, so its "Download companion" button on the Agents page always points at a real release asset.

Development

This repo ships a Nix flake that provides the full toolchain (Rust, Tauri CLI, WebKitGTK, Node 22, pnpm) — there is no system Rust toolchain requirement.

direnv allow          # or: nix develop
scripts/fetch-sidecars.sh   # downloads the pinned buzz-acp / buzz-agent binaries
scripts/fetch-node.sh       # downloads the pinned bundled Node runtime
pnpm install
cargo tauri dev        # run the app in dev mode

Testing:

cargo test              # Rust unit tests (from src-tauri/)
pnpm test                # frontend unit tests

Build

The companion is distributed as a Linux AppImage built by .forgejo/workflows/appimage.yml.

Automated builds (CI)

Forgejo Actions automatically builds and publishes the AppImage on:

  • Every push to main (artifact upload, no release)
  • Every v* tag (creates/updates a release and uploads the asset)
  • Workflow dispatch (manual trigger via Forgejo UI)

The workflow:

  1. Runs docker build -f ci/appimage.Dockerfile . — the checkout goes to the docker daemon as the build context (the runner is docker-out-of-docker, so bind mounts would be empty). Inside the ubuntu:22.04 image, scripts/ci/build-appimage.sh installs the toolchain, fetches Node/sidecars and runs tauri build, then scripts/ci/smoke-appimage.sh checks the result. A v* tag whose version differs from tauri.conf.json fails here.
  2. Copies /out/edufeed-agent_X.Y.Z_amd64.AppImage out with docker create + docker cp and uploads it as a build artifact.
  3. On tag builds: uploads the asset to the Forgejo release using scripts/ci/upload-release-asset.sh (requires RELEASE_TOKEN secret).

Local validation

To validate the build locally before pushing, run exactly what CI runs:

docker build -t edufeed-agent-appimage:local -f ci/appimage.Dockerfile .
cid=$(docker create edufeed-agent-appimage:local)
docker cp "$cid":/out/. ./out/ && docker rm "$cid"
docker rmi edufeed-agent-appimage:local

The build and smoke-test log is part of the docker build output; the AppImage lands in ./out/edufeed-agent_X.Y.Z_amd64.AppImage (owned by you — nothing is written to the checkout as root).

Publish

Publishing a release is a manual process performed by the repo maintainer:

  1. Tag the commit on main:

    git tag vX.Y.Z
    ngit push origin vX.Y.Z  # or: git push origin vX.Y.Z
    
  2. Wait for CI — The workflow will build, smoke-test, and upload the asset to a Forgejo release. This requires the repo secret RELEASE_TOKEN (a repo-scoped Forgejo token with write access to releases).

  3. Verify the release — Visit https://git.edufeed.org/edufeed/edufeed-agent-connect/releases/tag/vX.Y.Z and confirm the AppImage asset is present.

The web app's AGENT_DOWNLOAD_URL env var should be set to the pattern:

https://git.edufeed.org/edufeed/edufeed-agent-connect/releases/download/vX.Y.Z/edufeed-agent_X.Y.Z_amd64.AppImage

Note: This repo is nostr-hosted via ngit with Forgejo as a push mirror. The first push after ngit init will establish the nostr repository. Subsequent pushes fan out to all configured remotes (nostr, Forgejo, GitHub) automatically.

Sidecars

scripts/fetch-sidecars.sh downloads the pinned Buzz desktop .deb release, verifies its sha256, and extracts buzz-acp / buzz-agent into src-tauri/binaries/<bin>-x86_64-unknown-linux-gnu, where Tauri's externalBin expects them. These binaries are gitignored and must be fetched locally before cargo tauri dev / cargo tauri build will find them.

For dev, EDUFEED_AGENT_SIDECAR_DIR can point the app at an alternate directory of sidecar binaries instead of the bundled ones (e.g. a local buzz-acp build). This matters on a NixOS dev box in particular: the bundled sidecars are generic-Linux binaries and cannot run there without nix-ld, so point the app at the nixpkgs-built binary instead:

export EDUFEED_AGENT_SIDECAR_DIR=$(dirname $(readlink -f $(which buzz-acp)))

Bundled Node (for app-local ACP adapter installs)

scripts/fetch-node.sh downloads the pinned Node release, verifies its sha256, and extracts it into src-tauri/resources/node (trimmed of include/, share/, the corepack module, and bin/corepack — the symlink into that module, which tauri-build refuses to bundle once it's dangling — none of which the adapters need), where Tauri's bundle.resources bundles it from. Like the sidecars, this directory is gitignored (save for a .gitkeep placeholder so cargo build/cargo test succeed without it — tauri-build validates the bundle.resources glob at compile time, i.e. a path that doesn't exist at all fails the build) and must be fetched locally before cargo tauri dev / cargo tauri build.

This bundled Node/npm is what adapters::install_adapter uses to install ACP adapter packages (@agentclientprotocol/claude-agent-acp, @agentclientprotocol/codex-acp) app-locally under the companion's data directory — a user never needs Node installed themselves for that. For dev, EDUFEED_AGENT_NODE_DIR can point adapters::locate_node at an alternate Node install instead of the bundled one (same override convention as EDUFEED_AGENT_SIDECAR_DIR).

NODE_VERSION and NODE_SHA256 at the top of scripts/fetch-node.sh must be bumped together: the hash is specific to that exact release's node-<version>-linux-x64.tar.xz, found in its SHASUMS256.txt (e.g. https://nodejs.org/dist/v22.23.3/SHASUMS256.txt). Bumping NODE_VERSION without updating NODE_SHA256 makes every fetch fail closed (a sha256sum -c mismatch), not silently fetch an unverified tarball.

First cargo tauri dev

  • The window opens on Stage::NeedsAppUrl until an app URL is set (via the setup screen or an edufeed-agent://setup?app=<url> deep link). Once a site is configured, a deep link to a different site is not applied silently: the window asks "Switch to ? Agents on this computer will stop." and only a confirmed switch re-points the companion (stopping every agent first).
  • Closing the window stops every agent: on exit the app stops sync and the supervisor, which terminates each buzz-acp process group (adapter included). Agents run in their own process group and also get SIGTERM if the app itself dies.
  • The window opens immediately regardless of relay connectivity: bootstrap only does local work (open the keystore/registry, load the cached config, start the supervisor). If a config was already cached from a previous run, sync starts in the background right after (spawn_initial_sync) and a status-changed event follows once it's up (or fails) — the window is never blocked on a slow/unreachable relay at startup.
  • A WARN/ERROR pair about update-desktop-database failing to register the edufeed-agent:// scheme is expected on hosts without that command (e.g. a minimal Nix dev shell) and is non-fatal — the app falls back to handling deep links only for as long as it's already running.
  • State lives under dirs::data_dir()/edufeed-agent (keystore, agents.json) and dirs::config_dir()/edufeed-agent (cached app config) — delete both to reset a dev instance to a clean first-run state.
  • The keystore prefers the desktop Secret Service; on a headless/CI box without one it falls back to a 0600 secrets.json file in the data dir (logged once as a WARN).

Troubleshooting: commands can feel briefly unresponsive

Unlike startup, the mutating commands (set_app_url, start_pairing's own success path, remove_agent) hold AppState's single lock for the whole duration of restart_sync, including sync::start's network calls (relay connect + subscribe). This is intentional — bounded by nostr-sdk's own connect/subscribe timeouts, not unbounded — but it does mean every other command (including get_status) queues up behind whichever one is currently talking to a slow relay. If the UI seems to hang for a few seconds right after pairing or changing the app URL against a flaky relay, this is why; raise RUST_LOG to debug and check copy_diagnostics/the terminal for what sync::start is waiting on.

Dev/runtime environment variables

  • EDUFEED_AGENT_SIDECAR_DIR — override directory for the buzz-acp / buzz-agent sidecar binaries (defaults to the directory of the running executable, i.e. Tauri's bundled externalBin location).

  • EDUFEED_AGENT_COMMAND_OVERRIDE — dev-only escape hatch that replaces the resolved command for every persona runtime (claude-code-acp, codex-acp, buzz-agent) with a whitespace-split command arg1 arg2 ..., e.g. EDUFEED_AGENT_COMMAND_OVERRIDE="python3 stub-acp-agent.py --flag" to point every agent at a local stub ACP adapter instead of a real sidecar.

  • PATH — the user's PATH (plus ~/.local/bin, where the Claude/Codex installers put their CLIs) is searched for claude/codex, after the bundled Node's bin/. An adapter is taken from its own runtime's app-local dir (<data_dir>/edufeed-agent/adapters/<runtime>/node_modules/.bin) first, then from that search path. Each agent's buzz-acp runs with its own runtime's adapter dir in front of the same search path, so an app-installed adapter wins and its node shebang resolves.

  • EDUFEED_AGENT_NODE_DIR — override directory for the Node install that adapters::locate_node resolves at startup (defaults to <resource_dir>/node, i.e. the bundled bundle.resources tree fetched by scripts/fetch-node.sh). Must contain bin/node and lib/node_modules/npm/bin/npm-cli.js. Without a Node, the install_adapter command answers nodeMissing. NixOS dev recipe (the nix dev shell's Node):

    export EDUFEED_AGENT_NODE_DIR=$(dirname $(dirname $(which node)))
    

    Deliberately without readlink -f: nixpkgs' nodejs package symlinks bin/node into nodejs-slim, which has no npm, so resolving the symlink lands in a tree locate_node rejects.

  • RUST_LOG — standard tracing_subscriber::EnvFilter syntax (e.g. RUST_LOG=edufeed_agent_lib=debug); defaults to info. The same lines (redacted) are also kept in an in-memory ring buffer the UI's "copy diagnostics" action reads from — see AGENT_SHUTDOWN_TIMEOUT below for one of the things worth raising the log level to debug when a removed/restarted agent seems slow to stop.

AGENT_SHUTDOWN_TIMEOUT (a fixed 10s constant in supervisor.rs, not currently an env var) is how long the supervisor waits for a removed agent's buzz-acp process to actually exit before giving up and moving on anyway — relevant if remove_agent/unpairing seems to hang briefly.

Verify end to end

Two ways to run a full end-to-end flow:

GUI mode (when the desktop is unlocked)

  1. Start pyramid with the agent-bridge branch on :3334. Use an existing checkout of the pyramid-edufeed fork (e.g. ../pyramid-edufeed-agent-bridge, already on pr/agent-bridge), or clone it with ngit's git-remote-nostr helper installed:

    git clone nostr://npub19rtu4896peq0txzre5wd2p7p4yffr9mf73y45recmu4s4vpr30zqn7zrla/relay.ngit.dev/pyramid-edufeed
    cd pyramid-edufeed && git checkout pr/agent-bridge
    cargo run -- --listen 127.0.0.1:3334 --groups-enabled --allow-kind +20001,+30175,+30177 --relay-url wss://groups.edufeed.org
    

    Create a public group in the web app and save its admin keypair.

  2. Start the web app with agents enabled:

    git clone https://git.edufeed.org/edufeed/edufeed-app.git
    cd edufeed-app && git checkout pr/agents-page
    AGENTS_ENABLED=true GROUPS_RELAYS=ws://localhost:3334 pnpm dev --port 5188
    

    Log in with the group admin account in the browser.

  3. From the companion repo, start the app pointing to your own stub ACP adapter:

    EDUFEED_AGENT_COMMAND_OVERRIDE="python3 /path/to/pyramid-edufeed-agent-bridge/scripts/stub-acp-agent.py" \
    cargo tauri dev
    

    Or with real buzz-acp from nixpkgs (if the desktop environment has access):

    export EDUFEED_AGENT_SIDECAR_DIR=$(dirname $(readlink -f $(which buzz-acp)))
    cargo tauri dev
    
  4. In the companion window, enter the web app URL (http://localhost:5188), click Connect, confirm the pairing in the browser. In the editor: give the agent a name (e.g. "Lehrbot"), pick the test group, click Save.

Headless verification (CI or locked desktop)

src-tauri/tests/live_flow.rs exercises the whole core flow (config fetch, NIP-46 pairing, keystore + registry persistence, relay sync, the agent supervisor) end to end, without the Tauri GUI. This is useful when the desktop GUI/WebKitGTK stack is unavailable (e.g., in CI or on a headless machine) but a local Pyramid + web app stack is running. The test is #[ignore]d since it requires a live relay stack; run it explicitly:

nix develop -c bash -c 'cd src-tauri && cargo test --test live_flow -- --ignored --nocapture'

It expects, all running locally first: pyramid pr/agent-bridge on :3334 with the agent kinds (30175/30177/5) allowed, the web app pr/agents-page on :5188 with AGENTS_ENABLED=true, and a stub ACP agent reachable via EDUFEED_AGENT_COMMAND_OVERRIDE (no real claude-code-acp/codex-acp is assumed to be installed). It prints the pairing connect_url to open in a browser, then every agent status change, with MILESTONE running and MILESTONE stopped lines marking the two states the controller watches for.

Expected observations

  • Startup: The companion runs without blocking on relay connectivity; the window is ready immediately.
  • Pairing: The companion generates a unique nostrconnect:// URI with a secret. Confirm it in the browser to pair.
  • After Save in the editor: The agent's row transitions Stopped → Starting → Running within ~5 seconds. On the relay, nak req -k 0 -a <agent pubkey> shows the agent's kind 0 with the name you entered.
  • In group chat: A mention of the agent (nostr:npub…) is answered by the agent within seconds, with the Agent badge and online indicator visible.
  • After removal on the website: Within seconds the agent's buzz-acp process exits and its row reads "Removed on the website. Delete its key?" with one Remove button (and a checkbox to also delete the key); removing it there drops the row.

Observed 2026-10-01

  • Status transitions: Stopped → Starting → Running all observed; transitions took ~5 seconds from Save to Running.
  • Relay events: Kind 0 published with agent display name; kinds 30175 and 30177 published by owner; agent added to group roster (kind 39002).
  • Group chat mention: Answered within seconds with "pong 1", rendered with Agent badge.
  • Process lifecycle: buzz-acp binary running correctly; stub ACP adapter invoked; process exits on removal.
  • Deletion: Kind 5 deletion events published correctly with a-tags; processes cleaned up.
  • Error recovery: (Not run live; covered by unit tests crash_backs_off_exponentially_then_fails and start_again_resets_failure.)

Observed 2026-10-01, plan 3B head (0eeb46f)

Re-run of the same headless harness after the runtimes/terminal/adapter/keystore rewrite, against a fresh local pyramid pr/agent-bridge (groups enabled, kinds +20001,+30175,+30177, one public group) and the web app pr/agents-page: pairing confirmed in the browser → agent saved with the group → Stopped → Starting → Running (~5 s) → kind 0 "Lehrbot 3B" on the relay, agent in the 39002 roster → owner mention answered "pong 1" with the Agent badge → removal on the website → kind 5 with the 30177/30175 a tags, roster without the agent, Stopped, MILESTONE stopped, no buzz-acp/stub process left; harness 1 passed in 236 s. The runtime readiness probes did not interfere: the dev override wins over cli:/signin: checks by design (override_env_wins). Not covered here: the no-group hint (nixpkgs buzz 0.5.14 does not log channel counts) and the embedded terminal (GUI only).

Known issues

  • Tauri GUI blank window on nix dev shell (re-check): The companion window rendered white/blank in the nix dev shell on this host (2026-10-01) despite the same HTML rendering correctly in Chrome at http://127.0.0.1:1420. The likely cause: build.devUrl was http://localhost:1420 while Vite binds 127.0.0.1 only, and WebKitGTK may resolve localhost to ::1. devUrl is now http://127.0.0.1:1420; this must be re-checked on an unlocked desktop. (WEBKIT_DISABLE_DMABUF_RENDERER=1 and LIBGL_ALWAYS_SOFTWARE=1 did not help before the change.) Headless verification works regardless.

  • Presence indicator dot on the web Agents list: The agent's online dot is driven by its presence event, an ephemeral kind 20001 that relays forward but never store or replay. A web Agents list opened after the last heartbeat has nothing to show until the next one arrives, so the dot can be missing although the agent runs and answers. This is a web app / pyramid matter (presence would need to be stored or re-announced on subscribe), not the companion's.

Runtimes

Every agent runs as Block's buzz-acp harness (bundled sidecar), which in turn starts the agent's runtime as an ACP adapter. The companion supports three runtime modes; which one is chosen per agent on the web app:

Claude Code

  • CLI: claude (installed via curl -fsSL https://claude.ai/install.sh | bash)
  • Sign-in: Uses your own Claude Code subscription. The companion runs claude auth login in an embedded terminal, which opens your browser for login. Sign-in state is managed by the Claude Code CLI itself.
  • Adapter: @agentclientprotocol/claude-agent-acp (npm package). The companion installs it app-locally using its bundled Node; an adapter already on PATH works too.

Codex

  • CLI: codex (installed via curl -fsSL https://chatgpt.com/codex/install.sh | sh)
  • Sign-in: Uses your own Codex subscription. Sign-in flow is identical to Claude Code: codex login in the embedded terminal.
  • Adapter: @agentclientprotocol/codex-acp (npm package). Installed app-locally or from PATH.

Buzz Agent

  • Model provider: Choose from Anthropic, OpenAI, OpenAI-compatible (e.g. Ollama), or OpenRouter.
  • API key: Paste or paste-and-save your key in the agent's settings in the companion. The key is stored in your system keyring if available, or falls back to a 0600 file on disk (secrets.json in the data directory) if the keyring is unavailable (e.g., on a headless machine or in CI). Keys never leave this computer.
  • No CLIs needed: The bundled buzz-agent sidecar talks directly to the provider's API.

Agent readiness and setup

When an agent is waiting for setup, its row says so in plain language — e.g. "Claude Code needs setup" — with the one button that fixes it: Open runtimes (CLI missing, not signed in, adapter missing), Open settings (a buzz-agent's provider, model or API key) or Show how (this section). The machine codes behind these (cli:claude, adapter:claude, signin:claude, …) only appear in Copy diagnostics. The companion retries every waiting agent by itself once an install or sign-in terminal closes or an adapter install finishes; after fixing something outside the app, click Check again on the Runtimes tab. A running agent that is in no group yet shows "Not in any group yet" with a button to the website's Agents page.

  • A sign-in probe that clearly says "signed out" blocks a start.
  • A probe that times out is retried anyway (it might recover on the next try).
  • Provider and subscription errors (invalid key, usage limit, account issues) appear as the agent's last error message. Check Copy diagnostics for the full runtime output.

Data storage

  • Keys and credentials: Stored in your system keyring service (edufeed-agent namespace), or in ~/.local/share/edufeed-agent/secrets.json (0600 file, warning shown in the UI) if no keyring is available.
  • Installed adapters: One npm prefix per runtime, e.g. ~/.local/share/edufeed-agent/adapters/claude/node_modules and …/adapters/codex/node_modules, each replaced as a whole on reinstall.
  • Keyring marker: ~/.local/share/edufeed-agent/keyring-used records that a secret was ever stored in the system keyring here, so a later boot without a keyring can tell "key missing" from "keyring unreachable".
  • Agent configuration: Stored in ~/.local/share/edufeed-agent/agents.json (registry of your paired agents and their settings).
  • Cached app config: Stored in ~/.config/edufeed-agent/ (app URL and relay configuration, fetched once from the web app and cached).

Removing credentials and adapters

To fully reset your companion installation:

# Remove the agent registry, the 0600 key file (if used), cached config and adapter installs
rm -rf ~/.local/share/edufeed-agent ~/.config/edufeed-agent

# Keys stored in the system keyring are NOT removed by rm -rf. Delete them with
# your keyring tool, e.g.:
secret-tool clear service edufeed-agent
# (or remove the "edufeed-agent" entries in Seahorse / KWallet)

# Optionally: remove CLI credentials (not needed for the companion to work again)
# claude auth logout
# codex logout

Removing a single agent in the companion stops it and forgets it. Its keys (the agent's own key and any API key) are deleted only when you tick Delete its key in the remove confirmation — otherwise they stay in the keyring (or the key file). Adapters are never removed with an agent (they are shared by every agent of that runtime); remove one by hand if needed, e.g. rm -rf ~/.local/share/edufeed-agent/adapters/claude.

VS Code + Svelte + Tauri + rust-analyzer.