When Not to Host an Agent on Free Inference A developer has published a refusal protocol for running AI agents on free inference endpoints, arguing that such hosts are sandboxes unsuitable for production tasks that can mutate state or handle sensitive data. The protocol includes a job card and an offline preflight gate script that blocks jobs touching forbidden data, tools, or sinks. The article was prepared as part of MonkeyCode's product outreach, which offers free model access and server options for experimental work. 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. bash /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.