docs: formal spec for remote agents and their management - #3748
Conversation
Specifies the backend-provider plugin protocol (discovery, info/deploy, payload, security obligations), the remote lifecycle model (presence-is- status, !shutdown, inactivity self-stop), and the Kubernetes binding (buzz-backend-kubernetes) in the style of git-on-object-storage.md: stated invariants (I1-I5), explicit trust boundaries, a provider conformance checklist, implementation correspondence table, and the open decisions (A-E) pending owner ruling. Design thread: buzz-remote-agents 0eba4858 (Eva/Dawn/Wren). Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…review Wren's four correctness blockers: - I4: deploy restated as a reconciliation loop with normative conflict convergence (create-conflict re-reads and returns the winner, delete-not-found is success, loop bounded by the 600s deadline) - Truncated-label selection now requires full-pubkey annotation verification before any no-op/delete/Secret-mutation/return; mismatches are never GC'd - I5 restated as bounded *idle* lifetime / inactivity reaping, conditional on a live harness event loop; not an absolute TTL - M1 restated as no persistent post-deploy session / no substrate API in the protocol, not credential absence (kubeconfig is ambient by design) Wren's three contracts: nsec->pubkey derivation as step 0 with exact annotation/label/Secret/pod-name/agent_id names; Running->no-op means zero mutation (Secret lifecycle now follows the pod generation, resolving the old 'replaced atomically on re-deploy' conflict); full pod launch ABI (entrypoint, payload->env table, comma-join agent_args honesty note, HOME/cwd, image override must carry the runtime ABI). Dawn's three findings: deletionTimestamp checked before phase (no 'Terminating' phase exists; delete-marked pods stay Running), delete returns before the name frees (poll for 404; terminal pods delete immediately), entrypoint must exec so buzz-acp is the signal-receiving PID 1, and ~37s requalified as the bounded tail after an unbounded pool shutdown. GC redefined as a preflight reconciliation pass. Conformance section now points at fake-provider + envtest/kind tests instead of mechanization. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…seconds Wren round-3 findings: - Secret split-brain race: a deterministic shared Secret name lets a losing concurrent deploy's payload win the Secret write while the winner's pod wins the name — the winning generation runs the loser's config. Fixed with per-attempt immutable uniquely-named Secrets (buzz-agent-<12hex>-<gen>); pod spec references its exact Secret, pod creation elects the winner, losing contenders delete only their own unreferenced Secret, GC reaps annotation-verified orphan Secrets. - inactivity_seconds was inert: the ABI table hardcoded BUZZ_ACP_EXIT_AFTER_INACTIVITY=7200 while the schema exposed the field. Now one knob: config field feeds the env var, 7200 is the schema default (Auto-Stop section updated to match). - 'generation' now has a concrete definition in §GC (one pod-create attempt + its uniquely-named Secret; current = referenced by the surviving pod's envFrom). Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…cs, GC age gate, hardening Fold in verified findings from Dawn, Wren, Mari, and Perci: - §Launch data: typed launch block with three-tier env precedence (overridable policy_env → user/layered env → authoritative), exact policy_env contents (team instructions, resolved session title, owner_pubkey, unconditional runtime default_env, lazy_pool=true, MCP_HOOK_SERVERS), dedup/steer deliberately unset (harness defaults), host-resolved re-derivation incl. CLAUDE_CODE_EXECUTABLE, generation token doubling as BUZZ_MANAGED_AGENT_START_NONCE - §Deploy State Machine: startup is part of create — success requires container state.running; started/never-started rows; 600s deadline is the explicit cold-start budget, expiry = startup-not-confirmed - §K8s GC: normative orphan-Secret age gate (>2× deploy deadline) closing the preflight-GC vs in-flight-attempt Secret race; ownerReferences alternative considered and rejected with rationale; log shipping named a production prerequisite with correlation contract - §Discovery: shadowed/invalid candidate diagnostics; §info: protocol_version with compatible-major rule - §Pod shape: normative hardening defaults (no SA token automount, non-root, no privilege escalation, drop-all caps, RuntimeDefault seccomp); §Image: digest-pinned default reference, :latest rejected - §Auto-Stop: reaper must not depend on pool_ready (lazy-pool deadlock), BUZZ_ACP_EXIT_AFTER_INACTIVITY joins RESERVED_ENV_KEYS; Known Defect 4 records the unimplemented reaper and the tick trap - Open Decisions F–H (mesh deployability, remote override semantics, startup budget SLO) added for ratification Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Rich guarantees are allowed; complex implementations are not. Name the rule explicitly in §Invariants: every property in the spec was chosen because its enforcing mechanism is one small, boring thing, and a conforming implementation that is not small is evidence of a spec bug. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…n-thrashing startup classification Round-6 review amendments from Dawn and Wren: - Same-clock rule (normative): the orphan-Secret age comparison must use the apiserver's clock via the HTTP Date header on the GC list call; absent/unparseable header means skip orphan GC for the pass, never a local-clock fallback. A fast desktop clock is not a race — it deterministically deletes every in-flight Secret. - Corrected ownerReferences rationale: never-started winners are repairable (kubelet retries a nil-status container regardless of restartPolicy: Never), so Secret-first wins on diagnostics, not unrepairability. ownerReferences omitted entirely in v1 under the complexity budget; constraints stated (blockOwnerDeletion: false, same-namespace, non-deploy-failing) for any future attachment. - State machine: the single fatal never-started row split into three — provably non-recoverable (evidence, not reason strings), past-bound on the apiserver clock, and self-healable-within-bound (Unschedulable during scale-from-zero, pull backoff, transient config errors) — preventing thrash and preserving the cold-start budget. - Conformance: never-started winner repairable (delete-recreate, not no-op) and fast-local-clock must-not-delete cases added; conformance item 4 reworded to the evidence-based classification. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…, preconditioned deletes, anti-livelock startup Round-7 review amendments from Wren and Dawn: - Precedence: the four record-derived behavior knobs (system_prompt, idle_timeout, max_turn_duration, parallelism) fold into launch.policy_env; provider MUST NOT remap the top-level copies — locally they sit before the user env layer (runtime.rs:716-729,763 vs :860) and are not reserved, so remapping would invert an override that works locally. - Pre-secret negotiation gate (normative + Known Defect 5): resolve provider once, invoke info, check protocol_version, then deploy on the same executable identity before the nsec crosses; UI-time probe does not satisfy the gate. provider_deploy at c1bca1b sends deploy unchecked. - Redaction follow-through on Known Defect 3: launch.env / launch.policy_env values join the literal-value scrub; env_secrets_from_request currently reads only agent.env_vars. - Controlled-view rule (normative): destructive-authorizing reads use quorum semantics (resourceVersion unset), and every classification- authorized DELETE carries UID+resourceVersion preconditions from that observation; failed precondition re-enters, never retries. Unified with the same-clock rule. - Anti-livelock: the past-bound age-delete row is removed — recoverable startup timeout is observational across calls (any finite pod-age threshold can collide with Cluster Autoscaler's new-pod-scale-up-delay and delete-recreate resets the age it keys on). 600s bounds only synchronous wait; creationTimestamp survives. Decision H narrowed to a UX-only SLO. - Conformance: classification→DELETE race, provisioning-after-600s adoption, protocol-mismatch and executable-replaced cases; item 4 rewritten. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…ytes executable identity Wren's round-8 findings on the pre-secret gate: - Missing protocol_version is an error, never a presumed 1: the one-major-cycle grace period granted the nsec to exactly the class of binary that never declared compatibility. No deployed provider population exists to grandfather; failing closed is also simpler (no migration clock). - Executable identity = staged bytes, not path+metadata: unchanged dev/inode/size/mtime misses in-place content rewrites, and a pathname can be swapped between the check and Command spawn (check-then-exec race). The gate now requires copying the resolved candidate into a desktop-owned private staging file (digest computed during copy) and running both info and deploy from that artifact, so the nsec goes to the exact bytes that answered info. Platforms where staged execution is impossible degrade explicitly to accidental-replacement detection and must surface the weaker level — never silently claim the gate. - Conformance: same-inode content rewrite and pathname-swap-after- validation cases replace the simple rename case; absent protocol_version rejection added. - Known Defect 5 and Implementation Correspondence updated to the stage-and-digest shape. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…t fingerprint Dawn's round-9 findings, with the fingerprint design converged by both reviewers: - 409 discriminator is Status.reason, never the status code: NewConflict and NewAlreadyExists both carry 409 (verified against apimachinery); reason `Conflict` -> re-read and re-classify, reason `AlreadyExists` -> the create-conflict convergence path. This does not contradict the container-waiting-reason distrust rule (API Status.reason is a machine-readable contract; kubelet waiting reasons are not). - Config-wedge escape: the never-started recoverable row splits in two (state machine is now seven rows). Fingerprint matches -> observe forever, never delete; fingerprint differs or absent -> preconditioned delete-recreate. Divergence is evidence, not a clock: age resets only on a user-initiated config change, so no autoscaler-threshold collision. - Create-intent fingerprint (annotation buzz.block.xyz/create-intent): unkeyed SHA-256 over the canonical non-secret create-intent template, computed pre-create. Scope covers only provider-controlled fields affecting scheduling/container creation; never Secret data or attempt identity — Secret values cannot cause the never-started wedge, and excluding them removes the dictionary oracle and any need for an HMAC key. Per-attempt Secret name in envFrom normalized to a placeholder; comparison is recorded annotation vs freshly-computed intent, never the admission-mutated live spec. Started pods strict no-op regardless of divergence; missing Secret stays with the absence check. - Conflict-path asymmetry: the create loser never applies the divergence row to the winner it just lost to (prevents ping-pong deletes); a subsequent deploy replaces a never-started divergent winner normally. - Conformance: third mandatory family — 409 split, identical-intent never deletes, wedge escape, started-pod no-op, admission no false divergence, structural serializer property (Secret values/generated name never change the fingerprint; fingerprinted fields do), and the conflict-path asymmetry. - Open Decision I: is divergence-triggered replacement enough, or does v1 owe an explicit "clear stuck deployment" affordance? Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Dawn's post-sign-off precision correction, seconded by Wren (non-blocking; both hold 9/9/9): "age resets only on a user-initiated config change" overclaimed. The fingerprint includes the resolved image reference, and for users who never set `image` that resolves to the digest the provider bakes at compile time — so a provider upgrade changes the computed intent with no user action. The divergence row now names both sources — user config change, and provider upgrade moving the baked default digest — and states why the second is deliberate (a fingerprint blind to the default resolution could never replace a pod wedged by a bad baked default: unpullable digest, wrong arch — exactly the population that cannot override `image`) and its bounded cost (an upgrade mid-cold-pull discards in-flight startup progress; not age-driven or periodic, so no autoscaler-livelock shape). Conformance: default-digest-change case added to the third family. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
## What Adds `VISION_REMOTE_AGENTS.md` — the vision doc for remote agents, joining the VISION family (`VISION_AGENT.md`, `VISION_MESH.md`, `VISION_SOVEREIGN.md`, …). The one-line thesis: **the relay is the management plane** — an agent's identity, history, presence, and ordinary control all live on the relay, so the body (a pod today, anything tomorrow) is replaceable, and deployment never grows a second control plane. ## Provenance - Distilled from the remote-agents spec (`docs/remote-agents.md`, PR #3748); this doc stays deliberately generic where the spec is Kubernetes-specific. - Five review rounds in the #buzz-remote-agents channel; both reviewers (Wren: thesis/shape/scope, Dawn: truthfulness/minimalness/elegance) converged at 9/9/9, scored against spec head `b4f4ed1a6` with command-level receipts. - Final editorial pass by Tyler (opening line, vignette phrasing, closing tagline), applied live in-channel before this PR. Doc-only change — no code, no effect on #3748, which remains blocked solely on the Open Decisions A–I rulings. --------- Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
There was a problem hiding this comment.
Naming the trust boundary, then
saying which properties hold despite an untrusted provider holding the nsec, is
the right frame, and the M1 argument (the relay was the management plane all
along) is the part that makes the rest small.
For whatever it is worth as a conformance data point: #3449 is a working
provider for a different substrate, buzz-backend-ssh, which deploys agents as
systemd --user units over SSH. It was built before this spec existed, and Jud
ran it end to end this week from macOS against a Debian 12 guest behind a
ProxyJump alias. I went through the invariants against it at 879f57da3. Most
of it lines up, and where it does not, the spec is usually right and we are the
counterexample. Rough tally:
- I1 identity fail-closed: conforms on both sides. The provider re-checks the
empty-key case itself rather than trusting the desktop
(buzz-backend-ssh/src/deploy.rs:108-115), which I would suggest making a
conformance item. It is oneif, and it catches a desktop regression at the
boundary instead of producing an identityless agent. - I2 no secrets in config: conforms. Five config fields, no credential in any of
them, passwords structurally impossible because every session runs
BatchMode=yes. One scar worth a sentence in I2: the anti-secret rule is a
key-name rule, so it rejects legitimate non-secret names. Our field is
ssh_identity_fileand notssh_key_pathonly because the latter word-splits
to["ssh","key","path"]and is refused byvalidate_provider_config
(backend.rs:461-491). The refusal is explicit and names the key, so it is
not a silent failure, but it lands at schema-design time on a field that
holds a path rather than a credential. One sentence in I2 saying the rule
matches key names, not values, would save the next provider author the
detour. - I3 presence-is-status: conforms, and the harness's
offlinepublish is the
same code path. - I4 at-most-one: conforms by deterministic pubkey-keyed unit naming. Inline
comment on step 0 about derive-or-verify and about the rekeying migration. - I5 bounded lifetime: does not conform, twice over, and the second way is a
spec-strengthening finding. Inline comment 2. - M1: partial and interesting. The SSH provider holds an SSH credential to the
host, but no op queries status, fetches logs, or kills anything
(main.rs:82-96), so the protocol-surface half holds. Your own kubeconfig
concession already covers the credential half. The thing SSH raises that
Kubernetes does not: here the staleness M1 buys is genuinely cheap to avoid
(onesystemctl is-activeis a 200ms round trip), so M1 as an absolute ban
rather than "no required management channel" is a bigger ask on some
substrates than others. Not asking you to change it, just flagging that the
price varies. - Provider protocol: discovery order, id regex, output caps, exit rule,
redaction, I2 validation and the reserved-key rule all match.launch,
protocol_versionand the staged-binary gate are absent on our branch too,
same as your Known Defects 3 and 5.
On Known Defects: #1 (Windows .exe id pollution) is already fixed in
#3449 by 285924daf, and slightly more than your one-line description. Strip
the extension before deriving the id, and also move deduplication from the
filename to the id, otherwise a host carrying both buzz-backend-ssh and
buzz-backend-ssh.exe offers two providers the desktop reads as different.
Stripping is unconditional rather than cfg(windows) on the grounds that what
id a filename means should not depend on which machine reads it, and a rule
that only compiles on one platform is one CI never exercises. Happy to split
that commit out as a standalone PR if it helps this land faster.
#4 I confirmed line for line against our tree, and it would have bitten us:
the tick is pool_ready-gated at lib.rs:1743, pool_ready starts false under
lazy pool (:1320) and flips only after a wake (:2570), and wakes need
pending work (:1713-1716). Our env file pins BUZZ_ACP_LAZY_POOL=true for
the same reasons yours does, so a reaper on that tick would have been dead in
exactly the case it exists for.
Two additions to #3's consequence list, both from running this:
BUZZ_ACP_RELAY_OBSERVER=trueis inert without a resolved owner. The frames
are encrypted and addressed to the owner's pubkey, so with neitherauth_tag
nor an owner the harness logs that it will publish nothing and the setting
does nothing. Easy to hit rather than theoretical: dropping the NIP-OA
identity is the current workaround for the cross-machine mention gate (#3277,
#2987), and such a deploy otherwise works fine: the agent answers mentions,
pushes branches, and only its telemetry is quietly missing. So your owner
resolution rule fixes a silent observability hole as well as!shutdown,
which is a stronger argument for it than the one currently in §Launch data.CLAUDE_CODE_EXECUTABLEneeds re-resolution, not deletion (inline 4).
One hardening note. We added NoNewPrivileges=true to the unit (7bbb95dd3)
after dschwartzAI raised it from a self-hosted VPS pilot: without it the harness
climbs past the SSH user's privileges through any setuid binary or through
passwordless sudo granted to that user. That pilot exercised the harness, the
CLI, NIP-OA owner-reviewed drafts and repo branch pushes with it set and
nothing broke. It
is deliberately the whole delta, for the same reason you defer
readOnlyRootFilesystem: ProtectSystem/ProtectHome would be guessing at
which of the user's home an agent legitimately needs, and there is no modeled
workspace yet. Which suggests stating the hardening floor substrate-neutrally,
"no privilege escalation beyond the launching principal", with
allowPrivilegeEscalation: false + drop-all and NoNewPrivileges=true as the
two bindings of it.
On DECISION A: the SSH binding has no workspace at all. The unit has no
WorkingDirectory, so an agent starts wherever systemd --user puts it with no
Buzz-native checkout, and in practice every long-running remote agent we ran
operated in a separately provisioned checkout someone made by hand. Recommend
yes on A, and recommend the workspace be a protocol field the desktop
states rather than something the image scaffolds, since the image cannot know
which project.
On DECISION E: we went the other way, restart rather than no-op, because a
systemd restart is about five seconds and immediate config application was
worth more than mid-turn safety at that price. A pod recreate is a cold pull,
which is a completely different tradeoff. Suggest making E a per-binding ruling
rather than a protocol-wide one, with the spec stating the property that must
hold either way (a Start never leaves two live instances, and whichever choice
a binding makes is documented in its section).
Finally, a mechanical thing: this PR adds docs/remote-agents.md as a new file
and so does #3449, so whichever merges second gets an add/add conflict. Yours is
the normative spec and should keep the name. I will rename ours to
docs/remote-agents-ssh.md and retitle it as the SSH binding, so this does not
sit on your critical path. If you would rather absorb a short "§The SSH
Binding" section here (unit shape, lingering, PATH, NoNewPrivileges,
Restart= semantics) with the long-form troubleshooting staying in the
separate file, I will write that section against whatever shape you want.
Happy to take any of the above as issues instead of spec edits if that gets
this merged sooner. The thing I care about is that this ships natively.
| zeroes the grace period and deletes immediately, so the normal restart | ||
| path needs no meaningful wait — do not add a fixed sleep, and do not use | ||
| zero-grace cleanup as a reason to skip the poll in the live-pod arm. | ||
| - **`terminationGracePeriodSeconds: 60`.** The harness's graceful shutdown |
There was a problem hiding this comment.
The derivation is missing a term. There are two 30s timeouts on that path in
series, not one: buzz-acp/src/lib.rs:2612-2615 drains wake tasks under
timeout(Duration::from_secs(30), ...), and it runs before the prompt drain
you counted at :2629-2636 (also 30s). Then 2s presence (:2702-2712) and 5s
relay close (:2720, budget confirmed in relay.rs:900-912).
I do not think that makes the tail 67s today, and I want to give you the
argument rather than just the number. The two drains look close to mutually
exclusive: pool_ready starts false under lazy pool (:1320) and flips true
only after a wake completes (:2570), and the only non-test producers of
pool.join_set are dispatch_pending (:2968, reached only after a
successful try_claim) and dispatch_heartbeat (:3581, gated on
pool.any_idle()). Before the wake lands, no slot holds an agent, so the
prompt drain has nothing to wait on; once it lands, wake_tasks is empty. One
30s branch plus 2 + 5 is the realistic worst case, so 37s survives.
What I would change is that this is nowhere stated. The mutual exclusion is an
emergent property of the lazy-pool state machine, not an enforced one, and the
spec mandates BUZZ_ACP_LAZY_POOL=true for remote pods (§Launch data), so the
lazy path is the only path here. A later change that lets a wake overlap a live
pool moves the tail to 67s and silently makes 60s too small, with no test and no
sentence anywhere pointing at it.
Two suggestions. Name both timeouts in the derivation and say why only one is
payable, so the invariant is written down where someone changing the pool will
see it. And consider bounding both drains against one shared deadline in the
harness, which is a few lines and makes 37s true by construction rather than
true by coincidence. Worth the precision because your own qualifier ("anyone
tuning a grace period downward from 37s is reading the number without its
qualifier") tells readers to treat the figure as load-bearing, and they will.
Smaller note on the same budget: the per-agent reap is bounded at 5s each
(acp.rs:440, SIGKILL then a bounded wait) and runs serially over slots, so the
segment after the drain scales with BUZZ_ACP_AGENTS rather than being free.
| - **(I5) Bounded idle lifetime.** No remote agent instance outlives its | ||
| usefulness *silently*: an instance whose harness is live terminates on | ||
| owner `!shutdown` or when inactivity exceeds a configured bound, and the | ||
| substrate never resurrects a terminated instance. This is inactivity |
There was a problem hiding this comment.
This clause has more teeth than it looks, and I can say so because the first
non-Kubernetes binding violated it and I did not notice until I sent
!shutdown and watched the agent come back.
We run remote agents as systemd --user units in #3449. The unit is
Restart=always / RestartSec=5, which is the obvious default for a
long-running agent and the exact opposite of restartPolicy: Never. So
!shutdown stops the process, systemd restarts it five seconds later, and
both halves of I5 evaporate: the owner stop does not stop, and an inactivity
reaper (once BUZZ_ACP_EXIT_AFTER_INACTIVITY exists) would reap into an
immediate respawn loop. Under systemd, conformance costs a distinguished clean
exit code plus Restart=on-failure, or an explicit self-systemctl stop.
Neither is free and neither is obvious.
Suggestion: promote this from a clause inside I5 to a named conformance item
with the substrate obligation spelled out, something like "the substrate's
restart policy MUST treat harness exit as terminal; a supervisor that restarts
on clean exit voids I5 and the !shutdown contract". Kubernetes gets it by
picking a non-default restartPolicy; systemd gets it by picking a non-default
Restart=. Both are a deliberate choice against the substrate's default, which
is exactly the kind of thing a spec should name.
Related: restartPolicy: Never also gets you the harness's own exit code for
free as a signal. Under systemd we would need that code to distinguish
"reaped for inactivity" (do not restart) from "crashed" (do restart), and
buzz-acp currently exits 0 on the graceful path with no way to tell the two
apart. If the reaper lands with a distinct exit code, both bindings benefit.
| - Bounded reads: stdout capped (1MB), stderr capped (64KB), no `read_to_end` | ||
| on pipes a daemonizing child could hold open; deadline polling with | ||
| `try_wait`. | ||
| - **Non-zero exit is failure even if stdout parsed.** Partial output from a |
There was a problem hiding this comment.
This is right, and it contradicts Conformance item 1, which asks for
"meaningful exit codes". A provider author who reads item 1 first will encode
failure classes in the exit status and lose every structured error, because
invoke_provider discards stdout entirely on non-zero exit
(backend.rs:194-208) and reports raw stderr instead.
Our SSH provider resolves it by always exiting 0 and putting every failure
in the JSON body (buzz-backend-ssh/src/main.rs:59-62). That is not a
workaround, it is the only reading of §Invocation that works: exit code carries
exactly one bit, "the process crashed vs it did not", and an operation failure
is a successful process emitting {"ok": false}.
Worth saying that outright in §Invocation and changing item 1 from "meaningful
exit codes" to something like "exit 0 for any operation that produced a
response, including in-band failures; non-zero means the process itself
failed". Otherwise the two sections are a conformance contradiction, and the
half a provider author picks decides whether their error messages ever reach a
user.
One concrete thing that rides on this: we return an optional
recovery: {action, url} alongside {"ok": false} for the one failure that has
a machine-actionable fix (a tailnet ACL demanding a browser re-auth). It is
additive and unnegotiated: a desktop that does not know the key still renders
error, which carries the URL as text. If the spec wants providers to surface
actionable recovery rather than prose, an optional recovery object is a cheap
place to standardize, and it only works if the failure response is trusted,
which means exit 0.
| crashed operation is never trusted. | ||
| - `{"ok": false, "error": …}` is the in-band failure form. | ||
| - **Environment**: `P` inherits `D`'s environment. On macOS a GUI launch | ||
| means launchd's minimal PATH; providers whose substrate credentials invoke |
There was a problem hiding this comment.
This is the same bug as Known Defect 2 and it is bigger than macOS. I hit it on
the other end of the wire, and there it is not a credential problem, it is a
"the product does not work" problem.
A non-interactive ssh host sh -s reads no profile. On stock Debian that
leaves PATH at /usr/local/bin:/usr/bin:/bin:/usr/games, which does not
contain ~/.local/bin, where every Buzz tool installs, and where pipx and
npm --prefix=~/.local put the ACP adapters. On a host with all twelve
adapters installed and runnable, discover_harnesses reported the entire
catalog as available: false, and the deploy that followed refused the
operator's own pin with "harness not found". The host was fine. Fixed in
e8a8e34fb with one shared preamble prepended to every generated script, and
the unit's own PATH then written from that same shell's $PATH verbatim so the
two cannot drift.
The generalized rule I would put in this bullet: PATH is a property of how a
process was started, at both ends of the protocol. Provider-side (launchd),
and agent-side (non-interactive SSH, systemd --user, cron, anything that is
not a login shell). The Kubernetes binding gets a free pass on the agent side
because the image controls its own PATH, which is precisely why a
Kubernetes-first spec would not surface the second half.
Small detail from getting this wrong once: write the prepend as
export PATH="${HOME:-}/.local/bin${PATH:+:$PATH}", not :$PATH. With PATH
unset the naive form leaves a trailing empty element, which means the current
directory, and that is the one PATH value worth refusing to write.
Also, since §Launch data says CLAUDE_CODE_EXECUTABLE is a host path that must
not be forwarded: dropping it is not sufficient, it has to be re-resolved,
preferring ~/.local/bin/claude (the native launcher, which follows Claude
Code updates) over the adapter's bundled point-in-time binary. Unset it and you
silently get the stale bundled SDK build, which is a version regression rather
than a failure, so nobody notices.
|
|
||
| **Step 0 — derive and verify identity.** The payload carries the nsec, not | ||
| the pubkey. Before any substrate read or mutation, the provider MUST parse | ||
| `private_key_nsec` and derive the public key from it; a malformed or |
There was a problem hiding this comment.
Strongly agree with keying on the pubkey, from having shipped the bug this
prevents. Our first version keyed the systemd unit on the display name. Two
agents both called "Research Bot" on one SSH account therefore shared one unit,
one env file and one backend_agent_id, so the second deploy overwrote the
first agent's minted nsec and starting either record drove whichever identity
was written last. 449a5b9ae. We independently landed on the same 12-hex
fragment you specify, which is a decent cross-binding check on the choice.
Two things I would add here.
First, "MUST derive, never a caller-supplied pubkey" prices in a dependency the
spec does not mention. Our SSH provider has five direct dependencies (serde,
serde_json, zeroize, base64, sha2); deriving the pubkey means linking secp256k1
plus bech32, which is a real addition to that surface for a binary whose whole
job is to shell out to ssh. A middle ground with the same guarantee: the provider
MUST derive or MUST verify a supplied pubkey against the key it derives, and
a mismatch is a fatal in-band error. That names the actual property (the unit
name and the identity running in it agree) rather than the implementation, and
it keeps thin providers thin. Right now we take the payload's pubkey and
validate its shape (64 hex chars) but cannot cross-check it, which is the gap
your rule closes and I would rather close it cheaply than not at all.
Second, deterministic identity-keyed naming has a migration hazard worth one
sentence in this section. When the naming rule changes, the instance name
changes, so a host provisioned under the old rule gets a new instance
alongside the old one on redeploy rather than a replacement. In our case the
orphan kept running under Restart=always with an env file that still held an
nsec, and with no undeploy op there was nothing to clean it up with; we had to
ship manual removal instructions (41b8a628d). Kubernetes has the same shape
(old pod name, old Secret, GC only reaps terminated pods matching the current
label). Since §K8s GC keys on buzz.block.xyz/agent-pubkey, any future change
to the truncation length or the label key strands the previous generation
permanently.
|
I'm curious - why does an agent have to be spawned by a desktop app? Why can't I start up a relay and an agent connect to it without ever opening the desktop app? The first announcements I saw of Buzz said it was different to Slack because Agents were first-class and not owned by users, but Agents appear to be tied directly to a specific human's running desktop (even in this spec about remote agents). Everything appears to be architected so that communication with an agent is always through the relay, so I don't understand why starting them is tied to the desktop app. Why can't I start an agent from a CLI in a container, and send its public key to an admin to allow to join the server? What happens if I need to restart the host running a relay and agents - can the agents not continue unless I open the desktop app on the machine where I set them up? 🤔 |
|
At the risk of naivety: was registering the substrate itself on the relay ever considered — VMs, clusters, etc. as keypairs, so deploy targets are relay principals the way agents are? I can see M1 deliberately keeps the provider protocol free of any substrate API, so I'm curious whether a relay-registered environment was considered-and-rejected, or is just beyond v1's scope. |
|
Reviewed from the position of an operator running the #3449 SSH provider in Everything below was verified against the tree at #3449 head The two documents collide#3449 and #3748 both add Where the spec and the SSH provider disagreeOrdered by how hard they bite. 1.
|
|
+1. would love the same UX as a desktop spawned agent for a remote runtime. |
Creating an agent outside the desktop will work perfectly fine as long as you have your nsec to sign a NIP-OA tag with when you spawn an agent.
Agents can run independently or can be managed by a principle like a user or team (given a NIP-OA tag with specific authorization). NIP-OA ("owning an agent") makes it easier to run a relay with an npub allowlist so that the relay owner/admins don't need to add every agent individually. It's also nice to know who is responsible for a given agent.
You can do this! The agent harness will retry with backoff to connect to a failed/restarted relay. @DanTup great questions. Thanks for participating here! |
This is an interesting idea! Your substrate would need to support sending events to the relay for this to be useful. ie "I'm about to shut down", "I have a hardware issue", "Restarting proc 1", etc If you have a subtrate that wants to adopt this, we can look at this idea @Shipitrealgood For now, we'll target generic ones to get folks going and look at Buzz specific substrates later. We have something in mind already. Keep an eye out ;) |
…cher layering, shutdown budget Folds the channel review (Tyler's two rulings, Sami's C1/C2 + F1-F3, Wren's three-layer taxonomy and guardrails) into one revision: - I5 restated: the invariant is "stops when told, stays down, never silently resurrected" — intentional termination is final. Bounded idle lifetime demotes to the Kubernetes binding's default policy; `inactivity_seconds: 0` is a legal, blessed value meaning no inactivity bound (indefinite agents are an explicit owner choice). - New §Launchers: three nested contracts — [L1] agent/harness (binds every launcher, including a bash script with an nsec + NIP-OA tag), [L2] provider/deployer, [L3] per-substrate binding policy. The desktop is one launcher among many; two-op protocol and deploy-scope uniqueness are L2, not universal. Conformance items tagged by layer; item 4 restated substrate-neutrally with the K8s spellings as the binding's L3 realization (Sami F3). - Restart policy follows lifetime policy: bounded -> `restartPolicy: Never`; indefinite -> `OnFailure`, gated on a pinned exit-code contract. New Known Defect 6: "graceful exit = 0" is emergent (`Ok(())` at lib.rs:2460 path; SIGTERM into the shutdown channel), defended by zero tests — the distinguished-exit-code contract plus pinning test MUST land before any OnFailure/Restart=on-failure ships (Sami C2, Troy's systemd primitive). `Restart=always` remains non-conforming at every layer. Wren's wording taken: clean exit is terminal *to automatic supervisor restart*; a fresh owner Start is still resurrection working as intended. - Shutdown tail is a budget, not a sum (Sami C1). The "~37s" arithmetic was wrong in kind: the post-drain reap loop is serial, 5s per occupied slot (acp.rs:393), outside the 30s drain timeout — ~157s worst case at the default parallelism of 24 (DEFAULT_AGENT_PARALLELISM, types.rs:725), ~197s at the harness cap of 32, against a 60s grace. New Known Defect 7 + normative rule: the harness MUST bound its total shutdown tail under one shared deadline <= the declared grace budget, degrading rather than overrunning. - I3: staleness bound pinned to 90s (PRESENCE_TTL_SECS, buzz-pubsub/src/presence.rs:16); `BUZZ_ACP_NO_PRESENCE` (config.rs:377) MUST join RESERVED_ENV_KEYS — remotely, presence is the only signal, so the knob converts I3 into "wrong indefinitely" (Sami F1). Community-scoping consequence documented. - I2: the anti-secret rule documented as a key-name lint that accepts false positives (`ssh_key_path` holds a path, not a credential) — rename fields rather than weaken the validator (Troy's scar). - §Deploy State Machine: auto-repair fenced to Buzz-authored, positively identified residue (ownership/fingerprint evidence); unknown or substrate-authored wreckage still fails closed to the operator (Wren's guardrail; Open Decision I partially ruled). - Open Decisions: A revised (nest workspace as a desktop-stated protocol field, not image scaffold); E ruled (per-binding; universal property is never-two-live-instances). Implementation Correspondence gains rows for the exit-code contract and the shutdown-budget enforcement, both "to be added". All code citations re-verified against pinned commit c1bca1b. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…p marker, finalization slice Fixes the three blockers plus both recommendations from Wren's review of 4432030 (the layer split leaked in three load-bearing places): - Conformance restructured into three checklists, one per §Launchers layer, replacing the mixed tagged list. [L1] is now a launcher-neutral five-item contract (nonempty identity; promises not suppressed — presence stays on, inactivity knob is deliberate owner policy; signal reaches the harness; intentional exit under the pinned clean-exit contract; supervisor never restarts a clean exit) — a systemd unit can satisfy it and a bash launcher can no longer satisfy it vacuously. "Container/PID 1" wording moves to the Kubernetes [L3] list. [L2] keeps the wire/reconciler items plus a generic L3 obligation stated as a property (document lifetime + clean-exit restart realization); the K8s spellings (inactivity_seconds, OnFailure/Never selection, terminationGracePeriodSeconds) live only under [L3]. Cross-references updated (testability paragraph now cites L2/L3 items). - I1 restated launcher-neutrally: "no agent is ever launched identityless, refused wherever the env is assembled", with the payload refusal as the provider path's enforcement — no longer only "no deploy request is emitted". - Auto-repair fence now requires positive ownership evidence, not identity evidence: new management marker (app.kubernetes.io/managed-by: buzz-backend-kubernetes + buzz.block.xyz/binding-version) on every pod and Secret, required before any destructive repair or GC — on top of the annotation check and UID+resourceVersion fence. Honestly framed as protocol evidence, not cryptographic proof (cluster writers can forge metadata; the marker fails accidental collisions closed). §Pod shape labels, §K8s GC pass, L2 item 4, and a new mandatory conformance case (marker-absent object is never deleted) all updated. - Shutdown budget gains a reserved finalization slice: presence offline publish + relay close are held back out of the shared deadline and child reaping degrades first, so cleanup cannot legally consume the entire grace before the one action the grace exists to protect. §Stop, §Pod shape grace bullet, Known Defect 7 aligned. - Minimalness: §Pod shape's OnFailure bullet no longer re-argues I5's rationale; it states the selection and points at I5. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…prerequisite, coercion defect Fixes Sami's R1 (blocker), R3, and the flagged minor from his review of 4432030. (R2 was independently fixed by ffe760a — Wren's blocker 1/2, same finding.) - R1: the parallelism "correction" was backwards. c1bca1b — the document's pre-existing citation pin — is an unmerged feature-branch commit that diverged from main Jul 18 and predates #3038 (default parallelism 24 -> 10, which IS in this PR's base b4f4ed1). The shutdown-tail worst case is ~87s at default parallelism 10, not ~157s at 24. Corrected in §Stop, §K8s Grace, and Known Defect 7, with every line reference in those passages re-verified against b4f4ed1 itself (drain timeout lib.rs:2636/2657, reap segment :2664-2688, graceful Ok(()) :2723, !shutdown :2045, Ctrl-C :1635, SIGTERM :1644, acp.rs:436, types.rs:809, config.rs:293, config.rs:378 for no_presence). Known Defects gains a citation-pin caveat naming the stale pin and promising a follow-up re-pin; structural claims unchanged (87s still blows the 60s grace). - R3: OnFailure's second prerequisite named. A crash-looping harness (phase Running, waiting{CrashLoopBackOff}, restartCount > 0) falls through every deploy-state-machine row and fails the started criterion while the kubelet is reviving it. §Pod shape now states that the exit-code contract alone is NOT the green light: a crash-loop classification row + restartCount treatment in the started-criterion MUST land before the binding ships OnFailure; L3 conformance item 2 gates on both prerequisites. - Known Defect 8 (new): coerceConfigValues (ProviderConfigFields.tsx:6) ships a cleared numeric field as "" — a JSON string — and blessing inactivity_seconds: 0 puts that arm on a documented path. Desktop fix (cleared -> omit-or-default) plus provider-side in-band error for non-numeric numerics. Verified: git merge-base --is-ancestor c1bca1b b4f4ed1 -> false; DEFAULT_AGENT_PARALLELISM = 10 at b4f4ed1 types.rs:809; all replaced line references checked against git show b4f4ed1 in this worktree. Surviving pool_ready cites (lib.rs:1743/:1320/:2570, pool_lifecycle.rs:42) hold identically at b4f4ed1 and were left. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…nded not 87s, finalization slice quantified Fixes Wren's three blockers from his re-review at 2076faf. - Blocker 1 (I5 layer leak): the universal I5 restart-policy paragraph no longer mandates Kubernetes spellings. Generic rule: if a supervisor exists, its policy MAY revive abnormal death and MUST NOT revive an intentional clean exit; a supervisor-less hand launch satisfies it vacuously. The bounded->Never / indefinite->OnFailure mapping is named as this binding's [L3] policy living in §Pod shape; enforcement prose now says 'the substrate's unit of execution' with the container realization pointed at §Pod shape. Same fix applied to the §Auto-Stop closing paragraph, which repeated the mapping in universal text. - Blocker 2 (partial sum labeled worst case): ~87s/~197s are now stated as what the counted segment ALONE can reach — already proving the 60s overrun — not the worst case. All three passages (§Stop, §K8s Grace, Known Defect 7) now name the earlier uncounted segments at b4f4ed1: the 30s wake-task drain (lib.rs:2612) and the awakened-pool shutdown (:2620-2624) whose per-slot acp.shutdown() loop (:3747-3751) has no timeout, and state the total tail is not bounded by today's segment timeouts. - Blocker 3 (unquantified finalization slice): the reserved slice MUST be no smaller than the finalizers' declared bounds — currently 2s presence + 5s relay close = 7s — in §Stop, §K8s Grace, and Known Defect 7. Verified: wake-drain/awakened-pool/shutdown_agent_pool line numbers checked against git show b4f4ed1:crates/buzz-acp/src/lib.rs in this worktree. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…aph launcher-neutral Fixes Wren's remaining blocker at c249b28: the §Auto-Stop closing paragraph still mandated container/PID-1 vocabulary in universal text (the same L3 leak removed from I5 one commit earlier — I fixed that paragraph's tail sentence and missed its opening). - The universal rule now reads: harness exit MUST terminate the substrate's unit of execution; the substrate's termination signal MUST reach the harness process itself; any wrapper MUST forward it. The container/PID-1 spelling survives only as this binding's realization, pointed at §K8s Entrypoint's exec rule and §Pod shape ([L3]; L1 item 3 carries the universal form). - I5's 'Lifetime is owner policy' paragraph no longer implies every launcher exposes inactivity_seconds: the universal choice is 'no inactivity bound'; inactivity_seconds: 0 is named as the Kubernetes binding's spelling of it (§Pod shape), with the hand-launcher realization (never set the reaper env) alongside. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
- Incorporate upstream desktop changes required by the pre-push overlap guard - Preserve the link preview thumbnail stabilization work on the updated base Co-authored-by: Taylor Ho <taylorkmho@gmail.com> * origin/main: (26 commits) docs: formal spec for remote agents and their management (#3748) fix(nip-oa): accept raw Nostr tag form in parse_json_array (#4203) perf(relay): serve relay-membership checks from the read replica (#4124) chore(deps): bump nostr-relay-pool for RUSTSEC-2026-0224 (#4139) docs(nostr): document #h requirement for live reaction subscriptions (#3487) docs(chart): fix ArgoCD example for native OCI sources (full artifact repoURL + path) (#3426) docs(readme): clarify which release asset to download per platform (#3481) fix(relay): allow open relays to set their NIP-11 workspace icon (kind:9033) (#3998) docs: note that addressable channel events scope by d, not h (#4103) docs: fix stale kind count, quick-start numbering, and empty Further Reading (#2613) fix(desktop): keep thread-open affordance in archived channels (#4012) docs: add one-click Railway deploy for a hosted relay (#2733) fix(desktop): point Oh My Pi preset at omp.sh (#3516) fix(mesh): stop restarting a busy or loading shared-compute node (#3909) fix(desktop): preserve first huddle speech (#3962) feat(desktop): Agent Trading Cards — mintable agent-snapshot card PNGs with optional NIP-44 lock (#3278) fix(buzz-acp): thread cache-read tokens into NIP-AM kind:44200 events (#3999) feat(relay): accept kind:30621 multi-repo projects at ingest (#3171) fix(release): preserve main in desktop PR body (#3979) chore(release): release Buzz Desktop version 0.5.3 (#3972) ... Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
|
@tlongwell-block thank you for the replies!
Are there any examples/docs on this? I had a relay running and I wanted to spawn and connect an agent in a fresh container. I tried a few things like starting https://github.com/block/buzz/tree/main/crates/buzz-acp with codex set up, and it claimed to connect to the relay and found 0 channels, and it was never visible to me in the desktop. I had added its public key to the relay (via the relay admin scripts). I spent a bunch of time with various AIs and none of them could figure out what was wrong and they all concluded it was not supported. For now, I don't mind whether the agent is standalone if "owned by me", I just want to test and play around - although eventually it would be nice if the agents could be "owned by me" even when started this way. This is the thing holding me back from trying this out, I just won't run them on my desktop for anything 😄 I understand you're all very busy, but if there aren't existing docs/examples, an example script (even written by agents) would be really helpful. Maybe even an example agent in the Thanks! |
What
A formal specification for remote agents and their management —
docs/remote-agents.md— in the style ofdocs/git-on-object-storage.md: stated system model, named invariants, explicit trust boundaries, provider conformance checklist, and an implementation-correspondence table.Requested by Tyler in the buzz-remote-agents design thread; co-designed with Dawn and Wren (review pending).
Structure
info/deploywire contract, untrusted-output rules, the reserved-key rule, and the deploy state machine (Running → no-op).--exit-after-inactivity/BUZZ_ACP_EXIT_AFTER_INACTIVITY, default off, definition of "inactive", and why it must not share a name with the three existing timeout concepts.buzz-backend-kubernetes: kubeconfig-only auth, random-default namespace via schemadefault, the sprig image, pod shape (bare Pod,terminationGracePeriodSeconds: 60, 32-hex label / full-pubkey annotation), secrets, GC, config budget.c1bca1b56(Windows.exeid pollution; provider env inheritance vs kubeconfig exec plugins).Notes for review
Docs-only. Every code claim was verified against the tree (correspondence table maps each spec concept to its file/function). The spec deliberately documents two desktop bugs as Known Defects rather than fixing them here — fixes are follow-up PRs.