Unknown keys must die before any tool runs. A local schema gate can prove that rule. If the gate fails, kill the idea in ninety minutes.
The result you need
Ship only a gate that rejects unknown keys. Kill the spike when one bad payload passes. An empty failure sample is inconclusive, not a win.
This is a preflight check, not a model review. The tool stays offline until the log says ACCEPT.
One hypothesis
A tool caller will emit keys the schema never listed. It may also fill optional keys the task never supplied. The gate must stop unknown keys before any socket opens.
That single bet is the entire spike scope. A second hypothesis needs its own separate clock. Do not widen the question at minute forty.
Why this angle now
Agent write-ups move fast in early October 2026. Many of them show a happy path and a screenshot. Few show the reject path with a fixture id.
This spike ignores vendor headlines on purpose today. Those numbers age faster than a schema rule. The contract under test is plain JSON, which you can diff tomorrow.
No benchmark against other hosts appears in this note. No latency claim appears in this spike writeup. Add numbers only from a log you captured the same day.
Clock and kill rules
You get ninety minutes and only one question. These rules stay fixed for the whole clock.
Fixed blocks
- Minute 0 to 20 covers the schema card plus pure gate.
- Minute 20 to 35 requires hand fixtures with zero fails.
- Minute 35 to 60 saves model drafts as files only.
- Minute 60 to 80 runs drafts through the same gate.
- Minute 80 to 90 writes ship or kill with row ids.
Automatic kills
No new tool enters the scope after minute 35. No live create call is allowed in this clock. A network attempt before ACCEPT is an automatic kill.
Schema card
Use one tool only, named create_issue, for this clock. Allowed keys are title, body, and labels. Required keys for this card are title and body.
The labels field is an optional array of strings. Unknown keys are always errors in this gate. Missing required keys are always errors here too.
Wrong JSON types are always errors in this subset. This subset does not walk nested objects at all. Write that limit in the log header.
Gate implementation
Save the following listing as gate.py before the clock. The listing uses the Python standard library only. It is a spike subset, not a full JSON Schema compiler.
import json
import sys
ALLOWED = {"title": str, "body": str, "labels": list}
REQUIRED = ("title", "body")
class GateError(ValueError):
pass
def check(payload):
if not isinstance(payload, dict):
raise GateError("payload is not an object")
extra = sorted(set(payload) - set(ALLOWED))
if extra:
raise GateError("unknown keys: " + ",".join(extra))
missing = [key for key in REQUIRED if key not in payload]
if missing:
raise GateError("missing keys: " + ",".join(missing))
for key, kind in ALLOWED.items():
if key not in payload:
continue
if not isinstance(payload[key], kind):
raise GateError("bad type: " + key)
labels = payload.get("labels")
if labels is not None and not all(isinstance(item, str) for item in labels):
raise GateError("labels must be strings")
return payload
def main():
try:
check(json.loads(sys.stdin.read()))
except (json.JSONDecodeError, GateError) as exc:
print("REJECT", exc)
return 2
print("ACCEPT")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Hand fixtures
Save the cases below as fixtures.jsonl in the repo. Each line in that file is one case.
{"id":"g1","expect":"ACCEPT","payload":{"title":"t","body":"b"}}
{"id":"g2","expect":"ACCEPT","payload":{"title":"t","body":"b","labels":["bug"]}}
{"id":"b1","expect":"REJECT","payload":{"title":"t","body":"b","priority":"p0"}}
{"id":"b2","expect":"REJECT","payload":{"title":"t","body":"b","assignee":"sam"}}
{"id":"b3","expect":"REJECT","payload":{"title":"t"}}
{"id":"b4","expect":"REJECT","payload":{"title":1,"body":"b"}}
{"id":"b5","expect":"REJECT","payload":{"title":"t","body":"b","labels":"bug"}}
{"id":"b6","expect":"REJECT","payload":{}}
Add four more reject lines before the model step. Use repo, team, severity, and url as the extra keys. Keep every expected result for those lines as REJECT. Do not delete a fail to stay green.
Local runner
Save the runner below as run_gate.py in the same folder.
import json
import subprocess
import sys
def judge(line):
row = json.loads(line)
proc = subprocess.run(
[sys.executable, "gate.py"],
input=json.dumps(row["payload"]),
text=True,
capture_output=True,
)
got = "ACCEPT" if proc.returncode == 0 else "REJECT"
return row["id"], got == row["expect"], got, row["expect"]
def main():
fails = 0
with open("fixtures.jsonl", encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if not line:
continue
fid, ok, got, expect = judge(line)
print(f"{fid} ok={ok} got={got} expect={expect}")
fails += int(not ok)
print("FAILS", fails)
return 1 if fails else 0
if __name__ == "__main__":
raise SystemExit(main())
Run the hand file before any model draft exists.
python run_gate.py
You want the final line to read FAILS 0. Any other fail count stops the clock at once. Fix the gate, then restart the ninety minutes.
Do not carry a dirty gate into the next block. This sample below is an illustration, not a captured log.
g1 ok=True got=ACCEPT expect=ACCEPT
b1 ok=True got=REJECT expect=REJECT
FAILS 0
Decision table
| Signal | Meaning | Next action |
|---|---|---|
| Hand fixtures fail | Gate defect | Restart after the fix |
| Unknown key accepted | Hole in the gate | Kill the approach |
| Optional key filled | Policy gap, not a type bug | Log it, do not post |
| Output is not JSON | Parse reject | Count as reject, no I/O |
| Zero bad model drafts | Sample too small | Mark inconclusive |
| Socket opened before ACCEPT | Protocol breach | Kill immediately |
The decision table is the only pass rule. Do not edit a row after seeing model output. That move repeats a known failure mode: editing the check after the run.
Draft prompts
Two prompts, one contrast
Ask for one JSON object and nothing else. Save every raw reply under the drafts directory. Never pipe those replies into a live client.
Prompt A explicitly forbids keys outside the card. Prompt B omits that warning on purpose entirely. The contrast between those prompts is the evidence.
A single prompt cannot support a ship vote. Strip markdown fences before gate.py sees the file. A markdown fence is not a valid payload.
Count fence-only replies as rejects in the log. Paste prompt A first, then prompt B, as separate files. Keep the task text identical across both files.
Return one JSON object only.
Tool name: create_issue.
Allowed keys: title, body, labels.
Required keys: title, body.
Task: report that empty labels were dropped.
Do not add keys outside the allowed set.
Return one JSON object only.
Tool name: create_issue.
Allowed keys: title, body, labels.
Required keys: title, body.
Task: report that empty labels were dropped.
Where the free tier fits
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode enters only after the hand fixtures pass.
Free model access can produce the drafts in minute 35 to 60. A free server can run the same gate off the laptop. The outreach frames MonkeyCode as an open-source project with those two free options.
Claims to verify
Outreach for this piece states two availability claims. One is a free allowance of 10 million tokens. The other claim is a free server option.
Neither claim names a model, a region, or a hardware shape in this draft. Confirm both claims on the project docs the day you run. Copy the live figures into the spike log.
If the docs disagree with this paragraph, trust the docs. Do not treat 10 million as a permanent quota. Confirm the license in the repository you actually clone.
Order of operations
Free tokens do not bypass unknown-key checks at all. A free host does not make a side effect safe. Generate draft files first, before any accept decision.
Accept or reject those files in the second step. Call no remote tool in the third step.
Server constraint
Deploy gate.py to a free server only after an outbound check. The gate file itself opens no sockets at all. The risk is the host policy around it.
Run the probe on the remote host, not as a test of your laptop. A laptop connect often succeeds on a normal network. That result does not kill a local gate.
python -c "import socket; s=socket.socket(); s.settimeout(3); s.connect(('example.com', 443))"
If that probe succeeds on the free server, do not place tool credentials there. Keep the gate on the laptop for this clock. Record the host probe result in the spike log.
A timeout or refusal on the server supports a tighter boundary. Still store no issue-creating token in the spike. The gate does not need a write token.
Counts worth keeping
Write these fields in the log before you stop. Leave a field blank when the step did not run.
- Record the date of the same-day docs check.
- Record the hand fixture count from the file.
- Record the hand fail count from the runner.
- Record the draft file count under drafts.
- Record the reject count from the gate log.
- Record the accept count from the gate log.
- Record every network attempt that happened before ACCEPT.
- Write one ship or kill sentence at the end.
Blank is valid evidence of an unrun step. A filled guess is not evidence in this log. Do not convert blanks into zeros after the clock.
Ship sentence
Ship only when these four facts hold together. Hand fixtures must show the line FAILS 0. Every unknown-key draft must return the word REJECT.
Network attempts before ACCEPT must stay at zero. Docs for token and server terms were read the same day. Any missed fact is a kill for this clock.
Name the row id in the kill sentence. Open a new spike later with a new clock. Do not extend this one past ninety minutes.
Who should not use it
Skip the spike when free-form maps are a real product requirement. Skip it when you need full draft 2020-12 coverage today. Skip it when you cannot block outbound calls.
Skip it when the goal is a demo recording. This gate is not an authorization layer at all. Server-side checks still have to run after it.
A local ACCEPT is not permission to write production data. Treat production writes as a different spike with a new clock.
Limits of the subset
Nested objects are out of scope for this file. Format checks such as URI or date-time are out of scope. A non-empty wrong title can still pass.
Length caps belong in a later, separate spike. Optional labels may show up when the task omitted them. The type gate still allows that optional key.
A product policy may still reject that optional key. Keep that policy in a second function so ACCEPT stays narrow and honest. This article claims no speedup, no win rate, and no host comparison.
Publish a number only with the command, the date, and the raw output. Otherwise leave that metric cell blank in the log.
After ninety minutes
Commit the fixture file if the gate held. Add one reject line for each surprising key you actually saw. That loop matters more than a new prompt trick.
The next clock may attach a dry-run client. Live writes wait for a later ship sentence. The same rule still holds: no socket before ACCEPT.
If the hand gate is already green, read MonkeyCode's current docs. Confirm the free token allowance and the free server terms. Use that access only for draft files in a fresh clock.
Top comments (0)