fix(subagents): isolate resident worker prompts
- commit
- 6f4c43ceb666bd4ed3e8a42d16a0316a4be08104
- author
- brice · · 2026-08-24 10:40 UTC
- parent
- f9c8b6d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +29 -9
M packages/agent/args.go +6 -3
| @@ -28,9 +28,12 @@ const ( | |||
| 28 | 28 | type Args struct { | |
| 29 | 29 | Mode Mode | |
| 30 | 30 | Orchestrate bool | |
| 31 | - | Provider string | |
| 32 | - | Model string | |
| 33 | - | APIKey string | |
| 31 | + | // ResidentChild marks internal child resolution so primary-only delegation | |
| 32 | + | // policies and profile metadata are not added to worker prompts. | |
| 33 | + | ResidentChild bool | |
| 34 | + | Provider string | |
| 35 | + | Model string | |
| 36 | + | APIKey string | |
| 34 | 37 | ||
| 35 | 38 | BaseURL string // override provider base URL (for tests/self-hosted) | |
| 36 | 39 | SystemPrompt string | |
| @@ -28,9 +28,12 @@ const ( | |||||
| 28 | type Args struct { | 28 | type Args struct { | ||
| 29 | Mode Mode | 29 | Mode Mode | ||
| 30 | Orchestrate bool | 30 | Orchestrate bool | ||
| 31 | - | Provider string | 31 | + | // ResidentChild marks internal child resolution so primary-only delegation |
| 32 | - | Model string | 32 | + | // policies and profile metadata are not added to worker prompts. |
| 33 | - | APIKey string | 33 | + | ResidentChild bool |
| 34 | + | Provider string | |||
| 35 | + | Model string | |||
| 36 | + | APIKey string | |||
| 34 | 37 | ||||
| 35 | BaseURL string // override provider base URL (for tests/self-hosted) | 38 | BaseURL string // override provider base URL (for tests/self-hosted) | ||
| 36 | SystemPrompt string | 39 | SystemPrompt string | ||
M packages/agent/build.go +2 -2
| @@ -809,8 +809,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 809 | 809 | append_ = append(append_, skillAddendum) | |
| 810 | 810 | } | |
| 811 | 811 | interactiveMode := args.Mode == "" || args.Mode == ModeInteractive | |
| 812 | - | primaryInteractive := interactiveMode | |
| 813 | - | primaryOrchestrator := args.Orchestrate | |
| 812 | + | primaryInteractive := interactiveMode && !args.ResidentChild | |
| 813 | + | primaryOrchestrator := args.Orchestrate && !args.ResidentChild | |
| 814 | 814 | if (primaryInteractive || primaryOrchestrator) && autoSubagentsToolAllowed(args) { | |
| 815 | 815 | homeDir, _ := os.UserHomeDir() | |
| 816 | 816 | profiles, _ := subagents.Discover(args.CWD, homeDir) | |
| @@ -809,8 +809,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 809 | append_ = append(append_, skillAddendum) | 809 | append_ = append(append_, skillAddendum) | ||
| 810 | } | 810 | } | ||
| 811 | interactiveMode := args.Mode == "" || args.Mode == ModeInteractive | 811 | interactiveMode := args.Mode == "" || args.Mode == ModeInteractive | ||
| 812 | - | primaryInteractive := interactiveMode | 812 | + | primaryInteractive := interactiveMode && !args.ResidentChild |
| 813 | - | primaryOrchestrator := args.Orchestrate | 813 | + | primaryOrchestrator := args.Orchestrate && !args.ResidentChild |
| 814 | if (primaryInteractive || primaryOrchestrator) && autoSubagentsToolAllowed(args) { | 814 | if (primaryInteractive || primaryOrchestrator) && autoSubagentsToolAllowed(args) { | ||
| 815 | homeDir, _ := os.UserHomeDir() | 815 | homeDir, _ := os.UserHomeDir() | ||
| 816 | profiles, _ := subagents.Discover(args.CWD, homeDir) | 816 | profiles, _ := subagents.Discover(args.CWD, homeDir) | ||
M packages/agent/build_orchestrator_test.go +18 -2
| @@ -19,6 +19,13 @@ func TestResolveSeparatesInteractiveProactiveAndHeadlessStrictPolicies(t *testin | |||
| 19 | 19 | if err := os.MkdirAll(project, 0o755); err != nil { | |
| 20 | 20 | t.Fatal(err) | |
| 21 | 21 | } | |
| 22 | + | profilesDir := filepath.Join(root, "home", ".agents", "agents") | |
| 23 | + | if err := os.MkdirAll(profilesDir, 0o755); err != nil { | |
| 24 | + | t.Fatal(err) | |
| 25 | + | } | |
| 26 | + | if err := os.WriteFile(filepath.Join(profilesDir, "reviewer.md"), []byte("---\nname: reviewer\ndescription: Review delegated work\n---\nReview the requested scope."), 0o600); err != nil { | |
| 27 | + | t.Fatal(err) | |
| 28 | + | } | |
| 22 | 29 | enabled := true | |
| 23 | 30 | if err := SaveConfig(Config{AutoSubagentsEnabled: &enabled}); err != nil { | |
| 24 | 31 | t.Fatal(err) | |
| @@ -35,8 +42,17 @@ func TestResolveSeparatesInteractiveProactiveAndHeadlessStrictPolicies(t *testin | |||
| 35 | 42 | if err != nil { | |
| 36 | 43 | t.Fatalf("resolve interactive: %v", err) | |
| 37 | 44 | } | |
| 38 | - | if !strings.Contains(interactive.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(interactive.SystemPrompt, StrictOrchestratorSystemAddendum) { | |
| 39 | - | t.Fatalf("interactive prompt did not select proactive policy:\n%s", interactive.SystemPrompt) | |
| 45 | + | if !strings.Contains(interactive.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(interactive.SystemPrompt, StrictOrchestratorSystemAddendum) || !strings.Contains(interactive.SystemPrompt, "[subagents_list]") { | |
| 46 | + | t.Fatalf("interactive prompt did not select proactive primary policy and profiles:\n%s", interactive.SystemPrompt) | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | childArgs := residentChildArgs(base, "ollama", subagents.ResidentChildSpec{Provider: "ollama", Model: "any-local-model"}) | |
| 50 | + | child, err := Resolve(childArgs, false) | |
| 51 | + | if err != nil { | |
| 52 | + | t.Fatalf("resolve resident child: %v", err) | |
| 53 | + | } | |
| 54 | + | if strings.Contains(child.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(child.SystemPrompt, StrictOrchestratorSystemAddendum) || strings.Contains(child.SystemPrompt, "[subagents_list]") { | |
| 55 | + | t.Fatalf("resident child inherited primary delegation policy or profiles:\n%s", child.SystemPrompt) | |
| 40 | 56 | } | |
| 41 | 57 | ||
| 42 | 58 | headlessArgs := base | |
| @@ -19,6 +19,13 @@ func TestResolveSeparatesInteractiveProactiveAndHeadlessStrictPolicies(t *testin | |||||
| 19 | if err := os.MkdirAll(project, 0o755); err != nil { | 19 | if err := os.MkdirAll(project, 0o755); err != nil { | ||
| 20 | t.Fatal(err) | 20 | t.Fatal(err) | ||
| 21 | } | 21 | } | ||
| 22 | + | profilesDir := filepath.Join(root, "home", ".agents", "agents") | |||
| 23 | + | if err := os.MkdirAll(profilesDir, 0o755); err != nil { | |||
| 24 | + | t.Fatal(err) | |||
| 25 | + | } | |||
| 26 | + | if err := os.WriteFile(filepath.Join(profilesDir, "reviewer.md"), []byte("---\nname: reviewer\ndescription: Review delegated work\n---\nReview the requested scope."), 0o600); err != nil { | |||
| 27 | + | t.Fatal(err) | |||
| 28 | + | } | |||
| 22 | enabled := true | 29 | enabled := true | ||
| 23 | if err := SaveConfig(Config{AutoSubagentsEnabled: &enabled}); err != nil { | 30 | if err := SaveConfig(Config{AutoSubagentsEnabled: &enabled}); err != nil { | ||
| 24 | t.Fatal(err) | 31 | t.Fatal(err) | ||
| @@ -35,8 +42,17 @@ func TestResolveSeparatesInteractiveProactiveAndHeadlessStrictPolicies(t *testin | |||||
| 35 | if err != nil { | 42 | if err != nil { | ||
| 36 | t.Fatalf("resolve interactive: %v", err) | 43 | t.Fatalf("resolve interactive: %v", err) | ||
| 37 | } | 44 | } | ||
| 38 | - | if !strings.Contains(interactive.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(interactive.SystemPrompt, StrictOrchestratorSystemAddendum) { | 45 | + | if !strings.Contains(interactive.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(interactive.SystemPrompt, StrictOrchestratorSystemAddendum) || !strings.Contains(interactive.SystemPrompt, "[subagents_list]") { |
| 39 | - | t.Fatalf("interactive prompt did not select proactive policy:\n%s", interactive.SystemPrompt) | 46 | + | t.Fatalf("interactive prompt did not select proactive primary policy and profiles:\n%s", interactive.SystemPrompt) |
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | childArgs := residentChildArgs(base, "ollama", subagents.ResidentChildSpec{Provider: "ollama", Model: "any-local-model"}) | |||
| 50 | + | child, err := Resolve(childArgs, false) | |||
| 51 | + | if err != nil { | |||
| 52 | + | t.Fatalf("resolve resident child: %v", err) | |||
| 53 | + | } | |||
| 54 | + | if strings.Contains(child.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(child.SystemPrompt, StrictOrchestratorSystemAddendum) || strings.Contains(child.SystemPrompt, "[subagents_list]") { | |||
| 55 | + | t.Fatalf("resident child inherited primary delegation policy or profiles:\n%s", child.SystemPrompt) | |||
| 40 | } | 56 | } | ||
| 41 | 57 | ||||
| 42 | headlessArgs := base | 58 | headlessArgs := base | ||
M packages/agent/subagent_factory.go +2 -1
| @@ -100,9 +100,10 @@ func newResidentChildRunner(args Args, spec subagents.ResidentChildSpec, journal | |||
| 100 | 100 | // decision accepted at spawn time across explicit resume and restart. | |
| 101 | 101 | func residentChildArgs(args Args, parentProvider string, spec subagents.ResidentChildSpec) Args { | |
| 102 | 102 | next := args | |
| 103 | - | // --orchestrate is a role for the headless parent, not its workers. A child | |
| 103 | + | // Primary delegation policy belongs to the parent, not its workers. A child | |
| 104 | 104 | // receives its accepted profile instructions and exact child tool list. | |
| 105 | 105 | next.Orchestrate = false | |
| 106 | + | next.ResidentChild = true | |
| 106 | 107 | // An explicit CLI key belongs to the provider that resolved the parent. | |
| 107 | 108 | // A child can select another provider, but it must resolve that provider's | |
| 108 | 109 | // own credential rather than forwarding or persisting the parent's key. | |
| @@ -100,9 +100,10 @@ func newResidentChildRunner(args Args, spec subagents.ResidentChildSpec, journal | |||||
| 100 | // decision accepted at spawn time across explicit resume and restart. | 100 | // decision accepted at spawn time across explicit resume and restart. | ||
| 101 | func residentChildArgs(args Args, parentProvider string, spec subagents.ResidentChildSpec) Args { | 101 | func residentChildArgs(args Args, parentProvider string, spec subagents.ResidentChildSpec) Args { | ||
| 102 | next := args | 102 | next := args | ||
| 103 | - | // --orchestrate is a role for the headless parent, not its workers. A child | 103 | + | // Primary delegation policy belongs to the parent, not its workers. A child |
| 104 | // receives its accepted profile instructions and exact child tool list. | 104 | // receives its accepted profile instructions and exact child tool list. | ||
| 105 | next.Orchestrate = false | 105 | next.Orchestrate = false | ||
| 106 | + | next.ResidentChild = true | |||
| 106 | // An explicit CLI key belongs to the provider that resolved the parent. | 107 | // An explicit CLI key belongs to the provider that resolved the parent. | ||
| 107 | // A child can select another provider, but it must resolve that provider's | 108 | // A child can select another provider, but it must resolve that provider's | ||
| 108 | // own credential rather than forwarding or persisting the parent's key. | 109 | // own credential rather than forwarding or persisting the parent's key. | ||
M packages/agent/subagent_factory_test.go +1 -1
| @@ -36,7 +36,7 @@ func TestResidentChildArgsPreserveDurableProfileInheritance(t *testing.T) { | |||
| 36 | 36 | Provider: "openai", BaseURL: "https://current.example/v1", InsecureTLS: true, Model: "gpt-5", Workspace: "/repo/child", | |
| 37 | 37 | InheritSkills: &no, InheritProjectContext: &no, | |
| 38 | 38 | }) | |
| 39 | - | if next.Provider != "openai" || next.BaseURL != "https://current.example/v1" || !next.InsecureTLS || next.Model != "gpt-5" || next.CWD != "/repo/child" || !next.NoSkill || !next.NoContextFiles || next.Orchestrate { | |
| 39 | + | if next.Provider != "openai" || next.BaseURL != "https://current.example/v1" || !next.InsecureTLS || next.Model != "gpt-5" || next.CWD != "/repo/child" || !next.NoSkill || !next.NoContextFiles || next.Orchestrate || !next.ResidentChild { | |
| 40 | 40 | t.Fatalf("resident child args = %#v", next) | |
| 41 | 41 | } | |
| 42 | 42 | } | |
| @@ -36,7 +36,7 @@ func TestResidentChildArgsPreserveDurableProfileInheritance(t *testing.T) { | |||||
| 36 | Provider: "openai", BaseURL: "https://current.example/v1", InsecureTLS: true, Model: "gpt-5", Workspace: "/repo/child", | 36 | Provider: "openai", BaseURL: "https://current.example/v1", InsecureTLS: true, Model: "gpt-5", Workspace: "/repo/child", | ||
| 37 | InheritSkills: &no, InheritProjectContext: &no, | 37 | InheritSkills: &no, InheritProjectContext: &no, | ||
| 38 | }) | 38 | }) | ||
| 39 | - | if next.Provider != "openai" || next.BaseURL != "https://current.example/v1" || !next.InsecureTLS || next.Model != "gpt-5" || next.CWD != "/repo/child" || !next.NoSkill || !next.NoContextFiles || next.Orchestrate { | 39 | + | if next.Provider != "openai" || next.BaseURL != "https://current.example/v1" || !next.InsecureTLS || next.Model != "gpt-5" || next.CWD != "/repo/child" || !next.NoSkill || !next.NoContextFiles || next.Orchestrate || !next.ResidentChild { |
| 40 | t.Fatalf("resident child args = %#v", next) | 40 | t.Fatalf("resident child args = %#v", next) | ||
| 41 | } | 41 | } | ||
| 42 | } | 42 | } | ||