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 {
/// 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
/// Profile selector for child role configuration. Omit or use `inherit`
/// to derive reusable config from the spawner, use `default` for the
/// effective registry default profile, 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)]
@@ -204,7 +204,7 @@ enum SpawnProfileSelector {
fn parse_spawn_profile_selector(raw: Option<&str>) -> Result<SpawnProfileSelector, String> {
let Some(raw) = raw.map(str::trim).filter(|s| !s.is_empty()) else {
return Ok(SpawnProfileSelector::Default);
return Ok(SpawnProfileSelector::Inherit);
};
if raw == "default" {
return Ok(SpawnProfileSelector::Default);
@@ -2298,7 +2298,7 @@ max_tokens = 3333
}
#[test]
fn omitted_profile_resolves_effective_registry_default() {
fn explicit_default_profile_resolves_effective_registry_default() {
let tmp = TempDir::new().unwrap();
let project = tmp.path().join("project");
let delegated = tmp.path().join("delegated");
@@ -2322,7 +2322,7 @@ max_tokens = 3333
"child-default",
None,
&scope,
SpawnProfileSelector::Default,
parse_spawn_profile_selector(Some("default")).unwrap(),
);
assert_eq!(config.worker.name.as_deref(), Some("child-default"));
@@ -2373,6 +2373,44 @@ max_tokens = 3333
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]
fn inherit_copies_reusable_parent_fields_and_replaces_runtime_authority() {
let tmp = TempDir::new().unwrap();
@@ -2581,14 +2619,16 @@ max_tokens = 3333
#[test]
fn spawn_profile_selector_accepts_default_inherit_and_registry() {
for raw in [None, Some(""), Some(" "), Some("inherit")] {
assert_eq!(
parse_spawn_profile_selector(None).unwrap(),
SpawnProfileSelector::Default
);
assert_eq!(
parse_spawn_profile_selector(Some("inherit")).unwrap(),
parse_spawn_profile_selector(raw).unwrap(),
SpawnProfileSelector::Inherit
);
}
assert_eq!(
parse_spawn_profile_selector(Some("default")).unwrap(),
SpawnProfileSelector::Default
);
assert_eq!(
parse_spawn_profile_selector(Some("project:reviewer")).unwrap(),
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.
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.
Available registry profiles:
{{ available_profiles }}{% if profile_diagnostic %}