-
Notifications
You must be signed in to change notification settings - Fork 298
Description
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:
- The compiler generates one tool per allowed worker and adds
_call_workflow_namemetadata. - The compiler emits
config.call_workflowand the conditionalcall-*jobs. actions/setup/js/safe_outputs_tools_loader.cjswraps_call_workflow_nametools withdefaultHandler("call_workflow").actions/setup/js/safe_outputs_tools_loader.cjsalso contains registration logic that recognizes_call_workflow_nameand gates it onconfig.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_workflowtool names fall through to the genericenabledTools.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_nameis emptyneeds.safe_outputs.outputs.call_workflow_payloadis empty- all generated
call-*jobs are skipped
Reproduction
- Configure a workflow with:
safe-outputs:
call-workflow:
workflows:
- generic-worker- Ensure
generic-workerexists and declaresworkflow_call. - Compile the workflow.
- Run it through the normal safe-outputs HTTP path.
- Inspect the
safe_outputsstep 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:
- Detect
_call_workflow_name. - Gate those tools on
safeOutputsConfig.call_workflow. - Avoid comparing workflow-specific tool names against
enabledTools.has(tool.name). - Include
_call_workflow_namein 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:
call_workflowtools register whenconfig.call_workflowexists.call_workflowtools do not register whenconfig.call_workflowis absent.tools/listincludes 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.