aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-10 23:06:01 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-10 23:06:01 +0300
commitf4d04fab26d8b8733868b8e394439b69ed83df75 (patch)
tree1657f7da96f4cc82a23b576d691887589c372013
parent1fc5e84d461ff7c3cfcc0ed134df0939665fc937 (diff)
feat: add agent manifest update command
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--docs/next/website/src/content/docs/agents.mdx2
-rw-r--r--docs/next/website/src/content/docs/cli-reference.mdx3
-rw-r--r--src/cli/server.rs143
4 files changed, 147 insertions, 2 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md
index 8df18e15..8a5c4a23 100644
--- a/docs/next/CHANGELOG.md
+++ b/docs/next/CHANGELOG.md
@@ -17,6 +17,7 @@
### Added
- Added remote auto-updates for agent detection manifests, with per-agent validation, local override precedence, `herdr server agent-manifests` diagnostics, and explain output showing remote manifest status.
+- Added `herdr server update-agent-manifests` to fetch remote agent detection manifests immediately, reload the running server, and print the updated manifest status.
- Added `herdr agent explain` to show the manifest source, matched rule, evaluated matcher and region evidence, visible evidence flags, skipped-update reason, and idle fallback reason for live panes or saved screen fixtures.
- Added `herdr integration install droid` for Factory Droid hooks that report session ids through Herdr's socket API. When native agent session restore is enabled, Herdr can resume Droid panes with `droid --resume <id>`.
- Added `herdr integration install kilo` for Kilo Code CLI plugins that report lifecycle state and session ids through Herdr's socket API. When native agent session restore is enabled, Herdr can resume Kilo panes with `kilo --session <id>`.
diff --git a/docs/next/website/src/content/docs/agents.mdx b/docs/next/website/src/content/docs/agents.mdx
index e6319704..49f64881 100644
--- a/docs/next/website/src/content/docs/agents.mdx
+++ b/docs/next/website/src/content/docs/agents.mdx
@@ -62,7 +62,7 @@ Local overrides always win. Without a local override, Herdr uses the newer compa
Remote manifests patch detection rules for agents Herdr already knows how to identify. Adding a completely new agent still requires a Herdr binary update for process detection, labels, and integration behavior.
-The running server loads active manifests into memory on startup. Automatic remote manifest updates reload that in-memory cache after new rules are written. After editing a local override manually, restart Herdr or run `herdr server reload-agent-manifests` to apply the file to the running server.
+The running server loads active manifests into memory on startup. Automatic remote manifest updates reload that in-memory cache after new rules are written. Run `herdr server update-agent-manifests` to fetch remote manifest updates immediately and reload the running server. After editing a local override manually, restart Herdr or run `herdr server reload-agent-manifests` to apply the file to the running server.
Use `herdr agent explain` when a pane shows the wrong state:
diff --git a/docs/next/website/src/content/docs/cli-reference.mdx b/docs/next/website/src/content/docs/cli-reference.mdx
index 5ed9ce8f..32baa746 100644
--- a/docs/next/website/src/content/docs/cli-reference.mdx
+++ b/docs/next/website/src/content/docs/cli-reference.mdx
@@ -40,10 +40,11 @@ herdr server
herdr server stop
herdr server reload-config
herdr server agent-manifests [--json]
+herdr server update-agent-manifests [--json]
herdr server reload-agent-manifests
```
-`herdr server` runs the headless server explicitly. Use it for supervised or service-style setups. `reload-config` applies reloadable settings without restarting panes. `agent-manifests` shows the active agent detection manifest sources, cached remote versions, and last remote update results. `reload-agent-manifests` reloads agent detection manifests into the running server after local override edits.
+`herdr server` runs the headless server explicitly. Use it for supervised or service-style setups. `reload-config` applies reloadable settings without restarting panes. `agent-manifests` shows the active agent detection manifest sources, cached remote versions, and last remote update results. `update-agent-manifests` fetches remote manifest updates immediately, reloads them into the running server, and prints the updated manifest status; pass `--json` for the raw status response. `reload-agent-manifests` reloads agent detection manifests into the running server after local override edits.
## Notifications
diff --git a/src/cli/server.rs b/src/cli/server.rs
index f6cf7e54..b0648cde 100644
--- a/src/cli/server.rs
+++ b/src/cli/server.rs
@@ -11,6 +11,7 @@ pub(super) fn run_server_command(args: &[String]) -> std::io::Result<Option<i32>
"--handoff-import" => Ok(None),
"reload-config" => server_reload_config(&args[1..]).map(Some),
"agent-manifests" => server_agent_manifests(&args[1..]).map(Some),
+ "update-agent-manifests" => server_update_agent_manifests(&args[1..]).map(Some),
"reload-agent-manifests" => server_reload_agent_manifests(&args[1..]).map(Some),
"help" | "--help" | "-h" => {
print_server_help();
@@ -78,6 +79,69 @@ fn server_reload_agent_manifests(args: &[String]) -> std::io::Result<i32> {
})?)
}
+fn server_update_agent_manifests(args: &[String]) -> std::io::Result<i32> {
+ let json = match args {
+ [] => false,
+ [flag] if flag == "--json" => true,
+ _ => {
+ eprintln!("usage: herdr server update-agent-manifests [--json]");
+ return Ok(2);
+ }
+ };
+
+ let response = match update_agent_manifest_status(super::send_request, || {
+ crate::detect::manifest_update::check_and_update().map(|_| ())
+ })? {
+ Ok(response) => response,
+ Err(err) => {
+ if json {
+ return super::print_response(&agent_manifest_update_error_response(&err));
+ }
+ eprintln!("failed to update agent detection manifests: {err}");
+ return Ok(1);
+ }
+ };
+ if json || response.get("error").is_some() {
+ return super::print_response(&response);
+ }
+
+ print_agent_manifest_status(&response);
+ Ok(0)
+}
+
+fn update_agent_manifest_status(
+ mut send_request: impl FnMut(&Request) -> std::io::Result<serde_json::Value>,
+ update_manifests: impl FnOnce() -> Result<(), String>,
+) -> std::io::Result<Result<serde_json::Value, String>> {
+ if let Err(err) = update_manifests() {
+ return Ok(Err(err));
+ }
+
+ let reload_response = send_request(&Request {
+ id: "cli:server:reload-agent-manifests".into(),
+ method: Method::ServerReloadAgentManifests(EmptyParams::default()),
+ })?;
+ if reload_response.get("error").is_some() {
+ return Ok(Ok(reload_response));
+ }
+
+ send_request(&Request {
+ id: "cli:server:agent-manifests".into(),
+ method: Method::ServerAgentManifests(EmptyParams::default()),
+ })
+ .map(Ok)
+}
+
+fn agent_manifest_update_error_response(err: &str) -> serde_json::Value {
+ serde_json::json!({
+ "id": "cli:server:update-agent-manifests",
+ "error": {
+ "code": "agent_manifest_update_failed",
+ "message": err,
+ }
+ })
+}
+
fn print_agent_manifest_status(response: &serde_json::Value) {
let result = &response["result"];
let last_check = result["last_check_unix"]
@@ -187,6 +251,7 @@ fn print_server_help() {
eprintln!(" herdr server live-handoff hand off live panes to a new local server");
eprintln!(" herdr server reload-config reload config.toml in the running server");
eprintln!(" herdr server agent-manifests [--json] show agent detection manifest status");
+ eprintln!(" herdr server update-agent-manifests [--json] fetch and reload agent detection manifests");
eprintln!(" herdr server reload-agent-manifests reload agent detection manifests in the running server");
}
@@ -195,6 +260,84 @@ mod tests {
use super::*;
#[test]
+ fn update_agent_manifest_status_fetches_reloads_then_reads_status() {
+ let mut methods = Vec::new();
+ let response = update_agent_manifest_status(
+ |request| {
+ methods.push(request.method.clone());
+ match &request.method {
+ Method::ServerReloadAgentManifests(_) => Ok(serde_json::json!({
+ "id": request.id,
+ "result": { "type": "agent_manifest_reload", "manifests": [] }
+ })),
+ Method::ServerAgentManifests(_) => Ok(serde_json::json!({
+ "id": request.id,
+ "result": {
+ "type": "agent_manifest_status",
+ "last_result": "checked",
+ "manifests": []
+ }
+ })),
+ _ => panic!("unexpected request"),
+ }
+ },
+ || Ok(()),
+ )
+ .unwrap()
+ .unwrap();
+
+ assert_eq!(response["result"]["type"], "agent_manifest_status");
+ assert_eq!(
+ methods,
+ vec![
+ Method::ServerReloadAgentManifests(EmptyParams::default()),
+ Method::ServerAgentManifests(EmptyParams::default())
+ ]
+ );
+ }
+
+ #[test]
+ fn update_agent_manifest_status_skips_server_when_fetch_fails() {
+ let response = update_agent_manifest_status(
+ |_request| panic!("server should not be called after fetch failure"),
+ || Err("network unavailable".to_string()),
+ )
+ .unwrap();
+
+ assert_eq!(response, Err("network unavailable".to_string()));
+ assert_eq!(
+ agent_manifest_update_error_response("network unavailable")["error"]["code"],
+ "agent_manifest_update_failed"
+ );
+ }
+
+ #[test]
+ fn update_agent_manifest_status_stops_after_reload_error() {
+ let mut methods = Vec::new();
+ let response = update_agent_manifest_status(
+ |request| {
+ methods.push(request.method.clone());
+ Ok(serde_json::json!({
+ "id": request.id,
+ "error": {
+ "code": "reload_failed",
+ "message": "reload failed"
+ }
+ }))
+ },
+ || Ok(()),
+ )
+ .unwrap()
+ .unwrap();
+
+ assert_eq!(response["error"]["code"], "reload_failed");
+ assert_eq!(
+ methods,
+ vec![Method::ServerReloadAgentManifests(EmptyParams::default())]
+ );
+ }
+
+ #[test]
fn live_handoff_params_parse_remote_update_fields() {
let args = vec![
"--import-exe".to_string(),