Skip to content

Commit 045d1b6

Browse files
committed
feat(acp): config supports ACP
1 parent 673dbcd commit 045d1b6

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

src/api/agents.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,7 @@ pub(super) async fn create_agent(
531531
brave_search_key: None,
532532
cron_timezone: None,
533533
sandbox: None,
534+
acp: None,
534535
cron: Vec::new(),
535536
};
536537
let agent_config = raw_config.resolve(&instance_dir, defaults);

src/config.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ pub struct DefaultsConfig {
348348
pub history_backfill_count: usize,
349349
pub cron: Vec<CronDef>,
350350
pub opencode: OpenCodeConfig,
351+
/// ACP agent definitions shared across all agents.
352+
pub acp: HashMap<String, AcpAgentConfig>,
351353
/// Worker log mode: "errors_only", "all_separate", or "all_combined".
352354
pub worker_log_mode: crate::settings::WorkerLogMode,
353355
}
@@ -376,6 +378,7 @@ impl std::fmt::Debug for DefaultsConfig {
376378
.field("history_backfill_count", &self.history_backfill_count)
377379
.field("cron", &self.cron)
378380
.field("opencode", &self.opencode)
381+
.field("acp", &self.acp)
379382
.field("worker_log_mode", &self.worker_log_mode)
380383
.finish()
381384
}
@@ -567,6 +570,40 @@ impl Default for OpenCodeConfig {
567570
}
568571
}
569572

573+
/// ACP (Agent Client Protocol) agent configuration.
574+
///
575+
/// Configured under `[defaults.acp.<id>]` in config.toml. Each entry
576+
/// represents a separate ACP-compatible coding agent that Spacebot can spawn
577+
/// and communicate with over stdio.
578+
#[derive(Debug, Clone)]
579+
pub struct AcpAgentConfig {
580+
/// Unique identifier for this ACP agent (the TOML table key).
581+
pub id: String,
582+
/// Whether this ACP agent is available for use.
583+
pub enabled: bool,
584+
/// Path to the agent binary (supports "env:VAR_NAME" references).
585+
pub command: String,
586+
/// Arguments passed to the agent binary.
587+
pub args: Vec<String>,
588+
/// Environment variables set when spawning the agent process.
589+
pub env: HashMap<String, String>,
590+
/// Session timeout in seconds. `None` uses a default of 300s.
591+
pub timeout: u64,
592+
}
593+
594+
impl Default for AcpAgentConfig {
595+
fn default() -> Self {
596+
Self {
597+
id: String::new(),
598+
enabled: true,
599+
command: String::new(),
600+
args: Vec::new(),
601+
env: HashMap::new(),
602+
timeout: 300,
603+
}
604+
}
605+
}
606+
570607
/// Cortex configuration.
571608
#[derive(Debug, Clone, Copy)]
572609
pub struct CortexConfig {
@@ -766,6 +803,8 @@ pub struct AgentConfig {
766803
pub cron_timezone: Option<String>,
767804
/// Sandbox configuration for process containment.
768805
pub sandbox: Option<crate::sandbox::SandboxConfig>,
806+
/// Per-agent ACP overrides. None inherits from defaults.
807+
pub acp: Option<HashMap<String, AcpAgentConfig>>,
769808
/// Cron job definitions for this agent.
770809
pub cron: Vec<CronDef>,
771810
}
@@ -816,6 +855,7 @@ pub struct ResolvedAgentConfig {
816855
pub sandbox: crate::sandbox::SandboxConfig,
817856
/// Number of messages to fetch from the platform when a new channel is created.
818857
pub history_backfill_count: usize,
858+
pub acp: HashMap<String, AcpAgentConfig>,
819859
pub cron: Vec<CronDef>,
820860
}
821861

@@ -841,6 +881,7 @@ impl Default for DefaultsConfig {
841881
history_backfill_count: 50,
842882
cron: Vec::new(),
843883
opencode: OpenCodeConfig::default(),
884+
acp: HashMap::new(),
844885
worker_log_mode: crate::settings::WorkerLogMode::default(),
845886
}
846887
}
@@ -898,6 +939,7 @@ impl AgentConfig {
898939
),
899940
sandbox: self.sandbox.clone().unwrap_or_default(),
900941
history_backfill_count: defaults.history_backfill_count,
942+
acp: resolve_acp_configs(&defaults.acp, self.acp.as_ref()),
901943
cron: self.cron.clone(),
902944
}
903945
}
@@ -1720,6 +1762,8 @@ struct TomlDefaultsConfig {
17201762
brave_search_key: Option<String>,
17211763
cron_timezone: Option<String>,
17221764
opencode: Option<TomlOpenCodeConfig>,
1765+
#[serde(default)]
1766+
acp: HashMap<String, TomlAcpAgentConfig>,
17231767
worker_log_mode: Option<String>,
17241768
}
17251769

@@ -1820,6 +1864,18 @@ struct TomlOpenCodePermissions {
18201864
webfetch: Option<String>,
18211865
}
18221866

1867+
#[derive(Deserialize, Clone, Default)]
1868+
struct TomlAcpAgentConfig {
1869+
#[serde(default = "default_enabled")]
1870+
enabled: bool,
1871+
command: Option<String>,
1872+
#[serde(default)]
1873+
args: Vec<String>,
1874+
#[serde(default)]
1875+
env: HashMap<String, String>,
1876+
timeout: Option<u64>,
1877+
}
1878+
18231879
#[derive(Deserialize, Clone)]
18241880
struct TomlMcpServerConfig {
18251881
name: String,
@@ -1866,6 +1922,8 @@ struct TomlAgentConfig {
18661922
cron_timezone: Option<String>,
18671923
sandbox: Option<crate::sandbox::SandboxConfig>,
18681924
#[serde(default)]
1925+
acp: Option<HashMap<String, TomlAcpAgentConfig>>,
1926+
#[serde(default)]
18691927
cron: Vec<TomlCronDef>,
18701928
}
18711929

@@ -2173,6 +2231,20 @@ fn resolve_mcp_configs(
21732231
merged
21742232
}
21752233

2234+
/// Merge default ACP configs with optional per-agent overrides.
2235+
fn resolve_acp_configs(
2236+
default_configs: &HashMap<String, AcpAgentConfig>,
2237+
agent_configs: Option<&HashMap<String, AcpAgentConfig>>,
2238+
) -> HashMap<String, AcpAgentConfig> {
2239+
let mut merged = default_configs.clone();
2240+
if let Some(overrides) = agent_configs {
2241+
for (id, cfg) in overrides {
2242+
merged.insert(id.clone(), cfg.clone());
2243+
}
2244+
}
2245+
merged
2246+
}
2247+
21762248
impl Config {
21772249
/// Resolve the instance directory from env or default (~/.spacebot).
21782250
pub fn default_instance_dir() -> PathBuf {
@@ -2546,6 +2618,7 @@ impl Config {
25462618
brave_search_key: None,
25472619
cron_timezone: None,
25482620
sandbox: None,
2621+
acp: None,
25492622
cron: Vec::new(),
25502623
}];
25512624

@@ -3157,6 +3230,41 @@ impl Config {
31573230
}
31583231
})
31593232
.unwrap_or_else(|| base_defaults.opencode.clone()),
3233+
acp: {
3234+
let mut merged = base_defaults.acp.clone();
3235+
for (id, toml_acp) in &toml.defaults.acp {
3236+
let base_entry = merged.get(id);
3237+
let resolved_command = toml_acp
3238+
.command
3239+
.as_deref()
3240+
.and_then(resolve_env_value)
3241+
.or_else(|| toml_acp.command.clone())
3242+
.or_else(|| base_entry.map(|b| b.command.clone()))
3243+
.unwrap_or_default();
3244+
merged.insert(
3245+
id.clone(),
3246+
AcpAgentConfig {
3247+
id: id.clone(),
3248+
enabled: toml_acp.enabled,
3249+
command: resolved_command,
3250+
args: if toml_acp.args.is_empty() {
3251+
base_entry.map(|b| b.args.clone()).unwrap_or_default()
3252+
} else {
3253+
toml_acp.args.clone()
3254+
},
3255+
env: if toml_acp.env.is_empty() {
3256+
base_entry.map(|b| b.env.clone()).unwrap_or_default()
3257+
} else {
3258+
toml_acp.env.clone()
3259+
},
3260+
timeout: toml_acp
3261+
.timeout
3262+
.unwrap_or_else(|| base_entry.map(|b| b.timeout).unwrap_or(300)),
3263+
},
3264+
);
3265+
}
3266+
merged
3267+
},
31603268
worker_log_mode: toml
31613269
.defaults
31623270
.worker_log_mode
@@ -3308,6 +3416,30 @@ impl Config {
33083416
brave_search_key: a.brave_search_key.as_deref().and_then(resolve_env_value),
33093417
cron_timezone: a.cron_timezone.as_deref().and_then(resolve_env_value),
33103418
sandbox: a.sandbox,
3419+
acp: a.acp.map(|acp_map| {
3420+
acp_map
3421+
.into_iter()
3422+
.map(|(id, toml_acp)| {
3423+
let resolved_command = toml_acp
3424+
.command
3425+
.as_deref()
3426+
.and_then(resolve_env_value)
3427+
.or_else(|| toml_acp.command.clone())
3428+
.unwrap_or_default();
3429+
(
3430+
id.clone(),
3431+
AcpAgentConfig {
3432+
id,
3433+
enabled: toml_acp.enabled,
3434+
command: resolved_command,
3435+
args: toml_acp.args,
3436+
env: toml_acp.env,
3437+
timeout: toml_acp.timeout.unwrap_or(300),
3438+
},
3439+
)
3440+
})
3441+
.collect()
3442+
}),
33113443
cron,
33123444
})
33133445
})
@@ -3337,6 +3469,7 @@ impl Config {
33373469
brave_search_key: None,
33383470
cron_timezone: None,
33393471
sandbox: None,
3472+
acp: None,
33403473
cron: Vec::new(),
33413474
});
33423475
}
@@ -3625,6 +3758,8 @@ pub struct RuntimeConfig {
36253758
pub opencode: ArcSwap<OpenCodeConfig>,
36263759
/// Shared pool of OpenCode server processes. Lazily initialized on first use.
36273760
pub opencode_server_pool: Arc<crate::opencode::OpenCodeServerPool>,
3761+
/// ACP agent definitions for this runtime.
3762+
pub acp: ArcSwap<HashMap<String, AcpAgentConfig>>,
36283763
/// Cron store, set after agent initialization.
36293764
pub cron_store: ArcSwap<Option<Arc<crate::cron::CronStore>>>,
36303765
/// Cron scheduler, set after agent initialization.
@@ -3680,6 +3815,7 @@ impl RuntimeConfig {
36803815
skills: ArcSwap::from_pointee(skills),
36813816
opencode: ArcSwap::from_pointee(defaults.opencode.clone()),
36823817
opencode_server_pool: Arc::new(server_pool),
3818+
acp: ArcSwap::from_pointee(agent_config.acp.clone()),
36833819
cron_store: ArcSwap::from_pointee(None),
36843820
cron_scheduler: ArcSwap::from_pointee(None),
36853821
settings: ArcSwap::from_pointee(None),

0 commit comments

Comments
 (0)