huskies: merge 1218 story Remembered/sticky permission approvals so constrained agents stop re-prompting for the same action class

This commit is contained in:
Huskies Agent
2026-07-18 13:58:35 +00:00
parent fbaf5bf959
commit 7d3de2bb44
21 changed files with 313 additions and 214 deletions
+2 -2
View File
@@ -53,7 +53,7 @@ pub async fn create_worktree(
tokio::task::spawn_blocking(move || configure_sparse_checkout(&wt_clone))
.await
.map_err(|e| format!("spawn_blocking: {e}"))??;
write_mcp_json(&wt_path, port)?;
write_mcp_json(&wt_path, port, story_id)?;
return Ok(WorktreeInfo {
path: wt_path,
branch,
@@ -68,7 +68,7 @@ pub async fn create_worktree(
.await
.map_err(|e| format!("spawn_blocking: {e}"))??;
write_mcp_json(&wt_path, port)?;
write_mcp_json(&wt_path, port, story_id)?;
run_setup_commands(&wt_path, config).await;
Ok(WorktreeInfo {
+31 -6
View File
@@ -40,10 +40,23 @@ pub fn worktree_path(project_root: &Path, story_id: &str) -> PathBuf {
/// Write a `.mcp.json` file in the given directory pointing to the huskies
/// HTTP MCP endpoint at the given port.
pub fn write_mcp_json(dir: &Path, port: u16) -> Result<(), String> {
let content = format!(
"{{\n \"mcpServers\": {{\n \"huskies\": {{\n \"type\": \"http\",\n \"url\": \"http://localhost:{port}/mcp\"\n }}\n }}\n}}\n"
);
///
/// Embeds an `X-Huskies-Session` header set to `story_id` so the server can
/// scope remembered permission approvals to this worktree's agent session
/// (see `http::mcp::session`) without affecting other stories' agents.
pub fn write_mcp_json(dir: &Path, port: u16, story_id: &str) -> Result<(), String> {
let value = serde_json::json!({
"mcpServers": {
"huskies": {
"type": "http",
"url": format!("http://localhost:{port}/mcp"),
"headers": { "X-Huskies-Session": story_id }
}
}
});
let content = serde_json::to_string_pretty(&value)
.map_err(|e| format!("Serialize .mcp.json: {e}"))?
+ "\n";
std::fs::write(dir.join(".mcp.json"), content).map_err(|e| format!("Write .mcp.json: {e}"))
}
@@ -91,7 +104,7 @@ mod tests {
#[test]
fn write_mcp_json_uses_given_port() {
let tmp = TempDir::new().unwrap();
write_mcp_json(tmp.path(), 4242).unwrap();
write_mcp_json(tmp.path(), 4242, "1218").unwrap();
let content = std::fs::read_to_string(tmp.path().join(".mcp.json")).unwrap();
assert!(content.contains("http://localhost:4242/mcp"));
}
@@ -99,11 +112,23 @@ mod tests {
#[test]
fn write_mcp_json_default_port() {
let tmp = TempDir::new().unwrap();
write_mcp_json(tmp.path(), 3001).unwrap();
write_mcp_json(tmp.path(), 3001, "1218").unwrap();
let content = std::fs::read_to_string(tmp.path().join(".mcp.json")).unwrap();
assert!(content.contains("http://localhost:3001/mcp"));
}
#[test]
fn write_mcp_json_embeds_session_header_with_story_id() {
let tmp = TempDir::new().unwrap();
write_mcp_json(tmp.path(), 3001, "42_story_test").unwrap();
let content = std::fs::read_to_string(tmp.path().join(".mcp.json")).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(
parsed["mcpServers"]["huskies"]["headers"]["X-Huskies-Session"],
"42_story_test"
);
}
#[test]
fn worktree_path_is_inside_project() {
let project_root = Path::new("/home/user/my-project");