Skip to content

HTTP safe-outputs server does not register generated call-workflow tools #21074

@johnwilliams-12

Description

@johnwilliams-12

Summary

call-workflow is documented and compiled as a supported safe output, but the HTTP safe-outputs MCP server still does not register the generated call_workflow tools at runtime.

The compiler generates workflow-specific tools, the shared tool loader understands _call_workflow_name, and the docs describe the expected call-workflow fan-out behavior. But actions/setup/js/safe_outputs_mcp_server_http.cjs still special-cases only _workflow_name for dispatch_workflow. As a result, generated call_workflow tools are skipped during HTTP-server registration, call_workflow_name is never set, and every generated call-* job is skipped.

Root cause

The compiler side and the shared tool-loader side already handle call-workflow correctly:

  1. The compiler generates one tool per allowed worker and adds _call_workflow_name metadata.
  2. The compiler emits config.call_workflow and the conditional call-* jobs.
  3. actions/setup/js/safe_outputs_tools_loader.cjs wraps _call_workflow_name tools with defaultHandler("call_workflow").
  4. actions/setup/js/safe_outputs_tools_loader.cjs also contains registration logic that recognizes _call_workflow_name and gates it on config.call_workflow.

The drift is in actions/setup/js/safe_outputs_mcp_server_http.cjs.

That file still has its own duplicated predefined-tool registration loop. It recognizes _workflow_name for dispatch_workflow, but it does not recognize _call_workflow_name for call_workflow.

Because of that:

  • generated call_workflow tool names fall through to the generic enabledTools.has(tool.name) check
  • that check is incorrect for workflow-specific tool names such as generic_worker
  • the config key is call_workflow, not the generated tool name
  • the tool is skipped even though the config and metadata are valid

So the HTTP runtime path and the shared tool-loader path are out of sync.

Current evidence in main

Current actions/setup/js/safe_outputs_tools_loader.cjs already handles call_workflow tools:

if (tool._call_workflow_name) {
  const workflowName = tool._call_workflow_name;
  tool.handler = args => {
    return handlers.defaultHandler("call_workflow")({
      inputs: args,
      workflow_name: workflowName,
    });
  };
}

and:

if (tool._call_workflow_name) {
  server.debug(`Found call_workflow tool: ${tool.name} (_call_workflow_name: ${tool._call_workflow_name})`);
  if (config.call_workflow) {
    server.debug(`  call_workflow config exists, registering tool`);
    registerTool(server, tool);
    return;
  }
}

But current actions/setup/js/safe_outputs_mcp_server_http.cjs still only special-cases _workflow_name:

for (const tool of toolsWithHandlers) {
  const isDispatchWorkflowTool = tool._workflow_name && typeof tool._workflow_name === "string" && tool._workflow_name.length > 0;

  if (isDispatchWorkflowTool) {
    // register dispatch_workflow tool
  } else {
    if (!enabledTools.has(tool.name)) {
      // skip tool
      continue;
    }
  }
}

There is no parallel _call_workflow_name branch there.

Expected behavior

When safe-outputs.call-workflow.workflows is configured, the HTTP safe-outputs server registers the generated workflow-specific tools backed by _call_workflow_name metadata.

When the agent calls one of those tools:

  • the handler writes call_workflow_name
  • the handler writes call_workflow_payload
  • the matching generated call-* job runs

Actual behavior

The HTTP safe-outputs server skips the generated workflow-specific tool during registration.

Typical symptom in the safe_outputs step logs:

Skipping tool generic_worker (_call_workflow_name: "generic-worker") - not enabled in config

Then:

  • needs.safe_outputs.outputs.call_workflow_name is empty
  • needs.safe_outputs.outputs.call_workflow_payload is empty
  • all generated call-* jobs are skipped

Reproduction

  1. Configure a workflow with:
safe-outputs:
  call-workflow:
    workflows:
      - generic-worker
  1. Ensure generic-worker exists and declares workflow_call.
  2. Compile the workflow.
  3. Run it through the normal safe-outputs HTTP path.
  4. Inspect the safe_outputs step logs.

The generated worker tool is loaded but not registered by the HTTP server, so the worker never runs.

Proposed fix

Update actions/setup/js/safe_outputs_mcp_server_http.cjs so its predefined-tool registration logic mirrors the shared logic in actions/setup/js/safe_outputs_tools_loader.cjs.

Specifically:

  1. Detect _call_workflow_name.
  2. Gate those tools on safeOutputsConfig.call_workflow.
  3. Avoid comparing workflow-specific tool names against enabledTools.has(tool.name).
  4. Include _call_workflow_name in skip/debug logs.

Suggested hardening follow-up

To prevent this class of regression, the HTTP server should stop duplicating predefined-tool registration logic and reuse the shared helper that already knows about both _workflow_name and _call_workflow_name.

Test coverage to add

Add HTTP-path regression tests that verify:

  1. call_workflow tools register when config.call_workflow exists.
  2. call_workflow tools do not register when config.call_workflow is absent.
  3. tools/list includes generated workflow-specific tools for configured workers.

Relevant changelog and docs context

The changelog records the safe-outputs HTTP transport migration. That is the key update behind this issue: the bug is in the HTTP-specific registration path, not in the compiler or the shared tool loader.

The current safe-outputs docs also describe call-workflow as supported and document the generated conditional uses: jobs, so the runtime behavior currently falls short of the documented feature.

Metadata

Metadata

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions