Skip to content

Commit c93a8e2

Browse files
committed
fix: store app-server agents in standard dirs
1 parent 7954d02 commit c93a8e2

3 files changed

Lines changed: 132 additions & 27 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cortex-app-server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,6 @@ if-addrs = "0.13"
7272
gethostname = "0.5"
7373

7474
[dev-dependencies]
75+
serial_test = { workspace = true }
76+
tempfile = { workspace = true }
7577
tokio-test = { workspace = true }

src/cortex-app-server/src/api/agents.rs

Lines changed: 128 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! Custom agents API endpoints.
22
3-
use std::sync::Arc;
3+
use std::{
4+
path::{Path as FsPath, PathBuf},
5+
sync::Arc,
6+
};
47

58
use axum::{
69
Json,
@@ -16,7 +19,7 @@ use super::types::{
1619
};
1720

1821
/// Read agent file from disk.
19-
fn read_agent_file(path: &std::path::Path, scope: &str) -> Option<AgentDefinition> {
22+
fn read_agent_file(path: &FsPath, scope: &str) -> Option<AgentDefinition> {
2023
if path.extension().and_then(|e| e.to_str()) != Some("md") {
2124
return None;
2225
}
@@ -70,14 +73,31 @@ fn read_agent_file(path: &std::path::Path, scope: &str) -> Option<AgentDefinitio
7073
})
7174
}
7275

76+
fn project_agents_dir() -> PathBuf {
77+
PathBuf::from(".cortex/agents")
78+
}
79+
80+
fn user_agents_dir() -> AppResult<PathBuf> {
81+
maybe_user_agents_dir()
82+
.ok_or_else(|| AppError::Internal("Cannot find user config directory".to_string()))
83+
}
84+
85+
fn maybe_user_agents_dir() -> Option<PathBuf> {
86+
dirs::config_dir().map(|dir| dir.join("cortex/agents"))
87+
}
88+
89+
fn agent_file_path(dir: &FsPath, name: &str) -> PathBuf {
90+
dir.join(format!("{name}.md"))
91+
}
92+
7393
/// List all agents.
7494
pub async fn list_agents() -> AppResult<Json<Vec<AgentDefinition>>> {
7595
let mut agents = Vec::new();
7696

77-
// Project agents (.factory/agents/)
78-
let project_dir = std::path::Path::new(".factory/agents");
97+
// Project agents (.cortex/agents/)
98+
let project_dir = project_agents_dir();
7999
if project_dir.exists()
80-
&& let Ok(entries) = std::fs::read_dir(project_dir)
100+
&& let Ok(entries) = std::fs::read_dir(&project_dir)
81101
{
82102
for entry in entries.flatten() {
83103
if let Some(agent) = read_agent_file(&entry.path(), "project") {
@@ -86,9 +106,8 @@ pub async fn list_agents() -> AppResult<Json<Vec<AgentDefinition>>> {
86106
}
87107
}
88108

89-
// User agents (~/.factory/agents/)
90-
if let Some(home) = dirs::home_dir() {
91-
let user_dir = home.join(".factory/agents");
109+
// User agents (config-dir/cortex/agents/)
110+
if let Some(user_dir) = maybe_user_agents_dir() {
92111
if user_dir.exists()
93112
&& let Ok(entries) = std::fs::read_dir(&user_dir)
94113
{
@@ -106,14 +125,14 @@ pub async fn list_agents() -> AppResult<Json<Vec<AgentDefinition>>> {
106125
/// Get a specific agent.
107126
pub async fn get_agent(Path(name): Path<String>) -> AppResult<Json<AgentDefinition>> {
108127
// Check project first
109-
let project_path = std::path::Path::new(".factory/agents").join(format!("{}.md", name));
128+
let project_path = agent_file_path(&project_agents_dir(), &name);
110129
if let Some(agent) = read_agent_file(&project_path, "project") {
111130
return Ok(Json(agent));
112131
}
113132

114133
// Check user
115-
if let Some(home) = dirs::home_dir() {
116-
let user_path = home.join(".factory/agents").join(format!("{}.md", name));
134+
if let Some(user_dir) = maybe_user_agents_dir() {
135+
let user_path = agent_file_path(&user_dir, &name);
117136
if let Some(agent) = read_agent_file(&user_path, "user") {
118137
return Ok(Json(agent));
119138
}
@@ -125,17 +144,15 @@ pub async fn get_agent(Path(name): Path<String>) -> AppResult<Json<AgentDefiniti
125144
/// Create or update an agent.
126145
pub async fn create_agent(Json(req): Json<CreateAgentRequest>) -> AppResult<Json<AgentDefinition>> {
127146
let dir = if req.scope == "project" {
128-
std::path::PathBuf::from(".factory/agents")
147+
project_agents_dir()
129148
} else {
130-
dirs::home_dir()
131-
.ok_or_else(|| AppError::Internal("Cannot find home directory".to_string()))?
132-
.join(".factory/agents")
149+
user_agents_dir()?
133150
};
134151

135152
std::fs::create_dir_all(&dir)
136153
.map_err(|e| AppError::Internal(format!("Failed to create directory: {}", e)))?;
137154

138-
let path = dir.join(format!("{}.md", req.name));
155+
let path = agent_file_path(&dir, &req.name);
139156

140157
// Build markdown with YAML frontmatter
141158
let content = format!(
@@ -170,16 +187,16 @@ pub async fn create_agent(Json(req): Json<CreateAgentRequest>) -> AppResult<Json
170187
/// Delete an agent.
171188
pub async fn delete_agent(Path(name): Path<String>) -> AppResult<Json<serde_json::Value>> {
172189
// Try project first
173-
let project_path = std::path::Path::new(".factory/agents").join(format!("{}.md", name));
190+
let project_path = agent_file_path(&project_agents_dir(), &name);
174191
if project_path.exists() {
175192
std::fs::remove_file(&project_path)
176193
.map_err(|e| AppError::Internal(format!("Failed to delete: {}", e)))?;
177194
return Ok(Json(serde_json::json!({"deleted": true})));
178195
}
179196

180197
// Try user
181-
if let Some(home) = dirs::home_dir() {
182-
let user_path = home.join(".factory/agents").join(format!("{}.md", name));
198+
if let Some(user_dir) = maybe_user_agents_dir() {
199+
let user_path = agent_file_path(&user_dir, &name);
183200
if user_path.exists() {
184201
std::fs::remove_file(&user_path)
185202
.map_err(|e| AppError::Internal(format!("Failed to delete: {}", e)))?;
@@ -229,9 +246,8 @@ pub async fn update_agent(
229246
Json(req): Json<UpdateAgentRequest>,
230247
) -> AppResult<Json<AgentDefinition>> {
231248
// Find existing agent
232-
let project_path = std::path::Path::new(".factory/agents").join(format!("{}.md", name));
233-
let user_path =
234-
dirs::home_dir().map(|h| h.join(".factory/agents").join(format!("{}.md", name)));
249+
let project_path = agent_file_path(&project_agents_dir(), &name);
250+
let user_path = maybe_user_agents_dir().map(|dir| agent_file_path(&dir, &name));
235251

236252
let (existing, path) = if let Some(agent) = read_agent_file(&project_path, "project") {
237253
(agent, project_path)
@@ -353,17 +369,15 @@ pub async fn import_agent(Json(req): Json<ImportAgentRequest>) -> AppResult<Json
353369

354370
// Determine directory
355371
let dir = if req.scope == "project" {
356-
std::path::PathBuf::from(".factory/agents")
372+
project_agents_dir()
357373
} else {
358-
dirs::home_dir()
359-
.ok_or_else(|| AppError::Internal("Cannot find home directory".to_string()))?
360-
.join(".factory/agents")
374+
user_agents_dir()?
361375
};
362376

363377
std::fs::create_dir_all(&dir)
364378
.map_err(|e| AppError::Internal(format!("Failed to create directory: {}", e)))?;
365379

366-
let path = dir.join(format!("{}.md", name));
380+
let path = agent_file_path(&dir, &name);
367381

368382
// Write the file
369383
std::fs::write(&path, &req.content)
@@ -409,3 +423,90 @@ pub async fn generate_agent_prompt(
409423
permission_mode: "default".to_string(),
410424
}))
411425
}
426+
427+
#[cfg(test)]
428+
mod tests {
429+
use super::*;
430+
431+
use serial_test::serial;
432+
use tempfile::TempDir;
433+
434+
struct CurrentDirGuard {
435+
original: std::path::PathBuf,
436+
}
437+
438+
impl CurrentDirGuard {
439+
fn set(path: &std::path::Path) -> Self {
440+
let original = std::env::current_dir().unwrap();
441+
std::env::set_current_dir(path).unwrap();
442+
Self { original }
443+
}
444+
}
445+
446+
impl Drop for CurrentDirGuard {
447+
fn drop(&mut self) {
448+
std::env::set_current_dir(&self.original).unwrap();
449+
}
450+
}
451+
452+
fn create_project_agent_request(name: &str) -> CreateAgentRequest {
453+
CreateAgentRequest {
454+
name: name.to_string(),
455+
description: "Created through the app-server API".to_string(),
456+
tools: vec!["Read".to_string()],
457+
model: "inherit".to_string(),
458+
permission_mode: "default".to_string(),
459+
prompt: "Use the standard project agent directory.".to_string(),
460+
scope: "project".to_string(),
461+
}
462+
}
463+
464+
#[tokio::test]
465+
#[serial]
466+
async fn create_project_agent_writes_to_standard_project_agents_dir() {
467+
let temp = TempDir::new().unwrap();
468+
let _cwd = CurrentDirGuard::set(temp.path());
469+
470+
let name = "api-project-agent";
471+
let _ = create_agent(Json(create_project_agent_request(name)))
472+
.await
473+
.unwrap();
474+
475+
assert!(
476+
temp.path()
477+
.join(".cortex/agents")
478+
.join(format!("{name}.md"))
479+
.exists()
480+
);
481+
assert!(
482+
!temp
483+
.path()
484+
.join(".factory/agents")
485+
.join(format!("{name}.md"))
486+
.exists()
487+
);
488+
}
489+
490+
#[tokio::test]
491+
#[serial]
492+
async fn list_agents_reads_standard_project_agents_dir() {
493+
let temp = TempDir::new().unwrap();
494+
let _cwd = CurrentDirGuard::set(temp.path());
495+
let agents_dir = temp.path().join(".cortex/agents");
496+
std::fs::create_dir_all(&agents_dir).unwrap();
497+
498+
std::fs::write(
499+
agents_dir.join("listed-api-agent.md"),
500+
"---\ndescription: Listed through standard project storage\ntools: [\"Read\"]\nmodel: inherit\npermissionMode: default\n---\n\nVisible to app-server list.",
501+
)
502+
.unwrap();
503+
504+
let Json(agents) = list_agents().await.unwrap();
505+
506+
assert!(agents.iter().any(|agent| {
507+
agent.name == "listed-api-agent"
508+
&& agent.scope == "project"
509+
&& agent.description == "Listed through standard project storage"
510+
}));
511+
}
512+
}

0 commit comments

Comments
 (0)