fix: inherit parent profile for default subworkers

This commit is contained in:
2026-10-16 01:57:58 +09:00
parent 8a3d9d67d2
commit 9f1cbbd385
2 changed files with 54 additions and 13 deletions
+51 -11
View File
@@ -45,9 +45,9 @@ use protocol::Method;
struct SubWorkerSpawnInput { struct SubWorkerSpawnInput {
/// Identifier for the spawned Internal SubWorker. Must be unique among this Worker's direct children. /// Identifier for the spawned Internal SubWorker. Must be unique among this Worker's direct children.
name: String, name: String,
/// Profile selector for child role configuration. Omit or use `default` /// Profile selector for child role configuration. Omit or use `inherit`
/// for the effective child default profile, use `inherit` to derive /// to derive reusable config from the spawner, use `default` for the
/// reusable config from the spawner, or use a registry selector such as /// effective registry default profile, or use a registry selector such as
/// `project:coder`, `project:reviewer`, `builtin:companion`, or an /// `project:coder`, `project:reviewer`, `builtin:companion`, or an
/// unambiguous profile slug. Raw/path selectors are rejected. /// unambiguous profile slug. Raw/path selectors are rejected.
#[serde(default)] #[serde(default)]
@@ -204,7 +204,7 @@ enum SpawnProfileSelector {
fn parse_spawn_profile_selector(raw: Option<&str>) -> Result<SpawnProfileSelector, String> { fn parse_spawn_profile_selector(raw: Option<&str>) -> Result<SpawnProfileSelector, String> {
let Some(raw) = raw.map(str::trim).filter(|s| !s.is_empty()) else { let Some(raw) = raw.map(str::trim).filter(|s| !s.is_empty()) else {
return Ok(SpawnProfileSelector::Default); return Ok(SpawnProfileSelector::Inherit);
}; };
if raw == "default" { if raw == "default" {
return Ok(SpawnProfileSelector::Default); return Ok(SpawnProfileSelector::Default);
@@ -2298,7 +2298,7 @@ max_tokens = 3333
} }
#[test] #[test]
fn omitted_profile_resolves_effective_registry_default() { fn explicit_default_profile_resolves_effective_registry_default() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let project = tmp.path().join("project"); let project = tmp.path().join("project");
let delegated = tmp.path().join("delegated"); let delegated = tmp.path().join("delegated");
@@ -2322,7 +2322,7 @@ max_tokens = 3333
"child-default", "child-default",
None, None,
&scope, &scope,
SpawnProfileSelector::Default, parse_spawn_profile_selector(Some("default")).unwrap(),
); );
assert_eq!(config.worker.name.as_deref(), Some("child-default")); assert_eq!(config.worker.name.as_deref(), Some("child-default"));
@@ -2373,6 +2373,44 @@ max_tokens = 3333
assert!(config.scope.deny.is_empty()); assert!(config.scope.deny.is_empty());
} }
#[test]
fn omitted_profile_inherits_parent_ticket_features_instead_of_registry_default() {
let tmp = TempDir::new().unwrap();
let mut parent = parent_manifest(tmp.path(), None);
let scope = vec![abs_rule(tmp.path(), Permission::Read)];
let available = AvailableProfiles {
registry: Some(
ProfileDiscovery::with_sources(None, None)
.discover()
.unwrap(),
),
diagnostic: None,
};
let input: SubWorkerSpawnInput = serde_json::from_value(serde_json::json!({
"name": "child", "task": "inspect", "scope": []
}))
.unwrap();
for enabled in [false, true] {
parent.feature.ticket.enabled = enabled;
parent.feature.ticket.thread = enabled;
let config = child_config_from_profile(
&parent,
&available,
tmp.path(),
"child",
None,
&scope,
parse_spawn_profile_selector(input.profile.as_deref()).unwrap(),
);
assert_eq!(config.model.model_id.as_deref(), Some("parent-model"));
assert_eq!(config.engine.language.as_deref(), Some("Parentish"));
let ticket = config.feature.ticket.unwrap();
assert_eq!(ticket.enabled, Some(enabled));
assert_eq!(ticket.thread, Some(enabled));
assert_eq!(config.scope.allow, scope);
}
}
#[test] #[test]
fn inherit_copies_reusable_parent_fields_and_replaces_runtime_authority() { fn inherit_copies_reusable_parent_fields_and_replaces_runtime_authority() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
@@ -2581,14 +2619,16 @@ max_tokens = 3333
#[test] #[test]
fn spawn_profile_selector_accepts_default_inherit_and_registry() { fn spawn_profile_selector_accepts_default_inherit_and_registry() {
for raw in [None, Some(""), Some(" "), Some("inherit")] {
assert_eq!(
parse_spawn_profile_selector(raw).unwrap(),
SpawnProfileSelector::Inherit
);
}
assert_eq!( assert_eq!(
parse_spawn_profile_selector(None).unwrap(), parse_spawn_profile_selector(Some("default")).unwrap(),
SpawnProfileSelector::Default SpawnProfileSelector::Default
); );
assert_eq!(
parse_spawn_profile_selector(Some("inherit")).unwrap(),
SpawnProfileSelector::Inherit
);
assert_eq!( assert_eq!(
parse_spawn_profile_selector(Some("project:reviewer")).unwrap(), parse_spawn_profile_selector(Some("project:reviewer")).unwrap(),
SpawnProfileSelector::Registry(ProfileSelector::source_named( SpawnProfileSelector::Registry(ProfileSelector::source_named(
@@ -2,9 +2,10 @@ Spawn a parent-owned Internal SubWorker session to split context for a delegated
Optional `cwd`: when provided, the spawned SubWorker's tool default working directory only. It must be a Workdir-relative existing directory covered by the child's readable scope, and it does not change workspace/Profile/memory/Ticket roots or grant authority. `name` must be unique among this Worker's direct children. Optional `cwd`: when provided, the spawned SubWorker's tool default working directory only. It must be a Workdir-relative existing directory covered by the child's readable scope, and it does not change workspace/Profile/memory/Ticket roots or grant authority. `name` must be unique among this Worker's direct children.
Profile selection: `profile` may be omitted or set to `default` to use the effective child default profile, set to `inherit` to derive reusable child configuration from this Worker, or set to one of the registry selectors below. Raw/path profile selectors are not accepted by SubWorkerSpawn. `scope` is the child's only filesystem capability and replaces profile scope. `command` is a separate explicit grant, defaults to false, and is accepted only with a writable scope; writable scope alone does not grant command execution. Profile selection: omit `profile` or set it to `inherit` to derive reusable child configuration from this Worker. Set it to `default` to use the effective registry default profile, or to one of the registry selectors below. Raw/path profile selectors are not accepted by SubWorkerSpawn. `scope` is the child's only filesystem capability and replaces profile scope. `command` is a separate explicit grant, defaults to false, and is accepted only with a writable scope; writable scope alone does not grant command execution.
Default profile: {{ default_profile }} Default when omitted: inherit.
Registry default (`profile = "default"`): {{ default_profile }}
Special selector: inherit — derive reusable model/worker/tool policy from the spawner while replacing worker.name and scope. Special selector: inherit — derive reusable model/worker/tool policy from the spawner while replacing worker.name and scope.
Available registry profiles: Available registry profiles:
{{ available_profiles }}{% if profile_diagnostic %} {{ available_profiles }}{% if profile_diagnostic %}