A backend team shipped a local-first coding agent that kept repository secrets on each developer laptop during ordinary weekday work. The agent answered small questions after the first load, yet weekend batch reviews stalled whenever those laptops slept or left the office network. A product manager asked for a remote fallback that would never ship .env files, private keys, or customer dumps into a hosted context window. The useful design was not another model comparison; it was a job router that classified work before any tokens were spent.
Local-first agents still fail in predictable ways when every prompt is treated as equally safe to send elsewhere. Interactive edits want low latency and a warm local runtime, while overnight refactors want a machine that stays awake without closing the lid. Secret material wants disk and memory that the team already controls, while public fixtures can travel without violating residency rules. A router that inspects those four signals keeps the local-first promise without pretending the laptop is always the right host.
Four signals that actually change the route
The proposed classifier uses four signals that a wrapper can compute before it calls any model endpoint. None of the signals requires a public leaderboard score, and none of them assumes a particular vendor or hardware generation. Teams should replace the duration heuristic with measurements from their own laptops, because a cold local load and a warm local load are different jobs. The implementation below is a labeled proposal, not a report of production fleet telemetry.
The signals are numbered so a review can map each one to a log field.
- Secret residency. The flag is true when the prompt, tools, or retrieved chunks include credentials, customer records, or private repository material.
- Offline requirement. The flag is true when the job must finish without a network path, including airplane work and locked-down runners.
- Interactive budget. The flag is true when a human is waiting on the next token stream inside an editor or chat panel.
- Awake-host need. The flag is true when estimated wall time exceeds the laptop's reliable awake window, such as a Friday-night multi-file review.
A compact decision table follows. The last column is a policy outcome, not a quality ranking of models, and it should be treated as a starting contract.
| secrets | offline | interactive | awake-host need | route |
|---|---|---|---|---|
| yes | any | any | any | local only, or refuse remote |
| no | yes | any | any | local only, or refuse remote |
| no | no | yes | no | local preferred |
| no | no | no | yes | free remote server preferred |
| no | no | yes | yes | split: plan locally, run batch remotely |
| no | no | no | no | either; prefer local if already warm |
The interesting row is the split between planning and execution. Planning can stay on the laptop so that tool schemas and internal file paths remain private during the thinking phase. A sanitized batch of unit tests can then run on a remote server that will not sleep when the laptop lid closes. That split is the opposite of shipping the whole agent transcript to the cloud and hoping redaction works after the fact.
Recent public writing about agent workflows keeps returning to a related failure: systems assume a default environment instead of checking constraints. A job router is one way to stop that assumption from becoming a data-residency incident. The check happens before the first remote token, which is the only moment the policy can still say no. Latency still matters, yet latency is a later measurement, not a license to move secrets.
A proposed Python router
The following module is a proposed example for teams that already wrap local and remote runtimes behind one interface. It encodes the table as explicit branches so that a code review can see every deny path without searching a prompt. Operators can later swap the duration field for a measured p95 from their own traces. The file does not call a network API, and it does not rank model quality.
# job_router.py
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
class Route(str, Enum):
LOCAL = "local"
FREE_SERVER = "free_server"
SPLIT = "split"
REFUSE_REMOTE = "refuse_remote"
@dataclass(frozen=True)
class JobSignals:
contains_secrets: bool
requires_offline: bool
interactive: bool
estimated_seconds: int
local_runtime_warm: bool
laptop_awake_budget_seconds: int = 20 * 60
@dataclass(frozen=True)
class RoutingDecision:
route: Route
reason: str
sanitize_before_remote: bool
def classify_job(job: JobSignals) -> RoutingDecision:
"""Classify a single agent job. This is a policy function, not a model."""
needs_awake_host = job.estimated_seconds > job.laptop_awake_budget_seconds
if job.contains_secrets:
return RoutingDecision(
route=Route.REFUSE_REMOTE,
reason="secret material must remain on the controlling host",
sanitize_before_remote=False,
)
if job.requires_offline:
return RoutingDecision(
route=Route.LOCAL,
reason="offline constraint forbids any remote handoff",
sanitize_before_remote=False,
)
if job.interactive and not needs_awake_host:
reason = (
"interactive turn prefers a warm local runtime"
if job.local_runtime_warm
else "interactive turn still prefers local to avoid round-trip jitter"
)
return RoutingDecision(
route=Route.LOCAL,
reason=reason,
sanitize_before_remote=False,
)
if (not job.interactive) and needs_awake_host:
return RoutingDecision(
route=Route.FREE_SERVER,
reason="batch work needs a host that stays awake; payload has no secrets",
sanitize_before_remote=True,
)
if job.interactive and needs_awake_host:
return RoutingDecision(
route=Route.SPLIT,
reason="keep planning local; send only sanitized batch steps remotely",
sanitize_before_remote=True,
)
if job.local_runtime_warm:
return RoutingDecision(
route=Route.LOCAL,
reason="runtime already warm and no awake-host pressure",
sanitize_before_remote=False,
)
return RoutingDecision(
route=Route.FREE_SERVER,
reason="cold local runtime and non-interactive work; remote avoids warm-up wait",
sanitize_before_remote=True,
)
A tiny CLI makes the policy visible during incident review. Operators can paste the four signals from logs instead of arguing about which model felt faster that morning. The commands below are examples for a local checkout, not claimed production timings.
# route_cli.py
import argparse
import json
from job_router import JobSignals, classify_job
def main() -> None:
parser = argparse.ArgumentParser(description="Classify one agent job")
parser.add_argument("--secrets", action="store_true")
parser.add_argument("--offline", action="store_true")
parser.add_argument("--interactive", action="store_true")
parser.add_argument("--seconds", type=int, required=True)
parser.add_argument("--warm", action="store_true")
parser.add_argument("--awake-budget", type=int, default=1200)
args = parser.parse_args()
decision = classify_job(
JobSignals(
contains_secrets=args.secrets,
requires_offline=args.offline,
interactive=args.interactive,
estimated_seconds=args.seconds,
local_runtime_warm=args.warm,
laptop_awake_budget_seconds=args.awake_budget,
)
)
print(json.dumps({
"route": decision.route.value,
"reason": decision.reason,
"sanitize_before_remote": decision.sanitize_before_remote,
}, indent=2))
if __name__ == "__main__":
main()
python route_cli.py --secrets --interactive --seconds 12 --warm
python route_cli.py --seconds 5400
python route_cli.py --interactive --seconds 5400 --warm
python route_cli.py --offline --seconds 30
The first command should refuse remote work because secrets are present on an otherwise interactive turn. The second command should prefer a free remote server for a long batch with no secret flag and no offline constraint. The third command should split planning from execution because a human is waiting while the estimated work exceeds the awake budget. The fourth command should stay local because offline work cannot leave the machine even when the job is short.
Contract tests for the deny paths
Routing bugs show up as silent data movement, not as stack traces in the model client. The proposed tests lock the deny paths so a later performance tweak cannot move a secret job onto a remote host. The file is labeled as an unexecuted example until a team runs it in its own continuous integration. Assertions check policy, not token quality, which keeps the suite stable when models change.
# test_job_router.py
from job_router import JobSignals, Route, classify_job
def test_secrets_never_leave_even_when_the_laptop_will_sleep():
job = JobSignals(
contains_secrets=True,
requires_offline=False,
interactive=False,
estimated_seconds=10_000,
local_runtime_warm=False,
)
decision = classify_job(job)
assert decision.route == Route.REFUSE_REMOTE
assert decision.sanitize_before_remote is False
def test_offline_jobs_do_not_call_a_free_server():
job = JobSignals(
contains_secrets=False,
requires_offline=True,
interactive=False,
estimated_seconds=10_000,
local_runtime_warm=True,
)
assert classify_job(job).route == Route.LOCAL
def test_long_public_batch_prefers_an_awake_host():
job = JobSignals(
contains_secrets=False,
requires_offline=False,
interactive=False,
estimated_seconds=5_400,
local_runtime_warm=True,
)
decision = classify_job(job)
assert decision.route == Route.FREE_SERVER
assert decision.sanitize_before_remote is True
def test_interactive_long_job_splits_instead_of_shipping_context():
job = JobSignals(
contains_secrets=False,
requires_offline=False,
interactive=True,
estimated_seconds=5_400,
local_runtime_warm=True,
)
assert classify_job(job).route == Route.SPLIT
python -m pytest test_job_router.py -q
A team that already serializes agent traces can add one more assertion in the wrapper. Any job whose trace contains Authorization, BEGIN PRIVATE KEY, or a customer email pattern must classify as REFUSE_REMOTE before the HTTP client is constructed. That check belongs beside classify_job, not inside a model prompt that can ignore instructions. Secret scanners will miss novel formats, so the deny path still needs a human-set flag for regulated corpora.
Where an awake free server actually wins
Local inference still wins when the context is private, the user is waiting, or the network is gone. A free remote server wins when the payload is already public or synthetic, the laptop cannot be trusted to stay awake, and the job is a batch rather than a keystroke. Those conditions show up in eval sweeps over public fixtures, changelog drafting from public issues, and long static analysis of datasets that never included production records. The win is operational, not rhetorical: the remote host is allowed to outlive the laptop session.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. Teams that want an awake remote lane without standing up extra hardware can point the FREE_SERVER branch at MonkeyCode's free model access and free server option, then keep the REFUSE_REMOTE branch pointed at the laptop. Current availability and terms change, so operators should verify the live product page before planning capacity rather than copying older quota posts. The router remains useful if that remote lane is a different host entirely, because the policy lives in classify_job, not in a vendor SDK.
Sanitization is mandatory on every remote path, including the split route. A practical minimum is a denylist scan for secret patterns, a path allowlist that drops .env and key files, and a size cap so a supposedly public fixture dump cannot smuggle an accidental database export. The split route should send only the batch artifact, such as a tarball of tests, and never the original planning transcript that still names internal hosts. Tool access on the remote worker should be a separate allowlist, because a clean prompt does not stop a tool from fetching secrets later.
Numbered workflow to install the router
- Inventory job types for one week: interactive chat, local refactor, overnight eval, and offline travel work, with owners listed beside each type.
- Mark each type with the four signals, using written policy rather than model quality as the first filter, and record who may override a flag.
- Implement
classify_jobin the agent wrapper, ahead of the HTTP client and ahead of the local runtime call, so no path can skip the table. - Add the deny-path tests and a log line that records route, reason, estimated seconds, and whether sanitization ran before any remote bytes.
- Wire
FREE_SERVERto a host that stays awake for eligible non-secret jobs, using a remote lane the team already operates or is willing to review. - Review one week of logs for contradictions, especially jobs that were interactive yet routed remotely, or jobs that contained secrets yet requested sanitization instead of refusal.
Step six is the actual control loop. A router that is never compared with traces becomes folklore, and folklore is how assumed cloud defaults return. Teams should treat a missing contains_secrets value as true, not false, until the wrapper can prove the payload is public. Failure-closed routing is slower on mixed workloads, yet it matches the original product promise that local context stays local.
Limitations
The classifier does not measure token quality, hallucination rate, or tool-calling accuracy against a held-out set. It will send a public batch to a weak remote model if the policy says the laptop will sleep, and that may be the wrong engineering trade for a user-facing document. Duration estimates remain guesses until a team records real wall times on the same hardware that runs the agent. A wrong estimate can split a short job or pin a long job to a laptop that is about to close, which recreates the original weekend stall.
Secret detection is only as good as the flags and scanners feeding contains_secrets. If a developer pastes a production token into a public fixture, the router will believe the public flag and classify a remote route. The split route needs an extra contract: the remote worker must not call tools that reach production systems or private object stores. Offline mode also cannot be a best-effort flag; if the local runtime is absent, the honest outcome is failure, not a quiet remote call that violates the airplane rule.
Who should not use this approach
Teams that must keep every token on approved hardware should not add a free-server branch at all, even for text that looks public, until legal review says otherwise. Teams without a sanitizer and a tool allowlist should not enable FREE_SERVER or SPLIT, because those routes move bytes the wrapper no longer controls. Interactive products that cannot tolerate a classification bug should fail closed to local or refuse, rather than defaulting remote when signals are missing. Fully connected cloud agents that never had a local runtime need a different isolation story, and this table will not supply one.
The local-first promise survives contact with sleeping laptops only when jobs are classified before models are invoked. Secrets, offline constraints, interactive budgets, and awake-host need are boring fields, which is why they belong in a router instead of a prompt. A free remote server is a legitimate row in that table for public batch work, and it is a policy violation for everything else.
Top comments (0)