Skip to content

bug(workflow-automation): chat-driven workflow creation now succeeds but returns three fixed echo steps for every request #13809

Description

@mrveiss

Discovered by verifying the effect of #13730 rather than assuming it.

Context

Before #13730, create_workflow_from_chat_request returned None for every request and
api/workflow.py:292 turned that into HTTP 500. The path is now live for the first time
since the #5058/#6951 migrations. That is an improvement, but the path it unblocked does not
produce a useful workflow.

Finding

Every chat request produces the same three placeholder steps, whatever was asked:

request: "install docker and wipe logs"

step='step_1' confirm=False cmd="echo 'Executing: analyze_request'"
step='step_2' confirm=True  cmd="echo 'Executing: execute_plan'"
step='step_3' confirm=False cmd="echo 'Executing: synthesize_results'"

Two independent reasons the user's request never reaches the command:

  1. The plan is a fixed template. orchestrator.plan_workflow_steps returns a hardcoded
    1-step (SIMPLE) or 3-step (COMPLEX) skeleton with actions analyze_request /
    execute_plan / synthesize_results. It puts the request in inputs["query"] and never
    derives an action from it. Note this is not the LLM planner —
    orchestrator.create_workflow_plan does real planning, but this path does not call it.
  2. The command fallback cannot match. manager._extract_command_from_step first looks for
    step.inputs["command"], which this plan never sets (it sets query), then falls back to
    keyword-matching the action — but the actions are the three fixed strings above, which
    match none of its update/install/search branches. So every step lands on the final
    echo 'Executing: {action}' branch.

Combined with #13807 (classification silently disabled → every request is COMPLEX), the
endpoint returns an identical 3-echo workflow for every input.

Security note: no injection here

Worth stating explicitly since this generates shell commands: the user's text does not flow
into the command. _extract_command_from_step only ever emits a literal echo of a fixed
action name, or a hardcoded apt/find string on keyword match — and those branches are
unreachable from this plan. Verified with a deliberately hostile request. This issue is about
the capability being hollow, not about an injection risk.

Why file it rather than leave it

/api/workflow-automation/create_from_chat defaults to auto_start=True and
require_approval=False, so the default path now creates and starts a workflow that does
nothing meaningful — where it previously failed loudly with a 500. A silent no-op is harder to
notice than an error, so unblocking the path without this follow-up trades a visible failure
for an invisible one.

Options

  1. Route this endpoint at orchestrator.create_workflow_plan (the real LLM planner) instead of
    the fixed plan_workflow_steps skeleton — likely the intent, and it converges the two
    planning paths rather than adding a third.
  2. Keep the skeleton but make plan_workflow_steps derive real actions/commands from the
    request.
  3. If the endpoint is not meant to be reachable yet, gate it behind a flag and say so, rather
    than leaving a live route that returns placeholder work.

Recommendation: 1, consistent with create_workflow_plan already being the canonical
planner (see the supersession record in #13751).

Acceptance criteria

  • A chat request produces steps that reflect the request, not three fixed echoes.
  • A test asserts two different requests yield different plans — the current behaviour would
    fail such a test.
  • Decide whether auto_start=True remains the right default for a path that executes
    generated commands, and record the reasoning.
  • Confirm which planner this endpoint should use, so the codebase is not left with both
    plan_workflow_steps and create_workflow_plan serving chat requests.

Activity

  1. mrveiss commented on Aug 10, 2026

    @mrveiss
    OwnerAuthor

    Proof landed (PR #13928) — the routing decision is yours, and it has a safety half

    The defect is now machine-checked: two xfail(strict=True) tests describe the intended behaviour and fail today, so whoever fixes this must remove them. Plus a characterisation test pinning today's fixed skeleton, and a security test pinning that hostile text cannot reach a generated command.

    I did not wire the real planner. Reading the route first is what stopped me:

    auto_start      = request.get("auto_start", True)         # start immediately
    require_approval = request.get("require_approval", False)  # no approval

    Today the endpoint builds three echo steps and runs them — useless but harmless. Routing it at create_workflow_plan behind those defaults turns it into a route that generates shell commands from user text and executes them without approval. That is a change in what the system may do unattended, not a refactor, and shipping it while the defaults stand would deliver the risky half of the fix without the decision that governs it.

    The two decisions, with recommendations

    1. Which planner? — recommend create_workflow_plan (option 1 in this issue). It converges on the canonical planner rather than adding a third path, consistent with the #13751 supersession record. Option 2 (teach plan_workflow_steps to derive actions) would build a second real planner beside the existing one, which is the fork pattern.

    2. Does auto_start=True survive? — recommend no, and that it flips before the planner is wired, not after. The ordering matters: land the planner first and there is a window where the default path auto-executes generated commands. Flipping first costs a backward-compatibility break on a route that currently produces nothing of value — the cheapest moment this will ever be.

    If you would rather keep auto_start=True for compatibility, then option 3 (gate the route behind a flag until approval is the default) is the safer sequencing, and I would take that over wiring the planner into the current defaults.

    What I would do on your word

    Either: flip the defaults, then route at create_workflow_plan, removing the xfail markers as the fix lands. Or: gate the route and leave the planner alone. Both are a single PR; the choice is which risk you would rather hold.

    Labelling needs-decision and moving on.

  2. added
    needs-decisionBlocked on an owner decision; options and a recommendation are on the issue
    on Aug 10, 2026
  3. mrveiss commented on Aug 10, 2026

    @mrveiss
    OwnerAuthor

    Proof landed — PR #13928 merged. Verified in base: autobot-backend/tests/services/test_chat_workflow_is_request_shaped.py is present, with two xfail(strict=True) markers that will XPASS (and therefore fail) the moment the plans become request-shaped, plus a security test that must survive whatever planner replaces this one.

    The two decisions in the comment above are unchanged and still blocking the fix: which planner, and whether auto_start=True survives. My recommendation remains to flip the default before wiring the planner, since landing it first leaves a window where the default path auto-executes generated commands.

  4. added this to the Backlog milestone on Sep 12, 2026
  5. mrveiss commented on Oct 3, 2026

    @mrveiss
    OwnerAuthor

    Correction: I called this closeable. It is not — the guard pins the defect as live

    I triaged this as closeable on the strength of its last comment — "Proof landed — PR #13928 merged … two xfail(strict=True) markers" — and another session caught the error before anything was closed. Recording it here because this issue has a merged PR and a delivered-looking history that will mislead the next sweep exactly as it misled me.

    The inversion

    A strict=True xfail asserts today's broken behaviour. It fails the moment the behaviour is fixed, which forces whoever fixes it to remove the marker. So the marker's presence is the strongest available evidence that the defect is still there — the opposite of what I read it as.

    The test file states this itself, at autobot-backend/tests/services/test_chat_workflow_is_request_shaped.py:23-27:

    The two tests below are marked xfail(strict=True) deliberately. They describe the behaviour the endpoint is supposed to have, so when #13809 is fixed they XPASS — which fails — and whoever fixes it must remove the markers.

    Verified on merged main, not against the test's description of itself

    autobot-backend/orchestrator.py still builds the fixed three-step skeleton the issue describes:

    task_id="step_1"   agent_type="analyzer"      action="analyze_request"      description="Analyze user request"
    task_id="step_3"   agent_type="synthesizer"   action="synthesize_results"   description="Synthesize results"
    

    Hardcoded actions, not derived from the request. AC1 is unmet. The merge added one new test file and zero lines of orchestrator.py — the commit title says so outright: "pin that chat plans are fixed echoes and must not be".

    What would have to be true to close this

    The plans become request-shaped in orchestrator.py, and the two xfail(strict=True) markers are removed in the same change — the markers are designed to fail at that moment, so a fix that leaves them in place turns the suite red. That coupling is deliberate and it is also the closure signal: this issue is closeable exactly when those markers are gone and the tests pass on their own.

    The general lesson, since this is the second shape of it today

    "A guard landed" and "the fix landed" are different claims, and a guard's presence points in whichever direction the guard asserts. A guard asserting the fixed behaviour passing is evidence the fix is in. A guard asserting the broken behaviour — an xfail-strict, a ratchet pinned at today's count, a baseline entry — is evidence the defect is in. Reading the first pattern onto the second is how a well-built guard becomes a false closure argument.

    Earlier today the same family appeared pointed the other way: a search whose absence of callers was by design, recorded only in a comment at the declaration. Both are cases where the artifact's defence looks like the thing it defends against.

    No state changed — nothing ticked, closed or relabelled. The correction is mine; the catch was not.

  6. mrveiss commented on Oct 3, 2026

    @mrveiss
    OwnerAuthor

    Owner ruling, 2026-10-03 — planner, approval default, and the security test

    Two questions were put to the owner in the session implementing this issue. Recorded here so the PR and the criteria can cite them.

    What the code showed before asking. _extract_command_from_step returns step.inputs["command"] verbatim when present. If a real planner writes that field, user text reaches the generated command by design. So the property pinned by test_the_user_request_never_reaches_a_command holds today only because the endpoint is hollow, and it cannot survive a working fix. A second finding: create_workflow_plan's prompt (orchestration/orchestrator_prompts.py) asks for agent tasks {agent, action, inputs: {}} and never for a shell command. Rerouting alone would produce request-shaped actions whose steps still all run echo 'Executing: <action>'.

    Ruling:

    1. Planner (AC4): create_from_chat uses orchestrator.create_workflow_plan, the single canonical planner. plan_workflow_steps stops serving chat requests.
    2. Command generation (AC1): an opt-in contract on the shared planning prompt asks for inputs.command on shell tasks. Only this chat path uses it; every other caller's prompt is unchanged.
    3. Default (AC3): the server default becomes require_approval=True, auto_start=False. The frontend's only caller (useWorkflowBuilder.ts) already defaults to requireApproval=true, so this removes a server/client divergence rather than tightening a default anyone relies on.
    4. Per-step confirmation: every step whose command the LLM authored carries requires_confirmation=True. That holds as a property of the step that executes, independent of the route default.
    5. Security test: test_the_user_request_never_reaches_a_command is replaced by an assertion of the property that does hold with a real planner: no generated command runs without human approval. It is asserted on behaviour (route defaults and step flags), not on tokens in generated text.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions