ZhYQ1 314e2a3489
feat(task_tool): introduce read-only preflight validation before delegation side effects (#8120)
* feat(task_tool): introduce read-only preflight validation before delegation side effects

Add _preflight_delegation_checks() as a zero-side-effect preflight that runs before any delegation side effects (tool assembly, executor creation, background task registration). Existing inline checks for context_mode, bash host policy, and subagent_type config membership are centralized into this function.

A rejected preflight returns immediately with status='failed' and leaves state unchanged, so callers can safely retry with corrected arguments.

A reserved resume_task_id parameter is included for a future recovery feature, pointing to issue #7633 (validate before claim/cancel) to avoid the reject-after-claim anti-pattern.

* fix(task_tool): return config from preflight, drop dead resume_task_id param, soften side-effect comment

Fixes UnboundLocalError on successful delegation paths: the helper now returns (config, error) and callers destructure both. Previously config was scoped inside the helper so the happy-path references at lines 1044/1074/1082 raised UnboundLocalError. Verified against test_task_tool_core_logic.py.

Drops the unused resume_task_id parameter and its hardcoded rejection branch — dead code since the only call site never passes it, and a future recovery feature should plug real read-only recoverability checks here (see issue #7633).

Narrows the helper-side-effect claim in the caller comment: input_control.child(tool_call_id) already registers a benign retry-reusable child gate before the preflight runs, so the wording now scopes the zero-side-effect promise to the helper itself.

* test(task_tool): cover preflight abort and happy paths

Adds unit coverage for _preflight_delegation_checks (invalid context_mode, host bash disabled/enabled, subagent outside caller policy, no permitted subagents) plus a rejected-delegation test asserting no executor is created and no tools are assembled.

Adds a happy-path regression test that would have caught the UnboundLocalError: it asserts the config resolved by the preflight reaches SubagentExecutor, including the unconditional config.model == 'inherit' branch.

* fix(task_tool): resolve catalog inside preflight after guards

Copilot review flagged that catalog discovery (get_available_subagent_names) was hoisted out of the helper and ran before any guard, so an invalid context_mode could hit the managed-subagent store instead of returning status='failed'.

Now the helper takes only allowed_subagents and performs its own catalog lookup strictly after the context_mode and bash guards. The caller no longer calls get_available_subagent_names ahead of preflight.

Adds test_preflight_resolves_catalog_after_guards asserting get_available_subagent_names is never invoked when context_mode is invalid.

* test(runtime): use ToolRuntime for native task results

* docs(task_tool): translate preflight comments to English

Comment-only change; no behavior impact.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: AochenShen99 <sac1063467113@hotmail.com>
2026-11-12 18:59:04 +08:00
..