DEV Community

Harper Xu
Harper Xu

Posted on

Pin the Patch Contract, Not the Model

A remote generator is a vendor you cannot pin. Pin the patch contract instead of the endpoint. Treat every model swap as an architecture event.

Chat output is not a safe apply API. That leftover hole sits in the contract. An unpinned model can ship a well-formed lie.

The constraint you actually have

You do not own the remote model you call. You do not pin its weights like a library. You also do not freeze its decoding path.

A free remote path makes this constraint visible. The generator can move overnight without any notice. Your repository does not move with that generator.

Your old tests may still pass after the swap. They mostly remember yesterday's habits and shapes. They do not name today's intent at all.

Think of the model as a shipping carrier. You do not control the truck on that route. You control the crate and the seal.

MonkeyCode offers free model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Use that path as an untrusted vendor. Never hand that path your apply credentials.

Data flow worth drawing

Keep the data flow one-directional and dull. Intent leaves your repo as a typed request. A patch returns as a typed envelope.

Apply never reads chat prose as input. Chat remains a scratchpad only, nothing more. The envelope is the object you store.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "PatchEnvelope",
  "type": "object",
  "additionalProperties": false,
  "required": ["intent_id", "base_sha", "files", "tests"],
  "properties": {
    "intent_id": { "type": "string", "minLength": 8 },
    "base_sha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
    "files": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["path", "action", "diff"],
        "properties": {
          "path": {
            "type": "string",
            "pattern": "^(src|tests)/[A-Za-z0-9_./-]+$"
          },
          "action": { "enum": ["modify", "add"] },
          "diff": { "type": "string", "minLength": 1 }
        }
      }
    },
    "tests": {
      "type": "array",
      "minItems": 1,
      "items": { "type": "string", "pattern": "^tests/" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Save the schema as contracts/patch-envelope.schema.json. The schema is the pin you actually own. The model is not the pin you own.

Your generator may run on a free remote server. That server still sits outside the apply room. Source leaves your tree only as packed intent.

Secrets stay on your side of the door. Envelopes come back for review on your side. Review never happens in the generator room itself.

# tools/pack_intent.py
# Proposal: freeze an intent before any remote call.
import argparse, json, subprocess, datetime, sys

parser = argparse.ArgumentParser()
parser.add_argument("--intent-id", required=True)
parser.add_argument("--goal", required=True)
parser.add_argument("--allow", action="append", required=True)
args = parser.parse_args()
base = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
intent = {
    "intent_id": args.intent_id,
    "goal": args.goal,
    "base_sha": base,
    "allow": args.allow,
    "packed_at": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
}
json.dump(intent, sys.stdout, indent=2)
print(file=sys.stderr)
Enter fullscreen mode Exit fullscreen mode
python3 tools/pack_intent.py \
  --intent-id "timeout-$(date +%s)" \
  --goal "fail closed when the HTTP client exceeds 2s" \
  --allow 'src/**' --allow 'tests/**' \
  > /tmp/intent.json
Enter fullscreen mode Exit fullscreen mode

That command does not generate any code. It only freezes the request for later use. Generation runs later against this frozen file.

Do not generate against a chat scroll. Chat scrolls are not replayable objects later. Frozen files survive a later model swap.

Failure domains

Draw three rooms with locked doors between them. The intent room holds a repo view. The generator room holds the remote model only.

The apply room holds your apply credentials. A model swap lives only next door. It must not open the apply door.

Schema failure needs a fourth locked room. Call that fourth room the envelope gate. The gate holds no apply credentials at all.

If the envelope fails schema, you drop it. Do not tidy a bad envelope inside apply. Hand repair mixes vendor text with trusted edits.

Network failure is not an apply retry. You may retry generate only after a drop. Apply then needs a completely fresh envelope.

Give that envelope a new intent id. Colliding intent ids hide those retries from audit. Hidden retries will break a later audit.

Model drift looks like a quiet roommate. The produced diff can still apply cleanly. Your old tests can still pass afterward.

The new behavior was never named in tests. That silence is the real outage mode. Passing tests are not a named contract.

The envelope therefore requires named tests for the intent. Those tests are not the whole suite. They are this intent's contract, nothing else.

A gate you can run locally

Treat the next script as a proposal. It fails closed on purpose, not open. It is not a production service yet.

# tools/gate_envelope.py
# Proposal: fail closed on schema, path, or base mismatch.
import json, subprocess, sys, pathlib
from jsonschema import Draft202012Validator

ROOT = pathlib.Path(__file__).resolve().parents[1]
schema = json.loads((ROOT / "contracts/patch-envelope.schema.json").read_text())
envelope = json.loads(pathlib.Path(sys.argv[1]).read_text())
Draft202012Validator(schema).validate(envelope)

head = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
if envelope["base_sha"] != head:
    sys.exit("base_sha does not match HEAD; regenerate, do not apply")

for item in envelope["files"]:
    path = pathlib.Path(item["path"])
    if path.is_absolute() or ".." in path.parts:
        sys.exit(f"rejected path: {path}")
    if item["action"] == "modify" and not path.exists():
        sys.exit(f"modify target missing: {path}")

print("envelope accepted for review")
Enter fullscreen mode Exit fullscreen mode

Install the checker, then run it locally. Do this before any apply step runs. Skip this step and the rooms collapse.

python3 -m pip install jsonschema
python3 tools/gate_envelope.py /tmp/envelope.json
git apply --check /tmp/proposed.diff
Enter fullscreen mode Exit fullscreen mode

A dry apply check is not behavior proof. It only proves that the diff parses. Behavior proof lives in the named tests.

mapfile -t T < <(python3 -c 'import json,sys; print("\n".join(json.load(open(sys.argv[1]))["tests"]))' /tmp/envelope.json)
pytest -q "${T[@]}"
Enter fullscreen mode Exit fullscreen mode

If the model also wrote those tests, stop. You have built a circular test gate. Circular gates smile and let poison through.

Write the tests in the intent room. Keep high-risk tests off the model path. Let the model fill implementation details only.

Add a stub generator in CI next. The stub must emit invalid envelopes on purpose. CI must fail when the gate is missing.

# tools/stub_generator.py
# Proposal: negative fixture for the envelope gate.
import json, sys
bad = {
    "intent_id": "x",
    "base_sha": "not-a-sha",
    "files": [{"path": "../etc/passwd", "action": "modify", "diff": ""}],
    "tests": ["not-a-test"],
}
json.dump(bad, sys.stdout)
Enter fullscreen mode Exit fullscreen mode
python3 tools/stub_generator.py --invalid > /tmp/bad.json || true
if python3 tools/gate_envelope.py /tmp/bad.json; then
  echo "gate failed open" >&2
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

A green pipeline with no negative stub is theater. Theater is how drift lands in main. Force that drift to die in generate.

Decision matrix

Use this table as a hard gate. Do not use mood as a substitute. Mood belongs only in later review comments.

Signal Generator room Apply room
Model endpoint changed Regenerate envelope Block
Schema invalid Drop output Block
base_sha mismatch Pack a new intent Block
Path outside allowlist Drop output Block
Named tests missing Reject intent Block
Named tests pass, extra files appear Drop output Block
Reviewer note only, no envelope Ignore Block

The apply column is boring on purpose. Apply does not improvise on bad input. Improvisation stays in the generate room only.

Cheap failure belongs in the generate room. Expensive failure belongs nowhere in this design. The gate exists to keep them apart.

What you would change next

Store intent, envelope, and test output together. That bundle is your replay handle later. Replay is how you survive a model swap.

Then shrink what you send on the remote path. Stop shipping whole files for a local change. Send a function body when that is enough.

Less repo in the generator room always helps. The blast radius shrinks with the payload. Hostile disks then hold less of you.

If you use a free server, treat its disk as hostile. Do not mount your working tree on it. Send the packed intent file only, nothing else.

Add dual control on apply as a later change. One person accepts the envelope in review. Another person then runs the apply command.

Chat approval is not dual control at all. Chat history is vapor after the session. Vapor does not bind an apply action.

Limitations

This gate does not score model quality. It checks shape, path, and named tests. A wrong patch can still fit the crate.

The schema forbids deletes and absolute paths today. That rule will block some legitimate work. Widen it only with an explicit intent flag.

Remote generation moves source across a public network. Skip this remote path for any secrets. Skip it for customer data as well.

Skip it for unreleased product code too. Air-gapped teams should generate only on local hosts. Some teams should not generate remotely at all.

Teams without a human reviewer should stop here. The gate is a lock, not judgment. Locks do not read product intent for you.

Do not point apply credentials at the generator host. A free server remains a separate room. Shared credentials merge the rooms you drew.

If you try a free remote generator, start small. Use a throwaway branch and this gate. Leave production apply untouched until the gate holds.

Top comments (0)