{
    "componentChunkName": "component---src-templates-blog-post-js",
    "path": "/my-infrastructure-accepts-pull-requests-from-a-language-model-now/",
    "result": {"data":{"site":{"siteMetadata":{"title":"catch { snail }","author":"Andrew"}},"markdownRemark":{"id":"0ec13999-ad81-5f85-ab2e-f8d06c8a10eb","excerpt":"Most of my infrastructure changes are not engineering. They’re dictation: “add memory limits to the api pod,” “bump that image tag,” “open port 9090 for…","html":"<p>Most of my infrastructure changes are not engineering. They’re <em>dictation</em>: “add memory limits to the api pod,” “bump that image tag,” “open port 9090 for prometheus.” I know the change before I open the editor; the editor is just where I go to make typos in YAML.</p>\n<p>Language models are extremely good at dictation. They are also confident liars. The whole design question of AI automation is how to get the first property without being destroyed by the second, and I think I’ve landed on an answer I actually trust in production:</p>\n<p><strong>The model doesn’t get access to anything. It gets a pen.</strong> It writes pull requests. The PR must survive a gauntlet of mechanical validation before a human ever sees it, the human merges, and GitOps applies whatever lands in <code class=\"language-text\">main</code> — same as any other contributor. The model isn’t an operator. It’s a very fast junior colleague with zero credentials and excellent handwriting.</p>\n<h2 id=\"the-pipeline\" style=\"position:relative;\"><a href=\"#the-pipeline\" aria-label=\"the pipeline permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The pipeline</h2>\n<p>Five stages, one script, no daemon:</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">request ──► context ──► generate ──► gauntlet ──► pull request\n (text)      (repo)     (codex)      (CI tools)      (human)</code></pre></div>\n<p>The entry point is deliberately dumb — a CLI:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">./scribe <span class=\"token string\">\"add memory limits (512Mi) and cpu limits (500m) to the api deployment\"</span></code></pre></div>\n<h2 id=\"stage-12-context-then-generation\" style=\"position:relative;\"><a href=\"#stage-12-context-then-generation\" aria-label=\"stage 12 context then generation permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Stage 1–2: context, then generation</h2>\n<p>Models write better infrastructure when they can see the file they’re changing, so the prompt is mostly quotation. One thing I learned early and will fight about: <strong>ask for the whole new file, never a diff.</strong> Models produce plausible-looking patches with wrong line numbers and off-by-one context — a diff is a <em>positional</em> format and the model has no position, only text. Ask for the full file; compute the diff yourself with tools that can’t imagine things.</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\"><span class=\"token keyword\">import</span> openai<span class=\"token punctuation\">,</span> pathlib<span class=\"token punctuation\">,</span> sys\n\n<span class=\"token keyword\">def</span> <span class=\"token function\">generate</span><span class=\"token punctuation\">(</span>request<span class=\"token punctuation\">:</span> <span class=\"token builtin\">str</span><span class=\"token punctuation\">,</span> path<span class=\"token punctuation\">:</span> <span class=\"token builtin\">str</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">-</span><span class=\"token operator\">></span> <span class=\"token builtin\">str</span><span class=\"token punctuation\">:</span>\n    current <span class=\"token operator\">=</span> pathlib<span class=\"token punctuation\">.</span>Path<span class=\"token punctuation\">(</span>path<span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>read_text<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n    prompt <span class=\"token operator\">=</span> <span class=\"token string-interpolation\"><span class=\"token string\">f\"\"\"# Task: modify the Kubernetes manifest below.\n# Change requested: </span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>request<span class=\"token punctuation\">}</span></span><span class=\"token string\">\n# Rules: output the complete new file, valid YAML, no commentary.\n# Do not add, remove, or rename any resources unless asked.\n\n# Current file (</span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>path<span class=\"token punctuation\">}</span></span><span class=\"token string\">):\n</span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>current<span class=\"token punctuation\">}</span></span><span class=\"token string\">\n\n# Complete new file:\n\"\"\"</span></span>\n    resp <span class=\"token operator\">=</span> openai<span class=\"token punctuation\">.</span>Completion<span class=\"token punctuation\">.</span>create<span class=\"token punctuation\">(</span>\n        model<span class=\"token operator\">=</span><span class=\"token string\">\"code-davinci-002\"</span><span class=\"token punctuation\">,</span>   <span class=\"token comment\"># codex beta: currently free, absurdly</span>\n        prompt<span class=\"token operator\">=</span>prompt<span class=\"token punctuation\">,</span>\n        max_tokens<span class=\"token operator\">=</span><span class=\"token number\">1500</span><span class=\"token punctuation\">,</span>\n        temperature<span class=\"token operator\">=</span><span class=\"token number\">0</span><span class=\"token punctuation\">,</span>              <span class=\"token comment\"># dictation, not poetry</span>\n        stop<span class=\"token operator\">=</span><span class=\"token punctuation\">[</span><span class=\"token string\">\"# Task:\"</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">)</span>\n    <span class=\"token keyword\">return</span> resp<span class=\"token punctuation\">.</span>choices<span class=\"token punctuation\">[</span><span class=\"token number\">0</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">.</span>text<span class=\"token punctuation\">.</span>strip<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span></code></pre></div>\n<p>Temperature 0 because there is exactly one correct way to add a memory limit and I’d like it every time. Which file to edit is either given on the CLI or picked by a first, tiny completion that sees only <code class=\"language-text\">git ls-files '*.yaml'</code> — choosing from a real list is a task models are hard to break on, since every possible answer already exists.</p>\n<h2 id=\"stage-3-the-gauntlet\" style=\"position:relative;\"><a href=\"#stage-3-the-gauntlet\" aria-label=\"stage 3 the gauntlet permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Stage 3: the gauntlet</h2>\n<p>Here’s the heart of the design. I never evaluate the model’s output by reading it — reading is how tired humans get fooled by confident text. The output must instead survive tools that are incapable of being charmed:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token shebang important\">#!/bin/sh</span>\n<span class=\"token builtin class-name\">set</span> -e  <span class=\"token comment\"># any failure kills the PR before it exists</span>\n\nyamllint -d relaxed <span class=\"token string\">\"<span class=\"token variable\">$FILE</span>\"</span>                      <span class=\"token comment\"># 1. is it even YAML</span>\nkubeconform -strict -summary <span class=\"token string\">\"<span class=\"token variable\">$FILE</span>\"</span>             <span class=\"token comment\"># 2. is it valid *Kubernetes* —</span>\n                                                 <span class=\"token comment\">#    schemas don't negotiate</span>\nkubectl <span class=\"token function\">diff</span> -f <span class=\"token string\">\"<span class=\"token variable\">$FILE</span>\"</span> --server-side <span class=\"token operator\">||</span> <span class=\"token boolean\">true</span>    <span class=\"token comment\"># 3. what would actually change</span>\nkubectl apply -f <span class=\"token string\">\"<span class=\"token variable\">$FILE</span>\"</span> --dry-run<span class=\"token operator\">=</span>server        <span class=\"token comment\"># 4. would the API server accept it</span>\n\n<span class=\"token comment\"># terraform changes get the equivalent treatment:</span>\nterraform plan -detailed-exitcode -out<span class=\"token operator\">=</span>tf.plan   <span class=\"token comment\"># exit 2 = changes, 1 = broken</span>\nterraform show tf.plan <span class=\"token operator\">|</span> <span class=\"token function\">grep</span> -q <span class=\"token string\">\"destroy\"</span> <span class=\"token operator\">&amp;&amp;</span> <span class=\"token builtin class-name\">exit</span> <span class=\"token number\">1</span>   <span class=\"token comment\"># the bot may not destroy. ever.</span></code></pre></div>\n<p>Then the paranoid layer — a blast-radius allowlist enforced on the <em>diff</em>, not the intent:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">git</span> <span class=\"token function\">diff</span> --name-only main <span class=\"token operator\">|</span> <span class=\"token function\">grep</span> -vE <span class=\"token string\">'^k8s/(deployments|services|ingress)/'</span> <span class=\"token punctuation\">\\</span>\n  <span class=\"token operator\">&amp;&amp;</span> <span class=\"token punctuation\">{</span> <span class=\"token builtin class-name\">echo</span> <span class=\"token string\">\"touched files outside the sandbox\"</span><span class=\"token punctuation\">;</span> <span class=\"token builtin class-name\">exit</span> <span class=\"token number\">1</span><span class=\"token punctuation\">;</span> <span class=\"token punctuation\">}</span>\n<span class=\"token function\">git</span> <span class=\"token function\">diff</span> main <span class=\"token operator\">|</span> <span class=\"token function\">grep</span> -qE <span class=\"token string\">'kind:\\s*(Secret|PersistentVolume)'</span> <span class=\"token punctuation\">\\</span>\n  <span class=\"token operator\">&amp;&amp;</span> <span class=\"token punctuation\">{</span> <span class=\"token builtin class-name\">echo</span> <span class=\"token string\">\"nice try\"</span><span class=\"token punctuation\">;</span> <span class=\"token builtin class-name\">exit</span> <span class=\"token number\">1</span><span class=\"token punctuation\">;</span> <span class=\"token punctuation\">}</span></code></pre></div>\n<p>The model can <em>want</em> whatever it likes. The gauntlet only cares what the bytes do. Every check here existed before language models and will outlive them — that’s the point. <strong>Generation is allowed to be creative; validation is a compiler.</strong> Never staff both jobs with the same kind of mind.</p>\n<h2 id=\"stage-3-one-round-of-feedback-not-a-loop\" style=\"position:relative;\"><a href=\"#stage-3-one-round-of-feedback-not-a-loop\" aria-label=\"stage 3 one round of feedback not a loop permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Stage 3½: one round of feedback, not a loop</h2>\n<p>When a check fails, the error message goes back into the prompt for exactly one retry:</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\">retry_prompt <span class=\"token operator\">=</span> prompt <span class=\"token operator\">+</span> generated <span class=\"token operator\">+</span> <span class=\"token string-interpolation\"><span class=\"token string\">f\"\"\"\n\n# The file above failed validation with this error:\n# </span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>stderr<span class=\"token punctuation\">}</span></span><span class=\"token string\">\n# Output the corrected complete file:\n\"\"\"</span></span></code></pre></div>\n<p>This fixes a surprising share of failures — schema validators write great error messages, and the model reads them better than I do at 9am. But it’s one round, hard-coded. An unbounded fix-it-yourself loop is a self-licking ice cream cone that burns tokens converging on nothing; if two attempts can’t pass a linter, the request was ambiguous and a human should hear about it. Loops need leashes.</p>\n<h2 id=\"stage-4-the-pr-is-the-audit-log\" style=\"position:relative;\"><a href=\"#stage-4-the-pr-is-the-audit-log\" aria-label=\"stage 4 the pr is the audit log permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Stage 4: the PR is the audit log</h2>\n<p>Survivors become pull requests, and the PR body carries the receipts:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">git</span> checkout -b <span class=\"token string\">\"scribe/<span class=\"token variable\">${SLUG}</span>\"</span>\n<span class=\"token function\">git</span> commit -am <span class=\"token string\">\"scribe: <span class=\"token variable\">${REQUEST}</span>\"</span> --author<span class=\"token operator\">=</span><span class=\"token string\">\"Scribe &lt;scribe@ohsnail.com>\"</span>\ngh <span class=\"token function\">pr</span> create <span class=\"token punctuation\">\\</span>\n  --title <span class=\"token string\">\"🖋 <span class=\"token variable\">${REQUEST}</span>\"</span> <span class=\"token punctuation\">\\</span>\n  --body <span class=\"token string\">\"<span class=\"token variable\"><span class=\"token variable\">$(</span><span class=\"token function\">cat</span> <span class=\"token operator\">&lt;&lt;</span><span class=\"token string\">EOF\n**Request:** <span class=\"token variable\">${REQUEST}</span>\n**Server dry-run diff:**\n\\<span class=\"token variable\"><span class=\"token variable\">`</span><span class=\"token punctuation\">\\</span><span class=\"token variable\">`</span></span>\\<span class=\"token variable\"><span class=\"token variable\">`</span>\n<span class=\"token punctuation\">$(</span>kubectl <span class=\"token function\">diff</span> -f <span class=\"token string\">\"<span class=\"token variable\">$FILE</span>\"</span> --server-side<span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">\\</span><span class=\"token variable\">`</span></span>\\<span class=\"token variable\"><span class=\"token variable\">`</span><span class=\"token punctuation\">\\</span><span class=\"token variable\">`</span></span>\n**Gauntlet:** yamllint ✓ kubeconform ✓ dry-run ✓ blast-radius ✓\nEOF</span>\n<span class=\"token variable\">)</span></span>\"</span></code></pre></div>\n<p>Review takes seconds, because I’m not reviewing prose or trust — I’m reviewing a machine-verified diff with the evidence attached. Merge, and the GitOps controller ships it like any other commit. The model never held a credential; the merge button is the last mile of intent, and it stays human.</p>\n<h2 id=\"ten-weeks-of-numbers\" style=\"position:relative;\"><a href=\"#ten-weeks-of-numbers\" aria-label=\"ten weeks of numbers permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Ten weeks of numbers</h2>\n<p>38 requests since March. <strong>26 merged untouched. 7 merged after small edits</strong> (it loves adding labels nobody asked for — the junior-colleague energy is uncanny). <strong>3 correctly killed by the gauntlet</strong> — the best one invented <code class=\"language-text\">apiVersion: apps/v2</code>, a version that does not exist, delivered with total confidence and executed by kubeconform in four milliseconds. <strong>2 died in ambiguity</strong>, as they should have: my request was the bug.</p>\n<p>Time per dictation-class change: was ~10 minutes of editor and kubectl archaeology, now ~40 seconds of reading a diff. But the number I care about is different: <strong>zero incidents caused, zero possible by construction</strong> — the failure domain is “a bad PR gets opened,” and a bad PR is litter, not fire.</p>\n<h2 id=\"the-design-rule-id-generalize\" style=\"position:relative;\"><a href=\"#the-design-rule-id-generalize\" aria-label=\"the design rule id generalize permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The design rule I’d generalize</h2>\n<p>Everyone building on these models is choosing a point on a line between “autocomplete” and “autonomy,” and most of the debate treats capability as the axis that matters. I don’t think it is. The axis that matters is <strong>what artifact the model produces</strong>. Give it a terminal and its mistakes are <em>events</em>. Give it a pen and its mistakes are <em>documents</em> — diffable, lintable, rejectable, versioned, reviewed. Pull requests turn out to be a nearly perfect prosthetic for a brilliant liar: all of the leverage, none of the blast radius, and an audit trail as a side effect.</p>\n<p>Don’t give the model hands. Give it a pen, and make the paper do the vetting.</p>","frontmatter":{"title":"My infrastructure accepts pull requests from a language model now","date":"June 06, 2022","description":"Don't give the model hands — give it a pen. A pipeline where GPT writes Kubernetes and Terraform changes as PRs that must survive a mechanical gauntlet before a human ever sees them"}}},"pageContext":{"slug":"/my-infrastructure-accepts-pull-requests-from-a-language-model-now/","previous":{"fields":{"slug":"/odd-bot-out-a-party-game-where-gpt-3-hides-among-your-relatives/"},"frontmatter":{"title":"Odd Bot Out: a party game where GPT-3 hides among your relatives"}},"next":{"fields":{"slug":"/twenty-questions-against-a-player-with-no-secret/"},"frontmatter":{"title":"Twenty questions against a player with no secret"}}}},
    "staticQueryHashes": ["3435731857","426816048","63159454"]}