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
58use 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.
7494pub 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.
107126pub 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.
126145pub 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.
171188pub 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+ "---\n description: Listed through standard project storage\n tools: [\" Read\" ]\n model: inherit\n permissionMode: default\n ---\n \n Visible 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