DEV Community

Emery Li
Emery Li

Posted on

A Redaction Gate for Agent Turns That Outlive the Tunnel

A coding agent on a commuter laptop had already opened three files when the train entered a tunnel and the link dropped. The working tree still held a half-written migration, a failing test log, and a snippet copied from a local environment file. The next turn could stay on the laptop, wait for the signal to return, or borrow a remote seat after the tunnel. That choice is a placement problem rather than a branding choice, and it should be scored before any bytes leave the machine.

Recent public threads circle model quality, yet local-first work still fails first on secrets, reachability, and where the files already live. A remote seat can still win when the laptop is thermally busy or the local model is still cold from a fresh boot. The same remote seat can also help when the remaining work is a long batch that contains no private spans at all. The gate in this article does not select a vendor, and it only records why a turn stayed home or traveled after redaction.

What the gate measures

Four pressures decide the seat, and each pressure can veto a remote hop without waiting for the other three. Latency matters when a developer is waiting on the next edit, because a round trip can cost more than a small local completion. Secrets matter more than latency, because a leaked key remains useful long after the tunnel ends and the commute finishes. Offline state matters when the interface is down, since a remote call that cannot connect should remain a queued turn rather than a failed job.

A free remote seat wins only after those three checks pass and the remaining work is large enough to justify leaving the laptop. The pressures stay independent, so a fast network does not excuse a secret and a free seat does not excuse a missing redaction. Teams that collapse the four pressures into one vague cloud switch will ship private spans on busy afternoons. The checklist below keeps each pressure visible in the run log instead of hiding it inside a single boolean.

Five steps before any hop

The workflow assumes a local queue already stores each agent turn as plain text plus a small metadata record beside it. No step requires a particular model name, a published quota, or a specific hardware shape on either side of the hop. Operators should read the current product documentation before they treat any free seat as available, because availability language changes. A large free-token figure should stay out of this runbook until current documentation confirms both the number and the expiry.

  1. Capture the turn on disk before any network call is attempted, including the user text, the tool excerpts, and a timestamp. Write that record under a local directory that is already gitignored, and refuse to continue if the write fails for any reason. A turn that cannot be stored locally should not be exported later under commute pressure or a flaky reconnect.
mkdir -p .agent-queue
git check-ignore -q .agent-queue || echo 'add .agent-queue/ to .gitignore before continuing'
printf '%s\n' "$TURN_TEXT" > .agent-queue/turn.txt
test -s .agent-queue/turn.txt && echo 'local capture ok'
Enter fullscreen mode Exit fullscreen mode
  1. Scan the stored text for secret-shaped spans, including environment assignments, private-key headers, and long token-like strings. Mark each hit with a byte offset and a label, then replace the span with a stable placeholder before any later copy. Keep the original record on the laptop, and never send the unredacted file as a convenience attachment for debugging.

  2. Probe connectivity with a short timeout, and record only reachable or unreachable together with the measured delay. Do not treat a successful probe as permission to upload the repository, the shell history, or the full conversation transcript. A probe that fails means the turn stays in the local queue until a later pass finds the interface up again.

python - <<'PY'
import os, socket, time
host = os.environ.get('SEAT_PROBE_HOST', 'example.com')
port = int(os.environ.get('SEAT_PROBE_PORT', '443'))
start = time.perf_counter()
try:
    socket.create_connection((host, port), timeout=1.5).close()
    ok, err = True, ''
except OSError as exc:
    ok, err = False, type(exc).__name__
elapsed = int((time.perf_counter() - start) * 1000)
print('probe_ok=%s probe_ms=%s err=%s' % (ok, elapsed, err))
PY
Enter fullscreen mode Exit fullscreen mode
  1. Score the seat with the function in the next section, using secret hits, the probe result, payload size, and whether the developer is waiting. Stay local when any secret remains, when the probe failed, or when the payload is only a short interactive edit. Allow a remote seat only when the payload is clean, the probe succeeded, and the job is a batch rather than a live edit.

  2. Log the decision, the redaction count, and the seat name beside the turn identifier before any client is invoked. Review that log after the commute, and delete remote copies that are no longer required for the batch that just finished. A missing log line is a failed run, even when the model returned a patch that appears to compile on the first try.

An example log line, not a captured production event, can use a single fixed field order for every turn. The fields are a timestamp, a turn identifier, a seat name, a redaction count, and the probe pair. A reviewer should be able to rebuild the table row from that line without opening the original prompt. If the seat is remote_allowed, the same line should also name the redacted file that was actually sent.

2026-10-13T08:14:02Z turn=8f3a seat=local_only redactions=1 probe_ok=true probe_ms=42 file=-
Enter fullscreen mode Exit fullscreen mode

Decision table the score must honor

The table below is the contract the scoring function must honor, and a later code change should not soften a row without a new test. A free server can help a long batch, yet that same server is the wrong seat while a turn still contains a key. Cost and novelty do not appear in the table, because neither one repairs a leak or restores a missing network. When several rows match, the most restrictive seat wins, and a secret hit outranks both the queue and the remote seat.

Condition Seat Reason
Probe failed or interface down local_queue Offline work cannot borrow a remote seat
Any secret hit remains local_only Private material does not travel to buy speed
Developer waiting and payload is small local_model Round-trip delay dominates a short edit
Interactive probe slower than 800 ms local_model A sluggish hop is worse than a local completion
Clean payload, network up, long batch remote_allowed Laptop stays responsive while the batch runs
Scanner crashed or output missing local_only Unknown secret state counts as a hit

Runnable gate and checks

The module below is a proposal for the gate, not a record of a measured production deployment or a vendor benchmark. It uses only the Python standard library so a reader can run it on a laptop without installing a vendor SDK. The thresholds are examples, and each team should retune them after measuring its own tunnels, editors, and local model startup. Nothing in the sample invents a model name or a token quota, because those facts belong in current product documentation.

from dataclasses import dataclass
import re

SECRET_PATTERNS = (
    re.compile(r'(?i)\b(api[_-]?key|secret|token|password)\b\s*[:=]\s*\S+'),
    re.compile(r'-----BEGIN [A-Z ]*PRIVATE KEY-----'),
    re.compile(r'\bsk-[A-Za-z0-9]{20,}\b'),
)

@dataclass(frozen=True)
class TurnFacts:
    text: str
    probe_ok: bool
    probe_ms: int
    interactive: bool
    batch_chars: int = 4000

def secret_hits(text: str) -> int:
    return sum(len(pattern.findall(text)) for pattern in SECRET_PATTERNS)

def redact(text: str) -> str:
    cleaned = text
    for pattern in SECRET_PATTERNS:
        cleaned = pattern.sub('[REDACTED]', cleaned)
    return cleaned

def choose_seat(facts: TurnFacts) -> str:
    if secret_hits(facts.text):
        return 'local_only'
    if not facts.probe_ok:
        return 'local_queue'
    if facts.interactive and len(facts.text) < facts.batch_chars:
        return 'local_model'
    if facts.interactive and facts.probe_ms > 800:
        return 'local_model'
    return 'remote_allowed'

def seat_or_hold(facts: TurnFacts) -> str:
    # Proposal only: production code should catch a narrow scanner error.
    try:
        return choose_seat(facts)
    except Exception:
        return 'local_only'
Enter fullscreen mode Exit fullscreen mode

A companion check can live beside the module and run under an ordinary test runner on the same laptop. The cases lock the veto order so a later edit cannot quietly prefer the remote seat when a secret is present. Offline turns stay queued, short interactive edits stay local, and only a clean batch is allowed to leave. Those expectations are the acceptance bar for this artifact, and they do not require a network connection to evaluate.

from span_gate import TurnFacts, choose_seat, redact, seat_or_hold

def test_secret_vetoes_remote_seat():
    facts = TurnFacts(
        'token = sk-live-abcdefghijklmnopqrstuvwxyz',
        True,
        40,
        False,
    )
    assert choose_seat(facts) == 'local_only'
    assert 'sk-live' not in redact(facts.text)

def test_offline_stays_queued():
    facts = TurnFacts('refactor the parser', False, 0, False)
    assert choose_seat(facts) == 'local_queue'

def test_short_interactive_stays_local():
    facts = TurnFacts('rename this helper', True, 30, True)
    assert choose_seat(facts) == 'local_model'

def test_clean_batch_may_leave():
    facts = TurnFacts('x' * 5000, True, 120, False)
    assert choose_seat(facts) == 'remote_allowed'

def test_scanner_failure_fails_closed(monkeypatch):
    def boom(_text):
        raise RuntimeError('scanner down')
    monkeypatch.setattr('span_gate.secret_hits', boom)
    facts = TurnFacts('plain batch text ' * 400, True, 30, False)
    assert seat_or_hold(facts) == 'local_only'
Enter fullscreen mode Exit fullscreen mode

Save the module as span_gate.py and the checks as test_span_gate.py, then run both commands from the repository root. The test command should fail the build if a secret-bearing turn can still select the remote_allowed seat. The second command should print local_only and a redacted line where the password value has been replaced by a placeholder. If either result differs, the gate is not ready for a reconnect, regardless of how idle the remote seat looks.

python -m pytest test_span_gate.py -q
python - <<'PY'
from span_gate import TurnFacts, choose_seat, redact
sample = 'password = hunter2\nplease summarize the diff'
print(choose_seat(TurnFacts(sample, True, 50, False)))
print(redact(sample))
PY
Enter fullscreen mode Exit fullscreen mode

Wiring a free remote seat

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is relevant here only as one operator-supplied option that currently describes free model access and a free server seat. This article does not treat a token allotment, a model name, a hardware shape, or a duration as a verified current fact. Readers who need those details should check the project documentation on the day they run the gate, then record the terms they observed.

If that free server is the chosen remote seat, the same function still applies, and the product name does not change the veto order. A clean batch may use the free seat so the laptop fan stays quieter and the editor remains responsive during the run. A turn with a key, a failed probe, or a short interactive edit should not move, even when the remote option is free. Free capacity is a scheduling convenience, not a reason to widen the set of bytes that are allowed to leave the machine.

Operators can wire the seat name into a client they already trust, provided the client receives the redacted text rather than the original file. A practical check is to diff the outbound body against the redacted string before the request is signed or sent. If that diff is not empty, the client should abort and leave the turn marked local_only until a person reviews the span. That abort path matters more than the brand of the seat, because the leak happens at the moment of copy.

Limits and poor fits

The patterns catch common assignment shapes and one illustrative key prefix, but they miss secrets that look like ordinary prose. They also miss secrets that sit inside encoded blobs, compressed logs, or screenshots that a later tool excerpt might describe in words. A team that handles production credentials should use a dedicated scanner and a reviewed denylist, then treat this module as a teaching aid. The latency cutoffs are examples, and a laptop on a congested link may need a higher delay before the local model is preferred.

This approach is a poor fit for organizations that already force every prompt through a managed inspection proxy with contractual retention limits. It is also a poor fit for agents that must call remote tools against private production systems, because redaction does not shrink tool privilege. Readers who cannot keep the original turn on an encrypted disk should not export anything until that storage gap is closed. A remote answer can be stronger than a local one and still be the wrong seat if the prompt remains dirty.

Teams that want quality comparisons should run them on synthetic fixtures that contain no customer data and no live credentials. Those fixtures should live in the same repository as the tests, so a reviewer can see exactly which strings were allowed to travel. The gate ignores model quality on purpose, because quality is a separate experiment from egress control and should not be mixed into one score. Mixing them tends to excuse a leak whenever the remote draft looks more fluent than the local draft.

Drain the queue after the link returns

When the train leaves the tunnel, the queue can be drained without guessing which turns were safe to move. Each record already carries a seat, a redaction count, and a timestamp, so the later pass forwards only items marked remote_allowed. Items marked local_only stay on the laptop even if the network is perfect and the remote seat is sitting idle. That split remains useful if the remote product named in the notes is replaced by another seat on a later day.

Readers can adapt the table to their own editor and probe, then keep the secret check first in that revised order. The module is short enough to review in one sitting, and the tests fail closed if a future edit tries to soften that order. Teams with a free server seat can point remote_allowed at that seat only after these tests pass on a clean sample. If the chosen seat surprises the team, change the threshold in the test first so the disagreement stays visible in version control.

Top comments (0)