- Rust 76.3%
- JavaScript 13.2%
- Svelte 6%
- Shell 2.9%
- CSS 1%
- Other 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
AppImage / appimage (push) Successful in 8m56s
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. |
||
| .forgejo/workflows | ||
| .vscode | ||
| ci | ||
| docs | ||
| scripts | ||
| src | ||
| src-tauri | ||
| static | ||
| .dockerignore | ||
| .envrc | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| jsconfig.json | ||
| package.json | ||
| pnpm-lock.yaml | ||
| README.md | ||
| svelte.config.js | ||
| vite.config.js | ||
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:
- 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 theubuntu:22.04image,scripts/ci/build-appimage.shinstalls the toolchain, fetches Node/sidecars and runstauri build, thenscripts/ci/smoke-appimage.shchecks the result. Av*tag whose version differs fromtauri.conf.jsonfails here. - Copies
/out/edufeed-agent_X.Y.Z_amd64.AppImageout withdocker create+docker cpand uploads it as a build artifact. - On tag builds: uploads the asset to the Forgejo release using
scripts/ci/upload-release-asset.sh(requiresRELEASE_TOKENsecret).
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:
-
Tag the commit on
main:git tag vX.Y.Z ngit push origin vX.Y.Z # or: git push origin vX.Y.Z -
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). -
Verify the release — Visit
https://git.edufeed.org/edufeed/edufeed-agent-connect/releases/tag/vX.Y.Zand 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::NeedsAppUrluntil an app URL is set (via the setup screen or anedufeed-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-acpprocess group (adapter included). Agents run in their own process group and also getSIGTERMif the app itself dies. - The window opens immediately regardless of relay connectivity:
bootstraponly 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 astatus-changedevent follows once it's up (or fails) — the window is never blocked on a slow/unreachable relay at startup. - A
WARN/ERRORpair aboutupdate-desktop-databasefailing to register theedufeed-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) anddirs::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.jsonfile in the data dir (logged once as aWARN).
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 thebuzz-acp/buzz-agentsidecar binaries (defaults to the directory of the running executable, i.e. Tauri's bundledexternalBinlocation). -
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-splitcommand 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'sPATH(plus~/.local/bin, where the Claude/Codex installers put their CLIs) is searched forclaude/codex, after the bundled Node'sbin/. 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'sbuzz-acpruns with its own runtime's adapter dir in front of the same search path, so an app-installed adapter wins and itsnodeshebang resolves. -
EDUFEED_AGENT_NODE_DIR— override directory for the Node install thatadapters::locate_noderesolves at startup (defaults to<resource_dir>/node, i.e. the bundledbundle.resourcestree fetched byscripts/fetch-node.sh). Must containbin/nodeandlib/node_modules/npm/bin/npm-cli.js. Without a Node, theinstall_adaptercommand answersnodeMissing. NixOS dev recipe (the nix dev shell's Node):export EDUFEED_AGENT_NODE_DIR=$(dirname $(dirname $(which node)))Deliberately without
readlink -f: nixpkgs'nodejspackage symlinksbin/nodeintonodejs-slim, which has no npm, so resolving the symlink lands in a treelocate_noderejects. -
RUST_LOG— standardtracing_subscriber::EnvFiltersyntax (e.g.RUST_LOG=edufeed_agent_lib=debug); defaults toinfo. The same lines (redacted) are also kept in an in-memory ring buffer the UI's "copy diagnostics" action reads from — seeAGENT_SHUTDOWN_TIMEOUTbelow 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)
-
Start pyramid with the agent-bridge branch on
:3334. Use an existing checkout of thepyramid-edufeedfork (e.g.../pyramid-edufeed-agent-bridge, already onpr/agent-bridge), or clone it with ngit'sgit-remote-nostrhelper 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.orgCreate a public group in the web app and save its admin keypair.
-
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 5188Log in with the group admin account in the browser.
-
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 devOr with real
buzz-acpfrom nixpkgs (if the desktop environment has access):export EDUFEED_AGENT_SIDECAR_DIR=$(dirname $(readlink -f $(which buzz-acp))) cargo tauri dev -
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→Runningwithin ~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-acpprocess 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→Runningall 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-acpbinary 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_failsandstart_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.devUrlwashttp://localhost:1420while Vite binds127.0.0.1only, and WebKitGTK may resolvelocalhostto::1.devUrlis nowhttp://127.0.0.1:1420; this must be re-checked on an unlocked desktop. (WEBKIT_DISABLE_DMABUF_RENDERER=1andLIBGL_ALWAYS_SOFTWARE=1did 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 viacurl -fsSL https://claude.ai/install.sh | bash) - Sign-in: Uses your own Claude Code subscription. The companion runs
claude auth loginin 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 onPATHworks too.
Codex
- CLI:
codex(installed viacurl -fsSL https://chatgpt.com/codex/install.sh | sh) - Sign-in: Uses your own Codex subscription. Sign-in flow is identical to
Claude Code:
codex loginin the embedded terminal. - Adapter:
@agentclientprotocol/codex-acp(npm package). Installed app-locally or fromPATH.
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
0600file on disk (secrets.jsonin 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-agentsidecar 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-agentnamespace), 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_modulesand…/adapters/codex/node_modules, each replaced as a whole on reinstall. - Keyring marker:
~/.local/share/edufeed-agent/keyring-usedrecords 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.
Recommended IDE Setup
VS Code + Svelte + Tauri + rust-analyzer.