Skip to content

safe_outputs: support local body file references with strict runtime safeguards #44951

Closed

Description

@straub

Summary

For editor/update workflows, agents often edit large content in a local file, but must still emit the full final body inline in safe output payloads. This increases token cost and context pressure.

Request: add an opt-in safe_outputs capability to reference a local file for body content (for example body_file) while preserving the existing safety model.

Background / problem

Current pattern for large updates:

  1. Read issue/PR/discussion body
  2. Write to local file
  3. Edit surgically
  4. Emit safe output

Step 4 currently requires the entire body inline, even when the authoritative edited content is already on disk.

Proposed shape (example)

{
  "type": "update_discussion",
  "discussion_number": 123,
  "body_file": "gh-aw-safe/body.md",
  "body_sha256": "<digest>"
}

Apply to body-bearing operations where appropriate (e.g. update_discussion, update_issue, update_pull_request, add_comment, etc.).

Adversarial review / threat model

A naive implementation is unsafe. Main risks:

  • policy bypass if file-based bodies skip current validators/sanitizers
  • TOCTOU mismatch between reviewed content and posted content
  • path exfiltration (pointing at unintended files)
  • reduced auditability/provenance compared with inline payloads
  • non-deterministic replay/staged behavior with mutable workspace state
  • prompt-injection pivot to attacker-controlled file paths
  • resource abuse via huge/binary files

Required guardrails

  1. Opt-in only (disabled by default)
  2. Strict path allowlist (for example under $RUNNER_TEMP/gh-aw-safe/ only)
  3. Reject traversal/symlink escapes
  4. Text-only with hard size limits
  5. body_sha256 required + verified
  6. Atomic single read/snapshot at safe-output execution
  7. Pass the exact same validation/sanitization pipeline used by inline bodies
  8. Emit audit metadata (path, digest, bytes) without leaking content by default
  9. Same deterministic behavior in staged/non-staged modes

Non-goals

  • arbitrary file references across workspace
  • bypassing existing safe-output content policy
  • changing cross-run persistence semantics

Agentic implementation plan

  1. Schema + parser updates

    • Add optional body_file / body_sha256 fields to relevant output item schemas.
    • Enforce mutual exclusivity/precedence rules between body and body_file.
  2. Path resolution hardening

    • Add a resolver that canonicalizes and enforces allowlisted root(s).
    • Block traversal and symlink escapes.
  3. Content ingestion layer

    • Read file once, compute SHA-256, verify exact digest match.
    • Enforce text-only and max-size checks before further processing.
  4. Unify policy pipeline

    • Route file-backed content into the existing body sanitizer/validator path (URL rules, mention/reference handling, footer/disclosure behavior, length constraints).
  5. Execution + observability

    • Ensure handlers use the frozen in-memory snapshot only.
    • Emit structured audit fields for file source metadata.
  6. Docs + examples

    • Update safe-outputs docs with examples and explicit security constraints.
    • Clarify recommended workflow pattern for surgical edits.
  7. Tests

    • Happy path: valid allowlisted file + digest.
    • Failure cases: digest mismatch, binary file, oversize, disallowed path, symlink escape, both body and body_file invalid combinations.
    • Verify parity with inline-body sanitization behavior.

Acceptance criteria

  • MVP support for at least one update operation using body_file
  • All guardrails above enforced
  • Inline and file-backed paths share one sanitization/validation pipeline
  • Clear compile/runtime errors for invalid references
  • Documentation includes threat model and secure usage guidance

Activity

  1. locked and limited conversation to collaborators on Jul 11, 2026
  2. unlocked this conversation on Jul 11, 2026
  3. straub commented on Jul 17, 2026

    @straub
    Author

    Workaround that worked reliably for us (without re-embedding the full markdown body in model context):

    jq -Rs --arg discussion_number "$DISCUSSION_NUMBER" \
      '{discussion_number: ($discussion_number|tonumber), body: .}' \
      discussion-body.md \
      | safeoutputs update_discussion .

    Why this helps: piping raw markdown directly (for example cat discussion-body.md | safeoutputs update_discussion ...) did not populate the body field in our run, and safe-outputs rejected it with:

    update_discussion requires at least one of: 'title', 'body', 'labels' fields

    Using jq -Rs makes the payload explicit JSON, so body is always present while still reading from file.

  4. locked and limited conversation to collaborators on Jul 17, 2026
  5. unlocked this conversation on Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions