Free inference is a sandbox, not a substrate. An agent job that can change production state, retain customer text, or invent missing architecture should be refused before the first token is requested. The cheapest host is still the wrong host when the failure mode is irreversible.
Teams keep parking agent loops on complimentary model endpoints because the queue looks empty and the invoice looks like zero. That habit collapses the moment the loop is allowed to assume a schema, a secret, or a deployment target. The rest of this article is a refusal protocol: a job card, a local preflight gate, and exit criteria that fire even after a run has started.
Complimentary stacks exist. MonkeyCode currently advertises free model access and a free server option for experimental work. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Those two availability claims are the only product facts used here. They do not license a production role, a data-processing agreement, or a promise that a remote session is memoryless.
The protocol still applies if a different vendor is hosting the sandbox. The gate inspects the job, not the logo on the endpoint.
A free-tier agent run should begin as a file, not as a chat. The card below is deliberately boring. Boring cards are auditable. Flashy prompts are not.
# jobcard.yaml — a job this article would allow only as a sandbox draft
id: draft-readme-only
host_class: free_inference
allowed_tools: [read_file, list_dir]
forbidden_tools: [git_push, kubectl, send_email, http_post]
data_classes: [public_docs]
output_sink: local_drafts/
sla: none
human_merge: required
max_wall_clock_s: 180
max_tool_calls: 12
assumption_policy: refuse_if_unspecified
The card is a contract with the operator, not with the model. Models do not honor YAML. Operators can.
The script that follows never calls a model. It only decides whether a model is allowed to be called. That distinction is the whole point. If the gate cannot run offline, it is not a gate.
#!/usr/bin/env python3
"""preflight_gate.py — refuse free-inference jobs that do not belong there."""
from __future__ import annotations
import sys
from pathlib import Path
try:
import yaml
except ImportError:
sys.stderr.write("pip install pyyaml\n")
sys.exit(2)
FORBIDDEN_DATA = {"customer_pii", "prod_logs", "secrets", "auth_tokens"}
FORBIDDEN_TOOLS = {"git_push", "kubectl", "terraform_apply", "send_email", "http_post"}
FORBIDDEN_SINKS = {"origin/main", "production_db", "customer_ticket"}
def load_card(path: Path) -> dict:
data = yaml.safe_load(path.read_text())
if not isinstance(data, dict):
raise ValueError("job card must be a mapping")
return data
def refusals(card: dict) -> list[str]:
reasons: list[str] = []
data_classes = set(card.get("data_classes") or [])
tools = set(card.get("allowed_tools") or [])
sink = card.get("output_sink") or ""
sla = (card.get("sla") or "none").lower()
overlap = data_classes & FORBIDDEN_DATA
if overlap:
reasons.append(f"data class not allowed on free inference: {sorted(overlap)}")
bad_tools = tools & FORBIDDEN_TOOLS
if bad_tools:
reasons.append(f"tool can mutate the outside world: {sorted(bad_tools)}")
if any(tag in sink for tag in FORBIDDEN_SINKS):
reasons.append(f"output sink leaves the sandbox: {sink}")
if sla not in {"none", "best_effort"}:
reasons.append(f"SLA {sla!r} requires a contracted endpoint")
if card.get("human_merge") != "required":
reasons.append("free-inference output must stay off any unattended merge")
if int(card.get("max_wall_clock_s") or 0) <= 0:
reasons.append("missing wall-clock budget owned outside the model")
if int(card.get("max_tool_calls") or 0) <= 0:
reasons.append("missing tool-call budget owned outside the model")
if card.get("assumption_policy") != "refuse_if_unspecified":
reasons.append("agent may fill unspecified architecture with guesses")
if card.get("host_class") != "free_inference":
reasons.append("card is not claiming free inference; refuse to guess the host")
return reasons
def main(argv: list[str]) -> int:
if len(argv) != 2:
sys.stderr.write("usage: preflight_gate.py jobcard.yaml\n")
return 2
card = load_card(Path(argv[1]))
reasons = refusals(card)
if reasons:
sys.stderr.write("REFUSE\n")
for line in reasons:
sys.stderr.write(f"- {line}\n")
return 1
sys.stdout.write("ALLOW_SANDBOX\n")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
Run it before any client is pointed at a free endpoint.
python3 preflight_gate.py jobcard.yaml
echo $?
A non-zero exit is the success case for this article. The gate did its job when it blocked a run, not when it blessed one.
The first red flag is architectural silence. Agent loops are fluent at filling gaps. A missing service boundary becomes a guessed queue. A missing auth story becomes a hardcoded header. That fluency is useful on a whiteboard and poisonous on a complimentary endpoint, because the host has no duty to record why the guess was made. If the job card cannot name the components the agent is allowed to mention, the job does not belong on free inference. It belongs in a design review, or in a deterministic scaffold that fails closed.
The second red flag is residual data. Unredacted production logs look like perfect few-shot material. They also look like an accidental export. A hallway whiteboard is a fair analogy for a free endpoint: anyone who can see the board can copy it, and nobody issues a retention certificate for the eraser. Customer identifiers, session cookies, and stack traces with hostnames should never ride that board. Paid endpoints with a written processing term are the alternative. Local offline models are another. Silence is a third.
The third red flag is an unbounded outside world. Read-only tools still exfiltrate. Write tools mutate. If http_post, kubectl, or git_push appears on the allow list, the sandbox has already been redefined as production-adjacent. A free server that holds the only copy of agent state is the same mistake in a different coat. State that matters needs a disk the operator controls, not a session the operator cannot inventory.
The fourth red flag is a service promise the host never made. On-call copy, customer replies, and release-blocking summaries all imply an SLA, even when nobody wrote the letters S, L, and A. Complimentary inference is best-effort by nature. Treating it as capacity planning is a category error. The alternative is a contracted API, a human-authored template, or a script that does not speak.
A second card shows the refuse path without theatre.
# jobcard.bad.yaml — expected REFUSE
id: page-the-customer
host_class: free_inference
allowed_tools: [read_file, http_post, send_email]
data_classes: [customer_pii, prod_logs]
output_sink: customer_ticket
sla: p1_15m
human_merge: optional
max_wall_clock_s: 0
max_tool_calls: 0
assumption_policy: fill_gaps
python3 preflight_gate.py jobcard.bad.yaml
# REFUSE
# - data class not allowed on free inference: ['customer_pii', 'prod_logs']
# - tool can mutate the outside world: ['http_post', 'send_email']
# - output sink leaves the sandbox: customer_ticket
# - SLA 'p1_15m' requires a contracted endpoint
# ...
Preflight is not enough once a run has started. The model can still request a tool that was never on the card, or keep answering after the wall clock has expired. The wrapper below is a local proxy. It does not improve model quality. It only preserves the refusal.
# tool_proxy.py — labeled example; wire this in front of any tool runner
import time
class FreeTierExit(RuntimeError):
pass
class ToolProxy:
def __init__(self, card: dict):
self.allowed = set(card["allowed_tools"])
self.max_calls = int(card["max_tool_calls"])
self.deadline = time.monotonic() + int(card["max_wall_clock_s"])
self.calls = 0
self.schema_misses = 0
def check(self, tool_name: str, args_ok: bool) -> None:
if time.monotonic() > self.deadline:
raise FreeTierExit("wall-clock budget exhausted; abandon the host")
if tool_name not in self.allowed:
raise FreeTierExit(f"tool {tool_name!r} is not on the card")
self.calls += 1
if self.calls > self.max_calls:
raise FreeTierExit("tool-call budget exhausted; abandon the host")
if not args_ok:
self.schema_misses += 1
if self.schema_misses >= 2:
raise FreeTierExit("repeated schema failure; model is guessing")
Exit criteria should be dull enough to automate. Two schema misses. One disallowed tool name. One request to disable the gate. One inability to replay the job card from disk. Any of those is sufficient. Charming explanations from the model are not a counter-argument. They are the failure mode.
Better alternatives are usually less conversational. Version bumps and changelog formatting belong in scripts. Incident notes belong in a runbook a human already trusted last quarter. Private drafts of public docs can sit on a local model that never accepts a network route. Customer email belongs on an endpoint with retention controls, or it belongs unwritten until a person writes it. Free inference is what remains after those jobs have been removed.
The approach has limits. The gate believes the card. A human who labels production logs as public_docs will receive ALLOW_SANDBOX and a problem. The gate cannot prove that a remote free server is stateless, isolated, or free of cross-session residue. It cannot measure model quality, latency, or quota, and this article invents none of those figures. It also cannot stop a later process from pasting sandbox output into a merge. Refusal is a local habit, not a cryptographic seal.
Who should not use this pattern is clearer than who should. Regulated workloads should not. On-call should not. Anything with a customer on the other end of the sink should not. Solo experiments that cannot name a second reviewer should not treat ALLOW_SANDBOX as review. The pattern is for operators who already suspect the complimentary host is the wrong place and want that suspicion encoded as a non-zero exit code.
If the gate stays red, leave the complimentary host idle. That idle result is the useful one. If it prints ALLOW_SANDBOX and the work is a disposable draft with a human still sitting on the merge, the free model access and free server option named above is one place that draft can live. Run the gate until it refuses something real. A sandbox that never refuses is not a sandbox.
Top comments (0)