<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: T. Devon Artis</title>
    <description>The latest articles on DEV Community by T. Devon Artis (@t_devonartis_c9f34e5721).</description>
    <link>https://dev.to/t_devonartis_c9f34e5721</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2227388%2F7623c2a0-cdcc-4d34-81ad-e6ccfd10c64a.jpg</url>
      <title>DEV Community: T. Devon Artis</title>
      <link>https://dev.to/t_devonartis_c9f34e5721</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/t_devonartis_c9f34e5721"/>
    <language>en</language>
    <item>
      <title>Stop Credentialing Your AI Agents Like It's 2019</title>
      <dc:creator>T. Devon Artis</dc:creator>
      <pubDate>Wed, 06 May 2026 22:00:23 +0000</pubDate>
      <link>https://dev.to/t_devonartis_c9f34e5721/stop-credentialing-your-ai-agents-like-its-2019-ec9</link>
      <guid>https://dev.to/t_devonartis_c9f34e5721/stop-credentialing-your-ai-agents-like-its-2019-ec9</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Your agent lives for 2 minutes. Its credential lives for 60. That mismatch is your attack surface. A broker that issues task-scoped, short-lived credentials closes the gap before the sprawl starts.&lt;/p&gt;




&lt;p&gt;AI agents are still new. Most teams are just now deploying their first agents at scale. 2026 is year one. And a lot of the identity conversation already assumes the mess exists: registries, inventories, entitlement reviews, cleanup workflows.&lt;/p&gt;

&lt;p&gt;But the mess is not inevitable. It's a choice you make at the beginning.&lt;/p&gt;

&lt;p&gt;If you start with a broker where every agent gets a short-lived, task-scoped credential at spawn time, the individual agent credential doesn't have to become another long-lived thing you track forever.&lt;/p&gt;

&lt;p&gt;This is the prevention argument: govern the things that persist, but issue ephemeral credentials to the things that don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Right now, most teams are credentialing their agents one of three ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shared service account with a static API key.&lt;/strong&gt; Every agent uses the same key. When one gets compromised, you rotate the key and everything breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth token with a 15-60 minute TTL.&lt;/strong&gt; The agent runs for a short task, but the credential stays valid much longer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broad IAM role assigned "just in case."&lt;/strong&gt; Scoped wide enough to handle every possible task. When an agent gets compromised, it has access to everything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The common thread: &lt;strong&gt;credentials outlive the work.&lt;/strong&gt; The agent is ephemeral. The credential is not. That mismatch is your attack surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Math on Credential Exposure
&lt;/h2&gt;

&lt;p&gt;Let's make it concrete.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Agent Lifetime&lt;/th&gt;
&lt;th&gt;Credential Lifetime&lt;/th&gt;
&lt;th&gt;Exposure Window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Static API key&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;td&gt;Forever&lt;/td&gt;
&lt;td&gt;Forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth token&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;td&gt;60 minutes&lt;/td&gt;
&lt;td&gt;30x agent lifetime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broker (task-scoped)&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;td&gt;Short TTL + release/revocation&lt;/td&gt;
&lt;td&gt;Close to task lifetime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At scale, the difference is not academic. The exact numbers depend on your workload, TTLs, and renewal policy, but the shape of the risk is the same.&lt;/p&gt;

&lt;p&gt;Every 2-minute agent task backed by a 60-minute token leaves 58 extra minutes where a stolen credential is still useful. Multiply that across thousands of agent runs and you're generating a massive amount of unnecessary credential lifetime every single day.&lt;/p&gt;

&lt;p&gt;When a credential gets stolen, the attacker doesn't get access to what the agent was doing. They get access to everything that credential &lt;em&gt;could&lt;/em&gt; do, for as long as it stays valid.&lt;/p&gt;




&lt;h2&gt;
  
  
  Broker vs. Registry: Two Philosophies
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Registry model:&lt;/strong&gt; Persistent systems, applications, owners, policies, and audit trails get registered and governed. That's useful. But if every short-lived agent instance also becomes a persistent identity record, you accumulate thousands of identities, entitlements, and cleanup tasks.&lt;/p&gt;

&lt;p&gt;At that point, the registry's value proposition becomes "we'll help you manage the sprawl."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broker model:&lt;/strong&gt; Every agent gets a credential at spawn. The credential is scoped to exactly what that task needs. It has a short TTL and can be released or revoked when the work is done. The persistent governance layer still exists above the agent, but the per-agent credential doesn't become a standing entitlement.&lt;/p&gt;

&lt;p&gt;The broker assumes at least some sprawl is preventable. Its value proposition is "don't create long-lived agent credentials in the first place."&lt;/p&gt;

&lt;p&gt;Prevention is usually cheaper than cleanup. Fewer stale identities. Fewer periodic access reviews. Fewer "why did this old agent still have access?" incidents.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Looks Like in Code
&lt;/h2&gt;

&lt;p&gt;Same agent, same system prompt, same LLM, same decision. The only thing that changes is the credential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All three examples start here:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# The system prompt defines what this agent is and what tools it can call.
&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a customer support agent. You have these tools:
- lookup_billing: Fetch billing history for a customer
- edit_account: Edit a customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s account info
- lookup_billing_all: Fetch billing history across all customers
Use the appropriate tool based on the customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s request.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# A request arrives. The LLM decides what tool to call.
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the billing history for customer 12345?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# lookup_billing, edit_account, lookup_billing_all
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The LLM chose lookup_billing for customer 12345.
# Extract the customer_id from the LLM's decision.
&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# "12345"
&lt;/span&gt;
&lt;span class="c1"&gt;# Now: how does the agent get the credential to make that call?
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Static API key (what most teams do today):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Same LLM decision. Same customer_id extracted above.
# The credential is a shared key baked into the environment.
&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SHARED_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/customers/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# This works. But that same key could also call:
# requests.get("https://api.example.com/customers", headers=headers)
# ...and pull EVERY customer. The key doesn't know or care
# that the LLM only asked for one. And it never expires.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;OAuth token (better, but still mismatched):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Same LLM decision. Same customer_id.
# We know we only need one customer. But OAuth scopes are defined
# at the client level, not per-task. You can't mint a token for
# "just customer 12345" without a separate OAuth client per customer.
&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_oauth_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                        &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                        &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read:customers:*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# broad because it has to be
&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/customers/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# This also works. But the token can read ALL customers,
# and it's valid for 60 minutes. Agent is done in 2.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Broker (task-scoped, ephemeral):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agentwrit&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentWritApp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agentwrit.errors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AuthorizationError&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentWritApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;broker_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENTWRIT_BROKER_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENTWRIT_CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENTWRIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Same LLM decision. Same customer_id.
# But now the credential is scoped to exactly what the LLM asked for.
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;orch_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;requested_scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read:data:customer-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;AuthorizationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Broker says no. Scope exceeds what this app is allowed to issue.
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# "scope exceeds app ceiling"
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# "scope_violation"
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt;

&lt;span class="c1"&gt;# Verify the token through the app. Returns the broker's signed claims.
&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ['read:data:customer-12345'] -- nothing else
&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/customers/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bearer_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# This works. But if something tries to pull ALL customers
# with this token, the API checks the scope and rejects it.
&lt;/span&gt;
&lt;span class="c1"&gt;# Done. Kill the token at the broker. Right now. Not in 58 minutes.
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three examples use the same LLM, the same system prompt, the same tool call, the same customer_id. The code that talks to the API is nearly identical. The difference is the credential.&lt;/p&gt;

&lt;p&gt;With the static key and the OAuth token, the developer already knows they only need customer 12345. But the credential can't enforce that. It's too broad to scope per-task, and there's no mechanism to narrow it at runtime.&lt;/p&gt;

&lt;p&gt;With the broker, the credential is built from the LLM's actual decision. &lt;code&gt;customer_id&lt;/code&gt; flows directly into &lt;code&gt;requested_scope&lt;/code&gt;. The token can only do what the LLM asked for, nothing more. If the LLM had picked a different tool or a different customer, the scope would have been different. And if the requested scope exceeds what the app is allowed to issue, the broker rejects it before the agent touches any data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-Agent Delegation: The Attack Vector Nobody Is Talking About
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting.&lt;/p&gt;

&lt;p&gt;Most serious agent deployments use multiple agents working together. Agent A researches. Agent B drafts. Agent C reviews. Agent D publishes. The output of one agent becomes the input of the next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; How does Agent A give Agent B permission to act on its behalf?&lt;/p&gt;

&lt;p&gt;The naive approach: Agent A shares its credential with Agent B. Now Agent B has Agent A's permissions. If Agent A could read all customers, so can Agent B. Permissions expanded. This is credential escalation, and it's trivially easy in most agent architectures.&lt;/p&gt;

&lt;p&gt;The registry-only approach: Agent B gets its own standing identity and permissions, and you rely on governance later to prove that those permissions are still correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The broker approach: delegation chain verification.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When Agent A delegates to Agent B, it passes a token that says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Agent A authorized Agent B to act on its behalf, with scope exactly equal to Agent A's current scope. Agent B cannot escalate. The delegation is cryptographically signed and time-bounded."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If Agent A had &lt;code&gt;read:data:customer-12345&lt;/code&gt;, Agent B gets &lt;code&gt;read:data:customer-12345&lt;/code&gt;. Not &lt;code&gt;read:data:*&lt;/code&gt;. Not &lt;code&gt;write:data:customer-12345&lt;/code&gt;. Exactly what Agent A had, nothing more.&lt;/p&gt;

&lt;p&gt;The delegation chain is a series of signed tokens. Each link is bound to the previous one, so resource layers can verify the lineage instead of treating each delegated token as unrelated.&lt;/p&gt;

&lt;p&gt;This isn't just a feature. It's the security property I care about most: delegation should preserve or reduce authority, never expand it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Now
&lt;/h2&gt;

&lt;p&gt;2026 is year one for agent deployment at scale. Most teams are figuring this out right now. The architectural decisions made in the next 12 months will persist for years.&lt;/p&gt;

&lt;p&gt;If you bake in long-lived agent credentials today, you'll spend the next two years cleaning them up. Access reviews. Entitlement audits. "Who had access to what when" forensics. Or it just doesn't get cleaned up at all. The enterprise vendors will sell you tools to manage the mess, because the mess will be real.&lt;/p&gt;

&lt;p&gt;If you start with a broker, you still need governance for the persistent systems around the agent. But the short-lived agent instance doesn't need to leave behind a standing credential.&lt;/p&gt;

&lt;p&gt;The registry vendors aren't wrong that sprawl is a problem. I think they're too quick to assume all of it is inevitable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Argument, Not the Pitch
&lt;/h2&gt;

&lt;p&gt;I'm not here to sell you AgentWrit. I'm here to argue that the credential model you choose today determines your security posture for the next five years.&lt;/p&gt;

&lt;p&gt;If you start with long-lived credentials and registry-managed sprawl, you're choosing a future of cleanup, audits, and accumulated risk.&lt;/p&gt;

&lt;p&gt;If you start with ephemeral, task-scoped credentials, you're choosing a future where credentials don't outlive the work, where delegation doesn't escalate, and where the individual agent instance doesn't become a permanent entitlement.&lt;/p&gt;

&lt;p&gt;The broker model isn't new. It's how cloud-native systems have handled short-lived compute for years. VMs get credentials at boot. Containers get credentials at start. Serverless functions get credentials per invocation. The credential dies with the compute.&lt;/p&gt;

&lt;p&gt;Agents are just compute that happens to be intelligent. The same principle applies.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built AgentWrit because I needed this for my own agent deployments. It's a self-hosted credential broker for AI agents, source-available under PolyForm Internal Use for internal deployments.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ephemeral identity:&lt;/strong&gt; Every agent spawns with a unique cryptographic identity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task-scoped tokens:&lt;/strong&gt; Scoped to exactly what the task needs, not broad IAM roles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short-lived credentials:&lt;/strong&gt; Tokens expire in minutes and can be released or revoked early&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four-level revocation:&lt;/strong&gt; Token, agent, task, or full delegation chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegation chain verification:&lt;/strong&gt; Permissions cannot expand at each hop, cryptographically enforced&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's written in Go. Runs with Docker. The broker is source-available under PolyForm Internal Use 1.0.0; the Python SDK is MIT-licensed and live on PyPI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/devonartis/agentwrit" rel="noopener noreferrer"&gt;https://github.com/devonartis/agentwrit&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Python SDK:&lt;/strong&gt; &lt;a href="https://github.com/devonartis/agentwrit-python" rel="noopener noreferrer"&gt;https://github.com/devonartis/agentwrit-python&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Security pattern (CC BY-SA 4.0):&lt;/strong&gt; &lt;a href="https://github.com/devonartis/AI-Security-Blueprints" rel="noopener noreferrer"&gt;https://github.com/devonartis/AI-Security-Blueprints&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pattern is aligned with OWASP Agentic Top 10 (2026), NIST IR 8596, and IETF WIMSE. It's published separately because the architecture matters more than any single implementation.&lt;/p&gt;

&lt;p&gt;The full security architecture is also published as a preprint on &lt;a href="https://zenodo.org/records/19713391" rel="noopener noreferrer"&gt;Zenodo&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It in 10 Minutes
&lt;/h2&gt;

&lt;p&gt;Pull the broker Docker image, install the Python SDK, and run one of the two demos end to end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MedAssist&lt;/strong&gt; is a FastAPI clinical assistant. You ask a plain-language question about a patient; an LLM picks tools (records, labs, billing, prescriptions); the app spawns broker agents on demand, each scoped to one patient and one category. Cross-patient questions are denied. Prescription writes flow through a delegation chain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/devonartis/agentwrit-python/blob/main/demo/README.md" rel="noopener noreferrer"&gt;demo/README.md&lt;/a&gt; has run instructions, a scenario playbook, and a code map.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support Tickets&lt;/strong&gt; is a three-agent pipeline built with Flask + HTMX + SSE. Three LLM-driven agents (triage, knowledge, response) process customer tickets. Anonymous tickets halt at triage. Dangerous tools like &lt;code&gt;delete_account&lt;/code&gt; and &lt;code&gt;send_external_email&lt;/code&gt; are in the LLM's tool list but not in the agent's scope, so they never execute. One scenario deliberately skips &lt;code&gt;release()&lt;/code&gt; to watch a 5-second TTL die on its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/devonartis/agentwrit-python/blob/main/demo2/README.md" rel="noopener noreferrer"&gt;demo2/README.md&lt;/a&gt; has run instructions, five scenarios, and a code map.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Question
&lt;/h2&gt;

&lt;p&gt;How are you credentialing your agents today?&lt;/p&gt;

&lt;p&gt;If the answer involves shared API keys, long-lived OAuth tokens, or broad IAM roles, you might be building the mess that registry vendors will later sell you tools to manage.&lt;/p&gt;

&lt;p&gt;Start with prevention. It's cheaper to avoid standing agent credentials than to clean them up later.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Devon Artis. Principal Security Engineer. CSA AI Controls Matrix contributor. Published the Ephemeral Agent Credentialing pattern as a &lt;a href="https://zenodo.org/records/19713391" rel="noopener noreferrer"&gt;preprint on Zenodo&lt;/a&gt;. Building AgentWrit. One person, no VC.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
      <category>cloudnative</category>
    </item>
    <item>
      <title>Stop Credentialing Your AI Agents Like It's 2019</title>
      <dc:creator>T. Devon Artis</dc:creator>
      <pubDate>Wed, 06 May 2026 21:48:21 +0000</pubDate>
      <link>https://dev.to/t_devonartis_c9f34e5721/stop-credentialing-your-ai-agents-like-its-2019-bi2</link>
      <guid>https://dev.to/t_devonartis_c9f34e5721/stop-credentialing-your-ai-agents-like-its-2019-bi2</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Your agent lives for 2 minutes. Its credential lives for 60. That mismatch is your attack surface. A broker that issues task-scoped, short-lived credentials closes the gap before the sprawl starts.&lt;/p&gt;




&lt;p&gt;AI agents are still new. Most teams are just now deploying their first agents at scale. 2026 is year one. And a lot of the identity conversation already assumes the mess exists: registries, inventories, entitlement reviews, cleanup workflows.&lt;/p&gt;

&lt;p&gt;But the mess is not inevitable. It's a choice you make at the beginning.&lt;/p&gt;

&lt;p&gt;If you start with a broker where every agent gets a short-lived, task-scoped credential at spawn time, the individual agent credential doesn't have to become another long-lived thing you track forever.&lt;/p&gt;

&lt;p&gt;This is the prevention argument: govern the things that persist, but issue ephemeral credentials to the things that don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Right now, most teams are credentialing their agents one of three ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shared service account with a static API key.&lt;/strong&gt; Every agent uses the same key. When one gets compromised, you rotate the key and everything breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth token with a 15-60 minute TTL.&lt;/strong&gt; The agent runs for a short task, but the credential stays valid much longer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broad IAM role assigned "just in case."&lt;/strong&gt; Scoped wide enough to handle every possible task. When an agent gets compromised, it has access to everything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The common thread: &lt;strong&gt;credentials outlive the work.&lt;/strong&gt; The agent is ephemeral. The credential is not. That mismatch is your attack surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Math on Credential Exposure
&lt;/h2&gt;

&lt;p&gt;Let's make it concrete.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Agent Lifetime&lt;/th&gt;
&lt;th&gt;Credential Lifetime&lt;/th&gt;
&lt;th&gt;Exposure Window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Static API key&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;td&gt;Forever&lt;/td&gt;
&lt;td&gt;Forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth token&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;td&gt;60 minutes&lt;/td&gt;
&lt;td&gt;30x agent lifetime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broker (task-scoped)&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;td&gt;Short TTL + release/revocation&lt;/td&gt;
&lt;td&gt;Close to task lifetime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At scale, the difference is not academic. The exact numbers depend on your workload, TTLs, and renewal policy, but the shape of the risk is the same.&lt;/p&gt;

&lt;p&gt;Every 2-minute agent task backed by a 60-minute token leaves 58 extra minutes where a stolen credential is still useful. Multiply that across thousands of agent runs and you're generating a massive amount of unnecessary credential lifetime every single day.&lt;/p&gt;

&lt;p&gt;When a credential gets stolen, the attacker doesn't get access to what the agent was doing. They get access to everything that credential &lt;em&gt;could&lt;/em&gt; do, for as long as it stays valid.&lt;/p&gt;




&lt;h2&gt;
  
  
  Broker vs. Registry: Two Philosophies
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Registry model:&lt;/strong&gt; Persistent systems, applications, owners, policies, and audit trails get registered and governed. That's useful. But if every short-lived agent instance also becomes a persistent identity record, you accumulate thousands of identities, entitlements, and cleanup tasks.&lt;/p&gt;

&lt;p&gt;At that point, the registry's value proposition becomes "we'll help you manage the sprawl."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broker model:&lt;/strong&gt; Every agent gets a credential at spawn. The credential is scoped to exactly what that task needs. It has a short TTL and can be released or revoked when the work is done. The persistent governance layer still exists above the agent, but the per-agent credential doesn't become a standing entitlement.&lt;/p&gt;

&lt;p&gt;The broker assumes at least some sprawl is preventable. Its value proposition is "don't create long-lived agent credentials in the first place."&lt;/p&gt;

&lt;p&gt;Prevention is usually cheaper than cleanup. Fewer stale identities. Fewer periodic access reviews. Fewer "why did this old agent still have access?" incidents.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Looks Like in Code
&lt;/h2&gt;

&lt;p&gt;Same agent, same system prompt, same LLM, same decision. The only thing that changes is the credential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All three examples start here:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# The system prompt defines what this agent is and what tools it can call.
&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a customer support agent. You have these tools:
- lookup_billing: Fetch billing history for a customer
- edit_account: Edit a customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s account info
- lookup_billing_all: Fetch billing history across all customers
Use the appropriate tool based on the customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s request.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# A request arrives. The LLM decides what tool to call.
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the billing history for customer 12345?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# lookup_billing, edit_account, lookup_billing_all
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The LLM chose lookup_billing for customer 12345.
# Extract the customer_id from the LLM's decision.
&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# "12345"
&lt;/span&gt;
&lt;span class="c1"&gt;# Now: how does the agent get the credential to make that call?
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Static API key (what most teams do today):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Same LLM decision. Same customer_id extracted above.
# The credential is a shared key baked into the environment.
&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SHARED_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/customers/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# This works. But that same key could also call:
# requests.get("https://api.example.com/customers", headers=headers)
# ...and pull EVERY customer. The key doesn't know or care
# that the LLM only asked for one. And it never expires.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;OAuth token (better, but still mismatched):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Same LLM decision. Same customer_id.
# We know we only need one customer. But OAuth scopes are defined
# at the client level, not per-task. You can't mint a token for
# "just customer 12345" without a separate OAuth client per customer.
&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_oauth_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                        &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                        &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read:customers:*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# broad because it has to be
&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/customers/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# This also works. But the token can read ALL customers,
# and it's valid for 60 minutes. Agent is done in 2.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Broker (task-scoped, ephemeral):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agentwrit&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentWritApp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agentwrit.errors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AuthorizationError&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentWritApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;broker_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENTWRIT_BROKER_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENTWRIT_CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENTWRIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Same LLM decision. Same customer_id.
# But now the credential is scoped to exactly what the LLM asked for.
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;orch_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;requested_scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read:data:customer-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;AuthorizationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Broker says no. Scope exceeds what this app is allowed to issue.
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# "scope exceeds app ceiling"
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# "scope_violation"
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt;

&lt;span class="c1"&gt;# Verify the token through the app. Returns the broker's signed claims.
&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ['read:data:customer-12345'] -- nothing else
&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/customers/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bearer_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# This works. But if something tries to pull ALL customers
# with this token, the API checks the scope and rejects it.
&lt;/span&gt;
&lt;span class="c1"&gt;# Done. Kill the token at the broker. Right now. Not in 58 minutes.
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three examples use the same LLM, the same system prompt, the same tool call, the same customer_id. The code that talks to the API is nearly identical. The difference is the credential.&lt;/p&gt;

&lt;p&gt;With the static key and the OAuth token, the developer already knows they only need customer 12345. But the credential can't enforce that. It's too broad to scope per-task, and there's no mechanism to narrow it at runtime.&lt;/p&gt;

&lt;p&gt;With the broker, the credential is built from the LLM's actual decision. &lt;code&gt;customer_id&lt;/code&gt; flows directly into &lt;code&gt;requested_scope&lt;/code&gt;. The token can only do what the LLM asked for, nothing more. If the LLM had picked a different tool or a different customer, the scope would have been different. And if the requested scope exceeds what the app is allowed to issue, the broker rejects it before the agent touches any data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-Agent Delegation: The Attack Vector Nobody Is Talking About
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting.&lt;/p&gt;

&lt;p&gt;Most serious agent deployments use multiple agents working together. Agent A researches. Agent B drafts. Agent C reviews. Agent D publishes. The output of one agent becomes the input of the next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; How does Agent A give Agent B permission to act on its behalf?&lt;/p&gt;

&lt;p&gt;The naive approach: Agent A shares its credential with Agent B. Now Agent B has Agent A's permissions. If Agent A could read all customers, so can Agent B. Permissions expanded. This is credential escalation, and it's trivially easy in most agent architectures.&lt;/p&gt;

&lt;p&gt;The registry-only approach: Agent B gets its own standing identity and permissions, and you rely on governance later to prove that those permissions are still correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The broker approach: delegation chain verification.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When Agent A delegates to Agent B, it passes a token that says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Agent A authorized Agent B to act on its behalf, with scope exactly equal to Agent A's current scope. Agent B cannot escalate. The delegation is cryptographically signed and time-bounded."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If Agent A had &lt;code&gt;read:data:customer-12345&lt;/code&gt;, Agent B gets &lt;code&gt;read:data:customer-12345&lt;/code&gt;. Not &lt;code&gt;read:data:*&lt;/code&gt;. Not &lt;code&gt;write:data:customer-12345&lt;/code&gt;. Exactly what Agent A had, nothing more.&lt;/p&gt;

&lt;p&gt;The delegation chain is a series of signed tokens. Each link is bound to the previous one, so resource layers can verify the lineage instead of treating each delegated token as unrelated.&lt;/p&gt;

&lt;p&gt;This isn't just a feature. It's the security property I care about most: delegation should preserve or reduce authority, never expand it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Now
&lt;/h2&gt;

&lt;p&gt;2026 is year one for agent deployment at scale. Most teams are figuring this out right now. The architectural decisions made in the next 12 months will persist for years.&lt;/p&gt;

&lt;p&gt;If you bake in long-lived agent credentials today, you'll spend the next two years cleaning them up. Access reviews. Entitlement audits. "Who had access to what when" forensics. Or it just doesn't get cleaned up at all. The enterprise vendors will sell you tools to manage the mess, because the mess will be real.&lt;/p&gt;

&lt;p&gt;If you start with a broker, you still need governance for the persistent systems around the agent. But the short-lived agent instance doesn't need to leave behind a standing credential.&lt;/p&gt;

&lt;p&gt;The registry vendors aren't wrong that sprawl is a problem. I think they're too quick to assume all of it is inevitable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Argument, Not the Pitch
&lt;/h2&gt;

&lt;p&gt;I'm not here to sell you AgentWrit. I'm here to argue that the credential model you choose today determines your security posture for the next five years.&lt;/p&gt;

&lt;p&gt;If you start with long-lived credentials and registry-managed sprawl, you're choosing a future of cleanup, audits, and accumulated risk.&lt;/p&gt;

&lt;p&gt;If you start with ephemeral, task-scoped credentials, you're choosing a future where credentials don't outlive the work, where delegation doesn't escalate, and where the individual agent instance doesn't become a permanent entitlement.&lt;/p&gt;

&lt;p&gt;The broker model isn't new. It's how cloud-native systems have handled short-lived compute for years. VMs get credentials at boot. Containers get credentials at start. Serverless functions get credentials per invocation. The credential dies with the compute.&lt;/p&gt;

&lt;p&gt;Agents are just compute that happens to be intelligent. The same principle applies.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built AgentWrit because I needed this for my own agent deployments. It's a self-hosted credential broker for AI agents, source-available under PolyForm Internal Use for internal deployments.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ephemeral identity:&lt;/strong&gt; Every agent spawns with a unique cryptographic identity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task-scoped tokens:&lt;/strong&gt; Scoped to exactly what the task needs, not broad IAM roles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short-lived credentials:&lt;/strong&gt; Tokens expire in minutes and can be released or revoked early&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four-level revocation:&lt;/strong&gt; Token, agent, task, or full delegation chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegation chain verification:&lt;/strong&gt; Permissions cannot expand at each hop, cryptographically enforced&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's written in Go. Runs with Docker. The broker is source-available under PolyForm Internal Use 1.0.0; the Python SDK is MIT-licensed and live on PyPI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/devonartis/agentwrit" rel="noopener noreferrer"&gt;https://github.com/devonartis/agentwrit&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Python SDK:&lt;/strong&gt; &lt;a href="https://github.com/devonartis/agentwrit-python" rel="noopener noreferrer"&gt;https://github.com/devonartis/agentwrit-python&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Security pattern (CC BY-SA 4.0):&lt;/strong&gt; &lt;a href="https://github.com/devonartis/AI-Security-Blueprints" rel="noopener noreferrer"&gt;https://github.com/devonartis/AI-Security-Blueprints&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pattern is aligned with OWASP Agentic Top 10 (2026), NIST IR 8596, and IETF WIMSE. It's published separately because the architecture matters more than any single implementation.&lt;/p&gt;

&lt;p&gt;The full security architecture is also published as a preprint on &lt;a href="https://zenodo.org/records/19713391" rel="noopener noreferrer"&gt;Zenodo&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It in 10 Minutes
&lt;/h2&gt;

&lt;p&gt;Pull the broker Docker image, install the Python SDK, and run one of the two demos end to end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MedAssist&lt;/strong&gt; is a FastAPI clinical assistant. You ask a plain-language question about a patient; an LLM picks tools (records, labs, billing, prescriptions); the app spawns broker agents on demand, each scoped to one patient and one category. Cross-patient questions are denied. Prescription writes flow through a delegation chain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/devonartis/agentwrit-python/blob/main/demo/README.md" rel="noopener noreferrer"&gt;demo/README.md&lt;/a&gt; has run instructions, a scenario playbook, and a code map.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support Tickets&lt;/strong&gt; is a three-agent pipeline built with Flask + HTMX + SSE. Three LLM-driven agents (triage, knowledge, response) process customer tickets. Anonymous tickets halt at triage. Dangerous tools like &lt;code&gt;delete_account&lt;/code&gt; and &lt;code&gt;send_external_email&lt;/code&gt; are in the LLM's tool list but not in the agent's scope, so they never execute. One scenario deliberately skips &lt;code&gt;release()&lt;/code&gt; to watch a 5-second TTL die on its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/devonartis/agentwrit-python/blob/main/demo2/README.md" rel="noopener noreferrer"&gt;demo2/README.md&lt;/a&gt; has run instructions, five scenarios, and a code map.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Question
&lt;/h2&gt;

&lt;p&gt;How are you credentialing your agents today?&lt;/p&gt;

&lt;p&gt;If the answer involves shared API keys, long-lived OAuth tokens, or broad IAM roles, you might be building the mess that registry vendors will later sell you tools to manage.&lt;/p&gt;

&lt;p&gt;Start with prevention. It's cheaper to avoid standing agent credentials than to clean them up later.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Devon Artis. Principal Security Engineer. CSA AI Controls Matrix contributor. Published the Ephemeral Agent Credentialing pattern as a &lt;a href="https://zenodo.org/records/19713391" rel="noopener noreferrer"&gt;preprint on Zenodo&lt;/a&gt;. Building AgentWrit. One person, no VC.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
      <category>cloudnative</category>
    </item>
    <item>
      <title>Stop Credentialing Your AI Agents Like It's 2019</title>
      <dc:creator>T. Devon Artis</dc:creator>
      <pubDate>Wed, 06 May 2026 20:37:34 +0000</pubDate>
      <link>https://dev.to/t_devonartis_c9f34e5721/stop-credentialing-your-ai-agents-like-its-2019-i3l</link>
      <guid>https://dev.to/t_devonartis_c9f34e5721/stop-credentialing-your-ai-agents-like-its-2019-i3l</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Your agent lives for 2 minutes. Its credential lives for 60. That mismatch is your attack surface. A broker that issues task-scoped, short-lived credentials closes the gap before the sprawl starts.&lt;/p&gt;




&lt;p&gt;AI agents are still new. Most teams are just now deploying their first agents at scale. 2026 is year one. And a lot of the identity conversation already assumes the mess exists: registries, inventories, entitlement reviews, cleanup workflows.&lt;/p&gt;

&lt;p&gt;But the mess is not inevitable. It's a choice you make at the beginning.&lt;/p&gt;

&lt;p&gt;If you start with a broker where every agent gets a short-lived, task-scoped credential at spawn time, the individual agent credential doesn't have to become another long-lived thing you track forever.&lt;/p&gt;

&lt;p&gt;This is the prevention argument: govern the things that persist, but issue ephemeral credentials to the things that don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Right now, most teams are credentialing their agents one of three ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shared service account with a static API key.&lt;/strong&gt; Every agent uses the same key. When one gets compromised, you rotate the key and everything breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth token with a 15-60 minute TTL.&lt;/strong&gt; The agent runs for a short task, but the credential stays valid much longer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broad IAM role assigned "just in case."&lt;/strong&gt; Scoped wide enough to handle every possible task. When an agent gets compromised, it has access to everything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The common thread: &lt;strong&gt;credentials outlive the work.&lt;/strong&gt; The agent is ephemeral. The credential is not. That mismatch is your attack surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Math on Credential Exposure
&lt;/h2&gt;

&lt;p&gt;Let's make it concrete.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Agent Lifetime&lt;/th&gt;
&lt;th&gt;Credential Lifetime&lt;/th&gt;
&lt;th&gt;Exposure Window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Static API key&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;td&gt;Forever&lt;/td&gt;
&lt;td&gt;Forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth token&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;td&gt;60 minutes&lt;/td&gt;
&lt;td&gt;30x agent lifetime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broker (task-scoped)&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;td&gt;Short TTL + release/revocation&lt;/td&gt;
&lt;td&gt;Close to task lifetime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At scale, the difference is not academic. The exact numbers depend on your workload, TTLs, and renewal policy, but the shape of the risk is the same.&lt;/p&gt;

&lt;p&gt;Every 2-minute agent task backed by a 60-minute token leaves 58 extra minutes where a stolen credential is still useful. Multiply that across thousands of agent runs and you're generating a massive amount of unnecessary credential lifetime every single day.&lt;/p&gt;

&lt;p&gt;When a credential gets stolen, the attacker doesn't get access to what the agent was doing. They get access to everything that credential &lt;em&gt;could&lt;/em&gt; do, for as long as it stays valid.&lt;/p&gt;




&lt;h2&gt;
  
  
  Broker vs. Registry: Two Philosophies
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Registry model:&lt;/strong&gt; Persistent systems, applications, owners, policies, and audit trails get registered and governed. That's useful. But if every short-lived agent instance also becomes a persistent identity record, you accumulate thousands of identities, entitlements, and cleanup tasks.&lt;/p&gt;

&lt;p&gt;At that point, the registry's value proposition becomes "we'll help you manage the sprawl."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broker model:&lt;/strong&gt; Every agent gets a credential at spawn. The credential is scoped to exactly what that task needs. It has a short TTL and can be released or revoked when the work is done. The persistent governance layer still exists above the agent, but the per-agent credential doesn't become a standing entitlement.&lt;/p&gt;

&lt;p&gt;The broker assumes at least some sprawl is preventable. Its value proposition is "don't create long-lived agent credentials in the first place."&lt;/p&gt;

&lt;p&gt;Prevention is usually cheaper than cleanup. Fewer stale identities. Fewer periodic access reviews. Fewer "why did this old agent still have access?" incidents.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Looks Like in Code
&lt;/h2&gt;

&lt;p&gt;Same agent, same system prompt, same LLM, same decision. The only thing that changes is the credential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All three examples start here:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# The system prompt defines what this agent is and what tools it can call.
&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a customer support agent. You have these tools:
- lookup_billing: Fetch billing history for a customer
- edit_account: Edit a customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s account info
- lookup_billing_all: Fetch billing history across all customers
Use the appropriate tool based on the customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s request.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# A request arrives. The LLM decides what tool to call.
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the billing history for customer 12345?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# lookup_billing, edit_account, lookup_billing_all
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The LLM chose lookup_billing for customer 12345.
# Extract the customer_id from the LLM's decision.
&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# "12345"
&lt;/span&gt;
&lt;span class="c1"&gt;# Now: how does the agent get the credential to make that call?
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Static API key (what most teams do today):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Same LLM decision. Same customer_id extracted above.
# The credential is a shared key baked into the environment.
&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SHARED_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/customers/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# This works. But that same key could also call:
# requests.get("https://api.example.com/customers", headers=headers)
# ...and pull EVERY customer. The key doesn't know or care
# that the LLM only asked for one. And it never expires.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;OAuth token (better, but still mismatched):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Same LLM decision. Same customer_id.
# We know we only need one customer. But OAuth scopes are defined
# at the client level, not per-task. You can't mint a token for
# "just customer 12345" without a separate OAuth client per customer.
&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_oauth_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                        &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                        &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read:customers:*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# broad because it has to be
&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/customers/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# This also works. But the token can read ALL customers,
# and it's valid for 60 minutes. Agent is done in 2.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Broker (task-scoped, ephemeral):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agentwrit&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentWritApp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agentwrit.errors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AuthorizationError&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentWritApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;broker_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENTWRIT_BROKER_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENTWRIT_CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENTWRIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Same LLM decision. Same customer_id.
# But now the credential is scoped to exactly what the LLM asked for.
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;orch_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;requested_scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read:data:customer-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;AuthorizationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Broker says no. Scope exceeds what this app is allowed to issue.
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# "scope exceeds app ceiling"
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# "scope_violation"
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt;

&lt;span class="c1"&gt;# Any downstream service can verify the token independently.
&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;broker_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ['read:data:customer-12345'] — nothing else
&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/customers/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bearer_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# This works. But if something tries to pull ALL customers
# with this token, the API checks the scope and rejects it.
&lt;/span&gt;
&lt;span class="c1"&gt;# Done. Kill the token at the broker. Right now. Not in 58 minutes.
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three examples use the same LLM, the same system prompt, the same tool call, the same customer_id. The code that talks to the API is nearly identical. The difference is the credential.&lt;/p&gt;

&lt;p&gt;With the static key and the OAuth token, the developer already knows they only need customer 12345. But the credential can't enforce that. It's too broad to scope per-task, and there's no mechanism to narrow it at runtime.&lt;/p&gt;

&lt;p&gt;With the broker, the credential is built from the LLM's actual decision. &lt;code&gt;customer_id&lt;/code&gt; flows directly into &lt;code&gt;requested_scope&lt;/code&gt;. The token can only do what the LLM asked for, nothing more. If the LLM had picked a different tool or a different customer, the scope would have been different. And if the requested scope exceeds what the app is allowed to issue, the broker rejects it before the agent touches any data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-Agent Delegation: The Attack Vector Nobody Is Talking About
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting.&lt;/p&gt;

&lt;p&gt;Most serious agent deployments use multiple agents working together. Agent A researches. Agent B drafts. Agent C reviews. Agent D publishes. The output of one agent becomes the input of the next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; How does Agent A give Agent B permission to act on its behalf?&lt;/p&gt;

&lt;p&gt;The naive approach: Agent A shares its credential with Agent B. Now Agent B has Agent A's permissions. If Agent A could read all customers, so can Agent B. Permissions expanded. This is credential escalation, and it's trivially easy in most agent architectures.&lt;/p&gt;

&lt;p&gt;The registry-only approach: Agent B gets its own standing identity and permissions, and you rely on governance later to prove that those permissions are still correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The broker approach: delegation chain verification.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When Agent A delegates to Agent B, it passes a token that says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Agent A authorized Agent B to act on its behalf, with scope exactly equal to Agent A's current scope. Agent B cannot escalate. The delegation is cryptographically signed and time-bounded."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If Agent A had &lt;code&gt;read:data:customer-12345&lt;/code&gt;, Agent B gets &lt;code&gt;read:data:customer-12345&lt;/code&gt;. Not &lt;code&gt;read:data:*&lt;/code&gt;. Not &lt;code&gt;write:data:customer-12345&lt;/code&gt;. Exactly what Agent A had, nothing more.&lt;/p&gt;

&lt;p&gt;The delegation chain is a series of signed tokens. Each link is bound to the previous one, so resource layers can verify the lineage instead of treating each delegated token as unrelated.&lt;/p&gt;

&lt;p&gt;This isn't just a feature. It's the security property I care about most: delegation should preserve or reduce authority, never expand it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Now
&lt;/h2&gt;

&lt;p&gt;2026 is year one for agent deployment at scale. Most teams are figuring this out right now. The architectural decisions made in the next 12 months will persist for years.&lt;/p&gt;

&lt;p&gt;If you bake in long-lived agent credentials today, you'll spend the next two years cleaning them up. Access reviews. Entitlement audits. "Who had access to what when" forensics. Or it just doesn't get cleaned up at all. The enterprise vendors will sell you tools to manage the mess, because the mess will be real.&lt;/p&gt;

&lt;p&gt;If you start with a broker, you still need governance for the persistent systems around the agent. But the short-lived agent instance doesn't need to leave behind a standing credential.&lt;/p&gt;

&lt;p&gt;The registry vendors aren't wrong that sprawl is a problem. I think they're too quick to assume all of it is inevitable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Argument, Not the Pitch
&lt;/h2&gt;

&lt;p&gt;I'm not here to sell you AgentWrit. I'm here to argue that the credential model you choose today determines your security posture for the next five years.&lt;/p&gt;

&lt;p&gt;If you start with long-lived credentials and registry-managed sprawl, you're choosing a future of cleanup, audits, and accumulated risk.&lt;/p&gt;

&lt;p&gt;If you start with ephemeral, task-scoped credentials, you're choosing a future where credentials don't outlive the work, where delegation doesn't escalate, and where the individual agent instance doesn't become a permanent entitlement.&lt;/p&gt;

&lt;p&gt;The broker model isn't new. It's how cloud-native systems have handled short-lived compute for years. VMs get credentials at boot. Containers get credentials at start. Serverless functions get credentials per invocation. The credential dies with the compute.&lt;/p&gt;

&lt;p&gt;Agents are just compute that happens to be intelligent. The same principle applies.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built AgentWrit because I needed this for my own agent deployments. It's a self-hosted credential broker for AI agents, source-available under PolyForm Internal Use for internal deployments.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ephemeral identity:&lt;/strong&gt; Every agent spawns with a unique cryptographic identity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task-scoped tokens:&lt;/strong&gt; Scoped to exactly what the task needs, not broad IAM roles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short-lived credentials:&lt;/strong&gt; Tokens expire in minutes and can be released or revoked early&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four-level revocation:&lt;/strong&gt; Token, agent, task, or full delegation chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegation chain verification:&lt;/strong&gt; Permissions cannot expand at each hop, cryptographically enforced&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's written in Go. Runs with Docker. The broker is source-available under PolyForm Internal Use 1.0.0; the Python SDK is MIT-licensed and live on PyPI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/devonartis/agentwrit" rel="noopener noreferrer"&gt;https://github.com/devonartis/agentwrit&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Python SDK:&lt;/strong&gt; &lt;a href="https://github.com/devonartis/agentwrit-python" rel="noopener noreferrer"&gt;https://github.com/devonartis/agentwrit-python&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Security pattern (CC BY-SA 4.0):&lt;/strong&gt; &lt;a href="https://github.com/devonartis/AI-Security-Blueprints" rel="noopener noreferrer"&gt;https://github.com/devonartis/AI-Security-Blueprints&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pattern is aligned with OWASP Agentic Top 10 (2026), NIST IR 8596, and IETF WIMSE. It's published separately because the architecture matters more than any single implementation.&lt;/p&gt;

&lt;p&gt;The full security architecture is also published as a preprint on &lt;a href="https://zenodo.org/records/19713391" rel="noopener noreferrer"&gt;Zenodo&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It in 10 Minutes
&lt;/h2&gt;

&lt;p&gt;Pull the broker Docker image, install the Python SDK, and run one of the two demos end to end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MedAssist&lt;/strong&gt; is a FastAPI clinical assistant. You ask a plain-language question about a patient; an LLM picks tools (records, labs, billing, prescriptions); the app spawns broker agents on demand, each scoped to one patient and one category. Cross-patient questions are denied. Prescription writes flow through a delegation chain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/devonartis/agentwrit-python/blob/main/demo/README.md" rel="noopener noreferrer"&gt;demo/README.md&lt;/a&gt; has run instructions, a scenario playbook, and a code map.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support Tickets&lt;/strong&gt; is a three-agent pipeline built with Flask + HTMX + SSE. Three LLM-driven agents (triage, knowledge, response) process customer tickets. Anonymous tickets halt at triage. Dangerous tools like &lt;code&gt;delete_account&lt;/code&gt; and &lt;code&gt;send_external_email&lt;/code&gt; are in the LLM's tool list but not in the agent's scope, so they never execute. One scenario deliberately skips &lt;code&gt;release()&lt;/code&gt; to watch a 5-second TTL die on its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/devonartis/agentwrit-python/blob/main/demo2/README.md" rel="noopener noreferrer"&gt;demo2/README.md&lt;/a&gt; has run instructions, five scenarios, and a code map.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Question
&lt;/h2&gt;

&lt;p&gt;How are you credentialing your agents today?&lt;/p&gt;

&lt;p&gt;If the answer involves shared API keys, long-lived OAuth tokens, or broad IAM roles, you might be building the mess that registry vendors will later sell you tools to manage.&lt;/p&gt;

&lt;p&gt;Start with prevention. It's cheaper to avoid standing agent credentials than to clean them up later.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Devon Artis. Principal Security Engineer. CSA AI Controls Matrix contributor. Published the Ephemeral Agent Credentialing pattern as a &lt;a href="https://zenodo.org/records/19713391" rel="noopener noreferrer"&gt;preprint on Zenodo&lt;/a&gt;. Building AgentWrit. One person, no VC.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
      <category>cloudnative</category>
    </item>
  </channel>
</rss>
