# GitHub Action

The `kosa8/sandbox` action runs a command in a sealed kosa8 Cloud sandbox from a workflow, prints its output and deletes the sandbox. Use it for untrusted code, an AI agent's changes, or anything you would rather not run on the CI runner itself.

### Add your key as a secret

Make a kosa8 Cloud API key with the `sandboxes:write` scope at [app.kosa8.com](https://app.kosa8.com/app#keys). In the repository, open **Settings → Secrets and variables → Actions** and add it as `KOSA8_API_KEY`.

### Use the action

```
jobs:
  sandbox:
    runs-on: ubuntu-latest
    steps:
      - id: box
        uses: kosa8/sandbox@v1
        with:
          api-key: ${{ secrets.KOSA8_API_KEY }}
          template: python
          command: python3 -c "import platform; print(platform.platform())"
      - run: echo "exit ${{ steps.box.outputs.exit-code }} in ${{ steps.box.outputs.sandbox-id }}"
```

## Inputs and outputs

| Input | Default | What it does |
|---|---|---|
| `api-key` | required | A kosa8 Cloud API key. Masked in the log. |
| `command` | required | A shell line, run as `sh -c` in the sandbox. Several lines are fine. |
| `template` | org default | `base`, `python`, `node`, or any template your organization has. |
| `timeout` | `600` | Seconds to wait for the command. The sandbox is also created with a lifetime of `timeout` + 300 s, so a cancelled job leaves nothing running. |
| `keep` | `false` | `true` keeps the sandbox (no lifetime) for later steps or for looking at afterwards; delete it yourself. |
| `fail-on-error` | `true` | Fail the step when the command exits non-zero. |
| `api-url` | `https://api.kosa8.com` | The API to call. |

| Output | Is |
|---|---|
| `exit-code` | The command's exit code. |
| `sandbox-id` | The sandbox's id, `sbx_…`. |

## How it behaves

- stdout and stderr are printed, in collapsible groups, when the command finishes. The API returns a command's output at the end today, so a long command shows nothing until it is done.

- The sandbox is deleted when the step ends, whether the command passed, failed or the step errored. If that delete fails, the step warns, and the sandbox's lifetime removes it.

- A refusal is explained in the log. For `402 payment_required` or `spending_cap_reached` the log says what to do and links [app.kosa8.com/#usage](https://app.kosa8.com/#usage).

- It is plain bash with `curl` and `jq`, both on GitHub's hosted runners: no Node.js build and nothing downloaded at run time.

- It costs what any kosa8 Cloud sandbox costs: per second while it runs ([kosa8 Cloud](https://docs.kosa8.com/guides/cloud/) has the rates).

**Keep untrusted text out of `command`.** GitHub pastes `${{ … }}` expressions into the line before it runs, so a branch name or PR title put there becomes shell code in the sandbox. Values such as `github.repository` and `github.sha` are safe.

## A full workflow

This runs a public repository's tests inside a sandbox on every push:

```
name: tests in a kosa8 sandbox
on:
  push:
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - id: box
        uses: kosa8/sandbox@v1
        with:
          api-key: ${{ secrets.KOSA8_API_KEY }}
          template: python
          timeout: "900"
          command: |
            set -e
            cd /workspace
            python3 -c "import io, tarfile, urllib.request; tarfile.open(fileobj=io.BytesIO(urllib.request.urlopen('https://codeload.github.com/${{ github.repository }}/tar.gz/${{ github.sha }}').read())).extractall('src')"
            cd src/*
            python3 -m pip install -q -r requirements.txt pytest
            python3 -m pytest -q
```

Plain HTTP calls work from any CI system too; the action is the same three calls (`POST /v1/sandboxes`, `POST /v1/sandboxes/{id}/exec`, `DELETE /v1/sandboxes/{id}`) shown in [kosa8 Cloud](https://docs.kosa8.com/guides/cloud/).

---
Source: https://docs.kosa8.com/guides/cloud-github-action/
