Skip to content

[WIP] feat(spawner): server-hosted agents via buzz-spawner daemon + server-aware agent editing - #3113

Open
siddhartpai wants to merge 40 commits into
block:mainfrom
siddhartpai:server-agent-editing
Open

[WIP] feat(spawner): server-hosted agents via buzz-spawner daemon + server-aware agent editing#3113
siddhartpai wants to merge 40 commits into
block:mainfrom
siddhartpai:server-agent-editing

Conversation

@siddhartpai

Copy link
Copy Markdown

Summary

Implements the first-party server-hosted agent path proposed in #2859, plus the desktop editing flow for those agents:

  • New crates/buzz-spawner daemon — watches the relay for kind:30178 agent specs and reconciles them into Docker-isolated buzz-acp containers, with 30179 status events and the 24201 NIP-44 attestation handshake (nsec minted on the server, owner signs the NIP-OA tag from the client). Ships with Dockerfile.spawner, Dockerfile.acp, and a deploy/compose service.
  • Relay support — new spawner event kinds in buzz-core/src/kind.rs, ingest/req gates, and the owner-delegated persona read-gate.
  • buzz-sdk/src/spawner.rs — typed builders/parsers for the spawner event family (opt-in projection: specs cannot carry an nsec, auth tag, or env blob).
  • Desktop: server-aware agent editing — editing a server-hosted agent pushes prompt updates to the spawner via a durable per-relay queue (lazy hydration, resolved-relay-origin only, empty pushes skipped, team instructions preserved across updates) with a pending chip + ack flow. Editing server-hosted agents no longer demands local API keys, since credentials live spawner-side — this also addresses the "local config presented as authoritative for remote agents" portion of [Bug] Provider-backed agents display local Claude config instead of remote runtime and expose env values #3034.
  • Design docs for server agent editing and per-owner server agent credentials.

Related issue

Implements #2859. Also addresses the local-API-key requirement aspect of #3034. (Searched existing issues/PRs — no duplicate PR found.)

Testing

  • crates/buzz-spawner/examples/owner_sim.rs — end-to-end owner simulation exercising spec publish, attestation handshake, prompt-update push, and ack against a live relay.
  • Spawner + relay unit tests for the new kinds, gates, and reconciliation.
  • Desktop: Playwright screenshot spec for server-aware agent editing (fc6e0d50), plus unit tests around the prompt-update queue hydration/persistence rules.

🤖 Generated with Claude Code

@siddhartpai
siddhartpai requested a review from a team as a code owner July 27, 2026 11:45
@siddhartpai siddhartpai changed the title feat(spawner): server-hosted agents via buzz-spawner daemon + server-aware agent editing [WIP] feat(spawner): server-hosted agents via buzz-spawner daemon + server-aware agent editing Jul 27, 2026
siddhartpai and others added 28 commits July 27, 2026 23:26
buzz-spawner daemon runs agents in containers, attested via the
NIP-AS kind:24201 encrypted handshake. Agents can relocate from the
desktop to a spawner keeping their pubkey, channels, profile, and
NIP-AE memory.

Relocation is persisted as state, not just performed as an action:
the hand-off marks relocated_to_spawner on the ManagedAgentRecord
(publish -> mark -> stop), and every local start path — manual start,
app-launch restore, runtime reconcile, lazy @mention wake, and the
auto-restart policy — refuses a relocated identity. Without this the
auto-restart loop resurrected the local copy after hand-off, running
one identity in two places (duplicate replies, double-billed turns).

Mentions of a relocated agent skip the local wake entirely; the
server copy sees them through the relay. The Agents screen shows a
server badge instead of Start for relocated agents.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…al::hash

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ames

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…date send path

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…size guard

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A desktop prompt update carries only system prompt, model, and provider, so
replacing the stored material wholesale wiped the agent's team instructions on
the next container restart. Carry them forward in a new record field instead of
merging them into the stored prompt: that field must stay byte-identical to the
frame the owner sent, since its hash is what the status event echoes and what
the client matches to confirm the update landed.

Also warn on `PromptMaterial::hash` that the hash is published world-readable,
so no credential field may ever join the struct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The queue read storage at module import, when the cached relay origin is still
null, so it loaded from a bare key while every write went to the origin-scoped
one — pending updates never survived a restart. Hydrate lazily on first access
instead, and make the community reset clear only in-memory state: persisting an
empty map under the departing relay's key deleted that community's pending
edits.

Ack by the agent pubkey the spawner reports, with the slug only as a fallback
for a status that has none. The slug a client queues under can drift from the
spawner's (name-derived fallback, or a rename), and matching on it alone meant
an ack that never arrived — a resend and a container restart every three
minutes, forever.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An all-blank edit was still queued, but the spawner drops empty material
without storing or acking it, so the entry stayed pending and was resent
forever. Skip it at the push site.

The pending chip also claimed the server was offline during the normal
awaiting-ack window; it now says that only when the send itself failed, and it
renders for any agent row with a pending update rather than relocated ones
alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t a resolved relay origin

The community reset nulls the cached relay origin right after resetting this
queue, and it only refreshes asynchronously — so any access in that window
hydrated from the shared empty-origin key and latched hydration for the whole
session. The new community's persisted entries never loaded, and the next write
pushed that wrong-key map onto the real one.

Skip hydration (without latching) and skip persisting while the origin is
unknown, merge rather than replace when it finally resolves so an edit queued
in the meantime survives, and hydrate on the origin subscription instead of
waiting for the next access.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… agents

A spawner-hosted agent authenticates on the server (e.g. OAuth), but both
edit dialogs still ran local model discovery and gated Save on local
provider credentials, so editing such an agent demanded an ANTHROPIC_API_KEY
this machine never uses and surfaced a spurious "Enter an Anthropic API key"
warning.

- usePersonaModelDiscovery: new serverManaged flag skips local discovery
  and the "Runtime not available" warning for server-hosted agents; the
  model catalog already comes from the spawner announcement.
- localModeGateSatisfiedForSubmit: definition-dialog Save waives local
  credential env keys when server-managed (provider/model still required).
- computeEditAgentFormValidity: instance-dialog Save ignores
  requiredEnvKeyMissing when server-managed.

Mirrors the existing Databricks OAuth precedent of not requiring a typed
credential when auth happens elsewhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ial status

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
siddhartpai and others added 9 commits July 27, 2026 23:28
… host-global keys

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… updates with ack and restart

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ential badge

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in fixtures)

Post-rebase integration: main's build_managed_agent_summary gained a
GlobalAgentConfig parameter and two new test fixtures construct
ManagedAgentRecord, which now carries relocated_to_spawner. Also drop
the runtimeAvailabilityWarning import left unused by the server-aware
Edit dialog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: sid <siddhartpai@gmail.com>
@siddhartpai
siddhartpai force-pushed the server-agent-editing branch from 0b5a872 to 387ac5b Compare July 27, 2026 18:09
siddhartpai and others added 3 commits July 28, 2026 00:02
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: sid <siddhartpai@gmail.com>
…e build

Lead with the one rule that explains most failures (every client must use
the same public hostname, since the relay derives the tenant from the Host
header), document the silent cross-host spawner failure mode, explain
BUZZ_SPAWNER_AGENT_RELAY_URL constraints inside containers, and show how
to build a local buzz-acp image instead of pulling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: sid <siddhartpai@gmail.com>
…wner identity

Container names are owner+slug scoped, but list() only sees containers
labelled with the current spawner pubkey. A container created by a
previous identity (wiped state dir, re-minted nsec) is therefore
invisible to reconcile while still holding the name, and every create
for that slug fails with Docker 409 forever.

On a 409, inspect the squatter: if it carries the com.buzz.agent label
it is spawner-managed (whichever identity created it) — remove it and
retry the create once. Unlabelled containers are refused, so an
operator's unrelated container of the same name is never touched. The
workspace volume is a named mount and survives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: sid <siddhartpai@gmail.com>
@siddhartpai

Copy link
Copy Markdown
Author

This branch has been split into a reviewable 4-PR stack: #3185 (spawner foundation, ready for review) → #3186 (prompt/model editing) → #3187 (per-owner credentials) → #3188 (docs & deploy). Same content, re-signed and rebased onto current main. Review #3185 first; the drafts shrink to their own commits as predecessors merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant