Skip to main content
CI/CD pipelines can use AI agents to review pull requests, generate tests, and validate code changes automatically. Nullspace sandboxes provide the isolated Linux environment where automation can clone pull request code, install dependencies, run untrusted tests, and report results without executing the PR branch on the GitHub Actions runner. This pattern keeps the runner responsible for trusted orchestration and PR comments. The PR branch is fetched and tested only inside a disposable Nullspace sandbox.

GitHub Actions workflow

The workflow runs from the trusted base branch with pull_request_target, then checks out the base commit so the review script itself cannot be replaced by the pull request. Store NULLSPACE_API_KEY, NULLSPACE_API_URL, and OPENAI_API_KEY as GitHub Actions secrets. For private repositories, add a separate read-only GIT_READ_TOKEN secret for sandbox-side Git clone and fetch operations.
name: Nullspace AI Review

on:
  pull_request_target:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: read
  issues: write

jobs:
  ai-review:
    runs-on: ubuntu-latest
    steps:
      - name: Check out trusted workflow code
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.base.sha }}

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: python -m pip install "nullspace-sdk==0.1.9" openai requests

      - name: Run AI review in Nullspace
        env:
          NULLSPACE_API_KEY: ${{ secrets.NULLSPACE_API_KEY }}
          NULLSPACE_API_URL: ${{ secrets.NULLSPACE_API_URL }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          OPENAI_MODEL: gpt-5.2-mini
          GITHUB_TOKEN: ${{ github.token }}
          GIT_READ_TOKEN: ${{ secrets.GIT_READ_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
          NULLSPACE_TEMPLATE: base
          INSTALL_COMMAND: python3 -m pip install -e .
          TEST_COMMAND: python3 -m pytest
        run: python .github/scripts/nullspace_ai_review.py

Review script

The workflow calls this script on every pull request. It creates a Nullspace sandbox, fetches the PR branch inside the sandbox, sends the diff to an LLM, runs your test command, and posts one PR comment with the review and validation result.
import os
import shlex
import sys

import requests
from nullspace import GitHttpsAuth, Sandbox
from openai import OpenAI

WORKDIR = "/workspace/repo"


def required(name: str) -> str:
    value = os.environ.get(name, "").strip()
    if not value:
        raise RuntimeError(f"{name} is required")
    return value


def run_checked(
    sandbox: Sandbox,
    command: str,
    *,
    cwd: str = WORKDIR,
    timeout: int = 300,
    stream: bool = True,
):
    result = sandbox.commands.run(
        command,
        shell=True,
        cwd=cwd,
        timeout=timeout,
        envs={"PYTHONUNBUFFERED": "1"},
        on_stdout=(lambda data: print(data, end="")) if stream else None,
        on_stderr=(lambda data: print(data, end="", file=sys.stderr)) if stream else None,
    )
    if result.exit_code != 0:
        details = result.stderr.strip() or result.stdout.strip()
        raise RuntimeError(f"command failed ({result.exit_code}): {command}\n{details}")
    return result


def review_diff(diff: str) -> str:
    if not diff.strip():
        return "No diff was detected for this pull request."

    max_chars = int(os.environ.get("MAX_DIFF_CHARS", "120000"))
    if len(diff) > max_chars:
        diff = diff[:max_chars] + "\n\n[Diff truncated before review.]"

    client = OpenAI()
    response = client.chat.completions.create(
        model=os.environ.get("OPENAI_MODEL", "gpt-5.2-mini"),
        messages=[
            {
                "role": "system",
                "content": (
                    "You are a senior code reviewer. Provide concise, actionable "
                    "feedback. Focus on bugs, security issues, regressions, and "
                    "missing tests."
                ),
            },
            {"role": "user", "content": f"Review this diff:\n\n{diff}"},
        ],
    )
    return response.choices[0].message.content or "No review content returned."


def post_comment(body: str) -> None:
    repo = required("GITHUB_REPOSITORY")
    pr_number = required("PR_NUMBER")
    response = requests.post(
        f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
        headers={
            "Authorization": f"Bearer {required('GITHUB_TOKEN')}",
            "Accept": "application/vnd.github+json",
        },
        json={"body": body},
        timeout=30,
    )
    response.raise_for_status()


def main() -> int:
    repo = required("GITHUB_REPOSITORY")
    pr_number = required("PR_NUMBER")
    base_ref = required("PR_BASE_REF")
    template = os.environ.get("NULLSPACE_TEMPLATE", "base")
    sandbox_timeout = int(os.environ.get("NULLSPACE_SANDBOX_TIMEOUT", "900"))

    repo_url = f"https://github.com/{repo}.git"
    pr_branch = f"pr-{pr_number}"
    git_read_token = os.environ.get("GIT_READ_TOKEN", "").strip()
    auth = (
        GitHttpsAuth(username="x-access-token", password=git_read_token)
        if git_read_token
        else None
    )

    test_summary = "Validation did not run."
    exit_status = 0

    with Sandbox.create(template=template, timeout=sandbox_timeout) as sandbox:
        print(f"Sandbox created: {sandbox.id}")

        if auth is not None:
            sandbox.git.dangerously_authenticate("x-access-token", git_read_token)

        sandbox.git.clone(repo_url, path=WORKDIR, branch=base_ref, depth=1, auth=auth)
        run_checked(
            sandbox,
            f"git fetch origin {shlex.quote(f'pull/{pr_number}/head:{pr_branch}')} --depth 100",
            timeout=120,
        )
        run_checked(sandbox, f"git checkout {shlex.quote(pr_branch)}", timeout=60)

        diff_result = run_checked(
            sandbox,
            f"git diff --no-ext-diff --unified=80 {shlex.quote(f'origin/{base_ref}')}..HEAD",
            timeout=120,
            stream=False,
        )
        review = review_diff(diff_result.stdout)

        try:
            install_command = os.environ.get("INSTALL_COMMAND", "").strip()
            if install_command:
                run_checked(sandbox, install_command, timeout=600)

            test_command = required("TEST_COMMAND")
            test_result = run_checked(sandbox, test_command, timeout=600)
            test_summary = (
                f"Passed `{test_command}` with exit code {test_result.exit_code}."
            )
        except RuntimeError as err:
            test_summary = f"Failed validation:\n\n{str(err)[-3500:]}"
            exit_status = 1

    post_comment(
        f"## Nullspace AI Code Review\n\n{review}\n\n"
        f"## Sandbox Validation\n\n{test_summary}"
    )
    return exit_status


if __name__ == "__main__":
    sys.exit(main())

How it works

  1. Run trusted orchestration — the GitHub runner checks out the base commit, installs the Nullspace SDK, and runs the review script from trusted code.
  2. Create sandboxSandbox.create() starts a disposable Nullspace VM for the pull request.
  3. Fetch the PRsandbox.git.clone() checks out the base branch, then a sandbox-local git fetch loads refs/pull/<number>/head.
  4. AI reviewcommands.run() generates a diff inside the sandbox and the runner sends it to an LLM.
  5. Run validation — the script runs INSTALL_COMMAND and TEST_COMMAND inside the sandbox. Nullspace command results report exit_code, stdout, and stderr; non-zero exits are handled explicitly.
  6. Post results — the runner posts one PR comment through the GitHub REST API after the sandbox closes.

Security notes

  • Do not check out the PR branch on the GitHub runner when using pull_request_target.
  • Do not pass GITHUB_TOKEN, OPENAI_API_KEY, or NULLSPACE_API_KEY into the sandbox environment. The example keeps those values runner-side.
  • Leave GIT_READ_TOKEN unset for public repositories. For private repositories, use a token scoped only for read access because PR code can read any credentials made available inside the sandbox.
  • Build a custom Nullspace template for heavyweight CI dependencies such as browsers, language toolchains, or system packages.

Git helpers

Clone repositories, manage branches, inspect diffs, and push changes from sandboxes.

Commands

Run foreground, streaming, and background commands inside a sandbox.

Custom templates

Preinstall toolchains and CI dependencies for repeatable sandbox runs.

Agents

Run Codex, Claude Code, Amp, OpenCode, or custom agents in isolated workspaces.