DEV Community

Taylor Zhu
Taylor Zhu

Posted on

Don't Ship the Sandbox: A Fail-Closed Promotion Checklist

A green agent run on free compute is not a merge decision. It is a draft produced on an untrusted host. You promote the change only after isolation, provenance, and a trusted re-run all pass. If any of those are missing, you fail closed.

Agents assume. Free sandboxes hide that. Your job is to make promotion boring, labeled, and refusible.

The merge you should refuse

You let an agent edit a service on a laptop, a shared VM, or a complimentary cloud box. Tests pass there. The PR looks complete. Then you merge from that host’s word.

That is the bug. The sandbox is not your CI. It is not your production identity provider. It is not allowed to mint a release.

Treat every agent workspace as hostile until a trusted pipeline repeats the same git SHA with your real secrets policy, your real network policy, and your real artifact store.

What “untrusted host” actually means

Untrusted does not mean “malware.” It means you cannot prove four things:

  • Who ran it. A free box may be shared, preempted, or reused.
  • What it could see. Prompt context, .env files, and cloud metadata are easy to leak.
  • Where it could send bytes. Egress is the silent failure.
  • Whether you can replay it. If you cannot re-run the SHA, you cannot defend the merge.

You do not argue with the model. You refuse the promotion path.

Copy this promotion checklist

Use it as a gate file in the repo. Empty fields fail the job. “Not applicable” is not a field. Delete a gate only when the change cannot touch prod, data, or credentials.

Gate 0 — Label the environment

Every agent job exports an environment class. No label, no artifacts.

# proposal: required env for any agent runner
AGENT_ENV_CLASS=sandbox   # sandbox | trusted_ci | prod
AGENT_RUN_ID=$(uuidgen)
AGENT_GIT_SHA=$(git rev-parse HEAD)
Enter fullscreen mode Exit fullscreen mode

Fail closed if AGENT_ENV_CLASS=prod on a developer laptop, a free VM, or any host missing your CI OIDC identity. Sandbox may propose. Only trusted_ci may attest.

Gate 1 — Data and secrets stay out of the sandbox

You do not load production dumps “just this once.” You do not mount the org’s cloud keys so the agent can “debug faster.”

Fail closed when any of these are true:

  • Production connection strings are present in the sandbox process environment.
  • The workspace can read ~/.aws, ~/.kube, or CI OIDC tokens.
  • Logs may contain live customer payloads.
  • The prompt pack includes real tickets with PII.

Replacement rule: synthetic fixtures, scrubbed snapshots, or a dedicated sandbox account with no prod IAM trust.

Gate 2 — Network and writes are allowlisted

If the agent can hit the internet, it can exfiltrate the repo. If it can write anywhere, it can persist a hook you will not review.

Minimum allowlist:

Direction Allow Deny by default
Egress Package registry, your git host, your artifact store Everything else
Writes Repo worktree, job temp dir $HOME, Docker socket, extra volumes
Reads Declared fixture paths Secret stores, sibling repos

A missing allowlist is a deny. Do not “open it for now.”

Gate 3 — Trusted re-run is mandatory

Sandbox green is a hint. Merge green is a second execution.

You pin the SHA. You rebuild. You re-run tests and scanners on trusted_ci. You compare artifact digests. If the sandbox cannot produce a digest, it cannot propose a merge.

# proposal: promotion.json (checked into the PR, not a screenshot)
{
  "git_sha": "REPLACE_WITH_FULL_SHA",
  "sandbox_run_id": "",
  "sandbox_artifact_sha256": "",
  "trusted_ci_run_id": "",
  "trusted_ci_artifact_sha256": "",
  "env_class_attested": "trusted_ci",
  "feature_flag": "agent-change-TICKET",
  "owner": "oncall-handle",
  "rollback": "gh pr revert --yes"
}
Enter fullscreen mode Exit fullscreen mode

Fail closed if sandbox_artifact_sha256 equals trusted_ci_artifact_sha256 but env_class_attested is still sandbox. Same bytes from the wrong host are not an attestation.

Gate 4 — Blast radius is numeric

“Small PR” is not a control. You cap what the change may touch.

  • Max paths changed (example: 20 files, 2 services).
  • Max IAM actions added (example: zero in sandbox-origin PRs).
  • Max regions or clusters (example: one staging namespace).
  • Feature flag default off.
  • No schema drop, no authz change, no secret rotation from an agent sandbox.

If the diff exceeds the cap, the promotion job fails. A human splits the work. The agent does not get a bigger blast radius because the model was confident.

Gate 5 — Owner and rollback exist before merge

No owner, no merge. No rollback command that has been typed once in staging, no merge.

You need a named human, a chat channel, and a revert that does not depend on the agent being available. If rollback is “ask the model to undo it,” you do not have rollback.

A fail-closed workflow you can paste

Label the following as a proposal. Wire it to your CI. Do not run it as a clever local alias that can be skipped with --no-verify.

# proposal: .github/workflows/promotion-gate.yml
name: promotion-gate
on:
  pull_request:
    types: [opened, synchronize, reopened]
jobs:
  refuse-sandbox-attestation:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Fail closed on missing promotion evidence
        env:
          AGENT_ENV_CLASS: ${{ vars.AGENT_ENV_CLASS }}
        run: bash scripts/promotion_gate.sh
Enter fullscreen mode Exit fullscreen mode
# proposal: scripts/promotion_gate.sh
set -euo pipefail
FILE="promotion.json"

if [[ ! -f "$FILE" ]]; then
  echo "fail-closed: $FILE missing"
  exit 1
fi

python3 - <<'PY'
import json, sys
required = [
    "git_sha", "sandbox_run_id", "sandbox_artifact_sha256",
    "trusted_ci_run_id", "trusted_ci_artifact_sha256",
    "env_class_attested", "feature_flag", "owner", "rollback"
]
data = json.load(open("promotion.json"))
missing = [k for k in required if not str(data.get(k, "")).strip()]
if missing:
    print("fail-closed: empty fields:", ", ".join(missing))
    sys.exit(1)
if data["env_class_attested"] != "trusted_ci":
    print("fail-closed: sandbox cannot attest a merge")
    sys.exit(1)
if data["sandbox_artifact_sha256"] == data["trusted_ci_artifact_sha256"] and not data["trusted_ci_run_id"]:
    print("fail-closed: digest copied without a trusted run id")
    sys.exit(1)
print("promotion evidence present; human review still required")
PY
Enter fullscreen mode Exit fullscreen mode

Keep the script stupid. Clever exceptions become merge holes.

Decision table: sandbox result vs merge action

Sandbox result Trusted CI result Secrets/egress gates Action
Pass Pass, same SHA Pass Human review, flag off
Pass Fail Pass Do not merge; sandbox was a false green
Fail Pass Pass Do not auto-merge; explain the delta
Pass Not run Anything Fail closed
Pass Pass Missing allowlist Fail closed
Pass on prod label from a laptop Anything Anything Fail closed, rotate credentials

The interesting row is sandbox pass / trusted fail. That is the whole reason this checklist exists. You will see it. You should want to see it.

Where a free sandbox still helps

You still want a cheap place to let an agent thrash. Draft diffs. Throwaway fixtures. Prompt iteration. That host should be labeled sandbox and stripped of prod trust.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. If you need a place to iterate without burning paid CI minutes, MonkeyCode’s free model access and free server option can sit on the sandbox side of this split. They do not replace trusted_ci. They must not write env_class_attested.

Use whatever sandbox you already have. The gate is the product. The host is disposable.

Limitations

This checklist does not prove the change is correct. It proves you refused a class of unsafe promotions.

It will not catch a wrong business rule that still compiles. It will not catch a model that omitted a test you never wrote. It will not survive a team that grants themselves continue-on-error: true.

Digests can match and still be wrong if both runs used the same poisoned fixture. Environment labels can be spoofed if your CI does not mint them. Feature flags do not help if the flag wraps only the happy path.

Free sandboxes also disappear, throttle, or change under you. That is another reason they cannot be the system of record. Pin the SHA on infrastructure you operate.

Who should not use this

Skip the full gate if you are:

  • Working in a throwaway repo with no secrets, no customers, and no deploy.
  • Pairing on a spike that will be deleted before it touches main.
  • Already running agents only inside a locked-down trusted_ci image with no sandbox hop.

Do not skip it if the agent can open a PR, talk to a package registry, or see a staging cluster. That is already production-adjacent.

What you do Monday

Pick one service. Add promotion.json as required evidence. Add the script. Break a PR on purpose by leaving trusted_ci_run_id empty. Confirm the job fails.

Then break it again by labeling a laptop run as trusted_ci. If that merge is possible, your identity story is the incident, not the model.

Sandbox green is allowed. Sandbox authority is not. Fail closed, then promote.

Top comments (0)