Repository navigation
Make safeoutputs CLI transport fail loudly instead of fail open - #61427
Conversation
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>
|
Warning Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding. What happenedThe threat detection engine failed to produce results. Review the workflow run logs for details.
|
|
✅ Ponytail Reviewer completed successfully! Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "ab.chatgpt.com"See Network Configuration for more information.
|
|
✅ 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 happenedThe threat detection engine failed to produce results. Review the workflow run logs for details.
|
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅
|
|
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).
|
There was a problem hiding this comment.
🟡 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
noopaliases. - 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 matchessafeoutputs noopinside a quoted payload, for exampleprintf '{"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
!jsonOutputguard lets an unrecognized positional argument pass whenever--jsonis also present. For example,safeoutputs <zero-input-tool> 'no action needed' --jsonproduces 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 === 1means a documented inline payload followed by a CLI option (for examplesafeoutputs 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.
| // 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 '.'.`; |
| // 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]); |
| function findInlineJsonPayloadArg(args) { | ||
| if (args.length !== 1) return null; | ||
| const trimmed = args[0].trim(); | ||
| return trimmed.startsWith("{") ? trimmed : null; | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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|;&><betweenprintf/echoand the literal tokensafeoutputs— it doesn't verifysafeoutputsis 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,
--messageflag cases are all covered). - Unrelated
.github/aw/actions-lock.jsondowngrade:docker/build-push-actionanddocker/setup-buildx-actionare pinned back from v7.4.0/v4.4.0 to v7.3.0/v4.3.0, while every generated.lock.ymlin 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,parseToolArgsinline 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) { |
There was a problem hiding this comment.
[/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); |
There was a problem hiding this comment.
[/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" | ||
| }, |
There was a problem hiding this comment.
[/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.
|
Latest blocking review points:
Run URL: https://github.com/github/gh-aw/actions/runs/35185468342 Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "github.com"See Network Configuration for more information.
|
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>
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>
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. |
|
🎉 This pull request is included in a new release. Release: |
The documented
safeoutputsCLI form puts the binary in the middle of the command line (printf '{...}' | safeoutputs noop .). Dropping the|makesprintfconsumesafeoutputs 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.Inline JSON payload (
mcp_cli_bridge.cjs)safeoutputs noop '{"message":"..."}'. The binary comes first, so every malformed variant still executes it..is unchanged; inline takes precedence when both are present.No silent argument dropping
safeoutputsserver, 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.'{}'inline, or stdin.) stay exempt so zero-input tools still call through.Prompt and schema-derived docs
mcp_cli_tools_with_safeoutputs_prompt.mdleads 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> --helpemitsafeoutputs <tool> '<json object>'instead of theprintf ... | ... .shape.noopschema leniencynoop.messagegainsx-synonyms(reason,summary,details,text,note,status) in both copies ofsafe_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 missingmessage.Self-diagnosing failure reports (
handle_agent_failure.cjs)safeoutputscommands found in the agent transcript (add-mask redacted, deduped, capped at 10 × 300 chars).printf/echowriter has no pipe or separator beforesafeoutputs, 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.safeoutputsCLI transport fails open: a dropped pipe exits 0 with identical stdout #61391