Free agent compute should host planning work, not irreversible tool calls you cannot rewind. I split every agent loop into a plan lane and an act lane before I pick a host. That split is more honest than asking whether a shared box looks idle tonight. If a step can send money, rewrite production, or leak a secret, it does not belong on a free shared host.
A single host is usually the wrong question
Have you noticed how agent demos collapse planning, retrieval, and mutation into one cheerful chat transcript? That collapse hides the only question that still matters when the compute is free and shared. Who else can see the context window while your agent is still thinking out loud? A planning lane can tolerate noisy neighbors; an acting lane cannot, because side effects do not wait for a retry.
I treat the host decision as a routing problem, not as a shopping problem with a single winner. Free model access plus a free server can be the right lane for drafts, retrieval summaries, and rejected plans. Self-hosted or paid isolated compute is the right lane once a tool would change a system I cannot rewind. Mixing those lanes because the chat UI looks unified is how accidental writes sneak into a shared box.
Step 1: Label every tool as plan, act, or forbidden
Start with the tool list, not with the vendor comparison table you were about to open. I ask one blunt question per tool: after this call, can a human undo the world with a git revert? If the honest answer is no, the tool belongs in act or forbidden, and never in plan. Plan tools read tickets, search docs, draft patches, and explain diffs without touching credentials that can mutate state.
Here is a proposed classifier I keep next to the agent config, not a production benchmark.
# proposed_example: plan_act_label.py
from enum import Enum
class Lane(str, Enum):
PLAN = "plan"
ACT = "act"
FORBIDDEN = "forbidden"
UNDOABLE_PREFIXES = ("read_", "search_", "draft_", "explain_", "lint_")
ACT_PREFIXES = ("apply_", "deploy_", "pay_", "email_", "delete_")
FORBIDDEN_SUBSTRINGS = ("prod_admin", "rotate_secret", "wire_transfer")
def label_tool(name: str) -> Lane:
lowered = name.lower()
if any(token in lowered for token in FORBIDDEN_SUBSTRINGS):
return Lane.FORBIDDEN
if lowered.startswith(ACT_PREFIXES):
return Lane.ACT
if lowered.startswith(UNDOABLE_PREFIXES):
return Lane.PLAN
return Lane.ACT # unknown tools default to isolated compute
Would you really let an unknown tool inherit the free lane because the name sounded polite? I default unknown names to act so a free host never receives a surprise mutation from a cute alias. That default is conservative on purpose, and it is supposed to feel slightly annoying during review. A slightly annoying router is cheaper than an irreversible write that landed on the wrong host.
Step 2: Write a host contract the plan lane must pass
A host contract is a tiny file the router can evaluate before any prompt is sent. I care about isolation promises, log retention, and whether tool execution is even enabled on that host. I do not care about marketing adjectives on a pricing page, because adjectives do not block a write. If a host cannot satisfy the plan contract, I do not send it drafts just to save a few tokens.
# proposed_example: host_contract.yaml
plan_lane:
allow_tool_execution: false
allow_secret_injection: false
max_untrusted_neighbors: "unknown-ok"
output: "draft_only"
act_lane:
allow_tool_execution: true
allow_secret_injection: true
max_untrusted_neighbors: 0
output: "mutates_with_receipt"
Disclosure: This article was prepared as part of MonkeyCode's product outreach. When I need a plan lane, I send drafts through MonkeyCode for free model access and a free server. I still move labeled act work onto a host I isolate, because a free shared server should not hold production credentials. If you are mapping a plan lane this week, that free-model and free-server path is one drafting room you can try.
Step 3: Keep act on a box you can isolate
The act lane needs a network policy you can describe in one paragraph without waving your hands. Can you name the outbound allowlist, the identity the tools assume, and the disk that stores receipts? If you cannot name those three, you do not have an act host yet, only a hope. Free shared compute almost never answers those three questions with the precision an irreversible tool deserves.
# proposed_example: act-host-smoke.sh
set -euo pipefail
test -n "${ACT_HOST:?set ACT_HOST to an isolated box}"
ssh "$ACT_HOST" 'test ! -w /etc/prod-creds || exit 1'
ssh "$ACT_HOST" 'iptables -S | grep -q "policy DROP" || echo "WARN: no default drop"'
ssh "$ACT_HOST" 'test -d /var/agent-receipts'
I run that smoke check before the first act tool is enabled, not after a scary demo. Does your current free box even let you SSH in and inspect a default-drop firewall? If the answer is no, it can still be a fine plan lane, and it should stay that way. I would rather lose convenience than pretend a closed platform is an isolated act host.
Step 4: Gate the handoff with a signed plan receipt
The dangerous moment is the handoff between lanes, not the first token of the draft. I refuse to stream a plan into an act tool without a receipt for tools, host, and a human decision. That receipt is boring on purpose, because boring gates survive when the chat UI wants to feel magical. Have you ever tried to reconstruct an agent write from a screenshot of a streaming token?
# proposed_example: handoff_receipt.py
import hashlib, json, time
def make_receipt(plan_text: str, allowed_act_tools: list[str], decided_by: str) -> dict:
return {
"plan_sha256": hashlib.sha256(plan_text.encode()).hexdigest(),
"allowed_act_tools": sorted(allowed_act_tools),
"decided_by": decided_by,
"decided_at": int(time.time()),
"lane": "act",
}
def assert_handoff(receipt: dict, requested_tool: str) -> None:
if requested_tool not in receipt["allowed_act_tools"]:
raise PermissionError(f"{requested_tool} is not on the signed plan")
I keep the receipt next to the patch, not inside a screenshot of the model conversation. Can a teammate replay the decision without watching you scroll through a long chat? If they cannot, the handoff is still a vibe, not a gate you can audit later.
A walkthrough with a hypothetical ticket
Take a hypothetical ticket that asks the agent to draft a changelog and then tag a release. Reading the issue and drafting the changelog are plan work, because a human can still throw the draft away. Tagging the release is act work, because git tag -a followed by a push is not a polite suggestion. Would you run that tag command on a free shared host just because the draft looked good in the same thread?
I dry-run the labels on my laptop before anyone enables network credentials on the act host. The commands below are a proposed checklist, not evidence from a production incident. If draft_changelog does not land in plan, I stop and rename tools until the classifier agrees. If tag_release lands in plan, I treat the classifier as broken and I refuse the free host.
# proposed_example: dry-run the split
python - <<'PY'
from plan_act_label import label_tool
print("draft_changelog", label_tool("draft_changelog"))
print("tag_release", label_tool("deploy_tag_release"))
print("do_the_needful", label_tool("do_the_needful"))
PY
A comparison table I actually fill in
I score a candidate against the lanes, not against a vague sense that free is cheaper. Fill the table per agent, not once per company, because a docs bot and a deploy bot do not share a lane. A comparison that cannot tell those bots apart will push mutating tools onto the drafting room again. I would rather maintain two boring configs than one clever host that does both jobs poorly.
| Question | Prefer free shared plan lane | Prefer self-hosted or paid act lane |
|---|---|---|
| Does the step only draft or explain? | Yes, keep it here | Wasteful unless you already have spare isolation |
| Can a neighbor read the prompt? | Acceptable for public docs | Unacceptable for customer data or secrets |
| Is the tool undoable with git? | Drafts only | Required once apply or deploy is in play |
| Do you need an outbound allowlist? | Usually cannot prove one | Required before the first mutating call |
| Will a queue delay break money movement? | Fine for planning | Not fine for act work with deadlines |
Is that more ceremony than a weekend demo wants, especially when the chat already feels complete? Yes, that extra ceremony is the entire point of treating compute choice as a routing guide. The table is a filter for the next tool, not a trophy for the host you already liked.
Limitations, and who should skip this
This split is a workflow, not a certification, and it will not save a leaked token already sitting in the prompt. I still assume the plan lane may be logged, sampled, or reused by a platform I do not control. If your planning context includes customer payloads, treat even the plan lane as isolated, and ignore the free option entirely. Shared drafts are a convenience feature, not a data-processing addendum you can wave at a customer.
Do not use this approach when a regulator expects a single audited runtime for every token. Do not use it when the agent must act inside one tightly timed transaction with no human receipt. Do not use it when you cannot name an isolated act host, because then you are only drawing lanes on a whiteboard. Teams that ship autonomous production changes without a reject path should not park anything on shared free compute.
The tests below are proposed guards for the classifier, not a vendor evaluation harness. They will not prove a platform is safe; they only prove your names still match your routing rules. Run them locally after you drop the classifier beside the agent config you actually ship.
# proposed_example: test_plan_act_label.py
from plan_act_label import Lane, label_tool
def test_draft_stays_on_plan_lane():
assert label_tool("draft_release_notes") == Lane.PLAN
def test_deploy_is_never_free_lane():
assert label_tool("deploy_canary") == Lane.ACT
def test_unknown_defaults_to_act():
assert label_tool("do_the_needful") == Lane.ACT
Run them with python -m pytest test_plan_act_label.py -q after the files sit together in one directory. A failing unknown-tool test is a gift, because it blocks a cute alias from inheriting the free lane. That is a small proof, and small proofs are what keep a free plan lane from quietly becoming an act lane.
What I refuse to optimize
I refuse to pick a host because leftover capacity looked friendly on a dashboard this morning. Capacity is not a lane, and a free server is not a personality test for your agent. The useful question is whether the next tool is still a draft, or whether it already needs a receipt. If you keep answering that question, the free versus paid debate gets quieter, and the routing file gets shorter.
Top comments (0)