feat: add relay-native execution nodes - #4597
Conversation
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
- enforce #![deny(unsafe_code)] and anyhow-based binary errors in buzz-node - run pairing over the shared buzz-ws-client (OK-checked publishes, bounded waits) instead of hand-rolled tungstenite - hot-reload owners.json in the run loop and re-announce on pairing - publish standard kind-20001 presence heartbeats (60s) so node liveness rides the same lease as members and agents - box WorkloadSpec in ExecutionCommand::Deploy (clippy large_enum_variant) - make removal tombstones sequence-aware via a supersedes_removal echo: deliberate redeploys succeed, stale replays still conflict; legacy tombstone state migrates on load - reject ephemeral execution kinds on HTTP POST /events with a clean 400 (WebSocket-only), matching the gift-wrap/presence transport gate - ignore the .buzz-node data dir (node identity key and pairing state) Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
End-to-end coverage for kinds 30630/24201/24202: announcement acceptance, NIP-33 replacement, and global readability; p-gated command/receipt fanout reaching only the addressee; restricted/403 on unscoped REQ, COUNT, and HTTP query shapes; fanout-only (never stored) semantics; and the clean 400 on HTTP publish of the WebSocket-only kinds. Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
- swap an agent's backend from the instance edit dialog: teardown-then- adopt with serialized transitions, receipt-confirmed workload removal, and redeploy through the authoritative deploy path - fix the BackendKind::ExecutionNode wire shape to camelCase nodeId (with a node_id alias for persisted records) — creation and summary reads were broken in both directions - derive node availability from presence instead of a 2-minute announcement-staleness window - replace the Run on select with a card picker: liveness dot (green/yellow/dimmed gray), radio semantics, bottom-aligned titles; drop the legacy/compatibility framing for provider backends (flag renamed to VITE_ENABLE_BACKEND_PROVIDERS) - show a Runs on row in the agent profile runtime tab - route profile-panel Edit to the instance dialog when a live instance exists (it was unreachable for persona-linked agents) - dedupe execution-node control actions and share the run-on prefix constant; restore the rodio/PTT comment dropped in an earlier commit Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Split the node's execution handling into a durable WorkloadLedger (reconciliation bookkeeping: admitted specs, lifecycles, sequenced removal tombstones) and an async Substrate seam with three implementations: - InertSubstrate: no-op bookkeeping, the library/test default - ProcessSubstrate: supervises the sprig ACP harness as a child process with the desktop launcher's env contract; identity keys live only in memory and start fails closed after a node restart - DockerSubstrate: runs agent bodies in containers where the container definition is the key store; deploys converge via rm-before-run, lifecycle survives node restarts, and exit watchers re-arm from labeled containers on boot Ledger state mutates only after the substrate accepts an operation, substrate failures surface as failed receipts, and bodies that exit on their own are recorded (clean stop vs failure) and never resurrected. LLM credentials come from a documented node-operator env allowlist, never the workload spec. Dockerfile.agent builds the agent body image from sprig: a slim default plus per-runtime variants (goose/claude/codex) layered with the same official installers the desktop's auto-install uses, resolved per workload as repository:runtime tags and failing closed when a variant is not built. Also enables the rustls ring feature buzz-test-client's mention bin relies on (pre-existing standalone build failure). Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Upstream block#4539 added a read-only saved-settings summary to the edit dialog premised on backends being fixed at creation. This branch makes backends swappable from that dialog, so the two features now compose: the interactive Run on picker owns location and changes, and the summary renders beneath it for provider agents only, showing the saved config rows (namespace, image, limits) without the immutability copy. summarizeRunOn learns the execution-node backend variant (no config rows — node runtime details stay on the node), and the edit dialog consumes upstream's applyProbeResult seam instead of the draft-keyed probe effect it replaced. Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Align buzz-node with the remote-agents spec that landed upstream: - Feed the harness inactivity reaper. New --inactivity-seconds / BUZZ_NODE_INACTIVITY_SECONDS (default 7200, mirroring the Kubernetes binding's schema default) sets BUZZ_ACP_EXIT_AFTER_INACTIVITY on the body. 0 is the blessed 'no bound' opt-in and omits the variable, since the harness default already means disabled. Remote bodies opt in, and a node is remote by definition. - Default the Docker substrate to the digest-pinned published body image (ghcr.io/block/buzz-sprig), because the object holding an agent's key must not run a movable tag. A test asserts the pin still matches the Kubernetes backend's constant, so drift fails rather than ships. - Resolve per-runtime images from a dedicated variant repository (--variant-image-repo, default buzz-agent) instead of retagging the base reference: goose/claude/codex images are local override images built by 'just agent-image <runtime>', never a fatter default. A missing image fails the deploy with the exact command to run, still checked before the previous container is removed. Two conformance bugs found while auditing against the spec: - A workload whose record carries no auth tag launched a body with no resolvable owner: unable to match an owner shutdown request and silently dropping every message under respond_to=owner-only. The node already holds a verified owner pubkey for every command, so it now supplies BUZZ_ACP_AGENT_OWNER when no attestation is present. - Deploy reported success at launch rather than at start, so a body that died immediately (bad credential, missing runtime) still earned a succeeded receipt. Both substrates now confirm liveness within a bounded window before the terminal receipt, keeping the exit watcher as the after-the-fact correction. A failed confirmation leaves the container in place so its logs remain readable. Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
Implementation walkthroughI kept the PR description focused on the motivation and architectural direction. This comment captures the current implementation shape. The Desktop integration is intended to preserve the existing agent model while making the execution body selectable. The agent configuration still shows the normal runtime and ACP command, while indicating that the agent runs on a connected Buzz execution node and remains owner-controlled. Agent configurationSelecting the execution bodyThe current Desktop flow allows the agent to run on this computer or on a connected execution node. Relay protocolThe execution-node protocol currently uses:
The announcement contains the node identity, capabilities, pairing attestations, and safe workload status. Presence remains the availability signal rather than host telemetry. Command lifecycleDesktop sends encrypted commands through the relay. The node then:
The current command set covers deployment, start, stop, restart, removal, and provider authentication. The workload projection intentionally contains only safe execution data. Runtime-specific infrastructure, container details, and raw credentials remain node-local. Runtime boundary
Both use the same relay, identity, runtime, and environment contract as Desktop. The node protocol does not need to know whether the body is a process or a container. Identity and credentialsThe agent identity remains the managed agent’s identity. The execution node has a separate persistent Nostr identity used for relay authentication, announcements, commands, and receipts. Nodes are paired with owners through the relay. Provider credentials remain node-local and are referenced by workloads rather than transmitted as ordinary workload configuration. The process substrate keeps the launch key in memory and therefore requires redeployment after a node restart. The Docker substrate uses the running container as the key store, allowing lifecycle operations to survive a node restart while still failing closed if the container is gone. Desktop integrationDesktop discovers paired nodes through their announcements and derives their live availability from presence. A managed agent can then be assigned to an execution node while retaining the same identity, profile, channels, history, and agent-facing UI as a locally running agent. The intention is that “remote” changes where the body runs, not what kind of agent it is. I expect the protocol and substrate boundaries to evolve as this is reviewed. The main question I’m trying to answer with this implementation is whether this is the right relay-native execution layer to standardize across hosted and persistent substrates. |
|
@Onnokh, I think the node is the better execution target. It keeps the agent identity stable and makes One boundary needs aligning before this grows. Merged #3748 now gives local and provider-backed agents one place to resolve the command, arguments, layered environment and launch policy. Could |
|
@wolfyy970 That feels like the right next step to me. I agree that the node is the better execution target, and that “Run on” should change where the agent runs rather than which configuration it receives. I think the convergence seam is the launch contract that Desktop already resolves today through I don’t want to add That should also let us remove the duplicate node-side runtime inference and the separate provider launch type, while keeping substrate-specific supervision and persistence where they belong. I could draft the follow-up later today. |
Promote the provider launch block into a shared buzz-core LaunchSpec (versioned: command, args, mcp_command, layered env, policy_env, owner) and make every execution path consume the same resolve: - Desktop resolves the contract once (managed_agents::launch); the provider deploy block, execution-node deploys, and local spawn all consume it. The wire shape of the provider launch block is preserved; all additions (version, mcp_command, runtime metadata env, respond-to gate, steer/dedup) are additive. - buzz-node substrates adapt the contract instead of reconstructing it from the runtime identifier: the node-side catalog shrinks to image variant + Claude CLI pointer, and the duplicate default_env/model_env/ provider_env inference is deleted. Wire env is layered policy-under- user with a reserved-key strip for node-owned identity variables. - AgentWorkloadContext slims to identity (key handoff, relay, auth tag, channel); AgentRuntimeSettings is gone — behavior travels in the contract. Provider credential names (shared PROVIDER_CREDENTIAL_ENV) are stripped at the node boundary and never persisted in the ledger. - Local spawn keeps its inherited-environment semantics explicitly: ambient-wins for runtime default_env, clear-when-unset for policy-managed keys, and the caller-decided lazy-pool flag. - spawn_config_hash digests the contract's merged effective view plus the few non-contract inputs, so the restart badge is literally 'the contract changed'. All 34 badge-behavior tests pass unmodified. Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
|
WorkloadSpec now carries a versioned LaunchSpec in buzz-core — the same block providers already consume, evolved additively (snake_case wire shape unchanged). Desktop resolves it once; local spawn, provider deploy, and node deploy all consume the same contract. Run on changes where the agent runs, not which setup it gets. For #4301, the seam is resolve_launch_spec → LaunchSpec. Happy to extract more into buzz-core if you need pieces it doesn't carry yet. |
|
Thanks. This is the shared execution boundary I was hoping for. I’ll rebase the Project-binding work onto |
|
@Onnokh, I compared the new contract with the Project-binding and portable-agent work. The remaining shared pieces I need are:
I want If you want to take these shared types in #4597 or a follow-up, I’ll build #4301 and #4588 against them. If some belong elsewhere, tell me where you think the boundary should sit and I’ll adapt. |
|
@tlongwell-block, I want to check this against the remote-agent direction that has already landed. #3748 and #3924 make provider deployment the generic path and deliberately avoid a substrate management channel after deploy. #4597 adds a relay-addressable execution node with deploy, start, stop and restart commands. I can see that working as a Buzz-specific execution target, but it is a different management model. I am building the portable configuration side in #4301: one resolved setup for instructions, Skills, Project-bound tools and updates, independent of where it runs. Should I treat |
Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com> # Conflicts: # desktop/src-tauri/src/commands/agents_deploy.rs # desktop/src-tauri/src/managed_agents/runtime.rs # desktop/src-tauri/src/managed_agents/spawn_hash.rs # desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
SpawnConfigSnapshot::from_inputs re-derived mcp_command from the runtime catalog — the last value the restart badge computed on its own instead of taking from the resolved contract. It is now an input like every other captured value: the spawn stamp passes launch.mcp_command (what actually fed the body), and the prospective side uses the same shared derivation (launch::effective_mcp_command) the contract resolver calls. Signed-off-by: Onno Klein Hofmeijer <onnokh@hotmail.com>
49a0198 to
0660ab7
Compare


First, great work on the remote-agent direction so far, especially the discussion and specification in #3748.
Over the past couple of days, I’ve been working on a relay-native execution-node approach, and it happened to converge with many of the same questions and constraints that emerged in the discussion around this PR and the related work.
This is intentionally an early exploration rather than a finished or settled architecture. I’m sharing it to make the direction concrete and see how well it fits with the Buzz vision and the provider model.
Summary
This PR introduces
buzz-node: a standalone, relay-native execution node for running Buzz agents away from the desktop.The goal is to let the same Buzz agent continue existing on remote or persistent infrastructure while preserving the normal Buzz model:
This is intended to address the hosted and persistent agent direction discussed in #4174.
Motivation
Remote agents currently lead toward several separate solutions: provider-specific launchers, host-specific supervisors, custom control APIs, and one-off integrations for Docker, SSH, Kubernetes, and local processes.
That risks creating a different product model for every execution environment.
I’m exploring a shared execution-node model instead. A node is a relay-connected Buzz principal that can be paired with an owner, receive authorized lifecycle commands, run workloads through a local substrate, and publish status and receipts back through the relay.
The agent itself remains the same agent. Only its execution body changes.
Architecture
The lifecycle is divided into a few clear responsibilities:
Desktop creates or selects a managed-agent identity using the existing agent model.
An execution node is paired with the owner through the relay and receives its own persistent Nostr identity.
Desktop publishes encrypted workload commands to the node through the relay. Commands include deployment, start, stop, restart, removal, and provider-authentication operations.
The node authenticates the command, verifies the owner/node/relay binding, applies idempotency and sequencing checks, and reconciles the requested workload state.
A runtime-neutral substrate starts the agent body. The initial substrates are local processes and Docker containers, using the same ACP/runtime contract used by Desktop.
The node publishes encrypted receipts and safe workload status through the relay. Desktop uses those results to present the remote agent alongside other agents.
Node availability is represented through normal Buzz presence. The node announcement contains slower-moving identity, capability, pairing, and workload information; presence represents whether the node is currently connected.
Security and lifecycle boundaries
The node is deliberately relay-first:
The relay is the coordination plane. The node is the execution participant. The substrate only runs the requested workload.
Relationship to the remote-agent specification
This builds on the direction established by #3748:
The part this PR explores further is the Buzz-specific substrate model discussed in the PR conversation: making the execution environment itself a relay-registered principal with an identity, pairing relationship, lifecycle protocol, and relay-visible status.
This is intended to complement the generic provider model rather than create a second agent model.
Current scope
This PR currently includes:
buzz-nodeservice;PR size
I recognize that this is a substantial PR with a broad surface area and a lot of change to review at once. The main review areas are approximately:
buzz-nodeand the implemented process/Docker substrates: ~6.8k linesOpen questions
I’m sharing this implementation to discuss whether this is the right Buzz-specific layer for hosted and persistent agents:
buzz-nodebecome the common relay-native execution model across substrates?