//! `SubWorkerSpawn` tool — start a parent-owned Internal Worker session. //! //! Resolves a child profile, validates filesystem delegation, constructs a normal Worker with the //! parent's explicit Workspace authority, installs its enabled features, and hands it to the //! in-process Internal Worker session actor. No Runtime Worker record, OS process, PID, Unix socket, //! or machine-wide child allocation is created. use std::path::{Path, PathBuf}; use std::sync::Arc; use arc_swap::ArcSwap; use async_trait::async_trait; use fs_operation::FsPath; use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput}; use manifest::{ CompactionConfigPartial, EngineManifestConfig, FileUploadLimitsPartial, Permission, PermissionConfigPartial, ProfileDiscovery, ProfileError, ProfileRegistry, ProfileRegistrySource, ProfileResolveOptions, ProfileResolver, ProfileSelector, ScopeConfig, ScopeRule, SessionConfigPartial, ToolOutputLimitsPartial, WorkerManifest, WorkerManifestConfig, WorkerMetaConfig, }; use serde::Deserialize; use tokio::sync::mpsc; use workdir::{ WorkdirDelegationPermission, WorkdirDelegationRequest, WorkdirDelegationRule, WorkdirSessionHandle, }; use crate::PromptCatalogSource; use crate::controller::register_worker_tools; use crate::internal_worker::{ EphemeralSessionStore, InternalWorkerSessionStatus, prepare_internal_worker_session, }; use crate::prompt::catalog::PromptCatalog; use crate::spawn::registry::SpawnedWorkerRegistry; use crate::worker::{ ReviewerChildWorkspaceClient, ReviewerContext, Worker, WorkerFilesystemAuthority, WorkspaceRequest, WorkspaceRequestMethod, }; use protocol::Method; #[derive(Debug, Deserialize, schemars::JsonSchema)] struct SubWorkerSpawnInput { /// Identifier for the spawned Internal SubWorker. Must be unique among this Worker's direct children. name: String, /// Profile selector for child role configuration. Omit or use `default` /// for the effective child default profile, use `inherit` to derive /// reusable config from the spawner, or use a registry selector such as /// `project:coder`, `project:reviewer`, `builtin:companion`, or an /// unambiguous profile slug. Raw/path selectors are rejected. #[serde(default)] profile: Option, /// Exact catalog-root dotted Prompt name (for example `default` or `role.coder`). #[serde(default)] instruction: Option, /// Child process/tool working directory. This is not the runtime workspace /// root and grants no filesystem authority. When omitted, the spawned SubWorker /// starts in the spawner's current working directory. #[serde(default)] cwd: Option, /// First message sent to the spawned SubWorker via `Method::Run`. task: String, /// Allow rules delegated to the spawned SubWorker. Must be a subset of the /// spawner's explicit delegation authority; direct tool scope alone is not /// sufficient. Omit `recursive` for normal workspace/worktree delegation; it defaults to true. scope: Vec, /// Binds an actual read-only builtin Reviewer child to the current Merge Request candidate. /// Review capability material is generated by the trusted spawn layer. #[serde(default)] review: Option, } #[derive(Debug, Deserialize, schemars::JsonSchema)] struct ReviewerHandoffInput { ticket_id: String, } #[derive(Debug, Deserialize, schemars::JsonSchema)] struct ScopeRuleInput { /// Absolute target path. Relative paths are rejected. target: PathBuf, /// `"read"` or `"write"`. permission: PermissionInput, /// When `false`, the rule matches the target itself and its direct /// children only. Defaults to `true`. #[serde(default = "default_true")] recursive: bool, } #[derive(Debug, Deserialize, schemars::JsonSchema, Clone, Copy)] #[serde(rename_all = "lowercase")] enum PermissionInput { Read, Write, } fn default_true() -> bool { true } impl From for Permission { fn from(p: PermissionInput) -> Self { match p { PermissionInput::Read => Permission::Read, PermissionInput::Write => Permission::Write, } } } #[derive(Debug, Clone)] struct AvailableProfiles { registry: Option, diagnostic: Option, } impl AvailableProfiles { fn discover(cwd: &Path) -> Self { match ProfileDiscovery::for_cwd(cwd).discover() { Ok(registry) => Self { registry: Some(registry), diagnostic: None, }, Err(error) => Self { registry: None, diagnostic: Some(error.to_string()), }, } } fn compact_list(&self) -> String { let Some(registry) = &self.registry else { return "- profile discovery failed; use `inherit` or retry after fixing discovery" .into(); }; if registry.entries().is_empty() { return "- no registry profiles discovered; `inherit` is still available".into(); } registry .entries() .iter() .map(|entry| { let default = if entry.is_default { " (default)" } else { "" }; let desc = entry .description .as_deref() .map(|d| format!(" — {d}")) .unwrap_or_default(); format!("- `{}`{}{}", entry.qualified_name(), default, desc) }) .collect::>() .join("\n") } fn default_label(&self) -> String { self.registry .as_ref() .and_then(|registry| registry.default_entry().ok()) .map(|entry| entry.qualified_name()) .unwrap_or_else(|| "none resolved".into()) } fn diagnostic(&self) -> &str { self.diagnostic.as_deref().unwrap_or("") } fn error_suffix(&self) -> String { format!( "\nUse `default`, `inherit`, or one of these registry selectors:\n{}", self.compact_list() ) } } #[derive(Debug, Clone, PartialEq, Eq)] enum SpawnProfileSelector { Default, Inherit, Registry(ProfileSelector), } fn parse_spawn_profile_selector(raw: Option<&str>) -> Result { let Some(raw) = raw.map(str::trim).filter(|s| !s.is_empty()) else { return Ok(SpawnProfileSelector::Default); }; if raw == "default" { return Ok(SpawnProfileSelector::Default); } if raw == "inherit" { return Ok(SpawnProfileSelector::Inherit); } if raw.starts_with("path:") || raw.starts_with('/') || raw.starts_with("./") || raw.starts_with("../") || raw.contains('/') || raw.ends_with(".dcdl") || raw.ends_with(".json") || raw.ends_with(".toml") || raw.ends_with(".nix") { return Err(format!( "SubWorkerSpawn.profile accepts `default`, `inherit`, or registry selectors only; path-like selector `{raw}` is not allowed" )); } if let Some((prefix, name)) = raw.split_once(':') { let source = match prefix { "builtin" => ProfileRegistrySource::Builtin, "user" => ProfileRegistrySource::User, "project" => ProfileRegistrySource::Project, _ => { return Err(format!( "unsupported SubWorkerSpawn.profile selector prefix `{prefix}`; use builtin:, user:, project:, default, or inherit" )); } }; if name.is_empty() { return Err( "SubWorkerSpawn.profile registry selector has an empty profile name".into(), ); } return Ok(SpawnProfileSelector::Registry( ProfileSelector::source_named(source, name), )); } Ok(SpawnProfileSelector::Registry(ProfileSelector::named(raw))) } #[derive(Clone)] pub(crate) enum ParentNotificationTarget { Controller(mpsc::WeakSender), Buffer(crate::ipc::notify_buffer::NotifyBuffer), } impl ParentNotificationTarget { fn notify(&self, message: String, auto_run: bool) { match self { Self::Controller(parent_method_tx) => { let Some(parent_method_tx) = parent_method_tx.upgrade() else { tracing::warn!( "parent Worker controller closed before Internal SubWorker completion notification" ); return; }; tokio::spawn(async move { if let Err(error) = parent_method_tx .send(Method::Notify { message, auto_run }) .await { tracing::warn!( %error, "failed to notify parent Worker about Internal SubWorker completion" ); } }); } Self::Buffer(parent_notifies) => parent_notifies.push_notify(message, auto_run), } } } /// Runtime dependencies the `SubWorkerSpawn` tool needs in order to launch a /// child SubWorker and record the handoff locally. Constructed by the Worker /// controller once per Worker lifetime. pub struct SubWorkerSpawnTool { /// Spawner's own Worker name, used for direct-child identity collision checks. spawner_name: String, workspace_context: crate::worker::WorkerWorkspaceContext, parent_notifications: ParentNotificationTarget, /// Runtime-owned root used only for bounded Internal Worker tool artifacts such as Bash spill /// output. It is not an Internal Worker identity or catalog location. runtime_base: PathBuf, /// Inherited runtime workspace root for Profile/project/Ticket/workflow/ /// memory context. SubWorkerSpawn `cwd` must not affect this value. workspace_root: PathBuf, /// Directory the spawned SubWorker's tools should use when the LLM did not /// override it. Defaults to the spawner's cwd. spawner_cwd: PathBuf, /// Active provider-backed Workdir session from which child leases are captured. source_workdir_session: Option, /// Parent-owned in-memory registry shared by the five SubWorker tools. registry: Arc, /// Spawner's resolved Manifest. `profile = "inherit"` derives the /// child config from reusable fields here, and selected profiles are /// merged into the same internal handoff shape before launch. spawner_manifest: WorkerManifest, prompt_loader: PromptCatalogSource, /// Compact selector list shared by tool description and diagnostics. available_profiles: AvailableProfiles, internal_client_override: Option>, } impl SubWorkerSpawnTool { #[cfg(test)] fn with_internal_client(mut self, client: Box) -> Self { self.internal_client_override = Some(client); self } fn new( spawner_name: String, workspace_context: crate::worker::WorkerWorkspaceContext, parent_notifications: ParentNotificationTarget, runtime_base: PathBuf, workspace_root: PathBuf, spawner_cwd: PathBuf, source_workdir_session: Option, registry: Arc, spawner_manifest: WorkerManifest, prompt_loader: PromptCatalogSource, available_profiles: AvailableProfiles, ) -> Self { Self { spawner_name, workspace_context, parent_notifications, runtime_base, workspace_root, spawner_cwd, source_workdir_session, registry, spawner_manifest, prompt_loader, available_profiles, internal_client_override: None, } } } fn validate_reviewer_handoff(input: &SubWorkerSpawnInput) -> Result<(), ToolError> { let Some(review) = &input.review else { return Ok(()); }; if review.ticket_id.trim().is_empty() { return Err(ToolError::InvalidArgument( "reviewer handoff requires non-empty ticket_id".to_string(), )); } if input.profile.as_deref() != Some("builtin:reviewer") { return Err(ToolError::InvalidArgument( "reviewer handoff requires the explicit effective profile builtin:reviewer".to_string(), )); } if input .scope .iter() .any(|rule| matches!(rule.permission, PermissionInput::Write)) { return Err(ToolError::InvalidArgument( "Merge Request Reviewer SubWorkers must have read-only delegated scope".to_string(), )); } Ok(()) } #[async_trait] impl Tool for SubWorkerSpawnTool { async fn execute( &self, input_json: &str, _ctx: llm_engine::tool::ToolExecutionContext, ) -> Result { let input: SubWorkerSpawnInput = serde_json::from_str(input_json).map_err(|e| { ToolError::InvalidArgument(format!("invalid SubWorkerSpawn input: {e}")) })?; // `delegate_scope` catches this too (as `DuplicateWorkerName`), but // the dedicated message is kinder to the LLM — which gets the // error back verbatim — than the generic duplicate-name error. if input.name == self.spawner_name { return Err(ToolError::InvalidArgument(format!( "spawned worker name `{}` collides with spawner's own name", input.name ))); } validate_reviewer_handoff(&input)?; let name_reservation = self .registry .reserve_internal_name(input.name.clone()) .map_err(|error| ToolError::InvalidArgument(error.to_string()))?; let scope_allow = parse_scope(&input.scope)?; let source_workdir_session = require_active_workdir_session(self.source_workdir_session.as_ref())?; let delegation_request = self.workdir_delegation_request(input.cwd.as_deref(), &scope_allow)?; let workdir_delegation = source_workdir_session .delegate(delegation_request) .await .map_err(|error| { ToolError::InvalidArgument(format!("delegate Workdir session: {error}")) })?; let spawn_selector = parse_spawn_profile_selector(input.profile.as_deref()).map_err(|msg| { ToolError::InvalidArgument(format!( "{msg}{}", self.available_profiles.error_suffix() )) })?; let spawn_config_json = self .build_spawn_config_json( &input.name, input.instruction.as_deref(), &scope_allow, spawn_selector, ) .map_err(|e| ToolError::InvalidArgument(format!("{e}")))?; let mut child_config: WorkerManifestConfig = serde_json::from_str(&spawn_config_json) .map_err(|error| { ToolError::ExecutionFailed(format!("resolve child manifest: {error}")) })?; child_config.delegation_scope = ScopeConfig { allow: scope_allow.clone(), deny: Vec::new(), }; let mut child_manifest = WorkerManifest::try_from(WorkerManifestConfig::builtin_defaults().merge(child_config)) .map_err(|error| { ToolError::ExecutionFailed(format!("resolve child manifest: {error}")) })?; // Delegated children stay bound to their scoped session and cannot use // Workspace attachment tools to replace it with parent-level authority. child_manifest.feature.manage_workdir.enabled = false; let reviewer_capability = input.review.as_ref().map(|review| { ( review.ticket_id.clone(), format!( "{}{}", uuid::Uuid::now_v7().simple(), uuid::Uuid::now_v7().simple() ), ) }); let child_workspace_context = if let Some((ticket_id, capability_token)) = &reviewer_capability { let workspace_id = self.workspace_context .workspace_id() .cloned() .ok_or_else(|| { ToolError::InvalidArgument( "reviewer handoff requires Workspace identity".to_string(), ) })?; let parent_client = self.workspace_context.client_handle(); if !parent_client.is_available() { return Err(ToolError::InvalidArgument( "reviewer handoff requires Workspace API authority".to_string(), )); } let child_client: Arc = Arc::new(ReviewerChildWorkspaceClient::new( parent_client.clone(), ReviewerContext { ticket_id: ticket_id.clone(), }, capability_token.clone(), )); crate::worker::WorkerWorkspaceContext::with_client(Some(workspace_id), child_client) } else { self.workspace_context.clone() }; let store = EphemeralSessionStore::default(); let filesystem_authority = WorkerFilesystemAuthority::None; let mut child = Worker::, EphemeralSessionStore>::from_internal_manifest_with_context( child_manifest, store.clone(), self.prompt_loader.clone(), child_workspace_context, filesystem_authority, self.internal_client_override .as_ref() .map(|client| client.clone_boxed()), ) .await .map_err(|error| ToolError::ExecutionFailed(format!("build Internal Worker: {error}")))?; child.bind_workdir_session(Some(workdir_delegation.scoped_session.clone())); let child_scope = child.scope().clone(); let child_registry = SpawnedWorkerRegistry::new_internal(input.name.clone(), child_scope); register_worker_tools( &mut child, self.runtime_base .join("internal-workers") .join(&input.name) .join("bash-output"), self.runtime_base.clone(), child_registry, None, ) .await .map_err(|error| { ToolError::ExecutionFailed(format!("install Internal Worker features: {error}")) })?; #[cfg(test)] let installed_tools = child .engine() .tool_server_handle() .tool_definitions_sorted() .into_iter() .map(|definition| definition.name) .collect(); let child_name = input.name.clone(); let registry = Arc::downgrade(&self.registry); let parent_notifications = self.parent_notifications.clone(); let session_result = prepare_internal_worker_session( child, store, Some(Arc::new(move |status| { if status == InternalWorkerSessionStatus::Failed { if let Some(registry) = registry.upgrade() { if let Err(error) = registry.reclaim_internal_scope(&child_name) { tracing::warn!( child_name, %error, "failed to reclaim delegated scope after Internal SubWorker failure" ); } } } let message = format!( "SubWorker `{child_name}` turn ended with status {status:?}. Inspect its committed session with worker-observation tools before making completion decisions." ); parent_notifications.notify(message, true); })), ) .await; let session = session_result.map_err(|error| { ToolError::ExecutionFailed(format!("prepare Internal Worker session: {error}")) })?; if let Some((ticket_id, capability_token)) = &reviewer_capability { let workspace_id = self.workspace_context.workspace_id().ok_or_else(|| { ToolError::ExecutionFailed("review capability lost Workspace identity".to_string()) })?; let child_session_id = session.session_id_string(); let child_registration = WorkspaceRequest::json( WorkspaceRequestMethod::Post, format!( "/api/w/{}/internal/reviewer-child-sessions", workspace_id.as_str() ), serde_json::json!({"child_session_id": child_session_id}).to_string(), ); let child_response = self .workspace_context .client() .execute(child_registration) .map_err(|error| { ToolError::ExecutionFailed(format!( "register Runtime-owned Reviewer child session: {error}" )) })?; if !child_response.is_success() { let _ = session.stop().await; return Err(ToolError::ExecutionFailed(format!( "register Runtime-owned Reviewer child session failed with status {}: {}", child_response.status, child_response.body ))); } let body = serde_json::json!({ "child_session_id": child_session_id, "capability_token": capability_token, }); let request = WorkspaceRequest::json( WorkspaceRequestMethod::Post, format!( "/api/w/{}/tickets/{}/merge-request/review-capabilities", workspace_id.as_str(), ticket_id ), body.to_string(), ); let response = self .workspace_context .client() .execute(request) .map_err(|error| { ToolError::ExecutionFailed(format!("register review capability: {error}")) })?; if !response.is_success() { let _ = session.stop().await; return Err(ToolError::ExecutionFailed(format!( "register review capability failed with status {}: {}", response.status, response.body ))); } } let record = crate::spawn::registry::InternalSpawnedWorkerRecord::new( input.name.clone(), scope_allow, workdir_delegation, #[cfg(test)] installed_tools, session.clone(), ); if let Err(error) = name_reservation.commit(record) { let _ = session.stop().await; return Err(ToolError::ExecutionFailed(format!( "register Internal Worker session: {error}" ))); } if let Err(error) = session.send(input.task).await { let _ = session.stop().await; let _ = self.registry.remove_internal(&input.name).await; return Err(ToolError::ExecutionFailed(format!( "start Internal Worker session: {error}" ))); } Ok(ToolOutput { summary: format!("spawned internal worker `{}`", input.name), content: None, attachments: Vec::new(), }) } } impl SubWorkerSpawnTool { fn workdir_delegation_request( &self, cwd: Option<&Path>, scope_allow: &[ScopeRule], ) -> Result { let rules = scope_allow .iter() .map(|rule| { Ok(WorkdirDelegationRule { target: self.logical_workdir_path(&rule.target)?, permission: match rule.permission { Permission::Read => WorkdirDelegationPermission::Read, Permission::Write => WorkdirDelegationPermission::Write, }, recursive: rule.recursive, }) }) .collect::, ToolError>>()?; let cwd = cwd.unwrap_or(&self.spawner_cwd); if !cwd.is_absolute() { return Err(ToolError::InvalidArgument(format!( "cwd must be absolute, got `{}`", cwd.display() ))); } Ok(WorkdirDelegationRequest { rules, cwd: self.logical_workdir_path(cwd)?, }) } fn logical_workdir_path(&self, path: &Path) -> Result { let logical = if self.workspace_root == Path::new("/") { path.strip_prefix(Path::new("/")) } else { path.strip_prefix(&self.workspace_root) } .map_err(|_| { ToolError::InvalidArgument(format!( "scope target `{}` is not a Workdir-owned logical path", path.display() )) })?; let logical = logical.to_str().ok_or_else(|| { ToolError::InvalidArgument(format!( "scope target `{}` is not valid UTF-8", path.display() )) })?; FsPath::new(logical).map_err(|error| { ToolError::InvalidArgument(format!( "scope target `{}` is not a valid logical Workdir path: {error}", path.display() )) }) } } fn require_active_workdir_session( session: Option<&WorkdirSessionHandle>, ) -> Result<&WorkdirSessionHandle, ToolError> { session.ok_or_else(|| { ToolError::InvalidArgument( "SubWorkerSpawn requires an active Workdir session; attach a Workdir before delegating filesystem access" .to_string(), ) }) } fn parse_scope(rules: &[ScopeRuleInput]) -> Result, ToolError> { if rules.is_empty() { return Err(ToolError::InvalidArgument("scope must not be empty".into())); } rules .iter() .map(|r| { if !r.target.is_absolute() { return Err(ToolError::InvalidArgument(format!( "scope.target must be absolute: {}", r.target.display() ))); } Ok(ScopeRule { target: r.target.clone(), permission: r.permission.into(), recursive: r.recursive, }) }) .collect() } /// Serialise the internal manifest config that gets handed to the child /// Worker runtime process via the hidden `--spawn-config-json` flag. /// `WorkerManifestConfig`'s `Serialize` impl is the single source of truth for the /// internal handoff shape. /// /// The child's tool working directory is carried separately through /// the child runtime entrypoint; it is not part of the manifest. impl SubWorkerSpawnTool { fn build_spawn_config_json( &self, name: &str, instruction_override: Option<&str>, scope_allow: &[ScopeRule], selector: SpawnProfileSelector, ) -> Result { build_spawn_config_json_for_profile( &self.spawner_manifest, &self.available_profiles, &self.workspace_root, name, instruction_override, scope_allow, selector, ) } } fn build_spawn_config_json_for_profile( spawner_manifest: &WorkerManifest, available_profiles: &AvailableProfiles, workspace_root: &Path, name: &str, instruction_override: Option<&str>, scope_allow: &[ScopeRule], selector: SpawnProfileSelector, ) -> Result { let mut config = match selector { SpawnProfileSelector::Inherit => manifest_to_reusable_config(spawner_manifest), SpawnProfileSelector::Default | SpawnProfileSelector::Registry(_) => { let registry = available_profiles.registry.as_ref().ok_or_else(|| { format!( "profile discovery failed for SubWorkerSpawn: {}{}", available_profiles.diagnostic().if_empty("unknown error"), available_profiles.error_suffix() ) })?; let profile_selector = match selector { SpawnProfileSelector::Default => ProfileSelector::Default, SpawnProfileSelector::Registry(selector) => selector, SpawnProfileSelector::Inherit => unreachable!(), }; let resolved = ProfileResolver::new() .with_workspace_base(workspace_root) .resolve_from_registry( &profile_selector, registry, ProfileResolveOptions::with_worker_name(name), ) .map_err(|e| profile_error_with_available(e, available_profiles))?; manifest_to_reusable_config(&resolved.manifest) } }; config.worker.name = Some(name.to_string()); config.scope = ScopeConfig { allow: scope_allow.to_vec(), deny: Vec::new(), }; if let Some(instruction) = instruction_override { config.engine.instruction = Some(instruction.to_string()); } serde_json::to_string(&config).map_err(|e| format!("spawn config serialisation: {e}")) } #[cfg(test)] fn build_spawn_config_json( name: &str, instruction: &str, scope_allow: &[ScopeRule], model: &manifest::ModelManifest, record_event_trace: bool, ) -> Result { let config = WorkerManifestConfig { worker: WorkerMetaConfig { name: Some(name.to_string()), }, model: model.clone(), engine: EngineManifestConfig { instruction: Some(instruction.to_string()), ..Default::default() }, scope: ScopeConfig { allow: scope_allow.to_vec(), deny: Vec::new(), }, session: record_event_trace.then_some(SessionConfigPartial { record_event_trace: Some(true), }), ..Default::default() }; serde_json::to_string(&config) } trait IfEmpty { fn if_empty(&self, fallback: &str) -> String; } impl IfEmpty for str { fn if_empty(&self, fallback: &str) -> String { if self.is_empty() { fallback.into() } else { self.into() } } } fn profile_error_with_available(error: ProfileError, available: &AvailableProfiles) -> String { format!( "invalid SubWorkerSpawn.profile: {error}{}", available.error_suffix() ) } fn manifest_to_reusable_config(manifest: &WorkerManifest) -> WorkerManifestConfig { WorkerManifestConfig { worker: WorkerMetaConfig { name: Some(manifest.worker.name.clone()), }, model: manifest.model.clone(), engine: EngineManifestConfig { instruction: Some(manifest.engine.instruction.clone()), language: Some(manifest.engine.language.clone()), max_tokens: manifest.engine.max_tokens, max_turns: manifest.engine.max_turns, temperature: manifest.engine.temperature, top_p: manifest.engine.top_p, top_k: manifest.engine.top_k, stop_sequences: (!manifest.engine.stop_sequences.is_empty()) .then_some(manifest.engine.stop_sequences.clone()), reasoning: manifest.engine.reasoning.clone(), tool_output: ToolOutputLimitsPartial { default_max_bytes: Some(manifest.engine.tool_output.default_max_bytes), per_tool: manifest.engine.tool_output.per_tool.clone(), }, file_upload: FileUploadLimitsPartial { max_bytes: Some(manifest.engine.file_upload.max_bytes), }, }, scope: ScopeConfig { allow: manifest.scope.allow.clone(), deny: manifest.scope.deny.clone(), }, // `inherit` reuses behavioral configuration, not subdelegation authority. delegation_scope: ScopeConfig::default(), session: Some(SessionConfigPartial { record_event_trace: Some(manifest.session.record_event_trace), }), permissions: manifest .permissions .as_ref() .map(|p| PermissionConfigPartial { default_action: Some(p.default_action), rules: p.rules.clone(), }), feature: manifest.feature.clone().into(), plugins: manifest.plugins.clone(), mcp: manifest.mcp.clone(), compaction: manifest .compaction .as_ref() .map(|c| CompactionConfigPartial { prune_protected_tokens: Some(c.prune_protected_tokens), prune_min_savings: Some(c.prune_min_savings), threshold: c.threshold, request_threshold: c.request_threshold, retained_tokens: Some(c.retained_tokens), overview_target_tokens: Some(c.overview_target_tokens), overview_warning_tokens: Some(c.overview_warning_tokens), overview_deadline_tokens: Some(c.overview_deadline_tokens), worker_context_max_tokens: Some(c.worker_context_max_tokens), finish_warning_remaining_tokens: Some(c.finish_warning_remaining_tokens), final_reserve_tokens: Some(c.final_reserve_tokens), worker_max_turns: c.worker_max_turns, summary_target_tokens: Some(c.summary_target_tokens), summary_max_tokens: Some(c.summary_max_tokens), auto_read_budget_tokens: Some(c.auto_read_budget_tokens), result_context_max_tokens: Some(c.result_context_max_tokens), model: c.model.clone(), }), web: manifest.web.clone(), memory: manifest.memory.clone(), skills: manifest.skills.clone(), } } /// Tail of the spawned child's `stderr.log` to splice into a startup /// failure message. Capped so a chatty child can't blow up the LLM's /// tool-result budget — debugging beyond this should read the file /// directly. /// Factory for the `SubWorkerSpawn` tool. pub(crate) fn sub_worker_spawn_tool( spawner_name: String, workspace_context: crate::worker::WorkerWorkspaceContext, parent_notifications: ParentNotificationTarget, runtime_base: PathBuf, workspace_root: PathBuf, spawner_cwd: PathBuf, source_workdir_session: Option, registry: Arc, spawner_manifest: WorkerManifest, prompts: Arc>, ) -> ToolDefinition { sub_worker_spawn_tool_impl( spawner_name, workspace_context, parent_notifications, runtime_base, workspace_root, spawner_cwd, source_workdir_session, registry, spawner_manifest, prompts, ) } fn sub_worker_spawn_tool_impl( spawner_name: String, workspace_context: crate::worker::WorkerWorkspaceContext, parent_notifications: ParentNotificationTarget, runtime_base: PathBuf, workspace_root: PathBuf, spawner_cwd: PathBuf, source_workdir_session: Option, registry: Arc, spawner_manifest: WorkerManifest, prompts: Arc>, ) -> ToolDefinition { Arc::new(move || { let schema = schemars::schema_for!(SubWorkerSpawnInput); let schema_value = serde_json::to_value(schema).unwrap_or(serde_json::json!({})); let available_profiles = AvailableProfiles::discover(&workspace_root); let description = prompts .load_full() .sub_worker_spawn_tool_description( &available_profiles.compact_list(), &available_profiles.default_label(), available_profiles.diagnostic(), ) .unwrap_or_else(|e| { format!( "Spawn an in-process Internal SubWorker session to split context for a delegated task. Profile description rendering failed: {e}. Available profiles:\n{}", available_profiles.compact_list() ) }); let meta = ToolMeta::new("SubWorkerSpawn") .description(description) .input_schema(schema_value); let tool: Arc = Arc::new(SubWorkerSpawnTool::new( spawner_name.clone(), workspace_context.clone(), parent_notifications.clone(), runtime_base.clone(), workspace_root.clone(), spawner_cwd.clone(), source_workdir_session.clone(), registry.clone(), spawner_manifest.clone(), prompts.load_full().source(), available_profiles, )); (meta, tool) }) } #[cfg(test)] mod tests { use super::*; use manifest::{DelegationScope, Scope, SharedScope}; use std::pin::Pin; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::time::Duration; use crate::WorkspaceId; use async_trait::async_trait; use futures::Stream; use llm_engine::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent}; use llm_engine::llm_client::types::ContentPart; use llm_engine::llm_client::{ClientError, LlmClient, Request}; use llm_engine::{Item, Role}; use manifest::{AuthRef, ModelManifest, SchemeKind, WorkerManifest}; use tempfile::TempDir; use crate::worker::{ WorkspaceClient, WorkspaceClientError, WorkspaceRequest, WorkspaceResponse, }; #[test] fn missing_active_workdir_session_fails_deterministically() { let error = require_active_workdir_session(None).unwrap_err(); assert!(matches!( error, ToolError::InvalidArgument(message) if message.contains("requires an active Workdir session") )); } #[test] fn reviewer_handoff_requires_explicit_builtin_profile_and_read_only_scope() { let valid: SubWorkerSpawnInput = serde_json::from_value(serde_json::json!({ "name":"reviewer","task":"review","profile":"builtin:reviewer", "scope":[{"target":"/tmp/work","permission":"read"}], "review":{"ticket_id":"T1"} })) .unwrap(); assert!(validate_reviewer_handoff(&valid).is_ok()); let wrong_profile: SubWorkerSpawnInput = serde_json::from_value(serde_json::json!({ "name":"reviewer","task":"review","profile":"builtin:coder", "scope":[{"target":"/tmp/work","permission":"read"}], "review":{"ticket_id":"T1"} })) .unwrap(); assert!(validate_reviewer_handoff(&wrong_profile).is_err()); let writable: SubWorkerSpawnInput = serde_json::from_value(serde_json::json!({ "name":"reviewer","task":"review","profile":"builtin:reviewer", "scope":[{"target":"/tmp/work","permission":"write"}], "review":{"ticket_id":"T1"} })) .unwrap(); assert!(validate_reviewer_handoff(&writable).is_err()); } fn abs_rule(path: &Path, permission: Permission) -> ScopeRule { ScopeRule { target: path.to_path_buf(), permission, recursive: true, } } const INTERNAL_REVIEWER_PROFILE: &str = r#" slug = "reviewer" scope = "workspace_read" [model] scheme = "anthropic" model_id = "reviewer-model" [model.auth] kind = "none" [engine] instruction = "role.reviewer" language = "Reviewerish" max_tokens = 3333 [feature.ticket] enabled = true thread = true [feature.memory] enabled = true [memory] extract_threshold = 4000 "#; #[tokio::test] async fn parent_controller_notification_target_does_not_keep_channel_open() { let (parent_method_tx, mut parent_method_rx) = mpsc::channel(1); let target = ParentNotificationTarget::Controller(parent_method_tx.downgrade()); drop(parent_method_tx); assert!(parent_method_rx.recv().await.is_none()); target.notify("late completion".to_string(), true); } #[tokio::test] async fn reviewer_profile_spawns_and_notifies_parent_controller() { let runtime = TempDir::new().unwrap(); let workspace_root = runtime.path().join("project"); let available_profiles = write_project_profile_registry( &workspace_root, Some("reviewer"), &[("reviewer", "reviewer.toml", INTERNAL_REVIEWER_PROFILE)], ); let mut manifest = parent_manifest(&workspace_root, None); manifest.delegation_scope = ScopeConfig { allow: vec![abs_rule(&workspace_root, Permission::Write)], deny: Vec::new(), }; let spawner_scope = SharedScope::new(Scope::from_config(&manifest.scope).unwrap()); let registry = SpawnedWorkerRegistry::new_internal("parent".into(), spawner_scope.clone()); let workspace_context = crate::worker::WorkerWorkspaceContext::with_client( Some(WorkspaceId::new("workspace-test").unwrap()), Arc::new(AvailableWorkspaceClient), ); let calls = Arc::new(AtomicUsize::new(0)); let observed_parent_write_revoked = Arc::new(AtomicBool::new(false)); let observed_instruction_override = Arc::new(AtomicBool::new(false)); let fail_requests = Arc::new(AtomicBool::new(false)); let prompt_loader = PromptCatalogSource::builtins_only(); let (parent_method_tx, mut parent_method_rx) = mpsc::channel(8); let source_workdir_session = workdir::delegation_capable_session(Arc::new( workdir::LocalWorkdirSession::materialized_bound( workdir::Workdir::new("test-workdir"), workspace_root.clone(), workspace_root.clone(), spawner_scope.clone(), workdir::WorkdirSessionCapabilities::ALL, ), )); let tool = SubWorkerSpawnTool::new( "parent".into(), workspace_context, ParentNotificationTarget::Controller(parent_method_tx.downgrade()), runtime.path().to_path_buf(), workspace_root.clone(), workspace_root.clone(), Some(source_workdir_session), registry.clone(), manifest.clone(), prompt_loader, available_profiles, ) .with_internal_client(Box::new(ScriptedInternalClient { calls: calls.clone(), parent_scope: spawner_scope.clone(), delegated_path: workspace_root.clone(), observed_parent_write_revoked: observed_parent_write_revoked.clone(), observed_instruction_override: observed_instruction_override.clone(), fail_requests: fail_requests.clone(), })); let input = serde_json::json!({ "name": "reviewer-child", "profile": "project:reviewer", "instruction": "role.reviewer", "task": "review immutable commit", "scope": [{ "target": workspace_root.clone(), "permission": "read", "recursive": true }] }); assert!(spawner_scope.snapshot().is_writable(&workspace_root)); let mut invalid_input = input.clone(); invalid_input["scope"][0]["target"] = serde_json::json!(runtime.path().join("outside-parent-scope")); tool.execute( &serde_json::to_string(&invalid_input).unwrap(), llm_engine::tool::ToolExecutionContext::direct(), ) .await .expect_err("invalid delegation must fail before child preparation"); assert_eq!(calls.load(Ordering::SeqCst), 0); assert!(spawner_scope.snapshot().is_writable(&workspace_root)); let output = tool .execute( &serde_json::to_string(&input).unwrap(), llm_engine::tool::ToolExecutionContext::direct(), ) .await .expect("spawn project reviewer as Internal Worker"); assert!(output.summary.contains("internal worker `reviewer-child`")); assert!(spawner_scope.snapshot().is_writable(&workspace_root)); let record = registry .get_internal("reviewer-child") .expect("Internal reviewer registry record"); assert!(record.installed_tools.iter().any(|name| name == "Read")); for denied in ["Write", "Edit", "Bash"] { assert!( !record.installed_tools.iter().any(|name| name == denied), "read-only child unexpectedly received {denied}: {:?}", record.installed_tools ); } assert!( !record .installed_tools .iter() .any(|name| matches!(name.as_str(), "WorkdirAttachSelf" | "WorkdirDetachSelf")) ); assert_eq!( record.session.wait_until_idle().await, crate::internal_worker::InternalWorkerSessionStatus::Idle ); assert_eq!(calls.load(Ordering::SeqCst), 1); assert!(!observed_parent_write_revoked.load(Ordering::SeqCst)); assert!(observed_instruction_override.load(Ordering::SeqCst)); let completion = tokio::time::timeout(Duration::from_secs(1), parent_method_rx.recv()) .await .expect("SubWorker completion must wake the parent method channel") .expect("parent method channel remains open"); assert!(matches!( completion, Method::Notify { message, auto_run: true, } if message.contains("SubWorker `reviewer-child` turn ended with status Idle") )); assert!(!runtime.path().join("reviewer-child/sock").exists()); let duplicate_error = tool .execute( &serde_json::to_string(&input).unwrap(), llm_engine::tool::ToolExecutionContext::direct(), ) .await .expect_err("duplicate child name must be rejected before a first turn starts"); assert!( format!("{duplicate_error:?}").contains("already registered"), "unexpected duplicate error: {duplicate_error:?}" ); assert_eq!( calls.load(Ordering::SeqCst), 1, "duplicate rejection must not invoke the child provider" ); assert!(matches!( parent_method_rx.try_recv(), Err(tokio::sync::mpsc::error::TryRecvError::Empty) )); let context = llm_engine::tool::ToolExecutionContext::direct(); let list = (crate::spawn::comm_tools::sub_worker_list_tool(registry.clone()))().1; let listed = list.execute("{}", context.clone()).await.unwrap(); assert!( listed .content .unwrap_or_default() .contains("reviewer-child") ); let observation = crate::feature::builtin::worker_observation::SpawnedSubWorkerObservationProvider::new( registry.clone(), ); let observed_child = crate::feature::builtin::worker_observation::WorkerObservationSubjectRef::SubWorker { name: "reviewer-child".to_string(), }; let first_capture = crate::feature::builtin::worker_observation::WorkerObservationProvider::capture_worker_session( &observation, &observed_child, ) .await .unwrap(); assert!(first_capture.items.iter().any(|item| { matches!(item, Item::Message { role: Role::Assistant, content, .. } if content.iter().any(|part| matches!(part, ContentPart::Text { text } if text.contains("reviewed")))) })); let send = (crate::spawn::comm_tools::sub_worker_send_tool(registry.clone()))().1; send.execute( r#"{"name":"reviewer-child","message":"review follow-up"}"#, context.clone(), ) .await .unwrap(); assert_eq!( record.session.wait_until_idle().await, crate::internal_worker::InternalWorkerSessionStatus::Idle ); assert_eq!(calls.load(Ordering::SeqCst), 2); let latest_capture = crate::feature::builtin::worker_observation::WorkerObservationProvider::capture_worker_session( &observation, &observed_child, ) .await .unwrap(); assert!(latest_capture.items.len() > first_capture.items.len()); fail_requests.store(true, Ordering::SeqCst); send.execute( r#"{"name":"reviewer-child","message":"trigger terminal failure"}"#, context.clone(), ) .await .unwrap(); assert_eq!( record.session.wait_until_idle().await, InternalWorkerSessionStatus::Failed ); assert_eq!(calls.load(Ordering::SeqCst), 3); assert!( spawner_scope.snapshot().is_writable(&workspace_root), "Failed terminal child must release its delegated Workdir session" ); assert!( !record.workdir_delegation.is_active(), "failed child must revoke cloned scoped sessions" ); assert!(registry.get_internal("reviewer-child").is_some()); let stop = (crate::spawn::comm_tools::sub_worker_stop_tool(registry.clone()))().1; stop.execute(r#"{"name":"reviewer-child"}"#, context) .await .unwrap(); assert!(registry.get_internal("reviewer-child").is_none()); assert!(spawner_scope.snapshot().is_writable(&workspace_root)); fail_requests.store(false, Ordering::SeqCst); let mut teardown_input = input; teardown_input["name"] = serde_json::json!("reviewer-child-parent-drop"); tool.execute( &serde_json::to_string(&teardown_input).unwrap(), llm_engine::tool::ToolExecutionContext::direct(), ) .await .unwrap(); assert!(spawner_scope.snapshot().is_writable(&workspace_root)); drop(list); drop(send); drop(stop); drop(observation); drop(tool); drop(registry); assert!(spawner_scope.snapshot().is_writable(&workspace_root)); } #[test] fn spawn_worker_input_schema_includes_optional_cwd() { let schema = serde_json::to_value(schemars::schema_for!(SubWorkerSpawnInput)).unwrap(); let properties = schema .get("properties") .and_then(serde_json::Value::as_object) .expect("schema properties"); assert!(properties.contains_key("cwd"), "schema: {schema}"); let required = schema .get("required") .and_then(serde_json::Value::as_array) .expect("schema required list"); assert!( !required.iter().any(|value| value.as_str() == Some("cwd")), "cwd must remain optional: {schema}" ); } #[test] fn orchestration_delegation_allows_root_read_and_worktree_writes_not_root_writes() { let tmp = TempDir::new().unwrap(); let workspace_root = tmp.path().join("original"); let implementation_worktree = workspace_root.join(".worktree/ticket-1"); std::fs::create_dir_all(&implementation_worktree).unwrap(); let delegation = DelegationScope::from_config(&ScopeConfig { allow: vec![ abs_rule(&workspace_root, Permission::Read), abs_rule(&workspace_root.join(".worktree"), Permission::Write), ], deny: Vec::new(), }) .unwrap(); let coder_scope = vec![ abs_rule(&workspace_root, Permission::Read), abs_rule(&implementation_worktree, Permission::Write), ]; assert!( coder_scope .iter() .all(|rule| delegation.allows_rule(rule).unwrap()) ); let reviewer_scope = vec![abs_rule(&workspace_root, Permission::Read)]; assert!( reviewer_scope .iter() .all(|rule| delegation.allows_rule(rule).unwrap()) ); let root_writer_scope = vec![abs_rule(&workspace_root, Permission::Write)]; assert!( root_writer_scope .iter() .any(|rule| !delegation.allows_rule(rule).unwrap()) ); } #[derive(Clone)] struct ScriptedInternalClient { calls: Arc, parent_scope: SharedScope, delegated_path: PathBuf, observed_parent_write_revoked: Arc, observed_instruction_override: Arc, fail_requests: Arc, } #[async_trait] impl LlmClient for ScriptedInternalClient { fn clone_boxed(&self) -> Box { Box::new(self.clone()) } async fn stream( &self, request: Request, ) -> Result> + Send>>, ClientError> { self.calls.fetch_add(1, Ordering::SeqCst); self.observed_parent_write_revoked.store( !self .parent_scope .snapshot() .is_writable(&self.delegated_path), Ordering::SeqCst, ); self.observed_instruction_override.store( request .system_prompt .as_deref() .is_some_and(|prompt| prompt.contains("review")), Ordering::SeqCst, ); if self.fail_requests.load(Ordering::SeqCst) { return Err(ClientError::Config( "scripted Internal Worker failure".into(), )); } Ok(Box::pin(futures::stream::iter(vec![ Ok(LlmEvent::text_block_start(0)), Ok(LlmEvent::text_delta(0, "reviewed")), Ok(LlmEvent::text_block_stop(0, None)), Ok(LlmEvent::Status(StatusEvent { status: ResponseStatus::Completed, })), ]))) } } #[derive(Debug)] struct AvailableWorkspaceClient; impl WorkspaceClient for AvailableWorkspaceClient { fn workspace_id(&self) -> Option<&str> { Some("workspace-test") } fn kind(&self) -> &str { "test" } fn is_available(&self) -> bool { true } fn execute( &self, _request: WorkspaceRequest, ) -> Result { Err(WorkspaceClientError::Unavailable("not invoked".into())) } } fn parent_manifest(root: &Path, deny: Option<&Path>) -> WorkerManifest { WorkerManifestConfig { worker: WorkerMetaConfig { name: Some("parent".into()), }, model: ModelManifest { scheme: Some(SchemeKind::Anthropic), model_id: Some("parent-model".into()), auth: Some(AuthRef::None), ..Default::default() }, engine: EngineManifestConfig { instruction: Some("default".into()), language: Some("Parentish".into()), max_tokens: Some(1234), stop_sequences: Some(vec!["STOP".into()]), ..Default::default() }, scope: ScopeConfig { allow: vec![abs_rule(root, Permission::Write)], deny: deny .map(|path| vec![abs_rule(path, Permission::Read)]) .unwrap_or_default(), }, session: Some(SessionConfigPartial { record_event_trace: Some(true), }), ..Default::default() } .try_into() .unwrap() } fn write_project_profile_registry( project: &Path, default: Option<&str>, profiles: &[(&str, &str, &str)], ) -> AvailableProfiles { let profile_dir = project.join("explicit-project-profiles"); std::fs::create_dir_all(&profile_dir).unwrap(); let mut registry_toml = String::new(); if let Some(default) = default { registry_toml.push_str(&format!("default = \"{default}\"\n")); } registry_toml.push_str("[profile]\n"); for (name, file, body) in profiles { std::fs::write(profile_dir.join(file), body).unwrap(); registry_toml.push_str(&format!("{name} = \"explicit-project-profiles/{file}\"\n")); } let registry_path = project.join("explicit-project-profiles.toml"); std::fs::write(®istry_path, registry_toml).unwrap(); AvailableProfiles { registry: Some( ProfileDiscovery::with_sources(None, Some(registry_path)) .discover() .unwrap(), ), diagnostic: None, } } fn child_config_from_profile( spawner_manifest: &WorkerManifest, available: &AvailableProfiles, cwd: &Path, name: &str, instruction_override: Option<&str>, scope: &[ScopeRule], selector: SpawnProfileSelector, ) -> WorkerManifestConfig { let json = build_spawn_config_json_for_profile( spawner_manifest, available, cwd, name, instruction_override, scope, selector, ) .unwrap(); serde_json::from_str(&json).unwrap() } const CODER_PROFILE: &str = r#" slug = "coder" scope = "workspace_write" [model] scheme = "anthropic" model_id = "coder-model" [engine] instruction = "role.coder" language = "Coderish" max_tokens = 2222 "#; const REVIEWER_PROFILE: &str = r#" slug = "reviewer" scope = "workspace_write" [model] scheme = "anthropic" model_id = "reviewer-model" [engine] instruction = "role.reviewer" language = "Reviewerish" max_tokens = 3333 "#; #[test] fn spawn_config_inherits_inline_spawner_model() { let model = ModelManifest { scheme: Some(SchemeKind::Anthropic), base_url: Some("https://example.test".into()), model_id: Some("claude-sonnet-4".into()), auth: Some(AuthRef::ApiKey { file: Some(PathBuf::from("/etc/keys/anthropic")), }), ..Default::default() }; let config_json = build_spawn_config_json("child", "default", &[], &model, false).unwrap(); let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap(); assert_eq!(parsed.model.scheme, Some(SchemeKind::Anthropic)); assert_eq!(parsed.model.model_id.as_deref(), Some("claude-sonnet-4")); assert_eq!( parsed.model.base_url.as_deref(), Some("https://example.test") ); let file = match parsed.model.auth { Some(AuthRef::ApiKey { file, .. }) => file, _ => panic!("expected ApiKey"), }; assert_eq!(file.as_deref(), Some(Path::new("/etc/keys/anthropic"))); } #[test] fn spawn_config_inherits_ref_spawner_model() { let model = ModelManifest { ref_: Some("anthropic/claude-sonnet-4-6".into()), ..Default::default() }; let config_json = build_spawn_config_json("child", "default", &[], &model, false).unwrap(); let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap(); assert_eq!( parsed.model.ref_.as_deref(), Some("anthropic/claude-sonnet-4-6") ); } #[test] fn spawn_config_preserves_record_event_trace_when_enabled() { let model = ModelManifest { ref_: Some("anthropic/claude-sonnet-4-6".into()), ..Default::default() }; let scope = vec![ScopeRule { target: PathBuf::from("/tmp/child"), permission: Permission::Read, recursive: true, }]; let config_json = build_spawn_config_json("child", "default", &scope, &model, true).unwrap(); let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap(); assert_eq!( parsed.session.as_ref().and_then(|s| s.record_event_trace), Some(true) ); let manifest: WorkerManifest = WorkerManifestConfig::builtin_defaults() .merge(parsed) .try_into() .unwrap(); assert!(manifest.session.record_event_trace); } #[test] fn spawn_config_omits_record_event_trace_when_disabled() { let model = ModelManifest { ref_: Some("anthropic/claude-sonnet-4-6".into()), ..Default::default() }; let config_json = build_spawn_config_json("child", "default", &[], &model, false).unwrap(); let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap(); assert!(parsed.session.is_none()); } #[test] fn omitted_profile_resolves_effective_registry_default() { let tmp = TempDir::new().unwrap(); let project = tmp.path().join("project"); let delegated = tmp.path().join("delegated"); std::fs::create_dir_all(&project).unwrap(); std::fs::create_dir_all(&delegated).unwrap(); let available = write_project_profile_registry( &project, Some("reviewer"), &[ ("coder", "coder.toml", CODER_PROFILE), ("reviewer", "reviewer.toml", REVIEWER_PROFILE), ], ); let parent = parent_manifest(&project, None); let scope = vec![abs_rule(&delegated, Permission::Read)]; let config = child_config_from_profile( &parent, &available, &project, "child-default", None, &scope, SpawnProfileSelector::Default, ); assert_eq!(config.worker.name.as_deref(), Some("child-default")); assert_eq!(config.model.model_id.as_deref(), Some("reviewer-model")); assert_eq!(config.engine.instruction.as_deref(), Some("role.reviewer")); assert_eq!(config.engine.language.as_deref(), Some("Reviewerish")); assert_eq!(config.scope.allow, scope); assert!(config.scope.deny.is_empty()); } #[test] fn source_qualified_profile_role_config_reaches_spawn_config() { let tmp = TempDir::new().unwrap(); let project = tmp.path().join("project"); let delegated = tmp.path().join("delegated"); std::fs::create_dir_all(&project).unwrap(); std::fs::create_dir_all(&delegated).unwrap(); let available = write_project_profile_registry( &project, Some("coder"), &[ ("coder", "coder.toml", CODER_PROFILE), ("reviewer", "reviewer.toml", REVIEWER_PROFILE), ], ); let parent = parent_manifest(&project, None); let scope = vec![abs_rule(&delegated, Permission::Write)]; let config = child_config_from_profile( &parent, &available, &project, "review-child", None, &scope, SpawnProfileSelector::Registry(ProfileSelector::source_named( ProfileRegistrySource::Project, "reviewer", )), ); assert_eq!(config.worker.name.as_deref(), Some("review-child")); assert_eq!(config.model.model_id.as_deref(), Some("reviewer-model")); assert_eq!(config.engine.instruction.as_deref(), Some("role.reviewer")); assert_eq!(config.engine.language.as_deref(), Some("Reviewerish")); assert_eq!(config.engine.max_tokens, Some(3333)); assert_eq!(config.scope.allow, scope); assert!(config.scope.deny.is_empty()); } #[test] fn inherit_copies_reusable_parent_fields_and_replaces_runtime_authority() { let tmp = TempDir::new().unwrap(); let parent_root = tmp.path().join("parent-root"); let parent_deny = parent_root.join("secret"); let delegated = tmp.path().join("delegated"); std::fs::create_dir_all(&parent_deny).unwrap(); std::fs::create_dir_all(&delegated).unwrap(); let parent = parent_manifest(&parent_root, Some(&parent_deny)); let scope = vec![abs_rule(&delegated, Permission::Read)]; let available = AvailableProfiles { registry: None, diagnostic: None, }; let config = child_config_from_profile( &parent, &available, tmp.path(), "inherited-child", None, &scope, SpawnProfileSelector::Inherit, ); assert_eq!(config.worker.name.as_deref(), Some("inherited-child")); assert_eq!(config.model.model_id.as_deref(), Some("parent-model")); assert_eq!(config.engine.instruction.as_deref(), Some("default")); assert_eq!(config.engine.language.as_deref(), Some("Parentish")); assert_eq!(config.engine.max_tokens, Some(1234)); assert_eq!( config.engine.stop_sequences.as_deref(), Some(&["STOP".to_string()][..]) ); assert_eq!( config.session.as_ref().and_then(|s| s.record_event_trace), Some(true) ); assert_eq!(config.scope.allow, scope); assert!(config.scope.deny.is_empty()); } #[test] fn instruction_override_changes_only_worker_instruction() { let tmp = TempDir::new().unwrap(); let project = tmp.path().join("project"); let delegated = tmp.path().join("delegated"); std::fs::create_dir_all(&project).unwrap(); std::fs::create_dir_all(&delegated).unwrap(); let available = write_project_profile_registry( &project, Some("reviewer"), &[("reviewer", "reviewer.toml", REVIEWER_PROFILE)], ); let parent = parent_manifest(&project, None); let scope = vec![abs_rule(&delegated, Permission::Write)]; let config = child_config_from_profile( &parent, &available, &project, "override-child", Some("role.reviewer"), &scope, SpawnProfileSelector::Default, ); assert_eq!(config.engine.instruction.as_deref(), Some("role.reviewer")); assert_eq!(config.model.model_id.as_deref(), Some("reviewer-model")); assert_eq!(config.engine.language.as_deref(), Some("Reviewerish")); assert_eq!(config.engine.max_tokens, Some(3333)); assert_eq!(config.scope.allow, scope); } #[test] fn profile_and_inherited_scope_are_replaced_by_delegated_scope() { let tmp = TempDir::new().unwrap(); let project = tmp.path().join("project"); let delegated = tmp.path().join("delegated"); let parent_root = tmp.path().join("parent-root"); std::fs::create_dir_all(&project).unwrap(); std::fs::create_dir_all(&delegated).unwrap(); std::fs::create_dir_all(&parent_root).unwrap(); let available = write_project_profile_registry( &project, Some("reviewer"), &[("reviewer", "reviewer.toml", REVIEWER_PROFILE)], ); let parent = parent_manifest(&parent_root, Some(&parent_root.join("deny"))); let scope = vec![abs_rule(&delegated, Permission::Read)]; let profile_config = child_config_from_profile( &parent, &available, &project, "profile-child", None, &scope, SpawnProfileSelector::Default, ); let inherit_config = child_config_from_profile( &parent, &available, &project, "inherit-child", None, &scope, SpawnProfileSelector::Inherit, ); for config in [profile_config, inherit_config] { assert_eq!(config.scope.allow, scope); assert!(config.scope.deny.is_empty()); assert!(!config.scope.allow.iter().any(|rule| rule.target == project)); assert!( !config .scope .allow .iter() .any(|rule| rule.target == parent_root) ); } } #[test] fn invalid_ambiguous_and_no_default_diagnostics_include_available_selectors() { let tmp = TempDir::new().unwrap(); let project = tmp.path().join("project"); std::fs::create_dir_all(&project).unwrap(); let available = write_project_profile_registry( &project, None, &[("coder", "coder.toml", CODER_PROFILE)], ); let parent = parent_manifest(&project, None); let scope = vec![abs_rule(&project, Permission::Read)]; let invalid = parse_spawn_profile_selector(Some("./reviewer.toml")) .map_err(|msg| format!("{msg}{}", available.error_suffix())) .unwrap_err(); assert!(invalid.contains("Use `default`, `inherit`")); assert!(invalid.contains("`project:coder`")); let default_error = build_spawn_config_json_for_profile( &parent, &available, &project, "child", None, &scope, SpawnProfileSelector::Default, ) .unwrap_err(); assert!(default_error.contains("no default profile is configured")); let user_config = tmp.path().join("user-profiles.toml"); std::fs::write(&user_config, "[profile]\ncoder = \"user-coder.toml\"\n").unwrap(); let project_config = project.join("explicit-project-profiles.toml"); let ambiguous = AvailableProfiles { registry: Some( ProfileDiscovery::with_sources(Some(user_config), Some(project_config)) .discover() .unwrap(), ), diagnostic: None, }; let ambiguous_error = build_spawn_config_json_for_profile( &parent, &ambiguous, &project, "child", None, &scope, SpawnProfileSelector::Registry(ProfileSelector::named("coder")), ) .unwrap_err(); assert!(ambiguous_error.contains("ambiguous"), "{ambiguous_error}"); assert!(ambiguous_error.contains("user:coder")); assert!(ambiguous_error.contains("project:coder")); assert!(ambiguous_error.contains("Use `default`, `inherit`")); } #[test] fn spawn_profile_selector_rejects_path_like_values() { for raw in [ "./reviewer.toml", "path:./reviewer.toml", "/tmp/reviewer.toml", "legacy.nix", ] { let err = parse_spawn_profile_selector(Some(raw)).unwrap_err(); assert!(err.contains("registry selectors only"), "{raw}: {err}"); } } #[test] fn spawn_profile_selector_accepts_default_inherit_and_registry() { assert_eq!( parse_spawn_profile_selector(None).unwrap(), SpawnProfileSelector::Default ); assert_eq!( parse_spawn_profile_selector(Some("inherit")).unwrap(), SpawnProfileSelector::Inherit ); assert_eq!( parse_spawn_profile_selector(Some("project:reviewer")).unwrap(), SpawnProfileSelector::Registry(ProfileSelector::source_named( ProfileRegistrySource::Project, "reviewer" )) ); assert_eq!( parse_spawn_profile_selector(Some("coder")).unwrap(), SpawnProfileSelector::Registry(ProfileSelector::named("coder")) ); } }