feat: add Explore mode as subagent-only modality (#24448)

> This PR was authored by Mux on behalf of Mike.

Introduce Explore mode, a read-only subagent modality for delegated
discovery and code investigation.

## What

Adds a `spawn_explore_agent` tool that creates child chats restricted to
read-only operations. An admin can optionally configure a
deployment-wide
model override so Explore subagents use a model optimized for large
context
or reasoning without changing the root chat's model.

### Backend

- New `ChatModeExplore` enum value (migration 000471).
- `spawn_explore_agent` tool definition with read-only allowlist:
`read_file`, `execute`, `process_output`, `read_skill`,
`read_skill_file`.
  Write tools, file editors, and nested subagent spawning are blocked.
- Deployment config storage for the Explore model override
  (`agents_chat_explore_model_override` in `site_configs`).
- Model resolution hierarchy: configured override, then current turn
model,
then global default. Silent fallback with warning log when the override
  becomes unavailable.
- RBAC: `AsChatd` for daemon reads, `ActionRead` and `ActionUpdate` on
  `ResourceDeploymentConfig` for admin API calls.
- Plan mode root chats can use `spawn_explore_agent` for read-only
research,
  matching the planning prompt guidance.
- The Explore override config API now reports malformed saved overrides
as
  "treated as unset" so admins can clear them explicitly.

### Frontend

- `ExploreModelOverrideSettings` component in admin agent behavior
settings.
  Uses `ModelSelector`, handles unavailable model warnings, and supports
  explicit Save and Clear actions.
- Malformed saved overrides show a warning and require an explicit Save
to
  clear, instead of Clear auto-submitting behind the scenes.

### Tests

- Integration: `TestExploreSubagentIsReadOnly` (full spawn flow, tool
  verification, prompt overlay, DB state).
- Unit: tool allowlist tests for explore, plan, and default modes.
- Internal: model override resolution with valid, invalid UUID,
disabled, and
  unconfigured override scenarios.
- RBAC: `dbauthz_test.go` for `GetChatExploreModelOverride` and
  `UpsertChatExploreModelOverride`.
- API: admin set and clear, malformed stored override reporting,
disabled
  model rejection, non-admin denial.
This commit is contained in:
Michael Suchacz
2026-04-17 13:40:17 +02:00
committed by GitHub
parent 890c610e08
commit 73b5058923
42 changed files with 1852 additions and 157 deletions
+44
View File
@@ -554,6 +554,21 @@ type UpdateChatPlanModeInstructionsRequest struct {
PlanModeInstructions string `json:"plan_mode_instructions"`
}
// ChatExploreModelOverrideResponse is the response body for the Explore
// subagent model override configuration endpoint.
type ChatExploreModelOverrideResponse struct {
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
// HasMalformedOverride reports whether the saved override is malformed and
// is currently being treated as unset.
HasMalformedOverride bool `json:"has_malformed_override"`
}
// UpdateChatExploreModelOverrideRequest is the request body for updating the
// Explore subagent model override configuration endpoint.
type UpdateChatExploreModelOverrideRequest struct {
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
}
// UserChatCustomPrompt is the request and response body for the
// user chat custom prompt configuration endpoint.
type UserChatCustomPrompt struct {
@@ -1958,6 +1973,35 @@ func (c *ExperimentalClient) UpdateChatPlanModeInstructions(ctx context.Context,
return nil
}
// GetChatExploreModelOverride returns the deployment-wide Explore subagent
// model override.
func (c *ExperimentalClient) GetChatExploreModelOverride(ctx context.Context) (ChatExploreModelOverrideResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/explore-model-override", nil)
if err != nil {
return ChatExploreModelOverrideResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatExploreModelOverrideResponse{}, ReadBodyAsError(res)
}
var resp ChatExploreModelOverrideResponse
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// UpdateChatExploreModelOverride updates the deployment-wide Explore subagent
// model override.
func (c *ExperimentalClient) UpdateChatExploreModelOverride(ctx context.Context, req UpdateChatExploreModelOverrideRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/explore-model-override", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetUserChatCustomPrompt fetches the user's custom chat prompt.
func (c *ExperimentalClient) GetUserChatCustomPrompt(ctx context.Context) (UserChatCustomPrompt, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/user-prompt", nil)