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.
51 lines
1.8 KiB
Rust
51 lines
1.8 KiB
Rust
//! `GET` and `PUT /api/agents/{name}/agents-md`: an agent's `AGENTS.md`,
|
|
//! read from the copy its harness publishes and edited by a diff the harness
|
|
//! applies.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Success body of `GET /api/agents/{name}/agents-md`.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
|
|
pub struct AgentsMdView {
|
|
/// The file's text as the agent last published it; `""` for an agent
|
|
/// with no `AGENTS.md`.
|
|
pub content: String,
|
|
/// The git blob id of that text, what `git hash-object` prints for it.
|
|
pub revision: String,
|
|
}
|
|
|
|
/// Body of `PUT /api/agents/{name}/agents-md`.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
|
|
pub struct AgentsMdEdit {
|
|
/// The [`AgentsMdView::revision`] the diff was made against.
|
|
pub base_revision: String,
|
|
/// A unified diff as `git diff` writes it, naming `a/AGENTS.md` and
|
|
/// `b/AGENTS.md`.
|
|
pub diff: String,
|
|
}
|
|
|
|
/// Success body of `PUT /api/agents/{name}/agents-md`.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
|
|
pub struct AgentsMdSaved {
|
|
/// The file's revision after the edit.
|
|
pub revision: String,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::AgentsMdEdit;
|
|
|
|
#[test]
|
|
fn an_edit_round_trips() {
|
|
let edit = AgentsMdEdit {
|
|
base_revision: "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391".to_owned(),
|
|
diff: "--- a/AGENTS.md\n".to_owned(),
|
|
};
|
|
let wire = r#"{"base_revision":"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391","diff":"--- a/AGENTS.md\n"}"#;
|
|
assert_eq!(serde_json::to_string(&edit).unwrap(), wire);
|
|
assert_eq!(serde_json::from_str::<AgentsMdEdit>(wire).unwrap(), edit);
|
|
}
|
|
}
|