DEV Community

Emery Li
Emery Li

Posted on

Preflight Before Tokens: A Four-Signal Gate for Local-First Agents

A payments engineer sat in a coworking space and asked a local coding agent to inspect a webhook retry loop. The prompt included a staging secret that had been pasted from a long terminal scrollback buffer. The laptop fan was already loud from a Docker build, and the cafe Wi-Fi dropped packets every few minutes. The engineer wanted local inference for privacy, yet the machine was about to thermal-throttle and miss a latency budget.

This article treats that coworking-space scene as a routing problem rather than a model-quality problem. Many local-first agent stacks assume the host will stay cool, secret-free, and fully offline-capable. Those assumptions collapse together when a laptop is hot, a prompt is sensitive, and the network is only partly present. A small preflight gate can refuse generation until the four signals agree on one route.

Generation-first designs leak work and secrets

Most agent CLIs call a model as soon as the user hits enter, then apply filters afterward. Post-hoc redaction cannot unsend a staging secret that already left the developer machine. Post-hoc latency logs also cannot recover an editor interaction that already stalled on a hot laptop. The safer operational order is classify, decide, then generate, with a printed reason code.

Local-first still remains the default for this audience, because source, secrets, and unfinished diffs should not travel by habit. Cloud spill is a privilege that the matrix grants after the prompt is safe and the host is actually failing its budget. Offline mode is not a badge of purity when the runtime is down and the task is public documentation. The interesting design is a gate that can say local, remote, or refuse, and then stop.

The four signals

Signal one is secret class, and that label is not the same thing as user intent. A public docstring rewrite can leave the machine, while a pasted environment fragment must not. Teams that skip this label often route by task type and then leak credentials inside an otherwise harmless refactor. The preflight therefore fails closed when the classifier is unsure rather than hoping a remote log will forget.

Signal two is connectivity, including degraded cafe networks that still return one successful ping packet. A single ICMP reply does not mean a long generation stream will survive NAT idle timeouts. Signal three is the local latency budget, compared with a recent p95 rather than a peak token rate. Signal four is spare remote capacity on an allowed path, which matters only after secrets and connectivity permit a spill.

The matrix below is a proposal for laptop agents, not a measured production SLO. Operators should replace the sample p95 with their own runtime probe before trusting the remote branch. Secret class remains a policy input, not a model output, because classifiers invent labels under pressure. Connectivity uses a short HTTPS probe to the intended spill host rather than a generic ping.

secret_class connectivity local_runtime p95 vs budget route reason_code
PUBLIC OFFLINE up any local LOCAL_OFFLINE_OK
PUBLIC OFFLINE down any refuse REFUSE_OFFLINE_DEAD
PUBLIC ONLINE up within budget local LOCAL_BUDGET_OK
PUBLIC ONLINE up over budget remote REMOTE_LATENCY_SPILL
PUBLIC DEGRADED up over budget local LOCAL_DEGRADED_HOLD
INTERNAL ONLINE up over budget remote_sanitized REMOTE_STRIP_THEN_SPILL
SECRET any up any local LOCAL_SECRET_PIN
SECRET OFFLINE down any refuse REFUSE_SECRET_NO_RUNTIME
INTERNAL OFFLINE down any refuse REFUSE_INTERNAL_OFFLINE_DEAD

That table is the artifact teams can copy into a design review without adopting a vendor. Remote appears in only two rows, and both rows require a non-secret remainder after stripping. Degraded networks do not spill, because a hung stream is worse than a slow local answer. Refuse is a first-class route, not an exception handler buried in HTTP client code.

Implement the preflight in five steps

The following Python module is an unexecuted example that teams can paste into a scratch file. It does not call a vendor until a later adapter is attached behind the remote routes. Place it beside the agent CLI so the reason code prints before any tokenizer runs. Keep the secret scanner deliberately boring, because clever NLP classifiers are how internal tokens escape.

1. Define the policy types

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

from dataclasses import dataclass
from enum import Enum
from typing import Optional
import re
import time
import urllib.request

class SecretClass(str, Enum):
    PUBLIC = "PUBLIC"
    INTERNAL = "INTERNAL"
    SECRET = "SECRET"
    UNKNOWN = "UNKNOWN"

class Connectivity(str, Enum):
    OFFLINE = "OFFLINE"
    DEGRADED = "DEGRADED"
    ONLINE = "ONLINE"

class Route(str, Enum):
    LOCAL = "local"
    REMOTE = "remote"
    REMOTE_SANITIZED = "remote_sanitized"
    REFUSE = "refuse"

@dataclass(frozen=True)
class Signals:
    secret_class: SecretClass
    connectivity: Connectivity
    local_runtime_up: bool
    local_p95_ms: int
    latency_budget_ms: int

@dataclass(frozen=True)
class Decision:
    route: Route
    reason_code: str
    sanitized_prompt: Optional[str]
Enter fullscreen mode Exit fullscreen mode

Those types make the later matrix readable in logs, which matters when a refuse decision looks like a product outage. Frozen dataclasses prevent a later hook from mutating the secret class after the printout. Enums serialize cleanly into JSON traces for postmortems without leaking the prompt body. The sanitized prompt is optional because refuse and local-secret routes must not keep a stripped copy hanging around.

2. Scan for secrets before any network call

SECRET_PATTERNS = (
    re.compile(r"sk-[A-Za-z0-9]{8,}"),  # placeholder shape, not a live credential
    re.compile(r"-----BEGIN (?:RSA |OPENSSH )?PRIVATE KEY-----"),
    re.compile(r"(?i)(api[_-]?key|secret|password|token)\s*[:=]\s*\S+"),
    re.compile(r"AKIA[0-9A-Z]{16}"),
)

INTERNAL_MARKERS = (
    re.compile(r"(?i)staging\."),
    re.compile(r"(?i)internal-only"),
    re.compile(r"(?i)\b10\.\d+\.\d+\.\d+\b"),
)

def classify_prompt(text: str) -> SecretClass:
    if any(p.search(text) for p in SECRET_PATTERNS):
        return SecretClass.SECRET
    if any(p.search(text) for p in INTERNAL_MARKERS):
        return SecretClass.INTERNAL
    if not text.strip():
        return SecretClass.UNKNOWN
    return SecretClass.PUBLIC

def strip_internal(text: str) -> str:
    redacted = text
    for p in SECRET_PATTERNS:
        redacted = p.sub("[REDACTED_SECRET]", redacted)
    redacted = re.sub(r"(?i)\b10\.\d+\.\d+\.\d+\b", "[REDACTED_IP]", redacted)
    return redacted
Enter fullscreen mode Exit fullscreen mode

The scanner is intentionally pattern-based so a reviewer can list every match in a pull request. It will miss novel secret formats, which is why UNKNOWN and SECRET both fail closed in the matrix. INTERNAL is a weaker class for hostnames and RFC1918 addresses that should not become public training context. Stripping happens in memory and should be logged as a count of replacements, never as the replaced values.

3. Probe connectivity and local p95 without sending the prompt

def probe_connectivity(url: str, timeout_s: float = 2.0) -> Connectivity:
    started = time.monotonic()
    try:
        req = urllib.request.Request(url, method="HEAD")
        with urllib.request.urlopen(req, timeout=timeout_s) as resp:
            elapsed_ms = (time.monotonic() - started) * 1000
            if resp.status >= 500:
                return Connectivity.DEGRADED
            if elapsed_ms > 800:
                return Connectivity.DEGRADED
            return Connectivity.ONLINE
    except Exception:
        return Connectivity.OFFLINE

def read_local_p95(path: str = "/tmp/local-agent-p95.ms") -> int:
    try:
        return int(open(path).read().strip())
    except (OSError, ValueError):
        return 10**9  # fail closed: missing probe counts as infinitely slow
Enter fullscreen mode Exit fullscreen mode

The probe uses HEAD against the intended spill host so DNS and TLS failures count as offline. Ping is skipped because ICMP is often blocked on cafe networks that still allow HTTPS. Local p95 is read from a sidecar file that a runtime heartbeat should already be writing. Missing metrics fail closed, which keeps a cold laptop from claiming it is fast.

A tiny heartbeat writer can run beside the local runtime the team already operates on the laptop. The next snippet is a proposal for that sidecar rather than a claim about any particular model binary. Dummy samples keep the article honest, because no live p95 from this account is being reported here. Replace the list with real timings before any remote spill is enabled for public prompts.

# proposal: write a dummy local p95 from ten placeholder samples
python - <<'PY'
from pathlib import Path
samples = [1800, 2100, 1950, 2400, 2600, 1900, 2050, 2300, 2500, 2200]
samples.sort()
p95 = samples[int(0.95 * (len(samples) - 1))]
Path("/tmp/local-agent-p95.ms").write_text(str(p95))
print(p95)
PY
Enter fullscreen mode Exit fullscreen mode

4. Encode the matrix as total functions

def decide(signals: Signals, prompt: str) -> Decision:
    secret = signals.secret_class
    online = signals.connectivity
    up = signals.local_runtime_up
    over = signals.local_p95_ms > signals.latency_budget_ms

    if secret in (SecretClass.SECRET, SecretClass.UNKNOWN):
        if up:
            return Decision(Route.LOCAL, "LOCAL_SECRET_PIN", None)
        return Decision(Route.REFUSE, "REFUSE_SECRET_NO_RUNTIME", None)

    if online is Connectivity.OFFLINE:
        if up:
            return Decision(Route.LOCAL, "LOCAL_OFFLINE_OK", None)
        code = (
            "REFUSE_INTERNAL_OFFLINE_DEAD"
            if secret is SecretClass.INTERNAL
            else "REFUSE_OFFLINE_DEAD"
        )
        return Decision(Route.REFUSE, code, None)

    if online is Connectivity.DEGRADED:
        if up:
            return Decision(Route.LOCAL, "LOCAL_DEGRADED_HOLD", None)
        return Decision(Route.REFUSE, "REFUSE_DEGRADED_DEAD", None)

    # ONLINE from here
    if up and not over:
        return Decision(Route.LOCAL, "LOCAL_BUDGET_OK", None)

    if secret is SecretClass.PUBLIC:
        if up and over:
            return Decision(Route.REMOTE, "REMOTE_LATENCY_SPILL", prompt)
        if not up:
            return Decision(Route.REMOTE, "REMOTE_RUNTIME_DOWN", prompt)

    if secret is SecretClass.INTERNAL and (over or not up):
        return Decision(
            Route.REMOTE_SANITIZED,
            "REMOTE_STRIP_THEN_SPILL",
            strip_internal(prompt),
        )

    return Decision(Route.REFUSE, "REFUSE_UNMAPPED", None)
Enter fullscreen mode Exit fullscreen mode

Every branch returns, which is the entire point of a preflight gate on a laptop. Public work may spill when the laptop is over budget or the local runtime is down. Internal work may spill only after stripping, and secret work never spills at all. Unmapped combinations refuse instead of inventing a clever default that cannot be audited.

5. Print the decision before the model adapter runs

def preflight_or_raise(prompt: str, spill_url: str, budget_ms: int = 2000) -> Decision:
    signals = Signals(
        secret_class=classify_prompt(prompt),
        connectivity=probe_connectivity(spill_url),
        local_runtime_up=_runtime_up(),
        local_p95_ms=read_local_p95(),
        latency_budget_ms=budget_ms,
    )
    decision = decide(signals, prompt)
    print(
        f"route={decision.route.value} reason={decision.reason_code} "
        f"secret={signals.secret_class.value} net={signals.connectivity.value}"
    )
    if decision.route is Route.REFUSE:
        raise SystemExit(f"preflight refused: {decision.reason_code}")
    return decision

def _runtime_up(host: str = "127.0.0.1", port: int = 11434) -> bool:
    import socket
    try:
        with socket.create_connection((host, port), timeout=0.3):
            return True
    except OSError:
        return False
Enter fullscreen mode Exit fullscreen mode
python -c "from preflight import preflight_or_raise; preflight_or_raise('rewrite this public README section', 'https://example.invalid/health')"
Enter fullscreen mode Exit fullscreen mode

The printed reason line is the contract with the human sitting at the laptop. If the reason code is LOCAL_SECRET_PIN, the prompt never crosses the NIC, even if the fan is screaming. If the reason code is REMOTE_LATENCY_SPILL, the team has already accepted that the text is public and slow. If the reason code is a refuse, the CLI exits before a retry storm begins on cafe Wi-Fi.

A compact test plan

These tests are labeled as a proposal and do not report results from a live deployment. They lock the fail-closed behavior that design reviews actually care about in agent CLIs. Run them with pytest after copying the module into a throwaway virtualenv on the laptop. Extend the decision table rather than adding hidden branches inside the decide function.

# preflight_test.py — proposal
from preflight import (
    Connectivity, Route, SecretClass, Signals, decide, classify_prompt
)

def test_secret_never_spills_when_runtime_is_up():
    s = Signals(SecretClass.SECRET, Connectivity.ONLINE, True, 9000, 2000)
    d = decide(s, "token=sk-EXAMPLEONLY")
    assert d.route is Route.LOCAL
    assert d.reason_code == "LOCAL_SECRET_PIN"
    assert d.sanitized_prompt is None

def test_secret_refuses_when_runtime_is_down():
    s = Signals(SecretClass.SECRET, Connectivity.ONLINE, False, 9000, 2000)
    d = decide(s, "-----BEGIN RSA PRIVATE KEY-----")
    assert d.route is Route.REFUSE

def test_public_spills_on_latency():
    s = Signals(SecretClass.PUBLIC, Connectivity.ONLINE, True, 4000, 2000)
    d = decide(s, "Rewrite a public README badge section.")
    assert d.route is Route.REMOTE
    assert d.reason_code == "REMOTE_LATENCY_SPILL"

def test_degraded_does_not_spill():
    s = Signals(SecretClass.PUBLIC, Connectivity.DEGRADED, True, 4000, 2000)
    d = decide(s, "Rewrite a public README badge section.")
    assert d.route is Route.LOCAL
    assert d.reason_code == "LOCAL_DEGRADED_HOLD"

def test_classify_unknown_empty():
    assert classify_prompt("   ") is SecretClass.UNKNOWN
Enter fullscreen mode Exit fullscreen mode

These unit tests lock routing policy rather than generation quality or any vendor throughput claim. A green suite means the laptop will not silently export a private key because a p95 file looked ugly. Passing tests do not mean generation quality, tool-calling skill, or legal review is complete. Teams should add fixtures for their own secret shapes before calling the suite a control.

When a free server wins, and when it must not

Latency, secrets, and offline behavior pull in different directions, which is why a single default cannot carry a laptop agent. Local inference wins when the prompt is secret, the network is degraded, or the recent p95 still fits the editor budget. Offline is a feature for public work while the runtime is up, and a trap when that runtime is down. Remote compute wins only for public or sanitized-internal prompts on a healthy path after the host has blown its budget.

When the matrix selects remote compute, the implementation needs an endpoint that does not require a new vendor contract for a short trial. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option that can serve as the spill host for preflight-approved public work. The adapter belongs behind REMOTE reason codes only, and it is not a substitute for local pin rules.

A later adapter maps Route.REMOTE to that free server after the reason code prints to the terminal. Route.REMOTE_SANITIZED must send only decision.sanitized_prompt and never the original internal text. Route.LOCAL should keep using the on-desk runtime even when the remote probe looks idle and healthy. None of those mappings should bypass classify_prompt or rewrite a SECRET class after the printout.

Limitations

Regex secret detection is incomplete and will miss cloud keys that use unfamiliar prefixes. A determined user can still paste a credential that does not match the sample patterns. p95 files can be stale if the heartbeat dies while the model process remains fully up. HEAD probes can be blocked by captive portals that still allow a browser session through.

The matrix also ignores battery percentage, thermal zones, and disk encryption state on a real laptop. Adding those signals is useful, but each new axis multiplies review cost for the refuse paths. This design is for interactive coding agents on developer workstations, not for unattended batch jobs. The gate is not a compliance program and does not satisfy audit requirements by itself.

Who should not use this approach

Air-gapped teams with no legal remote spill should delete the remote rows instead of probing a free server. Organizations that cannot define INTERNAL versus SECRET should not ship the sanitizer as if it were a vault. Production incident responders who need a human on the loop should not auto-spill from a paging laptop. People who have not yet run a local runtime will hit refuse codes and should start with a local-only CLI.

What to do with the reason codes

Treat reason codes as product analytics for the agent itself and store them without prompt bodies. A week of LOCAL_SECRET_PIN events means the scanner is catching accidents and the remote path is staying unused. A week of REFUSE_OFFLINE_DEAD events means the local runtime needs a watchdog more than a new model. A week of REMOTE_LATENCY_SPILL events means overflow compute is earning a place without becoming the default.

Readers who already pin agents to the laptop can drop preflight.py beside the CLI and watch reason codes during ordinary work. If public spills appear only when the fan is already loud, the four-signal gate is routing better than a model switcher. After a day of clean codes, point only allowed public spills at the free server option mentioned above. Leave secrets on the desk and treat a refuse code as a successful preflight outcome.

Top comments (0)