manager mcp: expose 'remind' tool sharing storage helper with agent surface
This commit is contained in:
parent
0e6bac8388
commit
1770b51845
6 changed files with 120 additions and 24 deletions
|
|
@ -227,30 +227,32 @@ fn handle_remind(
|
|||
timing: &hive_sh4re::ReminderTiming,
|
||||
file_path: Option<&str>,
|
||||
) -> AgentResponse {
|
||||
let due_at = match resolve_due_at(timing) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
return AgentResponse::Err {
|
||||
message: format!("invalid reminder timing: {e:#}"),
|
||||
};
|
||||
}
|
||||
};
|
||||
let (stored_message, stored_path) = match prepare_remind_storage(agent, message, file_path) {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => return AgentResponse::Err { message: e },
|
||||
};
|
||||
match coord
|
||||
match store_remind(coord, agent, message, timing, file_path) {
|
||||
Ok(()) => AgentResponse::Ok,
|
||||
Err(message) => AgentResponse::Err { message },
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared remind-storage path used by both the agent and the manager
|
||||
/// dispatchers. Validates timing, applies the auto-file overflow
|
||||
/// dance (see [`prepare_remind_storage`]), and writes the reminder
|
||||
/// row. Returns `Ok(())` on success, or a caller-ready error string
|
||||
/// the dispatcher wraps in `*Response::Err`.
|
||||
pub(crate) fn store_remind(
|
||||
coord: &Arc<Coordinator>,
|
||||
agent: &str,
|
||||
message: &str,
|
||||
timing: &hive_sh4re::ReminderTiming,
|
||||
file_path: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let due_at = resolve_due_at(timing).map_err(|e| format!("invalid reminder timing: {e:#}"))?;
|
||||
let (stored_message, stored_path) = prepare_remind_storage(agent, message, file_path)?;
|
||||
let id = coord
|
||||
.broker
|
||||
.store_reminder(agent, &stored_message, stored_path.as_deref(), due_at)
|
||||
{
|
||||
Ok(id) => {
|
||||
tracing::info!(%id, %agent, %due_at, "reminder scheduled");
|
||||
AgentResponse::Ok
|
||||
}
|
||||
Err(e) => AgentResponse::Err {
|
||||
message: format!("failed to store reminder: {e:#}"),
|
||||
},
|
||||
}
|
||||
.map_err(|e| format!("failed to store reminder: {e:#}"))?;
|
||||
tracing::info!(%id, %agent, %due_at, "reminder scheduled");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decide what we actually store in the reminders row, applying the
|
||||
|
|
|
|||
|
|
@ -307,6 +307,20 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
|||
},
|
||||
}
|
||||
}
|
||||
ManagerRequest::Remind {
|
||||
message,
|
||||
timing,
|
||||
file_path,
|
||||
} => match crate::agent_server::store_remind(
|
||||
coord,
|
||||
MANAGER_AGENT,
|
||||
message,
|
||||
timing,
|
||||
file_path.as_deref(),
|
||||
) {
|
||||
Ok(()) => ManagerResponse::Ok,
|
||||
Err(message) => ManagerResponse::Err { message },
|
||||
},
|
||||
ManagerRequest::RequestApplyCommit {
|
||||
agent,
|
||||
commit_ref,
|
||||
|
|
|
|||
|
|
@ -165,6 +165,20 @@ pub fn write_payload(agent: &str, host_path: &Path, message: &str) -> Result<(),
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Container-visible state prefix the caller's `file_path` must live
|
||||
/// under. Sub-agents see their state at `/agents/<name>/state/`;
|
||||
/// the manager keeps the legacy `/state/` mount (see
|
||||
/// `lifecycle::set_nspawn_flags`). Auto-file paths use the same
|
||||
/// prefix so the round-trip is symmetric.
|
||||
#[must_use]
|
||||
pub fn container_state_prefix(agent: &str) -> String {
|
||||
if agent == hive_sh4re::MANAGER_AGENT {
|
||||
"/state/".to_owned()
|
||||
} else {
|
||||
format!("/agents/{agent}/state/")
|
||||
}
|
||||
}
|
||||
|
||||
/// Map an agent-visible container path to the matching host path,
|
||||
/// validating that it lives under the agent's own state subtree, has
|
||||
/// a non-empty relative tail, and doesn't try to traverse out via
|
||||
|
|
@ -172,7 +186,7 @@ pub fn write_payload(agent: &str, host_path: &Path, message: &str) -> Result<(),
|
|||
/// reason string on rejection. `pub` so `agent_server::handle_remind`
|
||||
/// can reuse it for the at-remind-time auto-file path.
|
||||
pub fn resolve_host_path(agent: &str, req_path: &str) -> Result<PathBuf, String> {
|
||||
let prefix = format!("/agents/{agent}/state/");
|
||||
let prefix = container_state_prefix(agent);
|
||||
let Some(rel) = req_path.strip_prefix(&prefix) else {
|
||||
return Err(format!(
|
||||
"must be absolute and under `{prefix}` (got `{req_path}`)"
|
||||
|
|
@ -230,6 +244,21 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manager_uses_legacy_state_prefix() {
|
||||
// The manager container mounts its state at `/state/` (legacy),
|
||||
// not `/agents/manager/state/`. Same host path; different
|
||||
// container-visible path. resolve_host_path needs to know.
|
||||
assert_eq!(container_state_prefix("manager"), "/state/");
|
||||
let p = resolve_host_path("manager", "/state/reminders/x.md").unwrap();
|
||||
assert_eq!(
|
||||
p,
|
||||
PathBuf::from("/var/lib/hyperhive/agents/manager/state/reminders/x.md")
|
||||
);
|
||||
// And the sub-agent prefix must NOT be accepted for the manager.
|
||||
assert!(resolve_host_path("manager", "/agents/manager/state/x.md").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_body_passthrough_when_no_file_path() {
|
||||
let s = prepare_body("foo", "hello world", None);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue