Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions actions/setup/js/workflow_tool_policy_alignment.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,24 @@ describe("workflow native MCP permission alignment", () => {
expect(prompt).not.toContain("For `mcpscripts`, always use the CLI commands above");
expect(prompt).toContain("A CLI example does not grant permission");
});

it("keeps safe-output payload guidance independent of auxiliary shell commands", () => {
for (const name of ["mcp_cli_tools_with_safeoutputs_prompt", "safe_outputs_prompt", "shell_expansion_guard_rejected"]) {
const prompt = readFileSync(resolve(root, `actions/setup/md/${name}.md`), "utf8");
expect(prompt).not.toMatch(/\bjq\b|\bnode\b|<<['"]?EOF|printf .*safeoutputs/);
expect(prompt).toContain("JSON object");
expect(prompt).toContain("`\\n`");
}
const prompt = readFileSync(resolve(root, "actions/setup/md/mcp_cli_tools_with_safeoutputs_prompt.md"), "utf8");
expect(prompt).toContain("not auxiliary commands, pipelines, file writes, or shell expansions");
expect(prompt).toContain("do not repeat it or switch to an unapproved shell workaround");
expect(prompt).toContain("safeoutputs report_incomplete");

const example = prompt.match(/```bash\n([^\n]+)\n```/)[1];
const handler = buildCopilotSDKPermissionHandler({ allowedTools: ["shell(safeoutputs:*)"] }, () => ({ kind: "approve-once" }));
expect(handler({ kind: "shell", commands: [], fullCommandText: example })).toEqual({ kind: "approve-once" });
const payload = JSON.parse(example.match(/^safeoutputs add_comment '(.+)'$/)[1]);
expect(payload).toEqual({ item_number: 42, body: "### Title\n\nBody." });
expect(handler({ kind: "shell", commands: [], fullCommandText: "jq -Rs '{body: .}' body.md | safeoutputs add_comment ." }).kind).toBe("reject");
});
});
29 changes: 3 additions & 26 deletions actions/setup/md/mcp_cli_tools_with_safeoutputs_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,13 @@ For `mcpscripts`, prefer its native functions when they are explicitly exposed i

For `safeoutputs`, every successful call is a real write-intent declaration - do not use it for probing, auth checks, or placeholder payloads. Use `noop` or `report_incomplete` if not ready to emit the final action.

For multiple or complex arguments, pass the JSON object inline as a single quoted argument — the binary comes first, so a malformed command still runs the CLI and reports an error:
Pass the complete JSON object inline as a single shell-quoted argument. Encode newlines inside JSON strings as `\n`, and escape JSON and shell quotes correctly. The binary comes first, so a malformed payload still runs the CLI and reports an error:
```bash
safeoutputs add_comment '{"item_number":42,"body":"### Title\n\nBody."}'
```
Alternatively, pipe a JSON object on stdin using `.` as the sentinel (prefer this only for large bodies built from files):
```bash
printf '{"item_number":42,"body":"### Title\n\nBody."}' | safeoutputs add_comment .
# or read from a file: safeoutputs create_pull_request . < /tmp/payload.json
```
When piping, make sure the `|` is actually present: `printf '{...}' safeoutputs add_comment .` (no pipe) prints the JSON and exits 0 without ever running the CLI, so nothing is emitted.

**Multi-line or long `body` content:** do NOT build the JSON payload with `printf`/`echo` embedding raw newlines or many escaped characters directly in the command line — the sandbox's shell command-injection guard may reject long or complex quoted arguments (reporting "expansion patterns"/"command substitution" even though none are present) and retrying the identical command will fail again. Instead, write the content to a temp file with a heredoc, then use `jq -Rs` to inject it as the `body` field:
```bash
cat <<'EOF' > /tmp/gh-aw/body.md
Title
CLI proxying grants the mounted executable, not auxiliary commands, pipelines, file writes, or shell expansions. Do not stage or serialize payloads with helper commands unless the workflow explicitly permits them. Raw Markdown and `@filename` references are not JSON payloads.

Multi-line body content goes here.
EOF
jq -Rs '{title: "My title", body: .}' /tmp/gh-aw/body.md | safeoutputs create_discussion .
```
If a shell command is rejected for containing expansion patterns, do not retry the same command — switch to the heredoc + `jq -Rs` pattern above.

To inject an entire local file as the `body` field without re-embedding its content in the model context, use `jq -Rs`:
```bash
jq -Rs --arg discussion_number "$DISCUSSION_NUMBER" \
'{discussion_number: ($discussion_number|tonumber), body: .}' \
discussion-body.md \
| safeoutputs update_discussion .
```
`jq -Rs` reads the file as a raw string (`-R`) and slurps it into a single JSON string value (`-s`), so `body` is always a valid JSON field. Piping `cat file | safeoutputs ...` does not populate `body` and will be rejected.
If the sandbox rejects a command, do not repeat it or switch to an unapproved shell workaround. Use a native safe-output function only if it is explicitly exposed in your tool inventory; otherwise call `safeoutputs report_incomplete` with inline JSON containing `reason` and optional `details`. If even that call is unavailable or denied, state the blocker in your final response; do not probe with real writes.

The generated command syntax above is schema-derived from each enabled tool's final `inputSchema` and is the source of truth for required/optional parameters.
Use `<server> --help` and `<server> <tool> --help` for the same schema-derived signatures and examples before calling any command.
Expand Down
2 changes: 1 addition & 1 deletion actions/setup/md/safe_outputs_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ Safe-output calls are write-once declarations for real downstream side effects.

temporary_id: optional cross-reference field for future resources created by safe outputs. Canonical form: '#aw_' followed by 3–12 alphanumeric or underscore characters — e.g., '#aw_abc1', '#aw_pr_fix'. Pattern: /^#?aw_[A-Za-z0-9_]{3,12}$/i (the '#' prefix is optional; bare 'aw_abc1' is accepted and normalised to '#aw_abc1' automatically). Use this form for all field values (temporary_id, item_number, issue_number, parent, etc.). In body/markdown text, '#aw_abc1' references are replaced with the real issue/PR number after creation. Omit entirely when not needed.

**Note**: safeoutputs tools do NOT support `@filename` file name expansion. Always provide content inline — do not use `@filename` references in tool arguments. To inject an entire file as the `body` field, use `jq -Rs` to read it as a JSON string and pipe the resulting payload: `jq -Rs '{body: .}' file.md | safeoutputs update_discussion .`
**Note**: safeoutputs tools do NOT support `@filename` file name expansion. Provide the actual content inline in the `body` field. For native tools, pass structured arguments directly. For CLI transport, pass one complete shell-quoted JSON object with newlines encoded as `\n`; do not assume auxiliary serialization commands or shell file writes are permitted.
</safe-outputs>
15 changes: 4 additions & 11 deletions actions/setup/md/shell_expansion_guard_rejected.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,9 @@ This signal was detected from engine runtime logs and is usually caused by retry
<summary>How to remediate</summary>

- Do **not** retry the identical rejected command.
- Put multi-line content in a temporary file with a single-quoted heredoc.
- Use `jq -Rs` to JSON-escape the file contents before piping to `safeoutputs`.

```bash
cat <<'EOF' > /tmp/gh-aw/body.md
Title

Multi-line body content goes here.
EOF
jq -Rs '{title: "My title", body: .}' /tmp/gh-aw/body.md | safeoutputs create_discussion .
```
- Use structured arguments through a native safe-output function only if it is exposed in the tool inventory.
- For CLI transport, pass a complete shell-quoted JSON object directly to `safeoutputs`; encode newlines as `\n`.
- Do not assume auxiliary commands, pipelines, temporary-file writes, or shell expansions are permitted.
- If the intended output cannot be submitted safely, call `report_incomplete` through an available, permitted transport with the concrete blocker. If that call is also unavailable or denied, state the blocker in the final response.

</details>
Loading