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:
parent
f4f459f0b2
commit
88674eb63a
22 changed files with 1296 additions and 47 deletions
|
|
@ -27,8 +27,8 @@ use crate::{AppState, error_problem};
|
|||
/// What became of one command.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Outcome {
|
||||
/// The agent answered.
|
||||
Answered(AgentControlReply),
|
||||
/// The agent answered with this payload, a reply to the command sent.
|
||||
Answered(Vec<u8>),
|
||||
/// Nothing subscribes to the agent's control subject.
|
||||
NotRunning,
|
||||
/// A subscriber held the command and did not answer in time.
|
||||
|
|
@ -50,10 +50,7 @@ impl ControlSink for async_nats::Client {
|
|||
.payload(payload.into())
|
||||
.timeout(Some(swarm_queue_client::AGENT_CONTROL_REPLY_DEADLINE));
|
||||
match self.send_request(subject, request).await {
|
||||
Ok(reply) => match serde_json::from_slice(&reply.payload) {
|
||||
Ok(reply) => Outcome::Answered(reply),
|
||||
Err(e) => Outcome::Failed(format!("the agent's answer was unreadable: {e}")),
|
||||
},
|
||||
Ok(reply) => Outcome::Answered(reply.payload.to_vec()),
|
||||
Err(e) => match e.kind() {
|
||||
RequestErrorKind::TimedOut => Outcome::Unconfirmed,
|
||||
RequestErrorKind::NoResponders => Outcome::NotRunning,
|
||||
|
|
@ -117,7 +114,7 @@ pub(crate) async fn post_agent_cancel(
|
|||
|
||||
/// The agent and who is asking, or the refusal. Everything decided before the
|
||||
/// queue is touched.
|
||||
fn target(
|
||||
pub(crate) fn target(
|
||||
headers: &HeaderMap,
|
||||
agent: &str,
|
||||
) -> Result<(String, String), (StatusCode, &'static str)> {
|
||||
|
|
@ -143,6 +140,12 @@ async fn send(
|
|||
})?;
|
||||
match sink.request(subject.clone(), payload).await {
|
||||
Outcome::Answered(reply) => {
|
||||
let reply: AgentControlReply = serde_json::from_slice(&reply).map_err(|e| {
|
||||
error_problem(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&format!("{agent}'s answer was unreadable: {e}"),
|
||||
)
|
||||
})?;
|
||||
tracing::info!(%agent, %from, ?command, acted = reply.acted, "agent command answered");
|
||||
Ok(Json(CancelResponse { acted: reply.acted }))
|
||||
}
|
||||
|
|
@ -231,10 +234,14 @@ mod tests {
|
|||
async fn the_answer_is_relayed_from_the_agent_s_own_control_subject() {
|
||||
let answers: [(bool, fn() -> Outcome); 2] = [
|
||||
(true, || {
|
||||
Outcome::Answered(AgentControlReply { acted: true })
|
||||
Outcome::Answered(
|
||||
serde_json::to_vec(&AgentControlReply { acted: true }).expect("encodes"),
|
||||
)
|
||||
}),
|
||||
(false, || {
|
||||
Outcome::Answered(AgentControlReply { acted: false })
|
||||
Outcome::Answered(
|
||||
serde_json::to_vec(&AgentControlReply { acted: false }).expect("encodes"),
|
||||
)
|
||||
}),
|
||||
];
|
||||
for (acted, answer) in answers {
|
||||
|
|
|
|||
Loading…
Reference in a new issue