Dev.to AI 🤖 Ai 👁 0 📖 7 min read

Split Generate and Apply Into Two Planes

You should not generate on the canonical tree. Generation and apply remain separate failure domains always. Mix those planes and one bad patch owns both. The public AI coding debate skips this split. People argue about

You should not generate on the canonical tree. Generation and apply remain separate failure domains always. Mix those planes and one bad patch owns both.

The public AI coding debate skips this split. People argue about model skill and tests. They rarely draw the architecture boundary first.

Think of a kitchen and a dining room. You cook food in one room only. You do not plate dinner on the stove.

Treat the canonical repository as the dining room. The agent workspace is the hot kitchen. Food moves only after a pass at the counter.

Constraints you actually have

You do not control the model's next token. You do control mounts, env, and apply. Those three knobs define the real architecture now.

Ignore them and the model owns your filesystem. The generator needs a compiler, tests, and files. It does not need production secrets at all.

It does not need your private deploy keys. It does not need a writable origin mount. Shared mounts collapse two planes into one.

Canonical state still has a different job entirely. It stores reviewed history and named release tags. It must stay boring, small, and fully attributable.

An agent session is none of those things. The review window collapses under real deadlines. Teams then apply from the same dirty checkout.

That is how scratch files become production code. You need a plane the agent cannot keep. You also need a plane the agent cannot see.

A free remote host can hold the kitchen. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode provides free model access and a free server option.

You can keep generation off your laptop disk. That remote host is still untrusted compute though. Treat it as a contractor laptop in a lobby.

You hand it a task bundle, not house keys. You take back a patch file, not shells. Write access to origin never leaves your apply host.

Data flow that survives a bad session

Start with a task bundle, not a live tree. The bundle contains a sparse checkout recipe only. It also contains a test command and size budget.

It contains no dotenv files and no ssh agents. The generator builds scratch state from that bundle. It writes code, runs tests, and emits artifacts.

The only exit products are a diff plus logs. Those files leave through a review inbox path. They do not leave through a git push.

You inspect the diff on a machine you trust. You reject path escapes, secrets, and binary blobs. You apply with git apply and then commit.

Canonical history never saw the generator disk layout. Here is a proposed guard for local apply. Treat the script as an example, not gospel.

#!/usr/bin/env python3
"""plane_guard.py — refuse mixed generate/apply planes."""
from pathlib import Path
import os
import sys

CANONICAL = Path(os.environ["CANONICAL_ROOT"]).resolve()
SCRATCH = Path(os.environ["AGENT_SCRATCH"]).resolve()
DIFF = Path(os.environ["REVIEW_DIFF"]).resolve()

FORBIDDEN_ENV = ("SSH_AUTH_SOCK", "AWS_SECRET_ACCESS_KEY", "GITHUB_TOKEN")

def fail(msg: str) -> None:
    print(f"plane-guard: {msg}", file=sys.stderr)
    sys.exit(2)

if CANONICAL == SCRATCH:
    fail("scratch plane equals canonical root")

if CANONICAL in SCRATCH.parents:
    fail("scratch plane sits inside canonical tree")

if SCRATCH in CANONICAL.parents:
    fail("canonical tree sits inside scratch plane")

for key in FORBIDDEN_ENV:
    if os.environ.get(key):
        fail(f"secret env {key} is visible")

text = DIFF.read_text(encoding="utf-8", errors="replace")
if "\0" in text:
    fail("diff contains binary payload")

blocked = (".env", "id_rsa", "credentials.json", "kubeconfig")
for name in blocked:
    if name in text:
        fail(f"diff mentions blocked path {name}")

print("plane-guard: generate/apply split looks intact")

Wire this guard in front of apply only. Do not wire it inside the agent process. The agent must not skip its own fence.

#!/usr/bin/env bash
# apply_reviewed_diff.sh — proposed apply path, not a generator hook
set -euo pipefail

export CANONICAL_ROOT="${CANONICAL_ROOT:?}"
export AGENT_SCRATCH="${AGENT_SCRATCH:?}"
export REVIEW_DIFF="${1:?diff path required}"

python3 plane_guard.py
git -C "$CANONICAL_ROOT" apply --check "$REVIEW_DIFF"
git -C "$CANONICAL_ROOT" apply --index "$REVIEW_DIFF"
git -C "$CANONICAL_ROOT" commit -m "apply reviewed generator diff"

Notice the commit happens only after the check. The scratch path never receives a git commit. Secret values stay absent from that environment.

That is the data flow, not a poster slogan. A remote generation job can follow this sketch. Keep the canonical remotes off that host.

# proposed remote scratch session on throwaway compute
rsync -a --exclude '.env' --exclude '.git' task-bundle/ "$AGENT_SCRATCH/"
ssh generator-host 'cd "$AGENT_SCRATCH" && run-agent && git diff > /tmp/out.diff'
scp generator-host:/tmp/out.diff ./review/inbox/out.diff

The laptop remains the only apply principal here. The generator host remains a disposable kitchen still. If the session dies, you lose scratch only.

A tiny manifest keeps the bundle honest. Treat this file as a proposed contract. Do not treat it as a vendor schema.

# task-bundle/manifest.yml — proposed local contract
budget:
  max_files: 12
  max_diff_bytes: 65536
test: "python -m pytest -q tests/unit"
exclude:
  - ".env"
  - "*.pem"

Copy the manifest with the sparse sources. The generator may ignore budget if unsupervised. Your apply host must enforce the budget later.

Failure domains

Draw four boxes and refuse to merge them. The prompt box can lie without any warning. The scratch box can be wiped without grief.

The review box can stall and must stall. The apply box must stay dull forever. Dull apply is a feature, not a tax.

If the model prompt-injects itself, damage stays scratch-side. If the free server disk leaks, you lose bundles. You do not lose production deploy keys then.

That is the point of the plane split. If review is skipped, the architecture already failed. A pretty green test log is not review.

Tests ran on the generator's own chosen word. The generator may have written those tests too. Believe tests after they run on trusted apply.

Network is its own failure domain as well. Scratch compute should not reach your prod APIs. It should not reach cloud metadata services either.

It should not reach canonical git with write rights. A common leak is a shared Docker socket. Another leak is a cached cloud credential helper.

Another leak is a home directory rsync copy. Each leak collapses two planes into one plane. You want failure to be loud and local.

A killed scratch virtual machine is cheap failure. A rewritten main branch is expensive failure instead. Choose the cheap failure on purpose every time.

# proposed isolation checks before a generate session
test -z "${SSH_AUTH_SOCK:-}" || echo "agent socket is visible"
test ! -S /var/run/docker.sock || echo "docker socket is mounted"
env | grep -E 'AWS_|GITHUB_|KUBE' || true
git -C "$CANONICAL_ROOT" remote -v

Run those checks on the laptop before copy. Do not run them only on the generator. The generator has incentive to lie about them.

What you should change next

Stop mounting the repo into the agent runtime. Copy a sparse task bundle instead of mounts. Delete the scratch tree after the diff lands.

Keep the review inbox on a machine you trust. Add an ownership check on every apply step. The applying identity must not be the generator.

Different keys, different hosts, and different audit logs. Same human is fine, same principal is not. Cap the diff before charm can hide scope.

A generator that rewrites the monorepo is not helping. Reject diffs over a file budget you set. Architecture is also about saying no early.

# proposed size gate on the apply host
files=$(grep -c '^diff --git ' "$REVIEW_DIFF" || true)
bytes=$(wc -c < "$REVIEW_DIFF")
test "$files" -le 12 || { echo "too many files"; exit 1; }
test "$bytes" -le 65536 || { echo "diff too large"; exit 1; }

Do not chase model scores as the next move. The real bottleneck is the apply path today. Harden that path before you add more prompts.

A calmer apply path beats a louder model. If you need throwaway generation compute, split first. The free server option belongs on scratch only.

Do not point that plane at canonical state. Who should skip this two-plane design completely? Solo prototypes on throwaway repos can ignore it.

Kata folders you will delete tonight can too. Production history cannot ignore the split though. Customer data and deploy keys cannot ignore it.

This approach will annoy you at first pass. Extra copies feel slow and purely ceremonial now. That ceremony is the review boundary in motion.

Speed without planes is just shared fate. Limitations are real and you should name them. Sparse bundles miss local context the model wants.

Remote scratch hosts can vanish during a run. The guard script cannot parse every patch trick. You still need a human on the apply box.

You now have a two-plane architecture, not prompts. Keep cooking in the kitchen on purpose. Serve only what survived the counter pass.

Leave the dining room boring on purpose too. Boring history is the prize you keep. Let the agent rage in scratch, not origin.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.