DEV Community

Casey Sun
Casey Sun

Posted on

Refuse Free-Tier Routing on Privileged Agent Steps

The following walkthrough is a labeled scenario, not a production postmortem.

On a Tuesday deploy, an agent filled a routing gap.
The plan still needed a model for a migration dry-run.
No billed inference key existed in that environment.
The router selected free-tier inference as silent fallback.

The generated SQL looked almost syntactically correct to reviewers.
It was also almost destructive against production catalogs.
Nobody had marked the step as privileged work.
The free model guessed a plausible table name.

That guess became the incident ticket later that night.

Silent Fallback Is a Control-Plane Bug

Agent routers love a default path more than a hard error.
A missing paid key looks like a simple configuration hole.
Free model access looks like a helpful spare tire.
It is also an unreviewed change to the control plane.

Privileged steps still need deterministic human review before execution.
They do not need a convenient completion from any model.
This article is a when-not-to field guide.
It lists red flags, alternatives, and exit criteria.

The same rule applies to free shared servers.
A privileged agent step should not land there either.
Cheap availability does not equal an approved runtime.
Approval remains a policy decision, not a price tag.

Where Free Access Fits, And Where It Does Not

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Some teams evaluate MonkeyCode free model access during prototyping.
Some also try the free server option for disposable sandboxes.
Those options can be useful for throwaway drills.
They are the wrong substrate for privileged agent steps.

The rest of this guide stays useful without that product.
The gate is about step class, not vendor branding.
Remove every product name and the checklist still holds.
The router still needs an explicit deny list.

Classify Every Step Before a Model Is Chosen

Treat the agent plan as a sequence of typed steps.
Each step gets a class before a model is chosen.
The class decides whether free-tier routing is legal.
Unknown classes default to deny, not allow.

The following policy is a proposal, not a shipped standard.

  1. Schema migration and data backfill steps stay on billed, logged inference.
  2. Production deploy, rollback, and traffic-shift steps stay off free servers.
  3. Secret scan, credential rotation, and vault writes stay off free models.
  4. Billing, quota, and invoice mutations stay on reviewed runtimes.
  5. PII export, customer lookup, and support dumps stay off free endpoints.
  6. Legal hold, audit export, and retention changes stay fully attributed.
  7. Scratch refactors, comment cleanup, and sample apps may use free routing.

The seventh class is the only default allow.
Everything else requires an explicit override record.
Overrides expire after a named window and named owner.
They do not live in git as permanent exceptions.

Decision table

Step class Free model Free server Required alternative
migration deny deny billed model plus reviewed runner
deploy deny deny existing CD identity
secrets deny deny human plus vault CLI
pii deny deny redacted fixture only
scratch allow allow if no secrets ephemeral sandbox

This table is a starting policy, not a benchmark.
Teams should add classes from their own threat model.
Do not copy the table without an accountable owner.

Red Flags in Agent Plans

Watch the plan text before any tool runs.
The following signals mean free-tier routing is unsafe.

  • The step name contains migrate, deploy, rotate, or invoice.
  • The tool list includes shell, database, or cloud admin APIs.
  • The prompt embeds environment files, tokens, or customer rows.
  • The router comment says fallback, default, or cheapest available.
  • The server label is free, shared, or preemptible without a TTL.
  • The plan invents a resource name that was not in context.
  • Retry count is unbounded after a structured-output parse failure.
  • The agent claims the free path equals a production path.

Any two flags together should fail the plan.
A single secrets flag should fail the plan alone.
Invented names deserve the same treatment as missing names.
The model is filling gaps that policy should leave empty.

Artifact: Classify the Plan, Then Fail Closed

The following code is a proposal and an unexecuted example.
It does not claim production metrics or live incidents.
Teams should run it in a dry-run pipeline first.

# plan_route_guard.py — proposal / unexecuted example
from __future__ import annotations

import json
import re
import sys
from dataclasses import dataclass
from typing import Literal

Route = Literal["free_model", "free_server", "reviewed"]

PRIVILEGED = (
    r"\b(migrat|backfill|deploy|rollback|canary|rotate|vault|secret|pii|"
    r"invoice|billing|retention|audit|customer[-_ ]?export)\w*\b"
)
SCRATCH = r"\b(comment|readme|sample|sandbox|prototype|format|lint)\w*\b"
ADMIN_TOOLS = {"bash", "psql", "kubectl", "aws", "gcloud", "vault"}


@dataclass(frozen=True)
class Step:
    id: str
    intent: str
    route: Route
    tools: list[str]


def classify(intent: str) -> str:
    text = intent.lower()
    if re.search(PRIVILEGED, text):
        return "privileged"
    if re.search(SCRATCH, text):
        return "scratch"
    return "unknown"


def violations(step: Step) -> list[str]:
    found: list[str] = []
    klass = classify(step.intent)
    admin = ADMIN_TOOLS.intersection(set(step.tools))
    free = step.route in {"free_model", "free_server"}
    if klass in {"privileged", "unknown"} and free:
        found.append(f"{step.id}: {klass} step cannot use {step.route}")
    if admin and step.route == "free_server":
        found.append(f"{step.id}: admin tools cannot run on a free server")
    if admin and klass == "privileged" and step.route == "free_model":
        found.append(f"{step.id}: admin+privileged cannot use a free model")
    return found


def guard(plan: dict) -> int:
    bad: list[str] = []
    for raw in plan.get("steps", []):
        step = Step(
            id=str(raw["id"]),
            intent=str(raw["intent"]),
            route=raw["route"],
            tools=list(raw.get("tools", [])),
        )
        bad.extend(violations(step))
    if bad:
        print("REFUSE PLAN:")
        for item in bad:
            print(f"- {item}")
        return 1
    print("plan routing accepted")
    return 0


if __name__ == "__main__":
    payload = json.load(sys.stdin)
    raise SystemExit(guard(payload))
Enter fullscreen mode Exit fullscreen mode

A fixture that must fail closed:

{
  "steps": [
    {
      "id": "s1",
      "intent": "Generate SQL for the users table migration",
      "route": "free_model",
      "tools": ["psql"]
    },
    {
      "id": "s2",
      "intent": "Reformat comments in the sample app",
      "route": "free_model",
      "tools": []
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Commands to run the example locally:

python3 plan_route_guard.py < fixture-privileged-free.json
echo $?
# expected: 1

python3 - <<'PY'
from plan_route_guard import Step, violations
step = Step("t", "rotate vault token for prod", "free_server", ["vault"])
assert violations(step), "privileged free routing must fail"
print("classifier self-check ok")
PY
Enter fullscreen mode Exit fullscreen mode

The first command must exit non-zero.
The second command asserts the deny rule without a network.
No live model is required for this gate.
That independence is the point of a routing guard.

Retry Storms Make Free Routing Worse

Structured output fails often on tool-calling plans.
Agents retry the same privileged prompt with small nags.
A free pool then becomes an unbounded loop target.
The cost is not only money. It is also drift.

Each retry can invent a new table, flag, or host.
The Tuesday SQL guess is that pattern in miniature.
Bound retries to one replay against a reviewed checker.
Then stop and fail the plan instead of switching tiers.

A tiny retry cap belongs next to the classifier.

# proposal / unexecuted example
MAX_PLAN_RETRIES = 1

def allow_retry(step_class: str, attempt: int) -> bool:
    if step_class != "scratch":
        return False
    return attempt <= MAX_PLAN_RETRIES
Enter fullscreen mode Exit fullscreen mode

Scratch work may retry once on parse errors.
Privileged work may not retry onto a cheaper route.
Unknown work follows the privileged rule until labeled.

Better Alternatives

Do not replace the free model with hope.
Replace it with an owned path that already exists.

  • Keep privileged steps on a named, billed model endpoint.
  • Run those steps on a reviewed runner with audit logs.
  • Use frozen templates for migrations instead of generated SQL.
  • Require a human approval token for deploy and rotate tools.
  • Feed PII tasks with redacted fixtures, never live rows.
  • Pin tool schemas so the agent cannot invent resource names.
  • Bound retries when JSON parsing fails on a planner output.

A denied free route needs a documented substitute.
Silence is how the Tuesday incident started.
Write the substitute beside the deny reason in CI output.
The agent can replan only inside that narrower box.

Exit Criteria

Leave free-tier routing for a step class when any item is true.

  1. The step can mutate production data, auth, or money.
  2. The prompt may contain secrets, PII, or unreleased code.
  3. The server cannot provide isolation, logs, or a kill switch.
  4. The model output is executed without a second checker.
  5. Parse failures cause unbounded retries against the free pool.
  6. The team cannot name an owner for that runtime.

One true item is enough to exit.
Do not wait for a cluster of symptoms.
Record the exit in the plan artifact, not in chat lore.

Who Should Not Use This Approach

This gate is the wrong tool for some teams.

  • Solo toy chats with no tools and no production data.
  • Offline notebooks that never call a deploy API.
  • Vendors already wrapping every step in a policy engine.
  • Orgs that ban agents from privileged work entirely.

If agents never touch privileged classes, skip the classifier.
A cultural ban is simpler than a regex.
Do not add theater to a workflow that has no blast radius.

Limitations

The regex will miss novel verbs and local jargon.
It will also flag innocent docs that mention migration.
Unknown classes fail closed, which blocks some scratch work.
That cost is intentional and should be reviewed weekly.

This guide does not measure model quality or latency.
It does not claim capacity, quota, hardware, or uptime facts.
It does not prove a vendor is safe because inference is free.
Free is a billing state, not an authorization state.

The example is not unlabeled production evidence.
Treat it as a starting test, then add company-specific terms.
Re-run the fixture in CI on every agent plan artifact.
Do not run the guard only on one reviewer's laptop.

Practical Rollout

Store plans as JSON next to the pull request.
Run the guard as a required check on that file.
Reject merges when privileged steps name a free route.
Log the deny reason so the agent can replan inside policy.

A minimal CI stub:

# .github/workflows/agent-plan-guard.yml — proposal
name: agent-plan-guard
on: pull_request
jobs:
  guard:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: python3 plan_route_guard.py < agent-plan.json
Enter fullscreen mode Exit fullscreen mode

The workflow is short on purpose.
It answers a single routing question with a yes or no.
It does not grade prose, style, or model cleverness.
Cleverness is irrelevant once the step class is privileged.

Closing

Free model access is fine for scratch classes.
A free server is fine for disposable sandboxes.
Neither is a valid fallback for privileged agent steps.
Encode that rule before the router learns a default.

Teams with free model access can run the fixture on a scratch plan first.
Keep migrations, deploys, and secrets on reviewed runtimes after that check.

Top comments (0)