Skip to content

[bug] The #381 pre-flight check reads only step names, but geocode and fan_out dispatch http_request internally — so an exempt-looking pipeline is refused mid-run instead of at kick #396

Description

@serge-ivo

The guarantee #381 bought, and the door it left open

b11a740 states the point of the pre-flight check plainly: the rule is applied "as the same rule asked
of a whole definition ahead of time, so an authoring mistake is a 400 on attach rather than a refusal
eight steps into a run that has already spent money."
Three statements of one rule — attach 400, kick
refusal before any side effect, dispatch refusal.

The pre-flight arm reads only a step's tool field. ToolNamingSteps
(workers/api/src/lib/pipeline-tool-policy.ts) is literally:

export interface ToolNamingSteps {
	steps: ReadonlyArray<{ tool: string }>;
}

But three step handlers dispatch a connector tool from inside themselves, without it appearing
anywhere in the definition:

  • steps.ts:663fan_out in pages mode → runRegistryTool("http_request", ctx, pageReq)
  • steps.ts:786geocoderunRegistryTool("http_request", ctx, {…}), unconditionally
  • steps.ts:706enrichrunRegistryTool(tool, …) where tool came from the step's inputs

All three step names are part of the step library, so they are exempt by the derived rule (no
connector field ⇒ nothing a creator could declare ⇒ never gated). The pre-flight therefore sees a
definition naming only exempt steps and passes it. runRegistryTool then refuses http_request at
dispatch, mid-run.

So for a pipeline built from exempt steps, the guarantee inverts: the run starts, spends, and is
refused partway through
— the precise outcome the kick check exists to prevent, reached through a
different door.

enrich is a partially-known case: the commit calls out that a name-only gate would be walked past by
{"tool":"enrich","inputs":{"tool":"tmux_run_command"}}, which is why the enforcement lives in
runRegistryTool. That argument is about security and it holds. This issue is about the other half —
geocode and fan_out name no tool anywhere, not even in their inputs, so no author could know their
pipeline depends on http_request, and no static reading of the definition can tell them.

Why it matters now rather than in principle

The #394 audit found the account's only pipeline-running agent is refused at kick today, and its
lead_finder chain contains geocode. Once http_request is declared, that path is fine. But the
next author who builds a geocode-only pipeline gets a green attach, a green kick, and a half-spent
failed run — with a refusal message naming a tool their pipeline never mentions.

Suggested fix

Give a step handler a way to declare the registry tools it may dispatch — a static field on the step
definition (geocode["http_request"], fan_out["http_request"]) that pipelineDeclarationError
unions with the step names it already reads. enrich's tool is dynamic and cannot be fully resolved
statically, but when its inputs.tool is a literal it can be, and that covers the authoring mistake.

Whatever the shape, the refusal message should name the STEP that needs the tool, not just the tool —
"geocode needs http_request, which this agent does not declare" is actionable; "http_request is
not declared" sends the author looking for a step that does not exist.

What this does not claim

It does not claim the dispatch-time gate is wrong — it is the security boundary and must stay. It does
not claim a side effect always occurs before the refusal; a pipeline whose first step is geocode
fails early and cheaply. The defect is that the guarantee is unstated and unreliable, not that it never
holds.

Found by the #394 live-gate audit. Verified against main at 21ecf79 by reading the handlers and the
pre-flight interface, not by running a pipeline.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend / Worker / API workbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions