Skip to content

Make safeoutputs CLI transport fail loudly instead of fail open - #61427

Merged
pelikhan merged 12 commits into
mainfrom
copilot/fix-safeoutputs-cli-transport
Sep 17, 2026
Merged

pelikhan merged 12 commits into
mainfrom
copilot/fix-safeoutputs-cli-transport

Conversation

Copilot AI commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

The documented safeoutputs CLI form puts the binary in the middle of the command line (printf '{...}' | safeoutputs noop .). Dropping the | makes printf consume safeoutputs noop . as format arguments — identical stdout, exit 0, CLI never invoked — so the agent believes it emitted a safe output and the run ends with none, reported as an undiagnosed "produced no safe outputs" issue.

$ printf '{"message":"no action needed"}' safeoutputs noop .   # CLI never runs
{"message":"no action needed"}
$ echo $?
0

Inline JSON payload (mcp_cli_bridge.cjs)

  • A single quoted JSON object argument is now a first-class transport: safeoutputs noop '{"message":"..."}'. The binary comes first, so every malformed variant still executes it.
  • Malformed or non-object inline JSON throws a parse error instead of falling through the "skip non-flag arguments" path.
  • Stdin . is unchanged; inline takes precedence when both are present.

No silent argument dropping

  • For the safeoutputs server, arguments that resolve to zero recognized tool arguments (e.g. safeoutputs noop 'no action needed') now fail with guidance instead of printing help and exiting 0.
  • Explicit structured payloads ('{}' inline, or stdin .) stay exempt so zero-input tools still call through.

Prompt and schema-derived docs

  • mcp_cli_tools_with_safeoutputs_prompt.md leads with the inline form, keeps piping for large/file-sourced bodies, and states that a missing | prints the payload and exits 0 without running the CLI.
  • renderToolSignature / renderToolRecommendedExample / <tool> --help emit safeoutputs <tool> '<json object>' instead of the printf ... | ... . shape.

noop schema leniency

  • noop.message gains x-synonyms (reason, summary, details, text, note, status) in both copies of safe_outputs_tools.json. The MCP server normalizes synonyms before validation, so the exact payload from the diagnosed run (reason) now succeeds rather than erroring on a missing message.

Self-diagnosing failure reports (handle_agent_failure.cjs)

  • "Produced no safe outputs" reports now list safeoutputs commands found in the agent transcript (add-mask redacted, deduped, capped at 10 × 300 chars).
  • When a printf/echo writer has no pipe or separator before safeoutputs, the report says the CLI never ran and points at the inline form — separating "agent never tried" from "agent tried and the shell dropped it" for future occurrences.

Proposal 3 from the issue (removing the CLI transport for safeoutputs) is not included; it is the largest change and the issue flags it as possibly out of scope.

Tests cover inline detection/parsing, stdin precedence, malformed payload errors, main() routing (including the {} exemption and the unrecognized-argument failure), and transcript scanning with dropped-pipe classification.

Copilot AI and others added 4 commits September 16, 2026 23:25
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…output reports

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix safeoutputs CLI transport to handle dropped pipe correctly Make safeoutputs CLI transport fail loudly instead of fail open Sep 16, 2026
Copilot AI requested a review from pelikhan September 16, 2026 23:36
@pelikhan
pelikhan marked this pull request as ready for review September 17, 2026 04:17
Copilot AI balanced review requested due to automatic review settings September 17, 2026 04:17
@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

✅ Ponytail Reviewer completed successfully!

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • ab.chatgpt.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"

See Network Configuration for more information.

Generated by Ponytail Reviewer for #61427

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed test quality analysis.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Security scanning failed for Design Decision Gate 🏗️. Review the logs for details.

No ADR enforcement needed: PR does not have the 'implementation' label and has <=100 new lines of code in business logic directories (prefetch summary shows 3 additions).

🏗️ ADR gate enforced by Design Decision Gate 🏗️

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved moderate and critical findings remain in argument handling, command classification, and diagnostic report sanitization.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR hardens safeoutputs CLI transport handling and improves diagnostics for runs producing no safe outputs.

Changes:

  • Adds inline JSON payload support and stricter argument validation.
  • Updates schemas, prompts, generated documentation, and noop aliases.
  • Adds transcript-based failure diagnostics and synchronizes action pins.
File summaries
File Description
pkg/workflow/js/safe_outputs_tools.json Adds noop field synonyms.
actions/setup/md/mcp_cli_tools_with_safeoutputs_prompt.md Documents inline and stdin transports.
actions/setup/js/safe_outputs_tools.json Mirrors schema synonym updates.
actions/setup/js/mcp_cli_schema_docs.test.cjs Updates documentation tests.
actions/setup/js/mcp_cli_schema_docs.cjs Generates inline JSON examples.
actions/setup/js/mcp_cli_bridge.test.cjs Tests CLI parsing and routing.
actions/setup/js/mcp_cli_bridge.cjs Implements inline parsing and validation.
actions/setup/js/handle_agent_failure.test.cjs Tests transcript classification.
actions/setup/js/handle_agent_failure.cjs Adds safeoutputs transcript diagnostics.
.github/aw/actions-lock.json Aligns action pin entries.
.changeset/safeoutputs-cli-inline-json-payload.md Records the patch release.
Review details

Suppressed comments (4)

actions/setup/js/handle_agent_failure.cjs:3217

  • Because " and ' are accepted as command boundaries, this regex matches safeoutputs noop inside a quoted payload, for example printf '{"message":"safeoutputs noop"}', even though no CLI invocation exists. A real piped command whose payload contains that text can also be classified as dropped-pipe by the first occurrence. Scan shell tokens or otherwise exclude quoted payload contents before recording/classifying commands.
    if (!/(^|[|;&(\s"'`])safeoutputs\s+[a-z_][a-z0-9_]*/i.test(line)) continue;

actions/setup/js/handle_agent_failure.cjs:3219

  • These excerpts are add-mask redacted but otherwise inserted into a fenced Markdown block, and the failure issue body is not sanitized after template rendering. An agent command containing a triple backtick (for example, copied from an issue body) can close the fence and inject Markdown or mentions into the diagnostic issue. Escape the excerpt for the code block or sanitize this context before rendering it.
    const redacted = applyAddMaskRedaction(line, maskedValues);
    const truncated = redacted.length > SAFEOUTPUTS_CLI_EXCERPT_MAX_LENGTH ? `${redacted.slice(0, SAFEOUTPUTS_CLI_EXCERPT_MAX_LENGTH)}…` : redacted;

actions/setup/js/mcp_cli_bridge.cjs:1662

  • The !jsonOutput guard lets an unrecognized positional argument pass whenever --json is also present. For example, safeoutputs <zero-input-tool> 'no action needed' --json produces an empty argument object and invokes the tool, silently dropping the positional text. Exempt only a command containing recognized output flags, or track/reject unrecognized positional arguments independently of response formatting.
  const usedStructuredPayload = shouldReadStdin || findInlineJsonPayloadArg(toolUserArgs) !== null;
  if (serverName === SAFEOUTPUTS_SERVER_NAME && Object.keys(toolArgs).length === 0 && toolUserArgs.length > 0 && !jsonOutput && !usedStructuredPayload) {

actions/setup/js/mcp_cli_bridge.cjs:757

  • Requiring args.length === 1 means a documented inline payload followed by a CLI option (for example safeoutputs noop '{"message":"inline"}' --message fallback, or an inline payload plus --json) never enters inline mode. The JSON token is then skipped by the flag parser and only the other flags are sent, so the tool can emit a different payload or silently ignore the inline data. Detect/reject the inline payload independently of output flags instead of falling back to positional-argument skipping.
  if (args.length !== 1) return null;
  const trimmed = args[0].trim();
  return trimmed.startsWith("{") ? trimmed : null;
  • Files reviewed: 11/11 changed files
  • Comments generated: 3
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.

Comment thread actions/setup/js/mcp_cli_bridge.cjs Outdated
// payload modes are exempt: an explicit `{}` payload legitimately yields no arguments.
const usedStructuredPayload = shouldReadStdin || findInlineJsonPayloadArg(toolUserArgs) !== null;
if (serverName === SAFEOUTPUTS_SERVER_NAME && Object.keys(toolArgs).length === 0 && toolUserArgs.length > 0 && !jsonOutput && !usedStructuredPayload) {
const message = `no arguments were recognized for '${toolName}' from: ${toolUserArgs.join(" ")}. Pass a JSON object inline (${serverName} ${toolName} '{"key":"value"}'), use --key value flags, or pipe JSON on stdin with '.'.`;
Comment on lines +3181 to +3183
// A pipe (or command separator) between the writer and `safeoutputs` means the
// binary does run; only the separator-free form is silently skipped.
return !/[|;&><]/.test(match[2]);
Comment on lines +754 to +758
function findInlineJsonPayloadArg(args) {
if (args.length !== 1) return null;
const trimmed = args[0].trim();
return trimmed.startsWith("{") ? trimmed : null;
}

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ponytail pass (complexity-only): found a few places where the fix appears broader than needed and can likely be trimmed while preserving behavior.

net: -35 lines possible.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • ab.chatgpt.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"

See Network Configuration for more information.

Generated by ✂️ Ponytail Reviewer for #61427 · codex · gpt53codex · 6 AIC · ⌖ 3.21 AIC · ⊞ 12.5K
Comment /ponytail to run again

Comments that could not be inline-anchored

actions/setup/js/mcp_cli_bridge.cjs:274

actions/setup/js/mcp_cli_bridge.cjs:L274: yagni: separate looksLikeInlineJsonObject helper with one caller. Inline the check into parseToolArgs until reused.

actions/setup/js/handle_agent_failure.cjs:59

actions/setup/js/handle_agent_failure.cjs:L59: shrink: multiple constants/functions just to build a short diagnostics block. Fold extraction/classification into one compact pass over transcript lines.

actions/setup/js/handle_agent_failure.cjs:130

actions/setup/js/handle_agent_failure.cjs:L130: delete: dropped-pipe special-case narrative branch in failure rendering. Keep generic command excerpt output and remove this extra explanatory branch.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd to the new self-diagnosis and inline-JSON-transport logic, plus /codebase-design on the unrelated lock-file change bundled in this diff.

📋 Key Themes & Highlights

Key Themes

  • False-positive risk in isDroppedPipeSafeOutputsCommand: the regex only checks for absence of |;&>< between printf/echo and the literal token safeoutputs — it doesn't verify safeoutputs is actually the invoked command rather than prose inside the echoed string (e.g. echo "See docs: safeoutputs add_comment example" is misclassified as a dropped-pipe failure). This directly feeds the auto-generated failure report language ("the CLI never ran"), which could mislead future debugging.
  • Missing regression test for that false-positive shape, even though the surrounding test suite is otherwise thorough (piped, dropped-pipe, dedup, --message flag cases are all covered).
  • Unrelated .github/aw/actions-lock.json downgrade: docker/build-push-action and docker/setup-buildx-action are pinned back from v7.4.0/v4.4.0 to v7.3.0/v4.3.0, while every generated .lock.yml in the repo still references the newer SHAs — this looks like a stale rebase artifact unrelated to the safeoutputs fix and should be reverted or explained.

Positive Highlights

  • ✅ The core fix (inline JSON as a first-class transport, keeping the binary first on the command line so malformed input fails loudly) directly and correctly addresses the root cause from #61391 — the dropped-pipe silent-success failure mode.
  • ✅ Good test coverage for findInlineJsonPayloadArg, parseToolArgs inline mode, and the new "unrecognized args" failure path, including the {} zero-input exemption edge case.
  • ✅ Docs, schema-derived examples (renderToolSignature/renderToolRecommendedExample), and the prompt markdown were all updated consistently with the new recommended form.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · 85.3 AIC · ⌖ 16.6 AIC · ⊞ 10.6K
Comment /matt to run again

* @param {string} line - Single transcript line containing `safeoutputs`
* @returns {boolean}
*/
function isDroppedPipeSafeOutputsCommand(line) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] The dropped-pipe heuristic can false-positive on prose. isDroppedPipeSafeOutputsCommand only checks for printf/echo followed by safeoutputs with no |/;/&/>/< in between — it doesn't require safeoutputs to actually be the argument of printf/echo. A transcript line like echo "See docs: safeoutputs add_comment example" gets misclassified as a dropped pipe, and the failure report tells the author "the CLI never ran" when it may never have been invoked at all here (it's just prose in an echo statement).

💡 Reproduction
isDroppedPipeSafeOutputsCommand(`echo "See docs: safeoutputs add_comment example"`) // => true (false positive)

Consider requiring safeoutputs to appear as a quoted-string boundary token (e.g. right after '/") rather than anywhere in the echo/printf argument, or tightening the regex to the actual invocation shape (safeoutputs <tool_name>) followed by no closing quote before it.

@copilot please address this.

it("classifies piped and unpiped invocations", () => {
expect(isDroppedPipeSafeOutputsCommand(`printf '{"message":"x"}' safeoutputs noop .`)).toBe(true);
expect(isDroppedPipeSafeOutputsCommand(`printf '{"message":"x"}' | safeoutputs noop .`)).toBe(false);
expect(isDroppedPipeSafeOutputsCommand(`safeoutputs noop --message "x"`)).toBe(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] No regression test covers the false-positive case above (prose mentioning safeoutputs inside an echo/printf string being misclassified as a dropped pipe). The existing tests only cover clean-piped and cleanly-dropped-pipe cases, not the ambiguous "safeoutputs appears in the string content, not as the invoked command" case.

💡 Suggested test
it("does not misclassify prose containing 'safeoutputs' as a dropped pipe", () => {
  expect(isDroppedPipeSafeOutputsCommand(`echo "See docs: safeoutputs add_comment example"`)).toBe(false);
});

@copilot please address this.

"sha": "c3c9e263c25d99ce0380d002d59b67737d91b0dc"
"version": "v7.3.0",
"sha": "53b7df96c91f9c12dcc8a07bcb9ccacbed38856a"
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] This .github/aw/actions-lock.json change downgrades docker/build-push-action v7.4.0→v7.3.0 and docker/setup-buildx-action v4.4.0→v4.3.0, unrelated to this PR's stated purpose (safeoutputs CLI transport). Every generated .lock.yml in the repo (e.g. detection-analysis-report.lock.yml, daily-cache-strategy-analyzer.lock.yml) still references the newer v7.4.0/v4.4.0 SHAs, so this diff introduces a lock-file/workflow-output inconsistency rather than fixing one.

💡 Suggestion

Revert this hunk unless it was an intentional, separately-justified downgrade (e.g. reverting a bad upstream release). If unintentional, it's likely a stale rebase artifact — regenerate the lock file from the current main before merging.

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

pr-sous-chef
@copilot this PR still needs forward progress before a maintainer can investigate quickly. Please address the outstanding safeoutputs CLI review findings, re-run the smallest validation needed on the updated head, refresh the branch if needed, and then run the pr-finisher skill.

Latest blocking review points:

  • prevent dropped-pipe transcript detection from matching safeoutputs ... text inside quoted payloads or prose excerpts
  • sanitize/escape diagnostic command excerpts before rendering them inside fenced Markdown blocks
  • reject unrecognized positional arguments even when --json is present
  • detect or reject inline JSON payloads independently of trailing CLI flags so documented inline transport cannot be silently ignored
  • revert or explain the unrelated .github/aw/actions-lock.json downgrade noted in review
  • investigate the failed agent check: https://github.com/github/gh-aw/actions/runs/35181389777/job/105076009459

Run URL: https://github.com/github/gh-aw/actions/runs/35185468342

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • github.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "github.com"

See Network Configuration for more information.

Generated by 👨‍🍳 PR Sous Chef · pi · gpt54 · 22.2 AIC · ⊞ 9.4K · ◷
Comment /souschef to run again

Copilot AI and others added 2 commits September 17, 2026 05:39
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Copilot AI and others added 5 commits September 17, 2026 05:42
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

pr-sous-chef
@copilot this PR still needs forward progress before a maintainer can investigate quickly. Please address the outstanding safeoutputs CLI review findings, re-run the smallest validation...

Addressed the outstanding safeoutputs parsing, transcript classification, and fenced-excerpt safety issues; restored the unrelated actions-lock downgrade; and investigated the failed agent job. Final fix: f0d8b29.

Copilot AI requested a review from gh-aw-bot September 17, 2026 05:50
@pelikhan
pelikhan merged commit b793d41 into main Sep 17, 2026
43 checks passed
@pelikhan
pelikhan deleted the copilot/fix-safeoutputs-cli-transport branch September 17, 2026 06:43
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.89.17

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

safeoutputs CLI transport fails open: a dropped pipe exits 0 with identical stdout

4 participants