Skip to content

[WIP] Fix duplicate unwired tool for call_workflow feature - #53726

Closed
pelikhan with Copilot wants to merge 1 commit into
mainfrom
copilot/fix-call-workflow-duplication
Closed

[WIP] Fix duplicate unwired tool for call_workflow feature#53726
pelikhan with Copilot wants to merge 1 commit into
mainfrom
copilot/fix-call-workflow-duplication

Conversation

Copilot AI commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Thanks for asking me to work on this. I will get started on it and keep this PR's description up to date as I form a plan and make progress.


This section details on the original issue you should resolve

<issue_title>call_workflow (and other renamed dynamic tools) get a duplicate, unwired generic tool that silently no-ops</issue_title>
<issue_description>## Summary

When a workflow configures the built-in call_workflow safe-output feature targeting a specific reusable workflow, the compiler correctly generates a properly-typed tool named after the target workflow (e.g. agent_sandbox_stack for a workflow named agent-sandbox-stack). However, the runtime stdio safe-outputs MCP server (actions/setup/js/safe_outputs_tools_loader.cjs, registerDynamicTools()) also creates a second, generic, unwired tool literally named call_workflow for the same config key — because its "already handled?" dedup check only compares by exact tool name, and the real tool was renamed away from call_workflow.

An agent presented with both tools has no way to know one is fake. Calling the generic call_workflow tool returns a false "executed successfully" response (isError: false) and writes a malformed record with no workflow_name, which the downstream safe-outputs processor then rejects with Workflow name is empty — but by then the agent has already reported success and moved on. This causes a silent, hard-to-diagnose failure: the requested workflow call never happens, and nothing in the agent's own transcript indicates anything went wrong.

Root cause

  • pkg/workflow/safe_outputs_call_workflow.go (generateCallWorkflowTool / generateWorkflowToolDefinition) names the generated tool after the target workflow and tags it with _call_workflow_name metadata — not named call_workflow itself.
  • actions/setup/js/safe_outputs_tools_loader.cjs, registerDynamicTools(), dedups only by exact tool name:
    if (server.tools[normalizedKey] || tools.find(t => t.name === normalizedKey)) {
      return;
    }
    For config key call_workflow, neither check finds the already-registered agent_sandbox_stack tool (different name), so it falls through and synthesizes a second, generic tool — additionalProperties: true, handler writes {type: "call_workflow", ...args} straight to the output file, bypassing all the metadata-wrapping logic in attachHandlers().
  • This only affects the stdio safe-outputs MCP server path (the default/compiler-selected path). The HTTP variant (safe_outputs_mcp_server_http.cjs) uses a different registration loop and is not affected.
  • The same root cause (treating every safe-outputs config key as a potential agent-facing tool unless something already has its exact name) also exposes create_report_incomplete_issue as a bogus tool alongside the real report_incomplete tool, and would similarly affect dispatch_workflow, dispatch_repository, and replace_label if configured.
  • Degenerate case: if the target workflow itself normalizes to literally call_workflow (e.g. named call-workflow), the exact-name check happens to succeed and no duplicate is created — this only bites "normal" target names.

Reproduction

Live reproduction (not just source reading) against current main:

  • config: {call_workflow: {workflows: ["agent-sandbox-stack"]}}
  • Registered tools: ["agent_sandbox_stack", "call_workflow"]
  • Calling call_workflow with arbitrary args returns:
    {"content":[{"type":"text","text":"{\"result\":\"Safe-job 'call_workflow' executed successfully with arguments: {...}\"}"}],"isError":false}
    and writes raw record {"type":"call_workflow", ...args} — no workflow_name.
  • Passing that raw record to the real downstream handler (actions/setup/js/call_workflow.cjs) returns exactly: Workflow name is empty.

This exact failure mode occurred for us in production: an agent run had both agent_sandbox_stack and call_workflow available, called call_workflow, got a false success, and the requested stack-verification handoff silently never happened.

Related issues (not duplicates)

Suggested fix

Narrow/tactical: extend the dedup check for call_workflow (and dispatch_workflow, dispatch_repository) to also treat any tool carrying the corresponding metadata key (_call_workflow_name, _workflow_name, _dispatch_repository_tool) as "already handled," not just an exact name match. Roughly a one-line change per family, plus a regression test.

Structural: make registerDynamicTools() tool-definition-driven rather than config-key-driven — only synthesize a generic tool for config keys that don't correspond to any known tool family (via metadata), rather than inferring "custom safe-job" status from the mere presence of a config key. This also fixes the create_report_incomplete_issue leak and prevents future config keys from hitting the same trap.

Happy to provide the reproduction script if useful.
</issue_description>

Comments on the Issue (you are @copilot in this section)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

call_workflow (and other renamed dynamic tools) get a duplicate, unwired generic tool that silently no-ops

2 participants