<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>heyimusa</title>
    <link>https://heyimusa.blog/</link>
    <description></description>
    <pubDate>Thu, 03 Sep 2026 10:46:21 +0000</pubDate>
    <item>
      <title>GLM-5.3 vs Kimi K3 for agentic engineering: price, benchmarks, and the screenshot problem</title>
      <link>https://heyimusa.blog/glm-5-3-vs-kimi-k3-for-agentic-engineering</link>
      <description>&lt;![CDATA[  GLM-5.3 is much cheaper than Kimi K3 for text-heavy coding agents, and its published results are strong. That does not make it a drop-in replacement. Kimi K3 can inspect screenshots and other visual input; GLM-5.3 cannot. In an operations workflow, that boundary matters before any benchmark chart does.&#xA;    I compared the vendors&#39; current model cards, pricing pages, and the public Terminal-Bench leaderboard. This is a source-led comparison, not an independent bake-off. The published scores use different harnesses and budgets, so they are useful for choosing what to test, not for declaring a winner.&#xA;&#xA;!--more--&#xA;&#xA;The choice is not only about coding scores&#xA;&#xA;GLM-5.3 and Kimi K3 are both positioned as long-horizon agent models with a one-million-token context window. Both publish weights under model-specific licenses. Both are expensive enough to make sloppy agent loops visible on an invoice.&#xA;&#xA;The important difference is input modality.&#xA;&#xA;GLM-5.3 is text-only. It can work through logs, diffs, terminal output, Terraform plans, Kubernetes manifests, and a carefully written incident timeline. It cannot look at a Grafana panel, a browser console screenshot, or a failed UI state on its own.&#xA;&#xA;Kimi K3 accepts text and images. Its model card describes native multimodal support and a vision encoder. That makes it a more natural single-model option for workflows where a screenshot is part of the evidence, such as browser-driven regression triage, dashboard inspection, frontend validation, or CAD and diagram work.&#xA;&#xA;That distinction also limits what this article claims. Vision support does not prove that Kimi will diagnose a production issue better. Text-only input does not stop GLM from being useful when a separate vision or OCR step produces reliable text. The right question is whether the input boundary matches the work you actually expect an agent to do.&#xA;&#xA;The published coding results point in different directions&#xA;&#xA;Z.ai publishes a comparison table that includes Kimi K3. On that table, GLM-5.3 leads Kimi K3 on Terminal-Bench 3.0, PostTrainBench, CyberGym, AutomationBench, Agents&#39; Last Exam CLI, and several security-focused benchmarks. Kimi leads slightly on DeepSWE and more clearly on SWE-Marathon and Toolathlon Verified. Terminal-Bench 2.1 and NL2Repo are effectively tied.&#xA;&#xA;| Benchmark | GLM-5.3 | Kimi K3 | Higher published score |&#xA;|---|---:|---:|---|&#xA;| Terminal-Bench 2.1 | 88.2 | 88.3 | Kimi K3 by 0.1 |&#xA;| Terminal-Bench 3.0 | 28.3 | 17.4 | GLM-5.3 |&#xA;| DeepSWE v1.1 | 66.9 | 67.5 | Kimi K3 |&#xA;| NL2Repo | 58.0 | 58.0 | Tie |&#xA;| SWE-Marathon v1.1 | 42.5 | 48.1 | Kimi K3 |&#xA;| PostTrainBench | 39.8 | 32.0 | GLM-5.3 |&#xA;| CyberGym | 84.5 | 80.0 | GLM-5.3 |&#xA;| Toolathlon Verified | 73.0 | 76.5 | Kimi K3 |&#xA;| AutomationBench v1.0.6 | 48.2 | 46.7 | GLM-5.3 |&#xA;| Agents&#39; Last Exam CLI | 28.5 | 27.6 | GLM-5.3 |&#xA;&#xA;The security rows deserve caution. Z.ai reports much stronger GLM-5.3 results on ExploitGym and ExploitBench, but those are capability evaluations, not permission to hand an agent production credentials. For operational use, a stronger model should lead to narrower permissions, better logging, and more deliberate approval gates.&#xA;&#xA;There is one useful independent signal: Terminal-Bench 4.0 currently lists GLM-5.3 with Claude Code at 41.8% ± 3.2%. The same public leaderboard did not list Kimi K3 when I checked, so there is no current version-4 head-to-head result to quote.&#xA;&#xA;The methodology is part of the number&#xA;&#xA;The table looks cleaner than the evidence really is.&#xA;&#xA;Z.ai says it evaluated Terminal-Bench 3.0 with Claude Code 2.1.207, reasoning_effort=max, 400K context, up to 128K output tokens, three rollouts per task, a 600-turn cap, and a ten-hour timeout. Its DeepSWE setup uses the mini-swe-agent harness with a six-hour timeout and 400K context. Kimi&#39;s model card publishes its own suite and methodology. Those are not automatically the same agent implementation, prompt, sampling policy, timeout, or retry policy.&#xA;&#xA;A model that is cheaper per token may still be more expensive per completed task if it needs longer trajectories or more retries. A model that wins a terminal benchmark may still be a poor fit for a UI workflow if the decisive evidence is an image. Published scores should narrow the shortlist. They should not replace a test against the repositories, logs, screenshots, and review rules that matter to a team.&#xA;&#xA;Pricing changes how much room an agent gets to think&#xA;&#xA;At the current list prices, GLM-5.3 is materially cheaper for text generation.&#xA;&#xA;| Model | Input / 1M tokens | Cached input / 1M | Output / 1M |&#xA;|---|---:|---:|---:|&#xA;| GLM-5.3 | $1.40 | $0.26 | $4.40 |&#xA;| Kimi K3 | $3.00 | $0.30 | $15.00 |&#xA;&#xA;For a simple 100K-input, 10K-output request, that works out to about $0.184 with GLM-5.3 and $0.450 with Kimi K3 before provider-specific tool charges or routing markup. If all 100K input tokens are cache hits, the same request is about $0.070 for GLM and $0.180 for Kimi.&#xA;&#xA;That is not a promise of lower task cost. Long-running agents accumulate tool output, hidden reasoning, retries, and context. It does mean GLM leaves more budget for experiments when the job is mostly text: reviewing a large pull request, reading CI logs, comparing rendered GitOps artifacts, or iterating on a shell-based diagnosis.&#xA;&#xA;Kimi&#39;s premium buys a larger model and native visual input, not a universal result. If a workflow otherwise needs a separate OCR or vision model, the price comparison has to include that component and the operational risk of passing incomplete or distorted extracted text downstream.&#xA;&#xA;What I would test for a DevOps workflow&#xA;&#xA;I would not grant either model broad infrastructure access because a benchmark looked good. I would start with a closed, read-only evaluation set and measure the whole path.&#xA;&#xA;Use sanitized inputs: a CI failure bundle, rendered Kubernetes manifests, a Terraform plan, a short incident timeline, and a small set of browser or dashboard screenshots.&#xA;Give each model the same task contract, tool surface, timeout, and output schema. Keep write actions disabled.&#xA;Score claims against the source artifacts. A good answer should cite the specific log line, manifest field, or screenshot region that supports it.&#xA;Record completion rate, elapsed time, input and output tokens, retries, malformed tool calls, and total cost per completed task.&#xA;Add an approval boundary before anything that could mutate a cloud account, cluster, DNS record, deployment, or customer data.&#xA;&#xA;For text-only work, GLM-5.3 is the model I would evaluate first because the price-performance case is compelling. For a workflow that genuinely begins with screenshots, Kimi K3 avoids an extra model hop and deserves the same evaluation. The better result may be a routing policy rather than a single winner: vision-capable intake for visual evidence, then a cheaper text model for the long investigation and write-up.&#xA;&#xA;The short version&#xA;&#xA;GLM-5.3 has the more attractive economics for text-heavy agentic engineering, and its published results make it a serious candidate for terminal, repository, and automation work. Kimi K3 is more expensive, but it accepts visual input natively and remains competitive on several software-engineering and tool-use benchmarks.&#xA;&#xA;Neither result answers the operational question by itself. A useful agent needs evidence, bounded tools, and a way for a human to stop or reject the next action. That matters more than a one-point difference on a leaderboard.&#xA;&#xA;---&#xA;&#xA;Test notes: This was a source-led comparison conducted on 29 August 2026. I checked Z.ai&#39;s GLM-5.3 model card and pricing documentation, Moonshot&#39;s Kimi K3 model card and technical blog, OpenRouter&#39;s current Kimi K3 provider-price display, and the public Terminal-Bench 4.0 leaderboard. I did not run either model, reproduce any benchmark, test vision quality, or validate API behavior. Dollar examples are arithmetic illustrations using published per-million-token rates and exclude provider markup, tool charges, taxes, hidden reasoning-token accounting, and retries.&#xA;&#xA;Sources: Z.ai GLM-5.3 model card · Z.ai GLM-5.3 documentation · Z.ai API pricing · Moonshot Kimi K3 model card · Moonshot Kimi K3 technical blog · Kimi K3 provider pricing · Terminal-Bench 4.0 leaderboard · Terminal-Bench 4.0 methodology update.]]&gt;</description>
      <content:encoded><![CDATA[<blockquote><p>GLM-5.3 is much cheaper than Kimi K3 for text-heavy coding agents, and its published results are strong. That does not make it a drop-in replacement. Kimi K3 can inspect screenshots and other visual input; GLM-5.3 cannot. In an operations workflow, that boundary matters before any benchmark chart does.</p>

<p>I compared the vendors&#39; current model cards, pricing pages, and the public Terminal-Bench leaderboard. This is a source-led comparison, not an independent bake-off. The published scores use different harnesses and budgets, so they are useful for choosing what to test, not for declaring a winner.</p></blockquote>



<h2 id="the-choice-is-not-only-about-coding-scores">The choice is not only about coding scores</h2>

<p>GLM-5.3 and Kimi K3 are both positioned as long-horizon agent models with a one-million-token context window. Both publish weights under model-specific licenses. Both are expensive enough to make sloppy agent loops visible on an invoice.</p>

<p>The important difference is input modality.</p>

<p>GLM-5.3 is text-only. It can work through logs, diffs, terminal output, Terraform plans, Kubernetes manifests, and a carefully written incident timeline. It cannot look at a Grafana panel, a browser console screenshot, or a failed UI state on its own.</p>

<p>Kimi K3 accepts text and images. Its model card describes native multimodal support and a vision encoder. That makes it a more natural single-model option for workflows where a screenshot is part of the evidence, such as browser-driven regression triage, dashboard inspection, frontend validation, or CAD and diagram work.</p>

<p>That distinction also limits what this article claims. Vision support does not prove that Kimi will diagnose a production issue better. Text-only input does not stop GLM from being useful when a separate vision or OCR step produces reliable text. The right question is whether the input boundary matches the work you actually expect an agent to do.</p>

<h2 id="the-published-coding-results-point-in-different-directions">The published coding results point in different directions</h2>

<p>Z.ai publishes a comparison table that includes Kimi K3. On that table, GLM-5.3 leads Kimi K3 on Terminal-Bench 3.0, PostTrainBench, CyberGym, AutomationBench, Agents&#39; Last Exam CLI, and several security-focused benchmarks. Kimi leads slightly on DeepSWE and more clearly on SWE-Marathon and Toolathlon Verified. Terminal-Bench 2.1 and NL2Repo are effectively tied.</p>

<table>
<thead>
<tr>
<th>Benchmark</th>
<th align="right">GLM-5.3</th>
<th align="right">Kimi K3</th>
<th>Higher published score</th>
</tr>
</thead>

<tbody>
<tr>
<td>Terminal-Bench 2.1</td>
<td align="right">88.2</td>
<td align="right">88.3</td>
<td>Kimi K3 by 0.1</td>
</tr>

<tr>
<td>Terminal-Bench 3.0</td>
<td align="right">28.3</td>
<td align="right">17.4</td>
<td>GLM-5.3</td>
</tr>

<tr>
<td>DeepSWE v1.1</td>
<td align="right">66.9</td>
<td align="right">67.5</td>
<td>Kimi K3</td>
</tr>

<tr>
<td>NL2Repo</td>
<td align="right">58.0</td>
<td align="right">58.0</td>
<td>Tie</td>
</tr>

<tr>
<td>SWE-Marathon v1.1</td>
<td align="right">42.5</td>
<td align="right">48.1</td>
<td>Kimi K3</td>
</tr>

<tr>
<td>PostTrainBench</td>
<td align="right">39.8</td>
<td align="right">32.0</td>
<td>GLM-5.3</td>
</tr>

<tr>
<td>CyberGym</td>
<td align="right">84.5</td>
<td align="right">80.0</td>
<td>GLM-5.3</td>
</tr>

<tr>
<td>Toolathlon Verified</td>
<td align="right">73.0</td>
<td align="right">76.5</td>
<td>Kimi K3</td>
</tr>

<tr>
<td>AutomationBench v1.0.6</td>
<td align="right">48.2</td>
<td align="right">46.7</td>
<td>GLM-5.3</td>
</tr>

<tr>
<td>Agents&#39; Last Exam CLI</td>
<td align="right">28.5</td>
<td align="right">27.6</td>
<td>GLM-5.3</td>
</tr>
</tbody>
</table>

<p>The security rows deserve caution. Z.ai reports much stronger GLM-5.3 results on ExploitGym and ExploitBench, but those are capability evaluations, not permission to hand an agent production credentials. For operational use, a stronger model should lead to narrower permissions, better logging, and more deliberate approval gates.</p>

<p>There is one useful independent signal: Terminal-Bench 4.0 currently lists GLM-5.3 with Claude Code at 41.8% ± 3.2%. The same public leaderboard did not list Kimi K3 when I checked, so there is no current version-4 head-to-head result to quote.</p>

<h2 id="the-methodology-is-part-of-the-number">The methodology is part of the number</h2>

<p>The table looks cleaner than the evidence really is.</p>

<p>Z.ai says it evaluated Terminal-Bench 3.0 with Claude Code 2.1.207, <code>reasoning_effort=max</code>, 400K context, up to 128K output tokens, three rollouts per task, a 600-turn cap, and a ten-hour timeout. Its DeepSWE setup uses the mini-swe-agent harness with a six-hour timeout and 400K context. Kimi&#39;s model card publishes its own suite and methodology. Those are not automatically the same agent implementation, prompt, sampling policy, timeout, or retry policy.</p>

<p>A model that is cheaper per token may still be more expensive per completed task if it needs longer trajectories or more retries. A model that wins a terminal benchmark may still be a poor fit for a UI workflow if the decisive evidence is an image. Published scores should narrow the shortlist. They should not replace a test against the repositories, logs, screenshots, and review rules that matter to a team.</p>

<h2 id="pricing-changes-how-much-room-an-agent-gets-to-think">Pricing changes how much room an agent gets to think</h2>

<p>At the current list prices, GLM-5.3 is materially cheaper for text generation.</p>

<table>
<thead>
<tr>
<th>Model</th>
<th align="right">Input / 1M tokens</th>
<th align="right">Cached input / 1M</th>
<th align="right">Output / 1M</th>
</tr>
</thead>

<tbody>
<tr>
<td>GLM-5.3</td>
<td align="right">$1.40</td>
<td align="right">$0.26</td>
<td align="right">$4.40</td>
</tr>

<tr>
<td>Kimi K3</td>
<td align="right">$3.00</td>
<td align="right">$0.30</td>
<td align="right">$15.00</td>
</tr>
</tbody>
</table>

<p>For a simple 100K-input, 10K-output request, that works out to about $0.184 with GLM-5.3 and $0.450 with Kimi K3 before provider-specific tool charges or routing markup. If all 100K input tokens are cache hits, the same request is about $0.070 for GLM and $0.180 for Kimi.</p>

<p>That is not a promise of lower task cost. Long-running agents accumulate tool output, hidden reasoning, retries, and context. It does mean GLM leaves more budget for experiments when the job is mostly text: reviewing a large pull request, reading CI logs, comparing rendered GitOps artifacts, or iterating on a shell-based diagnosis.</p>

<p>Kimi&#39;s premium buys a larger model and native visual input, not a universal result. If a workflow otherwise needs a separate OCR or vision model, the price comparison has to include that component and the operational risk of passing incomplete or distorted extracted text downstream.</p>

<h2 id="what-i-would-test-for-a-devops-workflow">What I would test for a DevOps workflow</h2>

<p>I would not grant either model broad infrastructure access because a benchmark looked good. I would start with a closed, read-only evaluation set and measure the whole path.</p>
<ol><li>Use sanitized inputs: a CI failure bundle, rendered Kubernetes manifests, a Terraform plan, a short incident timeline, and a small set of browser or dashboard screenshots.</li>
<li>Give each model the same task contract, tool surface, timeout, and output schema. Keep write actions disabled.</li>
<li>Score claims against the source artifacts. A good answer should cite the specific log line, manifest field, or screenshot region that supports it.</li>
<li>Record completion rate, elapsed time, input and output tokens, retries, malformed tool calls, and total cost per completed task.</li>
<li>Add an approval boundary before anything that could mutate a cloud account, cluster, DNS record, deployment, or customer data.</li></ol>

<p>For text-only work, GLM-5.3 is the model I would evaluate first because the price-performance case is compelling. For a workflow that genuinely begins with screenshots, Kimi K3 avoids an extra model hop and deserves the same evaluation. The better result may be a routing policy rather than a single winner: vision-capable intake for visual evidence, then a cheaper text model for the long investigation and write-up.</p>

<h2 id="the-short-version">The short version</h2>

<p>GLM-5.3 has the more attractive economics for text-heavy agentic engineering, and its published results make it a serious candidate for terminal, repository, and automation work. Kimi K3 is more expensive, but it accepts visual input natively and remains competitive on several software-engineering and tool-use benchmarks.</p>

<p>Neither result answers the operational question by itself. A useful agent needs evidence, bounded tools, and a way for a human to stop or reject the next action. That matters more than a one-point difference on a leaderboard.</p>

<hr>

<p><strong>Test notes:</strong> This was a source-led comparison conducted on 29 August 2026. I checked Z.ai&#39;s GLM-5.3 model card and pricing documentation, Moonshot&#39;s Kimi K3 model card and technical blog, OpenRouter&#39;s current Kimi K3 provider-price display, and the public Terminal-Bench 4.0 leaderboard. I did not run either model, reproduce any benchmark, test vision quality, or validate API behavior. Dollar examples are arithmetic illustrations using published per-million-token rates and exclude provider markup, tool charges, taxes, hidden reasoning-token accounting, and retries.</p>

<p><strong>Sources:</strong> <a href="https://huggingface.co/zai-org/GLM-5.3">Z.ai GLM-5.3 model card</a> · <a href="https://docs.z.ai/guides/llm/glm-5.3">Z.ai GLM-5.3 documentation</a> · <a href="https://docs.z.ai/guides/overview/pricing">Z.ai API pricing</a> · <a href="https://github.com/MoonshotAI/Kimi-K3">Moonshot Kimi K3 model card</a> · <a href="https://www.kimi.ai/blog/kimi-k3">Moonshot Kimi K3 technical blog</a> · <a href="https://openrouter.ai/moonshotai/kimi-k3">Kimi K3 provider pricing</a> · <a href="https://www.tbench.ai/">Terminal-Bench 4.0 leaderboard</a> · <a href="https://www.tbench.ai/news/terminal-bench-4-0">Terminal-Bench 4.0 methodology update</a>.</p>
]]></content:encoded>
      <guid>https://heyimusa.blog/glm-5-3-vs-kimi-k3-for-agentic-engineering</guid>
      <pubDate>Sun, 30 Aug 2026 11:22:40 +0000</pubDate>
    </item>
    <item>
      <title>Operating AI Agents Like Production Systems, Part 3: Monitor Non-Deterministic Agent Work</title>
      <link>https://heyimusa.blog/health-signals-for-non-deterministic-agent-work</link>
      <description>&lt;![CDATA[  Part 3 of Operating AI Agents Like Production Systems. A green HTTP check can tell you that an agent endpoint answered. It cannot tell you whether the agent completed any useful work, whether it is stuck behind approval, or whether the tool runner stopped executing actions.&#xA;&#xA;I built the last fixture in this series around that distinction. It counts completed executions, review-queue items, and total requests. The input is intentionally tiny: synthetic event records, not model traces or production telemetry. The goal was to make one quiet failure visible: every request can receive an answer while the execution rate stays at zero.&#xA;&#xA;That failure is easy to miss if the dashboard only tracks request success and latency. The endpoint is alive. Users may even see polished answers. The business action never happened.&#xA;&#xA;!--more--&#xA;&#xA;A status code is the wrong finish line&#xA;&#xA;The synthetic health function marks a stream as unhealthy when it has events but none completed execution. It marks a stream degraded when work completes but the review queue has outstanding items.&#xA;&#xA;The demo had one completed billing action and one pending-review action:&#xA;&#xA;{&#xA;  &#34;executed&#34;: 1,&#xA;  &#34;executionrate&#34;: 0.5,&#xA;  &#34;reviewqueue&#34;: 1,&#xA;  &#34;status&#34;: &#34;degraded&#34;,&#xA;  &#34;total&#34;: 2&#xA;}&#xA;&#xA;That is not a universal threshold. A support agent may correctly send many requests to review. A deployment agent may execute rarely by design. The useful point is that execution and review backlog need their own signals. Neither can be inferred from HTTP success.&#xA;&#xA;The isolated test&#xA;&#xA;I added two health tests to the same local fixture used in the earlier posts. One feeds two answered-but-not-executed events and expects unhealthy. The other feeds one execution and one review item and expects degraded.&#xA;&#xA;testsilentnonexecutionisunhealthyevenwhenrequestssucceed ... ok&#xA;testcompletedworkwithonereviewqueueitemis_degraded ... ok&#xA;&#xA;Ran 8 tests in 0.001s&#xA;OK&#xA;&#xA;The test ran in python:3.12-alpine with no network, read-only root filesystem, no Linux capabilities, no-new-privileges, a 16 MiB temporary filesystem, 128 MiB memory, one CPU, a 64-PID cap, and UID/GID 65532.&#xA;&#xA;Signals I would add first&#xA;&#xA;Start with a request-to-outcome view rather than a token or latency view:&#xA;&#xA;count proposed actions by operation and policy result;&#xA;count approvals, denials, expiries, and review age;&#xA;count tool executions and tool failures separately from model responses;&#xA;record the ratio of executed actions to eligible actions;&#xA;sample evidence bundles for missing or stale references.&#xA;&#xA;Keep the model&#39;s response, policy decision, approval, and execution as linked events under one request ID. Otherwise operators end up correlating a chat log, approval queue, and tool log by timestamp during an incident.&#xA;&#xA;The boundary of this result&#xA;&#xA;This experiment does not recommend alert thresholds or prove a monitoring stack. It has no distributed traces, no retries, no queue, no real tool integration, and no model. It only demonstrates that an operational health rule can catch a silent nonexecution state that request success misses.&#xA;&#xA;Real systems also need to distinguish intentional waiting from blocked work. A review queue may be healthy if its oldest item is five minutes old and unhealthy if it is five days old. An idempotent retry may be safe for one tool and dangerous for another. Instrument the state transitions before deciding which counter should page someone.&#xA;&#xA;Closing the loop&#xA;&#xA;The series began with evidence because a confident answer is not a reviewable action. It added approval binding because a generic approval is not authorization for an arbitrary payload. Health comes last because both controls can still fail quietly in production.&#xA;&#xA;I would ship the small contracts first, then make their transitions observable. Fancy agent orchestration can wait. It is much harder to retrofit proof of what happened after the system has already learned how to act.&#xA;&#xA;Continue the series&#xA;&#xA;Part 1: Build an evidence gate&#xA;Part 2: Bind approvals to the exact action&#xA;&#xA;---&#xA;&#xA;Test notes: The health rules are local synthetic Python code in experiments/agent-operations/; all eight tests ran in a restricted Docker container. No model, telemetry backend, credentials, or external API was used.&#xA;&#xA;Sources: OpenTelemetry semantic conventions (instrumentation context) · No errors is not a health signal: monitoring the work that stopped · An agent needs an evidence bundle, not a confident answer.&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<blockquote><p><em>Part 3 of Operating AI Agents Like Production Systems.</em> A green HTTP check can tell you that an agent endpoint answered. It cannot tell you whether the agent completed any useful work, whether it is stuck behind approval, or whether the tool runner stopped executing actions.</p></blockquote>

<p>I built the last fixture in this series around that distinction. It counts completed executions, review-queue items, and total requests. The input is intentionally tiny: synthetic event records, not model traces or production telemetry. The goal was to make one quiet failure visible: every request can receive an answer while the execution rate stays at zero.</p>

<p>That failure is easy to miss if the dashboard only tracks request success and latency. The endpoint is alive. Users may even see polished answers. The business action never happened.</p>



<h2 id="a-status-code-is-the-wrong-finish-line">A status code is the wrong finish line</h2>

<p>The synthetic health function marks a stream as <code>unhealthy</code> when it has events but none completed execution. It marks a stream <code>degraded</code> when work completes but the review queue has outstanding items.</p>

<p>The demo had one completed billing action and one pending-review action:</p>

<pre><code class="language-json">{
  &#34;executed&#34;: 1,
  &#34;execution_rate&#34;: 0.5,
  &#34;review_queue&#34;: 1,
  &#34;status&#34;: &#34;degraded&#34;,
  &#34;total&#34;: 2
}
</code></pre>

<p>That is not a universal threshold. A support agent may correctly send many requests to review. A deployment agent may execute rarely by design. The useful point is that execution and review backlog need their own signals. Neither can be inferred from HTTP success.</p>

<h2 id="the-isolated-test">The isolated test</h2>

<p>I added two health tests to the same local fixture used in the earlier posts. One feeds two answered-but-not-executed events and expects <code>unhealthy</code>. The other feeds one execution and one review item and expects <code>degraded</code>.</p>

<pre><code class="language-text">test_silent_nonexecution_is_unhealthy_even_when_requests_succeed ... ok
test_completed_work_with_one_review_queue_item_is_degraded ... ok

Ran 8 tests in 0.001s
OK
</code></pre>

<p>The test ran in <code>python:3.12-alpine</code> with no network, read-only root filesystem, no Linux capabilities, <code>no-new-privileges</code>, a 16 MiB temporary filesystem, 128 MiB memory, one CPU, a 64-PID cap, and UID/GID <code>65532</code>.</p>

<h2 id="signals-i-would-add-first">Signals I would add first</h2>

<p>Start with a request-to-outcome view rather than a token or latency view:</p>
<ul><li>count proposed actions by operation and policy result;</li>
<li>count approvals, denials, expiries, and review age;</li>
<li>count tool executions and tool failures separately from model responses;</li>
<li>record the ratio of executed actions to eligible actions;</li>
<li>sample evidence bundles for missing or stale references.</li></ul>

<p>Keep the model&#39;s response, policy decision, approval, and execution as linked events under one request ID. Otherwise operators end up correlating a chat log, approval queue, and tool log by timestamp during an incident.</p>

<h2 id="the-boundary-of-this-result">The boundary of this result</h2>

<p>This experiment does not recommend alert thresholds or prove a monitoring stack. It has no distributed traces, no retries, no queue, no real tool integration, and no model. It only demonstrates that an operational health rule can catch a silent nonexecution state that request success misses.</p>

<p>Real systems also need to distinguish intentional waiting from blocked work. A review queue may be healthy if its oldest item is five minutes old and unhealthy if it is five days old. An idempotent retry may be safe for one tool and dangerous for another. Instrument the state transitions before deciding which counter should page someone.</p>

<h2 id="closing-the-loop">Closing the loop</h2>

<p>The series began with evidence because a confident answer is not a reviewable action. It added approval binding because a generic approval is not authorization for an arbitrary payload. Health comes last because both controls can still fail quietly in production.</p>

<p>I would ship the small contracts first, then make their transitions observable. Fancy agent orchestration can wait. It is much harder to retrofit proof of what happened after the system has already learned how to act.</p>

<h2 id="continue-the-series">Continue the series</h2>
<ul><li><strong>Part 1:</strong> <a href="/build-an-evidence-gate-for-an-ai-agent">Build an evidence gate</a></li>
<li><strong>Part 2:</strong> <a href="/bind-agent-approvals-to-the-action">Bind approvals to the exact action</a></li></ul>

<hr>

<p><strong>Test notes:</strong> The health rules are local synthetic Python code in <code>experiments/agent-operations/</code>; all eight tests ran in a restricted Docker container. No model, telemetry backend, credentials, or external API was used.</p>

<p><strong>Sources:</strong> <a href="https://opentelemetry.io/docs/specs/semconv/">OpenTelemetry semantic conventions</a> (instrumentation context) · <a href="https://heyimusa.blog/no-errors-is-not-a-health-signal">No errors is not a health signal: monitoring the work that stopped</a> · <a href="https://heyimusa.blog/an-agent-needs-an-evidence-bundle">An agent needs an evidence bundle, not a confident answer</a>.</p>
]]></content:encoded>
      <guid>https://heyimusa.blog/health-signals-for-non-deterministic-agent-work</guid>
      <pubDate>Fri, 21 Aug 2026 18:39:03 +0000</pubDate>
    </item>
    <item>
      <title>Operating AI Agents Like Production Systems, Part 2: Bind Approvals to the Exact Action</title>
      <link>https://heyimusa.blog/bind-agent-approvals-to-the-action</link>
      <description>&lt;![CDATA[  Part 2 of Operating AI Agents Like Production Systems. An approval is only useful when it names the action it approves. A generic approval can be replayed against a different refund, a different customer, or a different operation.&#xA;&#xA;I used the same synthetic refund from the first tutorial: $240 on invoice-42, with a policy that requires human approval above $100. The first version of the workflow only asked whether an approval existed. That is too loose. A reviewer could approve one request while the agent sends another payload to the billing tool.&#xA;&#xA;This tutorial adds a small action fingerprint: operation, target, and amount. It is not a cryptographic authorization protocol. It is the smallest testable rule that makes a mismatch visible before an external action is allowed.&#xA;&#xA;!--more--&#xA;&#xA;A presence check is not enough&#xA;&#xA;The unsafe question is: &#34;Do we have an approval?&#34; The safer question is: &#34;Does this approval bind to this exact action?&#34;&#xA;&#xA;The synthetic approval for the refund contains this fingerprint:&#xA;&#xA;createrefund:invoice-42:240&#xA;&#xA;The test rejects an approval with a different fingerprint even when its status says approved. It also denies an operation that the toy policy does not allow at all: deletecustomer.&#xA;&#xA;{&#xA;  &#34;unbound&#34;: {&#xA;    &#34;decision&#34;: &#34;needs-review&#34;,&#xA;    &#34;reason&#34;: &#34;approval is required&#34;&#xA;  },&#xA;  &#34;bound&#34;: {&#xA;    &#34;decision&#34;: &#34;allow&#34;,&#xA;    &#34;reason&#34;: &#34;matching synthetic approval is present&#34;&#xA;  }&#xA;}&#xA;&#xA;The test is small on purpose&#xA;&#xA;The action function has only three paths: deny an allowlist violation, request review when approval is missing or mismatched, and allow a matching approval. The eight-test Docker run covers this behavior alongside the evidence-gate and health examples.&#xA;&#xA;testdestructiveactionwithoutmatchingapprovalisdenied ... ok&#xA;testrefundoverpolicythresholdrequiresboundapproval ... ok&#xA;testmatchingapprovalallowsrefundoverpolicythreshold ... ok&#xA;&#xA;I ran those tests in the same isolated python:3.12-alpine container: no network, read-only root filesystem, dropped capabilities, a 16 MiB temporary filesystem, 128 MiB memory, one CPU, 64 PIDs, and an unprivileged user.&#xA;&#xA;What should be bound in a real system&#xA;&#xA;Do not copy the colon-separated string into production and call the job finished. A real approval record should bind at least the request identity, policy version, operation name, canonical action payload, requested scope, expiry, reviewer identity, and a nonce or idempotency key. The execution service should verify that binding itself instead of trusting an agent&#39;s summary.&#xA;&#xA;The important design choice is ownership. The model may propose the action and explain why. It should not manufacture approval. The approval system should create the authorization artifact, and the tool runner should verify it immediately before performing the side effect.&#xA;&#xA;Limits and failure modes&#xA;&#xA;This fixture does not sign data, authenticate a reviewer, prevent a privileged operator from changing policy, or call an external tool. It also does not solve race conditions between approval and execution. Those are the reasons this belongs in a separate policy and tool boundary, not inside a prompt.&#xA;&#xA;The point is more modest: make action drift testable. If a request changes from $240 to $2,400, the old approval must stop matching. If an agent changes createrefund to delete_customer, the allowlist must deny it.&#xA;&#xA;Next: health for work that looks successful&#xA;&#xA;A system can pass request-level checks and still fail quietly. An agent may answer every request while a tool integration never executes, or it may send every risky request to a review queue that nobody watches. The final post measures those gaps.&#xA;&#xA;Continue the series&#xA;&#xA;Part 1: Build an evidence gate&#xA;Part 3: Monitor non-deterministic agent work&#xA;&#xA;---&#xA;&#xA;Test notes: The behavior comes from the local synthetic fixture at experiments/agent-operations/, run in a restricted Docker container with no network and no external systems.&#xA;&#xA;Sources: OWASP Top 10 for LLM Applications (general context on excessive agency and authorization risk) · An agent needs an evidence bundle, not a confident answer.&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<blockquote><p><em>Part 2 of Operating AI Agents Like Production Systems.</em> An approval is only useful when it names the action it approves. A generic approval can be replayed against a different refund, a different customer, or a different operation.</p></blockquote>

<p>I used the same synthetic refund from the first tutorial: <code>$240</code> on <code>invoice-42</code>, with a policy that requires human approval above <code>$100</code>. The first version of the workflow only asked whether an approval existed. That is too loose. A reviewer could approve one request while the agent sends another payload to the billing tool.</p>

<p>This tutorial adds a small action fingerprint: operation, target, and amount. It is not a cryptographic authorization protocol. It is the smallest testable rule that makes a mismatch visible before an external action is allowed.</p>



<h2 id="a-presence-check-is-not-enough">A presence check is not enough</h2>

<p>The unsafe question is: “Do we have an approval?” The safer question is: “Does this approval bind to this exact action?”</p>

<p>The synthetic approval for the refund contains this fingerprint:</p>

<pre><code class="language-text">create_refund:invoice-42:240
</code></pre>

<p>The test rejects an approval with a different fingerprint even when its status says <code>approved</code>. It also denies an operation that the toy policy does not allow at all: <code>delete_customer</code>.</p>

<pre><code class="language-json">{
  &#34;unbound&#34;: {
    &#34;decision&#34;: &#34;needs-review&#34;,
    &#34;reason&#34;: &#34;approval is required&#34;
  },
  &#34;bound&#34;: {
    &#34;decision&#34;: &#34;allow&#34;,
    &#34;reason&#34;: &#34;matching synthetic approval is present&#34;
  }
}
</code></pre>

<h2 id="the-test-is-small-on-purpose">The test is small on purpose</h2>

<p>The action function has only three paths: deny an allowlist violation, request review when approval is missing or mismatched, and allow a matching approval. The eight-test Docker run covers this behavior alongside the evidence-gate and health examples.</p>

<pre><code class="language-text">test_destructive_action_without_matching_approval_is_denied ... ok
test_refund_over_policy_threshold_requires_bound_approval ... ok
test_matching_approval_allows_refund_over_policy_threshold ... ok
</code></pre>

<p>I ran those tests in the same isolated <code>python:3.12-alpine</code> container: no network, read-only root filesystem, dropped capabilities, a 16 MiB temporary filesystem, 128 MiB memory, one CPU, 64 PIDs, and an unprivileged user.</p>

<h2 id="what-should-be-bound-in-a-real-system">What should be bound in a real system</h2>

<p>Do not copy the colon-separated string into production and call the job finished. A real approval record should bind at least the request identity, policy version, operation name, canonical action payload, requested scope, expiry, reviewer identity, and a nonce or idempotency key. The execution service should verify that binding itself instead of trusting an agent&#39;s summary.</p>

<p>The important design choice is ownership. The model may propose the action and explain why. It should not manufacture approval. The approval system should create the authorization artifact, and the tool runner should verify it immediately before performing the side effect.</p>

<h2 id="limits-and-failure-modes">Limits and failure modes</h2>

<p>This fixture does not sign data, authenticate a reviewer, prevent a privileged operator from changing policy, or call an external tool. It also does not solve race conditions between approval and execution. Those are the reasons this belongs in a separate policy and tool boundary, not inside a prompt.</p>

<p>The point is more modest: make action drift testable. If a request changes from <code>$240</code> to <code>$2,400</code>, the old approval must stop matching. If an agent changes <code>create_refund</code> to <code>delete_customer</code>, the allowlist must deny it.</p>

<h2 id="next-health-for-work-that-looks-successful">Next: health for work that looks successful</h2>

<p>A system can pass request-level checks and still fail quietly. An agent may answer every request while a tool integration never executes, or it may send every risky request to a review queue that nobody watches. The final post measures those gaps.</p>

<h2 id="continue-the-series">Continue the series</h2>
<ul><li><strong>Part 1:</strong> <a href="/build-an-evidence-gate-for-an-ai-agent">Build an evidence gate</a></li>
<li><strong>Part 3:</strong> <a href="/health-signals-for-non-deterministic-agent-work">Monitor non-deterministic agent work</a></li></ul>

<hr>

<p><strong>Test notes:</strong> The behavior comes from the local synthetic fixture at <code>experiments/agent-operations/</code>, run in a restricted Docker container with no network and no external systems.</p>

<p><strong>Sources:</strong> <a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/">OWASP Top 10 for LLM Applications</a> (general context on excessive agency and authorization risk) · <a href="https://heyimusa.blog/an-agent-needs-an-evidence-bundle">An agent needs an evidence bundle, not a confident answer</a>.</p>
]]></content:encoded>
      <guid>https://heyimusa.blog/bind-agent-approvals-to-the-action</guid>
      <pubDate>Fri, 21 Aug 2026 18:39:03 +0000</pubDate>
    </item>
    <item>
      <title>Operating AI Agents Like Production Systems, Part 1: Build an Evidence Gate</title>
      <link>https://heyimusa.blog/build-an-evidence-gate-for-an-ai-agent</link>
      <description>&lt;![CDATA[  Part 1 of Operating AI Agents Like Production Systems. A helpful agent answer and an operable agent action are different artifacts. Before an agent can change an external system, a reviewer needs the input references, policy result, exact proposed action, approval state, and execution state.&#xA;&#xA;I built a tiny synthetic refund workflow because it makes the gap obvious. A model can say, &#34;The $240 refund was submitted,&#34; yet leave no way to tell which rule it applied, whether a person approved the charge, or whether the billing tool ran. That sentence may be useful to a customer. It is useless in a review.&#xA;&#xA;The post that prompted this series argued for evidence bundles. This one turns the idea into a small contract and a test. I ran it in a locked-down Docker container against synthetic JSON only. It did not call a model, payment provider, approval service, or production API.&#xA;&#xA;!--more--&#xA;&#xA;The contract is deliberately boring&#xA;&#xA;The synthetic record needs five fields besides its request ID and final answer: inputrefs, policydecision, proposedaction, approval, and execution. The validator does not decide whether a refund is morally or commercially correct. It answers a narrower question: can someone inspect the state needed to challenge the claim?&#xA;&#xA;A response-only record fails the check because it cannot answer that question:&#xA;&#xA;{&#xA;  &#34;decision&#34;: &#34;needs-review&#34;,&#xA;  &#34;missing&#34;: [&#xA;    &#34;inputrefs&#34;,&#xA;    &#34;policydecision&#34;,&#xA;    &#34;proposedaction&#34;,&#xA;    &#34;approval&#34;,&#xA;    &#34;execution&#34;&#xA;  ]&#xA;}&#xA;&#xA;A complete bundle with an approval still pending also returns needs-review. Structure is necessary, but it is not permission to execute.&#xA;&#xA;The Docker experiment&#xA;&#xA;I wrote eight unit tests and ran them inside python:3.12-alpine. The container had no network, a read-only root filesystem, a 16 MiB temporary filesystem, no Linux capabilities, no-new-privileges, a 64-PID cap, 128 MiB of memory, one CPU, and UID/GID 65532.&#xA;&#xA;Ran 8 tests in 0.001s&#xA;&#xA;OK&#xA;&#xA;The demonstration used a synthetic $240 refund. Its policy requires human approval above $100. The same fixture is used for the rest of this series, so the boundary stays clear: these are record and policy tests, not payment-system tests.&#xA;&#xA;What this proves&#xA;&#xA;The validator caught the missing review fields and refused to treat a pending approval as executable. That is the useful part. A chat response can remain short while the system keeps a separate record for an operator.&#xA;&#xA;It does not prove that the policy is correct, that references are fresh, that approval identity is trustworthy, or that an execution record came from a real provider. Those are separate controls. A JSON schema is not an audit system just because it has an execution key.&#xA;&#xA;Use the gate before the tool call&#xA;&#xA;Put this check between agent reasoning and the side-effecting tool. If the bundle is incomplete, return a review state rather than making the tool call. Store immutable references where possible, including the policy version and the exact action payload.&#xA;&#xA;The next post tightens the other half of the boundary: an approval must bind to the action that will run. A general &#34;looks good&#34; comment should not authorize a different refund or a destructive operation.&#xA;&#xA;Continue the series&#xA;&#xA;Part 2: Bind approvals to the exact action&#xA;Part 3: Monitor non-deterministic agent work&#xA;&#xA;---&#xA;&#xA;Test notes: The code is a local synthetic Python fixture in experiments/agent-operations/, tested in Docker with no network or external credentials. It validates small in-memory records only.&#xA;&#xA;Sources: An agent needs an evidence bundle, not a confident answer · OpenAI Presence announcement (context for approved actions; not exercised by this experiment).&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<blockquote><p><em>Part 1 of Operating AI Agents Like Production Systems.</em> A helpful agent answer and an operable agent action are different artifacts. Before an agent can change an external system, a reviewer needs the input references, policy result, exact proposed action, approval state, and execution state.</p></blockquote>

<p>I built a tiny synthetic refund workflow because it makes the gap obvious. A model can say, “The $240 refund was submitted,” yet leave no way to tell which rule it applied, whether a person approved the charge, or whether the billing tool ran. That sentence may be useful to a customer. It is useless in a review.</p>

<p>The post that prompted this series argued for evidence bundles. This one turns the idea into a small contract and a test. I ran it in a locked-down Docker container against synthetic JSON only. It did not call a model, payment provider, approval service, or production API.</p>



<h2 id="the-contract-is-deliberately-boring">The contract is deliberately boring</h2>

<p>The synthetic record needs five fields besides its request ID and final answer: <code>input_refs</code>, <code>policy_decision</code>, <code>proposed_action</code>, <code>approval</code>, and <code>execution</code>. The validator does not decide whether a refund is morally or commercially correct. It answers a narrower question: can someone inspect the state needed to challenge the claim?</p>

<p>A response-only record fails the check because it cannot answer that question:</p>

<pre><code class="language-json">{
  &#34;decision&#34;: &#34;needs-review&#34;,
  &#34;missing&#34;: [
    &#34;input_refs&#34;,
    &#34;policy_decision&#34;,
    &#34;proposed_action&#34;,
    &#34;approval&#34;,
    &#34;execution&#34;
  ]
}
</code></pre>

<p>A complete bundle with an approval still pending also returns <code>needs-review</code>. Structure is necessary, but it is not permission to execute.</p>

<h2 id="the-docker-experiment">The Docker experiment</h2>

<p>I wrote eight unit tests and ran them inside <code>python:3.12-alpine</code>. The container had no network, a read-only root filesystem, a 16 MiB temporary filesystem, no Linux capabilities, <code>no-new-privileges</code>, a 64-PID cap, 128 MiB of memory, one CPU, and UID/GID <code>65532</code>.</p>

<pre><code class="language-text">Ran 8 tests in 0.001s

OK
</code></pre>

<p>The demonstration used a synthetic <code>$240</code> refund. Its policy requires human approval above <code>$100</code>. The same fixture is used for the rest of this series, so the boundary stays clear: these are record and policy tests, not payment-system tests.</p>

<h2 id="what-this-proves">What this proves</h2>

<p>The validator caught the missing review fields and refused to treat a pending approval as executable. That is the useful part. A chat response can remain short while the system keeps a separate record for an operator.</p>

<p>It does not prove that the policy is correct, that references are fresh, that approval identity is trustworthy, or that an execution record came from a real provider. Those are separate controls. A JSON schema is not an audit system just because it has an <code>execution</code> key.</p>

<h2 id="use-the-gate-before-the-tool-call">Use the gate before the tool call</h2>

<p>Put this check between agent reasoning and the side-effecting tool. If the bundle is incomplete, return a review state rather than making the tool call. Store immutable references where possible, including the policy version and the exact action payload.</p>

<p>The next post tightens the other half of the boundary: an approval must bind to the action that will run. A general “looks good” comment should not authorize a different refund or a destructive operation.</p>

<h2 id="continue-the-series">Continue the series</h2>
<ul><li><strong>Part 2:</strong> <a href="/bind-agent-approvals-to-the-action">Bind approvals to the exact action</a></li>
<li><strong>Part 3:</strong> <a href="/health-signals-for-non-deterministic-agent-work">Monitor non-deterministic agent work</a></li></ul>

<hr>

<p><strong>Test notes:</strong> The code is a local synthetic Python fixture in <code>experiments/agent-operations/</code>, tested in Docker with no network or external credentials. It validates small in-memory records only.</p>

<p><strong>Sources:</strong> <a href="https://heyimusa.blog/an-agent-needs-an-evidence-bundle">An agent needs an evidence bundle, not a confident answer</a> · <a href="https://openai.com/index/introducing-openai-presence/">OpenAI Presence announcement</a> (context for approved actions; not exercised by this experiment).</p>
]]></content:encoded>
      <guid>https://heyimusa.blog/build-an-evidence-gate-for-an-ai-agent</guid>
      <pubDate>Fri, 21 Aug 2026 18:39:03 +0000</pubDate>
    </item>
    <item>
      <title>Faster distillation is not a release gate</title>
      <link>https://heyimusa.blog/faster-distillation-is-not-a-release-gate</link>
      <description>&lt;![CDATA[  A faster training loop can shorten the time between an idea and an artifact. It does not tell us whether that artifact should be released.&#xA;&#xA;I started with a smaller question than &#34;does distillation work?&#34; The environment available to me has Docker but no GPU, so it cannot establish a throughput result for a large teacher model. It can still answer a useful delivery question: what exactly is available to run, and how much confidence does that give us?&#xA;&#xA;Hugging Face&#39;s TRL documentation describes DistillationTrainer as on-policy knowledge distillation. The student generates its own completions; the trainer then compares the teacher&#39;s next-token distribution on those completions. That targets the gap between fixed training outputs and the outputs a student produces at inference time.&#xA;&#xA;The upstream project has also published a &#34;40x faster&#34; claim for a particular distillation setup. That is an upstream benchmark claim, not a release decision for another model, dataset, hardware profile, or product.&#xA;&#xA;!--more--&#xA;&#xA;A small compatibility probe&#xA;&#xA;I built a disposable Docker image from the current TRL main branch, commit f72460224c5477e65031b2363f54368699105182. The runtime had a read-only filesystem, no network, all Linux capabilities dropped, no-new-privileges, a two-CPU limit, a 3 GiB memory limit, and a 128-process limit.&#xA;&#xA;The probe did not train a model. It checked whether the feature&#39;s public API was present and whether the host could run the GPU work needed for a credible performance experiment.&#xA;&#xA;trldistillationprobe&#xA;DistillationConfigavailable= DistillationConfig&#xA;DistillationTraineravailable= DistillationTrainer&#xA;trainerinithasteachermodel= True&#xA;cudaavailable= False&#xA;verdict=APIimportandconstructorsurfaceavailable; notrainingrunwithoutGPU&#xA;&#xA;That is a deliberately narrow result. The current source exposes the expected API. This machine cannot turn that into a claim about training speed, memory use, model quality, or deployment safety.&#xA;&#xA;There was another useful result. A probe built from the released trl==0.29.0 package failed because it did not expose DistillationConfig. The documentation page I read is for a newer release line, while the implementation being discussed is available on current source. A team that copies a documentation example into a pinned environment may discover that distinction after the release pipeline has already been designed around it.&#xA;&#xA;Training speed measures one part of the system&#xA;&#xA;A distillation benchmark can be entirely legitimate and still leave the release question open. Faster teacher requests, batched work, better buffering, and smaller log-probability payloads change the economics of producing candidate models. They do not establish that a candidate preserved the behaviours that matter.&#xA;&#xA;The release decision needs evidence that belongs to the target use case:&#xA;&#xA;a fixed evaluation suite, including failures that previously mattered;&#xA;quality checks for the deployed inference path, not only the training job;&#xA;safety and policy evaluations appropriate to the model&#39;s capabilities;&#xA;latency, cost, and capacity measurements under the intended serving configuration;&#xA;a versioned model artifact, dataset and evaluation record; and&#xA;a rollback path that is tested before traffic is shifted.&#xA;&#xA;This is not a demand for one huge benchmark. It is an argument against substituting a single throughput number for all the other decisions. A model can be cheaper to train and more expensive to operate. It can score well on a general benchmark and fail the narrow task that prompted the project. It can pass an offline evaluation while a tokenizer, prompt template, quantization setting, or serving runtime changes the behaviour users see.&#xA;&#xA;Treat the model artifact like a deployable release&#xA;&#xA;The practical shape is familiar to anyone who has operated software delivery. Keep a release record that connects the candidate artifact to its inputs: base model, teacher model, training code revision, dataset revisions, hyperparameters, evaluation suite revision, results, and approval decision. Promote the same immutable artifact through environments. Evaluate again through the serving path. Start with bounded traffic if the product allows it. Keep the previous version available until the new one has earned confidence.&#xA;&#xA;Distillation can make iteration cheaper. That is valuable. It should make teams more disciplined about release evidence, not less. When a training loop becomes fast enough to create many candidates, the scarce resource becomes review: deciding which candidate deserves to reach users.&#xA;&#xA;---&#xA;&#xA;Test notes: Docker Engine 29.1.3; image built from python:3.11-slim with TRL source at f72460224c5477e65031b2363f54368699105182; two CPUs, 3 GiB memory, 128 PIDs, read-only root filesystem, isolated tmpfs, no network, dropped capabilities and no-new-privileges. The host reported no NVIDIA GPU. This was an API-availability probe, not a distillation benchmark, training run, evaluation, or production-serving test.&#xA;&#xA;Sources: Hugging Face TRL Distillation Trainer documentation; Hugging Face: Distilling 100B+ Models 40x Faster with TRL; TRL source repository.]]&gt;</description>
      <content:encoded><![CDATA[<blockquote><p>A faster training loop can shorten the time between an idea and an artifact. It does not tell us whether that artifact should be released.</p></blockquote>

<p>I started with a smaller question than “does distillation work?” The environment available to me has Docker but no GPU, so it cannot establish a throughput result for a large teacher model. It can still answer a useful delivery question: what exactly is available to run, and how much confidence does that give us?</p>

<p>Hugging Face&#39;s TRL documentation describes <code>DistillationTrainer</code> as on-policy knowledge distillation. The student generates its own completions; the trainer then compares the teacher&#39;s next-token distribution on those completions. That targets the gap between fixed training outputs and the outputs a student produces at inference time.</p>

<p>The upstream project has also published a “40x faster” claim for a particular distillation setup. That is an upstream benchmark claim, not a release decision for another model, dataset, hardware profile, or product.</p>



<h2 id="a-small-compatibility-probe">A small compatibility probe</h2>

<p>I built a disposable Docker image from the current TRL <code>main</code> branch, commit <code>f72460224c5477e65031b2363f54368699105182</code>. The runtime had a read-only filesystem, no network, all Linux capabilities dropped, <code>no-new-privileges</code>, a two-CPU limit, a 3 GiB memory limit, and a 128-process limit.</p>

<p>The probe did not train a model. It checked whether the feature&#39;s public API was present and whether the host could run the GPU work needed for a credible performance experiment.</p>

<pre><code class="language-text">trl_distillation_probe
DistillationConfig_available= DistillationConfig
DistillationTrainer_available= DistillationTrainer
trainer_init_has_teacher_model= True
cuda_available= False
verdict=API_import_and_constructor_surface_available; no_training_run_without_GPU
</code></pre>

<p>That is a deliberately narrow result. The current source exposes the expected API. This machine cannot turn that into a claim about training speed, memory use, model quality, or deployment safety.</p>

<p>There was another useful result. A probe built from the released <code>trl==0.29.0</code> package failed because it did not expose <code>DistillationConfig</code>. The documentation page I read is for a newer release line, while the implementation being discussed is available on current source. A team that copies a documentation example into a pinned environment may discover that distinction after the release pipeline has already been designed around it.</p>

<h2 id="training-speed-measures-one-part-of-the-system">Training speed measures one part of the system</h2>

<p>A distillation benchmark can be entirely legitimate and still leave the release question open. Faster teacher requests, batched work, better buffering, and smaller log-probability payloads change the economics of producing candidate models. They do not establish that a candidate preserved the behaviours that matter.</p>

<p>The release decision needs evidence that belongs to the target use case:</p>
<ul><li>a fixed evaluation suite, including failures that previously mattered;</li>
<li>quality checks for the deployed inference path, not only the training job;</li>
<li>safety and policy evaluations appropriate to the model&#39;s capabilities;</li>
<li>latency, cost, and capacity measurements under the intended serving configuration;</li>
<li>a versioned model artifact, dataset and evaluation record; and</li>
<li>a rollback path that is tested before traffic is shifted.</li></ul>

<p>This is not a demand for one huge benchmark. It is an argument against substituting a single throughput number for all the other decisions. A model can be cheaper to train and more expensive to operate. It can score well on a general benchmark and fail the narrow task that prompted the project. It can pass an offline evaluation while a tokenizer, prompt template, quantization setting, or serving runtime changes the behaviour users see.</p>

<h2 id="treat-the-model-artifact-like-a-deployable-release">Treat the model artifact like a deployable release</h2>

<p>The practical shape is familiar to anyone who has operated software delivery. Keep a release record that connects the candidate artifact to its inputs: base model, teacher model, training code revision, dataset revisions, hyperparameters, evaluation suite revision, results, and approval decision. Promote the same immutable artifact through environments. Evaluate again through the serving path. Start with bounded traffic if the product allows it. Keep the previous version available until the new one has earned confidence.</p>

<p>Distillation can make iteration cheaper. That is valuable. It should make teams more disciplined about release evidence, not less. When a training loop becomes fast enough to create many candidates, the scarce resource becomes review: deciding which candidate deserves to reach users.</p>

<hr>

<p><strong>Test notes:</strong> Docker Engine 29.1.3; image built from <code>python:3.11-slim</code> with TRL source at <code>f72460224c5477e65031b2363f54368699105182</code>; two CPUs, 3 GiB memory, 128 PIDs, read-only root filesystem, isolated tmpfs, no network, dropped capabilities and <code>no-new-privileges</code>. The host reported no NVIDIA GPU. This was an API-availability probe, not a distillation benchmark, training run, evaluation, or production-serving test.</p>

<p><strong>Sources:</strong> <a href="https://huggingface.co/docs/trl/distillation_trainer">Hugging Face TRL Distillation Trainer documentation</a>; <a href="https://huggingface.co/spaces/HuggingFaceTB/trl-distillation-trainer">Hugging Face: Distilling 100B+ Models 40x Faster with TRL</a>; <a href="https://github.com/huggingface/trl/tree/f72460224c5477e65031b2363f54368699105182">TRL source repository</a>.</p>
]]></content:encoded>
      <guid>https://heyimusa.blog/faster-distillation-is-not-a-release-gate</guid>
      <pubDate>Wed, 19 Aug 2026 16:16:47 +0000</pubDate>
    </item>
    <item>
      <title>The prompt is not the control plane</title>
      <link>https://heyimusa.blog/the-prompt-is-not-the-control-plane</link>
      <description>&lt;![CDATA[An agent becomes a different kind of system the moment it can do more than answer. Give it a terminal, a cloud API, a ticketing system, or a deployment tool and its output can change a real environment.&#xA;&#xA;That is where I stop treating the prompt as the main safety mechanism.&#xA;&#xA;A prompt can explain intent. It can tell an agent to be careful. It can ask for confirmation before a destructive operation. But it cannot enforce an allowlist, prove that an approval happened, prevent an unrecognised command from reaching an executor, or tell me whether the claimed outcome exists outside the chat transcript.&#xA;&#xA;Those jobs belong to a control plane.&#xA;&#xA;!--more--&#xA;&#xA;An agent is a loop, not a clever completion&#xA;&#xA;The useful mental model is not &#34;a chatbot with tools.&#34; It is a loop:&#xA;&#xA;a model receives a task, context, and the tools it may request;&#xA;it returns a response or a structured request for a tool;&#xA;the runtime decides whether and how to execute that request;&#xA;the result becomes new evidence for the next turn.&#xA;&#xA;Anthropic makes the same distinction in its tool-use documentation: the model requests a client-side tool call, while the application is responsible for executing it. Its engineering guidance describes agents as LLMs using tools from environmental feedback in a loop. It recommends pausing for human feedback at checkpoints or blockers, and using stopping conditions such as a maximum iteration count.&#xA;&#xA;That separation matters. A model can suggest restartservice. It does not get to make a process restart merely by emitting those words. An executor with the right identity and policy has to accept the request.&#xA;&#xA;This is not an argument against prompts. Good instructions make tool selection, planning, and explanations better. They are just the wrong place to put a non-negotiable security or change-management rule.&#xA;&#xA;What a production policy boundary must check&#xA;&#xA;Suppose the prompt says: &#34;Never restart production without approval.&#34; That is a useful instruction, but it remains advice delivered to a probabilistic system. Context can be incomplete. A retrieved document can be misleading. A tool description can be ambiguous. A model can simply make the wrong call.&#xA;&#xA;A production boundary should check rules after the model has made its request:&#xA;&#xA;Which tools exist for this agent and this task?&#xA;Is the requested argument shape valid for that tool?&#xA;Is the target inside the permitted scope?&#xA;Is the operation read-only or mutating?&#xA;If it mutates state, is there a recorded approval for this exact action?&#xA;Has the run exceeded its time, call, cost, or retry budget?&#xA;&#xA;The prototype below does not implement all of those checks. It only allowlists three tool names, requires that arguments is a JSON object, and checks a demonstration-only boolean before returning a decision. The broader list is the production design target, not a capability claim about the prototype.&#xA;&#xA;Microsoft&#39;s guidance on agent identity makes the point directly: do not treat the model as the security boundary. It recommends narrow permissions, separation of read and write actions, and policy checks, approvals, telemetry, and audit logs around the agent.&#xA;&#xA;The practical rule is simple: the model decides what to ask for; the control plane decides what may happen.&#xA;&#xA;A small policy experiment&#xA;&#xA;I built a deliberately small executor to test this boundary. It is not an AI model and it is not a production controller. It is a deterministic policy layer that accepts JSON tool requests on standard input.&#xA;&#xA;The experiment ran with this Docker command:&#xA;&#xA;docker run --rm --read-only --cap-drop ALL \&#xA;  --security-opt no-new-privileges:true \&#xA;  --pids-limit 32 --memory 64m --cpus 0.25 --network none \&#xA;  -i agent-control-plane-rnd:local&#xA;&#xA;Its policy contained three tools:&#xA;&#xA;| Tool | Class | Rule |&#xA;|---|---|---|&#xA;| readstatus | read | allow |&#xA;| restartservice | mutation | require explicit approval |&#xA;| deletedatabase | mutation | require explicit approval |&#xA;&#xA;I sent five requests through it. The policy did not try to infer intent from prose. It used a small allowlist and a boolean approval signal.&#xA;&#xA;readstatus without approval            → allow&#xA;restartservice without approval        → needsapproval&#xA;restartservice with approval=true      → allow&#xA;deletedatabase with approval=false     → needsapproval&#xA;shell, which was not allowlisted        → deny&#xA;&#xA;The assertion over the full sequence passed:&#xA;&#xA;verification=PASS&#xA;&#xA;The result is intentionally narrower than a real action pipeline. This program never invokes a downstream executor and never changes an environment. It demonstrates only that its own deterministic decision function returns needs_approval or deny for those inputs. In a real system, the executor must accept only an allow decision produced by a separate policy service, and the integration needs its own test proving that denied requests cannot reach the executor.&#xA;&#xA;The boolean approval signal is deliberately unsafe outside this demonstration. It is not bound to an actor, target, action, expiry, or one-time request identifier. A real approval design needs all of those properties, plus an audit record and an executor identity that cannot bypass the gate.&#xA;&#xA;A success message is not an outcome&#xA;&#xA;Agent transcripts are persuasive. &#34;Deployment completed&#34; sounds final. It is not evidence that the intended deployment became healthy, that traffic reaches the new version, or that a rollback did not leave an older route serving users.&#xA;&#xA;This is where the feedback loop needs ground truth. Anthropic recommends environmental feedback from tool calls or code execution at each step. Its 2026 guidance on agent evaluations distinguishes the transcript from the final state of the environment: an agent may claim a flight was booked while the database shows no reservation.&#xA;&#xA;For infrastructure work, outcome checks depend on the action:&#xA;&#xA;a GitOps change needs the desired revision plus reconciliation status and workload readiness;&#xA;a restart needs process health plus a user-path or synthetic check where the risk warrants it;&#xA;an access-policy update needs the intended authorization result from both sides of the boundary;&#xA;a database operation needs a bounded query or domain-level invariant, not a cheerful CLI exit message.&#xA;&#xA;The agent should receive that observed result as new context. The operator should receive a trace that makes it possible to reconstruct what happened without reading private payloads or secrets.&#xA;&#xA;Approval is a state transition&#xA;&#xA;A human approval request in the chat is useful only if the executor can verify it. Otherwise it is still a sentence in the model&#39;s context.&#xA;&#xA;Treat approval as a state transition with a narrow scope:&#xA;&#xA;planned action&#xA;  → policy creates approval request&#xA;  → named approver accepts or rejects the exact request&#xA;  → policy verifies a valid, unexpired approval&#xA;  → executor receives a bounded action&#xA;  → outcome is recorded&#xA;&#xA;That design prevents a common failure mode: an agent receives approval for one low-risk restart, then silently applies the approval to a different host or a deletion request later in the conversation.&#xA;&#xA;The record does not need to contain a full prompt or secret-bearing argument. It needs enough identity to answer: who approved which action against which target, under what policy, and what happened after execution.&#xA;&#xA;Evals are part of the control plane too&#xA;&#xA;A policy gate controls individual actions. It does not tell you whether a new prompt, model, tool schema, or routing change made the whole agent worse.&#xA;&#xA;That needs evaluations. Anthropic describes an agent harness as the system that processes inputs, orchestrates tool calls, and returns results; evaluating an agent therefore means evaluating the harness and model together. Its examples include code-based checks for tool calls, parameters, outcomes, transcript properties, latency, and token use.&#xA;&#xA;For an operations agent, I would start with a compact regression suite:&#xA;&#xA;read-only investigation requests must never produce a mutating tool request;&#xA;an unapproved mutation must stop at the policy gate;&#xA;an approved mutation must target only the approved resource;&#xA;unknown or malformed tool requests must fail closed;&#xA;the run must report an independently observed outcome, not only the model&#39;s final prose.&#xA;&#xA;Run those tests when changing the model, prompt, tool definitions, agent harness, or routing layer. They are not a guarantee of safe production behaviour. They are a way to notice that a change broke a contract you had already decided matters.&#xA;&#xA;Make the safe path the easy path&#xA;&#xA;The architecture does not have to be grand. Start with a small tool surface and a clear split between reading and changing state.&#xA;&#xA;A useful minimum looks like this:&#xA;&#xA;Give the agent a separate identity with least privilege.&#xA;Expose explicit, narrow tools instead of a general shell where possible.&#xA;Validate tool arguments and scopes outside the model.&#xA;Require approval at the policy boundary for mutations.&#xA;Set limits for turns, retries, wall time, and spend.&#xA;Record tool request, policy decision, execution identifier, and observed outcome without retaining secrets by default.&#xA;Build a few regression cases before changing the harness or model route.&#xA;&#xA;Google&#39;s agent architecture guidance lists tools, memory, runtimes, and design patterns as separate components of an agentic system. That separation is useful operationally. It lets us ask which component made a decision, which component enforced a boundary, and which component observed the result.&#xA;&#xA;The prompt belongs in that system. It is where intent, context, and judgement begin. It is not where production authority should end.&#xA;&#xA;Sources&#xA;&#xA;Anthropic, Tool use with Claude.&#xA;Anthropic, Building effective agents, 19 December 2024.&#xA;Anthropic, Demystifying evals for AI agents, 9 January 2026.&#xA;Microsoft Learn, Identity fundamentals for AI agents.&#xA;Google Cloud Architecture Center, Choose your agentic AI architecture components, reviewed 21 April 2026.&#xA;OWASP Gen AI Security Project, OWASP Top 10 for Agentic Applications for 2026.&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>An agent becomes a different kind of system the moment it can do more than answer. Give it a terminal, a cloud API, a ticketing system, or a deployment tool and its output can change a real environment.</p>

<p>That is where I stop treating the prompt as the main safety mechanism.</p>

<p>A prompt can explain intent. It can tell an agent to be careful. It can ask for confirmation before a destructive operation. But it cannot enforce an allowlist, prove that an approval happened, prevent an unrecognised command from reaching an executor, or tell me whether the claimed outcome exists outside the chat transcript.</p>

<p>Those jobs belong to a control plane.</p>



<h2 id="an-agent-is-a-loop-not-a-clever-completion">An agent is a loop, not a clever completion</h2>

<p>The useful mental model is not “a chatbot with tools.” It is a loop:</p>
<ol><li>a model receives a task, context, and the tools it may request;</li>
<li>it returns a response or a structured request for a tool;</li>
<li>the runtime decides whether and how to execute that request;</li>
<li>the result becomes new evidence for the next turn.</li></ol>

<p>Anthropic makes the same distinction in its <a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview">tool-use documentation</a>: the model requests a client-side tool call, while the application is responsible for executing it. Its <a href="https://www.anthropic.com/engineering/building-effective-agents">engineering guidance</a> describes agents as LLMs using tools from environmental feedback in a loop. It recommends pausing for human feedback at checkpoints or blockers, and using stopping conditions such as a maximum iteration count.</p>

<p>That separation matters. A model can suggest <code>restart_service</code>. It does not get to make a process restart merely by emitting those words. An executor with the right identity and policy has to accept the request.</p>

<p>This is not an argument against prompts. Good instructions make tool selection, planning, and explanations better. They are just the wrong place to put a non-negotiable security or change-management rule.</p>

<h2 id="what-a-production-policy-boundary-must-check">What a production policy boundary must check</h2>

<p>Suppose the prompt says: “Never restart production without approval.” That is a useful instruction, but it remains advice delivered to a probabilistic system. Context can be incomplete. A retrieved document can be misleading. A tool description can be ambiguous. A model can simply make the wrong call.</p>

<p>A production boundary should check rules after the model has made its request:</p>
<ul><li>Which tools exist for this agent and this task?</li>
<li>Is the requested argument shape valid for that tool?</li>
<li>Is the target inside the permitted scope?</li>
<li>Is the operation read-only or mutating?</li>
<li>If it mutates state, is there a recorded approval for this exact action?</li>
<li>Has the run exceeded its time, call, cost, or retry budget?</li></ul>

<p>The prototype below does <strong>not</strong> implement all of those checks. It only allowlists three tool names, requires that <code>arguments</code> is a JSON object, and checks a demonstration-only boolean before returning a decision. The broader list is the production design target, not a capability claim about the prototype.</p>

<p>Microsoft&#39;s <a href="https://learn.microsoft.com/en-us/startups/build/identity-management/identity-fundamentals-ai-agents">guidance on agent identity</a> makes the point directly: do not treat the model as the security boundary. It recommends narrow permissions, separation of read and write actions, and policy checks, approvals, telemetry, and audit logs around the agent.</p>

<p>The practical rule is simple: the model decides what to ask for; the control plane decides what may happen.</p>

<h2 id="a-small-policy-experiment">A small policy experiment</h2>

<p>I built a deliberately small executor to test this boundary. It is not an AI model and it is not a production controller. It is a deterministic policy layer that accepts JSON tool requests on standard input.</p>

<p>The experiment ran with this Docker command:</p>

<pre><code class="language-sh">docker run --rm --read-only --cap-drop ALL \
  --security-opt no-new-privileges:true \
  --pids-limit 32 --memory 64m --cpus 0.25 --network none \
  -i agent-control-plane-rnd:local
</code></pre>

<p>Its policy contained three tools:</p>

<table>
<thead>
<tr>
<th>Tool</th>
<th>Class</th>
<th>Rule</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>read_status</code></td>
<td>read</td>
<td>allow</td>
</tr>

<tr>
<td><code>restart_service</code></td>
<td>mutation</td>
<td>require explicit approval</td>
</tr>

<tr>
<td><code>delete_database</code></td>
<td>mutation</td>
<td>require explicit approval</td>
</tr>
</tbody>
</table>

<p>I sent five requests through it. The policy did not try to infer intent from prose. It used a small allowlist and a boolean approval signal.</p>

<pre><code class="language-text">read_status without approval            → allow
restart_service without approval        → needs_approval
restart_service with approval=true      → allow
delete_database with approval=false     → needs_approval
shell, which was not allowlisted        → deny
</code></pre>

<p>The assertion over the full sequence passed:</p>

<pre><code class="language-text">verification=PASS
</code></pre>

<p>The result is intentionally narrower than a real action pipeline. This program never invokes a downstream executor and never changes an environment. It demonstrates only that its own deterministic decision function returns <code>needs_approval</code> or <code>deny</code> for those inputs. In a real system, the executor must accept only an <code>allow</code> decision produced by a separate policy service, and the integration needs its own test proving that denied requests cannot reach the executor.</p>

<p>The boolean approval signal is deliberately unsafe outside this demonstration. It is not bound to an actor, target, action, expiry, or one-time request identifier. A real approval design needs all of those properties, plus an audit record and an executor identity that cannot bypass the gate.</p>

<h2 id="a-success-message-is-not-an-outcome">A success message is not an outcome</h2>

<p>Agent transcripts are persuasive. “Deployment completed” sounds final. It is not evidence that the intended deployment became healthy, that traffic reaches the new version, or that a rollback did not leave an older route serving users.</p>

<p>This is where the feedback loop needs ground truth. Anthropic recommends environmental feedback from tool calls or code execution at each step. Its <a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents">2026 guidance on agent evaluations</a> distinguishes the transcript from the final state of the environment: an agent may claim a flight was booked while the database shows no reservation.</p>

<p>For infrastructure work, outcome checks depend on the action:</p>
<ul><li>a GitOps change needs the desired revision plus reconciliation status and workload readiness;</li>
<li>a restart needs process health plus a user-path or synthetic check where the risk warrants it;</li>
<li>an access-policy update needs the intended authorization result from both sides of the boundary;</li>
<li>a database operation needs a bounded query or domain-level invariant, not a cheerful CLI exit message.</li></ul>

<p>The agent should receive that observed result as new context. The operator should receive a trace that makes it possible to reconstruct what happened without reading private payloads or secrets.</p>

<h2 id="approval-is-a-state-transition">Approval is a state transition</h2>

<p>A human approval request in the chat is useful only if the executor can verify it. Otherwise it is still a sentence in the model&#39;s context.</p>

<p>Treat approval as a state transition with a narrow scope:</p>

<pre><code class="language-text">planned action
  → policy creates approval request
  → named approver accepts or rejects the exact request
  → policy verifies a valid, unexpired approval
  → executor receives a bounded action
  → outcome is recorded
</code></pre>

<p>That design prevents a common failure mode: an agent receives approval for one low-risk restart, then silently applies the approval to a different host or a deletion request later in the conversation.</p>

<p>The record does not need to contain a full prompt or secret-bearing argument. It needs enough identity to answer: who approved which action against which target, under what policy, and what happened after execution.</p>

<h2 id="evals-are-part-of-the-control-plane-too">Evals are part of the control plane too</h2>

<p>A policy gate controls individual actions. It does not tell you whether a new prompt, model, tool schema, or routing change made the whole agent worse.</p>

<p>That needs evaluations. Anthropic describes an <a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents">agent harness</a> as the system that processes inputs, orchestrates tool calls, and returns results; evaluating an agent therefore means evaluating the harness and model together. Its examples include code-based checks for tool calls, parameters, outcomes, transcript properties, latency, and token use.</p>

<p>For an operations agent, I would start with a compact regression suite:</p>
<ul><li>read-only investigation requests must never produce a mutating tool request;</li>
<li>an unapproved mutation must stop at the policy gate;</li>
<li>an approved mutation must target only the approved resource;</li>
<li>unknown or malformed tool requests must fail closed;</li>
<li>the run must report an independently observed outcome, not only the model&#39;s final prose.</li></ul>

<p>Run those tests when changing the model, prompt, tool definitions, agent harness, or routing layer. They are not a guarantee of safe production behaviour. They are a way to notice that a change broke a contract you had already decided matters.</p>

<h2 id="make-the-safe-path-the-easy-path">Make the safe path the easy path</h2>

<p>The architecture does not have to be grand. Start with a small tool surface and a clear split between reading and changing state.</p>

<p>A useful minimum looks like this:</p>
<ol><li>Give the agent a separate identity with least privilege.</li>
<li>Expose explicit, narrow tools instead of a general shell where possible.</li>
<li>Validate tool arguments and scopes outside the model.</li>
<li>Require approval at the policy boundary for mutations.</li>
<li>Set limits for turns, retries, wall time, and spend.</li>
<li>Record tool request, policy decision, execution identifier, and observed outcome without retaining secrets by default.</li>
<li>Build a few regression cases before changing the harness or model route.</li></ol>

<p><a href="https://docs.cloud.google.com/architecture/choose-agentic-ai-architecture-components">Google&#39;s agent architecture guidance</a> lists tools, memory, runtimes, and design patterns as separate components of an agentic system. That separation is useful operationally. It lets us ask which component made a decision, which component enforced a boundary, and which component observed the result.</p>

<p>The prompt belongs in that system. It is where intent, context, and judgement begin. It is not where production authority should end.</p>

<h2 id="sources">Sources</h2>
<ul><li>Anthropic, <a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview">Tool use with Claude</a>.</li>
<li>Anthropic, <a href="https://www.anthropic.com/engineering/building-effective-agents">Building effective agents</a>, 19 December 2024.</li>
<li>Anthropic, <a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents">Demystifying evals for AI agents</a>, 9 January 2026.</li>
<li>Microsoft Learn, <a href="https://learn.microsoft.com/en-us/startups/build/identity-management/identity-fundamentals-ai-agents">Identity fundamentals for AI agents</a>.</li>
<li>Google Cloud Architecture Center, <a href="https://docs.cloud.google.com/architecture/choose-agentic-ai-architecture-components">Choose your agentic AI architecture components</a>, reviewed 21 April 2026.</li>
<li>OWASP Gen AI Security Project, <a href="https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/">OWASP Top 10 for Agentic Applications for 2026</a>.</li></ul>
]]></content:encoded>
      <guid>https://heyimusa.blog/the-prompt-is-not-the-control-plane</guid>
      <pubDate>Sun, 16 Aug 2026 14:51:37 +0000</pubDate>
    </item>
    <item>
      <title>No errors is not a health signal: monitoring the work that stopped happening</title>
      <link>https://heyimusa.blog/no-errors-is-not-a-health-signal</link>
      <description>&lt;![CDATA[&#xA;&#xA;A service can be quiet for two very different reasons. Maybe it is healthy and nobody is using it. Maybe the checkout pipeline stopped or the nightly report never ran. Error rate can still be zero.&#xA;&#xA;That distinction matters because most alert rules start with an error condition. 5xx exceeds a threshold. Queue depth grows. Latency crosses a limit. Those are useful alerts. They answer a narrow question: did the system produce evidence of a bad thing?&#xA;&#xA;They do not answer a second question that is often closer to the outage: did the work we expected actually happen?&#xA;&#xA;!--more--&#xA;&#xA;An error-rate threshold remains green while an expected heartbeat stops; a freshness window later triggers an alert.&#xA;&#xA;A quiet graph is ambiguous&#xA;&#xA;I have seen dashboards that looked reassuring precisely because the failing path had stopped producing the data that drove the dashboard. There was no surge of errors. There was no obvious red line. There was just a missing stream of successful work.&#xA;&#xA;That can happen in ordinary ways:&#xA;&#xA;a scheduled job exits before its success metric is emitted;&#xA;a queue consumer is alive but no longer receives messages because a subscription, credential, or routing rule changed;&#xA;an analytics or billing pipeline keeps its workers up while new input stops arriving;&#xA;a deployment routes traffic somewhere unexpected, so the old service has clean metrics but the intended user path sees nothing useful;&#xA;the telemetry path fails, leaving the application and the monitor to disagree about what exists.&#xA;&#xA;The first mistake is to treat absence as a universal failure. A low-traffic service may legitimately have no requests for an hour. A batch job may only be expected once per day. Kubernetes workloads can disappear because a deployment intentionally scaled down. The contract has to name the thing that should recur and the time in which it should recur.&#xA;&#xA;“Zero requests” is not enough. “The reconciliation job that normally completes every fifteen minutes has not recorded a successful completion for thirty minutes” is an operational statement.&#xA;&#xA;Expected work needs a freshness contract&#xA;&#xA;For every path worth paging on, I want four answers written down:&#xA;&#xA;What event proves the path completed the useful work?&#xA;Who or what emits that evidence?&#xA;How late can it be before someone needs to decide something?&#xA;What action should the alert receiver take first?&#xA;&#xA;That evidence can be a timestamp gauge, a monotonically increasing completion counter, an externally observed heartbeat, a synthetic transaction, or a record in a downstream system. The mechanism matters less than the contract. The signal should describe completion of the work, not merely survival of the process assigned to do it.&#xA;&#xA;A readiness endpoint might prove that a web process can accept a request. It does not prove that a daily export still reaches its destination. A successful Kubernetes rollout shows that the workload reached its configured rollout conditions, such as updated and available replicas. It does not prove that the business event after that rollout still travels through every dependency.&#xA;&#xA;Google’s SRE workbook makes the related point about monitoring-data freshness: stale data slows incident response and can lead people to act on incorrect information. That applies to a dashboard, but it also applies to the system being observed. If the evidence of a required activity is stale, the operator should not have to infer health from a lack of complaints.&#xA;&#xA;A small Docker experiment&#xA;&#xA;I built a disposable Docker Compose experiment with two containers: a tiny HTTP exporter and Prometheus. For this test, the exporter served a heartbeat gauge whose value was the current Unix time on every scrape:&#xA;&#xA;jobheartbeatlastsuccessunixtime{job=&#34;nightly-report&#34;} current-unix-time&#xA;&#xA;Despite the metric name, this was deliberately only a scrape-path signal. It was not evidence that a nightly report completed. A real completion metric must update only after the job has completed its meaningful work.&#xA;&#xA;Prometheus scraped it every second. The alert rule was deliberately about absence, not a numeric threshold:&#xA;&#xA;absentovertime(&#xA;  jobheartbeatlastsuccessunixtime{exportedjob=&#34;nightly-report&#34;}[5s]&#xA;) == 1&#xA;&#xA;The rule waited another two seconds before firing. The timings are intentionally unrealistically short so the behavior fits in a small test; they are not a production recommendation.&#xA;&#xA;The healthy state produced one scrape target with health=up and no firing alert:&#xA;&#xA;targethealth=up&#xA;healthyfiringalerts=0&#xA;&#xA;Then I stopped only the exporter container. Prometheus remained running. Once no matching sample remained in the five-second range, the next rule evaluation made the alert pending. After the condition had remained true for at least for: 2s across subsequent evaluations, it fired. Exact wall-clock timing depends on scrape and rule-evaluation alignment:&#xA;&#xA;missingfiringalerts=1&#xA;alertstate=firing&#xA;alertname=ExpectedHeartbeatMissing&#xA;severity=page&#xA;&#xA;This selector was intentionally scoped so the absence function yielded one synthetic alert. It was not a per-target missing-series detector.&#xA;&#xA;That proves a narrow thing: for this scrape-path metric and this rule, Prometheus can turn missing scrape-visible data into an alert. It does not prove that a real nightly report completed, that its output was correct, or that a production notification channel would wake the right person. Those require a stronger, path-specific check.&#xA;&#xA;The experiment also surfaced an easy-to-miss detail. With Prometheus’s default honorlabels: false, the scrape configuration owns the target’s job label. Because the exporter also exposed job=&#34;nightly-report&#34;, Prometheus renamed the scraped label to exportedjob=&#34;nightly-report&#34;. A configuration using honorlabels: true behaves differently. That is exactly the kind of label collision that should be caught in a test before an absence alert earns trust.&#xA;&#xA;No data is not one state&#xA;&#xA;Missing evidence has several meanings, and collapsing them into one generic “no data” alert creates noise.&#xA;&#xA;Grafana distinguishes at least three cases: a query error, no data at all, and a missing series where other series still exist. Those need different responses. A datasource failure may be a monitoring incident. One missing regional series might be a workload problem. A deliberately scaled-down job could be expected behavior.&#xA;&#xA;Cloud Monitoring has a useful guardrail for metric-absence policies: it requires at least one successful measurement after the policy is installed or changed. Otherwise a never-emitted metric would look exactly like a broken metric. Prometheus does not enforce that guard. An absentovertime rule can become true when a matching series has never existed. In this experiment, the healthy pre-check established prior observation manually. In production, pair the detector with an expected-membership source, an initialization process, or an alerting policy that accounts for newly introduced signals.&#xA;&#xA;Prometheus’s absent() and absentovertime() are useful building blocks, but they do not replace that reasoning. A broad expression can tell you that nothing matched. It cannot decide whether the missing series was intentionally removed, whether a new label should exist, or whether an upstream event actually reached the customer-facing outcome.&#xA;&#xA;Where absence checks earn their place&#xA;&#xA;I would add an absence or freshness check when the missing work has a clear owner and a bounded recovery action. Some examples:&#xA;&#xA;| Expected activity | Useful evidence | First response |&#xA;| --- | --- | --- |&#xA;| Scheduled backup | Timestamp of a verified successful backup | Check scheduler, credentials, storage destination, and restore metadata |&#xA;| Payment/webhook ingestion | Recent accepted event counter or end-to-end synthetic event | Check provider delivery, queue/subscription, and consumer lag |&#xA;| Reconciliation job | Last successful completion timestamp and record count | Check source availability, job logs, and output freshness |&#xA;| Telemetry pipeline | Recent collector/exporter heartbeat plus ingestion signal | Separate application health from observability-path health |&#xA;| Customer route after a deploy | Bounded synthetic request with an expected semantic response | Check DNS, TLS, routing, auth boundary, and backend selection |&#xA;&#xA;The table is not a template for paging on everything. It is a prompt to state the unit of useful work. A low-volume workflow may need a scheduled expectation. A high-volume request path may need a rate floor with a seasonality-aware baseline. An external dependency may need an independent probe because a heartbeat emitted from inside the same failure domain can lie with the rest of the system.&#xA;&#xA;Do not turn freshness into another noisy threshold&#xA;&#xA;Absence alerts get bad quickly when they ignore operating context.&#xA;&#xA;Start with a generous window. A job expected every fifteen minutes should not page at minute sixteen if the scheduler occasionally starts late and the business can tolerate that delay. Make the window reflect the actual deadline, not the nominal schedule. Include a warning before a page when that helps someone intervene early.&#xA;&#xA;Keep labels that identify the owner and expected cadence in the alert design, but be precise about the PromQL behavior. For absentovertime alerts, put stable identity labels needed for routing in equality matchers or static rule labels. Labels that existed only on the vanished series, such as an ephemeral pod or instance, cannot be preserved by the absence result. A broad absence query produces one synthetic alert, not one alert for each missing target.&#xA;&#xA;For dynamic fleets, use a source of expected membership: a deployment desired-replica signal, a durable job registry, or a service-level aggregation. Otherwise scale-down and churn become indistinguishable from loss.&#xA;&#xA;Most importantly, keep the alert tied to a decision. “No data from metric X” is a debugging clue. “Nightly report did not complete by 06:30 UTC; customer statements may be stale; inspect run ID, source freshness, and destination write” gives the responder somewhere to begin.&#xA;&#xA;The check after the fix matters&#xA;&#xA;When the path is repaired, the dashboard often returns to green before the contract has been met. The process restarts. The consumer reconnects. The queue drains a little. None of that proves the missing work has completed.&#xA;&#xA;The recovery check should ask for fresh evidence:&#xA;&#xA;Did a new completion timestamp arrive?&#xA;Did a known synthetic transaction cross the boundary and produce the expected downstream record?&#xA;Did the next scheduled run finish within its window?&#xA;Did the telemetry signal return independently of the application’s own health endpoint?&#xA;&#xA;That is also the reason I prefer “last successful completion” over a generic process heartbeat for scheduled work. A process can be alive all day without doing the job it exists to do.&#xA;&#xA;A green dashboard is a claim with limits&#xA;&#xA;Threshold alerts catch bad values. Absence checks catch missing evidence. Synthetic checks catch whether an important path still behaves as a user or dependency expects. None is sufficient on its own.&#xA;&#xA;The practical goal is smaller than complete observability. Pick the work whose silence would matter, give it an explicit freshness contract, and test the detector by making that evidence disappear in a safe environment. Then make the alert useful enough that the person receiving it knows what decision belongs to them.&#xA;&#xA;No errors is not a health signal. Sometimes it only means nobody is left to report the failure.&#xA;&#xA;Sources&#xA;&#xA;Google SRE Workbook: Monitoring — monitoring-data freshness and incident-response implications.&#xA;Google SRE Workbook: Data Processing Pipelines — data/pipeline health and freshness-oriented operational concerns.&#xA;Prometheus query functions — semantics of absent() and absentovertime().&#xA;Grafana: Handle missing data in alerting — No Data, Missing Series, and explicit missing-data handling.&#xA;Google Cloud Monitoring: metric-absence alerting — requirement for an initial successful measurement before absence detection.&#xA;Sarah Drasner on X — the prompt: “Does your monitoring detect absence, or only error?” The article’s technical claims are based on the sources and Docker experiment above, not the post alone.]]&gt;</description>
      <content:encoded><![CDATA[<p>A service can be quiet for two very different reasons. Maybe it is healthy and nobody is using it. Maybe the checkout pipeline stopped or the nightly report never ran. Error rate can still be zero.</p>

<p>That distinction matters because most alert rules start with an error condition. <code>5xx</code> exceeds a threshold. Queue depth grows. Latency crosses a limit. Those are useful alerts. They answer a narrow question: did the system produce evidence of a bad thing?</p>

<p>They do not answer a second question that is often closer to the outage: did the work we expected actually happen?</p>



<p><img src="/img/articles/no-errors-is-not-a-health-signal-absence-v1.svg" alt="An error-rate threshold remains green while an expected heartbeat stops; a freshness window later triggers an alert."></p>

<h2 id="a-quiet-graph-is-ambiguous">A quiet graph is ambiguous</h2>

<p>I have seen dashboards that looked reassuring precisely because the failing path had stopped producing the data that drove the dashboard. There was no surge of errors. There was no obvious red line. There was just a missing stream of successful work.</p>

<p>That can happen in ordinary ways:</p>
<ul><li>a scheduled job exits before its success metric is emitted;</li>
<li>a queue consumer is alive but no longer receives messages because a subscription, credential, or routing rule changed;</li>
<li>an analytics or billing pipeline keeps its workers up while new input stops arriving;</li>
<li>a deployment routes traffic somewhere unexpected, so the old service has clean metrics but the intended user path sees nothing useful;</li>
<li>the telemetry path fails, leaving the application and the monitor to disagree about what exists.</li></ul>

<p>The first mistake is to treat absence as a universal failure. A low-traffic service may legitimately have no requests for an hour. A batch job may only be expected once per day. Kubernetes workloads can disappear because a deployment intentionally scaled down. The contract has to name the thing that should recur and the time in which it should recur.</p>

<p>“Zero requests” is not enough. “The reconciliation job that normally completes every fifteen minutes has not recorded a successful completion for thirty minutes” is an operational statement.</p>

<h2 id="expected-work-needs-a-freshness-contract">Expected work needs a freshness contract</h2>

<p>For every path worth paging on, I want four answers written down:</p>
<ol><li>What event proves the path completed the useful work?</li>
<li>Who or what emits that evidence?</li>
<li>How late can it be before someone needs to decide something?</li>
<li>What action should the alert receiver take first?</li></ol>

<p>That evidence can be a timestamp gauge, a monotonically increasing completion counter, an externally observed heartbeat, a synthetic transaction, or a record in a downstream system. The mechanism matters less than the contract. The signal should describe completion of the work, not merely survival of the process assigned to do it.</p>

<p>A readiness endpoint might prove that a web process can accept a request. It does not prove that a daily export still reaches its destination. A successful Kubernetes rollout shows that the workload reached its configured rollout conditions, such as updated and available replicas. It does not prove that the business event after that rollout still travels through every dependency.</p>

<p>Google’s SRE workbook makes the related point about monitoring-data freshness: stale data slows incident response and can lead people to act on incorrect information. That applies to a dashboard, but it also applies to the system being observed. If the evidence of a required activity is stale, the operator should not have to infer health from a lack of complaints.</p>

<h2 id="a-small-docker-experiment">A small Docker experiment</h2>

<p>I built a disposable Docker Compose experiment with two containers: a tiny HTTP exporter and Prometheus. For this test, the exporter served a heartbeat gauge whose value was the current Unix time on every scrape:</p>

<pre><code class="language-text">job_heartbeat_last_success_unixtime{job=&#34;nightly-report&#34;} &lt;current-unix-time&gt;
</code></pre>

<p>Despite the metric name, this was deliberately only a scrape-path signal. It was not evidence that a nightly report completed. A real completion metric must update only after the job has completed its meaningful work.</p>

<p>Prometheus scraped it every second. The alert rule was deliberately about absence, not a numeric threshold:</p>

<pre><code class="language-promql">absent_over_time(
  job_heartbeat_last_success_unixtime{exported_job=&#34;nightly-report&#34;}[5s]
) == 1
</code></pre>

<p>The rule waited another two seconds before firing. The timings are intentionally unrealistically short so the behavior fits in a small test; they are not a production recommendation.</p>

<p>The healthy state produced one scrape target with <code>health=up</code> and no firing alert:</p>

<pre><code class="language-text">target_health=up
healthy_firing_alerts=0
</code></pre>

<p>Then I stopped only the exporter container. Prometheus remained running. Once no matching sample remained in the five-second range, the next rule evaluation made the alert pending. After the condition had remained true for at least <code>for: 2s</code> across subsequent evaluations, it fired. Exact wall-clock timing depends on scrape and rule-evaluation alignment:</p>

<pre><code class="language-text">missing_firing_alerts=1
alert_state=firing
alertname=ExpectedHeartbeatMissing
severity=page
</code></pre>

<p>This selector was intentionally scoped so the absence function yielded one synthetic alert. It was not a per-target missing-series detector.</p>

<p>That proves a narrow thing: for this scrape-path metric and this rule, Prometheus can turn missing scrape-visible data into an alert. It does not prove that a real nightly report completed, that its output was correct, or that a production notification channel would wake the right person. Those require a stronger, path-specific check.</p>

<p>The experiment also surfaced an easy-to-miss detail. With Prometheus’s default <code>honor_labels: false</code>, the scrape configuration owns the target’s <code>job</code> label. Because the exporter also exposed <code>job=&#34;nightly-report&#34;</code>, Prometheus renamed the scraped label to <code>exported_job=&#34;nightly-report&#34;</code>. A configuration using <code>honor_labels: true</code> behaves differently. That is exactly the kind of label collision that should be caught in a test before an absence alert earns trust.</p>

<h2 id="no-data-is-not-one-state">No data is not one state</h2>

<p>Missing evidence has several meanings, and collapsing them into one generic “no data” alert creates noise.</p>

<p>Grafana distinguishes at least three cases: a query error, no data at all, and a missing series where other series still exist. Those need different responses. A datasource failure may be a monitoring incident. One missing regional series might be a workload problem. A deliberately scaled-down job could be expected behavior.</p>

<p>Cloud Monitoring has a useful guardrail for metric-absence policies: it requires at least one successful measurement after the policy is installed or changed. Otherwise a never-emitted metric would look exactly like a broken metric. Prometheus does not enforce that guard. An <code>absent_over_time</code> rule can become true when a matching series has never existed. In this experiment, the healthy pre-check established prior observation manually. In production, pair the detector with an expected-membership source, an initialization process, or an alerting policy that accounts for newly introduced signals.</p>

<p>Prometheus’s <code>absent()</code> and <code>absent_over_time()</code> are useful building blocks, but they do not replace that reasoning. A broad expression can tell you that nothing matched. It cannot decide whether the missing series was intentionally removed, whether a new label should exist, or whether an upstream event actually reached the customer-facing outcome.</p>

<h2 id="where-absence-checks-earn-their-place">Where absence checks earn their place</h2>

<p>I would add an absence or freshness check when the missing work has a clear owner and a bounded recovery action. Some examples:</p>

<table>
<thead>
<tr>
<th>Expected activity</th>
<th>Useful evidence</th>
<th>First response</th>
</tr>
</thead>

<tbody>
<tr>
<td>Scheduled backup</td>
<td>Timestamp of a verified successful backup</td>
<td>Check scheduler, credentials, storage destination, and restore metadata</td>
</tr>

<tr>
<td>Payment/webhook ingestion</td>
<td>Recent accepted event counter or end-to-end synthetic event</td>
<td>Check provider delivery, queue/subscription, and consumer lag</td>
</tr>

<tr>
<td>Reconciliation job</td>
<td>Last successful completion timestamp and record count</td>
<td>Check source availability, job logs, and output freshness</td>
</tr>

<tr>
<td>Telemetry pipeline</td>
<td>Recent collector/exporter heartbeat plus ingestion signal</td>
<td>Separate application health from observability-path health</td>
</tr>

<tr>
<td>Customer route after a deploy</td>
<td>Bounded synthetic request with an expected semantic response</td>
<td>Check DNS, TLS, routing, auth boundary, and backend selection</td>
</tr>
</tbody>
</table>

<p>The table is not a template for paging on everything. It is a prompt to state the unit of useful work. A low-volume workflow may need a scheduled expectation. A high-volume request path may need a rate floor with a seasonality-aware baseline. An external dependency may need an independent probe because a heartbeat emitted from inside the same failure domain can lie with the rest of the system.</p>

<h2 id="do-not-turn-freshness-into-another-noisy-threshold">Do not turn freshness into another noisy threshold</h2>

<p>Absence alerts get bad quickly when they ignore operating context.</p>

<p>Start with a generous window. A job expected every fifteen minutes should not page at minute sixteen if the scheduler occasionally starts late and the business can tolerate that delay. Make the window reflect the actual deadline, not the nominal schedule. Include a warning before a page when that helps someone intervene early.</p>

<p>Keep labels that identify the owner and expected cadence in the alert design, but be precise about the PromQL behavior. For <code>absent_over_time</code> alerts, put stable identity labels needed for routing in equality matchers or static rule labels. Labels that existed only on the vanished series, such as an ephemeral <code>pod</code> or <code>instance</code>, cannot be preserved by the absence result. A broad absence query produces one synthetic alert, not one alert for each missing target.</p>

<p>For dynamic fleets, use a source of expected membership: a deployment desired-replica signal, a durable job registry, or a service-level aggregation. Otherwise scale-down and churn become indistinguishable from loss.</p>

<p>Most importantly, keep the alert tied to a decision. “No data from metric X” is a debugging clue. “Nightly report did not complete by 06:30 UTC; customer statements may be stale; inspect run ID, source freshness, and destination write” gives the responder somewhere to begin.</p>

<h2 id="the-check-after-the-fix-matters">The check after the fix matters</h2>

<p>When the path is repaired, the dashboard often returns to green before the contract has been met. The process restarts. The consumer reconnects. The queue drains a little. None of that proves the missing work has completed.</p>

<p>The recovery check should ask for fresh evidence:</p>
<ul><li>Did a new completion timestamp arrive?</li>
<li>Did a known synthetic transaction cross the boundary and produce the expected downstream record?</li>
<li>Did the next scheduled run finish within its window?</li>
<li>Did the telemetry signal return independently of the application’s own health endpoint?</li></ul>

<p>That is also the reason I prefer “last successful completion” over a generic process heartbeat for scheduled work. A process can be alive all day without doing the job it exists to do.</p>

<h2 id="a-green-dashboard-is-a-claim-with-limits">A green dashboard is a claim with limits</h2>

<p>Threshold alerts catch bad values. Absence checks catch missing evidence. Synthetic checks catch whether an important path still behaves as a user or dependency expects. None is sufficient on its own.</p>

<p>The practical goal is smaller than complete observability. Pick the work whose silence would matter, give it an explicit freshness contract, and test the detector by making that evidence disappear in a safe environment. Then make the alert useful enough that the person receiving it knows what decision belongs to them.</p>

<p>No errors is not a health signal. Sometimes it only means nobody is left to report the failure.</p>

<h2 id="sources">Sources</h2>
<ul><li><a href="https://sre.google/workbook/monitoring/">Google SRE Workbook: Monitoring</a> — monitoring-data freshness and incident-response implications.</li>
<li><a href="https://sre.google/workbook/data-processing/">Google SRE Workbook: Data Processing Pipelines</a> — data/pipeline health and freshness-oriented operational concerns.</li>
<li><a href="https://prometheus.io/docs/prometheus/latest/querying/functions/">Prometheus query functions</a> — semantics of <code>absent()</code> and <code>absent_over_time()</code>.</li>
<li><a href="https://grafana.com/docs/grafana/latest/alerting/guides/missing-data/">Grafana: Handle missing data in alerting</a> — No Data, Missing Series, and explicit missing-data handling.</li>
<li><a href="https://docs.cloud.google.com/monitoring/alerts/metric-absence">Google Cloud Monitoring: metric-absence alerting</a> — requirement for an initial successful measurement before absence detection.</li>
<li><a href="https://x.com/sarah_edo/status/2087942460357798101">Sarah Drasner on X</a> — the prompt: “Does your monitoring detect absence, or only error?” The article’s technical claims are based on the sources and Docker experiment above, not the post alone.</li></ul>
]]></content:encoded>
      <guid>https://heyimusa.blog/no-errors-is-not-a-health-signal</guid>
      <pubDate>Thu, 13 Aug 2026 17:30:41 +0000</pubDate>
    </item>
    <item>
      <title>Refactoring is bonsai work, but the shears are tests</title>
      <link>https://heyimusa.blog/refactoring-is-bonsai-work</link>
      <description>&lt;![CDATA[A mature codebase rarely announces that it needs refactoring. It makes a small change feel oddly expensive. A new route requires touching three modules whose names no longer describe their responsibilities. A dependency update turns into archaeology. An incident fix needs a second person on the call because nobody is certain which branch of the conditional protects the customer path.&#xA;&#xA;That is the moment I find the bonsai metaphor useful, with one important correction: the job is not to make code look pretty. The job is to preserve a living system while shaping it so the next change has somewhere safe to go.&#xA;&#xA;!--more--&#xA;&#xA;The metaphor is useful because growth is the problem&#xA;&#xA;Bonsai is not a miniature tree that happens by accident. Its shape comes from repeated, deliberate interventions: pruning, wiring, repotting, and deciding which growth to keep. The tree stays alive, but it does not stay untouched.&#xA;&#xA;Software has the same tension. A healthy service keeps changing because product requirements, dependencies, traffic, and operators change. Left alone, the code can still compile and serve requests while its internal shape becomes costly to understand. The first symptom is not necessarily a defect. Often it is hesitation: the team starts treating a local edit as if it might wake something elsewhere.&#xA;&#xA;Martin Fowler&#39;s definition adds the part that is easy to lose: refactoring changes internal structure to make software easier to understand and cheaper to modify, without changing observable behavior. That excludes feature work, a migration that changes a contract, and a rewrite dressed up as cleanup. Those can all be worthwhile. They simply need different planning and different evidence.&#xA;&#xA;Calling everything &#34;refactoring&#34; is how a small maintenance task acquires an unlimited blast radius.&#xA;&#xA;A bonsai is not a weekly haircut&#xA;&#xA;The weak version of the metaphor produces cosmetic work: rename a few variables, reorder imports, split a long file, declare victory. Those changes can help, but they do not automatically make future changes safer.&#xA;&#xA;A better question is: what recurring change does this shape make expensive?&#xA;&#xA;If every new HTTP endpoint requires copying authorization checks, the useful work might be extracting an explicit policy boundary. If a feature flag has spread through handlers, workers, and templates, the issue is probably lifecycle ownership rather than the length of any one function. If an on-call engineer cannot identify the request path during an outage, improving observability or module boundaries may matter more than satisfying a linter.&#xA;&#xA;Refactoring earns its keep when a real next change has exposed a structural constraint.&#xA;&#xA;The shears are tests, not confidence&#xA;&#xA;There is a dangerous part of the gardening analogy. Pruning a branch is visible. A behavior regression may be invisible until production traffic finds it.&#xA;&#xA;That is why tests are not an optional polish step around refactoring. They are the evidence that the system still behaves as intended after an internal change. The refactoring literature repeatedly emphasizes small, safe mechanics. In a 2020 case study on an oversized class, Clare Sudbery describes keeping the code compiling and the tests running at every step, with coverage in place before the refactor starts.&#xA;&#xA;For a service, I would want the narrowest test that protects the behavior being moved, plus the checks that make the deployment credible:&#xA;&#xA;a unit or component test for the rule being extracted;&#xA;a contract or integration test where the code crosses a boundary; and&#xA;where proportionate, a post-deployment synthetic probe or smoke check for the important path.&#xA;&#xA;The exact stack differs. If the available evidence cannot support the claim that the intended behavior stayed stable, describe the change more modestly or add the missing check.&#xA;&#xA;This also gives a useful stop condition. When a proposed cleanup cannot be protected by a reasonably scoped test, the first task may be characterization: document and test what the system does today, including the awkward behavior nobody wants to rediscover during an incident. Only then is the pruning safe.&#xA;&#xA;AI makes overgrowth faster&#xA;&#xA;AI-assisted coding changes the economics here. It can produce a working branch, a duplicate helper, or another configuration path in seconds. That is useful when the team is deliberate. It also means structural debt can arrive at a rate that manual coding rarely achieved.&#xA;&#xA;A 2025 paper, The Shift from Writing to Pruning Software, proposes a bonsai-inspired interface for AI-generated code. Its core observation is sensible: when generation becomes cheap, the human task shifts toward navigating, evaluating, and reshaping a larger body of code. The paper proposes an interface; it is not evidence that the approach improves production reliability. Still, the direction matches a practical concern: generation can reduce the cost of adding code without reducing the cost of owning it.&#xA;&#xA;The response should not be &#34;never use AI.&#34; It should be to give generated code the same ownership questions as handwritten code:&#xA;&#xA;What responsibility does this new code own?&#xA;Which existing path does it duplicate or bypass?&#xA;What behavior proves it belongs here?&#xA;If it needs to change later, where would an engineer look first?&#xA;&#xA;If those questions have no good answer, the code is not ready just because it passes the current build.&#xA;&#xA;An infrastructure engineer prunes more than application code&#xA;&#xA;For an infrastructure or platform engineer, the tree extends beyond a service repository: delivery pipelines; infrastructure definitions and environment overlays; identity, network, and secret boundaries; runtime configuration; observability; and the runbooks and recovery paths that connect them.&#xA;&#xA;The same failure mode appears at every layer: a workaround becomes a pattern, a copied pipeline step diverges, an exception disappears into an overlay, or an alert keeps firing because no one owns the decision it is meant to support. Declarative infrastructure can accumulate structural debt just as application code can.&#xA;&#xA;That makes refactoring part of the platform engineer&#39;s job, not a side activity reserved for developers. The target is not maximal abstraction. It is a platform whose next user, deploy, incident, and security change can be made with a clear owner, a small review surface, and evidence.&#xA;&#xA;A practical sweep across the field looks like this:&#xA;&#xA;Platform and developer experience: turn tribal deployment knowledge into paved paths, understandable templates, and documented interfaces. Remove a platform abstraction when it only hides the system without removing work.&#xA;CI/CD and GitOps: consolidate duplicated release logic, make promotion and rollback paths explicit, and keep generated or rendered configuration reviewable. A pipeline refactor is incomplete if an operator cannot tell what will reach production.&#xA;Cloud and infrastructure as code: separate reusable modules from environment-specific intent, name ownership boundaries, and avoid copy-pasted resources that drift independently. Treat plans and policy checks as evidence, while remembering that they do not prove the live system is unchanged.&#xA;Kubernetes and runtime operations: simplify workload, ingress, Gateway, policy, and configuration overlays while preserving the routing and rollout contract. Rendered manifests and a bounded runtime check are stronger evidence than a tidy directory tree.&#xA;Security and identity: reduce broad permissions, clarify trust boundaries, remove obsolete access paths, and make secret rotation and break-glass procedures testable. A cleaner IAM policy is not safer unless the required workload still functions and the unwanted path is actually closed.&#xA;Networking and reliability: replace undocumented routing exceptions with explicit contracts; preserve DNS, TLS, timeout, retry, and failover behavior as you simplify. For a customer-facing path, verification must extend beyond a syntax check.&#xA;Observability and incident response: prune dashboards, alerts, and runbooks that no longer map to an operational decision. Keep the signals that let an engineer answer what changed, who is affected, and how to roll back.&#xA;Cost and capacity: remove idle or duplicated resources only after measuring dependency and demand. Cost reduction is a production change, not janitorial work.&#xA;&#xA;None of these areas needs a giant “platform cleanup” programme to improve. The valuable unit is still a small, explainable structural change backed by the evidence appropriate to that layer: an IaC plan, a rendered manifest diff, a policy evaluation, a synthetic request, a rollout observation, or a recovery exercise. The appropriate checks vary by layer, but each should support a specific preservation claim.&#xA;&#xA;Prune on the path of work&#xA;&#xA;A bonsai owner does not reshape the entire tree every time they notice one awkward branch. The same restraint matters in production code.&#xA;&#xA;I prefer refactoring that is adjacent to a real change: improve a boundary before extending it, remove duplication when the second copy appears, name a concept when the team needs to discuss it, and leave unrelated cleanups alone. This keeps the diff reviewable and lets the pull request tell a coherent story.&#xA;&#xA;It also makes verification more honest. A narrowly scoped refactor can state what it preserved, which tests exercised that claim, and what it deliberately did not touch. A broad &#34;cleanup&#34; commit usually cannot.&#xA;&#xA;There are times to fund a larger structural effort. Repeated incidents, a blocked security upgrade, an untestable critical path, or a migration that keeps failing for the same architectural reason are all stronger signals than aesthetic discomfort. Even then, split the work into behavior-preserving slices where possible. The system does not care that the end state looked tidy in a diagram if the path there breaks customers.&#xA;&#xA;A small operating loop&#xA;&#xA;Before calling a change refactoring, I would write down four things:&#xA;&#xA;The behavior to preserve. Describe it from the caller or user perspective, not from the internal class name.&#xA;The structural pressure. Name the repeated cost: duplicated policy, hidden dependency, impossible test setup, ambiguous ownership, or something equally concrete.&#xA;The evidence. Identify the test or probe that should remain green before and after the change.&#xA;The boundary. State what is not being redesigned in this pull request.&#xA;&#xA;Then make one structural move, run the evidence, and review the diff as if you were the next person debugging it at an inconvenient hour.&#xA;&#xA;That is less romantic than trimming a miniature tree. It is also more useful. Refactoring is routine maintenance of the structure through which future changes must travel.&#xA;&#xA;Limits of the metaphor&#xA;&#xA;Bonsai suggests a single careful owner and a stable aesthetic goal. Real systems have many owners, competing requirements, and behavior that must change. Refactoring will not replace migration planning, incident response, performance work, or product decisions. A green test suite also cannot prove every production property, especially across external dependencies and real traffic.&#xA;&#xA;So use the image lightly. Keep the discipline.&#xA;&#xA;Preserve the behavior you can state. Improve the structure that is making real work harder. Keep the change small enough to understand. And do not put the shears down until you have evidence that the tree still lives.&#xA;&#xA;Sources and further reading&#xA;&#xA;Martin Fowler, Refactoring. Fowler defines refactoring as changing internal structure to make software easier to understand and cheaper to modify without changing observable behavior.&#xA;Clare Sudbery, Refactoring: This class is too large (2020). A practical case study of incremental refactoring, covering tests, compiling at every step, and small commits.&#xA;Ulam Labs, Why is refactoring your code important? (2020). A useful example of the bonsai analogy, though this article intentionally avoids treating cleanliness or security as automatic outcomes of refactoring.&#xA;The Shift from Writing to Pruning Software: A Bonsai-Inspired IDE for Reshaping AI Generated Code, arXiv:2503.02833 (2025). A research proposal about navigating and reshaping generated code, not a production-effectiveness study.&#xA;&#xA;!-- taxonomy: #topicplatformengineering #topicgitopsdelivery --&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>A mature codebase rarely announces that it needs refactoring. It makes a small change feel oddly expensive. A new route requires touching three modules whose names no longer describe their responsibilities. A dependency update turns into archaeology. An incident fix needs a second person on the call because nobody is certain which branch of the conditional protects the customer path.</p>

<p>That is the moment I find the bonsai metaphor useful, with one important correction: the job is not to make code look pretty. The job is to preserve a living system while shaping it so the next change has somewhere safe to go.</p>



<h2 id="the-metaphor-is-useful-because-growth-is-the-problem">The metaphor is useful because growth is the problem</h2>

<p>Bonsai is not a miniature tree that happens by accident. Its shape comes from repeated, deliberate interventions: pruning, wiring, repotting, and deciding which growth to keep. The tree stays alive, but it does not stay untouched.</p>

<p>Software has the same tension. A healthy service keeps changing because product requirements, dependencies, traffic, and operators change. Left alone, the code can still compile and serve requests while its internal shape becomes costly to understand. The first symptom is not necessarily a defect. Often it is hesitation: the team starts treating a local edit as if it might wake something elsewhere.</p>

<p>Martin Fowler&#39;s definition adds the part that is easy to lose: refactoring changes internal structure to make software easier to understand and cheaper to modify, without changing observable behavior. That excludes feature work, a migration that changes a contract, and a rewrite dressed up as cleanup. Those can all be worthwhile. They simply need different planning and different evidence.</p>

<p>Calling everything “refactoring” is how a small maintenance task acquires an unlimited blast radius.</p>

<h2 id="a-bonsai-is-not-a-weekly-haircut">A bonsai is not a weekly haircut</h2>

<p>The weak version of the metaphor produces cosmetic work: rename a few variables, reorder imports, split a long file, declare victory. Those changes can help, but they do not automatically make future changes safer.</p>

<p>A better question is: <em>what recurring change does this shape make expensive?</em></p>

<p>If every new HTTP endpoint requires copying authorization checks, the useful work might be extracting an explicit policy boundary. If a feature flag has spread through handlers, workers, and templates, the issue is probably lifecycle ownership rather than the length of any one function. If an on-call engineer cannot identify the request path during an outage, improving observability or module boundaries may matter more than satisfying a linter.</p>

<p>Refactoring earns its keep when a real next change has exposed a structural constraint.</p>

<h2 id="the-shears-are-tests-not-confidence">The shears are tests, not confidence</h2>

<p>There is a dangerous part of the gardening analogy. Pruning a branch is visible. A behavior regression may be invisible until production traffic finds it.</p>

<p>That is why tests are not an optional polish step around refactoring. They are the evidence that the system still behaves as intended after an internal change. The refactoring literature repeatedly emphasizes small, safe mechanics. In a 2020 case study on an oversized class, Clare Sudbery describes keeping the code compiling and the tests running at every step, with coverage in place before the refactor starts.</p>

<p>For a service, I would want the narrowest test that protects the behavior being moved, plus the checks that make the deployment credible:</p>
<ul><li>a unit or component test for the rule being extracted;</li>
<li>a contract or integration test where the code crosses a boundary; and</li>
<li>where proportionate, a post-deployment synthetic probe or smoke check for the important path.</li></ul>

<p>The exact stack differs. If the available evidence cannot support the claim that the intended behavior stayed stable, describe the change more modestly or add the missing check.</p>

<p>This also gives a useful stop condition. When a proposed cleanup cannot be protected by a reasonably scoped test, the first task may be characterization: document and test what the system does today, including the awkward behavior nobody wants to rediscover during an incident. Only then is the pruning safe.</p>

<h2 id="ai-makes-overgrowth-faster">AI makes overgrowth faster</h2>

<p>AI-assisted coding changes the economics here. It can produce a working branch, a duplicate helper, or another configuration path in seconds. That is useful when the team is deliberate. It also means structural debt can arrive at a rate that manual coding rarely achieved.</p>

<p>A 2025 paper, <em>The Shift from Writing to Pruning Software</em>, proposes a bonsai-inspired interface for AI-generated code. Its core observation is sensible: when generation becomes cheap, the human task shifts toward navigating, evaluating, and reshaping a larger body of code. The paper proposes an interface; it is not evidence that the approach improves production reliability. Still, the direction matches a practical concern: generation can reduce the cost of adding code without reducing the cost of owning it.</p>

<p>The response should not be “never use AI.” It should be to give generated code the same ownership questions as handwritten code:</p>
<ul><li>What responsibility does this new code own?</li>
<li>Which existing path does it duplicate or bypass?</li>
<li>What behavior proves it belongs here?</li>
<li>If it needs to change later, where would an engineer look first?</li></ul>

<p>If those questions have no good answer, the code is not ready just because it passes the current build.</p>

<h2 id="an-infrastructure-engineer-prunes-more-than-application-code">An infrastructure engineer prunes more than application code</h2>

<p>For an infrastructure or platform engineer, the tree extends beyond a service repository: delivery pipelines; infrastructure definitions and environment overlays; identity, network, and secret boundaries; runtime configuration; observability; and the runbooks and recovery paths that connect them.</p>

<p>The same failure mode appears at every layer: a workaround becomes a pattern, a copied pipeline step diverges, an exception disappears into an overlay, or an alert keeps firing because no one owns the decision it is meant to support. Declarative infrastructure can accumulate structural debt just as application code can.</p>

<p>That makes refactoring part of the platform engineer&#39;s job, not a side activity reserved for developers. The target is not maximal abstraction. It is a platform whose next user, deploy, incident, and security change can be made with a clear owner, a small review surface, and evidence.</p>

<p>A practical sweep across the field looks like this:</p>
<ul><li><strong>Platform and developer experience:</strong> turn tribal deployment knowledge into paved paths, understandable templates, and documented interfaces. Remove a platform abstraction when it only hides the system without removing work.</li>
<li><strong>CI/CD and GitOps:</strong> consolidate duplicated release logic, make promotion and rollback paths explicit, and keep generated or rendered configuration reviewable. A pipeline refactor is incomplete if an operator cannot tell what will reach production.</li>
<li><strong>Cloud and infrastructure as code:</strong> separate reusable modules from environment-specific intent, name ownership boundaries, and avoid copy-pasted resources that drift independently. Treat plans and policy checks as evidence, while remembering that they do not prove the live system is unchanged.</li>
<li><strong>Kubernetes and runtime operations:</strong> simplify workload, ingress, Gateway, policy, and configuration overlays while preserving the routing and rollout contract. Rendered manifests and a bounded runtime check are stronger evidence than a tidy directory tree.</li>
<li><strong>Security and identity:</strong> reduce broad permissions, clarify trust boundaries, remove obsolete access paths, and make secret rotation and break-glass procedures testable. A cleaner IAM policy is not safer unless the required workload still functions and the unwanted path is actually closed.</li>
<li><strong>Networking and reliability:</strong> replace undocumented routing exceptions with explicit contracts; preserve DNS, TLS, timeout, retry, and failover behavior as you simplify. For a customer-facing path, verification must extend beyond a syntax check.</li>
<li><strong>Observability and incident response:</strong> prune dashboards, alerts, and runbooks that no longer map to an operational decision. Keep the signals that let an engineer answer what changed, who is affected, and how to roll back.</li>
<li><strong>Cost and capacity:</strong> remove idle or duplicated resources only after measuring dependency and demand. Cost reduction is a production change, not janitorial work.</li></ul>

<p>None of these areas needs a giant “platform cleanup” programme to improve. The valuable unit is still a small, explainable structural change backed by the evidence appropriate to that layer: an IaC plan, a rendered manifest diff, a policy evaluation, a synthetic request, a rollout observation, or a recovery exercise. The appropriate checks vary by layer, but each should support a specific preservation claim.</p>

<h2 id="prune-on-the-path-of-work">Prune on the path of work</h2>

<p>A bonsai owner does not reshape the entire tree every time they notice one awkward branch. The same restraint matters in production code.</p>

<p>I prefer refactoring that is adjacent to a real change: improve a boundary before extending it, remove duplication when the second copy appears, name a concept when the team needs to discuss it, and leave unrelated cleanups alone. This keeps the diff reviewable and lets the pull request tell a coherent story.</p>

<p>It also makes verification more honest. A narrowly scoped refactor can state what it preserved, which tests exercised that claim, and what it deliberately did not touch. A broad “cleanup” commit usually cannot.</p>

<p>There are times to fund a larger structural effort. Repeated incidents, a blocked security upgrade, an untestable critical path, or a migration that keeps failing for the same architectural reason are all stronger signals than aesthetic discomfort. Even then, split the work into behavior-preserving slices where possible. The system does not care that the end state looked tidy in a diagram if the path there breaks customers.</p>

<h2 id="a-small-operating-loop">A small operating loop</h2>

<p>Before calling a change refactoring, I would write down four things:</p>
<ol><li><strong>The behavior to preserve.</strong> Describe it from the caller or user perspective, not from the internal class name.</li>
<li><strong>The structural pressure.</strong> Name the repeated cost: duplicated policy, hidden dependency, impossible test setup, ambiguous ownership, or something equally concrete.</li>
<li><strong>The evidence.</strong> Identify the test or probe that should remain green before and after the change.</li>
<li><strong>The boundary.</strong> State what is not being redesigned in this pull request.</li></ol>

<p>Then make one structural move, run the evidence, and review the diff as if you were the next person debugging it at an inconvenient hour.</p>

<p>That is less romantic than trimming a miniature tree. It is also more useful. Refactoring is routine maintenance of the structure through which future changes must travel.</p>

<h2 id="limits-of-the-metaphor">Limits of the metaphor</h2>

<p>Bonsai suggests a single careful owner and a stable aesthetic goal. Real systems have many owners, competing requirements, and behavior that must change. Refactoring will not replace migration planning, incident response, performance work, or product decisions. A green test suite also cannot prove every production property, especially across external dependencies and real traffic.</p>

<p>So use the image lightly. Keep the discipline.</p>

<p>Preserve the behavior you can state. Improve the structure that is making real work harder. Keep the change small enough to understand. And do not put the shears down until you have evidence that the tree still lives.</p>

<h2 id="sources-and-further-reading">Sources and further reading</h2>
<ul><li>Martin Fowler, <a href="https://refactoring.com/">Refactoring</a>. Fowler defines refactoring as changing internal structure to make software easier to understand and cheaper to modify without changing observable behavior.</li>
<li>Clare Sudbery, <a href="https://martinfowler.com/articles/class-too-large.html">Refactoring: This class is too large</a> (2020). A practical case study of incremental refactoring, covering tests, compiling at every step, and small commits.</li>
<li>Ulam Labs, <a href="https://ulam.io/blog/why-refactoring-code-important/">Why is refactoring your code important?</a> (2020). A useful example of the bonsai analogy, though this article intentionally avoids treating cleanliness or security as automatic outcomes of refactoring.</li>
<li><em>The Shift from Writing to Pruning Software: A Bonsai-Inspired IDE for Reshaping AI Generated Code</em>, <a href="https://arxiv.org/abs/2503.02833">arXiv:2503.02833</a> (2025). A research proposal about navigating and reshaping generated code, not a production-effectiveness study.</li></ul>


]]></content:encoded>
      <guid>https://heyimusa.blog/refactoring-is-bonsai-work</guid>
      <pubDate>Tue, 11 Aug 2026 16:02:41 +0000</pubDate>
    </item>
    <item>
      <title>A failed CI run is evidence, not a replay recipe</title>
      <link>https://heyimusa.blog/failed-ci-run-evidence-not-replay-recipe</link>
      <description>&lt;![CDATA[  A failed GitHub Actions run is not a reproduction recipe. It is evidence from a particular revision, runner image, dependency state, and execution context. The useful first move is to preserve that evidence without pretending the environment can be recreated from one log line.&#xA;&#xA;I released ci-capsule v0.1.0 after building it around that constraint. It is a small, read-only CLI for failed GitHub Actions runs: collect a local evidence bundle, sanitize bounded credential-shaped values, recover a static command only when the workflow source supports it, and say unavailable when it does not.&#xA;&#xA;!--more--&#xA;&#xA;The debugging gap is usually evidence, not another runner&#xA;&#xA;A failed workflow already has useful material scattered across GitHub: the run identity, commit SHA, job and step outcomes, the workflow source at that SHA, a failed-job log, and metadata for any artifacts that existed.&#xA;&#xA;The usual handoff loses some of that context. Someone copies a failing command into chat, another person checks out the current branch rather than the failed SHA, and a local success becomes a vague answer to a different question.&#xA;&#xA;ci-capsule keeps the boundary deliberately narrow:&#xA;&#xA;it makes read-only GitHub API requests;&#xA;it writes a local bundle rather than uploading evidence to another service;&#xA;it inventories artifact metadata but does not download artifact or cache contents;&#xA;it does not rerun workflows or execute a recovered command.&#xA;&#xA;That last point is important. A command that appeared in a workflow is not automatically safe or meaningful to replay. Matrix values, expressions, inherited environment, action setup, shell selection, working directories, and runner behavior can all change what a step actually means.&#xA;&#xA;A candidate is earned, not guessed&#xA;&#xA;The CLI only emits a replay candidate for a uniquely matched, literal one-line run: step with a sufficiently simple execution context. Complex or inherited semantics turn into evidence-only or unavailable output instead.&#xA;&#xA;For example, the usable outcome is intentionally modest:&#xA;&#xA;REPLAY_CANDIDATE go test ./...&#xA;&#xA;That says: this command was recovered from the exact static workflow source, at a cited source line. It does not say it recreates a GitHub-hosted runner.&#xA;&#xA;If the tool cannot establish the same thing, it should not fill the gap with a confident shell command. An unavailable result is often the more operationally honest output because it tells the reviewer where manual context is still required.&#xA;&#xA;The bundle is designed for a bounded review&#xA;&#xA;A capsule includes sanitized workflow and failed-log evidence, source identity, static analysis, and artifact metadata. The sanitization model is best-effort, not a promise that an arbitrary log is secret-free. The release notes and README say that directly because credential-shaped matching cannot recognize every proprietary token or sensitive string.&#xA;&#xA;The implementation also treats ambiguous parsing as a stop condition. Duplicate YAML keys, anchors, aliases, merge keys, and multiple YAML documents are rejected rather than interpreted creatively. ZIP job logs are bounded; oversized or unsafe entries fail instead of silently producing partial evidence.&#xA;&#xA;These are small constraints, but they matter when a tool is placed between an incident and a decision. A compact local bundle is useful only if its omissions are visible.&#xA;&#xA;What I verified for v0.1.0&#xA;&#xA;The release process was intentionally kept separate from the claim that the tool can analyze a failed run.&#xA;&#xA;Before publishing, I verified:&#xA;&#xA;unit tests and go vet locally;&#xA;a read-only collection against a public failed GitHub Actions run;&#xA;an independent review of the evidence, filesystem, redirect, YAML, and replay boundaries;&#xA;a hardened container acceptance path with network disabled, a read-only root filesystem, a non-root user, dropped capabilities, and no-new-privileges;&#xA;a draft release archive, checksums, CycloneDX SBOM, and embedded binary version;&#xA;the same checks again from the publicly downloaded release assets.&#xA;&#xA;The release asset is Linux amd64 only for now. Its checksums and SBOM are useful provenance inputs, not a claim that an artifact is benign or that the entire supply chain is solved.&#xA;&#xA;Try it, but keep the claim small&#xA;&#xA;The project and release assets are available at:&#xA;&#xA;source repository&#xA;v0.1.0 release&#xA;&#xA;The intended question is simple: what can I preserve and state honestly about this failed run before I ask someone to reproduce it?&#xA;&#xA;That is a better starting point than turning a log excerpt into a promise.&#xA;&#xA;Sources&#xA;&#xA;ci-capsule repository&#xA;ci-capsule v0.1.0 release&#xA;v0.1.0 release workflow&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<blockquote><p>A failed GitHub Actions run is not a reproduction recipe. It is evidence from a particular revision, runner image, dependency state, and execution context. The useful first move is to preserve that evidence without pretending the environment can be recreated from one log line.</p></blockquote>

<p>I released <a href="https://github.com/heyimusa/ci-capsule/releases/tag/v0.1.0">ci-capsule v0.1.0</a> after building it around that constraint. It is a small, read-only CLI for failed GitHub Actions runs: collect a local evidence bundle, sanitize bounded credential-shaped values, recover a static command only when the workflow source supports it, and say <em>unavailable</em> when it does not.</p>



<h2 id="the-debugging-gap-is-usually-evidence-not-another-runner">The debugging gap is usually evidence, not another runner</h2>

<p>A failed workflow already has useful material scattered across GitHub: the run identity, commit SHA, job and step outcomes, the workflow source at that SHA, a failed-job log, and metadata for any artifacts that existed.</p>

<p>The usual handoff loses some of that context. Someone copies a failing command into chat, another person checks out the current branch rather than the failed SHA, and a local success becomes a vague answer to a different question.</p>

<p><code>ci-capsule</code> keeps the boundary deliberately narrow:</p>
<ul><li>it makes read-only GitHub API requests;</li>
<li>it writes a local bundle rather than uploading evidence to another service;</li>
<li>it inventories artifact metadata but does not download artifact or cache contents;</li>
<li>it does not rerun workflows or execute a recovered command.</li></ul>

<p>That last point is important. A command that appeared in a workflow is not automatically safe or meaningful to replay. Matrix values, expressions, inherited environment, action setup, shell selection, working directories, and runner behavior can all change what a step actually means.</p>

<h2 id="a-candidate-is-earned-not-guessed">A candidate is earned, not guessed</h2>

<p>The CLI only emits a replay candidate for a uniquely matched, literal one-line <code>run:</code> step with a sufficiently simple execution context. Complex or inherited semantics turn into evidence-only or unavailable output instead.</p>

<p>For example, the usable outcome is intentionally modest:</p>

<pre><code class="language-text">REPLAY_CANDIDATE go test ./...
</code></pre>

<p>That says: this command was recovered from the exact static workflow source, at a cited source line. It does <strong>not</strong> say it recreates a GitHub-hosted runner.</p>

<p>If the tool cannot establish the same thing, it should not fill the gap with a confident shell command. An unavailable result is often the more operationally honest output because it tells the reviewer where manual context is still required.</p>

<h2 id="the-bundle-is-designed-for-a-bounded-review">The bundle is designed for a bounded review</h2>

<p>A capsule includes sanitized workflow and failed-log evidence, source identity, static analysis, and artifact metadata. The sanitization model is best-effort, not a promise that an arbitrary log is secret-free. The release notes and README say that directly because credential-shaped matching cannot recognize every proprietary token or sensitive string.</p>

<p>The implementation also treats ambiguous parsing as a stop condition. Duplicate YAML keys, anchors, aliases, merge keys, and multiple YAML documents are rejected rather than interpreted creatively. ZIP job logs are bounded; oversized or unsafe entries fail instead of silently producing partial evidence.</p>

<p>These are small constraints, but they matter when a tool is placed between an incident and a decision. A compact local bundle is useful only if its omissions are visible.</p>

<h2 id="what-i-verified-for-v0-1-0">What I verified for v0.1.0</h2>

<p>The release process was intentionally kept separate from the claim that the tool can analyze a failed run.</p>

<p>Before publishing, I verified:</p>
<ul><li>unit tests and <code>go vet</code> locally;</li>
<li>a read-only collection against a public failed GitHub Actions run;</li>
<li>an independent review of the evidence, filesystem, redirect, YAML, and replay boundaries;</li>
<li>a hardened container acceptance path with network disabled, a read-only root filesystem, a non-root user, dropped capabilities, and <code>no-new-privileges</code>;</li>
<li>a draft release archive, checksums, CycloneDX SBOM, and embedded binary version;</li>
<li>the same checks again from the publicly downloaded release assets.</li></ul>

<p>The release asset is Linux amd64 only for now. Its checksums and SBOM are useful provenance inputs, not a claim that an artifact is benign or that the entire supply chain is solved.</p>

<h2 id="try-it-but-keep-the-claim-small">Try it, but keep the claim small</h2>

<p>The project and release assets are available at:</p>
<ul><li><a href="https://github.com/heyimusa/ci-capsule">source repository</a></li>
<li><a href="https://github.com/heyimusa/ci-capsule/releases/tag/v0.1.0">v0.1.0 release</a></li></ul>

<p>The intended question is simple: <em>what can I preserve and state honestly about this failed run before I ask someone to reproduce it?</em></p>

<p>That is a better starting point than turning a log excerpt into a promise.</p>

<h2 id="sources">Sources</h2>
<ul><li><a href="https://github.com/heyimusa/ci-capsule">ci-capsule repository</a></li>
<li><a href="https://github.com/heyimusa/ci-capsule/releases/tag/v0.1.0">ci-capsule v0.1.0 release</a></li>
<li><a href="https://github.com/heyimusa/ci-capsule/actions/runs/31421579813">v0.1.0 release workflow</a></li></ul>
]]></content:encoded>
      <guid>https://heyimusa.blog/failed-ci-run-evidence-not-replay-recipe</guid>
      <pubDate>Mon, 10 Aug 2026 19:07:11 +0000</pubDate>
    </item>
    <item>
      <title>An AUR adoption is a supply-chain boundary</title>
      <link>https://heyimusa.blog/aur-adoption-is-a-supply-chain-boundary</link>
      <description>&lt;![CDATA[  A package adoption is not a harmless maintenance event when its build recipe can change what executes on an operator’s machine.&#xA;&#xA;In late July 2026, Arch Linux temporarily disabled AUR package adoption after a wave of malicious adoptions and follow-up commits. The immediate story is about Arch users, but the operating lesson is wider: an artifact name is not an identity. A package that was benign last month can acquire a new maintainer, a different source URL, and a build step that runs code before the user has a meaningful chance to inspect the installed result.&#xA;&#xA;I did not install or analyze a malicious AUR package for this note. This is a source-led review of Arch Linux’s public incident communications, not a compromise assessment, malware analysis, or a claim about systems that do not use AUR.&#xA;&#xA;!--more--&#xA;&#xA;The trust boundary moved before runtime&#xA;&#xA;A package manager makes software delivery feel routine. Pick a familiar package name, accept an update, and wait for a binary to appear on disk. That mental model misses an important boundary for community-maintained build recipes: the consequential code can run during the build.&#xA;&#xA;On July 30, the Arch Linux DevOps team announced that it had disabled AUR package adoption because of an influx of malicious adoptions and follow-up commits. That is a precise operational signal. The problem was not only an unknown package appearing in a search result. It was a maintenance transition followed by a change to the delivery path.&#xA;&#xA;A separate AUR deletion request for openconnect-sso states that a malicious binary had been committed to the repository. Earlier in July, Arch confirmed that three AUR packages installed a script from a GitHub repository identified as a remote-access trojan. The affected packages were removed after the team became aware of them.&#xA;&#xA;Arch Linux DevOps, July 30, 2026:&#xA;&#xA;&#34;Due to the current influx of malicious package adoptions and follow-up&#xA;commits made via the AUR, package adoption is currently disabled while&#xA;we are handling the situation.&#34;&#xA;&#xA;The useful distinction is between a package label and its effective delivery contract: maintainer, build recipe, sources, checksums, privilege use, and the code executed while producing or installing the artifact.&#xA;&#xA;Why a familiar name is weak evidence&#xA;&#xA;An orphaned package can have reputation through history. A user may remember it as a tool they installed before, a name mentioned in a runbook, or a dependency their team has used without incident. Adoption changes who can alter its recipe. If that recipe fetches a new object, invokes a helper, or asks for elevated privileges, the review target has changed even though the name has not.&#xA;&#xA;That does not mean every adoption is malicious, nor that AUR itself is uniquely unsafe. It means a supply-chain review cannot stop at the package name, popularity score, or an old installation decision.&#xA;&#xA;The same shape appears elsewhere:&#xA;&#xA;| Delivery surface | Name that looks stable | Boundary that can actually change |&#xA;| --- | --- | --- |&#xA;| Community package | Package name | Maintainer, PKGBUILD, source URL, checksum, build commands |&#xA;| Container deployment | Image tag | Manifest digest, base layers, entrypoint, registry publisher |&#xA;| CI workflow | Action name and version tag | Exact action commit, permissions, trigger context, downloaded scripts |&#xA;| Bootstrap script | Project URL | Redirect target, script content, signature/checksum, execution privileges |&#xA;&#xA;The table is not evidence that these systems are equivalent. It is a review prompt: identify the executable artifact and the identity mechanism that binds it to a decision.&#xA;&#xA;What Arch’s public record establishes&#xA;&#xA;The public record supports a bounded set of facts:&#xA;&#xA;Arch published a security notice for librewolf-fix-bin, firefox-patch-bin, and zen-browser-patched-bin. It says they installed a script from a GitHub repository identified as a remote-access trojan, and that they were deleted from AUR.&#xA;Arch’s DevOps team later disabled package adoption during an influx of malicious adoptions and follow-up commits.&#xA;The AUR discussion records reports of additional suspicious packages and responses by an Arch team member that reported malicious commits had been reverted or packages handled.&#xA;A deletion request for openconnect-sso explicitly describes a malicious binary committed to the package repository.&#xA;&#xA;That is enough to justify a temporary operational posture for AUR users: reduce changes, inventory exposure, and inspect the actual delivery path before rebuilding trust.&#xA;&#xA;It is not enough to infer a complete list of affected packages, a full attacker capability set, or compromise of any specific machine. Mailing-list reports are useful leads and incident records; they are not a substitute for host-specific evidence.&#xA;&#xA;A small review record is more useful than a warning banner&#xA;&#xA;“Be careful with AUR” is directionally correct but operationally thin. A better response is a small, reviewable record for each package that matters:&#xA;&#xA;package: name&#xA;reason it is needed: specific workflow&#xA;maintainer/adoption change reviewed: yes | no | unknown&#xA;PKGBUILD diff reviewed: yes | no&#xA;source provenance verified: checksum/signature | none | unknown&#xA;build needs elevated privilege: yes | no | unknown&#xA;last known-good artifact: version + checksum/digest where available&#xA;exposure window to investigate: install/upgrade dates&#xA;decision: keep paused | build in disposable environment | replace | remove&#xA;&#xA;This record does not make a package safe. It makes the decision inspectable and exposes the unanswered questions before a build runs on a credential-bearing machine.&#xA;&#xA;Practical response for AUR users&#xA;&#xA;If a system uses AUR, the immediate objective is containment of uncertainty rather than a dramatic cleanup script.&#xA;&#xA;Pause non-essential AUR installs and upgrades while the incident response is active.&#xA;Inventory installed AUR packages and recent build/upgrade history. Compare against current primary-source notices, but do not assume a published list is exhaustive.&#xA;For a suspicious package, preserve enough local evidence for investigation before deleting build caches or logs. Avoid rebuilding or re-running it merely to “check.”&#xA;If a package build or installed payload could access credentials, treat credential rotation and persistence review as incident-response decisions based on the host’s actual exposure—not a generic checklist applied blindly.&#xA;In future, build untrusted or newly changed recipes in a disposable, non-privileged environment that contains no browser sessions, SSH keys, cloud credentials, or production access.&#xA;&#xA;For teams that do not use Arch, the direct remediation is usually none. The transferable work is to make adoption-like transitions visible: an image tag moving, an action tag changing implementation, a new registry publisher, or a bootstrap URL changing content should produce a reviewable diff before it reaches a privileged runner or production host.&#xA;&#xA;Limits and non-claims&#xA;&#xA;This note does not identify a complete set of malicious AUR packages, provide malware indicators, or establish that every package named in community reports was malicious at every point in time. It also does not claim that an AUR package user has been compromised.&#xA;&#xA;I did not execute reported packages, obtain samples, or reproduce the incident. The conclusions here are limited to the public statements and the operational review model they support. Users with suspected exposure should follow current Arch guidance and perform host-specific triage with evidence appropriate to their environment.&#xA;&#xA;---&#xA;&#xA;Test notes: Source-led analysis only. I reviewed the public Arch Linux mailing-list notice on disabled AUR adoptions, the Arch security notice for three malicious browser-related AUR packages, the deletion request for openconnect-sso, and the AUR discussion thread. No malware, package, or untrusted build recipe was downloaded or executed. No Docker experiment was needed because the article makes no experimental claim.&#xA;&#xA;Sources: Arch Linux DevOps: AUR packages adoption disabled · Arch security notice: malicious AUR browser packages · AUR deletion request: openconnect-sso · AUR incident discussion and remediation updates&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<blockquote><p>A package adoption is not a harmless maintenance event when its build recipe can change what executes on an operator’s machine.</p></blockquote>

<p>In late July 2026, Arch Linux temporarily disabled AUR package adoption after a wave of malicious adoptions and follow-up commits. The immediate story is about Arch users, but the operating lesson is wider: an artifact name is not an identity. A package that was benign last month can acquire a new maintainer, a different source URL, and a build step that runs code before the user has a meaningful chance to inspect the installed result.</p>

<p>I did not install or analyze a malicious AUR package for this note. This is a source-led review of Arch Linux’s public incident communications, not a compromise assessment, malware analysis, or a claim about systems that do not use AUR.</p>



<h2 id="the-trust-boundary-moved-before-runtime">The trust boundary moved before runtime</h2>

<p>A package manager makes software delivery feel routine. Pick a familiar package name, accept an update, and wait for a binary to appear on disk. That mental model misses an important boundary for community-maintained build recipes: the consequential code can run during the build.</p>

<p>On July 30, the Arch Linux DevOps team announced that it had disabled AUR package adoption because of an influx of malicious adoptions and follow-up commits. That is a precise operational signal. The problem was not only an unknown package appearing in a search result. It was a maintenance transition followed by a change to the delivery path.</p>

<p>A separate AUR deletion request for <code>openconnect-sso</code> states that a malicious binary had been committed to the repository. Earlier in July, Arch confirmed that three AUR packages installed a script from a GitHub repository identified as a remote-access trojan. The affected packages were removed after the team became aware of them.</p>

<pre><code class="language-text">Arch Linux DevOps, July 30, 2026:

&#34;Due to the current influx of malicious package adoptions and follow-up
commits made via the AUR, package adoption is currently disabled while
we are handling the situation.&#34;
</code></pre>

<p>The useful distinction is between a package <em>label</em> and its <em>effective delivery contract</em>: maintainer, build recipe, sources, checksums, privilege use, and the code executed while producing or installing the artifact.</p>

<h2 id="why-a-familiar-name-is-weak-evidence">Why a familiar name is weak evidence</h2>

<p>An orphaned package can have reputation through history. A user may remember it as a tool they installed before, a name mentioned in a runbook, or a dependency their team has used without incident. Adoption changes who can alter its recipe. If that recipe fetches a new object, invokes a helper, or asks for elevated privileges, the review target has changed even though the name has not.</p>

<p>That does not mean every adoption is malicious, nor that AUR itself is uniquely unsafe. It means a supply-chain review cannot stop at the package name, popularity score, or an old installation decision.</p>

<p>The same shape appears elsewhere:</p>

<table>
<thead>
<tr>
<th>Delivery surface</th>
<th>Name that looks stable</th>
<th>Boundary that can actually change</th>
</tr>
</thead>

<tbody>
<tr>
<td>Community package</td>
<td>Package name</td>
<td>Maintainer, <code>PKGBUILD</code>, source URL, checksum, build commands</td>
</tr>

<tr>
<td>Container deployment</td>
<td>Image tag</td>
<td>Manifest digest, base layers, entrypoint, registry publisher</td>
</tr>

<tr>
<td>CI workflow</td>
<td>Action name and version tag</td>
<td>Exact action commit, permissions, trigger context, downloaded scripts</td>
</tr>

<tr>
<td>Bootstrap script</td>
<td>Project URL</td>
<td>Redirect target, script content, signature/checksum, execution privileges</td>
</tr>
</tbody>
</table>

<p>The table is not evidence that these systems are equivalent. It is a review prompt: identify the executable artifact and the identity mechanism that binds it to a decision.</p>

<h2 id="what-arch-s-public-record-establishes">What Arch’s public record establishes</h2>

<p>The public record supports a bounded set of facts:</p>
<ol><li>Arch published a security notice for <code>librewolf-fix-bin</code>, <code>firefox-patch-bin</code>, and <code>zen-browser-patched-bin</code>. It says they installed a script from a GitHub repository identified as a remote-access trojan, and that they were deleted from AUR.</li>
<li>Arch’s DevOps team later disabled package adoption during an influx of malicious adoptions and follow-up commits.</li>
<li>The AUR discussion records reports of additional suspicious packages and responses by an Arch team member that reported malicious commits had been reverted or packages handled.</li>
<li>A deletion request for <code>openconnect-sso</code> explicitly describes a malicious binary committed to the package repository.</li></ol>

<p>That is enough to justify a temporary operational posture for AUR users: reduce changes, inventory exposure, and inspect the actual delivery path before rebuilding trust.</p>

<p>It is not enough to infer a complete list of affected packages, a full attacker capability set, or compromise of any specific machine. Mailing-list reports are useful leads and incident records; they are not a substitute for host-specific evidence.</p>

<h2 id="a-small-review-record-is-more-useful-than-a-warning-banner">A small review record is more useful than a warning banner</h2>

<p>“Be careful with AUR” is directionally correct but operationally thin. A better response is a small, reviewable record for each package that matters:</p>

<pre><code class="language-text">package: &lt;name&gt;
reason it is needed: &lt;specific workflow&gt;
maintainer/adoption change reviewed: yes | no | unknown
PKGBUILD diff reviewed: yes | no
source provenance verified: checksum/signature | none | unknown
build needs elevated privilege: yes | no | unknown
last known-good artifact: &lt;version + checksum/digest where available&gt;
exposure window to investigate: &lt;install/upgrade dates&gt;
decision: keep paused | build in disposable environment | replace | remove
</code></pre>

<p>This record does not make a package safe. It makes the decision inspectable and exposes the unanswered questions before a build runs on a credential-bearing machine.</p>

<h2 id="practical-response-for-aur-users">Practical response for AUR users</h2>

<p>If a system uses AUR, the immediate objective is containment of uncertainty rather than a dramatic cleanup script.</p>
<ul><li>Pause non-essential AUR installs and upgrades while the incident response is active.</li>
<li>Inventory installed AUR packages and recent build/upgrade history. Compare against current primary-source notices, but do not assume a published list is exhaustive.</li>
<li>For a suspicious package, preserve enough local evidence for investigation before deleting build caches or logs. Avoid rebuilding or re-running it merely to “check.”</li>
<li>If a package build or installed payload could access credentials, treat credential rotation and persistence review as incident-response decisions based on the host’s actual exposure—not a generic checklist applied blindly.</li>
<li>In future, build untrusted or newly changed recipes in a disposable, non-privileged environment that contains no browser sessions, SSH keys, cloud credentials, or production access.</li></ul>

<p>For teams that do not use Arch, the direct remediation is usually none. The transferable work is to make adoption-like transitions visible: an image tag moving, an action tag changing implementation, a new registry publisher, or a bootstrap URL changing content should produce a reviewable diff before it reaches a privileged runner or production host.</p>

<h2 id="limits-and-non-claims">Limits and non-claims</h2>

<p>This note does not identify a complete set of malicious AUR packages, provide malware indicators, or establish that every package named in community reports was malicious at every point in time. It also does not claim that an AUR package user has been compromised.</p>

<p>I did not execute reported packages, obtain samples, or reproduce the incident. The conclusions here are limited to the public statements and the operational review model they support. Users with suspected exposure should follow current Arch guidance and perform host-specific triage with evidence appropriate to their environment.</p>

<hr>

<p><strong>Test notes:</strong> Source-led analysis only. I reviewed the public Arch Linux mailing-list notice on disabled AUR adoptions, the Arch security notice for three malicious browser-related AUR packages, the deletion request for <code>openconnect-sso</code>, and the AUR discussion thread. No malware, package, or untrusted build recipe was downloaded or executed. No Docker experiment was needed because the article makes no experimental claim.</p>

<p><strong>Sources:</strong> <a href="https://lists.archlinux.org/archives/list/aur-general@lists.archlinux.org/message/DRDEU3JUSC72CB265XHXPFA3DFSLXPBP/">Arch Linux DevOps: AUR packages adoption disabled</a> · <a href="https://lists.archlinux.org/archives/list/aur-general@lists.archlinux.org/thread/7EZTJXLIAQLARQNTMEW2HBWZYE626IFJ/">Arch security notice: malicious AUR browser packages</a> · <a href="https://lists.archlinux.org/archives/list/aur-requests@lists.archlinux.org/message/CW332U3IO3XR7RBN67NLSAZDIB6XVQF3/">AUR deletion request: <code>openconnect-sso</code></a> · <a href="https://lists.archlinux.org/archives/list/aur-general@lists.archlinux.org/thread/P4WIRHTFNH2YZWQHGBAKQWX5YOAFIDLY/">AUR incident discussion and remediation updates</a></p>
]]></content:encoded>
      <guid>https://heyimusa.blog/aur-adoption-is-a-supply-chain-boundary</guid>
      <pubDate>Mon, 03 Aug 2026 03:53:51 +0000</pubDate>
    </item>
  </channel>
</rss>