name: "Aegis AI Agent Security Gate"
description: "Scan, govern, and audit AI agent actions in CI/CD. Policy enforcement, selection governance, prompt injection detection, and governance scoring — the Terraform plan for AI agent security."
author: "Acacian"
branding:
  icon: "shield"
  color: "blue"

inputs:
  command:
    description: "Command to run: scan, score, validate, plan, test, or all (default runs scan+validate+score)"
    required: false
    default: "all"
  policy-file:
    description: "Path to policy YAML file (for score/validate/test)"
    required: false
    default: "policy.yaml"
  scan-directory:
    description: "Directory to scan for ungoverned AI calls"
    required: false
    default: "."
  min-score:
    description: "Minimum governance score to pass (0-100)"
    required: false
    default: "0"
  fail-on-ungoverned:
    description: "Fail if ungoverned AI calls are found"
    required: false
    default: "false"
  python-version:
    description: "Python version to use"
    required: false
    default: "3.11"
  # --- plan command inputs ---
  old-policy:
    description: "Path to current policy YAML (for plan command)"
    required: false
    default: ""
  new-policy:
    description: "Path to proposed policy YAML (for plan command)"
    required: false
    default: ""
  audit-db:
    description: "Path to SQLite audit database for plan replay"
    required: false
    default: ""
  replay:
    description: "Path to JSONL file of recorded actions for plan replay"
    required: false
    default: ""
  # --- test command inputs ---
  test-suite:
    description: "Path to test suite YAML (for test command)"
    required: false
    default: ""
  regression-policy:
    description: "Path to old policy for test --regression comparison"
    required: false
    default: ""

outputs:
  score:
    description: "Governance score (0-100)"
    value: ${{ steps.score.outputs.score }}
  grade:
    description: "Governance grade (A+, A, B, C, D, F)"
    value: ${{ steps.score.outputs.grade }}
  ungoverned-count:
    description: "Number of ungoverned AI calls found"
    value: ${{ steps.scan.outputs.ungoverned_count }}
  badge-url:
    description: "Shields.io badge URL for governance score"
    value: ${{ steps.score.outputs.badge_url }}
  plan-newly-blocked:
    description: "Number of actions newly blocked by policy change (plan command)"
    value: ${{ steps.plan.outputs.newly_blocked }}
  test-passed:
    description: "Number of tests passed (test command)"
    value: ${{ steps.test.outputs.passed }}
  test-failed:
    description: "Number of tests failed (test command)"
    value: ${{ steps.test.outputs.failed }}
  test-total:
    description: "Total number of tests (test command)"
    value: ${{ steps.test.outputs.total }}

runs:
  using: "composite"
  steps:
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: ${{ inputs.python-version }}

    - name: Install Aegis
      shell: bash
      run: pip install agent-aegis

    - name: Run aegis scan
      id: scan
      if: ${{ inputs.command == 'scan' || inputs.command == 'all' }}
      shell: bash
      run: |
        echo "::group::Aegis Scan — Detecting ungoverned AI calls"
        set +e
        output=$(aegis scan ${{ inputs.scan-directory }} 2>&1)
        exit_code=$?
        echo "$output"
        echo "::endgroup::"

        # Extract ungoverned count from output
        count=$(echo "$output" | grep -oP '\d+(?= ungoverned)' || echo "0")
        echo "ungoverned_count=${count}" >> "$GITHUB_OUTPUT"

        if [ "${{ inputs.fail-on-ungoverned }}" = "true" ] && [ "$exit_code" -ne 0 ]; then
          echo "::error::Found ungoverned AI tool calls. Run 'aegis scan' locally to fix."
          exit 1
        fi

    - name: Run aegis validate
      if: ${{ (inputs.command == 'validate' || inputs.command == 'all') && inputs.policy-file != '' }}
      shell: bash
      run: |
        if [ -f "${{ inputs.policy-file }}" ]; then
          echo "::group::Aegis Validate"
          aegis validate ${{ inputs.policy-file }}
          echo "::endgroup::"
        else
          echo "::notice::No policy file found at ${{ inputs.policy-file }}, skipping validation"
        fi

    - name: Run aegis score
      id: score
      if: ${{ (inputs.command == 'score' || inputs.command == 'all') && inputs.policy-file != '' }}
      shell: bash
      run: |
        if [ ! -f "${{ inputs.policy-file }}" ]; then
          echo "score=0" >> "$GITHUB_OUTPUT"
          echo "grade=F" >> "$GITHUB_OUTPUT"
          echo "badge_url=" >> "$GITHUB_OUTPUT"
          echo "::notice::No policy file found, score=0"
          exit 0
        fi

        echo "::group::Aegis Score"
        json=$(aegis score ${{ inputs.policy-file }} --format json)
        echo "$json" | python3 -c "
        import sys, json
        d = json.load(sys.stdin)
        print(f\"Score: {d['score']}/100 (Grade: {d['grade']})\")
        "
        echo "::endgroup::"

        score=$(echo "$json" | python3 -c "import sys,json; print(json.load(sys.stdin)['score'])")
        grade=$(echo "$json" | python3 -c "import sys,json; print(json.load(sys.stdin)['grade'])")
        badge=$(echo "$json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('badge_url',''))")

        echo "score=${score}" >> "$GITHUB_OUTPUT"
        echo "grade=${grade}" >> "$GITHUB_OUTPUT"
        echo "badge_url=${badge}" >> "$GITHUB_OUTPUT"

        if [ "${{ inputs.min-score }}" -gt 0 ] && [ "${score}" -lt "${{ inputs.min-score }}" ]; then
          echo "::error::Governance score ${score} is below minimum ${{ inputs.min-score }}"
          exit 1
        fi

    - name: Run aegis plan
      id: plan
      if: ${{ inputs.command == 'plan' }}
      shell: bash
      run: |
        # Validate required inputs
        if [ -z "${{ inputs.old-policy }}" ] || [ -z "${{ inputs.new-policy }}" ]; then
          echo "::error::plan command requires old-policy and new-policy inputs"
          exit 1
        fi

        if [ ! -f "${{ inputs.old-policy }}" ]; then
          echo "::error::Old policy file not found: ${{ inputs.old-policy }}"
          exit 1
        fi

        if [ ! -f "${{ inputs.new-policy }}" ]; then
          echo "::error::New policy file not found: ${{ inputs.new-policy }}"
          exit 1
        fi

        # Build command
        cmd="aegis plan ${{ inputs.old-policy }} ${{ inputs.new-policy }} --ci --format json"

        if [ -n "${{ inputs.audit-db }}" ]; then
          if [ ! -f "${{ inputs.audit-db }}" ]; then
            echo "::error::Audit database not found: ${{ inputs.audit-db }}"
            exit 1
          fi
          cmd="$cmd --audit-db ${{ inputs.audit-db }}"
        fi

        if [ -n "${{ inputs.replay }}" ]; then
          if [ ! -f "${{ inputs.replay }}" ]; then
            echo "::error::Replay file not found: ${{ inputs.replay }}"
            exit 1
          fi
          cmd="$cmd --replay ${{ inputs.replay }}"
        fi

        echo "::group::Aegis Plan — Policy impact analysis"
        set +e
        output=$(eval "$cmd" 2>&1)
        exit_code=$?
        echo "$output"
        echo "::endgroup::"

        # Extract newly_blocked count from JSON output
        newly_blocked=$(echo "$output" | python3 -c "
        import sys, json
        try:
            d = json.load(sys.stdin)
            replay = d.get('replay', {})
            print(replay.get('newly_blocked', 0))
        except Exception:
            print(0)
        " 2>/dev/null || echo "0")

        echo "newly_blocked=${newly_blocked}" >> "$GITHUB_OUTPUT"

        if [ "$exit_code" -ne 0 ]; then
          echo "::error::Policy change would newly block ${newly_blocked} action(s). Review the plan output above."
          exit 1
        fi

    - name: Run aegis test
      id: test
      if: ${{ inputs.command == 'test' }}
      shell: bash
      run: |
        # Validate required inputs
        if [ -z "${{ inputs.test-suite }}" ]; then
          echo "::error::test command requires test-suite input"
          exit 1
        fi

        if [ ! -f "${{ inputs.policy-file }}" ]; then
          echo "::error::Policy file not found: ${{ inputs.policy-file }}"
          exit 1
        fi

        if [ ! -f "${{ inputs.test-suite }}" ]; then
          echo "::error::Test suite not found: ${{ inputs.test-suite }}"
          exit 1
        fi

        # Build command
        cmd="aegis test ${{ inputs.policy-file }} ${{ inputs.test-suite }} --format json"

        if [ -n "${{ inputs.regression-policy }}" ]; then
          if [ ! -f "${{ inputs.regression-policy }}" ]; then
            echo "::error::Regression policy not found: ${{ inputs.regression-policy }}"
            exit 1
          fi
          cmd="$cmd --regression ${{ inputs.regression-policy }}"
        fi

        echo "::group::Aegis Test — Policy regression tests"
        set +e
        output=$(eval "$cmd" 2>&1)
        exit_code=$?
        echo "$output"
        echo "::endgroup::"

        # Extract test counts from JSON output
        passed=$(echo "$output" | python3 -c "
        import sys, json
        try:
            d = json.load(sys.stdin)
            print(d.get('passed', 0))
        except Exception:
            print(0)
        " 2>/dev/null || echo "0")

        failed=$(echo "$output" | python3 -c "
        import sys, json
        try:
            d = json.load(sys.stdin)
            print(d.get('failed', 0))
        except Exception:
            print(0)
        " 2>/dev/null || echo "0")

        total=$(echo "$output" | python3 -c "
        import sys, json
        try:
            d = json.load(sys.stdin)
            print(d.get('total', 0))
        except Exception:
            print(0)
        " 2>/dev/null || echo "0")

        echo "passed=${passed}" >> "$GITHUB_OUTPUT"
        echo "failed=${failed}" >> "$GITHUB_OUTPUT"
        echo "total=${total}" >> "$GITHUB_OUTPUT"

        if [ "$exit_code" -ne 0 ]; then
          echo "::error::Policy tests failed (${failed}/${total}). See test output above for details."
          exit 1
        fi
