<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://abhi4u1947.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://abhi4u1947.github.io/" rel="alternate" type="text/html" /><updated>2026-07-16T16:33:44+00:00</updated><id>https://abhi4u1947.github.io/feed.xml</id><title type="html">Abhishek Dadhich</title><subtitle>Architect and technology leader with 19+ years building large-scale, business-critical systems. Writing on platform engineering, DevSecOps, IAM, supply chain security, observability, and agentic AI from a practitioner&apos;s perspective.
</subtitle><author><name>Abhishek Dadhich</name></author><entry><title type="html">Parallel isn’t coordinated: the real bottleneck in agentic coding</title><link href="https://abhi4u1947.github.io/2026/07/parallel-isnt-coordinated-agentic-coding/" rel="alternate" type="text/html" title="Parallel isn’t coordinated: the real bottleneck in agentic coding" /><published>2026-07-16T00:00:00+00:00</published><updated>2026-07-16T00:00:00+00:00</updated><id>https://abhi4u1947.github.io/2026/07/parallel-isnt-coordinated-agentic-coding</id><content type="html" xml:base="https://abhi4u1947.github.io/2026/07/parallel-isnt-coordinated-agentic-coding/"><![CDATA[<blockquote>
  <p>“Analysis is running in parallel (4 agents)…”</p>
</blockquote>

<p>That line from an AI coding agent made me stop scrolling. Four agents, working simultaneously. Impressive.</p>

<p>Then, a few lines later, the same agent quietly admitted the catch:</p>

<blockquote>
  <p>“The four analysis agents are already dispatched with fixed prompts, so I can’t inject that into them mid-run.”</p>
</blockquote>

<p>Parallel? Yes. Coordinated? Not even close.</p>

<p>Think about how a real engineering team works. A human notices new information. A human interrupts. A human reprioritizes. A human shares fresh context on the fly. Today’s coding agents mostly can’t. Once work starts, the plan is frozen. It feels less like a collaborative engineering team and more like submitting four batch jobs and waiting for them to finish.</p>

<p>So I went looking for evidence that this is a systemic limitation, not a one-off quirk.</p>

<p>Turns out, Anthropic’s own engineering team has said the quiet part out loud.</p>

<h2 id="exhibit-a-anthropics-multi-agent-research-system">Exhibit A: Anthropic’s multi-agent research system</h2>

<p>In their <a href="https://www.anthropic.com/engineering/multi-agent-research-system">write-up on building a multi-agent research system</a>, they describe running subagents synchronously: the lead agent waits for each batch to finish before moving on. By their own account, the lead agent can’t steer subagents mid-run, subagents can’t coordinate with each other, and the whole system can stall waiting on a single slow worker.</p>

<p>Their admission is refreshingly direct: today’s LLM agents “are not yet great at coordinating and delegating to other agents in real time.”</p>

<h2 id="exhibit-b-16-claudes-vs-the-linux-kernel">Exhibit B: 16 Claudes vs. the Linux kernel</h2>

<p>In February 2026, an Anthropic researcher <a href="https://www.anthropic.com/engineering/building-c-compiler">put 16 Claude agents on a shared codebase to build a C compiler from scratch</a>.</p>

<p>When the work was naturally parallel - hundreds of independent failing tests - 16 agents flew.</p>

<p>But the moment they hit one big shared task, compiling the Linux kernel, it fell apart. Every agent hit the same bug, fixed it, and overwrote each other’s changes. Sixteen agents didn’t help, because all sixteen were stuck on the same problem.</p>

<p>And the coordination mechanism? A bare-bones git-based file-locking scheme. No orchestrator. No live communication between agents.</p>

<p>That’s not a coordination strategy. That’s a mutex with extra steps.</p>

<h2 id="the-real-leap-isnt-more-agents">The real leap isn’t more agents</h2>

<p>Here’s my thesis: the next big jump in agentic software engineering won’t come from adding agents. It will come from making agents:</p>

<ul>
  <li><strong>Interruptible</strong> - able to absorb new information mid-run</li>
  <li><strong>Context-synchronized</strong> - continuously, not at batch boundaries</li>
  <li><strong>Renegotiable</strong> - able to re-plan and re-divide work when the situation changes</li>
  <li><strong>Mutually aware</strong> - knowing what every other agent is discovering, in real time</li>
</ul>

<p>The industry is already leaning this way. Anthropic ships a production research system on an orchestrator-worker pattern and is openly exploring the harder version: asynchronous execution, where agents work concurrently and spawn subagents on the fly. They’re candid that this unlocks parallelism but introduces thorny problems around result coordination, state consistency, and error propagation. Other labs are pushing on subagents and parallel execution too.</p>

<p>But dynamic coordination - running agents that update each other’s plans without restarting - is still very much an open problem.</p>

<p>Sound familiar? It should. This is the distributed-systems problem of the AI agent era. The bottleneck is shifting from raw model quality to coordination, ownership, synchronization, and orchestration.</p>

<h2 id="the-strongest-evidence-theres-no-free-pattern">The strongest evidence: there’s no free pattern</h2>

<p>In April 2026, Anthropic published a <a href="https://claude.com/blog/multi-agent-coordination-patterns">breakdown of five multi-agent coordination patterns</a>. What stands out is that every single one trades one failure mode for another - exactly the “pick your poison” reality anyone who has built distributed systems will recognize:</p>

<ol>
  <li><strong>Generator-verifier.</strong> One agent produces, another checks against explicit criteria. Solves quality control - but the verifier is only as good as its rubric. A vague “is this good?” check rubber-stamps everything, and the loop can oscillate forever without converging.</li>
  <li><strong>Orchestrator-subagent.</strong> A lead agent plans, delegates, and synthesizes - this is how Claude Code works. Solves clean task decomposition - but the orchestrator becomes an information bottleneck. When one subagent discovers something another needs, it travels back through the lead, and details get summarized away.</li>
  <li><strong>Agent teams.</strong> Persistent workers claim tasks from a shared queue and run autonomously for long stretches. Solves parallel, long-running work - but teammates can’t easily share intermediate findings and can collide on shared files. Exactly the Linux-kernel failure above.</li>
  <li><strong>Message bus.</strong> Agents publish and subscribe to events, so you can add agents without rewiring. Solves scaling an evolving ecosystem - but event flows are hard to trace, and a misrouted event fails silently: handling nothing while never crashing.</li>
  <li><strong>Shared state.</strong> Agents read and write a common store with no central coordinator. Solves real-time collaboration and removes the single point of failure - but agents can duplicate work or fall into reactive loops, burning tokens on work that never converges.</li>
</ol>

<p>Anthropic’s own advice: start with the simplest pattern that could work, watch where it breaks, and evolve. In production, teams usually end up combining patterns - an orchestrator for the workflow, shared state for the collaboration-heavy parts.</p>

<p>That’s not a solved problem. That’s engineering around trade-offs.</p>

<p>Which is exactly why I keep coming back to the distributed-systems analogy: we’re not chasing the one right pattern, we’re learning to compose imperfect ones. CAP theorem energy, but for agents.</p>

<h2 id="over-to-you">Over to you</h2>

<p>Two genuine questions I’d love a second opinion on:</p>

<ol>
  <li>Have you come across research or products tackling live coordination between long-running coding agents - not just parallel execution?</li>
  <li>If you’ve built multi-agent systems in production, which pattern (or combination) did you land on, and where did it break first?</li>
</ol>

<p>If you’ve got war stories or links on this, <a href="/about/">reach out</a>.</p>]]></content><author><name>Abhishek Dadhich</name></author><category term="agentic-ai" /><category term="multi-agent-systems" /><category term="ai-engineering" /><category term="distributed-systems" /><category term="platform-engineering" /><summary type="html"><![CDATA[Four Claude agents parallelized a C-compiler build beautifully, until they hit one shared task and started overwriting each other's fixes. Coordination, not agent count, is the next bottleneck in agentic coding.]]></summary></entry><entry><title type="html">Your AI agent doesn’t have an ID. It borrows yours.</title><link href="https://abhi4u1947.github.io/2026/07/aauth-agent-identity-guide/" rel="alternate" type="text/html" title="Your AI agent doesn’t have an ID. It borrows yours." /><published>2026-07-05T00:00:00+00:00</published><updated>2026-07-05T00:00:00+00:00</updated><id>https://abhi4u1947.github.io/2026/07/aauth-agent-identity-guide</id><content type="html" xml:base="https://abhi4u1947.github.io/2026/07/aauth-agent-identity-guide/"><![CDATA[<p>Here’s a small, uncomfortable fact about the AI agents everyone is rushing to deploy.</p>

<p>When your agent books a flight, files a ticket, queries a database, or calls another service on your behalf, it usually authenticates by <strong>borrowing a secret that belongs to you</strong> - an API key, an OAuth token, a session cookie. It has no identity of its own. To the service on the other end, there is no “the agent.” There is only <em>you</em>, or the application that wrapped you, holding a key that could just as easily have been copied by anyone.</p>

<p>For a chatbot that answers questions, that’s fine. For an autonomous system that plans, chains together services it has never seen before, spends money, and keeps working while you’re asleep, it’s a genuine problem - one that touches security, accountability, and governance all at once.</p>

<p>A new Internet-Draft called <strong>AAuth</strong> (Agent Authorization) is one of the more serious attempts to fix it. It’s worth understanding not because it’s guaranteed to become the standard - it isn’t, and I’ll be honest about that throughout - but because it names the problem unusually clearly and proposes a concrete, buildable answer.</p>

<p>This post walks through what AAuth is, the specific cracks it’s trying to fill, and how it works, in language aimed at anyone technical-adjacent. Where I’m summarizing the specification I’ll say so; where I’m interpreting or editorializing, I’ll flag it.</p>

<blockquote>
  <p><strong>A note on sources and freshness.</strong> Everything here is grounded in the AAuth Protocol Internet-Draft (<code class="language-plaintext highlighter-rouge">draft-hardt-oauth-aauth-protocol</code>) and the author’s own explainer at <a href="https://www.aauth.dev/">aauth.dev</a>. I’m writing against draft revision <strong>-09</strong>, which I read directly, cross-checked against the public IETF datatracker in mid-2026. This is a <strong>fast-moving individual draft</strong> - details, endpoint names, and even the exact revision number will have changed by the time you read this. Treat specifics as “true as of -09, verify against the live spec,” not gospel.</p>
</blockquote>

<hr />

<h2 id="tldr">TL;DR</h2>

<ul>
  <li><strong>The problem:</strong> AI agents have no independent identity. They authenticate by holding copyable secrets (API keys, bearer tokens) that belong to a user or app. Anything copyable eventually leaks, and a stolen secret works for whoever holds it.</li>
  <li><strong>The core idea:</strong> Give every agent its own <strong>cryptographic identity</strong> - an identifier like <code class="language-plaintext highlighter-rouge">aauth:local@domain</code> bound to a private signing key. The agent <strong>signs every request</strong> instead of carrying a password. Steal the token, and it’s useless without the key.</li>
  <li><strong>Who’s behind it:</strong> Dick Hardt, the editor of OAuth 2.0 itself, who concluded OAuth doesn’t fit AI agents after building authorization for an MCP server.</li>
  <li><strong>The shape:</strong> Four “access modes” that climb from <em>replace your API keys</em> up to <em>full cross-company policy federation</em>. You adopt only as far as you need. Plus a genuinely new idea - <strong>missions</strong> - that lets you govern an agent by its declared intent, in plain language.</li>
  <li><strong>The honest status:</strong> An early, rapidly iterating individual IETF draft - not an adopted standard, and not the only proposal in this space. Whether it wins is unknown. The problems it names are real regardless.</li>
</ul>

<hr />

<h2 id="part-1---the-quiet-problem-agents-borrow-your-keys">Part 1 - The quiet problem: agents borrow your keys</h2>

<p>Software used to be <em>written</em>. A developer decided, at build time, exactly which services an application would call and what permissions it needed. You registered the app once at each service’s developer portal, got a client ID and a secret, wired them in, and shipped. The list of integrations was fixed and known in advance.</p>

<p>AI agents don’t work like that. They <strong>discover</strong> what they need at runtime. They assemble a chain of tool calls one step at a time, deciding the next move as a task unfolds. They run long tasks that cross between different companies and clouds. And they routinely need a decision - a “yes, go ahead” - in the <em>middle</em> of a task, long after the human who started them has walked away.</p>

<p>The authentication machinery underneath was built for the old world. When you drop an autonomous agent onto it, the seams start to show. Let’s look at exactly where.</p>

<hr />

<h2 id="part-2---why-the-tools-we-already-have-dont-fit">Part 2 - Why the tools we already have don’t fit</h2>

<p>AAuth’s framing of the problem is sharp enough that it’s worth going through the specific cracks. None of these are exotic - they’re all things any engineer who has wired up an agent has bumped into.</p>

<p><strong>1. Identities don’t travel between services.</strong> In OAuth and OpenID Connect - the standards behind almost every “Sign in with…” button - a client has no identity of its own. A client ID issued by Google is meaningless at GitHub. The client only “exists” in the context of each server it has pre-registered with. That made sense when a human developer could visit each portal once. It falls apart when an agent needs to touch a dozen services it has never registered with.</p>

<p><strong>2. Copied secrets leak. Always.</strong> API keys are the purest version of the old model: a shared secret that a service issues and you copy into wherever your code runs, then present as a bearer credential. The draft puts the core truth bluntly - any secret that has to be copied to where a workload runs will, eventually, get copied somewhere it shouldn’t. And a bearer credential works for <strong>whoever holds it</strong>. Steal it, and you <em>are</em> the client.</p>

<p><strong>3. Consent arrives late - and “waiting” isn’t a supported state.</strong> An agent frequently needs a human’s approval partway through a task (“this purchase is over your limit - okay?”). But approvals happen on human timelines, which might be minutes or hours. Today’s protocols tend to treat “pending, waiting for a human” as an <em>error</em> rather than a normal, first-class part of the flow.</p>

<p><strong>4. Scopes describe access, not intent.</strong> A permission - a “scope” like <code class="language-plaintext highlighter-rouge">mail.read</code> - is <em>standing</em> access. It looks exactly the same whether the agent is summarizing your inbox or quietly scraping it. For an autonomous system making its own choices, <strong>why</strong> it’s acting right now matters as much as <strong>what</strong> it’s allowed to touch, and scopes simply can’t express that.</p>

<p><strong>5. Tool chains assemble live.</strong> The sequence of calls an agent makes isn’t written into its code. It picks the next tool as it goes, and the chain shifts with every task. Authorization models that assume a fixed, declared set of integrations have nothing to grab onto.</p>

<p><strong>6. Work crosses trust boundaries.</strong> Enterprise “workload identity” systems like SPIFFE and WIMSE are genuinely good - a workload can prove who it is without shared secrets. But they operate <em>within a single organization’s trust domain</em>. They don’t help an agent that needs to reach across organizational borders, or a developer’s tool running outside any enterprise platform at all.</p>

<p>Put those six together and a pattern emerges: <strong>the thing that’s missing is an identity for the client itself</strong> - one that it carries with it, that any party can verify, and that survives crossing between services and companies.</p>

<hr />

<h2 id="part-3---the-one-idea-worth-remembering">Part 3 - The one idea worth remembering</h2>

<p>If you take away a single sentence from this whole post, make it this one:</p>

<blockquote>
  <p><strong>Give every agent its own cryptographic identity, and have it sign every request instead of carrying a password.</strong></p>
</blockquote>

<p>That’s AAuth’s foundation. Concretely:</p>

<ul>
  <li>Each agent gets an identifier of the form <strong><code class="language-plaintext highlighter-rouge">aauth:local@domain</code></strong> - think of it like an email address for the agent, where <code class="language-plaintext highlighter-rouge">domain</code> belongs to whoever issued it.</li>
  <li>That identifier is bound to a <strong>private signing key</strong> that lives with the agent and never leaves it.</li>
  <li>The agent’s public key is published at a well-known URL, so <em>any</em> party can look it up and verify a signature - <strong>no pre-registration, no shared secret, no dependency on a particular central server.</strong></li>
  <li>On every request, the agent produces a cryptographic <strong>signature</strong> over that request. The receiving service checks it against the published public key.</li>
</ul>

<p>The security payoff is the part worth sitting with. A bearer token (an API key, a cookie) is dangerous because <em>possession equals authority</em> - whoever holds the bytes wins. A <strong>signed</strong> request is different: even if an attacker intercepts the token, it’s worthless without the private key, which never travelled. Cryptographers call this <strong>proof-of-possession</strong>. In plain terms: the agent doesn’t just <em>claim</em> to be who it says - it <em>proves</em> it, on every single call.</p>

<p>That’s it. Everything else in AAuth - modes, tokens, missions, federation - is built incrementally on top of that one move.</p>

<hr />

<h2 id="part-4---whos-proposing-this-and-why-that-matters">Part 4 - Who’s proposing this, and why that matters</h2>

<p>It would be easy to dismiss “yet another auth protocol.” What makes AAuth worth a second look is who wrote it and why.</p>

<p>AAuth comes from <strong>Dick Hardt</strong> - and here’s the detail that should raise an eyebrow: <strong>Hardt was the editor of OAuth 2.0 itself</strong> (the person listed as “Ed.” on RFC 6749, the framework the modern web runs its logins on). He’s also a co-author of the in-progress OAuth 2.1.</p>

<p>By his own account on aauth.dev, the origin story is this: after implementing authorization for an MCP server - the “Model Context Protocol” used to connect AI models to tools - he concluded that OAuth simply isn’t a good fit for that world, and started working with others in the identity community who’d hit the same walls. AAuth is what came out of it.</p>

<p>His one-line thesis is the cleanest summary of the whole effort:</p>

<blockquote>
  <p><em>“The web gave servers identity. It’s time clients got the same.”</em></p>
  <ul>
    <li>Dick Hardt, aauth.dev</li>
  </ul>
</blockquote>

<p>You don’t have to agree that AAuth is <em>the</em> answer to take the diagnosis seriously. When the person who edited OAuth 2.0 says OAuth 2.0 isn’t enough for agents, that’s a signal worth weighing - though, in fairness, it’s also exactly the kind of claim an author is motivated to make about their own new proposal. Judge the design on its merits.</p>

<hr />

<h2 id="part-5---the-four-access-modes-a-ladder-not-a-leap">Part 5 - The four access modes: a ladder, not a leap</h2>

<p>The single smartest design decision in AAuth, in my reading, is that it’s <strong>incremental</strong>. You don’t have to swallow the whole thing. It defines four “resource access modes” that climb from trivially simple to fully featured, and - crucially - <em>every party can adopt independently, with no coordinated migration.</em> A service that doesn’t understand AAuth just ignores the signatures and keeps working.</p>

<p>Here are the four rungs, in plain language:</p>

<p><strong>Rung 1 - Identity-based access.</strong> <em>Replaces API keys.</em> The agent signs its request; the service verifies who it is and applies its own access rules. There’s no authorization dance and no extra tokens - just cryptographic identity in place of a shared secret. This is the “drop-in for API keys” mode, and it involves only the agent and the resource.</p>

<p><strong>Rung 2 - Resource-managed access (two-party).</strong> <em>Replaces OAuth.</em> The service keeps whatever authorization it already has - consent screens, existing OAuth tokens, sessions - and simply <em>wraps</em> it. It hands back its own token, but bound to the agent’s signature so it can’t be stolen and replayed as a standalone bearer token. Still just the agent and the resource; still no new central party.</p>

<p><strong>Rung 3 - Person-server access (three-party).</strong> Now a new character enters: a <strong>person server</strong> that speaks for <em>you</em>. It’s a server <strong>you choose</strong> (not one imposed by anyone else) that can prove who you are to a resource, handle consent on your behalf, and keep an audit trail - and it works across many services without each of them having to set you up in advance.</p>

<p><strong>Rung 4 - Federated access (four-party).</strong> The most capable mode. The resource has its own policy engine (an “access server”), and your person server negotiates with it directly. This is what enables agents to cross organizational borders under real policy control.</p>

<p>The important property: <strong>each step is optional and additive.</strong> A service that adopts only Rung 1 still gets real value (leak-proof identity instead of API keys). It never has to climb higher than its needs require. That’s a very different adoption story from “rip everything out and migrate.”</p>

<hr />

<h2 id="part-6---the-cast-of-characters-and-the-three-tokens">Part 6 - The cast of characters (and the three tokens)</h2>

<p>To read anything else about AAuth, it helps to know the players. The specification is careful about roles, and once you have them, the flows make sense.</p>

<ul>
  <li><strong>Person</strong> - a human <em>or an organization</em>: the legal party on whose behalf the agent acts, and who is ultimately accountable for what it does.</li>
  <li><strong>Agent</strong> - the HTTP client acting for the person, identified by that <code class="language-plaintext highlighter-rouge">aauth:local@domain</code> URI.</li>
  <li><strong>Agent Provider (AP)</strong> - the server that vouches for the agent’s identity by issuing it an “agent token.” Think of it as the passport office for agents.</li>
  <li><strong>Resource</strong> - the service being accessed (an API, a data store). It may enforce its own policy or delegate that to an access server.</li>
  <li><strong>Person Server (PS)</strong> - the server that represents <em>you</em> to everything else: manages missions, handles consent, asserts your identity, brokers authorization. You pick it, and you can move to a different one whenever you like.</li>
  <li><strong>Access Server (AS)</strong> - a policy engine that evaluates requests and issues access on a resource’s behalf, in the four-party mode.</li>
</ul>

<p>And three kinds of <strong>token</strong>, each a signed JWT tied to whoever issued it:</p>

<ul>
  <li><strong>Agent token</strong> - establishes <em>who the agent is</em> (issued by the agent provider).</li>
  <li><strong>Resource token</strong> - describes <em>what access is being requested</em> (issued by the resource).</li>
  <li><strong>Auth token</strong> - actually <em>grants</em> access, carrying identity claims and/or authorized scopes (issued by a person server or access server).</li>
</ul>

<p>You don’t need to memorize this. The mental model is enough: an agent has a verifiable identity, it asks a resource what it needs, and a party you trust turns that into a grant - all signed, all bound to keys, no copyable bearer secrets in the mix.</p>

<p>One privacy detail worth calling out, because it’s a nice touch: the person server can hand each resource a <strong>different, pairwise identifier</strong> for the same user. So two services you use can’t quietly compare notes and realize you’re the same person. Identity without automatic cross-site correlation.</p>

<hr />

<h2 id="part-7---missions-the-genuinely-new-idea">Part 7 - Missions: the genuinely new idea</h2>

<p>If Rungs 1-4 are AAuth catching authentication up to the agent era, <strong>missions</strong> are where it tries to get <em>ahead</em> of it. This is the part I find most interesting, and it’s the piece that most directly answers “scopes describe access, not intent.”</p>

<p>A <strong>mission</strong> is a short, human-readable description - written in plain language - of what the agent is actually trying to accomplish. The agent proposes it; you (through your person server) review, maybe ask a clarifying question or two, and approve it <strong>once</strong>. From that point on, the approved mission is fixed - it can’t be quietly edited out from under you, because it’s pinned by a cryptographic hash of its contents.</p>

<p>Here’s why that’s powerful. Instead of every request being judged only against a static permission list, each action can be weighed against the mission:</p>

<blockquote>
  <p><em>“Does this next step actually fit the mission I agreed to?”</em></p>
</blockquote>

<p>That question can be answered by a human <em>or</em> by an AI acting as the decision-maker - and it can catch things a fixed rulebook never could. <code class="language-plaintext highlighter-rouge">mail.read</code> can’t tell “summarize my inbox” apart from “scrape my inbox,” but a mission can: one is on-mission, the other isn’t.</p>

<p>Around missions, the person server offers a few concrete governance tools:</p>

<ul>
  <li>A <strong>mission log</strong> - an ordered record of every interaction in the mission, so the whole thing can be reconstructed later for audit or investigation.</li>
  <li>A <strong>permission</strong> step for sensitive actions the agent wants to take that aren’t governed by any remote resource (tool calls, file writes, sending a message).</li>
  <li>An <strong>interaction</strong> channel to reach you mid-task - relay a question, forward a payment approval, or propose that the mission is complete.</li>
  <li>A <strong>clarification chat</strong> - during consent, <em>you</em> can ask the agent questions, and it can explain itself or adjust its request before you approve.</li>
</ul>

<p>My editorial read: missions are the most ambitious and least battle-tested part of the design. They’re also the most honest about what agent governance actually requires - because a lot of the decisions we want to make about autonomous agents genuinely <em>can’t</em> be reduced to machine-evaluable rules written in advance. Whether “a plain-language mission plus a hash” is a strong enough container for that is exactly the kind of thing that needs real-world implementation to settle. (It’s telling that “missions” were added as a first-class object partway through the draft’s life, and remain actively debated in the community - a sign this is live design, not settled fact.)</p>

<hr />

<h2 id="part-8---what-actually-makes-it-safer-and-where-its-honest-about-limits">Part 8 - What actually makes it safer (and where it’s honest about limits)</h2>

<p>A protocol’s security story is only as good as its willingness to name its own weak points. AAuth’s does reasonably well on that front. The genuine improvements:</p>

<ul>
  <li><strong>No bearer tokens anywhere.</strong> Every credential is bound to a signing key and is useless without it. This is the single biggest change from the status quo.</li>
  <li><strong>Proof-of-possession on every request.</strong> A stolen token can’t be replayed by someone who doesn’t hold the key.</li>
  <li><strong>Confused-deputy protection.</strong> Resource tokens are bound to a specific resource’s identity, which blocks a class of attacks where one service is tricked into acting for another.</li>
  <li><strong>Layered revocation.</strong> Multiple parties - the agent provider, your person server, the access server - can each independently deny renewal or revoke access. Short token lifetimes shrink the window between “revoke” and “expired.”</li>
  <li><strong>Accountability by construction.</strong> The spec insists each agent is bound to exactly <em>one</em> accountable person. Every action an agent takes traces back to a single party - which is precisely what enterprise audit and incident response need.</li>
</ul>

<p>And the limits it’s upfront about - which I appreciate, because a spec that only lists its strengths is a spec to be suspicious of:</p>

<ul>
  <li><strong>The person server is a high-value target.</strong> It sees <em>every</em> authorization an agent makes. Compromise it and you’ve compromised a lot. The design leans on “you chose it and can leave it” plus the option to delegate authentication and policy elsewhere, but centralization risk is real and acknowledged.</li>
  <li><strong>Signatures prove authenticity <em>at request time</em>, not forever.</strong> Agent keys are short-lived and rotated, so once a key is retired you can’t re-verify an old signature by re-fetching the public key later. If you need durable, long-term non-repudiation (for compliance, say), you have to capture the evidence <em>at the moment of verification</em> - the draft spells out how, and is clear this trades some privacy for that durability.</li>
  <li><strong>All input is untrusted.</strong> Justifications, mission descriptions, clarification replies - all of it can come from an adversary and must be sanitized before it’s shown to a human. (This matters especially because so much of it is free-form Markdown that gets rendered.)</li>
</ul>

<p>That last cluster is the mark of a design that’s been thought about seriously rather than sketched.</p>

<hr />

<h2 id="part-9---how-it-fits-with-oauth-it-doesnt-kill-it">Part 9 - How it fits with OAuth (it doesn’t kill it)</h2>

<p>A fair worry when a new protocol appears is: does adopting it mean tearing out what I have? For AAuth, the answer is no, by design.</p>

<ul>
  <li>It’s built to <strong>coexist</strong> with OAuth 2.0 and OpenID Connect, not replace them.</li>
  <li>It <strong>reuses their vocabulary</strong> - the same scope values and identity claims - specifically to lower the cost of adoption for services that already speak OIDC.</li>
  <li>It follows familiar patterns: publishing metadata at well-known URLs and signing keys via JWKS endpoints, the same way OAuth and OIDC discovery already work.</li>
  <li>The whole thing rests on existing web standards, chiefly <strong>HTTP Message Signatures (RFC 9421)</strong> for the signing.</li>
</ul>

<p>And the adoption model is deliberately un-dramatic: a service that recognizes AAuth signatures verifies them; one that doesn’t <strong>ignores them and keeps working</strong>. There’s no flag-day, no central coordinator, no requirement that everyone move at once. Each party climbs the ladder on its own schedule.</p>

<hr />

<h2 id="part-10---where-this-actually-stands-the-honest-part">Part 10 - Where this actually stands (the honest part)</h2>

<p>This is the section I’d want a reader to remember if they remember only one, because the temptation with a clean new protocol is to assume it’s further along than it is.</p>

<p><strong>It is an individual IETF Internet-Draft - not an adopted standard.</strong> Anyone can publish an Internet-Draft; doing so confers no official standing. As of the revision I read, it targets the security area but has no working group formally behind it yet. There <em>is</em> real momentum around it - a family of related drafts (bootstrapping guidance, an events spec, an exploratory “rich resource requests” vocabulary), open-source SDKs in a few languages, a demo you can run against a live test resource, community Slack channels, and in-person meetups. But momentum is not ratification.</p>

<p><strong>It is not the only proposal in this space.</strong> Other efforts are floating <em>extensions to OAuth 2.0</em> to handle AI-agent authentication and authorization rather than a new protocol. The industry has very much <strong>not</strong> converged on an approach. Reasonable, expert people disagree about whether agents need a whole new protocol or whether OAuth can be stretched to fit.</p>

<p><strong>Whether AAuth becomes the standard is genuinely unknown</strong> - and I’d be suspicious of anyone who tells you they know. New protocols face a brutal chicken-and-egg problem: resources won’t implement it until agents use it, and agents won’t use it until resources implement it. AAuth’s incremental, “ignore-it-if-you-don’t-support-it” design is a smart hedge against exactly that, but plenty of well-designed protocols never reach escape velocity.</p>

<p>None of that makes it a waste of time to understand. The <strong>problems</strong> AAuth names - no client identity, leaky secrets, mid-task consent, intent versus access, cross-domain trust - are real, and they’re going to have to be solved by <em>something</em>, whether that something is AAuth, an OAuth extension, or an idea not yet written down. Understanding the clearest articulation of the problem is useful no matter which answer wins.</p>

<hr />

<h2 id="part-11---how-to-look-closer">Part 11 - How to look closer</h2>

<p>If this made you curious, a few concrete next steps (all first-party sources):</p>

<ul>
  <li><strong>Read the draft.</strong> The IETF datatracker page for the AAuth Protocol is the authoritative spec: <code class="language-plaintext highlighter-rouge">datatracker.ietf.org/doc/draft-hardt-oauth-aauth-protocol</code>. It’s readable, with wire-level examples of every flow.</li>
  <li><strong>Start with the explainer.</strong> <a href="https://www.aauth.dev/">aauth.dev</a> lays out the “what changed” case and has an interactive protocol explorer.</li>
  <li><strong>Try it hands-on.</strong> There’s a walkthrough that bootstraps a real agent identity and makes signed calls to a test resource, which you can drive from a CLI agent. It’s the fastest way to <em>feel</em> the difference between a signed request and a bearer token.</li>
  <li><strong>Watch the design argue with itself.</strong> The GitHub repo (<code class="language-plaintext highlighter-rouge">github.com/dickhardt/AAuth</code>) has the open issues and discussions - the best place to see which parts (missions especially) are still contested.</li>
</ul>

<p>I’d genuinely encourage forming your own view rather than taking mine. I’ve tried to represent the design fairly and flag where I’m interpreting versus reporting, but I have opinions (I think the incremental-adoption story is the strongest part and the missions layer is the most interesting-but-unproven), and you should discount them accordingly.</p>

<hr />

<h2 id="the-question-underneath-it-all">The question underneath it all</h2>

<p>Strip away the tokens and the RFC numbers and AAuth is really about a single question we’re going to have to answer as AI agents move from demos into real infrastructure:</p>

<p><strong>Do we keep letting agents borrow our passwords - or do we give them identities of their own that we can actually govern?</strong></p>

<p>AAuth is one serious, well-argued proposal for the second path, from someone with the credibility to make it. It might win. It might not. But the question isn’t going away, and it’s a good one to have thought about before the systems you depend on quietly answer it for you.</p>

<hr />

<p><em>Written from a close reading of the AAuth Protocol Internet-Draft (revision -09) and the author’s explainer at aauth.dev, cross-checked against the public IETF datatracker in mid-2026. Because this is a fast-moving draft, verify any specific detail against the live specification before relying on it. Corrections welcome.</em></p>]]></content><author><name>Abhishek Dadhich</name></author><category term="aauth" /><category term="oauth" /><category term="agent-identity" /><category term="ietf" /><category term="security" /><category term="standards" /><summary type="html"><![CDATA[A plain-English guide to AAuth - the new IETF draft that gives AI agents a cryptographic identity of their own, from OAuth 2.0's original editor.]]></summary></entry><entry><title type="html">The Git tag go get couldn’t find: managing tags in a Go multi-module monorepo</title><link href="https://abhi4u1947.github.io/2026/07/go-multimodule-monorepo-tags/" rel="alternate" type="text/html" title="The Git tag go get couldn’t find: managing tags in a Go multi-module monorepo" /><published>2026-07-03T00:00:00+00:00</published><updated>2026-07-03T00:00:00+00:00</updated><id>https://abhi4u1947.github.io/2026/07/go-multimodule-monorepo-tags</id><content type="html" xml:base="https://abhi4u1947.github.io/2026/07/go-multimodule-monorepo-tags/"><![CDATA[<p>I tagged a Go module <code class="language-plaintext highlighter-rouge">v1.0.0</code>, pushed it, and watched a teammate’s <code class="language-plaintext highlighter-rouge">go get</code> come back with <code class="language-plaintext highlighter-rouge">invalid version: unknown revision</code>. The tag was sitting right there on GitHub. The commit existed. The <code class="language-plaintext highlighter-rouge">go.mod</code> was correct. Nothing about the failure made sense from the shell history in front of me.</p>

<p>It took me longer than I want to admit to find the actual rule, because most explanations of Go modules assume one module per repository and stop there. My repo had four. So I built a small, real monorepo to pin the rule down for good: <a href="https://github.com/abhi4u1947/go-multimodule-poc">go-multimodule-poc</a>, four independently versioned Go modules in one Git repository, plus <a href="https://github.com/abhi4u1947/go-multimodule-poc-consumer">go-multimodule-poc-consumer</a>, a downstream project that pulls specific versions from it. Every command below actually ran against that repo. Keep reading and you’ll be able to reproduce the exact failure - and the fix - yourself.</p>

<blockquote>
  <p><strong>TL;DR</strong>: nested-module tags need their full directory path or <code class="language-plaintext highlighter-rouge">go get</code> can’t see them, a nested <code class="language-plaintext highlighter-rouge">go.mod</code> is a hard boundary <code class="language-plaintext highlighter-rouge">./...</code> never crosses, Minimal Version Selection picks the ceiling of everyone’s declared floor rather than the newest tag anywhere, and <code class="language-plaintext highlighter-rouge">go.work</code>/<code class="language-plaintext highlighter-rouge">replace</code> are both local-only - no consumer ever sees either one. The rest of this post is where those rules come from and what breaks when you get them wrong.</p>
</blockquote>

<h2 id="why-put-four-modules-in-one-repository">Why put four modules in one repository</h2>

<p>A Go module is a unit of versioning: one <code class="language-plaintext highlighter-rouge">go.mod</code>, one semver history, one set of Git tags. A repository is just a place to keep files. Go does not require these to be the same thing, and for a set of small, tightly related components that release independently but get worked on together, keeping them in one repo has real advantages: one CI pipeline, one issue tracker, atomic commits across boundaries during a refactor, and no cross-repo dependency dance while you’re mid-change.</p>

<p>The cost is that Git tags, which are global to a repository, now have to disambiguate <em>which</em> module a <code class="language-plaintext highlighter-rouge">v1.1.0</code> refers to. That disambiguation is the entire mechanism this post is about.</p>

<p>My test repo has four modules under <code class="language-plaintext highlighter-rouge">entities/</code>:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">github.com/abhi4u1947/go-multimodule-poc/entities/shared-lib</code> - a small logger, config store, and string helpers, imported by everything else.</li>
  <li><code class="language-plaintext highlighter-rouge">github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc</code> - a stand-in service with a <code class="language-plaintext highlighter-rouge">cmd/</code>, an <code class="language-plaintext highlighter-rouge">internal/</code> package, and its own dependency on <code class="language-plaintext highlighter-rouge">shared-lib</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz</code> and <code class="language-plaintext highlighter-rouge">.../shopping-svc/ipfs</code> - two more modules, each with their own <code class="language-plaintext highlighter-rouge">go.mod</code>, physically living <em>inside</em> <code class="language-plaintext highlighter-rouge">shopping-svc</code>’s own directory tree.</li>
</ul>

<p>That last detail is not an accident. It’s the part that breaks most people’s mental model, and it’s where I’ll start.</p>

<h2 id="whats-actually-in-this-repo">What’s actually in this repo</h2>

<p>Here’s the real layout, from <a href="https://github.com/abhi4u1947/go-multimodule-poc">the repo root</a>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>go-multimodule-poc/
├── go.work
├── entities/
│   ├── shared-lib/
│   │   ├── go.mod
│   │   └── logger/  config/  utils/
│   └── shopping-svc/
│       ├── go.mod
│       ├── cmd/shopping/  api/  internal/  pkg/  version/
│       ├── estargz/
│       │   └── go.mod          # independent module
│       └── ipfs/
│           └── go.mod          # independent module
└── tools/
    └── goproxy-gen/
        └── go.mod              # a fifth module, deliberately unpublished
</code></pre></div></div>

<p>Notice what’s missing: there is no <code class="language-plaintext highlighter-rouge">go.mod</code> at the repository root. <code class="language-plaintext highlighter-rouge">go-multimodule-poc</code> is not itself a Go module. It’s a container - a directory that happens to hold five independent module graphs (four published, one internal tool). This is the thing people mean when they say “Go monorepo,” and it surprises people coming from ecosystems where the repo root usually <em>is</em> the package.</p>

<p>Each <code class="language-plaintext highlighter-rouge">go.mod</code>’s <code class="language-plaintext highlighter-rouge">module</code> line, read straight from the source:</p>

<table>
  <thead>
    <tr>
      <th>File</th>
      <th><code class="language-plaintext highlighter-rouge">module</code> directive</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/entities/shared-lib/go.mod"><code class="language-plaintext highlighter-rouge">entities/shared-lib/go.mod</code></a></td>
      <td><code class="language-plaintext highlighter-rouge">github.com/abhi4u1947/go-multimodule-poc/entities/shared-lib</code></td>
    </tr>
    <tr>
      <td><a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/entities/shopping-svc/go.mod"><code class="language-plaintext highlighter-rouge">entities/shopping-svc/go.mod</code></a></td>
      <td><code class="language-plaintext highlighter-rouge">github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc</code></td>
    </tr>
    <tr>
      <td><a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/entities/shopping-svc/estargz/go.mod"><code class="language-plaintext highlighter-rouge">entities/shopping-svc/estargz/go.mod</code></a></td>
      <td><code class="language-plaintext highlighter-rouge">github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz</code></td>
    </tr>
    <tr>
      <td><a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/entities/shopping-svc/ipfs/go.mod"><code class="language-plaintext highlighter-rouge">entities/shopping-svc/ipfs/go.mod</code></a></td>
      <td><code class="language-plaintext highlighter-rouge">github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/ipfs</code></td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">estargz</code> and <code class="language-plaintext highlighter-rouge">ipfs</code> sit inside <code class="language-plaintext highlighter-rouge">shopping-svc</code>’s directory but are not part of the <code class="language-plaintext highlighter-rouge">shopping-svc</code> module. That’s not a comment I’m adding for effect - it’s mechanically true, and the next section shows exactly where Go draws that line.</p>

<h2 id="where-does-shopping-svc-end-and-estargz-begin-module-boundaries-in-practice">Where does shopping-svc end and estargz begin: module boundaries in practice</h2>

<p>Run <code class="language-plaintext highlighter-rouge">go build ./...</code> from inside <code class="language-plaintext highlighter-rouge">entities/shopping-svc</code> and watch what gets included. From that repo, with the workspace file disabled so it can’t paper over module boundaries:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cd </span>entities/shopping-svc
<span class="nv">$ GOWORK</span><span class="o">=</span>off go list ./...
github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/api
github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/cmd/shopping
github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/internal
github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/pkg
github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/version
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">estargz/</code> and <code class="language-plaintext highlighter-rouge">ipfs/</code> are physically sitting right there in the same directory tree. They do not appear. Not filtered out, not excluded by a config flag - Go’s <code class="language-plaintext highlighter-rouge">./...</code> expansion walks down from the current module’s root and <strong>stops the instant it finds another <code class="language-plaintext highlighter-rouge">go.mod</code></strong>. The directory becomes a hard wall. <code class="language-plaintext highlighter-rouge">shopping-svc</code> has to depend on <code class="language-plaintext highlighter-rouge">estargz</code> the same way an outside consumer would: a <code class="language-plaintext highlighter-rouge">require</code> line and a real version, never a relative import.</p>

<p>I wrote this up in more detail in the repo’s own <a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/docs/module-discovery.md">module-discovery notes</a>, including the walk in both directions - up to find “what module am I in,” down to expand <code class="language-plaintext highlighter-rouge">./...</code>. The short version, as a picture:</p>

<p><img src="/assets/images/posts/2026-07-03-module-discovery-walk.svg" alt="Flowchart: go build ./... run from entities/shopping-svc finds a go.mod there and treats shopping-svc as the main module, then walks cmd, api, internal, pkg, and version. For each subdirectory it checks whether that subdirectory has its own go.mod - if not, the package is included; if yes, as with estargz and ipfs, the walk stops there and that subdirectory is excluded as a separate module." width="760" height="800" loading="lazy" /></p>

<p>This is also why <code class="language-plaintext highlighter-rouge">go.work</code> exists in this repo at all. Four independent modules means a normal <code class="language-plaintext highlighter-rouge">go build</code> in one of them can’t see uncommitted edits in the others without a real published version - exactly like an outside consumer. <a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/go.work"><code class="language-plaintext highlighter-rouge">go.work</code></a> tells the toolchain “use these local directories for these module paths” during development, without touching any <code class="language-plaintext highlighter-rouge">go.mod</code>. It’s a pure local override. The moment you leave this repository, <code class="language-plaintext highlighter-rouge">go.work</code> doesn’t exist as far as anyone else is concerned - which is exactly why the consumer repo needs a different tool for the same job, covered later.</p>

<h2 id="the-tag-naming-rule">The tag-naming rule</h2>

<p>Here’s the rule, stated once, precisely: for a module whose <code class="language-plaintext highlighter-rouge">go.mod</code> lives in a subdirectory of the repository root, the release tag must be the module’s directory path <strong>relative to the repo root</strong>, followed by <code class="language-plaintext highlighter-rouge">/vMAJOR.MINOR.PATCH</code>. A module whose <code class="language-plaintext highlighter-rouge">go.mod</code> sits at the repository root uses a bare <code class="language-plaintext highlighter-rouge">vX.Y.Z</code> with no prefix - but none of this repo’s four modules qualify, since none of them live at the root.</p>

<p>Applied to this repo, using <a href="https://github.com/abhi4u1947/go-multimodule-poc/tags"><code class="language-plaintext highlighter-rouge">git ls-remote --tags</code></a> against the live repository:</p>

<table>
  <thead>
    <tr>
      <th>Module</th>
      <th>Directory</th>
      <th>Real tags on GitHub right now</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">entities/shared-lib</code></td>
      <td><code class="language-plaintext highlighter-rouge">entities/shared-lib</code></td>
      <td><code class="language-plaintext highlighter-rouge">entities/shared-lib/v1.0.0</code>, <code class="language-plaintext highlighter-rouge">v1.1.0</code>, <code class="language-plaintext highlighter-rouge">v1.1.1</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">entities/shopping-svc</code></td>
      <td><code class="language-plaintext highlighter-rouge">entities/shopping-svc</code></td>
      <td><code class="language-plaintext highlighter-rouge">entities/shopping-svc/v1.0.0</code>, <code class="language-plaintext highlighter-rouge">v1.1.0</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">entities/shopping-svc/estargz</code></td>
      <td><code class="language-plaintext highlighter-rouge">entities/shopping-svc/estargz</code></td>
      <td><code class="language-plaintext highlighter-rouge">entities/shopping-svc/estargz/v0.18.1</code>, <code class="language-plaintext highlighter-rouge">v0.18.2</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">entities/shopping-svc/ipfs</code></td>
      <td><code class="language-plaintext highlighter-rouge">entities/shopping-svc/ipfs</code></td>
      <td><code class="language-plaintext highlighter-rouge">entities/shopping-svc/ipfs/v0.18.1</code>, <code class="language-plaintext highlighter-rouge">v0.18.2</code></td>
    </tr>
  </tbody>
</table>

<p>(There’s also a stray <code class="language-plaintext highlighter-rouge">entities/shared-lib/v0.1.0</code> tag on the real remote, left over from a bug in the release automation that I’ll come back to. It’s the kind of thing this rule makes easy to create by accident and easy to spot once you know what you’re looking at.)</p>

<p>That <code class="language-plaintext highlighter-rouge">entities/</code> prefix is the whole answer to the failure I opened with. A tag named <code class="language-plaintext highlighter-rouge">shopping-svc/v1.0.0</code> - no <code class="language-plaintext highlighter-rouge">entities/</code> - looks completely reasonable if you don’t know the module’s real path on disk. It is also not a tag that resolves to anything, because the actual directory is <code class="language-plaintext highlighter-rouge">entities/shopping-svc</code>. I proved this the hard way while building this repo: I created a tag at exactly that wrong, shorter prefix, pointing at real content that has a correct, matching tag elsewhere, and asked <code class="language-plaintext highlighter-rouge">go get</code> for it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ go get github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz@v0.18.4
go: github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz@v0.18.4:
    invalid version: unknown revision entities/shopping-svc/estargz/v0.18.4
</code></pre></div></div>

<p>That transcript is genuine, captured while constructing this repo (full context in <a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/docs/experiments.md#4-incorrect-git-tags"><code class="language-plaintext highlighter-rouge">docs/experiments.md</code>, experiment 4</a>) - I did not leave the wrong-prefix tag on the public repo afterward, so a fresh <code class="language-plaintext highlighter-rouge">git ls-remote</code> today will not show it. I’m calling that out explicitly because I’d rather tell you a demonstration tag no longer exists than let you go looking for it and think the mechanism stopped working. <code class="language-plaintext highlighter-rouge">go</code> matches tags by exact ref name. There’s no fuzzy fallback, no “did you mean,” nothing that scans for a tag containing the right version number at the wrong location. If the ref isn’t there byte-for-byte, it doesn’t exist, even if a tag with the same suffix does exist three directories over.</p>

<p>The resolution direction works the same way in reverse. Given <code class="language-plaintext highlighter-rouge">.../entities/shopping-svc/estargz@v0.18.2</code>, here’s what <code class="language-plaintext highlighter-rouge">go</code> actually does, and I’ve annotated each step against the real repo:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>requested:  github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz @ v0.18.2

1. Try successively shorter prefixes of the module path as candidate
   repository roots, longest first:
     .../entities/shopping-svc/estargz   -- not a repo root
     .../entities/shopping-svc           -- not a repo root
     .../entities                        -- not a repo root
     github.com/abhi4u1947/go-multimodule-poc  -- yes, this is the repo

2. Subtract the repo root from the module path to get the subdirectory:
     entities/shopping-svc/estargz

3. Append the requested version to get the expected tag:
     entities/shopping-svc/estargz/v0.18.2

4. Look up that exact ref. It exists, pointing at commit 204b1d6.
   Read entities/shopping-svc/estargz/go.mod from that commit's tree.
</code></pre></div></div>

<p>I confirmed step 4 independently of <code class="language-plaintext highlighter-rouge">go</code> with <code class="language-plaintext highlighter-rouge">git show entities/shopping-svc/estargz/v0.18.2 --stat</code> - the tag really does point at a commit containing exactly that subdirectory’s files. Full transcript in <a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/docs/experiments.md#9-mapping-a-requested-module-to-its-git-tag">experiment 9</a>.</p>

<h2 id="releasing-one-module-without-touching-the-others">Releasing one module without touching the others</h2>

<p>Independent versioning is the entire point of splitting a repo into modules this way: a change to <code class="language-plaintext highlighter-rouge">estargz</code> should cut an <code class="language-plaintext highlighter-rouge">estargz</code> release, not a <code class="language-plaintext highlighter-rouge">shopping-svc</code> release, even though <code class="language-plaintext highlighter-rouge">estargz</code> lives inside <code class="language-plaintext highlighter-rouge">shopping-svc</code>’s directory. I automated this with <a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/.github/workflows/release.yml"><code class="language-plaintext highlighter-rouge">.github/workflows/release.yml</code></a>, and the interesting part is not the tagging - it’s correctly figuring out <em>which</em> module changed.</p>

<p>The workflow diffs the push against its parent commit, then attributes changed files to a module by checking directory prefixes <strong>longest first</strong>: <code class="language-plaintext highlighter-rouge">shopping-svc/estargz</code> and <code class="language-plaintext highlighter-rouge">shopping-svc/ipfs</code> before the bare <code class="language-plaintext highlighter-rouge">shopping-svc</code>. Skip that ordering and a change under <code class="language-plaintext highlighter-rouge">entities/shopping-svc/estargz/</code> would get misattributed to the parent <code class="language-plaintext highlighter-rouge">shopping-svc</code> module, since <code class="language-plaintext highlighter-rouge">entities/shopping-svc/estargz/foo.go</code> also starts with <code class="language-plaintext highlighter-rouge">entities/shopping-svc/</code>. Once a module’s identified, the job builds and vets it standalone (<code class="language-plaintext highlighter-rouge">GOWORK=off</code>, so the local workspace file can’t hide a real problem), computes the next patch version from the last matching tag, tags it, and cuts a GitHub Release.</p>

<p>That “last matching tag” lookup taught me a sharper lesson than I expected. My first version used <code class="language-plaintext highlighter-rouge">grep -Ev -- '-'</code> to filter out pre-release-looking tags. It silently excluded every single tag for <code class="language-plaintext highlighter-rouge">shared-lib</code>, because the string <code class="language-plaintext highlighter-rouge">shared-lib</code> itself contains a hyphen. The fix was an exact-match regex - <code class="language-plaintext highlighter-rouge">^entities/shared-lib/v[0-9]+\.[0-9]+\.[0-9]+$</code> - not a “does it look weird” heuristic. That bug shipped one bad tag, <code class="language-plaintext highlighter-rouge">entities/shared-lib/v0.1.0</code>, before I caught it; it’s the stray tag mentioned in the table above, and it’s a small, honest illustration of how easy it is to get tag-matching subtly wrong even when you know the rule.</p>

<h2 id="three-requirers-one-winner">Three requirers, one winner</h2>

<p>Before you scroll past this paragraph: three different modules in this repo each state a different minimum version of <code class="language-plaintext highlighter-rouge">shared-lib</code>. When a consumer pulls in all three, Go has to pick exactly one version of <code class="language-plaintext highlighter-rouge">shared-lib</code> for the build. Which one does it pick - the newest available anywhere, the oldest anyone asked for, or something else? Decide before you read the table.</p>

<table>
  <thead>
    <tr>
      <th>Requirer</th>
      <th>states as its minimum</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">shopping-svc</code> (v1.1.0)</td>
      <td><code class="language-plaintext highlighter-rouge">shared-lib &gt;= v1.1.0</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">shopping-svc/estargz</code> (v0.18.2)</td>
      <td><code class="language-plaintext highlighter-rouge">shared-lib &gt;= v1.0.0</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">shopping-svc/ipfs</code> (v0.18.2)</td>
      <td><code class="language-plaintext highlighter-rouge">shared-lib &gt;= v1.0.0</code></td>
    </tr>
  </tbody>
</table>

<p>The rule is called Minimal Version Selection, and despite the name it does not pick the lowest number on the list. It picks, for each dependency, the <strong>maximum of every minimum anyone in the build graph declared</strong> - the lowest version that is still high enough to satisfy every stated requirement at once. Here that’s <code class="language-plaintext highlighter-rouge">max(v1.1.0, v1.0.0, v1.0.0) = v1.1.0</code>. Not the newest tag that exists anywhere (<code class="language-plaintext highlighter-rouge">shared-lib</code> has since gone on to <code class="language-plaintext highlighter-rouge">v1.1.1</code>), not the oldest - the ceiling of everyone’s floor.</p>

<p>I reproduced this from a clean scratch module against the real, live repository, requesting each dependency in the exact order a first-time consumer would:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>go get github.com/abhi4u1947/go-multimodule-poc/entities/shared-lib@v1.0.0
go: added .../entities/shared-lib v1.0.0

<span class="nv">$ </span>go get github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc@v1.1.0
go: downloading .../entities/shared-lib v1.1.0
go: upgraded .../entities/shared-lib v1.0.0 <span class="o">=&gt;</span> v1.1.0
go: added .../entities/shopping-svc v1.1.0

<span class="nv">$ </span>go get github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz@v0.18.2
go: added .../entities/shopping-svc/estargz v0.18.2

<span class="nv">$ </span>go get github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/ipfs@v0.18.2
go: added .../entities/shopping-svc/ipfs v0.18.2
</code></pre></div></div>

<p>The moment <code class="language-plaintext highlighter-rouge">shopping-svc</code> enters the graph, <code class="language-plaintext highlighter-rouge">shared-lib</code> jumps from <code class="language-plaintext highlighter-rouge">v1.0.0</code> to <code class="language-plaintext highlighter-rouge">v1.1.0</code> on its own - nobody ran a second command telling it to. Adding <code class="language-plaintext highlighter-rouge">estargz</code> and <code class="language-plaintext highlighter-rouge">ipfs</code> afterward, whose own stated floor is only <code class="language-plaintext highlighter-rouge">v1.0.0</code>, doesn’t pull it back down. MVS only ever moves a selected version up, never down, which is what makes builds reproducible from <code class="language-plaintext highlighter-rouge">go.mod</code> and <code class="language-plaintext highlighter-rouge">go.sum</code> alone: there’s no resolver out there that might hand you a different answer next Tuesday.</p>

<p>Two things worth being precise about, because I got tripped up by both while building this.</p>

<p>First: <code class="language-plaintext highlighter-rouge">go mod graph</code> prints the raw edges exactly as each <code class="language-plaintext highlighter-rouge">go.mod</code> declares them. <code class="language-plaintext highlighter-rouge">estargz</code> and <code class="language-plaintext highlighter-rouge">ipfs</code> still show <code class="language-plaintext highlighter-rouge">shared-lib@v1.0.0</code> there, forever - that’s genuinely what their <code class="language-plaintext highlighter-rouge">go.mod</code> says. <code class="language-plaintext highlighter-rouge">go list -m all</code> prints the <em>outcome</em> of MVS instead: one line per module, the version actually selected. Read the wrong one and you’ll think there’s a bug where there isn’t.</p>

<p>Second: today, <code class="language-plaintext highlighter-rouge">shared-lib</code>’s real latest tag is <code class="language-plaintext highlighter-rouge">v1.1.1</code>, one patch ahead of what <code class="language-plaintext highlighter-rouge">shopping-svc</code> requires. Go treats the first sight of an unresolved import as “give me whatever’s newest,” so a totally fresh <code class="language-plaintext highlighter-rouge">go mod tidy</code> on a brand-new module lands on <code class="language-plaintext highlighter-rouge">v1.1.1</code> directly, without walking through <code class="language-plaintext highlighter-rouge">v1.1.0</code> first. Both routes are MVS-consistent - <code class="language-plaintext highlighter-rouge">v1.1.1</code> still satisfies everyone’s stated floor - they just get there for different reasons. The consumer repo’s own committed <code class="language-plaintext highlighter-rouge">go.mod</code> sits at <code class="language-plaintext highlighter-rouge">v1.1.1</code> for exactly this reason, even though its README still says <code class="language-plaintext highlighter-rouge">v1.1.0</code> in a few places. The code moved on; the prose didn’t. Check <code class="language-plaintext highlighter-rouge">git log</code>, not the README.</p>

<p><strong>Pseudo-versions</strong> are what you get when you ask for a commit that has no tag at all. I asked for the current, real, untagged tip of <code class="language-plaintext highlighter-rouge">main</code> in this repo:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ go get github.com/abhi4u1947/go-multimodule-poc/entities/shared-lib@6f79cb92532cd48c86eb93c0974ad59d5bb55934
go: downloading .../entities/shared-lib v1.1.2-0.20260703130951-6f79cb92532c
</code></pre></div></div>

<p>Read the pieces: <code class="language-plaintext highlighter-rouge">v1.1.2</code> is one patch past <code class="language-plaintext highlighter-rouge">v1.1.1</code>, the highest real tag reachable from that commit. A pseudo-version has to sort strictly after the release it follows, so Go bumps the patch component to make that true. <code class="language-plaintext highlighter-rouge">20260703130951</code> is the commit’s UTC timestamp. <code class="language-plaintext highlighter-rouge">6f79cb92532c</code> is the first twelve hex characters of the real commit SHA. None of it is invented - it’s a deterministic encoding of “this exact commit, one step past the last release.”</p>

<p>One caveat cost me a few confused minutes: you can’t hand <code class="language-plaintext highlighter-rouge">go get</code> a branch name with a slash in it as the <code class="language-plaintext highlighter-rouge">@version</code>. Go rejects anything containing <code class="language-plaintext highlighter-rouge">/</code> in that position with <code class="language-plaintext highlighter-rouge">disallowed version string</code> - I confirmed this on both the go1.24.7 toolchain I verified everything else on and a later go1.26.1 build, so it’s not a version-specific quirk. Resolve the branch to a commit SHA first, then query by that.</p>

<p>For <strong>local development</strong>, this repo uses two different tools for two different situations, and it’s worth being clear that they don’t overlap. Inside <code class="language-plaintext highlighter-rouge">go-multimodule-poc</code> itself, <a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/go.work"><code class="language-plaintext highlighter-rouge">go.work</code></a> lets all four modules build against each other’s on-disk state at once. From <em>outside</em> the repo - which is where the consumer lives - <code class="language-plaintext highlighter-rouge">go.work</code> doesn’t apply, so the consumer instead uses a <code class="language-plaintext highlighter-rouge">replace</code> directive:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>replace github.com/abhi4u1947/go-multimodule-poc/entities/shared-lib =&gt; ../go-multimodule-poc/entities/shared-lib
</code></pre></div></div>

<p>(from <a href="https://github.com/abhi4u1947/go-multimodule-poc-consumer/blob/main/go.mod.replace-example"><code class="language-plaintext highlighter-rouge">go.mod.replace-example</code></a> in the consumer repo). With that line present, every build resolves <code class="language-plaintext highlighter-rouge">shared-lib</code> from the local checkout - unpublished, untagged, whatever’s on disk - and the version string in <code class="language-plaintext highlighter-rouge">require</code> becomes cosmetic while the directive is active. Remove the line and resolution snaps straight back to the tagged version in <code class="language-plaintext highlighter-rouge">go.sum</code>, with nothing else to change. I verified both states directly: with the <code class="language-plaintext highlighter-rouge">replace</code> line in, a locally patched log line showed up in the program’s output immediately; with it removed, the output went back to exactly what the tagged release produces.</p>

<h2 id="consuming-it-for-real">Consuming it for real</h2>

<p>The consumer repo’s <a href="https://github.com/abhi4u1947/go-multimodule-poc-consumer/blob/main/main.go"><code class="language-plaintext highlighter-rouge">main.go</code></a> imports all four modules independently, sub-packages included, not just module roots:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">(</span>
	<span class="n">shopping</span> <span class="s">"github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/version"</span>
	<span class="n">estargz</span>  <span class="s">"github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz/version"</span>
	<span class="n">ipfs</span>     <span class="s">"github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/ipfs/version"</span>

	<span class="n">estargzpkg</span> <span class="s">"github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz/pkg"</span>
	<span class="n">ipfspkg</span>    <span class="s">"github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/ipfs/pkg"</span>

	<span class="s">"github.com/abhi4u1947/go-multimodule-poc/entities/shared-lib/logger"</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Getting a specific version of one of them is a plain <code class="language-plaintext highlighter-rouge">go get module@version</code> - no special syntax for “this is a nested module,” because as far as the consumer is concerned it’s just another module path:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ go get github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz@v0.18.2
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">go mod tidy</code> afterward reconciles the full <code class="language-plaintext highlighter-rouge">require</code> block against what’s actually imported, adding anything missing and dropping anything unused, and writes the resolved content hashes into <code class="language-plaintext highlighter-rouge">go.sum</code>.</p>

<p>Four commands read the result back in different, non-overlapping ways, and mixing them up is an easy way to misdiagnose a version problem:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">go list -m all</code></strong> - one line per module, the version MVS actually selected for the build. This is “what will I compile against.”</li>
  <li><strong><code class="language-plaintext highlighter-rouge">go mod graph</code></strong> - every raw requirement edge, exactly as each <code class="language-plaintext highlighter-rouge">go.mod</code> declares it. <code class="language-plaintext highlighter-rouge">estargz</code>’s edge to <code class="language-plaintext highlighter-rouge">shared-lib</code> will show <code class="language-plaintext highlighter-rouge">v0.18.2 -&gt; v1.0.0</code> here forever, regardless of what version actually gets selected. This is “who asked for what.”</li>
  <li><strong><code class="language-plaintext highlighter-rouge">go mod why -m &lt;module&gt;</code></strong> - the shortest import path from the main module to that dependency, or a note that it isn’t needed. Without <code class="language-plaintext highlighter-rouge">-m</code>, <code class="language-plaintext highlighter-rouge">go mod why &lt;path&gt;</code> asks a narrower question - “is this exact package path imported” - and will tell you a module “does not need” a package that lives at the module’s own root if you only ever import its subpackages, which is exactly this repo’s shape.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">go list -m -json &lt;module&gt;</code></strong> - the same information a proxy would hand back for that module, as structured JSON: path, version, commit time, on-disk cache location, and the <code class="language-plaintext highlighter-rouge">go.sum</code> hashes.</li>
</ul>

<p>The consumer’s <a href="https://github.com/abhi4u1947/go-multimodule-poc-consumer/blob/main/README.md">README</a> walks all four with real transcripts against this exact dependency set, and I re-ran each of them fresh against the live repository while writing this post; they still match.</p>

<p>Staying current is where the two repos actually talk to each other. The consumer has <a href="https://github.com/abhi4u1947/go-multimodule-poc-consumer/blob/main/.github/dependabot.yml">Dependabot</a> configured for the <code class="language-plaintext highlighter-rouge">gomod</code> ecosystem on a daily schedule - ordinary Dependabot behavior, it doesn’t know or care that the upstream is a monorepo. Layered on top, the producer’s release workflow fires a <code class="language-plaintext highlighter-rouge">repository_dispatch</code> event (<a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/.github/workflows/release.yml"><code class="language-plaintext highlighter-rouge">module-released</code></a>) at the consumer the moment it tags a new version, and the consumer’s <a href="https://github.com/abhi4u1947/go-multimodule-poc-consumer/blob/main/.github/workflows/auto-update.yml"><code class="language-plaintext highlighter-rouge">auto-update.yml</code></a> reacts by bumping exactly that module and opening a PR within seconds instead of waiting for the next scheduled check. If the dispatch never arrives - the producer’s <code class="language-plaintext highlighter-rouge">CONSUMER_DISPATCH_TOKEN</code> secret isn’t set, say - the same workflow still runs on its own daily cron and on manual dispatch, bumping every tracked module to <code class="language-plaintext highlighter-rouge">@latest</code> as a fallback. Dependabot is the safety net; the dispatch is the fast path. Neither one depends on the other working.</p>

<h2 id="three-ways-this-breaks">Three ways this breaks</h2>

<p><strong>Module path mismatch.</strong> <code class="language-plaintext highlighter-rouge">go.mod</code>’s <code class="language-plaintext highlighter-rouge">module</code> line is the only source of truth for a module’s identity - not the tag, not the URL you fetched it from. If a module gets fetched at path P but its own <code class="language-plaintext highlighter-rouge">go.mod</code> declares path Q, <code class="language-plaintext highlighter-rouge">go</code> refuses before it ever reads a line of code:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>module declares its path as: github.com/abhi4u1947/go-multimodule-poc/estargz-wrong-path
        but was required as: github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz
</code></pre></div></div>

<p>I built this exact scenario while constructing the repo - a copy of <code class="language-plaintext highlighter-rouge">estargz</code> with its <code class="language-plaintext highlighter-rouge">module</code> line pointed at the wrong path, tagged and fetched as if it were the real thing. The full transcript is in <a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/docs/experiments.md#3-module-path-mismatch">experiment 3</a>. As with the wrong-prefix tag earlier, I didn’t leave that broken tag on the public repo afterward, so it isn’t something you can fetch from GitHub today - the error text above is genuine, captured output, not a guess at what the message would say. The check itself exists for a real reason: without it, a compromised or misconfigured module could quietly answer to a trusted import path it doesn’t actually own.</p>

<p><strong>Wrong-prefix tags.</strong> Covered above, worth restating as its own failure mode because it’s the one I actually hit in production: a tag can contain the exact right version string and still resolve to nothing, because <code class="language-plaintext highlighter-rouge">go</code> matches refs by exact name, not by scanning for a tag that merely mentions the right number somewhere in the repo.</p>

<p><strong>Module identity survives the tag; the tag doesn’t survive resolution.</strong> This repo has a genuinely interesting case of it: two <em>different</em> tags, <code class="language-plaintext highlighter-rouge">entities/shopping-svc/estargz/v0.18.2</code> and <code class="language-plaintext highlighter-rouge">entities/shopping-svc/ipfs/v0.18.1</code>, point at the exact same commit (<code class="language-plaintext highlighter-rouge">204b1d6</code>) - that commit changed <code class="language-plaintext highlighter-rouge">estargz</code> and left <code class="language-plaintext highlighter-rouge">ipfs</code> untouched since its prior release, so <code class="language-plaintext highlighter-rouge">ipfs</code>’s existing tag and <code class="language-plaintext highlighter-rouge">estargz</code>’s new one both land there. I confirmed both resolve correctly and separately, live, against the real repo:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ go list -m github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz@v0.18.2
github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz v0.18.2

$ go list -m github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/ipfs@v0.18.1
github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/ipfs v0.18.1
</code></pre></div></div>

<p>Same commit, two distinct, correctly identified modules, because each resolves through its own module path’s expected subdirectory and its own <code class="language-plaintext highlighter-rouge">go.mod</code>. The tag got you to the commit. After that, it’s gone - nothing in <code class="language-plaintext highlighter-rouge">go.mod</code>, <code class="language-plaintext highlighter-rouge">go.sum</code>, or a compiled binary ever refers back to the tag string again, only to the resolved version and content hash. You could rename every tag in this repo tomorrow and none of these modules’ identities would change.</p>

<h2 id="bonus-a-goproxy-you-can-hold-in-your-hand">Bonus: a GOPROXY you can hold in your hand</h2>

<p>One more piece, kept short on purpose. The producer repo has a small Go program, <a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/tools/goproxy-gen/main.go"><code class="language-plaintext highlighter-rouge">tools/goproxy-gen</code></a>, that generates a real, static implementation of the <a href="https://go.dev/ref/mod#goproxy-protocol">Go module proxy protocol</a> - <code class="language-plaintext highlighter-rouge">@v/list</code>, <code class="language-plaintext highlighter-rouge">@v/&lt;version&gt;.info</code>, <code class="language-plaintext highlighter-rouge">@v/&lt;version&gt;.mod</code>, <code class="language-plaintext highlighter-rouge">@v/&lt;version&gt;.zip</code>, <code class="language-plaintext highlighter-rouge">@latest</code> - directly from this repo’s own tags. It’s built entirely on the official <code class="language-plaintext highlighter-rouge">golang.org/x/mod</code> packages: <code class="language-plaintext highlighter-rouge">modfile</code> to discover modules, a from-scratch exact-semver regex to find each module’s real tags (informed by the same hyphen bug from the release workflow), and <code class="language-plaintext highlighter-rouge">zip.CreateFromVCS</code> to build each version’s zip straight from Git history. That last function is worth calling out on its own: it automatically excludes nested modules from the zip - <code class="language-plaintext highlighter-rouge">shopping-svc</code>’s archive never contains <code class="language-plaintext highlighter-rouge">estargz/</code> or <code class="language-plaintext highlighter-rouge">ipfs/</code> - using the same module-boundary rule this whole post has been about, because it’s the real Go toolchain’s own logic, not something I had to reimplement.</p>

<p>A <a href="https://github.com/abhi4u1947/go-multimodule-poc/blob/main/.github/workflows/goproxy-artifacts.yml">GitHub Actions workflow</a> runs this on every push to <code class="language-plaintext highlighter-rouge">main</code>, resolves all four modules through the freshly generated tree with a real <code class="language-plaintext highlighter-rouge">go get</code> as a sanity check, and uploads the result as a build artifact. Point <code class="language-plaintext highlighter-rouge">GOPROXY</code> at the unzipped artifact and every command in this post works with zero network access and zero GitHub - which is a genuinely useful trick for air-gapped CI, not just a demo.</p>

<h2 id="try-it-yourself">Try it yourself</h2>

<p>Everything here is copy-pasteable against the real repositories:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone and run the producer's own service via go.work</span>
git clone https://github.com/abhi4u1947/go-multimodule-poc.git
<span class="nb">cd </span>go-multimodule-poc
go run ./entities/shopping-svc/cmd/shopping

<span class="c"># Clone the consumer and run it as-is</span>
git clone https://github.com/abhi4u1947/go-multimodule-poc-consumer.git
<span class="nb">cd </span>go-multimodule-poc-consumer
go run <span class="nb">.</span>

<span class="c"># Pull one specific version of one specific nested module</span>
go get github.com/abhi4u1947/go-multimodule-poc/entities/shopping-svc/estargz@v0.18.1
go mod tidy

<span class="c"># See what actually got selected, versus what was asked for</span>
go list <span class="nt">-m</span> all
go mod graph
</code></pre></div></div>

<p>I ran each of these against the live repositories on Go 1.24.7 while writing this. Module resolution details - pseudo-version shape, exact error text, MVS output formatting - can shift slightly across Go releases, so if something on your machine looks a little different, check the version with <code class="language-plaintext highlighter-rouge">go version</code> first and cross-reference <a href="https://go.dev/ref/mod">go.dev/ref/mod</a> before assuming the mechanism changed.</p>

<h2 id="where-this-leaves-us">Where this leaves us</h2>

<p>That teammate’s <code class="language-plaintext highlighter-rouge">go get</code> failure from the top of this post was a tag missing its directory prefix - a bare <code class="language-plaintext highlighter-rouge">v1.0.0</code> where the tooling needed <code class="language-plaintext highlighter-rouge">entities/shopping-svc/v1.0.0</code>. Small mistake, and completely invisible until someone tries to pull the module from outside the repo.</p>

<p><strong>A nested module’s tag must carry its full directory path, or it doesn’t exist as far as <code class="language-plaintext highlighter-rouge">go get</code> is concerned.</strong> Not close, not fuzzy-matched - exact.</p>

<p><strong>A nested <code class="language-plaintext highlighter-rouge">go.mod</code> is a hard wall.</strong> <code class="language-plaintext highlighter-rouge">./...</code> stops there, in both directions, no configuration involved.</p>

<p><strong>Minimal Version Selection means the ceiling of everyone’s floor, not the newest tag in existence.</strong> Read <code class="language-plaintext highlighter-rouge">go list -m all</code> for the outcome, <code class="language-plaintext highlighter-rouge">go mod graph</code> for who asked for what.</p>

<p><strong><code class="language-plaintext highlighter-rouge">go.work</code> and <code class="language-plaintext highlighter-rouge">replace</code> solve the same local-development problem from two different vantage points</strong>, and neither one ever leaves your own machine - consumers never see either.</p>

<p><strong>The <code class="language-plaintext highlighter-rouge">module</code> line in <code class="language-plaintext highlighter-rouge">go.mod</code> is a module’s entire identity.</strong> The tag that found it is disposable the instant the commit is resolved.</p>

<p>If you’re carrying more than one Go module in a repository, or about to start, that’s the whole rulebook. I’d rather you learn it from this post than from a teammate’s confused Slack message about a tag that “should” be there.</p>]]></content><author><name>Abhishek Dadhich</name></author><category term="go" /><category term="golang" /><category term="go-modules" /><category term="monorepo" /><category term="platform-engineering" /><summary type="html"><![CDATA[How Go module boundaries, Git tags, and Minimal Version Selection actually work across a real four-module monorepo, verified command by command.]]></summary></entry><entry><title type="html">Zero-trust for local agents: what SPIFFE gets right, and the session identity gap it leaves open</title><link href="https://abhi4u1947.github.io/2026/05/spiffe-for-agent-identity/" rel="alternate" type="text/html" title="Zero-trust for local agents: what SPIFFE gets right, and the session identity gap it leaves open" /><published>2026-05-25T00:00:00+00:00</published><updated>2026-05-25T00:00:00+00:00</updated><id>https://abhi4u1947.github.io/2026/05/spiffe-for-agent-identity</id><content type="html" xml:base="https://abhi4u1947.github.io/2026/05/spiffe-for-agent-identity/"><![CDATA[<p>Every Goose session I run that touches infrastructure is authenticated with my credentials. My kubeconfig, my AWS profile, my GitHub token sitting in the shell environment. When I type a command, I’m accountable for it. When Goose runs one - via a shell tool, an MCP server, a kubectl invocation - the auth log shows me. There’s no separation between my identity and the agent’s identity, no session boundary, no scope enforced at the credential layer.</p>

<p>For the kind of work I described in my <a href="/2026/05/scoping-goose-for-ops/">scoping post</a>, that’s acceptable in the short term because I constrained access at the RBAC level. RBAC-only is not workload identity though. It’s a permission boundary with no traceability. When I’m running agents in environments that matter, I want what I’d want for any other workload: cryptographic identity, short-lived credentials, a clear audit trail.</p>

<p><a href="https://spiffe.io">SPIFFE</a> is the framework I’d reach for first. I’ve used SPIRE to issue workload identity for service meshes - mTLS between services, JWT-SVIDs for cross-cluster API calls, workload attestation tied to k8s pod metadata. The question I’ve been sitting with: does the same model transfer to agent workloads?</p>

<p>Partly. Here’s where I’ve landed.</p>

<h2 id="what-spiffespire-actually-does">What SPIFFE/SPIRE actually does</h2>

<p>The quick version, for readers who haven’t wired it up. SPIFFE defines a standard for workload identity: each workload gets a SPIFFE ID in the format <code class="language-plaintext highlighter-rouge">spiffe://&lt;trust-domain&gt;/&lt;path&gt;</code>, and a SVID (SPIFFE Verifiable Identity Document) that proves it. <a href="https://spiffe.io/docs/latest/spire-about/">SPIRE</a> is the reference implementation that issues and rotates them.</p>

<p>Two SVID types matter here:</p>

<ul>
  <li><strong>X.509-SVIDs</strong> are TLS certificates. They enable mTLS - mutual authentication where both sides prove identity. TTL defaults to 1 hour; SPIRE auto-rotates via a streaming Workload API connection, so workloads get fresh certs before expiry without polling.</li>
  <li><strong>JWT-SVIDs</strong> are signed tokens with mandatory <code class="language-plaintext highlighter-rouge">aud</code> (audience) and <code class="language-plaintext highlighter-rouge">exp</code> (expiry) claims (<a href="https://spiffe.io/docs/latest/spiffe-specs/jwt-svid/">per the SPIFFE JWT-SVID spec</a>). Short-lived, single-audience. Better for discrete API calls where you want to scope the credential to one downstream service.</li>
</ul>

<p>Attestation is how SPIRE knows a workload is who it claims to be. Node attestation verifies the underlying host: cloud instance identity documents, k8s projected service account tokens, TPM. Workload attestation verifies the process: Unix UID/GID, k8s pod metadata, docker labels, systemd unit name. The combination gives you: this specific process, running in this specific pod, on this specific node, with this specific identity.</p>

<h2 id="where-it-fits-for-agents">Where it fits for agents</h2>

<p>An agent binary running in a known environment attests cleanly with existing SPIRE selectors. A Goose process in a k8s pod gets a SPIFFE ID like <code class="language-plaintext highlighter-rouge">spiffe://prod/agents/goose-worker</code>, backed by a certificate issued to that pod’s identity. It auto-rotates. Any internal service configured to trust the same SPIRE server can verify it.</p>

<p>For tool calls specifically, JWT-SVIDs with audience binding are a real improvement over long-lived API keys. Instead of a static <code class="language-plaintext highlighter-rouge">GITHUB_TOKEN</code> sitting in the environment, the agent requests a JWT-SVID with <code class="language-plaintext highlighter-rouge">aud: github-proxy</code> before each call - short-lived, audience-scoped, auditable on the receiving end by checking the <code class="language-plaintext highlighter-rouge">aud</code> claim. The receiving service sees exactly which SPIFFE ID made the call and when.</p>

<p>I’ve done this for batch jobs calling internal services. The setup transfers directly to a Goose process running in a pod.</p>

<h2 id="the-session-identity-gap">The session identity gap</h2>

<p>Here’s where it gets complicated. SPIFFE’s attestation model is process-centric. The SVID belongs to the running process. One agent binary can run hundreds of sessions over its lifetime - from SPIFFE’s perspective, they’re all the same workload.</p>

<p>Three things break when you need session-level accountability:</p>

<p><strong>The audit trail problem.</strong> Your API gateway log shows <code class="language-plaintext highlighter-rouge">spiffe://prod/agents/goose-worker</code> called <code class="language-plaintext highlighter-rouge">kubectl get pods</code> 47 times. You can’t tell which session produced each call, which user prompt triggered it, or what chain of reasoning the model was following when it ran. For incident investigation, that’s not enough. For compliance, it’s useless. This is the same gap I named in the <a href="/2026/05/supply-chain-questions-for-agents/">supply chain post</a> – no attestation connecting executed commands back to the reasoning that produced them.</p>

<p><strong>The blast radius problem.</strong> If a session goes wrong - prompt injection, bad tool call, a model that decided to <code class="language-plaintext highlighter-rouge">kubectl delete</code> something it shouldn’t - you can’t revoke that session’s credentials independently of the agent process. Revoking the SVID kills all active sessions on that agent. You’re choosing between “leave the compromised session running” and “take down everything.”</p>

<p><strong>The subprocess identity problem.</strong> When Goose spawns a shell tool that calls <code class="language-plaintext highlighter-rouge">aws s3 ls</code>, that subprocess is not the attested workload. It inherits the parent’s environment. The Workload API socket binding is to the agent PID; child processes get nothing from SPIRE. If the subprocess needs to authenticate to something, it’s back to environment-variable credentials.</p>

<p>SPIFFE gives you process identity. Agents need session identity as a first-class concept. JWT-SVIDs are the closest fit - short-lived, audience-bound, can carry additional claims - but SPIRE issues them for the process, not the session. Minting a fresh JWT with session-scoped claims requires custom logic above the SPIFFE layer.</p>

<h2 id="what-a-session-identity-layer-looks-like">What a session-identity layer looks like</h2>

<p>A pattern I’ve been sketching sits on top of SPIFFE rather than extending it. The rough shape:</p>

<ol>
  <li>
    <p><strong>Process attestation via SPIRE.</strong> The agent binary gets a SPIFFE X.509-SVID the standard way - workload attestation, auto-rotating, 1-hour TTL. This is the base credential: it proves the agent is what it claims to be, running where it claims to be running.</p>
  </li>
  <li>
    <p><strong>A session minting service.</strong> A lightweight internal service, itself identified via SPIFFE, that issues session tokens. At session start, the agent presents its process SVID and gets back a short-lived JWT with session-scoped claims: <code class="language-plaintext highlighter-rouge">session_id</code>, <code class="language-plaintext highlighter-rouge">user_id</code>, <code class="language-plaintext highlighter-rouge">initiated_at</code>, <code class="language-plaintext highlighter-rouge">allowed_tools[]</code>, <code class="language-plaintext highlighter-rouge">max_ttl</code>. The session JWT is signed by the minting service’s key, not SPIRE’s. TTL is per-session policy. A receiving service that enforces <code class="language-plaintext highlighter-rouge">allowed_tools[]</code> rejects calls to any tool not in the session’s approved list – so a session scoped to read-only Kubernetes access literally cannot call an AWS or GitHub tool, even if the agent process has those MCP servers configured. The minting service populates this claim at session start based on the session’s declared scope, which can be derived from the user prompt, a session policy file, or both.</p>
  </li>
  <li>
    <p><strong>Session JWT on all tool calls.</strong> Every downstream call carries the session JWT as a bearer token. The receiving service validates it against the minting service’s public key, extracts the <code class="language-plaintext highlighter-rouge">session_id</code>, and logs it. The audit trail is now session-granular, not process-granular.</p>
  </li>
  <li>
    <p><strong>Minting service as revocation point.</strong> To kill a session, you invalidate its <code class="language-plaintext highlighter-rouge">session_id</code> at the minting service. Other sessions on the same agent process are unaffected.</p>
  </li>
</ol>

<p>This is not SPIFFE. It’s a pattern that uses SPIFFE for base attestation and adds the session layer on top. I’ve built analogous things for batch job pipelines that needed per-job credential scope; the mechanics are the same, the agent-specific claims (<code class="language-plaintext highlighter-rouge">allowed_tools[]</code>, <code class="language-plaintext highlighter-rouge">initiated_at</code>) are new.</p>

<p>The piece I haven’t solved: subprocess identity. Getting a spawned shell tool to carry the session JWT rather than fall back to environment credentials requires either a wrapper that injects the JWT into the subprocess environment, or an MCP server that brokers all tool calls through the session-aware layer. The second option is cleaner but requires more infrastructure. I want to prototype both before recommending one.</p>

<h2 id="what-the-ecosystem-needs">What the ecosystem needs</h2>

<p>SPIFFE and SPIRE are the right foundation. Platform-agnostic attestation, auto-rotating credentials, a standard Workload API - all of it transfers cleanly to agents at the process level.</p>

<p>The missing piece is session identity as a standard abstraction rather than a custom per-deployment pattern. What would help: a SPIFFE profile for agentic workloads that formalizes session token minting on top of SVIDs - the same way the existing k8s and AWS profiles define how attestation works in those environments. An “agent session” profile could specify the minting exchange, the required JWT claims, and the revocation mechanism, so every platform team doesn’t have to design this from scratch. This is the kind of standardization work that belongs at a neutral body – CNCF’s successor to TAG Security, or the AAIF working groups already convening around agent standards – rather than re-invented by every platform team.</p>

<p>I’m planning to prototype the session minting pattern in a future post - a real SPIRE setup, a real minting service, a real Goose session where every tool call carries a verifiable session JWT. I want to see where the subprocess identity problem bites in practice before writing up the solution.</p>

<p>If you’re already working on something in this space, <a href="/about/">reach out</a>.</p>]]></content><author><name>Abhishek Dadhich</name></author><category term="devsecops" /><category term="platform-engineering" /><category term="spiffe" /><category term="zero-trust" /><category term="agentic-ai" /><category term="workload-identity" /><summary type="html"><![CDATA[SPIFFE/SPIRE solves workload identity for microservices cleanly. Here is where it transfers to agentic workloads - and where the session identity gap requires something it doesn't yet provide.]]></summary></entry><entry><title type="html">What Open Plugins gets right, and what it still needs</title><link href="https://abhi4u1947.github.io/2026/05/open-plugins-standard/" rel="alternate" type="text/html" title="What Open Plugins gets right, and what it still needs" /><published>2026-05-22T00:00:00+00:00</published><updated>2026-05-22T00:00:00+00:00</updated><id>https://abhi4u1947.github.io/2026/05/open-plugins-standard</id><content type="html" xml:base="https://abhi4u1947.github.io/2026/05/open-plugins-standard/"><![CDATA[<p>Every MCP server I’ve added to my Goose setup has been its own small project. Some are a <code class="language-plaintext highlighter-rouge">git clone</code> and a config path. Some need <code class="language-plaintext highlighter-rouge">pip install</code> and the right virtual environment. A few are remote URLs I’m trusting based on the GitHub star count and a five-minute README scan. There is no standard shape, no standard install, no standard way to know what a server actually does before you’ve already run it.</p>

<p><a href="https://open-plugins.com/agent-builders">Open Plugins</a> is trying to fix that.</p>

<h2 id="the-distribution-problem">The distribution problem</h2>

<p>The agent tooling ecosystem has a packaging gap. We have <a href="https://modelcontextprotocol.io">MCP</a> for the protocol, <a href="https://agentsmd.org">AGENTS.md</a> for behavior declarations, and a growing catalog of Goose extensions - but no standard answer to the question: <em>how does a plugin get from the author’s repo to a running agent, in a way that’s reproducible, inspectable, and safe to do on a shared machine?</em></p>

<p>That gap is what Open Plugins is addressing. The spec defines a convention-based system for discovering, packaging, installing, and namespacing agent extensions. It covers skills, commands, agents, hooks, MCP servers, and LSP servers - essentially everything that runs alongside an agent and extends its capabilities. The goal is that any conformant agent tool can install and run any conformant plugin, without custom integration work on either end.</p>

<h2 id="how-the-standard-works">How the standard works</h2>

<p>At its core, a plugin is a directory with a predictable layout. A plugin manifest lives at <code class="language-plaintext highlighter-rouge">.plugin/plugin.json</code> and declares the plugin name plus any non-standard component paths. The manifest is optional - if a plugin follows the directory conventions, a conformant tool can discover everything without it.</p>

<p>Component discovery uses these default paths:</p>

<ul>
  <li>Skills: <code class="language-plaintext highlighter-rouge">/skills/*/SKILL.md</code></li>
  <li>Commands: <code class="language-plaintext highlighter-rouge">/commands/*.md</code></li>
  <li>Agents: <code class="language-plaintext highlighter-rouge">/agents/*.md</code></li>
  <li>Hooks: <code class="language-plaintext highlighter-rouge">/hooks/hooks.json</code></li>
  <li>MCP Servers: <code class="language-plaintext highlighter-rouge">/.mcp.json</code></li>
  <li>LSP Servers: <code class="language-plaintext highlighter-rouge">/.lsp.json</code></li>
</ul>

<p>Installation copies the plugin to a local cache directory and adds it to the <code class="language-plaintext highlighter-rouge">enabledPlugins</code> list in the tool’s settings. Plugins are expected to be self-contained - no external dependencies resolved at install time.</p>

<p>Path references inside the plugin use a <code class="language-plaintext highlighter-rouge">${PLUGIN_ROOT}</code> placeholder that expands to the plugin’s absolute path after installation. Hook commands, MCP configs, and LSP settings all use this mechanism. It’s the same problem npm solved with <code class="language-plaintext highlighter-rouge">__dirname</code>, and the Open Plugins answer is correct: lock the root at install, expand everywhere.</p>

<p>Namespacing uses a <code class="language-plaintext highlighter-rouge">pluginName:componentName</code> format. A <code class="language-plaintext highlighter-rouge">deploy-tools</code> plugin’s <code class="language-plaintext highlighter-rouge">status</code> skill becomes <code class="language-plaintext highlighter-rouge">deploy-tools:status</code>. This prevents collisions when multiple plugins expose components with the same name.</p>

<p>The spec ships with a reference CLI: <code class="language-plaintext highlighter-rouge">npx plugin-ref validate ./my-plugin</code> checks conformance, <code class="language-plaintext highlighter-rouge">npx plugin-ref inspect</code> shows what a tool would discover. Shipping a validator alongside the spec is exactly the right call.</p>

<p>Specs without reference implementations drift.</p>

<h2 id="what-it-gets-right">What it gets right</h2>

<p>Convention over configuration is the right default here. The manifest is optional, and the directory conventions are simple enough that most plugin authors will follow them without thinking about it. Adoption requires zero friction for the common case; Open Plugins mostly delivers that.</p>

<p>The minimal conformance requirements also help. A conformant tool only needs to handle one component type; it doesn’t have to implement the full spec to be useful. That’s a pragmatic choice for a young ecosystem where adoption matters more than completeness.</p>

<p>The <code class="language-plaintext highlighter-rouge">${PLUGIN_ROOT}</code> expansion solves a real portability problem. Without it, plugins either hardcode absolute paths (breaks on any machine that isn’t the author’s) or use relative paths (breaks when the plugin gets copied to a cache directory). The expansion mechanism is clean, and the path traversal protection - <code class="language-plaintext highlighter-rouge">../</code> sequences that escape the plugin root are rejected - is the right boundary.</p>

<p>Namespacing is underrated. I’ve already had skill name collisions in my Goose setup. Having a canonical format for disambiguation is going to matter once there are hundreds of plugins in circulation.</p>

<h2 id="the-security-questions-it-still-needs-to-answer">The security questions it still needs to answer</h2>

<p>This is where I put my platform-security hat on, and where I want to be careful to separate “this is wrong” from “this is appropriate for where the ecosystem is, and here’s what needs to happen next.”</p>

<p>The current trust model is roughly: install the plugin, hooks and MCP commands run with your user-level permissions, and the recommended mitigations are advisory. That’s not a critique of the spec authors - it’s the same pragmatism that got npm to adoption in 2010. But it’s worth naming the gaps precisely, because this is the point in the ecosystem’s lifecycle where they’re cheapest to fix.</p>

<p>The specific gaps I see:</p>

<p><strong>Hook execution at user level, with advisory mitigations.</strong> A plugin’s hooks run as the installing user. The spec recommends sandboxing, allowlisting, and user confirmation before enabling hooks - but these are implementation guidance, not conformance requirements. A tool that installs plugins and runs hooks with none of those controls is still conformant. The threat surface is roughly equivalent to <code class="language-plaintext highlighter-rouge">curl | bash</code>, and the spec treats it as a UX choice rather than a security boundary.</p>

<p><strong>No signing story.</strong> Nothing in the current spec tells a conformant tool how to verify that a plugin came from who it claims to come from. A release could be tampered with between the author’s repo and a user’s machine, and a conformant tool has no way to detect it. This is the same gap npm had in 2012.</p>

<p><strong>No SBOM.</strong> A plugin’s <code class="language-plaintext highlighter-rouge">/.mcp.json</code> can reference a remote MCP server. That server has its own dependencies, its own update cadence, its own author. The “self-contained” requirement covers the plugin package itself, but the MCP servers it configures are effectively untracked transitive dependencies. There’s no standard way to produce or consume a software bill of materials for a plugin.</p>

<p><strong>Path traversal protection as guidance, not requirement.</strong> The spec says implementations <em>should</em> reject <code class="language-plaintext highlighter-rouge">../</code> sequences that escape the plugin root. I’d like to see this as a <em>must</em>. “Should” in a security control means “optional in practice.”</p>

<p>I raised similar questions in my <a href="/2026/05/supply-chain-questions-for-agents/">supply chain post</a> about the MCP ecosystem in general. Open Plugins makes those questions concrete. Here’s the spec. Here are the specific places where the supply chain story is thin.</p>

<h2 id="why-the-timing-matters">Why the timing matters</h2>

<p>The standard is early. That’s the good news.</p>

<p>npm shipped without package signing in 2010. Sigstore-based provenance support arrived in 2023 – thirteen years later – and still isn’t universally enforced. Thirteen years of retrofitting security into an ecosystem that had grown around the gaps. The JavaScript community is still paying for decisions made when npm was a side project.</p>

<p>Open Plugins is at the beginning of that curve. Open Plugins is not currently an AAIF project – it is maintained by Vercel Labs – but the AAIF community is exactly the right group to adopt and endorse these norms before the ecosystem grows around the absence of them. Goose, MCP, and AGENTS.md already have the Linux Foundation governance structure and the audience that cares about supply chain security. The tooling (<a href="https://sigstore.dev">Sigstore</a>, <a href="https://in-toto.io">in-toto</a>, <a href="https://cyclonedx.org">CycloneDX</a>) exists. The question is whether the spec reaches for it before the install base gets large enough to make breaking changes painful.</p>

<h2 id="what-id-like-to-see">What I’d like to see</h2>

<p>Concrete asks, in rough priority order:</p>

<ul>
  <li><strong>Signing for plugin releases.</strong> <a href="https://sigstore.dev">Sigstore</a>-style, with a transparency log the community can verify. The <code class="language-plaintext highlighter-rouge">plugin-ref</code> CLI already validates structure; a <code class="language-plaintext highlighter-rouge">plugin-ref attest</code> subcommand that checks signatures would be a natural extension. Make signature verification a conformance requirement for enterprise-tier tools, advisory for community tools.</li>
  <li><strong>An SBOM requirement for plugins that reference external MCP servers.</strong> <a href="https://cyclonedx.org">CycloneDX</a> or SPDX format, generated at build time, included in the plugin package. Not optional for any plugin that ships a <code class="language-plaintext highlighter-rouge">/.mcp.json</code>.</li>
  <li><strong>A trust-level field in the manifest.</strong> Something like <code class="language-plaintext highlighter-rouge">community</code>, <code class="language-plaintext highlighter-rouge">verified</code>, <code class="language-plaintext highlighter-rouge">enterprise</code> that conformant tools surface to users before installation. The details can be worked out - the point is that install should not be binary.</li>
  <li><strong>Path traversal as a conformance requirement, not a recommendation.</strong></li>
</ul>

<p>None of these are blockers for adoption today. They’re the difference between a standard that enterprises can reference in a security policy and one that stays in developer-only workflows.</p>

<p>I’m planning to prototype some of this - signing a plugin release, generating an SBOM for a Goose configuration. If you’re working on any of it already, <a href="/about/">reach out</a>.</p>

<p>The standard is worth engaging with seriously. That’s why I’m raising the gaps.</p>]]></content><author><name>Abhishek Dadhich</name></author><category term="opinion" /><category term="open-plugins" /><category term="mcp" /><category term="supply-chain" /><category term="platform-engineering" /><category term="goose" /><summary type="html"><![CDATA[The Open Plugins standard takes a real swing at the agent extension distribution problem. Here is what it gets right - and the supply chain questions it still needs to answer.]]></summary></entry><entry><title type="html">The supply chain questions nobody is asking about MCP yet</title><link href="https://abhi4u1947.github.io/2026/05/supply-chain-questions-for-agents/" rel="alternate" type="text/html" title="The supply chain questions nobody is asking about MCP yet" /><published>2026-05-15T00:00:00+00:00</published><updated>2026-05-15T00:00:00+00:00</updated><id>https://abhi4u1947.github.io/2026/05/supply-chain-questions-for-agents</id><content type="html" xml:base="https://abhi4u1947.github.io/2026/05/supply-chain-questions-for-agents/"><![CDATA[<p>I spent a big part of the last year integrating supply chain security into SDLC pipelines - provenance with <a href="https://slsa.dev">SLSA</a>, attestation with <a href="https://in-toto.io">in-toto</a>, SBOMs with <a href="https://cyclonedx.org">CycloneDX</a>, signing with <a href="https://sigstore.dev">Sigstore</a>, the whole modern stack. The goal was the same as everyone else’s: when something shows up in production, I want to be able to answer <em>where did this come from, who signed off on it, and what was it built from</em>.</p>

<p>Then I started using <a href="https://github.com/aaif-goose/goose">Goose</a> and connecting it to MCP servers – first in the <a href="/2026/05/scoping-goose-for-ops/">scoped ops setup I wrote about last week</a> – and I realized none of that thinking has been applied here yet.</p>

<p>Almost every post you can find on MCP frames it the same way: <em>MCP is the USB-C of AI integrations</em>. Plug in any server, your agent gains capabilities. It’s slick, it’s growing fast, and 70+ extensions exist for <a href="https://aaif.io/projects/goose/">Goose</a> alone.</p>

<p>What nobody seems to be writing about: every MCP server you install is code from somewhere, running in your environment, with tools your agent will invoke based on natural language reasoning produced by an LLM. From a supply chain perspective, that is not a small surface.</p>

<p>Let me try to name the questions, because I think we need to start asking them out loud.</p>

<h2 id="what-supply-chain-even-means-for-an-agent-workflow">What “supply chain” even means for an agent workflow</h2>

<p>When a Goose session does something - say, “find the drift between our Terraform state and the AWS console” - the chain of things that produced that outcome looks roughly like:</p>

<ol>
  <li><strong>The model</strong>. A specific LLM, with a specific version, behind a specific API. Its training data, its safety tuning, its tool-calling behavior - all part of what shaped the output.</li>
  <li><strong>The agent code</strong>. Goose itself: Rust binary, version-pinned, signed (or not) at distribution.</li>
  <li><strong>The MCP servers</strong>. Each one is its own piece of software, running locally or remote, with its own dependencies, its own update cadence, its own author.</li>
  <li><strong>The tool definitions</strong>. The schema and natural-language descriptions exposed by each MCP server, which the agent uses to decide what to call.</li>
  <li><strong>The natural-language reasoning</strong> produced by the model and conditioned on a prompt - yours, the system prompt, and any context retrieved during the session.</li>
  <li><strong>The commands executed</strong>, which are downstream of all of the above.</li>
</ol>

<p>In a traditional CI/CD pipeline, we’d want provenance for every step. Here, we mostly have it for step 2 (the agent binary) and not much else.</p>

<h2 id="the-questions-i-think-we-should-be-asking">The questions I think we should be asking</h2>

<p>Some of these are solvable today with existing tooling applied to a new context; others require new conventions that don’t exist yet. I’ll call that out as we go.</p>

<h3 id="provenance-for-mcp-servers">Provenance for MCP servers</h3>

<p>When I add an MCP server to my Goose configuration, I’m adding code that will run on my machine and decide which of my files, APIs, and credentials to interact with. Right now, the equivalent of <code class="language-plaintext highlighter-rouge">pip install</code> for MCP servers - <code class="language-plaintext highlighter-rouge">goose configure</code> and add the path or URL - has roughly the same security model as curling a bash script and piping to sudo.</p>

<p>The questions:</p>

<ul>
  <li>Who signed this MCP server’s release? Is the signature checkable? Is there a key transparency mechanism?</li>
  <li>What’s the source-of-truth repo, and has the binary I’m running been built from the commit it claims?</li>
  <li>What dependencies did it pull in at build time? Where’s the SBOM?</li>
  <li>If the server is remote, what’s the attestation that the running instance matches the published source?</li>
</ul>

<p><a href="https://sigstore.dev">Sigstore</a>, <a href="https://slsa.dev">SLSA</a>, and <a href="https://in-toto.io">in-toto</a> have made enormous progress on these questions for traditional artifacts. As far as I can tell, none of that has been threaded through the MCP ecosystem yet.</p>

<h3 id="attestation-for-tool-calls">Attestation for tool calls</h3>

<p>When an agent calls a tool - <code class="language-plaintext highlighter-rouge">kubectl get pods</code>, or an MCP server’s <code class="language-plaintext highlighter-rouge">read_file</code> - that call should ideally be attestable. We should be able to say, after the fact: this command was issued by this agent, in this session, under this user, with this set of arguments, and the model’s stated reasoning was X.</p>

<p>We have most of the building blocks. Goose can already produce session logs. MCP servers can produce structured records of their tool invocations. What I haven’t seen is a coherent attestation format that ties them together in a way that survives later audit.</p>

<p>This matters for the same reason audit logs matter for human admins: when something goes wrong in production, you want to be able to reconstruct who or what did it.</p>

<h3 id="the-prompt-as-a-build-artifact">The prompt as a build artifact</h3>

<p>A working agent session is partly defined by its prompts - the system prompt, the user prompts, any retrieved context. From a supply chain view, the prompt is closer to source code than it is to configuration. It determines behavior. It should probably be versioned, reviewed, and signed.</p>

<p>I don’t see any serious treatment of this yet for production agentic systems. Most prompt management tooling today is product-focused (analytics, A/B testing) rather than supply-chain-focused (provenance, review, audit). This one requires new conventions, not just new tooling applied to a new context.</p>

<h3 id="the-model-as-a-dependency">The model as a dependency</h3>

<p>If my Goose session calls Anthropic’s Claude, OpenAI’s GPT, or a local Llama via Ollama, each of those is, in supply chain terms, a dependency. Different versions of the same model can behave meaningfully differently. Switching providers mid-session can produce different outputs from identical prompts.</p>

<p>We do not currently have anything resembling a version-pinned, attested model dependency in most real deployments. We have model names, sometimes with version suffixes, often without. That’s not enough. Like the prompt question, this one requires new standards work – there is no existing tooling to apply here.</p>

<h2 id="why-this-matters-now-not-later">Why this matters now, not later</h2>

<p>Two reasons.</p>

<p><strong>First, the agentic AI ecosystem is at a critical moment.</strong> Goose and MCP are growing fast and being adopted by real teams. The standards we set now - or fail to set - are the ones that get baked in. We did this badly with npm and the JavaScript ecosystem in the 2010s, and we’re still paying for it. We have a chance to not repeat that here, because the AAIF projects are open source, they’re hosted at the Linux Foundation, and the audience that cares about supply chain security knows what good looks like.</p>

<p><strong>Second, agents make the consequences worse, not better.</strong> A traditional pipeline executes deterministic code. An agent pipeline executes the output of natural-language reasoning conditioned on tool descriptions written by people you didn’t vet. If your supply chain has gaps now, agentic AI will exploit them at machine speed.</p>

<h2 id="what-id-love-to-see">What I’d love to see</h2>

<p>I’d love to see the AAIF community - Goose, MCP, AGENTS.md - start treating supply chain security as a first-class concern, not a “later” item. Specifically:</p>

<p>These aren’t competing solutions – they’re a stack. <a href="https://sigstore.dev">Sigstore</a> handles signing and key transparency. <a href="https://in-toto.io">in-toto</a> attests the pipeline steps. <a href="https://slsa.dev">SLSA</a> provides the level framework for what “attested” means at each stage. <a href="https://cyclonedx.org">CycloneDX</a> provides the artifact inventory format. Used together, they give you the same supply chain posture for agentic workflows that mature CI/CD pipelines already have.</p>

<ul>
  <li>An SBOM standard for MCP servers, with CycloneDX or SPDX format, generated at build time.</li>
  <li>Sigstore-style signing for MCP server releases, with a trust root the community can verify.</li>
  <li>in-toto attestation for agent tool invocations, with a structured format that integrates into existing supply chain tooling.</li>
  <li>A reference implementation of an “agentic supply chain” - Goose configured with attested MCP servers, producing signed session logs, integrated into an SLSA-style provenance chain.</li>
</ul>

<p>Some of this exists in pieces, in adjacent ecosystems. None of it is wired together yet for agentic AI specifically.</p>

<p>I’m going to start prototyping some of this in upcoming posts. Pinning MCP servers to specific signed releases. Generating SBOMs for Goose configurations. Producing structured session attestations. The <a href="/2026/05/open-plugins-standard/">next post</a> looks at the Open Plugins standard – the first concrete attempt at a distribution spec for agent extensions – and asks these same questions of a specific artifact.</p>

<p>If anyone is working on this already, please reach me - <a href="/about/">about page</a> has my coordinates. If you’re at AAIF and reading this, I’d love to talk about whether any of this is on the roadmap.</p>

<h2 id="a-note-on-tone">A note on tone</h2>

<p>I’m aware this post is more “raising questions” than “providing answers.” That’s deliberate. I’ve seen too many supply chain conversations get derailed because someone showed up with a half-built solution before the community had agreed on the problem. The point of this post is to put the problem on the table for the agentic AI community in the language platform-security people speak.</p>

<p>If we can agree the questions are worth taking seriously, the answers will follow.</p>]]></content><author><name>Abhishek Dadhich</name></author><category term="opinion" /><category term="mcp" /><category term="supply-chain" /><category term="devsecops" /><category term="goose" /><summary type="html"><![CDATA[If we treated MCP servers and agent workflows the way we treat any other code in our supply chain, what would we be doing differently?]]></summary></entry><entry><title type="html">Scoping Goose for production-adjacent ops work</title><link href="https://abhi4u1947.github.io/2026/05/scoping-goose-for-ops/" rel="alternate" type="text/html" title="Scoping Goose for production-adjacent ops work" /><published>2026-05-08T00:00:00+00:00</published><updated>2026-05-08T00:00:00+00:00</updated><id>https://abhi4u1947.github.io/2026/05/scoping-goose-for-ops</id><content type="html" xml:base="https://abhi4u1947.github.io/2026/05/scoping-goose-for-ops/"><![CDATA[<p>There’s a pattern I keep seeing in tutorials for <a href="https://github.com/aaif-goose/goose">Goose</a>: the author installs it, gives it their default kubeconfig or shell, and runs a demo. In my <a href="/2026/05/architect-notes-on-agentic-ai/">first post</a> I framed agents as a new class of workload – here’s what that means in practice. The demo works, the screenshots look great, and somewhere in the comments is a question like <em>“how do you keep this from <code class="language-plaintext highlighter-rouge">rm -rf</code>-ing your home directory?”</em> that nobody answers.</p>

<p>I’m an architect. That question is the entire interesting part for me. So this post is the inverse of the usual tutorial - it spends most of its words on the <em>configuration</em>, not the demo. The demo is at the end and is honestly the less important section.</p>

<blockquote>
  <p><strong>TL;DR</strong>: Before letting Goose touch anything real, scope its access through a dedicated service account with read-only RBAC, run it in a working directory it can’t escape from, and treat the boundary between “investigate” and “apply” as something enforced by IAM, not by trust in the agent’s judgment.</p>
</blockquote>

<h2 id="the-threat-model-im-working-from">The threat model I’m working from</h2>

<p>Goose is a local agent with shell access and MCP-based tool integrations. From a platform-security perspective, that means:</p>

<ul>
  <li>Anything in my shell environment is in scope for the agent.</li>
  <li>Anything my user can do, the agent can attempt.</li>
  <li>Anything the configured MCP servers can do is part of the agent’s reach.</li>
  <li>The LLM behind it can produce confidently wrong commands. It will eventually do this. Plan for it.</li>
</ul>

<p>The boundary I want is roughly the boundary I’d want for a competent junior engineer on their first week: investigate freely, propose changes, but apply nothing to shared infrastructure without a human in the loop. The way I enforce that boundary should not depend on the agent following instructions - it should be enforced where the credentials live.</p>

<h2 id="the-setup">The setup</h2>

<p>For this session I configured Goose to triage a misbehaving service in a staging Kubernetes cluster. Here’s the full configuration, with reasoning.</p>

<h3 id="1-a-dedicated-service-account-with-read-only-rbac-kubernetes-rbac-docs">1. A dedicated service account with read-only RBAC (<a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">Kubernetes RBAC docs</a>)</h3>

<p>I created a dedicated service account in the cluster, scoped to a single namespace, with read-only permissions:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ServiceAccount</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">goose-triage</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">staging</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">rbac.authorization.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Role</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">goose-triage-readonly</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">staging</span>
<span class="na">rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">apiGroups</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">"</span><span class="pi">]</span>
    <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">pods"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">pods/log"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">events"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">configmaps"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">services"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">endpoints"</span><span class="pi">]</span>
    <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">get"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">list"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">watch"</span><span class="pi">]</span>
  <span class="pi">-</span> <span class="na">apiGroups</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">apps"</span><span class="pi">]</span>
    <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">deployments"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">replicasets"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">statefulsets"</span><span class="pi">]</span>
    <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">get"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">list"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">watch"</span><span class="pi">]</span>
  <span class="pi">-</span> <span class="na">apiGroups</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">metrics.k8s.io"</span><span class="pi">]</span>
    <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">pods"</span><span class="pi">]</span>
    <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">get"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">list"</span><span class="pi">]</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">rbac.authorization.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">RoleBinding</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">goose-triage-binding</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">staging</span>
<span class="na">subjects</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">kind</span><span class="pi">:</span> <span class="s">ServiceAccount</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">goose-triage</span>
    <span class="na">namespace</span><span class="pi">:</span> <span class="s">staging</span>
<span class="na">roleRef</span><span class="pi">:</span>
  <span class="na">kind</span><span class="pi">:</span> <span class="s">Role</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">goose-triage-readonly</span>
  <span class="na">apiGroup</span><span class="pi">:</span> <span class="s">rbac.authorization.k8s.io</span>
</code></pre></div></div>

<p>I then generated a dedicated kubeconfig pointing at that service account’s token, scoped to that namespace, with <code class="language-plaintext highlighter-rouge">current-context</code> already set. No admin context, no other clusters, no other namespaces.</p>

<p>This took maybe ten minutes. If you skip this step, you’re not using an agent - you’re using a very fast autocomplete with root.</p>

<h3 id="2-a-scoped-working-directory">2. A scoped working directory</h3>

<p>I created <code class="language-plaintext highlighter-rouge">~/goose-sessions/triage-2026-05-22/</code> and launched Goose from there. The Developer extension’s shell tool can run anywhere your user can, so the right move is to give it a directory that doesn’t have your dotfiles, SSH keys, or git credentials in scope of its working set.</p>

<h3 id="3-an-explicit-prompt-about-boundaries">3. An explicit prompt about boundaries</h3>

<p>I started the session with this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>You have a read-only kubectl context configured (KUBECONFIG=./kubeconfig-goose-triage).
You have shell access in this working directory.
You do not have permission to apply any changes to the cluster, modify any files
outside this directory, or take any actions that affect shared infrastructure.

When you identify a likely root cause or recommend a fix, summarize it for me
to review - do not attempt to apply it yourself, even if you think the read-only
context will prevent harm.

Walk me through your reasoning before running each command.
</code></pre></div></div>

<p>The “walk me through your reasoning before running each command” line is important. Without it, Goose will sometimes batch 4-5 commands together and present results. Faster, but harder to interrupt when the reasoning is going somewhere wrong.</p>

<h2 id="the-session">The session</h2>

<p>The actual investigation took about 12 minutes. I’m going to summarize rather than paste the whole transcript, since the configuration is the part that’s worth your time.</p>

<p><strong>The scenario</strong>: a Node.js service in the <code class="language-plaintext highlighter-rouge">staging</code> namespace had been crash-looping for two weeks. Standard “we keep meaning to look at this” backlog item.</p>

<p><strong>What Goose did</strong>:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">kubectl describe</code> and <code class="language-plaintext highlighter-rouge">kubectl logs --previous</code> on the pod. Standard. Found <code class="language-plaintext highlighter-rouge">Last State: Terminated, Reason: OOMKilled</code>.</li>
  <li>Pulled the deployment manifest. Noted <code class="language-plaintext highlighter-rouge">limits.memory: 512Mi</code>.</li>
  <li>Inspected the container’s command: <code class="language-plaintext highlighter-rouge">node index.js</code>. Noted no <code class="language-plaintext highlighter-rouge">--max-old-space-size</code> flag.</li>
  <li>Inferred - correctly - that V8 was sizing its heap from the host’s total visible memory rather than the container’s cgroup limit – Node.js doesn’t read cgroup boundaries by default – so the OOM killer fired before V8’s garbage collector got aggressive.</li>
</ol>

<p>That inference is the interesting one. I knew the V8/cgroup interaction existed in theory but hadn’t connected it to this pod. Goose got from “OOMKilled” to “V8 doesn’t respect container limits unless you tell it to” in about 90 seconds. I’d estimate I’d have spent 20-30 minutes on it, starting from a different hypothesis (queue consumer memory leak).</p>

<p><strong>Where I had to step in</strong>: Goose suggested patching the deployment to add <code class="language-plaintext highlighter-rouge">--max-old-space-size=384</code> (<a href="https://nodejs.org/api/cli.html#--max-old-space-sizesize-in-megabytes">Node.js CLI docs</a>). The suggestion was reasonable. Two reasons I didn’t let it try:</p>

<ol>
  <li>The real fix lives in our GitOps repo, not in <code class="language-plaintext highlighter-rouge">kubectl edit</code>. A patched cluster that drifts from git is a worse problem than the OOM.</li>
  <li>384Mi is a guess. The right number comes from actually profiling heap behavior under load.</li>
</ol>

<p>I asked Goose to write a one-paragraph summary I could paste into a Jira ticket. It did, accurately.</p>

<h2 id="what-i-learned-about-scoping">What I learned about scoping</h2>

<p>A few things were clearer to me after this session than before:</p>

<p><strong>The read-only boundary is the whole game.</strong> Not because I expect Goose to act maliciously, but because the moment “investigate” and “apply” become the same operation, you’ve lost the ability to keep a human in the loop. RBAC enforces this whether the agent agrees with it or not. That’s the property I want.</p>

<p><strong>The “walk me through your reasoning” prompt matters.</strong> It’s the difference between an agent and an opaque oracle. It also produces text I can paste into a ticket or runbook, which is real ongoing value beyond the one session.</p>

<p><strong>Don’t let the agent write to shared state, ever.</strong> Even with read-only RBAC, the agent could in principle write to your local filesystem in ways that affect later sessions. Use a fresh working directory per session, or at minimum understand which files in your environment are reachable.</p>

<p><strong>The MCP integration is where this gets interesting and scary.</strong> Goose’s power comes from the 70+ MCP extensions it can talk to. Each one is a new permission surface. If you add an AWS MCP server, you now need to think about IAM policies on the role its credentials use. If you add a GitHub MCP server, that’s a token with some scope attached that the agent can invoke on your behalf. The right mental model is not “I’ve secured Goose” – it’s “I’ve secured this specific set of MCP integrations for this specific session.” Most of this isn’t documented yet. I go deeper on the supply chain angle in <a href="/2026/05/supply-chain-questions-for-agents/">the next post</a>.</p>

<h2 id="a-checklist-im-settling-on">A checklist I’m settling on</h2>

<p>For anyone running Goose on infrastructure that matters:</p>

<ol>
  <li><strong>Dedicated service account per session type</strong>, with the minimum RBAC for the workflow.</li>
  <li><strong>A dedicated kubeconfig</strong>, not your daily-driver one, with the SA token and a single context.</li>
  <li><strong>A scoped working directory</strong>, ideally inside a container or VM if you’re paranoid.</li>
  <li><strong>An explicit boundary prompt</strong> that names what the agent can and can’t do.</li>
  <li><strong>A “summarize, don’t apply” rule</strong> for any change to shared infrastructure.</li>
  <li><strong>Audit logs.</strong> If your platform doesn’t yet capture what the agent did, that’s the next project after this one.</li>
</ol>

<h2 id="whats-next">What’s next</h2>

<p>I want to do the same kind of writeup for a few other workflows: Terraform drift detection (the agent investigating against a real cloud account), CI pipeline triage, and the supply chain question - how do you treat MCP servers as part of your software supply chain?</p>

<p>If you have an ops workflow you’ve been curious whether Goose could help with - especially anything platform-engineering-shaped - <a href="/about/">tell me</a>. I’ll add it to the queue.</p>

<p>The <a href="https://goose-docs.ai/">Goose docs</a> cover the basics. The <a href="https://discord.com/invite/9zTwngHAMy">AAIF Discord</a> is where the real conversations happen. I’m <code class="language-plaintext highlighter-rouge">@abhi4u1947</code> in #goose.</p>]]></content><author><name>Abhishek Dadhich</name></author><category term="tutorial" /><category term="goose" /><category term="kubernetes" /><category term="devsecops" /><category term="platform-engineering" /><summary type="html"><![CDATA[A walkthrough of how I configured Goose with read-only Kubernetes access, ran a real triage session, and what I learned about what to lock down.]]></summary></entry><entry><title type="html">Agents as workloads: notes from the platform side</title><link href="https://abhi4u1947.github.io/2026/05/architect-notes-on-agentic-ai/" rel="alternate" type="text/html" title="Agents as workloads: notes from the platform side" /><published>2026-05-01T00:00:00+00:00</published><updated>2026-05-01T00:00:00+00:00</updated><id>https://abhi4u1947.github.io/2026/05/architect-notes-on-agentic-ai</id><content type="html" xml:base="https://abhi4u1947.github.io/2026/05/architect-notes-on-agentic-ai/"><![CDATA[<p>I’ve spent twenty years designing and evolving large-scale, business-critical systems. Mostly platform engineering, DevSecOps, IAM, and lately software supply chain security. The kind of work where you spend a year designing centralized authentication, workload identity with PKI, and zero-trust networking, and then another year building it into platforms that real product teams actually adopt across ten-plus customer accounts and ten-plus product lines.</p>

<p>When I look at the current wave of agentic AI tools, I don’t see them the way most of the writing about them frames them. I don’t see “a smart assistant for developers.” I see a new class of workload - one that will run inside the platforms I help build, with all the IAM, audit, observability, supply-chain, cost, and governance questions that implies.</p>

<p>That perspective is largely missing from the agentic-AI conversation. I want to add it.</p>

<h2 id="whats-missing-from-the-existing-writing">What’s missing from the existing writing</h2>

<p>If you search for tutorials on <a href="https://github.com/aaif-goose/goose">Goose</a> - the open-source agent now hosted by the <a href="https://aaif.io">Agentic AI Foundation</a> at the Linux Foundation alongside <a href="https://modelcontextprotocol.io">MCP</a> and <a href="https://agentsmd.org">AGENTS.md</a> - you’ll find a lot of the same post. <em>Install Goose. Ask it to write a Python script. Watch it generate code. Marvel at the AI.</em> The framing is consistently “Goose is an AI coding assistant.”</p>

<p>That’s accurate, but it’s the least interesting thing about it. Goose is something rarer:</p>

<blockquote>
  <p>A local agent with a real shell, real filesystem access, and a growing stack of MCP integrations - that can be pointed at production-adjacent infrastructure and asked to do real work.</p>
</blockquote>

<p>That’s not a Copilot. From a platform architect’s seat, that’s a new kind of workload identity problem. A new kind of audit trail problem. A new kind of supply chain question - because if my pipelines are now executing reasoning produced by an LLM that called MCP servers I didn’t write, what does provenance even mean here?</p>

<p>When Goose calls <code class="language-plaintext highlighter-rouge">kubectl exec</code> through an MCP server, the Kubernetes audit log records <em>my user identity</em> - not the agent’s, not the session’s, not the tool call that triggered it. There is no provenance connecting that API call back to the model reasoning that produced it. That’s the gap.</p>

<p>And the gap is no longer purely conceptual. The specs and tooling to close it have started landing - fast - and most of the platform community hasn’t caught up.</p>

<h2 id="what-the-last-six-months-brought">What the last six months brought</h2>

<p>Three things converged in the last six months that change how this conversation should be framed:</p>

<ol>
  <li>
    <p><strong>The Agentic AI Foundation formed</strong> at the Linux Foundation in December 2025, with platinum support from AWS, Anthropic, Block, Bloomberg, Cloudflare, Google, Microsoft, and OpenAI. MCP, Goose, and AGENTS.md are now under neutral governance. That removes the “which standard do we bet on” objection that was slowing enterprise adoption.</p>
  </li>
  <li>
    <p><strong>Agent identity moved from blog posts to IETF drafts.</strong> In March 2026, <code class="language-plaintext highlighter-rouge">draft-klrc-aiagent-auth-00</code> was published, composing the IETF WIMSE working group’s work with <a href="https://spiffe.io">SPIFFE</a> and OAuth 2.0 into a framework called AIMS (Agent Identity Management System). Separately, Dick Hardt (author of the original OAuth 2.0 RFC) has been developing <a href="https://aauth.dev">aauth.dev</a> - a specification that treats agents as first-class identities rather than as OAuth clients known at build time. The MCP authorization spec itself, built collaboratively with Anthropic, Microsoft, Okta/Auth0, Arcade.dev and others, now defines OAuth-style protected resources with audience binding via <a href="https://datatracker.ietf.org/doc/html/rfc8707">RFC 8707 resource indicators</a> and delegation via <a href="https://datatracker.ietf.org/doc/html/rfc8693">RFC 8693 token exchange</a>. That last bit matters a lot - and I’ll come back to why.</p>
  </li>
  <li>
    <p><strong>Microsoft shipped <a href="https://github.com/microsoft/apm">APM</a> - Agent Package Manager.</strong> One <code class="language-plaintext highlighter-rouge">apm.yml</code>, every harness, every machine. Lockfiles with content hashes. A policy file (<code class="language-plaintext highlighter-rouge">apm-policy.yml</code>) enforced at install time, including for transitive MCP servers. Tighten-only inheritance from enterprise to org to repo. This is the first piece of agent infrastructure that looks like it was designed by someone who has actually run an enterprise software supply chain program. It maps cleanly onto the SBOM / SLSA / in-toto thinking I’ve been doing for years - but applied to an entirely new artifact class: skills, prompts, plugins, and MCP server bindings.</p>
  </li>
  <li>
    <p><strong>The extension and skills layer is taking shape.</strong> The <a href="https://agentskills.io/specification">Agent Skills Specification</a> defines a standard for how agents declare, discover, and invoke reusable capabilities across harnesses. Alongside it, the <a href="https://open-plugins.com">Open Plugins</a> standard covers packaging and distribution - skills, commands, agents, hooks, and MCP server bindings as installable, namespaced artifacts with content-addressed lockfiles. Together they form a skills layer that sits between the protocol (MCP) and the identity layer (AIMS) - and it’s the missing middle of the emerging platform stack that neither MCP nor APM fully addresses on its own.</p>
  </li>
</ol>

<p>The conversation about agents is no longer “look what it can write.” It is: <em>how do we identify them, how do we package them, how do we govern them, and how do we keep them from setting our cloud bill on fire?</em> Those are platform engineering questions.</p>

<h2 id="how-i-got-here">How I got here</h2>

<p>A few years ago I started looking seriously at how AI was actually getting wired into enterprise SDLC pipelines. At Amdocs I integrated OpenAI’s GenAI into developer workflows and saw how quickly the conversation moves from “this is interesting” to “this is in production, who owns the failure modes?” I wrote about adjacent topics on LinkedIn in late 2024 - the CNCF End User Reference Architecture, NIST’s post-quantum cryptography work - but agentic AI specifically needed a different format. Long form, with code, configs, and references to the actual specifications. Somewhere I could think out loud.</p>

<p>So this site.</p>

<h2 id="what-im-planning-to-write">What I’m planning to write</h2>

<p>The questions cut across the whole agentic AI stack. Here are the threads, with the actual specifications and projects each one touches - because none of this exists in isolation anymore.</p>

<h3 id="identity-and-delegation-the-unsolved-problem">Identity and delegation: the unsolved problem</h3>

<p>OAuth 2.0 was designed for clients known at build time. Agents are not that. They make runtime decisions, spawn sub-agents, and traverse trust boundaries. The cracks are visible.</p>

<p>The interesting question isn’t “can we authenticate an agent.” <a href="https://spiffe.io">SPIFFE</a>/<a href="https://spiffe.io/docs/latest/spire-about/">SPIRE</a> solved workload identity for non-human callers years ago, and the CNCF’s 2026 recommendation for service-to-service auth is now explicitly <em>“SPIFFE for identity, OAuth 2.0 for access delegation, OPA for policy.”</em> The hard problem is <strong>delegation chains</strong>. When an orchestrator agent spawns a sub-agent that calls a tool that calls an external API, under whose authority is the final action taken? Does a user’s consent to the orchestrator flow automatically to the sub-agent? To what depth? Can a sub-agent end up with more permissions than its parent?</p>

<p>RFC 8693 token exchange supports nested delegation, but the IETF OAuth WG formally documented a delegation-chain-splicing weakness in early 2026, and the AIMS draft is solid on authentication but explicitly weaker on delegation. That gap is where the real platform-engineering work lives - and it’s where almost nothing is written from the perspective of someone who’s actually run an IAM program at scale. I want to write that.</p>

<h3 id="software-supply-chain-from-sbom-to-agent-manifests">Software supply chain: from SBOM to agent manifests</h3>

<p>I’ve spent the last year embedding SBOM (<a href="https://cyclonedx.org">CycloneDX</a>), <a href="https://slsa.dev">SLSA</a> provenance, and <a href="https://in-toto.io">in-toto</a> attestation into release pipelines at Netcracker, with measurable results - issue detection up to six months pre-release, CVE-driven redeployments cut in half. That mental model maps almost too cleanly onto Microsoft’s APM.</p>

<p>An <code class="language-plaintext highlighter-rouge">apm.yml</code> declares agent dependencies - skills, plugins, MCP servers, agent primitives - and the lockfile pins content hashes so a clone reproduces byte-for-byte. <code class="language-plaintext highlighter-rouge">apm-policy.yml</code> is enforced at install time, <em>including for transitive MCP servers</em>, with tighten-only inheritance. That last property is exactly the design pattern enterprise security teams asked for, and almost never got, in package manager history.</p>

<p>But agent manifests open new questions a normal SBOM doesn’t touch:</p>

<ul>
  <li>What’s the equivalent of SLSA build provenance for a <em>skill</em> - a file containing prompts and instructions that change agent behaviour?</li>
  <li>How do you attest the integrity of an MCP server when its surface area is a set of tools, each with permissions implications? CVE scanning doesn’t cover “this tool can <code class="language-plaintext highlighter-rouge">kubectl delete pod</code>.”</li>
  <li>Hidden-Unicode injection is now a baseline scan in APM. What’s the equivalent for skill instructions that don’t trigger normal lint?</li>
  <li>And the research is already pointing in uncomfortable directions: a 2026 study by Gloaguen et al. on 138 real-world repositories found that <em>LLM-generated AGENTS.md files reduce task success and inflate inference cost by over 20%</em>. The agent follows the file faithfully. If your supply chain ships skills generated by another LLM, your performance and cost regression isn’t a bug - it’s a property of the artifact.</li>
</ul>

<h3 id="cost-the-single-biggest-production-problem-in-2026">Cost: the single biggest production problem in 2026</h3>

<p>This is the question nobody was asking eighteen months ago and that everybody is asking now. Deloitte’s 2026 tokenomics work has finance leaders treating AI spend with the rigor they used to reserve for energy or capital. Gartner predicts more than 40% of agentic AI projects will be cancelled by the end of 2027 - and the cited reasons are cost, unclear business value, and inadequate risk controls. None of those are model problems.</p>

<p>The specific failure modes I’ve seen referenced in production retrospectives:</p>

<ul>
  <li><strong>Token maxing.</strong> Defaulting to the most capable model for every task, with no routing logic. One healthcare enterprise reportedly consumed a trillion tokens in six months - over $6M unplanned - before finance understood what was driving it.</li>
  <li><strong>Orchestration loops.</strong> A LangChain multi-agent system ran an infinite loop for eleven days before being noticed. The bill: $47,000.</li>
  <li><strong>Context bloat.</strong> Naive memory injection scales linearly; production traces hit 80-120K token contexts within two to three weeks.</li>
  <li><strong>Tool definition tax.</strong> MCP tool metadata can consume 40-50% of the context window on every call, regardless of relevance.</li>
</ul>

<p>The platform answer isn’t “pick a cheaper model.” It is structural: model routing layers, per-workflow token budgets, prompt caching for stable system prompts, hierarchical multi-agent topologies (frontier model for the orchestrator, cheap models for workers - published architectures hit ~97% of full-frontier accuracy at ~61% of the cost), and hard iteration caps on every agent loop.</p>

<p>This is where platform engineering earns its keep. <em>“Cost is an architectural concern, not an operational one”</em> - that’s the right frame, and it’s the natural extension of the FinOps work platform teams have already been doing on cloud spend.</p>

<h3 id="harness-engineering-the-new-layer">Harness engineering: the new layer</h3>

<p>This is the discipline that didn’t have a name two years ago.</p>

<p>In phase one (~2022-2023), the conversation was prompt engineering. In phase two (2024-2025), context engineering - feeding the model the right files, project rules, and architectural constraints. The 2026 conversation is <strong>harness engineering</strong>: the non-model runtime that wraps the model with tool orchestration, verification loops, context management, guardrails, and observability. Mitchell Hashimoto put it bluntly: <em>“Anytime you find an agent makes a mistake, you take the time to engineer a solution so that the agent never makes that mistake again.”</em> Most of the time, that solution lives in the harness, not in the model.</p>

<p>The implication is significant for platform teams. As frontier models converge in raw capability, competitive advantage shifts away from <em>which</em> model and toward <em>how good your harness is</em>. That’s good news - it means the model layer becomes commoditised, business logic and guardrail rules live in your harness code, and lock-in becomes manageable. It also means platform teams now own a new layer in the stack: the deterministic execution loop that wraps a non-deterministic component.</p>

<p>I’ll be writing about what that looks like - and where today’s harness designs still have unsolved problems, particularly around the inferential controls (LLM-as-judge) that don’t behave like the computational ones (linters, tests, type checkers) we know how to reason about.</p>

<h3 id="multi-agent-trust-and-a2a">Multi-agent trust and A2A</h3>

<p>Google’s Agent2Agent protocol, now under the Linux Foundation, defines how agents from different vendors discover each other (via <code class="language-plaintext highlighter-rouge">/.well-known/agent.json</code> Agent Cards) and exchange tasks over HTTP + JSON-RPC + SSE. MCP and A2A are complementary: MCP is how an agent connects to tools and data; A2A is how agents connect to other agents.</p>

<p>The trust questions multiply at A2A boundaries. When an Agent Card claims certain capabilities and authentication requirements, who attests that claim? When the orchestrator delegates to a third-party A2A agent that then calls its own MCP servers, what does the audit trail look like end-to-end? The Riptides work on SPIFFE-for-MCP and SPIFFE-for-A2A is one of the few honest attempts to answer this with a real implementation, not a slide deck - and it’s where I think the next two years of interesting standards work will happen.</p>

<h3 id="observability-for-agent-workflows">Observability for agent workflows</h3>

<p>Distributed tracing exists for microservices. What’s the equivalent for an agent that calls five tools, spawns a sub-task, modifies a file, and pauses for a six-hour async operation? OpenTelemetry has GenAI semantic conventions in progress, and a 2026 reverse-engineering of Claude Code’s architecture documented a five-stage progressive compaction strategy for context - that kind of internal state is mostly invisible to current tracing tools.</p>

<p>I led enterprise OpenTelemetry adoption at Amdocs and cut defect investigation time by 30% in microservices contexts. The agent-tracing problem is harder for reasons that should be intuitive to anyone who’s instrumented a stateful workload - but most of the published agent-observability writeups don’t engage with that depth.</p>

<h3 id="compliance-and-audit-at-scale">Compliance and audit at scale</h3>

<p>When agents run in production at volume, the audit trail question stops being theoretical. Today’s tooling still can’t tell you: which model reasoning produced which API call, under whose authorisation, with what context, through which chain of delegation. That answer exists for human users in mature IAM platforms. It does not exist for agents yet - not at the level of evidentiary quality a regulated industry actually needs. I want to map out what a real compliance story would look like, and what concrete things would need to change in the stack to support it.</p>

<h3 id="guardrails-layered-not-bolted-on">Guardrails: layered, not bolted on</h3>

<p>The framing I’ve found most useful, from the production retrospectives: <em>AI accuracy first, then layered guardrails matched to business risk.</em> Most teams add walls before optimising what the agent knows and how it reasons. That leads to brittle systems that are both expensive and unsafe.</p>

<p>The architecturally honest answer has three pillars: identity scoping (who/what can act, under whose delegation), runtime enforcement (policy at the gateway between agent and backend systems - OPA fits naturally), and behavioural monitoring (deviation detection over agent trajectories, not just output content filtering). Output filters like LlamaGuard solve a real but narrow problem; the harder one is <em>unreasonable but technically permitted</em> actions - what AgentDoG’s authors call the lack of agentic risk awareness in current guardrail models.</p>

<p>This is where platform teams and security teams need to converge, and it’s the cleanest argument I can make for why DevSecOps thinking maps onto agents better than pure ML-ops thinking does.</p>

<h3 id="real-ops-transcripts">Real ops transcripts</h3>

<p>Less abstract: incident triage sessions with Goose, including the parts where it was wrong and the parts where I had to override it. Configs that worked. Configs that broke production-adjacent things in interesting ways.</p>

<h3 id="platform-engineering-view">Platform engineering view</h3>

<p>How agentic AI fits into an Internal Developer Platform. What changes about your golden paths. What new capabilities you’d want to expose as self-service - agent provisioning with bounded identity, scoped MCP server access, cost budgets, audit policy templates - and what you absolutely should not.</p>

<h2 id="what-im-not-going-to-do">What I’m not going to do</h2>

<ul>
  <li>Write “the future of AI” essays. Other people are better at those.</li>
  <li>Pretend agents are magic. They are tools with failure modes. The failure modes are the interesting part.</li>
  <li>Ship demos that work in a sandbox and fall apart anywhere real.</li>
  <li>Treat the existing specs as either gospel or noise. AIMS, aauth.dev, the MCP auth spec, APM, A2A - each is a serious piece of work with real gaps. Engaging with them honestly is how the field gets better.</li>
  <li>Hide my mistakes. Twenty years in, I’ve made enough of them to know that writeups of failures are more useful than writeups of successes.</li>
</ul>

<h2 id="who-this-is-for">Who this is for</h2>

<p>If you’re a platform engineer, architect, SRE, or DevSecOps person - this is for you. If you care about IAM, supply chain security, zero-trust, FinOps, or how to build platforms real teams actually adopt, you’ll find things here that aren’t on the agentic-AI hype circuit. Agentic AI is the current thread, not the permanent scope. The underlying interests - IAM, zero-trust, software supply chain, observability, Internal Developer Platforms - have been the work for twenty years. Agents are just the current place where those questions are getting harder, faster, and more interesting.</p>

<p>If you’re an AI researcher or an app developer looking for hot takes on model capability, this is probably not your blog. There are better people writing for you.</p>

<p>If the perspective here - agents as workloads, not assistants - is one you haven’t seen framed this way before, that’s the opening thread. Next: how I scoped Goose for production-adjacent work, what I locked down before letting it touch anything, what the APM manifests and policy files actually look like, and where the AIMS-style identity model breaks down when you try to apply it to a real delegation chain.</p>

<p>Onward.</p>]]></content><author><name>Abhishek Dadhich</name></author><category term="platform-engineering" /><category term="agentic-ai" /><category term="devsecops" /><category term="iam" /><category term="supply-chain" /><category term="mcp" /><category term="a2a" /><category term="harness-engineering" /><summary type="html"><![CDATA[Why a Cloud, Platform & Security Architect is writing about agentic AI - and what the current wave of writing on Goose, MCP, A2A, APM, and AGENTS.md still mostly misses.]]></summary></entry></feed>