Skip to content

dispatch-workflow: omitted ref on command (issue_comment) triggers silently dispatches the default branch, bypassing allowed-refs #56121

Description

@blozano-tt

Summary

When an agent omits the (optional) ref argument on a dispatch-workflow safe-output call, dispatch_workflow.cjs silently falls back to the dispatching workflow's own ref. On a command / issue_comment trigger that is always refs/heads/main — so the dispatched CI runs test the default branch instead of the PR the command was issued on, and report green for code that was never exercised. allowed-refs cannot prevent this because it is only validated against explicit refs; the fallback bypasses it entirely.

This is the follow-up failure mode to the (all fixed, thank you!) #49400#49408, #49713, #50027 chain: the agent can now pass ref per call — but nothing makes it have to, and the failure when it doesn't is silent and wrong rather than loud.

Evidence run

https://github.com/tenstorrent/tt-metal/actions/runs/32947659949 — the /test command workflow in tenstorrent/tt-metal (source: test-command.md, compiled with gh-aw v0.86.2, dispatch-workflow configured with allowed-refs: ["**"] and 34 allowlisted workflows).

{"type":"dispatch_workflow","workflow_name":"sanity-tests","inputs":{"run-blackhole":true, "...":"..."}}
{"type":"dispatch_workflow","workflow_name":"perf-device-models","inputs":{"architecture":"[\"blackhole\", \"wormhole_b0\"]"}}

Net effect: real hardware pipelines burned on main, and a PR author who trusts the badges gets a green signal that tested none of their code.

Root cause

actions/setup/js/dispatch_workflow.cjs resolves the default ref once, before processing items:

if (config["target-ref"]) {
  defaultRef = config["target-ref"];
} else if (process.env.GITHUB_HEAD_REF) {
  // We're in a pull_request event, use the PR branch ref
  defaultRef = `refs/heads/${process.env.GITHUB_HEAD_REF}`;
} else if (process.env.GITHUB_REF || context.ref) {
  defaultRef = process.env.GITHUB_REF || context.ref;
} else {
  defaultRef = await getDefaultBranchRef();
}

On issue_comment (which is what command:/slash_command: workflows compile to) GITHUB_HEAD_REF is unset and GITHUB_REF is refs/heads/<default-branch>, so defaultRef is always the default branch — even though the run is unambiguously a PR context (context.payload.issue.pull_request is set, and gh-aw's own checkout_pr_branch.cjs resolves the PR head for the very same run).

And the per-item handling only validates explicit refs:

const outputRef = typeof message.ref === "string" ? message.ref.trim() : "";
let ref = defaultRef;
if (outputRef) {
  ref = normalizeRef(outputRef);
  // ... allowed-refs checks happen only inside this block ...
}

So there is no configuration that fails closed: allowed-refs constrains what the agent may say, not what an omission does. target-ref can't help either — it's a single static string, and the correct branch differs per invocation.

A contributing factor: the injected ref schema description reads "If omitted, the dispatching workflow's ref is used." In a command workflow the agent has just been checked out onto the PR branch, so a model can quite reasonably conclude omission is safe — its checked-out ref is the PR branch. The description is technically about GITHUB_REF, but nothing in the tool surface says "omission = default branch" on comment triggers.

Expected behavior (any of these would close the gap)

  1. Resolve the PR head in the fallback chain for issue_comment-on-PR events, mirroring checkout_pr_branch.cjs: when context.eventName === "issue_comment" and context.payload.issue.pull_request is set, fetch the PR head ref (the PR number is right there in the payload) instead of falling through to GITHUB_REF. This makes dispatch consistent with checkout — today the agent runs on the PR branch but dispatches at main.
  2. Apply allowed-refs to the fallback ref too (fail closed): if the resolved default ref doesn't match the configured patterns, refuse the dispatch with an error instead of silently proceeding. Repos could then express "PR branches only, never main".
  3. A require-ref: true (or similar) option on safe-outputs.dispatch-workflow that makes an omitted message.ref a hard per-item error, and marks ref as required in the generated per-workflow tool schemas.

Actual behavior

Omitted ref on an issue_comment-triggered workflow silently dispatches every target workflow against the default branch, with no warning and no way to configure a failure.

Reproduction

  1. A gh-aw workflow with on: command (PR comments), safe-outputs.dispatch-workflow with allowed-refs: ["**"] and at least one target workflow.
  2. Trigger it from a PR comment; have the agent call a dispatch tool without ref (models do this some fraction of the time even when instructed otherwise — that's the point of wanting a deterministic guard).
  3. Observe the dispatched run's headBranch is the default branch.

Workaround we're shipping in tenstorrent/tt-metal

A post-steps step that rewrites /tmp/gh-aw/agent_output.json after gh-aw's "Ingest agent output" step, forcing ref on every dispatch_workflow item to the runner-resolved PR head branch (pre-fetched deterministically in pre-agent-steps). It works because post-steps are emitted between ingestion and the agent artifact upload that the safe_outputs job consumes — but it's coupled to those internals, which is why a first-class fix here would be better.

gh-aw version: v0.86.2 (actions github/gh-aw-actions@6aab9e5, awf v0.27.44).

Activity

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

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions