Watch
0
0
Fork
You've already forked hyperhive
0

read an agent's agents.md through the swarm and edit it with git diffs

The agent's state/AGENTS.md stays the truth and its harness the only
writer. The harness mirrors the file into a new `agents-md` KV bucket,
and swarm-controller serves that copy, so a read works while the agent is
stopped. An edit is a unified diff plus the revision it was made against,
sent over the agent's existing control subject; the harness refuses a
stale base, applies the diff with `git apply --check` then `git apply` to
a temp copy, and writes the file only when both pass. Fixes #5519.

Files:
- swarm-queue-client/src/agents_md.rs, lib.rs: the bucket name and
  subject, the mirror value, the `ApplyAgentsMd` control command and its
  reply. `AgentControl` loses `Copy` because the new variant carries text.
- hive-agent/src/swarm_agents_md.rs, main.rs: the mirror task (start,
  10 s re-read, immediate republish after an edit) and the apply.
- hive-agent/src/swarm_input.rs: the control loop answers both commands.
- hive-agent/src/claude_md_watch.rs: its AGENTS.md path is shared.
- hive-agent/Cargo.toml, Cargo.lock: sha1 for the git blob id (already in
  the lock), tempfile moves from dev to runtime for the apply directory.
- swarm-controller/src/agents_md.rs, main.rs: the bucket reader and
  GET/PUT /api/agents/{name}/agents-md; test AppState literals get the
  new field.
- swarm-controller/src/agent_control.rs: the sink hands back raw reply
  bytes, so each command decodes its own reply type.
- swarm-controller-api/src/agents_md.rs: the route bodies, shared with
  swarmctl.
- swarmctl/src/agents_md.rs, main.rs: `swarmctl agent agents-md get|apply`.
- nix/host-modules/swarm-nats.nix, nix/module-eval/nats-authelia.nix: the
  agent's publish grant on its own `$KV.agents-md.<agent>` key.

git needs no new package: agent containers enable programs.git, and the
harness unit's PATH includes /run/current-system/sw.
This commit is contained in:
atlas 2026-10-18 03:11:15 +02:00 • committed by mara
commit 88674eb63a
22 changed files with 1296 additions and 47 deletions

View file

@ -26,6 +26,7 @@
//! for the same reason `hivectl` does not link `hive-c0re`.
mod agent;
mod agents_md;
mod forge;
mod inference;
mod login;
@ -302,6 +303,43 @@ enum AgentVerb {
#[command(subcommand)]
command: PermissionsVerb,
},
/// Show an agent's AGENTS.md, or edit it with a diff.
///
/// `get` shows the copy the agent last published, so it works while the
/// agent is stopped. `apply` hands the diff to the agent, which applies
/// it to the file with `git apply`; it needs the agent running.
AgentsMd {
#[command(subcommand)]
command: AgentsMdVerb,
},
}
#[derive(Subcommand)]
enum AgentsMdVerb {
/// Print an agent's AGENTS.md on stdout, and its revision on stderr.
Get(InferenceAgentArgs),
/// Apply a unified diff to an agent's AGENTS.md.
///
/// The diff names `a/AGENTS.md` and `b/AGENTS.md`, as
/// `diff -u a/AGENTS.md b/AGENTS.md` writes it. The agent refuses it when
/// the file is no longer at `--base`, or when it doesn't apply, and
/// leaves the file as it was.
Apply(AgentsMdApplyArgs),
}
#[derive(Args)]
struct AgentsMdApplyArgs {
/// Agent name.
agent: String,
/// The file holding the diff.
diff_file: PathBuf,
/// The revision the diff was made against, as `get` printed it. It is
/// the git blob id of that text, so `git hash-object` of the unedited
/// file prints it too.
#[arg(long, value_name = "REVISION")]
base: String,
#[command(flatten)]
socket: ControllerSocketArg,
}
#[derive(Subcommand)]
@ -645,6 +683,9 @@ fn main() -> Result<()> {
Verb::Agent {
command: AgentVerb::Permissions { command },
} => permissions(&remote, command),
Verb::Agent {
command: AgentVerb::AgentsMd { command },
} => agents_md(&remote, command),
// Same socket-resolution reasoning as `Create` above.
Verb::Forge {
command: ForgeVerb::MakeAdmin(args),
@ -724,6 +765,15 @@ fn permissions(remote: &RemoteArgs, command: PermissionsVerb) -> Result<()> {
}
}
fn agents_md(remote: &RemoteArgs, command: AgentsMdVerb) -> Result<()> {
match command {
AgentsMdVerb::Get(a) => agents_md::get(&a.socket.resolve(remote)?, &a.agent),
AgentsMdVerb::Apply(a) => {
agents_md::apply(&a.socket.resolve(remote)?, &a.agent, &a.base, &a.diff_file)
}
}
}
fn schedule(remote: &RemoteArgs, command: ScheduleVerb) -> Result<()> {
match command {
ScheduleVerb::Add(a) => schedule::add(