1use std::path::PathBuf;
6use std::sync::Arc;
7
8use crate::hooks::Hooks;
9use std::collections::HashMap;
10
11use crate::mcp::McpServer;
12use crate::permissions::CanUseToolCallback;
13use forge_primitives::subagents::SubagentEffort;
15pub use forge_primitives::{PermissionMode, SdkPluginConfig, SystemPromptKind};
16
17pub type WireTee = Arc<dyn Fn(&str) + Send + Sync>;
22
23pub type AutoApproveToolPredicate = Arc<dyn Fn(&str) -> bool + Send + Sync>;
27
28#[derive(Clone)]
32pub struct Options {
33 pub binary: String,
35 pub cwd: Option<PathBuf>,
37 pub resume: Option<String>,
39 pub model: Option<String>,
41 pub permission_mode: PermissionMode,
43 pub can_use_tool: Option<Arc<dyn CanUseToolCallback>>,
46 pub auto_approve_tool: Option<AutoApproveToolPredicate>,
57 pub mcp_servers: Vec<(String, McpServer)>,
61 pub external_mcp_servers: std::collections::HashMap<String, forge_primitives::McpServerConfig>,
66 pub hooks: Hooks,
68 pub allowed_tools: Vec<String>,
71 pub permission_prompt_tool_name: Option<String>,
74 pub minimum_cli_version: Option<String>,
79 pub system_prompt: Option<SystemPromptKind>,
83 pub max_turns: Option<u64>,
85 pub session_id: Option<String>,
88 pub plugins: Vec<SdkPluginConfig>,
90 pub env: HashMap<String, String>,
92 pub extra_args: HashMap<String, Option<String>>,
95 pub effort: Option<SubagentEffort>,
100 pub tee_inbound: Option<WireTee>,
107 pub tee_outbound: Option<WireTee>,
114}
115
116impl Default for Options {
117 fn default() -> Self {
118 Self {
119 binary: "claude".into(),
120 cwd: None,
121 resume: None,
122 model: None,
123 permission_mode: PermissionMode::Ask,
124 can_use_tool: None,
125 auto_approve_tool: None,
126 mcp_servers: Vec::new(),
127 external_mcp_servers: HashMap::new(),
128 hooks: Hooks::default(),
129 allowed_tools: Vec::new(),
130 permission_prompt_tool_name: None,
131 minimum_cli_version: Some("2.0.0".into()),
132 system_prompt: None,
133 max_turns: None,
134 session_id: None,
135 plugins: Vec::new(),
136 env: HashMap::new(),
137 extra_args: HashMap::new(),
138 effort: None,
139 tee_inbound: None,
140 tee_outbound: None,
141 }
142 }
143}
144
145impl std::fmt::Debug for Options {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 f.debug_struct("Options")
148 .field("binary", &self.binary)
149 .field("cwd", &self.cwd)
150 .field("resume", &self.resume)
151 .field("model", &self.model)
152 .field("permission_mode", &self.permission_mode)
153 .field("can_use_tool", &self.can_use_tool.as_ref().map(|_| "<callback>"))
154 .field("auto_approve_tool", &self.auto_approve_tool.as_ref().map(|_| "<predicate>"))
155 .field("mcp_servers", &format!("<{} servers>", self.mcp_servers.len()))
156 .field(
157 "external_mcp_servers",
158 &format!("<{} external>", self.external_mcp_servers.len()),
159 )
160 .field("hooks", &self.hooks)
161 .field("allowed_tools", &self.allowed_tools)
162 .field("permission_prompt_tool_name", &self.permission_prompt_tool_name)
163 .field("minimum_cli_version", &self.minimum_cli_version)
164 .field("system_prompt", &self.system_prompt)
165 .field("max_turns", &self.max_turns)
166 .field("session_id", &self.session_id)
167 .field("plugins", &self.plugins)
168 .field("env", &format!("<{} vars>", self.env.len()))
169 .field("extra_args", &format!("<{} flags>", self.extra_args.len()))
170 .field("effort", &self.effort)
171 .field("tee_inbound", &self.tee_inbound.as_ref().map(|_| "<callback>"))
172 .field("tee_outbound", &self.tee_outbound.as_ref().map(|_| "<callback>"))
173 .finish()
174 }
175}
176
177#[derive(Clone, Default)]
179pub struct OptionsBuilder {
180 inner: Options,
181}
182
183impl std::fmt::Debug for OptionsBuilder {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 f.debug_struct("OptionsBuilder").field("inner", &self.inner).finish()
186 }
187}
188
189impl OptionsBuilder {
190 pub fn new() -> Self {
192 Self::default()
193 }
194
195 pub fn binary(mut self, binary: impl Into<String>) -> Self {
197 self.inner.binary = binary.into();
198 self
199 }
200
201 pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
203 self.inner.cwd = Some(cwd.into());
204 self
205 }
206
207 pub fn resume(mut self, session_id: impl Into<String>) -> Self {
209 self.inner.resume = Some(session_id.into());
210 self
211 }
212
213 pub fn model(mut self, model: impl Into<String>) -> Self {
215 self.inner.model = Some(model.into());
216 self
217 }
218
219 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
221 self.inner.permission_mode = mode;
222 self
223 }
224
225 pub fn can_use_tool<C>(mut self, callback: C) -> Self
229 where
230 C: CanUseToolCallback + 'static,
231 {
232 self.inner.can_use_tool = Some(Arc::new(callback));
233 self
234 }
235
236 pub fn auto_approve_tool<F>(mut self, predicate: F) -> Self
242 where
243 F: Fn(&str) -> bool + Send + Sync + 'static,
244 {
245 self.inner.auto_approve_tool = Some(Arc::new(predicate));
246 self
247 }
248
249 pub fn mcp_server(mut self, name: impl Into<String>, server: McpServer) -> Self {
252 self.inner.mcp_servers.push((name.into(), server));
253 self
254 }
255
256 pub fn hooks(mut self, hooks: Hooks) -> Self {
258 self.inner.hooks = hooks;
259 self
260 }
261
262 pub fn allowed_tools<I, S>(mut self, tools: I) -> Self
264 where
265 I: IntoIterator<Item = S>,
266 S: Into<String>,
267 {
268 self.inner.allowed_tools = tools.into_iter().map(Into::into).collect();
269 self
270 }
271
272 pub fn permission_prompt_tool_name(mut self, name: impl Into<String>) -> Self {
277 self.inner.permission_prompt_tool_name = Some(name.into());
278 self
279 }
280
281 pub fn system_prompt(mut self, sp: SystemPromptKind) -> Self {
283 self.inner.system_prompt = Some(sp);
284 self
285 }
286
287 pub fn max_turns(mut self, n: u64) -> Self {
289 self.inner.max_turns = Some(n);
290 self
291 }
292
293 pub fn session_id(mut self, id: impl Into<String>) -> Self {
295 self.inner.session_id = Some(id.into());
296 self
297 }
298
299 pub fn plugins(mut self, plugins: Vec<SdkPluginConfig>) -> Self {
301 self.inner.plugins = plugins;
302 self
303 }
304
305 pub fn env(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
307 self.inner.env.insert(k.into(), v.into());
308 self
309 }
310
311 pub fn extra_arg(mut self, flag: impl Into<String>, value: Option<String>) -> Self {
313 self.inner.extra_args.insert(flag.into(), value);
314 self
315 }
316
317 pub fn effort(mut self, e: SubagentEffort) -> Self {
319 self.inner.effort = Some(e);
320 self
321 }
322
323 pub fn tee_inbound(mut self, cb: impl Fn(&str) + Send + Sync + 'static) -> Self {
328 self.inner.tee_inbound = Some(Arc::new(cb));
329 self
330 }
331
332 pub fn tee_outbound(mut self, cb: impl Fn(&str) + Send + Sync + 'static) -> Self {
337 self.inner.tee_outbound = Some(Arc::new(cb));
338 self
339 }
340
341 pub fn build(self) -> Options {
343 self.inner
344 }
345}
346
347#[cfg(test)]
348mod tests_skills_option {
349 #[allow(unused_imports)]
351 use super::*;
352
353 use crate::OptionsBuilder;
354
355 #[test]
356 fn allowed_tools_round_trip() {
357 let opts = OptionsBuilder::new().allowed_tools(["Read", "Grep"]).build();
358 assert_eq!(opts.allowed_tools, vec!["Read".to_string(), "Grep".into()]);
359 }
360}
361
362#[cfg(test)]
363mod tests_options_build {
364 #[allow(unused_imports)]
366 use super::*;
367
368 use std::path::PathBuf;
369
370 use crate::{OptionsBuilder, PermissionMode};
371
372 #[test]
373 fn default_options() {
374 let opts = OptionsBuilder::new().build();
375 assert_eq!(opts.binary, "claude");
376 assert!(opts.cwd.is_none());
377 assert!(opts.resume.is_none());
378 assert_eq!(opts.permission_mode, PermissionMode::Ask);
379 assert!(opts.model.is_none());
380 }
381
382 #[test]
383 fn builder_sets_model_and_cwd() {
384 let opts = OptionsBuilder::new().model("claude-opus-4-5").cwd("/tmp/project").build();
385 assert_eq!(opts.model.as_deref(), Some("claude-opus-4-5"));
386 assert_eq!(opts.cwd, Some(PathBuf::from("/tmp/project")));
387 }
388
389 #[test]
390 fn builder_sets_resume_session() {
391 let opts = OptionsBuilder::new().resume("sess_abc").build();
392 assert_eq!(opts.resume.as_deref(), Some("sess_abc"));
393 }
394
395 #[test]
396 fn builder_sets_permission_mode() {
397 let opts = OptionsBuilder::new().permission_mode(PermissionMode::AcceptEdits).build();
398 assert_eq!(opts.permission_mode, PermissionMode::AcceptEdits);
399 }
400
401 #[test]
402 fn builder_sets_custom_binary() {
403 let opts = OptionsBuilder::new().binary("/usr/local/bin/claude").build();
404 assert_eq!(opts.binary, "/usr/local/bin/claude");
405 }
406
407 #[test]
408 fn builder_stores_can_use_tool_callback() {
409 use forge_primitives::{PermissionDecision, ToolPermissionContext};
410
411 let opts = OptionsBuilder::new()
412 .can_use_tool(|_ctx: ToolPermissionContext| async move { PermissionDecision::allow() })
413 .build();
414 assert!(opts.can_use_tool.is_some());
415 }
416
417 #[test]
418 fn auto_approve_tool_predicate_exact_prefix_match() {
419 let opts = OptionsBuilder::new()
423 .auto_approve_tool(|name: &str| name.starts_with("mcp__forge__"))
424 .build();
425 let pred = opts.auto_approve_tool.expect("predicate stored");
426 assert!(pred("mcp__forge__agents__whoami"));
427 assert!(pred("mcp__forge__agents__send_message"));
428 assert!(pred("mcp__forge__agents__list"));
429 assert!(pred("mcp__forge__agents__spawn"));
432 assert!(pred("mcp__forge__agents__capacity"));
433 assert!(pred("mcp__forge__agents__update"));
434 assert!(pred("mcp__forge__agents__despawn"));
435 assert!(pred("mcp__forge__cron__create"));
437 assert!(pred("mcp__forge__cron__list"));
438 assert!(pred("mcp__forge__cron__delete"));
439 assert!(pred("mcp__forge__review__list"));
443 assert!(pred("mcp__forge__review__get"));
444 assert!(pred("mcp__forge__review__reply"));
445 assert!(pred("mcp__forge__review__resolve"));
446 assert!(pred("mcp__forge__"));
447 assert!(!pred("mcp__forgery__steal_secrets"));
449 assert!(!pred("mcp__forge"));
450 assert!(!pred("Bash"));
451 assert!(!pred("Read"));
452 assert!(!pred(""));
454 }
455
456 #[test]
457 fn auto_approve_tool_default_is_none() {
458 let opts = OptionsBuilder::new().build();
459 assert!(opts.auto_approve_tool.is_none());
460 }
461}