fix(hub): expose TodoWrite via SDK default toolset

Unrestricted roles were still passed an explicit --tools name list. The
native Claude binary only reliably registers bundled tools like TodoWrite
on --tools default. Treat role tools JSON null as unrestricted, use the
claude_code preset (→ default) in that case, and keep TodoWrite on allowedTools.
This commit is contained in:
2026-07-23 22:56:26 +08:00
parent b4fe734e80
commit bb426dfaf5
3 changed files with 35 additions and 19 deletions
+20 -12
View File
@@ -140,7 +140,10 @@ export async function runAgent(req: RunRequest): Promise<RunResult> {
let cleanupSecurity = async (): Promise<void> => {};
try {
await persistAgentMessage(req, "user", req.prompt);
const toolConfig = claudeSdkToolConfigForRole(req.tools);
// Role tools JSON null means unrestricted (omit), not "deny all".
const roleToolIds = req.tools === null ? undefined : req.tools;
const unrestricted = roleToolIds === undefined;
const toolConfig = claudeSdkToolConfigForRole(roleToolIds);
const workspaceRoot = req.project.workspaceRoot?.trim();
if (workspaceRoot === undefined || workspaceRoot === "") {
throw new Error("Agent run requires the configured workspace root");
@@ -154,20 +157,25 @@ export async function runAgent(req: RunRequest): Promise<RunResult> {
});
cleanupSecurity = security.cleanup;
const hasSkills = security.skillIds.length > 0;
type QueryOptions = NonNullable<Parameters<typeof query>[0]["options"]>;
// When unrestricted, pass the SDK default toolset (`--tools default`) instead of
// an explicit subset. Native claude uses that path to register bundled tools like
// TodoWrite; listing names alone can omit them from the model's function list.
// allowedTools still carries explicit MCP names + TodoWrite for permission.
const skillExtras = hasSkills ? (["Skill"] as const) : ([] as const);
const toolsOption: QueryOptions["tools"] = unrestricted
? { type: "preset", preset: "claude_code" }
: uniqueTools([...toolConfig.tools, "TodoWrite", ...skillExtras]);
const allowedToolsOption = uniqueTools([
...toolConfig.allowedTools,
"TodoWrite",
...skillExtras,
]);
const options: QueryOptions = {
cwd: security.cwd,
// `skills` controls discovery/allowlisting, but an explicit `tools`
// list still has to expose the Skill dispatcher itself.
// TodoWrite is always available so multi-step runs can surface a live checklist
// on the Feishu card (Manus-style progress), even when a role whitelists tools.
tools: uniqueTools([
...toolConfig.tools,
"TodoWrite",
...(hasSkills ? ["Skill"] : []),
]),
allowedTools: uniqueTools([...toolConfig.allowedTools, "TodoWrite"]),
tools: toolsOption,
allowedTools: allowedToolsOption,
maxTurns: cap,
includePartialMessages: true,
// ADR-0018: bypass interactive prompts (headless server); the sandbox