DEV Community

Cover image for Test AI Coding-Agent Permissions in GitHub Actions with APort
Uchi Uchibeke for APort

Posted on

Test AI Coding-Agent Permissions in GitHub Actions with APort

A coding agent can open a pull request without having permission to merge it. Those are separate decisions. Your CI configuration should make the difference visible.

This walkthrough adds APort Repository Guard to a test repository, starts in report-only mode, then checks whether a harmless change to a protected path stops a merge. No jailbreak prompt or model subscription is needed.

We maintain APort. The exercise is deliberately small: one repository, one required check, and two pull requests whose outcomes you can inspect. It does not establish that a repository or an agent is safe.

What is being checked?

The Open Agent Passport (OAP) describes an agent's identity, capabilities and limits. An authorization check evaluates a proposed action against that permission set. Authentication answers who is making the request; authorization answers whether that request is permitted.

For a runtime integration, pre-action authorization means checking before an integrated tool executes. For this GitHub exercise, the enforcement point is different: a required status check can gate a future merge. It cannot undo a pull request that already exists or a push that has already happened.

Repository Guard gathers repository evidence and checks it through the OAP code.repository.merge.v1 policy. It also reports structural findings, such as changes to protected paths. These are distinct signals: an attribution hint about a coding agent is not proof of its identity, and a protected-path finding is not a malware verdict.

Before you start

Use a repository you own and can afford to discard. Do not start on a production repository, use customer data, or add credentials to a test pull request.

You need:

  • Git and Bash, plus Node.js with npm. The installer declares Node 18 or later; use a currently supported Node release. On Windows, use WSL or another supported Bash environment.
  • A GitHub repository with Actions enabled and permission to add a workflow. To test merge enforcement, you also need permission to configure a ruleset or branch protection. Availability depends on the repository and GitHub plan.
  • An initialized default branch. This example uses main; substitute your actual branch name where relevant.

The CLI below is pinned to the published @aporthq/aport-agent-guardrails@1.0.33 package. The generated workflow uses aporthq/policy-verify-action@v1, a moving major-version tag. Review the Action and pin its full commit SHA if your dependency policy requires an immutable reference.

The default hosted path uses GitHub OpenID Connect (OIDC). It does not require you to create an APort API key. It does make requests to APort using workflow identity and repository context; read the Action source and inputs before enabling it on private code.

1. Generate a report-only workflow

From the root of your test repository, create a setup branch and run:

git switch -c add-aport-guard

npx @aporthq/aport-agent-guardrails@1.0.33 github \
  --protected-paths "docs/aport-deny-test.txt"
Enter fullscreen mode Exit fullscreen mode

The installer creates .github/workflows/aport-guard.yml. It does not enable branch protection, push the file, or install an agent runtime hook. Existing workflow files are left unchanged unless you explicitly request an overwrite. If one already exists, inspect it rather than assuming these options changed it.

docs/aport-deny-test.txt is our harmless tripwire. The path is extra protection for this exercise, not an assertion that a text file is dangerous. The Action also has built-in protected paths for workflow and policy configuration.

Review the generated workflow:

git status --short
sed -n '1,160p' .github/workflows/aport-guard.yml
Enter fullscreen mode Exit fullscreen mode

Its permissions include:

permissions:
  id-token: write
  contents: read
  pull-requests: read
Enter fullscreen mode Exit fullscreen mode

id-token: write permits the workflow to request an OIDC token. It does not grant permission to write repository contents. The generated job does not need to check out and execute code from the pull request.

Its Action step should include:

- name: APort repository guard
  uses: aporthq/policy-verify-action@v1
  with:
    mode: auto
    protected-paths: >-
      docs/aport-deny-test.txt
Enter fullscreen mode Exit fullscreen mode

These are excerpts, not replacements for the complete generated workflow. Keep its event triggers and job definition.

Commit the reviewed workflow and open a pull request using your normal process. These steps change your repository, so run them only in the test repository you chose.

2. Read the result before trusting the check mark

Open the workflow run in GitHub Actions and read its job summary. Look for the mode, attribution evidence, structural findings, and verification result. A hosted decision should have its own outcome and decision ID.

For this no-key setup, mode: auto is report-only. It attempts hosted OIDC verification and can fall back to labelled evidence-only reporting when hosted verification is unavailable. A green job can therefore contain findings or lack a hosted authorization decision.

Do not treat those cases as equivalent:

Observation What you can conclude
A valid hosted decision says allow The supplied action context passed the configured authorization checks.
A decision says deny, but the report-only job is green A restriction was found; this rollout mode is not enforcing it.
The summary says evidence-only or reports unavailable verification You have repository findings, not a successful hosted authorization check.
Attribution says coding agent The classifier found attribution signals. Inspect their source and confidence; this alone is not authorization.

Get this baseline working before changing merge rules. OIDC permissions, repository Actions settings and missing evidence can all matter. If the expected hosted decision is absent, investigate the summary instead of counting the run as a successful authorization test.

3. Turn the check into a merge requirement

After reviewing the baseline, change the existing Action inputs to:

with:
  mode: hosted
  protected-paths: >-
    docs/aport-deny-test.txt
  block-protected-paths: true
Enter fullscreen mode Exit fullscreen mode

Keep the rest of the workflow. In explicit hosted mode, the job fails when verification cannot return a valid signed decision, the decision denies the action, or a structural finding has blocking severity. The last input makes our protected-path test blocking.

Configuration changes themselves touch protected paths and can produce a denial. Review this setup through your repository's approved configuration-change process before making its check required. Do not copy a bypass label or weaken an existing production rule to get through the tutorial.

In GitHub's ruleset or branch-protection settings for main, require pull requests and require the generated job's status check, APort / OAP code.repository.merge.v1. Select the exact check name from an actual run. Review who can bypass the rules. A red optional check does not stop a maintainer from merging.

GitHub's protected-branch documentation explains required checks and bypass behavior. Keep your existing tests, scanners and human-review requirements in place.

The workflow also watches pushes. A failed push-event job reports what already happened; it is not a server-side pre-push hook. Use GitHub's own restrictions to prevent direct pushes to the protected branch.

4. Try two harmless pull requests

Create each PR from the configured default branch, after the setup changes are present there. Do not modify the guard in either test PR.

Baseline PR: add one sentence to README.md. Satisfy the repository's other approval and policy requirements. The expected result is no protected-path finding from this edit, a valid hosted allow decision, and a passing required check. If it is denied, inspect the specific reason. Fix the setup before interpreting the second test.

Protected-path PR: add docs/aport-deny-test.txt containing only:

This file is a harmless protected-path test.
Enter fullscreen mode Exit fullscreen mode

The expected structural finding is OAP.REPO.PROTECTED_PATH_TOUCHED. With the blocking input enabled and hosted mode selected, the check should fail. Confirm that GitHub prevents the merge for a user subject to the rules. Close this PR without merging it.

The filename matters, not the sentence. You are testing whether the configured path check and merge requirement connect, without introducing an exploit or an unsafe command.

Keep the run URLs and observed results. A successful test needs both the allowed baseline and the blocked test case. Two failed checks could just mean the verifier is unavailable; two green checks could mean you are still in report-only mode.

Afterward, use the same reviewed configuration-change process to replace the tutorial path with the paths your repository actually needs to protect. Plan how legitimate workflow and policy updates will receive review before enforcing those paths broadly.

Where runtime guardrails fit

The exercise above gates repository integration. It does not intercept the coding agent's local shell commands, file access, or network requests.

For an action to be checked before the agent's tool runs, the agent runtime must invoke a supported hook or provider. The APort Agent Guardrails documentation lists the integrations and their status. Follow the document for the specific runtime you use; installation steps and coverage differ.

A trusted check still needs a policy worth enforcing. Broad permissions can allow harmful actions, a runtime that can bypass its hook is outside that boundary, and an allowed change can still contain a bug. Keep credential scoping, code review and dedicated security scanners.

Why we separate requests from execution

Our payment research makes the same measurement distinction, in a different domain. Paper 1, Section 6.1 documents cases where a model generated a forbidden payment call and the policy engine denied it. The model making a request and the request executing were separate events.

Paper 2, APort Vault, replays 4,371 human-authored attacks across 14 models in a simulated bank. It contains 225,964 completed evaluations, not that many independent attacks. At Levels 2 to 4, where the passport permits some recipients and excludes others:

Evaluations containing a registered unpermitted transfer Model alone Behind the layer
All completed coverage at Levels 2 to 4 140 / 76,842 0 / 69,297
Matched model, prompt and replay track 105 / 68,970 0 / 68,970

Behind the layer, 25,370 of 69,297 evaluations at these levels still contained a successful payment. The zero was not achieved by refusing all payments. It spans 790 source sessions; the paper reports an approximate one-sided 95% upper bound of 0.38% per source session. That is uncertainty around the observed zero, not a guarantee.

A payment request alone does not establish compromise, and an allowlisted recipient does not prove legitimate intent. Single-turn coverage is complete for all fourteen models; multi-turn is complete for nine and partial for five. These are simulated-payment results, not measurements of GitHub protection or production losses.

The dataset and analysis scripts let readers examine the recorded outcomes and coverage. Dataset access requires accepting its terms. The papers explain the release limits and the endpoint being counted.

For your own agent, record the stages separately: what it proposed, what the check decided, and what actually executed. For a GitHub rollout, also record whether the check was required. Otherwise, a green CI badge can hide the difference between observing a denial and enforcing one.

Try the two-PR exercise in a repository you own. Which permission would you want to test next?

Disclosure: this tutorial was prepared with AI assistance from APort's source code, documentation and published preprints.

Top comments (0)