Skip to content

microsoftgbb/safe-outputs-action

Repository files navigation

Safe Outputs Action

Security gate for AI agent outputs in GitHub Actions. Validates constraints, sanitizes secrets, and applies actions through a controlled write pipeline.

This is an initial implementation inspired by GitHub Next Agentic Workflows (gh-aw).

gh-aw's safe outputs architecture enforces a critical security principle: the AI agent never has direct write access to your repository. Instead, the agent proposes actions as structured data, and a separate gated job validates, sanitizes, and applies them. This action brings that same pattern to standard GitHub Actions workflows where gh-aw's built-in sandbox is not available -- for example, when your agent needs to read from external systems like Kubernetes clusters, Azure resources, or third-party APIs.

Why this exists

gh-aw provides an excellent security model, but its safe outputs are tightly coupled to the gh-aw runtime and cannot be used independently. If your agentic workflow needs to:

  • Consume data from external systems (cloud APIs, databases, clusters)
  • Run in a standard GitHub Actions environment
  • Use a custom agent or model not supported by gh-aw

...then you need to implement your own output gate. This action provides that gate as a reusable, configurable step.

Architecture

+-----------------------------------------+
|  Agent Job (read-only permissions)      |
|                                         |
|  AI agent analyzes data, produces       |
|  structured JSON output artifact        |
+-----------------------------------------+
          |
          v  (artifact upload/download)
+-----------------------------------------+
|  Safe Outputs Job (write permissions)   |
|                                         |
|  1. Validate constraints (limits,       |
|     title prefix, allowed labels)       |
|  2. Sanitize secrets (JWTs, keys,       |
|     connection strings, custom)         |
|  3. Apply actions via GitHub API        |
+-----------------------------------------+
          |
          v
+-----------------------------------------+
|  GitHub (issues, PRs, comments, labels) |
+-----------------------------------------+

The agent and the write job are separate GitHub Actions jobs with different permission sets. The agent job runs with minimal (ideally read-only) permissions. The write job runs with scoped write permissions but contains no AI reasoning -- it mechanically applies validated, sanitized output.

Agent output schema

The agent must produce a JSON file conforming to this schema:

{
  "version": "1",
  "actions": [
    {
      "type": "issue_comment",
      "issue_number": 42,
      "body": "## Analysis Results\n..."
    },
    {
      "type": "create_issue",
      "title": "[bot] Node pressure detected",
      "body": "Details...",
      "labels": ["bug", "auto-generated"],
      "assignees": ["octocat"]
    },
    {
      "type": "create_pull_request",
      "title": "[bot] Fix HPA configuration",
      "body": "This PR adjusts the HPA settings...",
      "head": "fix/hpa-config",
      "base": "main"
    },
    {
      "type": "add_labels",
      "issue_number": 42,
      "labels": ["triaged", "cluster-doctor"]
    }
  ]
}

Supported action types

Type Description Required fields
issue_comment Add a comment to an existing issue or PR issue_number, body
create_issue Create a new issue title, body
create_pull_request Create a PR. Optionally provide files to create the branch automatically. title, body, head
add_labels Add labels to an existing issue or PR issue_number, labels

File-based pull requests

When files is provided in a create_pull_request action, the safe-outputs action handles the full workflow:

  1. Gets the base branch HEAD SHA
  2. Creates blobs for each file via the Git Data API
  3. Creates a new tree with those blobs
  4. Creates a commit on that tree
  5. Creates (or updates) the branch ref
  6. Opens the pull request

The agent never needs write access -- all Git operations happen in the gated write job.

{
  "type": "create_pull_request",
  "title": "[cluster-doctor] Fix HPA configuration",
  "body": "Adjusted maxReplicas based on node capacity analysis.",
  "head": "fix/hpa-config",
  "base": "main",
  "files": {
    "k8s/hpa.yaml": "apiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: my-app\nspec:\n  maxReplicas: 10",
    "docs/changes.md": "# Changes\n\nUpdated HPA max replicas to 10."
  },
  "commit_message": "fix: adjust HPA max replicas based on cluster capacity"
}
Field Required Description
files No Map of file path to content. When provided, branch is created automatically.
commit_message No Commit message for the file commit. Defaults to the PR title.

File contents are sanitized for secrets just like all other string fields.

Usage

Basic example

jobs:
  analyze:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4

      # Your agent step - produces output JSON
      - name: Run AI agent
        run: |
          # Agent writes its proposed actions to a JSON file
          copilot -p "Analyze this repo and propose improvements" \
            --output agent-output.json

      - uses: actions/upload-artifact@v4
        with:
          name: agent-output
          path: agent-output.json

  safe-outputs:
    needs: analyze
    runs-on: ubuntu-latest
    permissions:
      issues: write
      pull-requests: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: agent-output

      - uses: microsoftgbb/safe-outputs-action@v0
        with:
          artifact-path: agent-output.json
          max-issues: 1
          max-comments: 3
          title-prefix: '[bot] '
          allowed-labels: 'bug,auto-generated,enhancement'

With external data gathering (cluster diagnostics)

This is the primary use case -- an agent that needs access to external systems:

jobs:
  gather-diagnostics:
    runs-on: ubuntu-latest
    permissions:
      id-token: write # Azure OIDC
      contents: read
    steps:
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.ARM_CLIENT_ID }}
          tenant-id: ${{ secrets.ARM_TENANT_ID }}
          subscription-id: ${{ secrets.ARM_SUBSCRIPTION_ID }}

      - name: Collect cluster data
        run: |
          az aks get-credentials --resource-group $RG --name $CLUSTER
          kubectl get events -A -o json > diagnostics/events.json
          kubectl get pods -A -o json > diagnostics/pods.json
          kubectl top nodes -o json > diagnostics/nodes.json

      - uses: actions/upload-artifact@v4
        with:
          name: diagnostics
          path: diagnostics/

  analyze:
    needs: gather-diagnostics
    runs-on: ubuntu-latest
    permissions:
      contents: read # Read-only -- no cloud creds, no write token
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: diagnostics
          path: diagnostics/

      - name: AI analysis
        env:
          GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }}
        run: |
          copilot -p "Analyze the K8s diagnostics in diagnostics/ and produce
            a JSON report following the safe-outputs schema. Write to output.json" \
            --agent "cluster-doctor"

      - uses: actions/upload-artifact@v4
        with:
          name: agent-output
          path: output.json

  apply:
    needs: analyze
    runs-on: ubuntu-latest
    permissions:
      issues: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: agent-output

      - uses: microsoftgbb/safe-outputs-action@v0
        with:
          artifact-path: output.json
          title-prefix: '[cluster-doctor] '
          allowed-labels: 'cluster-doctor,bug,investigation'
          custom-secret-patterns: |
            10\.0\.\d+\.\d+
            aks-[a-z0-9]{8,}

Dry run mode

Validate and sanitize without applying -- useful for testing:

- uses: microsoftgbb/safe-outputs-action@v0
  with:
    artifact-path: output.json
    dry-run: true

Strict mode (fail on secrets)

Fail the workflow if the agent output contains any sensitive data, instead of redacting and proceeding:

- uses: microsoftgbb/safe-outputs-action@v0
  with:
    artifact-path: output.json
    fail-on-sanitize: true

Inputs

Input Description Required Default
artifact-path Path to the agent output JSON file Yes -
max-issues Max issues the agent can create per run No 1
max-comments Max comments the agent can create per run No 3
max-pull-requests Max PRs the agent can create per run No 1
max-labels Max add-labels actions per run No 5
title-prefix Required prefix for issue/PR titles No ''
allowed-labels Comma-separated label allowlist (empty = all allowed) No ''
custom-secret-patterns Additional regex patterns, one per line No ''
dry-run Validate and sanitize without applying No false
fail-on-sanitize Fail if any content is redacted No false
token GitHub token for write operations No ${{ github.token }}

Outputs

Output Description
applied-count Number of actions successfully applied
blocked-count Number of actions blocked by constraints
sanitized-count Number of fields with redacted content
summary JSON summary of all phases

Built-in secret patterns

The sanitizer scans for these patterns by default:

  • JWTs -- eyJ... header.payload.signature format
  • Azure connection strings -- DefaultEndpointsProtocol=...
  • Azure SAS tokens -- URL query parameters with signature components
  • AWS access keys -- AKIA..., ASIA... prefixes
  • GitHub tokens -- ghp_, gho_, ghs_, ghu_, ghr_ prefixes
  • Private key blocks -- PEM-encoded private keys
  • Bearer tokens -- Bearer <token> patterns
  • Generic key=value secrets -- password=, secret:, api_key=, etc.
  • Hex-encoded tokens -- Long hex strings associated with secret-like key names

Add domain-specific patterns (e.g., internal IPs, cluster names) via the custom-secret-patterns input.

AI-powered threat detection

When threat-detection: true is set and the Copilot CLI is installed on the runner, the action runs an AI-powered scan of the agent's proposed output. This catches threats that regex-based sanitization misses:

  • Prompt injection -- content designed to manipulate downstream LLMs
  • Encoded credentials -- base64-wrapped secrets, obfuscated tokens
  • Malicious code -- backdoors, data exfiltration, CI/CD weakening
  • Social engineering -- issue/PR content designed to trick human reviewers
- uses: microsoftgbb/safe-outputs-action@v0
  with:
    artifact-path: output.json
    threat-detection: true
  env:
    GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }}

If the Copilot CLI is not available on the runner, threat detection is silently skipped and the action proceeds with regex-based sanitization only.

The pipeline with threat detection enabled:

Validate constraints -> Sanitize secrets -> AI threat scan -> Apply actions

Composing with agent-sandbox-action

This action provides the output gate: it validates constraints, sanitizes secrets, runs optional AI threat detection, and applies the agent's proposed actions through a controlled write pipeline. However, it does not control what the agent can access while it runs (network, filesystem, environment).

For full defense-in-depth, pair it with agent-sandbox-action, which provides input containment: the agent runs inside a Docker container on an isolated network with a Squid proxy enforcing a domain allowlist.

Together the two actions cover both halves of the security model:

Layer Action What it guards
Input containment agent-sandbox-action Network access, filesystem, environment
Output gate safe-outputs-action Comments, PRs, file writes, secret leakage

Three-job pipeline example

# Full defense-in-depth pipeline for AI agents
# Combines agent-sandbox-action (input containment) with
# safe-outputs-action (output gate)

jobs:
  diagnose:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    env:
      AGENT_ALLOWED_DOMAINS: |
        api.githubcopilot.com
        api.github.com
        .azmk8s.io
        login.microsoftonline.com
    steps:
      - uses: actions/checkout@v5

      - uses: microsoftgbb/agent-sandbox-action@v1
        with:
          command: |
            copilot -p "Analyze the cluster diagnostics and produce
              findings as agent-output.json following the safe-outputs
              schema" --agent cluster-doctor --allow-all-tools
          allowed-domains: ${{ env.AGENT_ALLOWED_DOMAINS }}
          env-vars: |
            GITHUB_TOKEN=${{ secrets.COPILOT_CLI_TOKEN }}
          extra-mounts: |
            ${{ env.HOME }}/.kube/config:/home/agent/.kube/config:ro

      - uses: actions/upload-artifact@v4
        with:
          name: agent-output
          path: agent-output.json

  scan:
    needs: diagnose
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: agent-output

      - uses: microsoftgbb/safe-outputs-action@v1
        with:
          artifact-path: agent-output.json
          max-comments: 2
          max-pull-requests: 1
          title-prefix: "[bot] "
          threat-detection: true
          dry-run: true

      - uses: actions/upload-artifact@v4
        with:
          name: scanned-output
          path: agent-output.json
          overwrite: true

  apply:
    needs: [diagnose, scan]
    runs-on: ubuntu-latest
    permissions:
      issues: write
      contents: write
      pull-requests: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: scanned-output

      - uses: microsoftgbb/safe-outputs-action@v1
        with:
          artifact-path: agent-output.json
          max-comments: 2
          max-pull-requests: 1
          title-prefix: "[bot] "

Job 1 - diagnose: Runs the AI agent inside the sandbox with network access limited to the domain allowlist. The agent produces agent-output.json.

Job 2 - scan: Downloads the artifact and runs safe-outputs-action in dry-run mode. This validates constraints, sanitizes secrets, and (optionally) runs AI threat detection - without writing anything.

Job 3 - apply: If the scan passes, the validated artifact is applied: the action creates comments, PRs, or file changes on behalf of the agent.

How it compares to gh-aw safe outputs

Dimension gh-aw safe outputs This action
Integration Built into gh-aw runtime Standalone GitHub Action
Agent scope Repo-scoped only Any data source
Threat detection AI-powered scan Regex-based sanitization
Configuration Markdown frontmatter Action inputs
Network firewall Built-in (AWF) Not included (use separately)
Customization Declarative constraints Full control via inputs

This action implements the output gate portion of gh-aw's security model. For the full defense-in-depth stack, combine it with:

  • Scoped RBAC on your cloud credentials (read-only K8s ClusterRole, Azure Reader)
  • Network firewall via container networking or gh-aw's AWF
  • Separate jobs with distinct permission sets (gather / analyze / apply)

Development

npm install
npm test
npm run build

To verify the dist is up to date:

npm run build
git diff dist/

License

MIT

About

Security gate for AI agent outputs in GitHub Actions. Validates constraints, sanitizes secrets, and applies actions through a controlled pipeline. Inspired by GitHub Next Agentic Workflows (gh-aw).

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors