use serde::{Deserialize, Serialize};
use crate::filesystem::Filesystem;
use crate::types::BuiltinTool;
const MANIFEST_PATH: &str = "agent.json";
const LEGACY_PROMPT: &str = ".lh_system_prompt.txt";
const LEGACY_ALLOWLIST: &str = ".lh_tool_allowlist.txt";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub(crate) struct AgentManifest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<String>>,
}
pub(crate) async fn load() -> AgentManifest {
let fs = super::shared_opfs();
if let Ok(bytes) = fs.read(MANIFEST_PATH).await {
if let Ok(manifest) = serde_json::from_slice::<AgentManifest>(&bytes) {
return manifest;
}
}
let mut manifest = AgentManifest::default();
if let Ok(bytes) = fs.read(LEGACY_PROMPT).await {
if let Ok(text) = String::from_utf8(bytes) {
let trimmed = text.trim();
if !trimmed.is_empty() {
manifest.system_prompt = Some(trimmed.to_string());
}
}
}
if let Ok(bytes) = fs.read(LEGACY_ALLOWLIST).await {
if let Ok(text) = String::from_utf8(bytes) {
let tools: Vec<String> = text
.lines()
.filter_map(|line| {
let t = line.trim();
if t.is_empty() || t.starts_with('#') {
None
} else {
Some(t.to_string())
}
})
.collect();
if !tools.is_empty() {
manifest.tools = Some(tools);
}
}
}
manifest
}
pub(crate) async fn save(manifest: &AgentManifest) -> Result<(), String> {
let fs = super::shared_opfs();
let json = serde_json::to_vec_pretty(manifest).map_err(|e| format!("serialize: {e}"))?;
fs.write_atomic(MANIFEST_PATH, &json)
.await
.map_err(|e| format!("write: {e}"))
}
pub(crate) async fn set_system_prompt(prompt: Option<&str>) -> Result<(), String> {
let mut manifest = load().await;
manifest.system_prompt = prompt
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string);
save(&manifest).await
}
pub(crate) async fn set_tools(tools: Option<&[BuiltinTool]>) -> Result<(), String> {
let mut manifest = load().await;
manifest.tools = tools
.filter(|t| !t.is_empty())
.map(|t| t.iter().map(|tool| tool.wire_name().to_string()).collect());
save(&manifest).await
}
pub(crate) async fn system_prompt() -> Option<String> {
load()
.await
.system_prompt
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
pub(crate) async fn closure_tool_allowed(name: &str) -> bool {
match load().await.tools {
None => true, Some(tools) => tools.iter().any(|t| t == name),
}
}
pub(crate) async fn tool_allowlist() -> Option<Vec<BuiltinTool>> {
let names = load().await.tools?;
let tools: Vec<BuiltinTool> = names
.iter()
.filter_map(|n| BuiltinTool::ALL.iter().find(|t| t.wire_name() == n).copied())
.collect();
if tools.is_empty() {
None
} else {
Some(tools)
}
}