DEV Community

Sam Rivera
Sam Rivera

Posted on

Build a Cutover Card Before a Free-Model Sidecar Sees Traffic

The command printed ok and exited zero. I almost shipped it.

The sidecar had called a remote model. The reply looked fine. Then I opened the receipt.

model_id was blank. server said default. Who signed that?

A green exit is not a cutover. It is a shrug.

I wanted a card I could refuse to skip. One page. Evidence or no traffic. Could I promote a flag without a file? No.

The actual goal

Keep a tiny CLI on my laptop. Add one optional remote call. Treat free model access as a canary, not a brand.

Budget: forty minutes. Cost: zero dollars. No extra vendors.

Abandon the cutover if any required field lacks a timestamped file. Also abandon if that file is older than twenty-four hours.

This is a proposal you can copy today. I am not selling a war story. The failure fixture below is labeled. Run it before you flip anything.

Why a card, not a vibe

Vibe coding writes the sidecar. Engineering writes the gate.

If the model is free, the exit code still lies. If the server is free, the hostname still drifts. What proves the path you think you have?

A checklist in a README dies. A card with files can fail closed.

I use MonkeyCode here only as the cheap canary host. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Free model access and a free server option are the two claims I will use. I will not invent names, quotas, or hardware.

Park the sidecar there if you want a $0 path. Fork the card first.

Time box and rollback

  1. Create the card and the checker. Cap this at twenty minutes.
  2. Fill evidence from a local dry run. Cap this at fifteen minutes.
  3. Run the checker against a bad fixture. Cap this at five minutes.

Rollback is a one-line env change. SIDECAR_MODE=off returns you to local-only. If the checker fails, you do not flip SIDECAR_MODE=remote. That is the whole contract.

The card

Save this as cutover-card.yaml next to your CLI.

# cutover-card.yaml
# Fill every evidence path. Blank means fail closed.
schema: cutover-card.v1
feature: commit-msg-sidecar
owner: you@localhost
budget:
  minutes: 40
  dollars: 0
rollback:
  env: SIDECAR_MODE=off
  abandon_if: evidence missing or older than 24h
gates:
  - id: local_dry_run
    question: Does the CLI work with the sidecar off?
    evidence: evidence/local-dry-run.txt
    fail_closed: true
  - id: remote_identity
    question: Did we record host, route, and model id?
    evidence: evidence/remote-identity.json
    fail_closed: true
  - id: schema_receipt
    question: Does the reply match today's schema file?
    evidence: evidence/schema-receipt.json
    fail_closed: true
  - id: secret_scan
    question: Did the prompt omit tokens and .env files?
    evidence: evidence/secret-scan.txt
    fail_closed: true
  - id: cost_cap
    question: Is the canary still on the free path?
    evidence: evidence/cost-cap.txt
    fail_closed: true
  - id: owner_signoff
    question: Did a human type the promote word?
    evidence: evidence/signoff.txt
    fail_closed: true
Enter fullscreen mode Exit fullscreen mode

Six gates. Each one asks a question. Each one points at a file. No file, no traffic. Simple, right?

Evidence files you actually write

Create the folder first.

mkdir -p evidence
export SIDECAR_MODE=off
python3 cli.py --dry-run > evidence/local-dry-run.txt
Enter fullscreen mode Exit fullscreen mode

local-dry-run.txt must contain the command, the exit code, and a clock time. If the exit is not zero, stop. Why promote a broken local path?

Now the remote identity file. Fill it from the canary response headers or logs. Do not type default.

{
  "recorded_at": "2026-09-17T15:04:00Z",
  "host": "canary.example.invalid",
  "route": "/v1/sidecar",
  "model_id": "recorded-from-response",
  "auth": "env:SIDECAR_TOKEN set=true value=redacted",
  "mode": "free-canary"
}
Enter fullscreen mode Exit fullscreen mode

model_id must come from the response. If the body omits it, the gate fails. Guessing is not evidence.

Schema receipt next. Hash the schema file you send. Store the hash. Store the keys you required.

{
  "schema_file": "schemas/commit-msg.v2.json",
  "schema_sha256": "replace-with-real-hash",
  "required_keys": ["title", "body", "risk"],
  "observed_keys": ["title", "body", "risk"],
  "recorded_at": "2026-09-17T15:06:00Z"
}
Enter fullscreen mode Exit fullscreen mode

If observed_keys drops risk, the card fails. A pretty paragraph is not a schema.

Secret scan is a boring grep. Keep it boring.

rg -n "API_KEY|BEGIN |sk-|\.env" prompt.txt || true > evidence/secret-scan.txt
echo "scan_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> evidence/secret-scan.txt
Enter fullscreen mode Exit fullscreen mode

An empty match list is success. A hit is a hard stop. Do you really want the canary to see .env?

Cost cap is a sentence, not a dashboard.

path: free-model-canary
billable: no
recorded_at: 2026-09-17T15:08:00Z
note: abandon if the client switches off the free path
Enter fullscreen mode Exit fullscreen mode

Signoff must be typed. No script writes this file for you.

promote
owner: you@localhost
recorded_at: 2026-09-17T15:10:00Z
Enter fullscreen mode Exit fullscreen mode

The first line must be exactly promote. Anything else fails closed. That is the human gate.

The checker

Save this as check_cutover.py. It is the artifact. Run it. Do not skim it.

#!/usr/bin/env python3
"""Fail closed if the cutover card lacks fresh evidence."""
from __future__ import annotations

import json
import sys
from datetime import datetime, timezone
from pathlib import Path

try:
    import yaml  # type: ignore
except ImportError:
    yaml = None

MAX_AGE_HOURS = 24
REQUIRED_SIGN = "promote"


def die(msg: str) -> None:
    print(f"FAIL: {msg}")
    raise SystemExit(1)


def load_card(path: Path) -> dict:
    text = path.read_text(encoding="utf-8")
    if yaml is not None:
        return yaml.safe_load(text)
    # tiny fallback: enough for this template if PyYAML is missing
    die("PyYAML is required. pip install pyyaml")
    return {}


def parse_ts(raw: str) -> datetime:
    return datetime.fromisoformat(raw.replace("Z", "+00:00"))


def file_fresh(path: Path) -> None:
    age_h = (datetime.now(timezone.utc).timestamp() - path.stat().st_mtime) / 3600
    if age_h > MAX_AGE_HOURS:
        die(f"{path} is {age_h:.1f}h old; max is {MAX_AGE_HOURS}h")


def check_identity(path: Path) -> None:
    data = json.loads(path.read_text(encoding="utf-8"))
    for key in ("host", "route", "model_id", "recorded_at"):
        if not data.get(key) or str(data.get(key)).lower() in {"default", "unknown", ""}:
            die(f"{path} missing real {key}")
    parse_ts(data["recorded_at"])


def check_schema(path: Path) -> None:
    data = json.loads(path.read_text(encoding="utf-8"))
    required = list(data.get("required_keys") or [])
    observed = list(data.get("observed_keys") or [])
    if not required:
        die(f"{path} has no required_keys")
    missing = [k for k in required if k not in observed]
    if missing:
        die(f"{path} missing keys: {missing}")
    if not data.get("schema_sha256") or data["schema_sha256"].startswith("replace-"):
        die(f"{path} has a placeholder hash")


def check_signoff(path: Path) -> None:
    first = path.read_text(encoding="utf-8").splitlines()[0].strip()
    if first != REQUIRED_SIGN:
        die(f"{path} first line must be '{REQUIRED_SIGN}'")


def main() -> None:
    root = Path.cwd()
    card_path = root / "cutover-card.yaml"
    if not card_path.exists():
        die("cutover-card.yaml is missing")
    card = load_card(card_path)
    if card.get("schema") != "cutover-card.v1":
        die("unexpected schema")
    for gate in card.get("gates") or []:
        rel = gate["evidence"]
        path = root / rel
        if not path.exists():
            die(f"gate {gate['id']}: {rel} does not exist")
        file_fresh(path)
        gid = gate["id"]
        if gid == "remote_identity":
            check_identity(path)
        elif gid == "schema_receipt":
            check_schema(path)
        elif gid == "owner_signoff":
            check_signoff(path)
    print("PASS: cutover card is complete and fresh")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Install the one dependency, then run it twice.

pip install pyyaml
python3 check_cutover.py; echo exit:$?
Enter fullscreen mode Exit fullscreen mode

First run should fail. That is the point. You have no evidence yet.

Failure fixture

Here is the fixture I keep in fixtures/blank-identity.json. Label it as a fixture. Do not ship it.

{
  "recorded_at": "2026-09-17T15:04:00Z",
  "host": "default",
  "route": "/v1/sidecar",
  "model_id": "",
  "auth": "env:SIDECAR_TOKEN set=true value=redacted",
  "mode": "free-canary"
}
Enter fullscreen mode Exit fullscreen mode

Copy it over the real identity file and rerun.

cp fixtures/blank-identity.json evidence/remote-identity.json
python3 check_cutover.py; echo exit:$?
Enter fullscreen mode Exit fullscreen mode

You want FAIL: evidence/remote-identity.json missing real model_id. If you get PASS, the checker is wrong. Fix the checker, not the fixture.

A second fixture is a stale file. Touch it into last week.

touch -d "36 hours ago" evidence/local-dry-run.txt
python3 check_cutover.py; echo exit:$?
Enter fullscreen mode Exit fullscreen mode

Stale evidence is a silent promotion. The age gate exists because I forget dates. Do you?

Numbered cutover, once the card passes

  1. Keep SIDECAR_MODE=off until the checker prints PASS.
  2. Run one canary call against the free server path.
  3. Rewrite remote-identity.json from that response only.
  4. Hash the schema. Put the hash in the receipt.
  5. Type promote into evidence/signoff.txt yourself.
  6. Run python3 check_cutover.py again.
  7. Export SIDECAR_MODE=remote in that shell only.
  8. Run one real command. Then set the mode back to off.

If step 6 fails, you never reach step 7. That is fail closed.

Decision table

Gate Evidence Fail closed when
local dry run command log exit != 0
remote identity json from the canary host or model_id blank
schema receipt hash + keys missing required key
secret scan grep output token-like hit
cost cap one text file path is no longer free
owner signoff typed word first line != promote

Copy the table into your repo if YAML feels heavy. The checker still wants files. Files beat memory.

Who should not use this

Do not use this card for secrets, payroll, or personal data. A free canary is still a remote hop. I do not send customer text there.

Do not use this if you need an SLA. A free path can vanish. The card records the path. It does not promise uptime.

Do not use this as a model quality test. A schema match is not a good commit message. You still read the output.

Skip it if your tool never leaves localhost. Then you do not need a cutover. You need tests.

Limits I will not hide

The checker does not call the network. It only reads files. You can fake every file. The signoff line is the thin human brake.

The twenty-four hour window is arbitrary. Tighten it if you ship daily. Loosen it and you will promote ghosts.

I did not name models. Names rot. Record whatever the canary returns. If it returns nothing, you do not cut over.

MonkeyCode is optional in this workflow. Remove the product and the card still works. The free server option is just where I park a $0 canary when I want one. That is the only pitch.

What I ship from this

A yaml card. A python checker. One blank-identity fixture. A rollback env var.

Forty minutes. Zero dollars. One typed promote.

If the checker yells, I keep the sidecar off. That is the clean exit.

Which evidence field do you refuse to auto-fill, and why?

Top comments (0)