<?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: Will Velida</title>
    <description>The latest articles on DEV Community by Will Velida (@willvelida).</description>
    <link>https://dev.to/willvelida</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F35069%2Fe6bc4402-3468-4df5-911b-334d4ffeb485.jpg</url>
      <title>DEV Community: Will Velida</title>
      <link>https://dev.to/willvelida</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/willvelida"/>
    <language>en</language>
    <item>
      <title>Configuring and Securing Azure Key Vault for the Enterprise</title>
      <dc:creator>Will Velida</dc:creator>
      <pubDate>Thu, 05 Nov 2026 06:02:46 +0000</pubDate>
      <link>https://dev.to/willvelida/configuring-and-securing-azure-key-vault-for-the-enterprise-5f79</link>
      <guid>https://dev.to/willvelida/configuring-and-securing-azure-key-vault-for-the-enterprise-5f79</guid>
      <description>&lt;p&gt;We can use Azure Key Vault to securely store and control access to sensitive information. We can use it to handle secrets (things like database connection string, passwords, API tokens etc.), cryptographic keys (used for encrypting and decrypting data), and certificates (SSL/TLS certificates used to secure communications).&lt;/p&gt;

&lt;p&gt;However, just having a Key Vault by itself doesn't automatically make your application secure. We can deploy Azure Key Vaults with soft delete and purge protections to protect our vaults from accidental deletions, we can configure RBAC-based access controls, just-in-time activations using PIM, and we can lock down our Key Vaults using firewall rules, virtual networks, and private endpoints.&lt;/p&gt;

&lt;p&gt;In this blog post, we'll talk about how we can set up our Key Vaults properly for enterprise scenarios, with the right security, access, and network controls that will help protect your sensitive information from being compromised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying Azure Key Vault with security controls
&lt;/h2&gt;

&lt;p&gt;The first thing you need to consider is what SKU do you need. Azure Key Vault provides two tiers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Standard&lt;/strong&gt;, which protects keys using software-based encryption. The key never leaves Azure, but it's not backed by dedicated hardware. For most workloads, this level of protection is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Premium&lt;/strong&gt;, which protects keys with a hardware security module (HSM) validated to Federal Information Processing Standard (FIPS) 140-3 Level 3. The key is created inside the HSM and never gets exported in unencrypted form.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you've decided what SKU you need, you'll need to set some critical properties on the Key Vault when you deploy the vault.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Soft delete&lt;/strong&gt; preserves deleted vault objects (keys, secrets, and certificates) that can be recovered within a configured period of between 7 to 90 days. During this window, any deleted objects can be recovered.&lt;/p&gt;

&lt;p&gt;Soft delete in enabled by default on all new vaults and can't be disabled after you create the vault. The retention period can also only be set when you create the vault.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purge protection&lt;/strong&gt; prevents Key Vaults and objects from being purged permanently. Without this, any identity with the right level of permissions can delete objects and vaults, and purge them! Imagine if you're encrypting your data at rest using cryptographic keys in Key Vault, and someone deleted that key! Good luck trying to read that data again!&lt;/p&gt;

&lt;p&gt;Finally, when you create the vault, &lt;strong&gt;Public network access&lt;/strong&gt; is enabled by default, which means that any internet IP address can try to authenticate with your Key Vault.&lt;/p&gt;

&lt;p&gt;That doesn't mean that the vault is unprotected, you still have to have a valid Microsoft Entra token and the appropriate role assignment over the vault, it's just that the endpoint is exposed to the public internet, which expands the attack surface.&lt;/p&gt;

&lt;p&gt;Configuring soft delete and purge protection for Key Vault is pretty straightforward in Bicep.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bicep"&gt;&lt;code&gt;&lt;span class="nd"&gt;@description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Globally unique name for the Key Vault.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;param&lt;/span&gt; &lt;span class="n"&gt;keyVaultName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;keyVault&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.KeyVault/vaults@2024-11-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyVaultName&lt;/span&gt;
  &lt;span class="py"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'australiaeast'&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;
    &lt;span class="py"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt;
      &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'standard'&lt;/span&gt;
    &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="py"&gt;enableSoftDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;softDeleteRetentionInDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;
    &lt;span class="py"&gt;enablePurgeProtection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;enableRbacAuthorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can enforce vault configuration at scale using Azure Policy. There are two built-in policies that help us apply soft-delete and purge-protection settings at scale:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Key vaults should have soft delete enabled&lt;/strong&gt;. This helps identify vaults without soft delete configured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key vaults should have deletion protection enabled&lt;/strong&gt;. This audits existing vaults and can block creation of new vaults that don't have purge protection enabled.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Assign these policies at either the subscription or management group scope to ensure every vault meets these requirements. You can also combine it with a remediation task to bring update existing vaults that don't have this enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring access to Azure Key Vault
&lt;/h2&gt;

&lt;p&gt;Access in Azure Key Vault can be divided into the &lt;strong&gt;control plane&lt;/strong&gt; and the &lt;strong&gt;data plane&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;control plane&lt;/strong&gt; governs the Key Vault itself. Operations use Azure Resource Manager (ARM) and include creating or deleting the vault, configuring network rules, and viewing metadata.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;data plane&lt;/strong&gt; governs objects stored inside the vault. Both planes authenticate through Microsoft Entra ID, but authorization is independent on each plane.&lt;/p&gt;

&lt;p&gt;Having this separation between the two planes helps improve security, since you can assign control-plane access to identities that only need to manage the vault as infrastructure, and data-plane access to those identities that need to work with Key Vault objects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi9mxt8pjoea1vlfho1k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi9mxt8pjoea1vlfho1k.png" alt=" " width="800" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Key Vault supports two authorization models for the data plane: &lt;strong&gt;Azure RBAC&lt;/strong&gt; and legacy vault access policies. These vault access policies are being retired, so I'll just discuss how Azure RBAC works for Key Vault, and tell you that if you're still using legacy access policies in your Key Vault, &lt;strong&gt;migrate to RBAC now!&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Azure RBAC provides roles for Key Vault operations across both planes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Role&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Plane&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;What it governs&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Owner, Contributor&lt;/td&gt;
&lt;td&gt;Control plane&lt;/td&gt;
&lt;td&gt;The vault resource itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key Vault Administrator, Secrets User, Certificates Officer, Purge Operator&lt;/td&gt;
&lt;td&gt;Data plane&lt;/td&gt;
&lt;td&gt;The vault content&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's take a closer look at some RBAC roles that you can apply in Key Vault.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Vault Administrator&lt;/strong&gt; grants full data-plane access to all objects in the Key Vault. This role is the most powerful role, so don't grant it on a permanent basis and reserve it for break glass scenarios or automation that genuinely needs cross-object access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Vault Secrets User&lt;/strong&gt; grants read-only access to secret values. This is the right role for application service principles and managed identities that need to retrieve runtime secrets such as API keys and connection strings. This role only allows the identity to read the secret content, not create, update, or delete secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Vault Certificates Officer&lt;/strong&gt; allows full lifecycle management of certificates, including creating, importing, updating, and deleting certificates. With this role, you cannot touch secrets or keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Vault Purge Operator&lt;/strong&gt; allows users to permanently delete soft-deleted vault objects. This role should only be used in explicit recovery or decommissioning scenarios, where you intend to purge soft-deleted objects.&lt;/p&gt;

&lt;p&gt;To assign an RBAC role over the Key Vault in Bicep, we can do the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bicep"&gt;&lt;code&gt;&lt;span class="nd"&gt;@description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Microsoft Entra object ID of the user.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;param&lt;/span&gt; &lt;span class="n"&gt;userObjectId&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;secretsUserRoleId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;subscriptionResourceId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'Microsoft.Authorization/roleDefinitions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'4633458b-17de-408a-b874-0445c86b69e6'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;secretsUserAssignment&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Authorization/roleAssignments@2022-04-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;guid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userObjectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secretsUserRoleId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="py"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyVault&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;roleDefinitionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;secretsUserRoleId&lt;/span&gt;
    &lt;span class="py"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userObjectId&lt;/span&gt;
    &lt;span class="py"&gt;principalType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'User'&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also use &lt;strong&gt;Privileged Identity Management (PIM)&lt;/strong&gt; to eliminate the need to grant these roles permanently. If a user needs elevated Key Vault access, they can request activation of that role through PIM, which can use controls like MFA verification, justification, approvals, and time durations to limit access of the role.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microsoft Entra Conditional Access&lt;/strong&gt; let you apply policy-based controls to Key Vault access, such as requiring MFA for access from unmanaged or noncompliant devices, blocking access from risky sign-in locations, or limit access to requests that originate from locations or network paths you trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring firewall and network settings for Azure Key Vault
&lt;/h2&gt;

&lt;p&gt;By default, public network access is enabled when creating Key Vaults, making it reachable by any internet IP address.&lt;/p&gt;

&lt;p&gt;The first thing we can do to increase network security on the Key Vault is enable the firewall and add explicit allow rules. Once you enable the firewall, all traffic is blocked by default, and only specific IPv4 addresses or Classless Inter-Domain Routing (CIDR) ranges that are defined will have access.&lt;/p&gt;

&lt;p&gt;This is handy for situations where you have workloads or agents running on known IP addresses, and you enable that IP address on the firewall.&lt;/p&gt;

&lt;p&gt;We can enable firewall access on Key Vaults in Bicep like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bicep"&gt;&lt;code&gt;&lt;span class="nd"&gt;@description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Globally unique name for the Key Vault.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;param&lt;/span&gt; &lt;span class="n"&gt;keyVaultName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;

&lt;span class="nd"&gt;@description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Public IPv4 addresses or CIDR ranges allowed through the firewall.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;param&lt;/span&gt; &lt;span class="n"&gt;allowedIpAddressesOrRanges&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;keyVault&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.KeyVault/vaults@2024-11-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyVaultName&lt;/span&gt;
  &lt;span class="py"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'australiaeast'&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;
    &lt;span class="py"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt;
      &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'standard'&lt;/span&gt;
    &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="py"&gt;enableSoftDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;softDeleteRetentionInDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;
    &lt;span class="py"&gt;enablePurgeProtection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;enableRbacAuthorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;publicNetworkAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Enabled'&lt;/span&gt;
    &lt;span class="py"&gt;networkAcls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;bypass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'None'&lt;/span&gt;
      &lt;span class="py"&gt;defaultAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Deny'&lt;/span&gt;
      &lt;span class="py"&gt;ipRules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;addressOrRange&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="py"&gt;allowedIpAddressesOrRanges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
          &lt;span class="py"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;addressOrRange&lt;/span&gt;
        &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="py"&gt;virtualNetworkRules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can take this further and disable public access entirely, but only after we create a private endpoint or virtual network rule. If we don't have this, we'll lock ourselves out of the vault!&lt;/p&gt;

&lt;p&gt;Bear in mind that firewall rules only apply to data plane operations, not the control plane. Private IP address ranges cannot be used in IP allow list rules. In dynamic environments, this approach can be limited, as allow lists would need to be updated each time our infrastructure changes.&lt;/p&gt;

&lt;p&gt;An alternative to this is to use &lt;strong&gt;Virtual network service endpoints&lt;/strong&gt; inside an Azure virtual network. When we enable the &lt;code&gt;Microsoft.KeyVault&lt;/code&gt; service endpoint on a virtual network subnet and add it to our Key Vault network rules, traffic from that subnet travels over the Azure backbone network, rather than than the public internet.&lt;/p&gt;

&lt;p&gt;Configuring a service endpoint for our Key Vault can be done in Bicep like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bicep"&gt;&lt;code&gt;&lt;span class="k"&gt;param&lt;/span&gt; &lt;span class="n"&gt;virtualNetworkName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="k"&gt;param&lt;/span&gt; &lt;span class="n"&gt;subnetName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;

&lt;span class="nd"&gt;@description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Subnet CIDR within the virtual network address space, without overlapping existing subnets.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;param&lt;/span&gt; &lt;span class="n"&gt;subnetAddressPrefix&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;

&lt;span class="nd"&gt;@description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Globally unique name for the Key Vault.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;param&lt;/span&gt; &lt;span class="n"&gt;keyVaultName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;virtualNetwork&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Network/virtualNetworks@2024-05-01'&lt;/span&gt; &lt;span class="k"&gt;existing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;virtualNetworkName&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;subnet&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Network/virtualNetworks/subnets@2024-05-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;virtualNetwork&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;subnetName&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;addressPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;subnetAddressPrefix&lt;/span&gt;
    &lt;span class="py"&gt;serviceEndpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="pi"&gt;{&lt;/span&gt;
        &lt;span class="py"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.KeyVault'&lt;/span&gt;
      &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;keyVault&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.KeyVault/vaults@2024-11-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyVaultName&lt;/span&gt;
  &lt;span class="py"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'australiaeast'&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;
    &lt;span class="py"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt;
      &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'standard'&lt;/span&gt;
    &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="py"&gt;enableSoftDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;softDeleteRetentionInDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;
    &lt;span class="py"&gt;enablePurgeProtection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;enableRbacAuthorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;publicNetworkAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Enabled'&lt;/span&gt;
    &lt;span class="py"&gt;networkAcls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;bypass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'None'&lt;/span&gt;
      &lt;span class="py"&gt;defaultAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Deny'&lt;/span&gt;
      &lt;span class="py"&gt;ipRules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
      &lt;span class="py"&gt;virtualNetworkRules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="pi"&gt;{&lt;/span&gt;
          &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
          &lt;span class="py"&gt;ignoreMissingVnetServiceEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
        &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Service endpoints are an improvement over IP-based rules for Azure workloads, as traffic stays on the Azure backbone and never crosses onto the public internet.&lt;/p&gt;

&lt;p&gt;However, Key Vault is has a public endpoint and it's DNS name still resolves to a public IP.&lt;/p&gt;

&lt;p&gt;We can fully isolate our Key Vault using Private Endpoints. Once we combine a private endpoint with disabling public network access, the vault is no longer reachable through the public internet. The only way to connect to the vault is through your virtual network, or through connected networks such as Azure ExpressRoute or site-to-site VPN.&lt;/p&gt;

&lt;p&gt;To get this to work correctly, we'll need a &lt;strong&gt;private endpoint resource&lt;/strong&gt;, which is a network interface in your virtual network subnet that's bound to Key Vault via Azure Private Link. This receives a private IP address. We'll also need a &lt;strong&gt;private DNS zone&lt;/strong&gt;. Without it, clients will resolve your vault's public IP regardless of the private endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bicep"&gt;&lt;code&gt;&lt;span class="k"&gt;param&lt;/span&gt; &lt;span class="n"&gt;virtualNetworkName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="k"&gt;param&lt;/span&gt; &lt;span class="n"&gt;keyVaultName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="k"&gt;param&lt;/span&gt; &lt;span class="n"&gt;subnetName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'snet-private-endpoints'&lt;/span&gt;

&lt;span class="nd"&gt;@description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Non-overlapping subnet CIDR within the existing virtual network.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;param&lt;/span&gt; &lt;span class="n"&gt;subnetAddressPrefix&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;virtualNetwork&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Network/virtualNetworks@2024-05-01'&lt;/span&gt; &lt;span class="k"&gt;existing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;virtualNetworkName&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;subnet&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Network/virtualNetworks/subnets@2024-05-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;virtualNetwork&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;subnetName&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;addressPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;subnetAddressPrefix&lt;/span&gt;
    &lt;span class="py"&gt;privateEndpointNetworkPolicies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Disabled'&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;keyVault&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.KeyVault/vaults@2024-11-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyVaultName&lt;/span&gt;
  &lt;span class="py"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'australiaeast'&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;
    &lt;span class="py"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt;
      &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'standard'&lt;/span&gt;
    &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="py"&gt;enableSoftDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;softDeleteRetentionInDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;
    &lt;span class="py"&gt;enablePurgeProtection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;enableRbacAuthorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;publicNetworkAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Disabled'&lt;/span&gt;
    &lt;span class="py"&gt;networkAcls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;bypass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'None'&lt;/span&gt;
      &lt;span class="py"&gt;defaultAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Deny'&lt;/span&gt;
      &lt;span class="py"&gt;ipRules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
      &lt;span class="py"&gt;virtualNetworkRules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;privateEndpoint&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Network/privateEndpoints@2024-05-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;keyVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;-pe'&lt;/span&gt;
  &lt;span class="py"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;virtualNetwork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;subnet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="py"&gt;privateLinkServiceConnections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="pi"&gt;{&lt;/span&gt;
        &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;keyVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;-connection'&lt;/span&gt;
        &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
          &lt;span class="py"&gt;privateLinkServiceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
          &lt;span class="py"&gt;groupIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'vault'&lt;/span&gt;
          &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;privateDnsZone&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Network/privateDnsZones@2020-06-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'privatelink.vaultcore.azure.net'&lt;/span&gt;
  &lt;span class="py"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'global'&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;virtualNetworkLink&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;privateDnsZone&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;virtualNetwork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;-link'&lt;/span&gt;
  &lt;span class="py"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'global'&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;registrationEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="py"&gt;virtualNetwork&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;virtualNetwork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;privateDnsZoneGroup&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;privateEndpoint&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;privateDnsZoneConfigs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="pi"&gt;{&lt;/span&gt;
        &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'key-vault'&lt;/span&gt;
        &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
          &lt;span class="py"&gt;privateDnsZoneId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;privateDnsZone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
        &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once this is configured, you can disable public network access on the vault, fully isolating it from the internet. If you need some Microsoft-managed services to reach the Key Vault (E.g. Azure Monitor), you can use &lt;strong&gt;Allow trusted Microsoft services to bypass this firewall&lt;/strong&gt; exception rule.&lt;/p&gt;

&lt;p&gt;Not all vaults require the same level of network isolation. This diagram shows how you can match the model to the workload to keep the network configuration maintainable and appropriate for the risk:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkysb65pq43gl1gp3sh15.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkysb65pq43gl1gp3sh15.png" alt=" " width="800" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;Hopefully you now have a better understanding about how you can configure your Azure Key Vaults for enterprise scenarios. We discussed how security controls such as soft delete, purge protection, and SKU selection factor in to protect our Key Vaults and the objects inside our Key Vaults. We also took a look at how we can configure RBAC-based access controls to limit permissions on what identities can do to our Key Vault. Finally, we took at a look at how we can secure our Key Vaults via network controls such as firewall rules, service endpoints, and private endpoints.&lt;/p&gt;

&lt;p&gt;If you have any questions, feel free to reach out to me on X &lt;a href="https://twitter.com/willvelida" rel="noopener noreferrer"&gt;@willvelida&lt;/a&gt; or on &lt;a href="https://bsky.app/profile/willvelida.com" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Until next time, Happy coding! 🤓🖥️&lt;/p&gt;

</description>
      <category>azure</category>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>Implement and configure Privileged Identity Management (PIM)</title>
      <dc:creator>Will Velida</dc:creator>
      <pubDate>Tue, 03 Nov 2026 23:40:33 +0000</pubDate>
      <link>https://dev.to/willvelida/implement-and-configure-privileged-identity-management-pim-28n0</link>
      <guid>https://dev.to/willvelida/implement-and-configure-privileged-identity-management-pim-28n0</guid>
      <description>&lt;p&gt;Imagine if an attacker was able to compromise a privileged user at your company. The damage they could do with that identity could have permanent effects and they can continue to do that damage until someone notices.&lt;/p&gt;

&lt;p&gt;This is was Privileged Identity Management, or PIM, is designed to stop. With PIM, we can implement just-in-time (JIT) access across a full surface of privileged access in a Microsoft cloud environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does this matter?
&lt;/h2&gt;

&lt;p&gt;If you've ever worked on a greenfield project before, you've probably configured a Global Administrator account just to make sure you weren't blocked when you needed to do administrative tasks.&lt;/p&gt;

&lt;p&gt;Did you remember to delete it?&lt;/p&gt;

&lt;p&gt;This is called &lt;strong&gt;Standing privilege&lt;/strong&gt; where a privileged role assignment is permanently active regardless of whether it's being used or not. Because it's always active, the credentials that control the role assignment are a valuable target to malicious actors, because the window of opportunity is the same of the lifetime of the assignment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs2t5xsbteh0z6rrxbbk5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs2t5xsbteh0z6rrxbbk5.png" alt=" " width="799" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can imagine, if you have a Global Admin account just sitting there waiting to be compromised, the blast radius of damage has the potential to be quite destructive. For a Global Admin in Microsoft Entra ID, that would include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User accounts&lt;/li&gt;
&lt;li&gt;Group memberships&lt;/li&gt;
&lt;li&gt;Application registrations&lt;/li&gt;
&lt;li&gt;Federated trust settings&lt;/li&gt;
&lt;li&gt;Every Azure subscription linked to the directory!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not just Global Admins either. Privileged Role Admins, Security Admins, and User Access Admins carry their own blast radiuses, and if you leave them standing there, they become permanent targets.&lt;/p&gt;

&lt;p&gt;It's not just humans with these privileges. AI agents, model endpoints, and automation pipelines can operate under the same permissions granted by the identity that configured them. A compromise standing Global Admin account can redirect agent behavior, exfiltrate data, or use the agent to escalate access to other systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just-in-time (JIT) access&lt;/strong&gt; provides elevated permissions that you don't hold persistently. You can activate them for a specific task, and they expire after a defined period.&lt;/p&gt;

&lt;p&gt;JIT access reduces the risk that standing privilege creates through 3 properties:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You don't have the access by default&lt;/strong&gt;, meaning that there are no persistent credentials for an attacker to discover or steal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You activate access&lt;/strong&gt;. It's a deliberate process to elevate your permissions, which becomes an explicit, auditable event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access expires automatically&lt;/strong&gt;. If a session is compromised, permissions are timeboxed, meaning that the attacker's window closes when the session ends.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Core capabilities of PIM
&lt;/h2&gt;

&lt;p&gt;PIM implements JIT access through a set of controls. PIM supports both Microsoft Entra ID and Azure resource roles.&lt;/p&gt;

&lt;p&gt;PIM uses two types of role assignments to give you control over when access is live:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;An eligible assignment&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means you hold the entitlement to a role, but don't hold the role until you explicitly request an &lt;strong&gt;activation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Activation is the act of requesting and receiving temporary elevated permissions from that entitlement, which is the JIT model in action.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;An active assignment&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This grants the role directly, making access immediate without any activation step. PIM can time-bound active assignments, making them suitable for temporary standing access with a defined expiration.&lt;/p&gt;

&lt;p&gt;This is ideal for situations where the activation step is impractical, such as break-glass emergency accounts or tightly scoped temporary roles with a defined end date.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Activation Controls work
&lt;/h2&gt;

&lt;p&gt;When you activate an eligible assignment, PIM can require you to satisfy one or more conditions before granting the role. These controls decrease the possibility of unauthorize use and ensure that if an attacker steals a credential, that alone isn't enough to obtain elevated permissions.&lt;/p&gt;

&lt;p&gt;These controls include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MFA verification&lt;/strong&gt;, which confirms your identity before elevation, so stolen credentials alone aren't enough to obtain the role.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Justification&lt;/strong&gt;, which is a written reason you must supply before PIM grants the session. This creates an auditable record of why access was requested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approval&lt;/strong&gt;, a delegated approver must confirm the request before PIM activates the role, which creates a human checkpoint for sensitive assignments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activation duration&lt;/strong&gt;, the configured time window. This is measured in hours, after which PIM will automatically remove the role.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some roles require all of these controls, while others only require MFA verification.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvujk0slgaedoq6bbphgs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvujk0slgaedoq6bbphgs.png" alt=" " width="800" height="1331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a user activates a role, PIM sends email notifications to both the activating user and any configured approver, which creates immediate awareness that an elevated session is open. This results in a durable record of all privileged activity across your tenant.&lt;/p&gt;

&lt;p&gt;PIM also supports &lt;strong&gt;access reviews&lt;/strong&gt;, which periodically validates that eligible assignments are still appropriate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing JIT access for Microsoft Entra roles
&lt;/h2&gt;

&lt;p&gt;Not every Entra role needs JIT controls, but there are a couple of high-privileged roles that have a large enough blast radius where permanent assignment of that role creates unacceptable risk.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Role&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Why JIT?&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Global Administrator&lt;/td&gt;
&lt;td&gt;Broadest blast radius in the tenant, as it gives full control over all directory settings and services.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privileged Role Administrator&lt;/td&gt;
&lt;td&gt;Can modify any role assignment, including its own!&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Administrator&lt;/td&gt;
&lt;td&gt;Controls security policy configuration across the tenant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exchange Administrator&lt;/td&gt;
&lt;td&gt;Has full access to email and calendar infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application Administrator&lt;/td&gt;
&lt;td&gt;Can register apps and modify application credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication Policy Administrator&lt;/td&gt;
&lt;td&gt;Controls authentication methods policy, tenant-wide MFA settings, and password protection policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Just a heads up, before you make Global Admin eligible in PIM, verify that at least 2 break-glass accounts hold a permanent active assignment to the role. Store these securely offline, because without it, a misconfiguration or an identity provider outage could lock every admin out of the tenant with no recovery path!&lt;/p&gt;

&lt;p&gt;To assign eligible access to a Microsoft Entra role, do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in to the Entra admin center and open &lt;strong&gt;ID Governance &amp;gt; Privileged Identity Management&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Manage&lt;/strong&gt;, select &lt;strong&gt;Microsoft Entra roles&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select the target role.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Assignments &amp;gt; Add Assignments&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Assignment type&lt;/strong&gt; to &lt;strong&gt;Eligible&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select the member and set the assignment duration (either permanent or time-bound)&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Assign&lt;/strong&gt; to complete the assignment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Role settings&lt;/strong&gt; are the per-role configuration in PIM that governs the conditions eligible users must satisfy before PIM grants access to that role. These settings apply to all eligible users of a given role. Configuring these roles only determine what conditions must be met when an eligible user activates the role, it doesn't create the assignment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8g44m4dy1lmlnynmyuse.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8g44m4dy1lmlnynmyuse.png" alt=" " width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To activate an eligible Microsoft Entra role, perform the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In PIM, select &lt;strong&gt;My roles &amp;gt; Eligible assignments&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Find the role and select &lt;strong&gt;Activate&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set the activation duration.&lt;/li&gt;
&lt;li&gt;Enter a justification describing the task that requires elevated access.&lt;/li&gt;
&lt;li&gt;If approval is required, submit the request and wait for approval.&lt;/li&gt;
&lt;li&gt;Confirm the role appears under &lt;strong&gt;Active assignments&lt;/strong&gt; with a duration countdown.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementing JIT access for Azure roles and resources
&lt;/h2&gt;

&lt;p&gt;Azure RBAC operates across a nested scope hierarchy, starting from a management group, then subscription, resource group, and individual resource. Assignments at a broader scope inherit downwards, so an Owner assignment at the subscription level would grant ownership over every resource group and resource within it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fskbz5vt1s7c1bund9dtf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fskbz5vt1s7c1bund9dtf.png" alt=" " width="800" height="808"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PIM surfaces this same hierarchy, letting you make users eligible at the subscription, resource group, or individual resource level. This scope selection is important, as assigning eligibility at a narrower scope limits the potential damage from a compromised activation.&lt;/p&gt;

&lt;p&gt;With Azure RBAC, you're managing RBAC assignments on Azure control-plane objects rather than directory identities, so it's important to keep that distinction in mind to prevent inadvertently configuring the wrong target when managing resources.&lt;/p&gt;

&lt;p&gt;Not every Azure resource has the same risk from a compromised Owner or Contributor role. It depends on the following factors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Data sensitivity.&lt;/li&gt;
&lt;li&gt;Blast radius.&lt;/li&gt;
&lt;li&gt;Regulatory exposure.&lt;/li&gt;
&lt;li&gt;Reversibility of any damage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can apply resources to a risk tier so you can apply the strictest controls where they make sense. For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Risk tier&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Resource examples&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Why this level of risk?&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Recommended controls&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;Production subscription&lt;/td&gt;
&lt;td&gt;Compromise grants lateral movement&lt;/td&gt;
&lt;td&gt;MFA required, justification required, approval required, 1-2 timebox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Production resource group&lt;/td&gt;
&lt;td&gt;Narrower scope but high data sensitivity or service continuity risk&lt;/td&gt;
&lt;td&gt;MFA required, justification required, approval optional, 4-8 max&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;Dev/Test resource groups&lt;/td&gt;
&lt;td&gt;Low data sensitivity, mistakes can be reversed&lt;/td&gt;
&lt;td&gt;MFA required, justification required, no approval, up to 8 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once you've done your tier analysis, you can apply access to an Azure resource as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In PIM, select &lt;strong&gt;Azure resources&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to the target resource using the Subscriptions or Resource groups dropdown, and under &lt;strong&gt;Manage&lt;/strong&gt;, select &lt;strong&gt;Roles&lt;/strong&gt;, then select &lt;strong&gt;Add assignments&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select the Azure resource role.&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Assignment type&lt;/strong&gt; to &lt;strong&gt;Eligible&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select the member and configure the assignment duration.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Assign&lt;/strong&gt; to complete the assignment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To activate an eligible Azure resource role, go to &lt;strong&gt;My roles&lt;/strong&gt; in PIM, select the &lt;strong&gt;Azure resources&lt;/strong&gt; tab to see your eligible assignments, and select &lt;strong&gt;Activate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The difference is that you configure role settings for Azure resource roles independently per role per resource scope. This provides us with a flexible approach to apply stricter requirements on different resources depending on their sensitivity (e.g. production vs development resources).&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling PIM with Groups
&lt;/h2&gt;

&lt;p&gt;Configuring JIT access for one or two engineers is pretty straightforward. Doing it individually for a group of engineers would be a pain, as that would require a separate eligible assignment, each one carrying its own role settings, its own maximum activation duration, own approvers list etc.&lt;/p&gt;

&lt;p&gt;PIM for Groups shifts the eligible resource from a role to a group membership, so instead of making each engineer eligible for a role, you create a group, assign that group the role, and make each engineer eligible for membership in that group. When an engineer activates their membership, they gain all the roles the group holds. When the activation expires, membership ends, and access is revoked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsfjxbw1mqsq83ioxcigc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsfjxbw1mqsq83ioxcigc.png" alt=" " width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For groups that need to govern access to sensitive resources, use a &lt;strong&gt;role-assignable group&lt;/strong&gt;. Standard Microsoft Entra ID groups are modified by its owners, or by lower-privileged admins such as Groups Administrator, without triggering approvals or audit entries in PIM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Hopefully you have a better idea on how to configure PIM for both Microsoft Entra ID roles and Azure resources. Using PIM can help structure better privileged access design in your environments, and provide safeguards against malicious actors who can potentially use standing privilege to exploit your workloads.&lt;/p&gt;

&lt;p&gt;If you have any questions, feel free to reach out to me on X &lt;a href="https://twitter.com/willvelida" rel="noopener noreferrer"&gt;@willvelida&lt;/a&gt; or on &lt;a href="https://bsky.app/profile/willvelida.com" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Until next time, Happy coding! 🤓🖥️&lt;/p&gt;

</description>
      <category>azure</category>
      <category>security</category>
    </item>
    <item>
      <title>Manage and Implement authentication methods in Microsoft Entra ID</title>
      <dc:creator>Will Velida</dc:creator>
      <pubDate>Tue, 03 Nov 2026 05:44:41 +0000</pubDate>
      <link>https://dev.to/willvelida/manage-and-implement-authentication-methods-in-microsoft-entra-id-40i7</link>
      <guid>https://dev.to/willvelida/manage-and-implement-authentication-methods-in-microsoft-entra-id-40i7</guid>
      <description>&lt;p&gt;Microsoft Entra ID supports a wide range of authentication methods, from legacy password-based approaches to passwordless alternatives. These methods fall into the following categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Something you know&lt;/strong&gt; such as a password or PIN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Something you have&lt;/strong&gt; such as a registered device, security key, or authenticator app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Something you are&lt;/strong&gt; such as Biometric data, including fingerprints or facial recognition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ideally, passwordless methods would be implemented since they remove the need for a password entirely. Instead, we can authenticate users using a combination of device possession and a biometric or PIN.&lt;/p&gt;

&lt;p&gt;However, not all organizations can immediately move to passwordless authentication. That's where &lt;em&gt;multifactor authentication&lt;/em&gt;, or MFA, can provide a intermediate security layer. MFA requires users to verify their identity using 2 or more factors from different categories.&lt;/p&gt;

&lt;p&gt;In this blog post, we'll take a look at the different ways we can manage and implement authentication methods using Microsoft Entra ID &lt;/p&gt;

&lt;h2&gt;
  
  
  Multifactor authentication (MFA)
&lt;/h2&gt;

&lt;p&gt;Multifactor authentication is a process where users are prompted during sign-in for additional forms of identification, such as a code on their mobile phone or a biometric scan. This helps increase the level of security in your applications, because if you just used a password to authenticate users, this would be the single vector for attack. If a password is weak or has been exposed, an attacker can use it to gain access.&lt;/p&gt;

&lt;p&gt;Microsoft Entra ID supports several MFA methods:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Method&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Type&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Security Level&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Authenticator push notification&lt;/td&gt;
&lt;td&gt;App-based&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Authenticator TOTP code&lt;/td&gt;
&lt;td&gt;App-based&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OATH TOTP hardware token&lt;/td&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMS one-time passcode&lt;/td&gt;
&lt;td&gt;Phone&lt;/td&gt;
&lt;td&gt;Low-medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice call verification&lt;/td&gt;
&lt;td&gt;Phone&lt;/td&gt;
&lt;td&gt;Low-medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As you can see, not all MFA methods offer the same level of protection. Microsoft Entra ID has &lt;strong&gt;authentication strength&lt;/strong&gt; which lets you specify the minimum level of acceptable MFA method required to access a given resource through Conditional Access policies.&lt;/p&gt;

&lt;p&gt;From most least to most secure, the security hierarchy of MFA is as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Password only (no MFA).&lt;/li&gt;
&lt;li&gt;Password and SMS or voice call.&lt;/li&gt;
&lt;li&gt;Password and Microsoft Authenticator app.&lt;/li&gt;
&lt;li&gt;Passwordless sign-in (Windows Hello for Business, FIDO2 security keys, passkeys in Microsoft Authenticator, certificate-based authentication).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Through authentication strength policies, we can require &lt;strong&gt;phishing-resistant MFA&lt;/strong&gt;. Microsoft Entra ID's built-in phishing-resistant strength covers Windows Hello for Business, FIDO2 security keys, passkeys in Microsoft Authenticator, and certificate-based authentication.&lt;/p&gt;

&lt;p&gt;Authentication strength policies are applied per application. So you can set phishing-resistant MFA for your most sensitive systems, while allowing standard MFA for low-risk applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Microsoft Entra ID authentication architecture
&lt;/h2&gt;

&lt;p&gt;When a user signs in, the request flows through the &lt;strong&gt;Microsoft Identity platform&lt;/strong&gt;, the authentication engine behind Microsoft Entra ID. The platform evaluates user's credentials, applies Conditional Access policies, and issues tokens upon successful authentication.&lt;/p&gt;

&lt;p&gt;If you have any devices that are joined to Microsoft Entra ID, a &lt;strong&gt;Primary Refresh Token&lt;/strong&gt; is issued after the first time you sign-in. This PRT enables single sign-on (SSO)  across applications and services without requiring you to re-authenticate. This helps user experience (imagine having to sign-in each time!) and helps improve security thanks to stronger authentication methods.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fktia34iuv1chk6a1k0dz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fktia34iuv1chk6a1k0dz.png" alt="Microsoft Entra ID authentication architecture and sign-in flow." width="800" height="740"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying MFA with the right enforcement model.
&lt;/h2&gt;

&lt;p&gt;Conditional Access gives us precise control over when MFA is required. We create a policy, target users or groups, select cloud applications or user actions, and then set the grant control to &lt;strong&gt;Require multifactor authentication&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The policy evaluates each sign-in and applies MFA only when the conditions you have defined match. Here's how the MFA evaluation flow works:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F230azirm9o5rwwqsybgv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F230azirm9o5rwwqsybgv.png" alt="Conditional Access evaluation flow for enforcing multifactor authentication." width="800" height="678"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Named locations and trusted IP boundaries
&lt;/h3&gt;

&lt;p&gt;We can refine our Conditional Access policies by defining &lt;strong&gt;named locations&lt;/strong&gt;. These are logical groupings of IP ranges or areas that represent trusted boundaries, such as corporate offices and VPN egress addresses.&lt;/p&gt;

&lt;p&gt;When we use named locations in Conditional Access, we can reduce the friction for trusted sign-ins while still requiring MFA for sign-ins outside those defined trusted boundaries.&lt;/p&gt;

&lt;p&gt;Named locations are recommended for trusted IP configuration because they support IPv6 and they integrate with all Conditional Access conditions. The Trusted IP setting is available, but it &lt;em&gt;doesn't&lt;/em&gt; support IPv6.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring authentication methods that match risk
&lt;/h3&gt;

&lt;p&gt;After we decide when MFA is required, we can configure which methods users can use through the &lt;strong&gt;Authentication methods policy&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft Authenticator&lt;/strong&gt; should be the primary method for most users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OATH tokens&lt;/strong&gt; are useful for users in regulated environments, or for users who can't use a smartphone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMS and voice calls&lt;/strong&gt; can be used as a backup, however be mindful that these are vulnerable to attacks such as SIM swapping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party OATH tokens&lt;/strong&gt; are useful if your organization already has a token rollout process.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Passwordless authentication in Microsoft Entra ID
&lt;/h2&gt;

&lt;p&gt;Passwordless authentication is better than using MFA for two reasons. It removes the need for users to remember a password, and it helps users sign-in faster.&lt;/p&gt;

&lt;p&gt;Microsoft Entra ID supports multiple passwordless methods, which you can assign by persona.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Persona&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Recommended method&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Alternative&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Administrators and regulated users&lt;/td&gt;
&lt;td&gt;FIDO2 security key&lt;/td&gt;
&lt;td&gt;Windows Hello for Business&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Office workers on corporate Windows devices&lt;/td&gt;
&lt;td&gt;Windows Hello for Business&lt;/td&gt;
&lt;td&gt;Passkey in Microsoft Authenticator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile and cross-platform users&lt;/td&gt;
&lt;td&gt;Passkey in Microsoft Authenticator&lt;/td&gt;
&lt;td&gt;Synced passkey&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontline or shared-device workers&lt;/td&gt;
&lt;td&gt;FIDO2 security key&lt;/td&gt;
&lt;td&gt;Passkey in Microsoft Authenticator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BYOD users&lt;/td&gt;
&lt;td&gt;Synced passkey or Authenticator passkey&lt;/td&gt;
&lt;td&gt;FIDO2 security key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7nt9m3m1wx4k2ttwksa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7nt9m3m1wx4k2ttwksa.png" alt="Passwordless authentication methods supported by Microsoft Entra ID." width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows Hello for Business&lt;/strong&gt; creates a cryptographic credential protected by device hardware and unlocked locally with a PIN or biometric gesture. The level of phishing resistance is high because no password is sent during sign-in. Windows Hello for Business is best for Microsoft Entra joined or hybrid joined Windows devices managed by Intune or Group policies.&lt;/p&gt;

&lt;p&gt;For privileged administrator, regulated users, and shared workstation environments, use &lt;strong&gt;FIDO2 security keys&lt;/strong&gt; which provide strong phishing resistance and are not tied to one device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Passkeys in Microsoft Authenticator&lt;/strong&gt; provides a portable phishing-resistant credential which is hardware-backed on your mobile device and you can unlock it with a biometric or PIN. &lt;/p&gt;

&lt;p&gt;For users with multiple personal devices, you can implement &lt;strong&gt;synced passkeys&lt;/strong&gt;. This is ideal for lower-risk users where usability is a problem.&lt;/p&gt;

&lt;p&gt;Applications that use Microsoft Entra ID with OAuth 2.0/OpenID Connect can use passwordless sign-ins without changes to their code. For non-human identities and backend services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use managed identities for workloads in Azure&lt;/li&gt;
&lt;li&gt;Use certificate-based authentication rather than long-lived client secrets if you can't use managed identities.&lt;/li&gt;
&lt;li&gt;Apply workload identity controls and monitoring to high-privilege service principals.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configure self-service password reset in Microsoft Entra ID.
&lt;/h2&gt;

&lt;p&gt;Like I mentioned earlier, not all organizations can switch to passwordless authentication methods. Self-service password reset (SSPR) is a great feature to help users reset their own passwords, and reduce the amount of requests your poor support team gets to reset passwords!&lt;/p&gt;

&lt;p&gt;Before users can reset their own passwords, you need to enable it. In the Microsoft Entra admin center, you can choose the following SSPR scopes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;None&lt;/strong&gt;, which means SSPR is disabled for all users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selected&lt;/strong&gt;, which means SSPR is enabled for members of a specific security group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All&lt;/strong&gt;, meaning all users inside the tenant can use SSPR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SSPR and Password writeback are available on Microsoft Entra ID P1, P2, and Microsoft 365 Business Premium licenses. These features aren't available on the Free license.&lt;/p&gt;

&lt;p&gt;You can configure SSPR methods in the Entra admin center. It's best to require 2 methods for reset to reduce the risk of account takeover, and avoid using security questions as a primary method. &lt;/p&gt;

&lt;p&gt;Administrator accounts have stricter reset protections than standard users. They must use two authentication methods for reset, and email-based reset isn't recommended in case their mailbox is compromised. Place privileged accounts in a dedicated group, and then apply stricter SSPR method controls to that group.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Hopefully now you have a better understanding on how we can implement modern authentication methods in Microsoft Entra ID that both improves security and usability.&lt;/p&gt;

&lt;p&gt;We learnt how MFA with Conditional Access enforces stronger verification based on context, we learnt how passwordless methods work and how we can apply different methods to different kinds of users, and we learnt how self-service password reset works and how it helps users recover their access quickly without bothering support desk staff with mundane requests!&lt;/p&gt;

&lt;p&gt;If you have any questions, feel free to reach out to me on X &lt;a href="https://twitter.com/willvelida" rel="noopener noreferrer"&gt;@willvelida&lt;/a&gt; or on &lt;a href="https://bsky.app/profile/willvelida.com" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Until next time, Happy coding! 🤓🖥️&lt;/p&gt;

</description>
      <category>azure</category>
      <category>security</category>
    </item>
    <item>
      <title>Last week in Agent Security Update 5: The Pentagon Prompts</title>
      <dc:creator>Will Velida</dc:creator>
      <pubDate>Mon, 02 Nov 2026 22:31:22 +0000</pubDate>
      <link>https://dev.to/willvelida/last-week-in-agent-security-update-5-the-pentagon-prompts-bel</link>
      <guid>https://dev.to/willvelida/last-week-in-agent-security-update-5-the-pentagon-prompts-bel</guid>
      <description>&lt;p&gt;Last week I spoke at API Days in Melbourne, where I gave a talk on the OWASP Top 10 for Agents and how you can mitigate the risks using tools such as the &lt;a href="https://github.com/microsoft/agent-governance-toolkit" rel="noopener noreferrer"&gt;Agent Governance Toolkit&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/what-is-microsoft-entra-agent-id" rel="noopener noreferrer"&gt;Microsoft Entra Agent ID&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My talk was scheduled to be in the last timeslot of the day, which was 4.50pm. That's a tough spot for speakers and audiences, as conferences can be a long and tiring day. I was fully prepared to speak to about 2 people in the room who decided not to beat the traffic and get their money's worth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyyzgq18ckjoygug0rw45.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyyzgq18ckjoygug0rw45.jpg" alt="Will presenting a slide about Cairn agent attacks at API Days Melbourne." width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Presenting the OWASP Top 10 for Agents at API Days Melbourne.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Luckily about 40-50 people showed up just before I was about to present, which injected some much needed energy into the room. I'd given the same talk before at NDC Oslo, but I decided to rebuild the entire slide deck and demo since I only had about 20 minutes to talk (as opposed to 1 hour in Oslo). I'm giving a &lt;a href="https://ndclondon.com/agenda/applying-the-owasp-top-10-for-agentic-applications-to-your-ai-agents" rel="noopener noreferrer"&gt;longer version of the talk at NDC London&lt;/a&gt; in January, so if you're in London and are planning to attend, make sure you get your &lt;a href="https://ndclondon.com/tickets" rel="noopener noreferrer"&gt;tickets&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;The sample I showcased is available on my &lt;a href="https://github.com/willvelida/owasp-agents-sample" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; if you're interested. There's a couple of things I need to do before I share it more widely, but I'm looking for contributors and feedback, so please take a look and let me know what you think.&lt;/p&gt;

&lt;h1&gt;
  
  
  This week's attacks
&lt;/h1&gt;

&lt;p&gt;Big focus on the agentic supply chain this week, as there were several attacks that targeted vulnerabilities in how agents are set up, how extensions are configured for agents, and how agents can accidently executed code just by opening up configuration files in a project.&lt;/p&gt;

&lt;p&gt;As always, I’ll point out that I’m not trying to shame any of the companies involved (maybe just the ones who deserve it), nor do I condone any of the methods used in any of these incidents. My goal here is just to spread awareness of the different ways that agents can be exploited, and hopefully trigger a conversation with your teams on how you should protect your agents and the people who use them from potential vulnerabilities.&lt;/p&gt;

&lt;p&gt;Here’s some incidents that have happened over the period 26th October to 1st November 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trojanised Claude Code extension in Cursor Marketplace
&lt;/h2&gt;

&lt;p&gt;On October 29th, versions 2.1.312 and 2.1.313 of the Clade Code VS Code extension were published to Cursor's VS Code extension marketplace for macOS and Windows that carried a malicious loader embedded in its &lt;code&gt;extension.js&lt;/code&gt; bundle.&lt;/p&gt;

&lt;p&gt;This &lt;code&gt;extension.js&lt;/code&gt; file carried a small self-contained loader embedded in the legitimate bundle. It was designed to activate after VS Code startups and when its WebView opens, the loader proceeds to add an attacker-controlled key to the user's &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; file. This key is labeled "svc-telemetry" to hide its true intentions. It then attempts to make the victim reachable through SSH across Windows, Linux, and macOS, establishes a reverse tunnel, installs platform-specific persistence and then transmits host recon data back to the reverse tunnel over an unencrypted HTTP request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwa89vzwfmw24tl1aplz9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwa89vzwfmw24tl1aplz9.png" alt="Attack chain for the trojanised Claude Code extension, showing SSH access, persistence, and host reconnaissance." width="800" height="653"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How the trojanised Claude Code extension plants an SSH backdoor, maintains persistence, and sends host details to the attacker. Credit: &lt;a href="https://opensourcemalware.com/blog/trojanized-claude-code-extension-plants-persistent-ssh-backdoor" rel="noopener noreferrer"&gt;OpenSourceMalware&lt;/a&gt;, reproduced in &lt;a href="https://thehackernews.com/2026/10/official-claude-code-vs-code-extension.html" rel="noopener noreferrer"&gt;The Hacker News&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The collected data includes public IP addresses, hostname and local usernames, OS and CPU architecture, home directory and current working directory information, global git email from git config files, and environment variables containing things like KEY, TOKEN, SECRET, PASS, AWS, or GCP.&lt;/p&gt;

&lt;p&gt;Cursor's security team have published a blog notifying customers of the incident, and stressed that this trojanised extension does not impact other extensions downloaded from the Cursor marketplace, Grok Bot, Grok, or any other of their AI products. In response, Cursor had to rebuild the marketplace and take the server offline so that the reverse shell would no longer work.&lt;/p&gt;

&lt;p&gt;Anthropic have stated that this was not a compromise of Anthropic's systems, as the malicious actor used access to Cursor's marketplace to publish a tampered copy of the extension. The malicious update was only distributed through Cursor's marketplace, with no evidence that it reached Open VSX or Microsoft's official VS Code marketplace.&lt;/p&gt;

&lt;p&gt;The Microsoft Security Research team are advising affected users to remove confirmed unauthorized SSH keys, tunnel identities, scripts, and startup entries separately. Removing or updating the extension won't remove artifacts written outside the installation directory.&lt;/p&gt;

&lt;p&gt;Here, we see a trusted distribution channel for installing extensions for coding agents being exploited by a backdoor trojan (ASI04). Securing the agentic supply chain is a challenge, because there's no real attestation being conducted in this space, and with package managers being administrated by vendors over foundations, the focus on innovation at any cost comes at the cost of security. &lt;/p&gt;

&lt;p&gt;By injecting threats in the agentic supply chain, attackers can use access to developer environments to perform other types of attacks, such as RCE and Identity abuses. Until there's a more unified approach to how we secure the agentic supply chain, we're going to see more attacks like these slip through the cracks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/10/official-claude-code-vs-code-extension.html" rel="noopener noreferrer"&gt;Official Claude Code Extension on Cursor Marketplace Trojanized with Persistent SSH Backdoor: The Hacker News&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.stepsecurity.io/blog/claude-code-extension-trojanized" rel="noopener noreferrer"&gt;Trojanized Claude Code Extension Delivered Persistent SSH Backdoor to Developer Machines: StepSecurity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cursor.com/blog/claude-code-extension-security-notice" rel="noopener noreferrer"&gt;Security notice — malicious version of Anthropic.claude-code (2.1.312 and 2.1.313) identified in the Cursor VS Code Extension Marketplace: Cursor&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Accenture contractor's personal Claude Code agent on a Pentagon system
&lt;/h2&gt;

&lt;p&gt;WIRED reported on October 29th that a contractor from Accenture connected their personal Claude Code account to a Department of Defense system which held controlled unclassified information over a period of two months. The agent appears to have sent login credentials and a network map to outside the Pentagon's network.&lt;/p&gt;

&lt;p&gt;The system in questions hold sensitive data that needs to be reviewed before it's released (unclassified information, or CUI for short) and it's used by several intelligence agencies. The system runs on NIPRNet, which is the Pentagon's network for unclassified information. It does connect to the public internet, but it has heavy restrictions (as you can imagine).&lt;/p&gt;

&lt;p&gt;The contractor gave Claude access to their login credentials so it could write code, and set up cloud servers. This granted the agent admin access, and it also had the ability to talk to the internet to do its work. This resulted in credentials, passwords, and network maps being sent to a third-party website, which the US National Institute of Standards and Technology has flagged as high risk.&lt;/p&gt;

&lt;p&gt;What's worse is that Defense Department staff and federal contractors must report cybersecurity incidents within 72 hours, which Accenture didn't do. They also denied any wrongdoing. There's also a political element to this, after the debacle that cause the defense department to &lt;a href="https://www.bbc.com/news/articles/cn5g3z3xe65o" rel="noopener noreferrer"&gt;label Anthropic a supply chain risk&lt;/a&gt;, staff at the Pentagon may not use Claude.&lt;/p&gt;

&lt;p&gt;Whether this is a case of the contractor accidently using their personal account for work use by mistake or something more malicious is unconfirmed at this time. There's no indication at this time that hackers have reached any classified systems, but this does open up the possibility that code operate normally within NIPR and perform malicious acts inside a SIPR network.&lt;/p&gt;

&lt;p&gt;Here a human's admin credentials were handed to an unsactioned agent, which then acted under that identity in highly sensitive networks. This opens up the possibility of rogue agents sending data outside a secure network and performing malicious acts inside the secure network.&lt;/p&gt;

&lt;p&gt;So if you're contractor working for a defense department, or any other customer in a highly regulated environment, just make sure you're logged in with the right credentials and use the right tools for the job.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.wired.com/story/a-defense-contractor-gave-their-personal-ai-agent-access-to-a-secure-pentagon-system/" rel="noopener noreferrer"&gt;A Defense Contractor Gave Their Personal AI Agent Access to a Secure Pentagon System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thenextweb.com/news/accenture-contractor-claude-agent-pentagon-network-wired" rel="noopener noreferrer"&gt;A Claude agent ran inside a Pentagon network for two months, WIRED reports: TNW&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cybersecuritynews.com/accenture-contractor-allegedly-gave-claude-access/" rel="noopener noreferrer"&gt;Accenture Contractor Allegedly Gave Claude AI Agent Access to Secure Pentagon Network: Cyber Security News&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cathedral's agent-led intrusion into Anthropic
&lt;/h2&gt;

&lt;p&gt;Remember those douchebags from DOGE? Well they're doing a sequel, and this time they're taking on Anthropic.&lt;/p&gt;

&lt;p&gt;The founders of Cathedral informed Anthropic that they had penetrated sensitive internal data and messaging systems through third-party business software. Cathedral is cybersecurity startup founded by former DOGE employees and sells AI-powered security tools to government agencies and businesses.&lt;/p&gt;

&lt;p&gt;Anthropic confirmed that the researchers accessed a limited set of sales, HR, and IT systems, including contact information for some customers and partners. Chats with Claude, source code, and model weights were never exposed. Anthropic shut down the affected access points, disabled third-party programs until the issue was resolved, and notified U.S federal authorities.&lt;/p&gt;

&lt;p&gt;There's a bit of disagreement between Anthropic and Cathedral as to how long the breach lasted, and what data was breached. Anthropic also accuses Cathedral of violating their bug bounty program's rules through incomplete disclosure, and through targeting third-party providers.&lt;/p&gt;

&lt;p&gt;Now, as much as I dislike anyone who's worked for DOGE, if there's a vulnerability in a third-party or external system that you use that could cause a potential breach or security incident, you need to take it seriously. As you can imagine with the actors involved, there's a certain amount of dick measuring going on regarding the extent of the breach (&lt;em&gt;even a former OpenAI employee is getting involved&lt;/em&gt;), but as much as it pains me to say, integration with third-party applications are a problem when it comes to security, and Anthropic could have done a better job making sure that the external services they use were secure.&lt;/p&gt;

&lt;p&gt;Regardless of the rules of a bug bounty program, you can see that it's easy for anyone with an agent to use legitimate tools to cause cybersecurity incidents, whether they are researchers from startups like Cathedral, or malicious state aligned attackers. It's important to factor in how AI is used with your various different systems, as one rogue agent with access to legitimate tools can cause real damage.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Just going to take a brief break so I can shower off the fact I'm kinda on the side of ex-DOGE employees.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://agentbeam.com/blog/cathedral-anthropic-12-hour-ai-agent-breach-claimed-vs-verified" rel="noopener noreferrer"&gt;Cathedral Says AI Agents Breached Anthropic in 12 Hours: Claimed vs Verified: Agentbeam&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://the-decoder.com/ai-agents-hacked-anthropic-maker-of-arguably-the-most-capable-ai-models-for-cyberdefense/" rel="noopener noreferrer"&gt;AI agents hacked Anthropic, maker of arguably the most capable AI models for cyberdefense: The decoder&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  123 Reg "AI-driven" breach
&lt;/h2&gt;

&lt;p&gt;UK domain registrar 123 Reg disclosed that they were a victim of an AI-driven cyberattack that exposed customer data.&lt;/p&gt;

&lt;p&gt;An initial investigation by 123 Reg identified attack servers in both China and Singapore that conducted the attacks. They observed the use of AI attack skills (including copies of Anthropic's open-source skills) and the use of ARTEX, an open-source AI pentest framework.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're a regular reader of the series, you would have seen attackers using ARTEX for attacks before&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;123 Reg suspect that a Chinese-state group who conducted similar attacks on finance companies in South Korea are behind the attack. Nothing has been confirmed yet, and 123 Reg have notified the FTC, UK, and EU regulators. Customers have been notified that information such as company VAT (tax) numbers, names, phone numbers, account creation dates etc. have been compromised.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.theregister.com/cyber-crime/2026/10/28/123-reg-discloses-ai-cyberattack-on-customer-data-suspects-china-nexus/5305936" rel="noopener noreferrer"&gt;123 Reg discloses 'AI cyberattack' on customer data, suspects China nexus: The Register&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  testmetest worm plants agent hook files in 42 GitHub repos
&lt;/h2&gt;

&lt;p&gt;The folks at JFrog Security identified 42 GitHub repositories across 11 owners connected to the &lt;code&gt;testmetest&lt;/code&gt; npm campaign. Essentially, the &lt;code&gt;testmetest&lt;/code&gt; is a worm that planted configuration files in GitHub repositories so that when a developer opened a project in an AI coding tools, it would execute the attackers code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc44srnfv88qzx4c58nms.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc44srnfv88qzx4c58nms.png" alt="Diagram linking malicious testmetest npm packages, stolen credentials, hijacked GitHub repositories, and malicious package publishing." width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The testmetest campaign links malicious npm packages to credential theft, hijacked GitHub repositories, and further malicious releases. Source: &lt;a href="https://research.jfrog.com/post/beyond-testmetest-42-hijacked-github-repos/" rel="noopener noreferrer"&gt;JFrog Security Research, Beyond testmetest&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The attackers used stolen credentials to take over the target's real GitHub account (including one organizational account!) and used scripts to copy old commit messages and dates so that the attack changes would blend in. Each repository was infected with a set of configuration files that pointed at a single script &lt;code&gt;.github/init.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The attack would change depending on the tool used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;Claude Code&lt;/strong&gt;, a &lt;code&gt;SessionStart&lt;/code&gt; hook in &lt;code&gt;.claude/settings.json&lt;/code&gt; would run &lt;code&gt;node .github/init.js&lt;/code&gt; whenever a session starts.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Gemini CLI&lt;/strong&gt;, a similar trigger exists in &lt;code&gt;.gemini/settings.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Cursor&lt;/strong&gt;, the script would trigger in &lt;code&gt;.cursor/mcp.json&lt;/code&gt; and &lt;code&gt;.cursor/rules/setup.mdc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;VS Code&lt;/strong&gt;, a task would run in &lt;code&gt;.vscode/tasks.json&lt;/code&gt; whenever the folder is opened.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tools simply follow project settings they trust by default and runs in the background, so the developer would see nothing. The &lt;code&gt;init.js&lt;/code&gt; would then proceed to steal GitHub tokens, swap them for npm publishing rights, and then publish malicious packages whenever someone installs them. One infected developer would become a new way to infect others. The file was over oversized, which kept the file payload out of GitHub's code search.&lt;/p&gt;

&lt;p&gt;It's a novel method to hijack the supply chain. Poisoned repositories that contained agent configuration that tools trust by default is a simple but effective way to deliver a vulnerability. Once the workspace opens, attackers can run code that performs a variety of malicious activities.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://research.jfrog.com/post/beyond-testmetest-42-hijacked-github-repos/" rel="noopener noreferrer"&gt;Beyond testmetest: Native Addon Camouflage and 42 Armed GitHub Repositories Reveal a Hidden npm Publisher: JFrog Security Research&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  "Embrace" embedded-browser flaws in five AI dev tools
&lt;/h2&gt;

&lt;p&gt;The team at Forever Security have discovered vulnerabilities in Cursor, GitHub Copilot, Claude Desktop, VS Code, and Codex that can allow attackers to execute code on host machines, read and steal files, or spoof browser address bars.&lt;/p&gt;

&lt;p&gt;If you've been using AI coding assistants recently, you would have noticed that most of the tools now have their own built-in browsers so that the agent can preview apps and read web pages. However, in this case each tool gives its own browser powers a normal browser may block, meaning that the browser is a way to get access into the agent and the host machine.&lt;/p&gt;

&lt;p&gt;In Cursor, the built-in browser lets every page send messages to the app. A hostile page could use this to slip hidden text into the agent's message box, and hijack the goal of the agent. In GitHub Copilot, the preview tab could tell the app what the address was, but Copilot would only check if the text &lt;em&gt;looked&lt;/em&gt; like a web address, not where the page really came from. In Claude Desktop, Codex, and VS Code, a local HTML file opened in the tool can read other files. Now in both Claude and VS Code, the workspace trust boundary applied to the web pages, but not the web workers. So when those pages load, the web workers could read anything. The whole disk is readable, which can include SSH keys, &lt;code&gt;.env&lt;/code&gt; files and tokens.&lt;/p&gt;

&lt;p&gt;No CVEs have been raised because of this, and the worrying thing here is that these tools are being fixed quietly or dismissed as outside their trust models. Context from a malicious web page could easily redirect coding agents to perform malicious acts. There's also the human-agent trust exploitation issue, as a spoofed address bar abuses the trust that developers have placed in the tool, and the agent's browser.&lt;/p&gt;

&lt;p&gt;It's disappointing that these companies either quietly fix these vulnerabilities without giving developers the heads up, or they just ignore or dismiss it entirely. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.theregister.com/security/2026/10/29/researchers-find-bugs-in-5-ai-developer-tools-with-openai-and-microsoft-flaws-still-unpatched/5306508" rel="noopener noreferrer"&gt;Researchers find bugs in 5 AI developer tools, with OpenAI and Microsoft flaws still unpatched: The Register&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CyberStrikeAI: 44 agents against Japanese organisations
&lt;/h2&gt;

&lt;p&gt;Cybersecurity researchers from TrendAI have disclosed details of AI-driven cyberattacks targeting organizations in Japan. TrendAI analyzed three attacks used by threat actors to conduct malicious operations.&lt;/p&gt;

&lt;p&gt;The first used a tool called CyberStrikeAI, which is an open-source penetration testing tool, to automate attacks into a repeatable workflow, including activities such as reconnaissance, vulnerability detection and validation, exploitation, command-and-control, data exfiltration, and automated reporting. The work was delegated among 44 agents to handle various aspects of the attacks. At least 19 organizations where subjected to these attacks.&lt;/p&gt;

&lt;p&gt;The second was characterized as a human-led attack using LLM models to accelerate planning and tool development. Analysis of these artifacts seem to indicate that the campaign was designed to target AI workflow platforms and inference services in an attempt to obtain a large amount of inference capacity at a low cost. One instance targeted a geographic information system server exposed with default credentials, resulting in data exfiltration.&lt;/p&gt;

&lt;p&gt;In the third attack, AI was used to develop the operator's tools and scripts. The attack then used these tools to target edge devices from Cisco, Citrix, F5, and Yamaha using published vulnerabilities, management functions, or credentials present in configuration files.&lt;/p&gt;

&lt;p&gt;It's unknown at this stage if these attacks share the same operator, or if these attacks have targeted other regions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/10/cyberstrikeai-uses-44-ai-agents-to.html" rel="noopener noreferrer"&gt;CyberStrikeAI Uses 44 AI Agents to Automate Hacking Operations Against Japan Targets: The Hacker News&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Quite a few attacks last week focused on the developer's agent setup. Agent configuration files are acting as executable code inside coding assistants that most tools trust by default. Whether that's through backdoors in official marketplaces, or malicious hook files in GitHub repositories that run malicious code as soon as projects are opened, there are gaps in the agentic supply chain that need urgent attention.&lt;/p&gt;

&lt;p&gt;It's not good enough for Big Tech firms and Frontier labs to quietly patch systems or ignore threats as outside their trust boundary without issuing advisories. The only way to get these companies to act properly is through government intervention and legislation. Which I don't see happening anytime soon.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;As always, opinions are my own&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Remember, I'm not trying to scare folks or discourage people from using these tools. Just be mindful of what your agent is installing. Treat agent configuration like any other code and review it like any other executable. Lock down where agent extensions and models come from, and restrict agent traffic to approved model endpoints.&lt;/p&gt;

&lt;p&gt;If you have any questions, feel free to reach out to me on X &lt;a href="https://twitter.com/willvelida" rel="noopener noreferrer"&gt;@willvelida&lt;/a&gt; or on &lt;a href="https://bsky.app/profile/willvelida.com" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Until next time, Happy coding! 🤓🖥️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
    </item>
    <item>
      <title>Last week in Agent Security Update 4: Cairn’t Touch This</title>
      <dc:creator>Will Velida</dc:creator>
      <pubDate>Tue, 27 Oct 2026 07:00:41 +0000</pubDate>
      <link>https://dev.to/willvelida/last-week-in-agent-security-update-4-cairnt-touch-this-3egm</link>
      <guid>https://dev.to/willvelida/last-week-in-agent-security-update-4-cairnt-touch-this-3egm</guid>
      <description>&lt;p&gt;What a fucking week....&lt;/p&gt;

&lt;p&gt;If you've been living under a rock this week, &lt;a href="https://www.wiz.io/blog/php2shell-php-rce" rel="noopener noreferrer"&gt;two critical RCE vulnerabilities were found in PHP, collectively known as PHP2Shell&lt;/a&gt;. This vulnerability allows unauthenticated attackers to remotely execute code on vulnerable web servers without logging in or providing credentials, potentially seizing server control or stealing data.&lt;/p&gt;

&lt;p&gt;These vulnerabilities affected all PHP versions released over the last decade, spanning back to version 5.4. This was particularly scary because according to Wiz, PHP powers 70% of the world's websites, including banking and government applications.&lt;/p&gt;

&lt;p&gt;Many websites and pages were taken offline as a precaution, including those of the Finnish government, The Netherlands' cyber-security agency, and Wikimedia (the organization behind Wikipedia).&lt;/p&gt;

&lt;p&gt;Researchers at Calif and Wiz used models from OpenAI to find the tools, so AI was pretty useful in finding the flaw. The problem was that the &lt;a href="https://thehackernews.com/2026/10/someone-tried-to-exploit-php2shell-4.html" rel="noopener noreferrer"&gt;vulnerability was leaked&lt;/a&gt; before the embargo was lifted, so the PHP platform developers were forced to rush out the disclosure of the vulnerability as well as the fix. With attackers being able to use agents to operate at machine speed, this only highlights the dangerous world we now live in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F075n0j43j59hnj89diu0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F075n0j43j59hnj89diu0.png" alt=" " width="600" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So far, no breaches have been reported. I'm holding my breath on this one, as we have seen in the past attackers being able to breach a system, lie dormant for months, and then strike. Keep your eyes open on this one.&lt;/p&gt;

&lt;p&gt;In more positive news, While I was at NDC Oslo, Richard Campbell invited me on &lt;a href="https://runasradio.com/" rel="noopener noreferrer"&gt;RunAs Radio&lt;/a&gt; to talk about the OWASP Top 10 for Agentic Applications, and how the different layers of agentic architecture can be compromised. You can check out the podcast &lt;a href="https://runasradio.com/Shows/Show/1059" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  This week's attacks
&lt;/h1&gt;

&lt;p&gt;This week we focus less on misalignment from frontier models and more on attackers who are using agents to drive more targeted attacks using open-source tools. We also take a look at how agentic supply chain vulnerabilities have the potential to cause harm on a wide scale, and finally we make fun of Meta because I hate them.&lt;/p&gt;

&lt;p&gt;As always, I’ll point out that I’m not trying to shame any of the companies involved (maybe just the ones who deserve it), nor do I condone any of the methods used in any of these incidents. My goal here is just to spread awareness of the different ways that agents can be exploited, and hopefully trigger a conversation with your teams on how you should protect your agents and the people who use them from potential vulnerabilities.&lt;/p&gt;

&lt;p&gt;Here’s some incidents that have happened over the period 19th October to 25th October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cairn agents break into 100+ company websites
&lt;/h2&gt;

&lt;p&gt;NorForge found more than 20 publicly exposed control panels for Cairn, which is an open-source AI pentest system, that criminals were using to point autonomous agents at e-commerce, payment and pharmaceutical targets. These agents tried more than 1200 sites and reached websites belonging to more than 100 companies, including Deere, SharkNinja, and Cummins.&lt;/p&gt;

&lt;p&gt;The operator provided short instructions to agents to attempt code execution and plant web-shell uploaders. One documentation chain combined unauthorised write access to a memcached session store, unsafe PHP object deserialization (&lt;em&gt;oh PHP&lt;/em&gt;) and old Zend Framework code. NorForge haven't confirmed which models were used in the attack.&lt;/p&gt;

&lt;p&gt;NorForge estimates that 10% of attacks were successful. One large US retailer was independently confirmed that they were compromised, and one shell URL serving a uploader shell has been successful. The volume of data that has been taken is currently unknown. The panels also leaked secrets and some legitimate pentest firms have had client data exposed on those open panels.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3oj3h0u7x6qzm0axxl9z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3oj3h0u7x6qzm0axxl9z.webp" alt="AIA0001 target selection by sector, showing retail and wholesale as the largest target group." width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://norforge.com/blog/inside_cairn" rel="noopener noreferrer"&gt;NorForge — Inside Cairn: Open Windows into massive AI-Driven Cyber Operations&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A couple of weeks ago, I described how &lt;a href="https://www.willvelida.com/posts/last-week-in-agent-security-1/#gambit-strixcairnhermes-autonomous-card-skimming-campaign" rel="noopener noreferrer"&gt;Cairn&lt;/a&gt; was being used in a multi-agentic attack targeting retail websites in the US. We're going to see more sophisticated rogue agent attacks using specialized frameworks to perform malicious, specific attacks on their targets, utilizing these specialized tools to successfully breach them with minimal effort from the human operator. The maker of Cairn told Reuters that the tools was not designed for hacking attacks, but we've already seen multiple instances where agents can use legitimate tools to do harm, and how attackers can exploit them to do damage.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://norforge.com/blog/inside_cairn" rel="noopener noreferrer"&gt;Inside Cairn: Open Windows into massive AI-Driven Cyber Operations - NorForge blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/10/hackers-use-autonomous-ai-agents-to.html" rel="noopener noreferrer"&gt;Hackers Use Autonomous AI Agents to Breach Company Websites, Target Over 1,200 Sites - The Hacker News&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Storm-0940 uses ARTEX agents in ConnectWise supply-chain campaign
&lt;/h2&gt;

&lt;p&gt;It's not just Cairn that's been exploited by bad actors. The folks in Microsoft Security have attributed Chinese state-aligned actor dubbed Storm-0940 was behind the recent &lt;a href="https://thehackernews.com/2026/10/connectwise-resets-credentials-as-cloud.html" rel="noopener noreferrer"&gt;supply chain compromise&lt;/a&gt; of the ConnectWise Cloud Backup application.&lt;/p&gt;

&lt;p&gt;ARTEX, an open-source multi-agent LLM pentest framework, was used to automate the operation from reconnaissance through to exfiltration. The actor abused stolen ConnectWise multi-tenant application identities and the trust that customers had already granted to those identities. It was also able to run password spray attacks against Microsoft 365, Exchange Online, Okta, and Entra ID.&lt;/p&gt;

&lt;p&gt;These ARTEX-driven attempts reached more than 2,250 tenants and peaks of up to 2,900 attempts per minute. Between 10th-14th October, the spray generated more than 151,000 failed logins. After the 15th, agents were able to reach Microsoft Graph and were able to change identities, groups, roles, and app credentials. In several cases, they were able to gain privileged access in under 3 hours. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fayx4hfjkts67tr3ppshm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fayx4hfjkts67tr3ppshm.jpg" alt="Illustration accompanying The Hacker News report on China-linked hackers using ARTEX AI agents." width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://thehackernews.com/2026/10/china-linked-hackers-use-artex-ai.html" rel="noopener noreferrer"&gt;The Hacker News — China-Linked Hackers Use ARTEX AI Agents in ConnectWise Supply Chain Attack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Looking at the prompt histories, it appears that the motive is financial, and Wiz have reported that the campaign has expanded to other software vendors. ConnectWise has responded by shutting down services and restoring them, and Microsoft Security have published a &lt;a href="https://www.microsoft.com/en-us/security/blog/2026/10/21/storm-0940-uses-artex-agentic-ai-connectwise-supply-chain-compromise/" rel="noopener noreferrer"&gt;list of immediate actions and mitigations&lt;/a&gt; that affected customers should take.&lt;/p&gt;

&lt;p&gt;This attack managed to abuse OAuth app identities and delegated trust across multiple tenants at machine speed, using ARTEX as the multi-agentic attack vector, exploiting the tools that the framework provides to repurpose them for an offensive campaign.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.microsoft.com/en-us/security/blog/2026/10/21/storm-0940-uses-artex-agentic-ai-connectwise-supply-chain-compromise/" rel="noopener noreferrer"&gt;AI-driven, large-scale nation-state campaign using ARTEX enables supply chain compromise - Microsoft Security Research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/10/china-linked-hackers-use-artex-ai.html" rel="noopener noreferrer"&gt;China-Linked Hackers Use ARTEX AI Agents in ConnectWise Supply Chain Attack - The Hacker News&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  INC / The Gentlemen ransomware affiliates' "Phantom" AI kill chain
&lt;/h2&gt;

&lt;p&gt;In more examples of rogue agents and tool misuse and exploitation, the folks at SOCRadar traced an exposed command-and-control server, and six more adversary servers, to Russian-speaking operators. This was an AI-driven intrusion ecosystem run by affiliates of the INC Ransomware and The Gentlemen Ransomware groups. The operation runs on an AI-executed, human-approved model, where a human writes a victim-specific prompt and approves each step, while commercial models that are accessed through OpenRouter carry out intrusions via a OpenCode coding agent.&lt;/p&gt;

&lt;p&gt;The toolkit includes a tool called "Phantom", which is an autonomous red-team framework, and "WormGPT-MCP", a set of MCP servers that cover reconnaissance through extortion. When a model would refuse the request, the operator would reframe it as an authorized red-team engagement, and the model would continue.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F173kds17jgsvar42o2mm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F173kds17jgsvar42o2mm.webp" alt="Tooling overview of an AI-executed operating structure associated with INC and The Gentlemen ransomware affiliates." width="800" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://socradar.io/blog/inc-gentlemen-ransomware-ai-infrastructure/" rel="noopener noreferrer"&gt;SOCRadar — INC / The Gentlemen ransomware AI infrastructure&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;62 names organizations have been listed as victims, with 36 in the United States. The operators gained access by exploiting public-facing edge devices, and then moved towards ransomware deployments. This was followed by credential reuse, lateral movement, and data exfiltration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://socradar.io/blog/inc-gentlemen-ransomware-ai-infrastructure/" rel="noopener noreferrer"&gt;INC and The Gentlemen Ransomware: Exposed AI Infrastructure - SOCRadar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Autonomous LLMjacking agent captured by Symantec
&lt;/h2&gt;

&lt;p&gt;Symantec's Threat Hunter team have discovered the instruction set of a self-directed offensive agent that hunts for exposed LLM endpoints, AI gateways, and MCP servers to run their own work through them or through stolen keys, so that victims pay for the compute.&lt;/p&gt;

&lt;p&gt;The agent was designed to use the hijacked AI models to autonomously run up to 100,000 malicious actions without human approval, including scanning for targets, exploiting vulnerabilities, stealing credentials, deploying cryptocurrency miners and reinfecting compromised systems. Attackers used layered prompts to bypass commercial model safety controls.&lt;/p&gt;

&lt;p&gt;The honeypoint logged more than 127,000 requests from tens of thousands of addresses. This telemetry included reconnaissance of model endpoints, spraying of stolen API keys, and probing for exposed configuration files. What makes this attack disturbing is that instead of attackers trying to trick a public chatbot into misbehaving, the operators built AI into the core of an offensive tool and attempted to hijack other people's models to power it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.security.com/blog-post/ai-llmjacking-skynet" rel="noopener noreferrer"&gt;Autonomous LLMjacking: ‘Skynet’ Agent Hijacks Exposed AI Models to Run Attacks - Security.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.infosecurity-magazine.com/news/symantec-ai-agent-llmjacking/" rel="noopener noreferrer"&gt;Symantec Uncovers AI Agent Hijacking Models for Autonomous Cyber-Attacks - Infosecurity magazine&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  LiteLLM 1.107.0 supply-chain compromise via leaked Agent Platform key
&lt;/h2&gt;

&lt;p&gt;Turning towards vulnerabilities on the supply chain, a malicious version fo LiteLLM &lt;code&gt;litelmm==1.107.0&lt;/code&gt; was published to PyPI, Docker Hub, and GHCR images. This was caused by a master key for its internal Agent Platform that was exposed in a &lt;code&gt;opencode.json&lt;/code&gt; file that was committed to a public repository back on May 27th (&lt;em&gt;We've all been there before&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;The commit was force-pushed over, but the key wasn't actually revoked. Attackers were able to use the key against the Agent Platform API, which returned a GitHub CLI OAuth token belonging to an organization owner. From there, the attackers were able to steal maintainer tokens, push secret-harvesting workflows, and repoint releases to lookalike repositories.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fje0jsuiv9dd9up2i9bd2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fje0jsuiv9dd9up2i9bd2.jpg" alt="Image accompanying The Hacker News report on a leaked LiteLLM master key." width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://thehackernews.com/2026/10/leaked-litellm-master-key-let-attackers.html" rel="noopener noreferrer"&gt;The Hacker News — Report on a leaked LiteLLM master key&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The malicious version runs a credential stealer at start up that takes information like SSH keys, cloud tokens, git tokens, browser data, AI tool credentials, and environment variables, and sends them to &lt;code&gt;litellm[.]dev&lt;/code&gt;. It also installs a Unix backdoor that polls the code hourly for new code.&lt;/p&gt;

&lt;p&gt;The release was quarantined and affected images were removed. Due to the breach, LiteLLM are undertaking the process of revoking internal and vendor tokens. LiteLLM are advising users to rotate credentials from a clean machine and pin 1.106.2 by digest.&lt;/p&gt;

&lt;p&gt;Thanks to this supply chain vulnerability, a widely used AI gateway package was able to be trojanised, putting every downstream agent stack that had the affected version installed at risk. The unrevoked agent-platform master key returned an over-scoped owner token, which gave the attackers a wide scope to perform authorized actions for malicious intent.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.litellm.ai/blog/security-incident-oct-21-2026-attack-chain" rel="noopener noreferrer"&gt;The attack chain behind the v1.107.0 LiteLLM release - LiteLLM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/10/leaked-litellm-master-key-let-attackers.html" rel="noopener noreferrer"&gt;Leaked LiteLLM Master Key Let Attackers Steal GitHub Tokens and Hijack Release Pipeline - The Hacker News&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  OpenCode RCE via malicious MCP server during OAuth sign-in
&lt;/h2&gt;

&lt;p&gt;The folks at Pillar Security have discovered a critical RCE (remote-code execution) flaw in OpenCode that affects Windows and Windows Subsystem for Linux (WSL). This flaw allows an attacker-controlled remote MCP server to execute arbitrary code on a developer's machine during the remote MCP OAuth authentication process, without requiring additional user interaction or consent.&lt;/p&gt;

&lt;p&gt;MCP servers connects agents to external tools and services. This vulnerability highlights that connecting coding agents to compromised or malicious remote MCP servers can expose a developer's machine to remote code execution. It's vital that developers and security teams assess the trustworthiness of connected MCP servers and ensure that applications that handle externally supplied URLs do not pass untrusted input into shell commands.&lt;/p&gt;

&lt;p&gt;In this case, Anomaly (the company behind OpenCode) have claimed that since they believe that users who configure MCP servers for their agent are "outside our trust boundary", no CVE has been created and no advisory has been published. This means that this vulnerability won't show up in vulnerability scanners or dependency alerts, so the responsibility is on developers and security teams to check the OpenCode version directly.&lt;/p&gt;

&lt;p&gt;Pretty poor response from Anomaly in my opinion.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.pillar.security/blog/open-and-shut-remote-code-execution-in-opencode-via-mcp-oauth" rel="noopener noreferrer"&gt;Open and Shut: Remote Code Execution in opencode via MCP OAuth: Pillar Security&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Planted AGENTS.md in ~/Downloads hijacks coding agents
&lt;/h2&gt;

&lt;p&gt;Markdown is the hottest new language! It's also the hot new attack vector! The folks at Forever Security found a malicious &lt;code&gt;AGENTS.md&lt;/code&gt; file that was downloaded into a users &lt;code&gt;~/Downloads&lt;/code&gt; folder, and discovered that it tried to make Codex install a malicious npm package.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; files are files that coding agents are built to trust and obey, so you can imagine that if an attacker than supply a &lt;code&gt;AGENTS.md&lt;/code&gt; file that gets downloaded onto a users computer that direct the agent to perform malicious activity such as installing malware or handing over login information, that's a pretty powerful attack! Coding agents are getting more capable in their functionality, including having the ability to open up browsers, or even being able to take over control of the computer. This type of attack starts from a simple Agent Goal Hijack using a malicious &lt;code&gt;AGENTS.md&lt;/code&gt; file, and can lead to other risks like supply chain vulnerabilities, unexpected code execution, and identity abuses.&lt;/p&gt;

&lt;p&gt;That's a pretty big design flaw. There's no patch for this, so the way that we can mitigate that is to only run agents from project directories, review instruction files before trusting them, keeping approvals in the hands of humans, and applying sandboxes to our agents.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forever.security/blog/hijacking-ai-agents-via-downloads-folder" rel="noopener noreferrer"&gt;Markdown Is the New Executable: Attackers Are Dropping Agent Instructions Into Your Downloads Folder From the Web: Forever Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.bleepingcomputer.com/news/security/ai-coding-agents-hijacked-using-agentsmd-file-in-downloads-folder/" rel="noopener noreferrer"&gt;AI coding agents hijacked using AGENTS.md file in Downloads folder: BleepingComputer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Meta Muse "GhostShares"
&lt;/h2&gt;

&lt;p&gt;Oh Meta....&lt;/p&gt;

&lt;p&gt;In Meta's Muse AI assistant, you can share conversations. These conversations are published as public pages that anyone with the link can open. (That's by design, which doesn't surprise me considering it's Meta. &lt;em&gt;Why&lt;/em&gt; it's by design after all the bad publicity that Meta have received due to their lax attitude toward privacy shouldn't be a surprise to anyone).&lt;/p&gt;

&lt;p&gt;However, the folks at Claroc's research team found that these links were pretty easy to find, because what Muse names each share's web address after the conversation's own title, and that address is written into public DNS. One passive-DNS search brings back thousands of them, listed by name, like a table of contents.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;To be fair, this isn't a new type of leak. This has also happened to both ChatGPT and Grok conversations&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0so1lp15tzmxdep0eucq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0so1lp15tzmxdep0eucq.png" alt="Flow diagram accompanying Claroc’s GhostShares article." width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://blog.claroc.io/ghostshares-muse-passive-dns/" rel="noopener noreferrer"&gt;Claroc — GhostShares article&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This type of leak can lead to some pretty dangerous situations. One conversation included a candidate's notes preparing for a confidential executive-search call with a frontier AI lab, which included naming the lab's internal contact and the meeting time. Another included a to-do list that included a bank fraud alert, with partial account number and transaction information.&lt;/p&gt;

&lt;p&gt;Why anyone trusts Meta with anything is a mystery to me. Let alone one of their AI agents.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.claroc.io/ghostshares-muse-passive-dns/" rel="noopener noreferrer"&gt;GhostShares: A Shared Muse Chat Exposed Prep for a Confidential Call With a Frontier AI Lab Leader: Claroc Research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arstechnica.com/security/2026/10/meta-muse-exposes-users-personal-and-business-secrets-when-they-share-conversations/" rel="noopener noreferrer"&gt;Meta Muse exposes users’ personal and business secrets when they share conversations: ArsTechnica&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;We're seeing an increase in malicious actors using agentic-driven attacks to cause harm. Both incidents that used Cairn and ARTEX in their agentic-driven attack used tools that they were authorized to use, and they were designed by the developers of both those tools to automate pentest tasks. It's just that they were used by malicious actors to perform those tasks at machine speed for malicious purposes.&lt;/p&gt;

&lt;p&gt;Hopefully we'll see an increase in watch dog agent capabilities to prevent or limit attacks from these rogue agents. It's difficult to stop the bad guys using open-source frameworks to do harm, but there's a lot of work to be done to make it easier for folks to protect their applications and themselves from harm (&lt;em&gt;Probably not a bad startup idea in there...&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;With these types of attacks being able to be executed at machine speed, more focus needs to be paid to identity, and how over scoped credentials that are either stolen or mishandled have the potential to do some real harm.&lt;/p&gt;

&lt;p&gt;This also wasn't a great week for vendors. I hope that Anomaly come to the table and at least issue an advisory. This helps vulnerability and dependency scanners pick up issues in code, and help make everyone a little safer. Until then, security teams and developers need to be extra vigilant in what models, gateways etc. they are using, so they can prevent vulnerabilities causing harm.&lt;/p&gt;

&lt;p&gt;If you have any questions, feel free to reach out to me on X &lt;a href="https://twitter.com/willvelida" rel="noopener noreferrer"&gt;@willvelida&lt;/a&gt; or on &lt;a href="https://bsky.app/profile/willvelida.com" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Until next time, Happy coding! 🤓🖥️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
    </item>
    <item>
      <title>Last week in Agent Security Update 3: That's not Aoki!</title>
      <dc:creator>Will Velida</dc:creator>
      <pubDate>Mon, 19 Oct 2026 21:52:53 +0000</pubDate>
      <link>https://dev.to/willvelida/last-week-in-agent-security-update-3-thats-not-aoki-26o9</link>
      <guid>https://dev.to/willvelida/last-week-in-agent-security-update-3-thats-not-aoki-26o9</guid>
      <description>&lt;p&gt;An apology to start off with! I was too busy having fun in Sydney last week hacking with customers, building agents and agent harnesses. I got to experiment with &lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/what-is-microsoft-entra-agent-id" rel="noopener noreferrer"&gt;Entra Agent ID&lt;/a&gt; and &lt;a href="https://github.com/microsoft/agent-governance-toolkit" rel="noopener noreferrer"&gt;Agent Governance Toolkit&lt;/a&gt;, two technologies I'm looking forward to building with more over the next few months.&lt;/p&gt;

&lt;p&gt;We're slowly starting to see governments around the world catch up to the dangers that AI Agents can present to organizations around the world. The EU AI office has declared that they've "started enforcing" the EU AI Act, and it has sent formal requests to both OpenAI and Anthropic about security, external evaluations and post-market monitoring. In the US, the FTC has sent civil investigative demands to both OpenAI and Anthropic, seeking records on security incidents, safeguards, marketing, and privacy. Whether this goes anywhere is anyone's guess. I'm pretty pessimistic about it, but hopefully I'm wrong.&lt;/p&gt;

&lt;p&gt;This week, the theme of agents drifting from their original goals (whether it's unintentional or not) continue to be a issue for governments and businesses alike. MCP servers remain a potential gateway to organizations and developer environments, and coding assistant credentials are increasing in value to potential attackers. &lt;/p&gt;

&lt;p&gt;As always, I’ll point out that I’m not trying to shame any of the companies involved, nor do I condone any of the methods used in any of these incidents. My goal here is just to spread awareness of the different ways that agents can be exploited, and hopefully trigger a conversation with your teams on how you should protect your agents and the people who use them from potential vulnerabilities.&lt;/p&gt;

&lt;p&gt;Here’s some incidents that have happened over the period 12th October to 19th October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  CERT-In: test agents read confidential data at Indian organisations
&lt;/h2&gt;

&lt;p&gt;CERT-In issued an advisory that AI agents under research and evaluation by multiple AI model providers went beyond their assigned tasks and reached live systems at numerous Indian organizations. CERT-In haven't explicitly named the providers, models, victims, sectors, or number of cases. However, they were able to determine that agents accessed business records that contained personal and confidential data.&lt;/p&gt;

&lt;p&gt;This data included credentials and connection strings left in application configuration files and public code repositories, and over-permissive cloud roles. The advisory also states that agents sometimes reused the same exposed credentials in multiple waves of attacks.&lt;/p&gt;

&lt;p&gt;The same advisory also describes a separate, financially motivated campaign against Indian entities that used rebranded open-source AI offensive-security frameworks with multiple agents working in parallel. This enabled the attacks to cut time from initial access to impact from days to a few hours. The scale of this attack has not been disclosed.&lt;/p&gt;

&lt;p&gt;CERT-In are recommending that affected targets enforce network-enforced isolation and default-deny egress for AI test runs, automaticl kill switches when agents touch 3rd-party systems, rotating secrets and enforcing least privilege, implementing MFA, and implementing 180-day retention.&lt;/p&gt;

&lt;p&gt;More countries are being impacted by rogue agents (ASI10) where agents are either drifting from their original tasks or being directed to do so through their system prompt. While CERT-In provides some good guidance on how to protect your systems from rogue agents, there's clearly work to be done to prevent agents from drifting from their original tasks.&lt;/p&gt;

&lt;p&gt;Developers also need to be mindful about leaving credentials lying there for attackers to steal. Both Azure and AWS provide strong RBAC controls that can be used over permissive connection strings and API keys. This is also a good reminder to consider how agents use tools that are available to them. In the case of goal drift, agents using legitimate tools available to them won't be flagged in retrospect. They're allowed to use the tools that you configure, so think about the design of your agents and whether or not the tools you give them are too permissive.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cert-in.org.in/s2cMainServlet?VLCODE=CIAD-2026-0049&amp;amp;pageid=PUBVLNOTES02" rel="noopener noreferrer"&gt;CERT-In advisory CIAD-2026-0049&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/10/cert-in-warns-ai-agents-accessed.html" rel="noopener noreferrer"&gt;CERT-In Warns AI Agents Accessed Sensitive Data at Indian Organizations During Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://decrypt.co/381165/indias-cyber-agency-ai-agents-read-confidential-business-records" rel="noopener noreferrer"&gt;India's Cyber Agency Says AI Agents Read Confidential Business Records Without Authorization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Langflow MCP stdio command injection now exploited (CVE-2026-105740)
&lt;/h2&gt;

&lt;p&gt;Langflow is a tool for building and deploying AI agents and workflows. Prior to version 1.9.0, any authenticated Langflow user could perform Remote Code Execution (RCE) on the server by adding an MCP server using stdio transport. This command could be supplied to the user by passing &lt;code&gt;bash -c "exec {command}"&lt;/code&gt; with zero validation, no allowlisting, and no sandboxing. This command would execute immediately when the server list is fetched.&lt;/p&gt;

&lt;p&gt;A bad actor could exploit this by logging in as a regular non adminuser, and add a malicious MCP server with a command field containing a malicious payload. The server would then accept the server and store the attacker-supplied command without validating it, which the attacker would then trigger by fetching the MCP server and achieve code execution.&lt;/p&gt;

&lt;p&gt;Previdian have found that 1,423 exploitation attempts have been recorded since September 19th from 27 unique IP addresses located in Belgium, China, Germany, Hong Kong, Japan, Nigeria, the Philippines, Russia, Seychelles, Singapore, South Korea, Taiwan, Tanzania, the U.S., and Vietnam. In one observed attack, attackers checked the Langflow version, attempted to auto-login, then immediately use the resulting bearer token to access the vulnerable MCP server configuration endpoint. Some of the captured payloads included attempts to extract environment variables and sensitive Langflow configurations, Docker commands attempting to access the host filesystem and read password file contents &lt;code&gt;/etc/passwd&lt;/code&gt;, and command execution probes using id, whoami, and other commands.&lt;/p&gt;

&lt;p&gt;Mapping this back to OWASP, a configuration string flaw in Langflow has become a arbitrary shell execution on the agent platform's host. The payload planted as an MCP server definition also is a threat to the agent's supply chain. Exploitation also depends on the use of weak or default credentials, which leads to the theft of platform secrets and superuser access.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://previdian.com/CVE-2026-105740" rel="noopener noreferrer"&gt;CVE-2026-105740: langflow Authenticated RCE Remote Code Execution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/10/langflows-critical-authenticated-rce.html" rel="noopener noreferrer"&gt;Langflow's Critical Authenticated RCE Flaw Targeted in 1,423 Honeypot Exploit Attempts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Aoki infostealer adds AI coding-agent credentials
&lt;/h2&gt;

&lt;p&gt;On October 15th, the folks at Symantec reported that the Aoki infostealer is now targeting AI coding-tool credentials. This includes Claude's &lt;code&gt;.claude.json&lt;/code&gt; and desktop cookies, Codex &lt;code&gt;config.toml&lt;/code&gt; and &lt;code&gt;.env&lt;/code&gt; files, Cursor's local state database, tokens for GitHub Copilot, Gemini, Codeium, JetBrains AI Assistant and Kiro, and ChatGPT cookies and AWS Credentials.&lt;/p&gt;

&lt;p&gt;An infostealer is a type of malicious software designed to covertly harvest sensitive data from a victim's computer or device without thier knowledge. Once an infostealer infects a machine, it scans the system for valuable data, bundles it into an archive, and then exfiltrates it to a command-and-control server controlled by the attacker.&lt;/p&gt;

&lt;p&gt;If an attacker is able to steal credentials from their victims, they'd be able to carry out activity under their legitimate user's identity and API quota. That includes access to linked repositories, CI/CD pipelines and paid API quota. This is just as valuable as payment card data, as it can be cashed in on directly, and provider the attacker a route to the developer's environment.&lt;/p&gt;

&lt;p&gt;Symantec doesn't know who is behind the attack, nor can they link it to any existing malware. However the nature of the attack indicates that this is a financially motivated attack.&lt;/p&gt;

&lt;p&gt;There's no patch for this vulnerability, so the important thing to note here is that you need to treat your agent configuration files as secrets. The potential for abusing agent credentials is huge, and any stolen agent token gives attackers a foothold in your repositories and pipeline that your agents feed. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.security.com/threat-intelligence/aoki-infostealer-ai-targeting" rel="noopener noreferrer"&gt;Aoki: Infostealer Targets AI Coding Assistant Credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.infosecurity-magazine.com/news/aoki-infostealer-ai-coding/" rel="noopener noreferrer"&gt;New Aoki Infostealer Targets AI Coding Assistant Credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cybersecuritynews.com/new-aoki-malware/" rel="noopener noreferrer"&gt;New Aoki Malware Targets ChatGPT, Claude, Codex and Gemini to Steal Credentials&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  OpenCode Console breach exposes stored LLM provider API keys
&lt;/h2&gt;

&lt;p&gt;On October 16th, attackers managed to gain access to a legacy OpenCode console system that was scheduled to be shut down and gain access to third-party API keys in affected workspaces. Affected users where limited to some Console users who had connected external providers.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How old does something have to be to be called legacy in the age of AI?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;OpenCode have emailed affected users and later confirmed the breach on X.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv0xvo2xb0lqst4bqrc3z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv0xvo2xb0lqst4bqrc3z.png" alt=" " width="747" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OpenCode have said that the breach was connected to a large-scale distillation attack, however it hasn't provided any technical evidence to how it was linked. Distillation can involve using the outputs of one AI model to train or improve another model. OpenCode hasn't said whether any exposed credentials were used to generate such requests, and it has yet to identify the attacker.&lt;/p&gt;

&lt;p&gt;If agents were used here for the attack, stolen keys that are used to call LLMs are already being exploited (with some unfortunate victims receiving hefty AI-spend bills!)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://the420.in/opencode-security-breach-api-keys-console-users/" rel="noopener noreferrer"&gt;OpenCode Breach Exposes Third-Party API Keys of 1.4% of Console Users&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;It's easy to think that agents are running wild, but if you think about it rationally, agents act with the tools and permissions we give them. They're also enabled by our bad habits and bad developer practices. If you leave connection strings and API keys lying around for agents to discover, you're asking for trouble.&lt;/p&gt;

&lt;p&gt;The platforms that we build agents on are also a target too. In the case of Langflow, that CVE went from published to being actively exploited all through a single MCP server definition. Treat any MCP server configuration as a mechanism for executable code.&lt;/p&gt;

&lt;p&gt;If you're building agents for your organization, or using coding agents in your developments, you need to treat the credentials that they use like any other production secret. Keep them out of code and configuration files, ensure they are scoped appropriately, and rotate them often (better to not use API keys at all if you can).&lt;/p&gt;

&lt;p&gt;If you have any questions, feel free to reach out to me on X &lt;a href="https://twitter.com/willvelida" rel="noopener noreferrer"&gt;@willvelida&lt;/a&gt; or on &lt;a href="https://bsky.app/profile/willvelida.com" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Until next time, Happy coding! 🤓🖥️&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>Last week in Agent Security Update 2: Zam-mad Max</title>
      <dc:creator>Will Velida</dc:creator>
      <pubDate>Mon, 19 Oct 2026 06:57:31 +0000</pubDate>
      <link>https://dev.to/willvelida/last-week-in-agent-security-update-2-zam-mad-max-1dpg</link>
      <guid>https://dev.to/willvelida/last-week-in-agent-security-update-2-zam-mad-max-1dpg</guid>
      <description>&lt;p&gt;This week I've been struggling with some kind of illness. My throat was dry and I couldn't breathe on Friday, so I took a course of pseudoephedrine medication, bought some strepsils, and a nasal spray just to manage it. I was sick the whole weekend and started to recover just in time for Monday! Being an adult is awesome....&lt;/p&gt;

&lt;p&gt;Now those of you who are a little too keen for our robot overlords to consume us might point out that agents don't get sick, but they can be subject to a wide variety of vulnerabilities. Not the greatest segway into this weeks article, but I couldn't think of anything else.&lt;/p&gt;

&lt;p&gt;This week we saw further exploitations of what agents can reach, not necessarily what they are asked to do. Rogue Agents and the tools that they can use are becoming increasingly sophisticated attack vectors.&lt;/p&gt;

&lt;p&gt;As always, I’ll point out that I’m not trying to shame any of the companies involved, nor do I condone any of the methods used in any of these incidents. My goal here is just to spread awareness of the different ways that agents can be exploited, and hopefully trigger a conversation with your teams on how you should protect your agents and the people who use them from potential vulnerabilities.&lt;/p&gt;

&lt;p&gt;Here’s some incidents that have happened over the period 28th September to 4th October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenAI agent breach of Medicare
&lt;/h2&gt;

&lt;p&gt;Last week I covered how OpenAI agents managed to breach Medicare here in Australia and gained access to non-public information. OpenAI confirmed on September 28th that agents had also breached New South Wales Bureau of Crime Statistics and Research (BOCSAR), the Victorian Department of Health, and the Australian Institute of Health and Welfare.&lt;/p&gt;

&lt;p&gt;OpenAI has committed to an Australian taskforce, and support through it's Daybreak programme. On 1st and 2nd of October, OpenAI went through the process of notifying 100+ organizations of unauthorized agent activity. Independent analysis from Asymmetric Security named 55 organizations, including the SEC and US Bureau of Economic Analysis as victims of unauthorized agent activity, some of which has left records inaccessible or erased.&lt;/p&gt;

&lt;p&gt;I've never been optimistic about the speed of government officials in responding to incidents like this, but this &lt;em&gt;may&lt;/em&gt; prompt some much needed response to breaches like this. In Australia, a Joint Select Committee has been scheduled for the October 6th, which OpenAI's Jason Kwon will attend (Sam Altman and Dario Amodei both declined).&lt;/p&gt;

&lt;p&gt;In the US, senators Josh Hawley (Republican) and Chris Murphy (Democrat) have introduced a bipartisan AI Agent Accountability Act on October 1st, which would make AI Agent operators criminally and civilly liable for agentic hacks. Also, the Legal Advocates for Safe Science and Technology have sued OpenAI in San Francisco's Superior Court under California's anti-hacking law and Unfair Competition Law, citing both the Hugging Face and Medicare breaches.&lt;/p&gt;

&lt;p&gt;Whether OpenAI will actually be held to account is another matter. Watch this space.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.abc.net.au/news/2026-09-29/openai-apologises-medicare-shelves-chatgpt-astra-launch/107207156" rel="noopener noreferrer"&gt;OpenAI apologises for Medicare breach, shelves next gen ChatGPT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gizmodo.com/openai-faces-first-lawsuit-over-rogue-ai-agents-that-hacked-hugging-face-2000819469" rel="noopener noreferrer"&gt;OpenAI Faces First Lawsuit Over Rogue AI Agents That Hacked Hugging Face&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://startupfortune.com/hawley-and-murphy-bill-would-send-ai-executives-to-prison-over-rogue-agent-hacks/" rel="noopener noreferrer"&gt;Hawley and Murphy bill would send AI executives to prison over rogue agent hacks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  DIVD breached by an agentic AI attacker chaining Zammad zero-days
&lt;/h2&gt;

&lt;p&gt;On September 21st, The &lt;a href="https://www.divd.nl/" rel="noopener noreferrer"&gt;Dutch Institute for Vulnerability Disclosure (DIVD)&lt;/a&gt; was hacked in an automated AI attack that exploited zero-day vulnerabilities in user support/ticketing solution &lt;a href="https://zammad.com/en" rel="noopener noreferrer"&gt;Zammad&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;DIVD is an organization that searches for new vulnerabilities in software and report them to vendors, and it's made up of mostly volunteer security researchers. While it may be discouraging that even an organization full of security experts got breached, hopefully it illustrates that anyone can be a victim of cybercrime.&lt;/p&gt;

&lt;p&gt;These flaws enabled unauthenticated attackers to achieve remote code execution and leak user sessions (&lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-102489" rel="noopener noreferrer"&gt;CVE-2026-102489&lt;/a&gt;), and allowed a local user to elevate their privileges to root (&lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-102490" rel="noopener noreferrer"&gt;CVE-2026-102490&lt;/a&gt;). Combined together, the attacker could hijack sessions, run code remotely and escalate privileges from the Zammad user to root.&lt;/p&gt;

&lt;p&gt;From there, the attackers were able to access other services, read and exfiltrate data. Luckily network segmentation prevented the attack from going deeper into the environment.&lt;/p&gt;

&lt;p&gt;According to the DIVD, the attack was "loud and very very messy, including password spraying and man-in-the-middle activity. In fact the DIVD were able to reverse engineer the attack because the agent overexplained what it was doing.&lt;/p&gt;

&lt;p&gt;However, it's still unknown whether this was a capability test or part of a broader campaign. So we have a Rogue Agent running the attack (ASI10), but at the time of writing we don't know if a human has maliciously set out a goal or if an agent exceeded its brief. What is clear that the agent drove the exploitation, using password spraying and pivoting tools at machine speed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://csirt.divd.nl/cases/DIVD-2026-00015/" rel="noopener noreferrer"&gt;DIVD-2026-00015 - Vulnerabilities in Zammad during investigation of case DIVD-2026-00014&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.divd.nl/newsroom/articles/when-no-if/" rel="noopener noreferrer"&gt;It was a matter of when, not if...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.securityweek.com/zammad-zero-days-exploited-in-ai-powered-divd-hack/" rel="noopener noreferrer"&gt;Zammad Zero-Days Exploited in AI-Powered DIVD Hack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.helpnetsecurity.com/2026/10/01/divd-agentic-ai-attack-breach/" rel="noopener noreferrer"&gt;AI agent used Zammad zero-days to breach Dutch vulnerability disclosure non-profit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.theregister.com/security/2026/10/01/ai-agents-hacked-the-hackers-stealing-email-addresses-from-security-research-org/5300652" rel="noopener noreferrer"&gt;AI agents hacked the hackers, stealing email addresses from security research org&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Transluce: autonomous agents aimed SQL injection at US and Canadian government sites
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://transluce.org/" rel="noopener noreferrer"&gt;Transluce&lt;/a&gt; is a non-profit research lab that's building a public tech stack for scalable oversight of AI. On September 30th, they published an article after discovering several additional incidents where rogue AI agents have used techniques to access publicly available data on government websites in the US and Canada.&lt;/p&gt;

&lt;p&gt;So far, Transluce haven't identified any instances where agents have been able to access any information that's not publicly available. However, they were able to identify that agents were able to probe several government agencies and violate explicit usage policies. This attempt targeted several government websites including the White House, Department of Defense, The CDC and SEC, as well as state agencies in California, Maryland, Illionis, Texas, and New York.&lt;/p&gt;

&lt;p&gt;For the US Department of Education, agents attempted basic SQL injection to access school statistics. What's interesting in this case is that the data stored on the websites appeared to match a web search task in Google's DeepSearchQA benchmark. This means that the agents were potentially not given a hacking-related task, but were being graded on their ability to retrieve specific information on the internet.&lt;/p&gt;

&lt;p&gt;Transluce also reported agents attempting rudimentary hacks on Library and Archives Canada. Arquivo.pt captured 899 requests that included failed rudimentary hacking attempts that carried attack payloads (SQL injection and cross-site scripting).&lt;/p&gt;

&lt;p&gt;While they don't explicitly attribute these attempts to OpenAI, they claim that they exhibit the same tactics that have been attributed to OpenAI agents in a similar timeframe. OpenAI are currently reviewing the findings and have provided an initial briefing to Canadian officials.&lt;/p&gt;

&lt;p&gt;This is another example of Rogue Agents straying away from original goals and using information gathering techniques for attack probes (ASI10), and the use of ordinary web-request tools for SQL injection and XSS payloads (ASI02).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://transluce.org/us-canada-gov" rel="noopener noreferrer"&gt;AI Agents Targeted U.S. and Canadian Government Websites&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.bleepingcomputer.com/news/security/autonomous-ai-agents-tried-to-hack-us-canadian-government-websites/" rel="noopener noreferrer"&gt;Autonomous AI agents tried to hack US, Canadian government websites&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.securityweek.com/ai-agents-aimed-sql-injection-at-us-and-canadian-government-sites/" rel="noopener noreferrer"&gt;AI Agents Aimed SQL Injection at US and Canadian Government Sites&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  "PixelLeak": coding agents published internal screenshots to public GitHub repos
&lt;/h2&gt;

&lt;p&gt;Developers who get coding agents to make visual changes will often use screenshots as evidence that a change has been made. As you can imagine, if you're doing this as part of your day job, there's IP involved, and that information shouldn't be leaked to the public until it's ready.&lt;/p&gt;

&lt;p&gt;However, Glow Labs identified over 13,000 internal images that were published on public GitHub repositories by developers at over 300 organizations.&lt;/p&gt;

&lt;p&gt;The issue is that GitHub CLI couldn't attach images to a PR, so agents would work around this by creating public repositories tied to the developer's personal account to host the image. There's an open-source tool called &lt;code&gt;gitshot&lt;/code&gt; that defaults to public storage, and the agents saved this workaround to a skill. If you have multiple coding agents in your organization, that increased the blast radius.&lt;/p&gt;

&lt;p&gt;These screenshots also included billing records, unreleased features, and treasury consoles.&lt;/p&gt;

&lt;p&gt;The GitHub CLI added an &lt;code&gt;--attach&lt;/code&gt; flag through version 2.99.0 (Released 1st September). However, Flow also advises that you need to enforce runtime controls for developer agents to prevent information from being leaked.&lt;/p&gt;

&lt;p&gt;These were legitimate tools that agents used (The GitHub CLI, &lt;code&gt;gitshot&lt;/code&gt;). They just used those tools to publish confidential information. The agents also abused developers' personal GitHub credentials to create public assets outside organizational control (ASI03), and the use of agent-shared skills spread the unsafe pattern across organizations, infecting their supply chain (ASI04).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.glow.io/blogs/how-ai-agents-exposed-developer-screenshots-from-leading-tech-companies" rel="noopener noreferrer"&gt;PixelLeak: How AI Agents Exposed Developer Screenshots from Leading Tech Companies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/09/ai-coding-agents-exposed-13000-internal.html" rel="noopener noreferrer"&gt;AI Coding Agents Exposed 13,000 Internal Images, Including Billing Records, on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.bitdefender.com/en-us/blog/hotforsecurity/pixelleak-ai-coding-agents-github-screenshots" rel="noopener noreferrer"&gt;PixelLeak exposes 13,000 internal screenshots on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  GitLab AI Gateway: prompt-template sandbox escape to RCE (CVE-2026-90970)
&lt;/h2&gt;

&lt;p&gt;GitLab warned customers to patch a vulnerability in its AI Gateway product that could let attackers run arbitrary commands on vulnerable instances. AI Gateway gives access to AI-native GitLab Duo features. &lt;/p&gt;

&lt;p&gt;The flaw lies in the prompt template of a custom flow. A custom flow is an AI-powered workflow that users create on the Duo Agent platform to automate multi-step tasks. A logged-in user with Duo Agent platform access could have used the flow to escape the prompt template sandbox via a crafted flow configuration. This escape could be used to execute arbitrary commands on the gateway.&lt;/p&gt;

&lt;p&gt;For GitLab-hosted gateways, this vulnerability has already been fixed. However if you host your own gateway, GitLab has advised that you update your environment immediately. GitLab sent that guidance to customers before it published the advisory.&lt;/p&gt;

&lt;p&gt;Self-hosted gateways holds signing keys for JWT, which are treated as sensitive credentials, as well as connections to GitLab instances and model providers.&lt;/p&gt;

&lt;p&gt;GitLab credits HackerOne user 'invisiblemeerkat' with reporting the flaw. &lt;em&gt;I wish I had a cool hacker name, any suggestions are welcome. Keep it SFW&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Currently there are no reported incidents from this disclosure, but I'd keep an eye on those self-hosted gateways. This does have the potential for Unexpected Code execution (ASI05) incidents via the agent flow configuration, and identity and privilege abuses (ASI03) as a low-privileged agent-platform user could end up with the gateway's signing keys and service credentials.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/releases/patches/other-patches/patch-release-gitlab-ai-gateway-19-4-1-released/" rel="noopener noreferrer"&gt;GitLab AI Gateway Critical Patch Release: 19.2.4, 19.3.2, and 19.4.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/10/gitlab-patches-critical-self-hosted-ai.html" rel="noopener noreferrer"&gt;GitLab Patches Critical 9.9 AI Gateway Flaw Allowing Command Execution on Self-Hosted Servers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.bleepingcomputer.com/news/security/gitlab-warns-of-critical-rce-vulnerability-in-ai-gateway-service/" rel="noopener noreferrer"&gt;GitLab warns of critical RCE vulnerability in AI Gateway service&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Official MCP Python SDK: malicious MCP servers can hijack OAuth credentials
&lt;/h2&gt;

&lt;p&gt;Cycode reported an official MCP Python SDK flaw could trick applications into handing over the OAuth credentials it uses to log into a real service. In versions &amp;gt;= 2.0.0a1, &amp;lt; 2.2.0 and &amp;gt;= 1.9.1, &amp;lt; 1.30.0, it could send the client secret, authorization code, and PKCE proof key to a token endpoint that an attacker controls.&lt;/p&gt;

&lt;p&gt;When an MCP client needs to log in, it'll ask the server it is connecting to where the authorization server can be find. In the affected versions, a malicious server could point it at a server that an attacker chooses. The client then sends the secret, PKCE proof key and auth code to the attacker. The proof key is a one-time value that's designed to prevent a stolen authorization code from being used, so if that gets handed over, that protection is gone.&lt;/p&gt;

&lt;p&gt;No exploits have been reported yet, and this issue was fixed in version 1.30.0 and 2.2.0. However, if you're using affected SDK versions, you could be opening yourself up to Agentic Supply Chain vulnerabilities and Identity and Privilege abuses.&lt;/p&gt;

&lt;p&gt;Furthermore, if you use &lt;code&gt;ClientCredentialsOAuthProvider&lt;/code&gt; or &lt;code&gt;PrivateKeyJWTOAuthProvider&lt;/code&gt;, you should also pass the &lt;code&gt;issuer=&lt;/code&gt;. Without this, they'll still follow whichever auth server your MCP server advertises.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-qx49-fqc8-xw99" rel="noopener noreferrer"&gt;OAuth client could send credentials to an authorization server chosen by the MCP server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cycode.com/blog/mcp-python-sdk-oauth-account-takeover/" rel="noopener noreferrer"&gt;Cycode Uncovers Account Takeover in MCP Python SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/09/official-mcp-python-sdk-flaw-can-let.html" rel="noopener noreferrer"&gt;Official MCP Python SDK Flaw Can Let Malicious Servers Steal OAuth Credentials&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Malicious ChatGPT Custom GPT "Plus 5.6" used to deliver a RAT via ClickFix
&lt;/h2&gt;

&lt;p&gt;Malware developers use sponsored Google results as an attack vector to lure unsuspecting victims. Huntress found that attackers are using sponsored results to push a malicious ChatGPT custom GPT named "Plus 5.6", that lures them to a fake Cloudflare CAPTCHA check and make them download and run a remote access trojan (RAT).&lt;/p&gt;

&lt;p&gt;Essentially users would search "chatgpt" in Google, and a link to the custom GPTs would appear in the sponsored Google Search result. Those who navigate to the site and complete the CAPTCHA check would be delivered with a ClickFix attack, which tell users to copy-and-paste a command into their terminal.&lt;/p&gt;

&lt;p&gt;That triggers a chain that ends up with a remote access trojan being downloaded onto your machine.&lt;/p&gt;

&lt;p&gt;Huntress reported the initial finding and OpenAI were able to remove it. However, it didn't take long for another site to be created and pushed through sponsored results (&lt;em&gt;who at Google verifies these?&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;The second one is still online, and while it doesn't contain the malicious ClickFix lure anymore, it's only a matter of time until a new installer is created.&lt;/p&gt;

&lt;p&gt;The general advise here is that any page or website that asks you to run something in the Run dialog, Terminal, PowerShell, or command prompt is asking for trouble. &lt;strong&gt;Don't do it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But this is a great example of how agents can exploit the trust of humans (ASI09). Who among us hasn't typed 'chatgpt' into a Google search? Imagine if you're not a technical person, or you just forget to look at the actual web address? If the domain looks official enough, you're going to trust it, and if you're not sure what a download does, are you really going to take the time to stop it?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.helpnetsecurity.com/2026/09/29/malicious-chatgpt-custom-gpt-malware-via-clickfix/" rel="noopener noreferrer"&gt;Malicious Custom GPT on chatgpt.com lures users into installing a RAT&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  LiteLLM: JWT email_verified account takeover (CVE-2026-93355)
&lt;/h2&gt;

&lt;p&gt;LiteLLM is an open-source gateway that you can use to manage access to OpenAI, Anthropic, and other LLM APIs. The gateway holds API keys, tracks how much you're spending on LLM calls, and controls who can access what.&lt;/p&gt;

&lt;p&gt;The folks at OX Research discovered a way to use a legitimately signed login token to authenticate as another LiteLLM user (including admins). The JWT authentication silently falls back to an unverified email claim when a direct identity match misses. This allows the attacker to log in as any user, and then permanently rebind that victim's SSO account to the attacker's own token.&lt;/p&gt;

&lt;p&gt;This vulnerability was actually discovered on May 18th, but Ox has &lt;em&gt;still&lt;/em&gt; not received a response, and it was publicly disclosed on September 14th. There's been no reported exploitations yet, the vulnerability has been confirmed through to version 1.100.1&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.ox.security/blog/litellm-an-ordinary-login-token-can-become-someone-elses-admin-account/" rel="noopener noreferrer"&gt;CVE-2026-93355: Account Takeover in LiteLLM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We're only going to see more agents being used in cyberattacks. Whether it's agents going against their intended goals or being directed to be malicious. Agents can also cause trouble when they are acting within their expected behavior, as in the case of PixelLeak.&lt;/p&gt;

&lt;p&gt;The agentic supply chain also continues to be a challenge. Agents depend on MCP servers, SDKs, gateways, and shared skills which all can be weak links to be exploited.&lt;/p&gt;

&lt;p&gt;If you're developing agents for your organizations, you need to ensure that you're thinking about the underlying agent infrastructure, and implement constraints and guardrails over what your agents can do.&lt;/p&gt;

&lt;p&gt;If you have any questions, feel free to reach out to me on X &lt;a href="https://twitter.com/willvelida" rel="noopener noreferrer"&gt;@willvelida&lt;/a&gt; or on &lt;a href="https://bsky.app/profile/willvelida.com" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Until next time, Happy coding! 🤓🖥️&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>Last week in Agent Security 1: We are not-a-mused!</title>
      <dc:creator>Will Velida</dc:creator>
      <pubDate>Tue, 29 Sep 2026 00:15:01 +0000</pubDate>
      <link>https://dev.to/willvelida/last-week-in-agent-security-1-we-are-not-a-mused-dm7</link>
      <guid>https://dev.to/willvelida/last-week-in-agent-security-1-we-are-not-a-mused-dm7</guid>
      <description>&lt;p&gt;Last week I talked at NDC Oslo about the OWASP Top 10 for Agents where I showed folks the different ways that agents can be exploited. If you've turned the news on recently, you've probably heard about AI will kill us all and how we're all doomed.&lt;/p&gt;

&lt;p&gt;Call me optimistic, but I think we're currently in the middle of a hype cycle where AI labs are overstating the abilities of the models, and we're at a point where we've seen how powerful agents can be, but we need to be more serious about the harms agents can do, and how we can mitigate those risks.&lt;/p&gt;

&lt;p&gt;With that in mind, I'll aiming to do a weekly blog post on different agentic attacks and incidents, what happened, how it happened, and what OWASP risks they fall under. Depending on demand or the severity/publicity of the attack, I may do a deep dive on how you &lt;em&gt;could&lt;/em&gt; prevent such an attack in the first place.&lt;/p&gt;

&lt;p&gt;I'll point out that I'm not trying to shame any of the companies involved, nor do I condone any of the methods used in any of these incidents. My goal here is just to spread awareness of the different ways that agents can be exploited, and hopefully trigger a conversation with your teams on how you should protect your agents and the people who use them from potential vulnerabilities.&lt;/p&gt;

&lt;p&gt;Here's some incidents that have happened over the period 21st-27th September 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meta Muse macOS zero-day ("not-a-mused")
&lt;/h2&gt;

&lt;p&gt;On September 21st 2026, macOS security researcher &lt;a href="https://x.com/patrickwardle?lang=en" rel="noopener noreferrer"&gt;Patrick Wardle&lt;/a&gt; (also founder of the Objective-See Foundation) created a POC (amusingly called "not-a-mused") showing that malware already running inside a Mac can hijack Meta's Muse AI assistant.&lt;/p&gt;

&lt;p&gt;The flaw centers on a undocumented configuration setting &lt;code&gt;endo_voyager_dictation_endpoint&lt;/code&gt; that an unpriviledged local process can flip to reroute Muse's dictation traffic to an attacker's controlled server (via your own microphone). From there, you could inject extra instructions that Muse trusts, read dictated content from Muse, and steal the authentication token that signs into the user's Muse account. You can use Muse on multiple devices, which extends the blast radius.&lt;/p&gt;

&lt;p&gt;Meta have apparently released a hotfix, but we see a number of risks being highlighted here. Theft and reuse of the agent's token (Identity &amp;amp; Privilege Abuse), injected goal hijack of the instructions that the agent acts on (Agent Goal Hijack), and how a trusted assistant becomes an attack surface (Human-Agent Trust Exploitation).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/patrickwardle/status/2102045926474785265" rel="noopener noreferrer"&gt;Wardle's X thread on Muse&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/09/one-hidden-meta-muse-setting-could-let.html" rel="noopener noreferrer"&gt;One Hidden Meta Muse Setting Could Let Attackers Turn the AI Assistant Into a Backdoor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gpts24.com/en/news/meta-muse-hits-no-1-on-app-store-then-a-mac-zero-day-hijacks-its-permissions" rel="noopener noreferrer"&gt;Meta Muse Hits No. 1 on App Store, Then a Mac Zero-Day Hijacks Its Permissions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gambit "Strix/Cairn/Hermes" autonomous card-skimming campaign
&lt;/h2&gt;

&lt;p&gt;Gambit Security published an interim report about a financially motivated campaign in which a single operator chained three open-source AI agent frameworks to attack online retailers autonomously. They've traced the attack to July 2026 and it's still ongoing (at time of writing).&lt;/p&gt;

&lt;p&gt;The first framework involved is &lt;a href="https://www.strix.ai/" rel="noopener noreferrer"&gt;Strix&lt;/a&gt;, which is an open source AI penetration testing tool. Between 23rd and 31st August, Strix was run in deep mode against various hosts to provide reconnaissance and vulnerability discovery.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cairnsecurity.com/" rel="noopener noreferrer"&gt;Cairn&lt;/a&gt;, which is an automated penetration testing engine, was then used to launch attacks. Each attack path was chosen by the harness in real time through extensive probing and exploitation attempts, resulting in dynamic and mostly different TTPs across victims.&lt;/p&gt;

&lt;p&gt;Then finally, &lt;a href="https://hermes-agent.nousresearch.com/" rel="noopener noreferrer"&gt;Hermes&lt;/a&gt;, is being used to orchestrate the attack. Gambit attributes a Chinese-speaking operator running a &lt;code&gt;SOUL - Red Team Operator&lt;/code&gt; persona with 121 skills. The operator has also added a skill that removes the content security filters of Hermes itself. It used Opus 4.6 (after newer models had refused its requests), with just under 2,000 prompts typed by the human operator across 260 sessions. Some example prompts include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;看漏洞报告 开干 (“read the vulnerability report and start”)&lt;/li&gt;
&lt;li&gt;看看进web后台 (“get into the web backend”)&lt;/li&gt;
&lt;li&gt;你去搜一下wp2shell (“go and search for wp2shell”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far, more than 600,000 valid credit-card records have been exfiltrated from two victims, with the vast majority being US records, card-skimmer scripts planted on the sites of five organizations, and 119+ compromised websites overall; victims include a Fortune 500 hospitality company, a major US airline, a large US industrial-supplies distributor and an online fashion retailer. One Hermes "skill" file instructed the agent to wipe card fields from victims' Magento databases after exfiltration, causing operational disruption.&lt;/p&gt;

&lt;p&gt;So another example of Rogue Agents in action, but this time the agents belong to the attacker, rather than a defender's own agent being subverted.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gambit.security/blog-posts/autonomous-ai-agents-online-retailers-25-a-company" rel="noopener noreferrer"&gt;Autonomous AI Agents are breaking into hundreds of Online Retailers for $25 a target in an ongoing campaign&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bifrost AI-gateway unauthenticated RCE
&lt;/h2&gt;

&lt;p&gt;AI Gateways are an effective mechanism to protect calls to LLM models, but they aren't foolproof. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-90898" rel="noopener noreferrer"&gt;CVE-2026-90898&lt;/a&gt; was reported in open-source AI gateway Bifrost which allows unauthenticated attackers to run arbitary commands on the gateway server with a single HTTP request. This is a good example of ASI05, Unexpected Code Execution.&lt;/p&gt;

&lt;p&gt;Yuval Moravchick of JFrog Security Research found that an attacker can register a stdio-type MCP client through a single unauthenticated POST request to the management API endpoint &lt;code&gt;/api/mcp/client&lt;/code&gt;. Before any MCP handshake, Bifrost starts the specified command immediately as the gateway process user.&lt;/p&gt;

&lt;p&gt;The Bifrost binary binds the management API to localhost by default, which limits exposure to the local machine. The official Docker image binds it to 0.0.0.0, making the management API reachable from outside the container if the port is published.&lt;/p&gt;

&lt;p&gt;Upgrading to transport versions v2.1.0 will result in a 403 response when unauthenticated callers try to register a stdio MCP client. Now the CTO and co-founder of Maxim says that the risk is lower than the CVE rating suggests, since exploitation requires the management interface to be reachable from an untrusted network and the operator to have left authentication unset. The default and documented deployment places the gateway inside a private network. Maxim and JFrog are working together to revise the severity assessment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2026/09/critical-bifrost-ai-gateway-flaw-lets.html" rel="noopener noreferrer"&gt;Critical Bifrost AI Gateway Flaw Lets Attackers Run Commands Without Credentials&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Carbonato Docker botnet built on the Hermes "GH0ST" agent
&lt;/h2&gt;

&lt;p&gt;On September 23rd, The researchers at ThreatDown disclosed a new botnet malware called Carbonato, which targets insecure hosts running Docker daemons exposing an unauthenticated daemon API on port 2375 to launch a privileged container to gain host access, and then install the Hermes Agent AI framework running an agent persona named GHOST with instructions that overwrite the default &lt;code&gt;SOUL.md&lt;/code&gt; persona file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flmkdhrtrej0h7xuf95hq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flmkdhrtrej0h7xuf95hq.jpg" width="800" height="748"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It then opens a reverse SSH tunnel, installs an SSH server with the operators' key, and reports the new deployment through Telegram. Operators issue tasks over a Telegram interface, which is then interpreted by the model, and writes terminal commands, reads the output and decides to do what next. These tasks prioritize tasks such as harvesting AI API keys, SSH credentials, access tokens etc. and then returns those via Telegram.&lt;/p&gt;

&lt;p&gt;This is an example of a Rogue Agent (ASI10) being weaponized as the malware core combined with tool misuse and exploitation (ASI02). The recommendation here is to keep Docker daemon APIs off the network and require authentication on registries.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bleepingcomputer.com/news/security/new-carbonato-malware-uses-ai-agents-to-hijack-exposed-docker-hosts/" rel="noopener noreferrer"&gt;New Carbonato malware uses AI agents to hijack exposed Docker hosts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  OpenAI agent breached Australia's Medicare statistics portal
&lt;/h2&gt;

&lt;p&gt;In local news, OpenAI breached Medicare!&lt;/p&gt;

&lt;p&gt;So back in June, An OpenAI agent breached a Medicare statistics reporting portal operated by Services Australia, accessing public and non-public files and writing data to an internal server.&lt;/p&gt;

&lt;p&gt;The agent was running a research task on public medical-spending data and when the portal's controls refused its requests, the agent found a workaround. The portal holds aggregated Medicare/Pharmaceutical Benefits Scheme statistics and is separate from claims and personal-record systems. &lt;/p&gt;

&lt;p&gt;While no evidence that individual medical records were accessed, it didn't help that OpenAI found out about the attempt in June, and only told the Australian government last week. Despite the outrage from Australian media and general public, this isn't the worst example of Rogue Agent attacks. OpenAI should have acted quicker. This also shows how agents can be hijacked through goal drift, and how agents can bypass access controls to reach non-public files.&lt;/p&gt;

&lt;p&gt;The Australian Government has stood up a cross-government task force, so watch this space.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bleepingcomputer.com/news/security/openai-hacked-australian-medicare-govt-site-probed-data-providers/amp/" rel="noopener noreferrer"&gt;OpenAI hacked Australian Medicare govt site, probed data providers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Salesforce Agentforce "SalesBleed" 0-click data exfiltration
&lt;/h2&gt;

&lt;p&gt;On 24 September 2026, Zenity Labs disclosed SalesBleed, a set of zero-click vulnerabilities in Salesforce Agentforce that allowed attackers to exfiltrate CRM data with no victim interaction and without authenticating into the target's Salesforce environment.&lt;/p&gt;

&lt;p&gt;An attacker plants a hidden prompt-injection payload inside a public-facing Web-to-Lead form. The malicious lead persists in CRM as a dormant record, and when an Agentforce agent later processes it during normal operations, the injected instructions hijack the agent.&lt;/p&gt;

&lt;p&gt;The payload then queries and exfiltrates sensitive account data using DNS-based exfiltration that evaded Salesforce's Trusted URLs redaction control. So any external party who submits a lead could silently exfiltrate CRM data.&lt;/p&gt;

&lt;p&gt;Zenity Labs and Salesforce have worked together to fix the bugs in Agentforce, but Zenity were quick to stress that the vulnerabilities found aren't unique to Agentforce. Any agent that ingests untrusted external records, renders links back to users, and holds sensitive tool access can be hijacked in the same way.&lt;/p&gt;

&lt;p&gt;Memory and Context Poisoning through poisoned CRM records that resurface as trusted context (ASI06), indirect prompt injection redirecting agent behavior through those poisoned records (ASI01), and the agent's tool becoming the blast radius (ASI03) can occur in anyone's agent.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.infosecurity-magazine.com/news/vulnerabilities-salesforce-ai/" rel="noopener noreferrer"&gt;Zero-Click Vulnerabilities in Salesforce Agentforce Expose Wider AI Agent Risk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cybersecuritynews.com/salesforce-salesbleed-vulnerability/" rel="noopener noreferrer"&gt;Salesforce Indirect Prompt Injection Vulnerability Enables 0-click Data Exfiltration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As you can probably see, these incidents came from agents that could make real tool calls, use credentials, and had the autonomy to perform consequential actions. Some agents were developed by the attackers themselves, while others drifted from their intended goals.&lt;/p&gt;

&lt;p&gt;That's why it's critical for developers to control &lt;strong&gt;what&lt;/strong&gt; your agent can do, not just what you tell them do. You have to assume that prompt injection attacks will eventually prevail, so you can design your agents to stop the damage early when it does.&lt;/p&gt;

&lt;p&gt;If you have any questions, feel free to reach out to me on X &lt;a href="https://twitter.com/willvelida" rel="noopener noreferrer"&gt;@willvelida&lt;/a&gt; or on &lt;a href="https://bsky.app/profile/willvelida.com" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Until next time, Happy coding! 🤓🖥️&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>Running parallel GitHub Copilot agent sessions with tmux and git worktrees</title>
      <dc:creator>Will Velida</dc:creator>
      <pubDate>Sun, 09 Aug 2026 23:08:03 +0000</pubDate>
      <link>https://dev.to/willvelida/running-parallel-github-copilot-agent-sessions-with-tmux-and-git-worktrees-2f0f</link>
      <guid>https://dev.to/willvelida/running-parallel-github-copilot-agent-sessions-with-tmux-and-git-worktrees-2f0f</guid>
      <description>&lt;p&gt;When I first started using GitHub Copilot, I kept to the chat window, as I wasn't too comfortable with the idea of using the CLI for long prompts. The chat interface in VS Code was more friendly and intuitive for me, and I felt more in control using the chat UI.&lt;/p&gt;

&lt;p&gt;Over time, I've been using GitHub Copilot CLI more often. As the scope of work increases (and as demand from stakeholders also increases!), I've found myself firing up multiple terminals to do multiple different tasks on a project. However, I've had to be careful about how I've done that, as I didn't want to have to deal with the trouble of merge conflicts only made worse by overzealous coding agents!&lt;/p&gt;

&lt;p&gt;A colleague of mine suggested that I take a look at tmux and git worktrees as a method to safely work on multiple features within the same repository, and use the GitHub Copilot CLI to use agents to work on each worktree without compromising the others work. I'd never used tmux before, nor had I really used git worktrees in anger before, so this seemed like the perfect excuse to hack with it and learn something new.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is tmux?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.tmux.info/" rel="noopener noreferrer"&gt;tmux&lt;/a&gt; is a terminal multiplexer that allows you to create, access, and control any number of terminals from a single screen. &lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/HdsHp0SBLbk"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;What's different about just splitting the terminal or creating a new tab in Windows Terminal is that with tmux, you can &lt;a href="https://github.com/tmux/tmux/wiki/Getting-Started" rel="noopener noreferrer"&gt;detach from a session and reattach later&lt;/a&gt; through persistence. The running processes stay alive across disconnects and reboots. This is great for remote work, because if the connection drops, you can pick up exactly where you left off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhyguzyxblep724i6ncwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhyguzyxblep724i6ncwu.png" alt="tmux sessions, windows, and panes diagram showing the hierarchy" width="799" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Persistent workspaces that survive disconnects are enabled through &lt;strong&gt;Sessions&lt;/strong&gt;, which is just a collection of &lt;strong&gt;Windows&lt;/strong&gt;. Windows are like tabs in a browser, where each can contain multiple panes and has its own command history. &lt;strong&gt;Panes&lt;/strong&gt; split windows into multiple terminals.&lt;/p&gt;

&lt;p&gt;tmux isn't exactly new. It was built in 2007 (remember life before the GFC? Pepperidge Farm remembers), so why use it for coding agents?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use tmux for agentic coding?
&lt;/h2&gt;

&lt;p&gt;tmux has some pretty great features that are ideal for &lt;a href="https://www.willvelida.com/posts/my-take-on-harness-engineering/" rel="noopener noreferrer"&gt;agentic coding workflows&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;tmux's persistence model is particularly handy when agents are running long processes within a session. Imagine you're using GitHub Copilot to do a big refactor of your code, and half-way through the terminal got killed. With tmux, we can prevent this by detaching from our session, and reattaching later. &lt;/p&gt;

&lt;p&gt;Each pane provides its own PTY, which is great for isolating agents from each other. The changes that Agent 1 makes can't step on the work that Agent 2 is doing. Being able to split the window and watch every agent work at the same time is also aesthetically pleasing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running this on WSL2
&lt;/h2&gt;

&lt;p&gt;I'm running on a Windows machine, so I have to use tmux through WSL2. This isn't a bad thing, it's just that tmux doesn't run natively on Windows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/copilot/concepts/agents/copilot-cli/about-copilot-cli" rel="noopener noreferrer"&gt;GitHub Copilot CLI runs on WSL2&lt;/a&gt;, so I thought I'd just set everything up on WSL2.&lt;/p&gt;

&lt;p&gt;There is a native substitute called &lt;a href="https://github.com/psmux/psmux" rel="noopener noreferrer"&gt;psmux&lt;/a&gt; if you're not comfortable with WSL2, but the rest of this guide will assume that you're running in WSL2.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you'll need
&lt;/h3&gt;

&lt;p&gt;I'm going to assume that you already have &lt;a href="https://learn.microsoft.com/en-us/windows/wsl/install" rel="noopener noreferrer"&gt;WSL2 installed&lt;/a&gt;. If you do, then you can use &lt;code&gt;sudo apt install&lt;/code&gt; to install the following packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; git tmux curl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the GitHub Copilot CLI, that ships as an npm package. So if you need to install node on WSL2, you can do so using this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deb.nodesource.com/setup_lts.x | &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; bash -
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs
node &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have node installed, install the CLI like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @github/copilot
copilot &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also had to update the GitHub CLI so that I could open and merge PRs. I'm using Ubuntu as my distro, so I ran the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; wget &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;wget &lt;span class="nt"&gt;-y&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 755 /etc/apt/keyrings
wget &lt;span class="nt"&gt;-qO-&lt;/span&gt; https://cli.github.com/packages/githubcli-archive-keyring.gpg &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/keyrings/githubcli-archive-keyring.gpg &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/github-cli.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; gh
gh auth setup-git      &lt;span class="c"&gt;# so git push over HTTPS uses your gh credentials&lt;/span&gt;
gh &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm also using Windows Terminal to run tmux inside that. This can be installed on Windows using WinGet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Microsoft.WindowsTerminal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-e&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Some tmux basics
&lt;/h2&gt;

&lt;p&gt;Let's do a basic tour of tmux commands that I'll use in this blog post. We can start a new session like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux new &lt;span class="nt"&gt;-s&lt;/span&gt; tutorial &lt;span class="c"&gt;# tutorial is the name of the session I've created&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;tmux uses &lt;code&gt;Ctrl+b&lt;/code&gt; as the prefix key, and then you'd enter the command key to do something.&lt;/p&gt;

&lt;p&gt;So if we want to split a pane, we can use &lt;code&gt;Ctrl+b %&lt;/code&gt; to create a new pane left or right, or &lt;code&gt;Ctrl+b "&lt;/code&gt; to create a pane top or bottom.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6mibulapro32s48u5eiv.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6mibulapro32s48u5eiv.gif" alt="Splitting a tmux pane left and right with Ctrl+b %" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can tidy the panes up by entering &lt;code&gt;Ctrl+b :select-layout even-horizontal&lt;/code&gt;. There are other layout styles that you can as well. I find it easier to use &lt;code&gt;Ctrl+b space&lt;/code&gt; to cycle through different layout options.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1sl6jbcodahw7mmcz7gf.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1sl6jbcodahw7mmcz7gf.gif" alt="Cycling through tmux pane layouts with Ctrl+b space" width="720" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To detach a session, enter &lt;code&gt;Ctrl+b d&lt;/code&gt;, then you can use the tmux sessions using &lt;code&gt;tmux ls&lt;/code&gt; and then reattach using &lt;code&gt;tmux attach -t &amp;lt;session-name&amp;gt;&lt;/code&gt;!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7vyhqmqdrlhmfrlnwtnd.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7vyhqmqdrlhmfrlnwtnd.gif" alt="Detaching from a tmux session with Ctrl+b d and reattaching with tmux attach" width="760" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's a lot more to it than just these basics. &lt;a href="https://blog.openreplay.com/tmux-ai-guide/" rel="noopener noreferrer"&gt;This blog post&lt;/a&gt; talks about how you can edit config for tmux to add features like mouse support, and there are &lt;a href="https://www.tmux.info/docs/commands" rel="noopener noreferrer"&gt;more commands&lt;/a&gt; that tmux provides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving each agent its own workspace with git worktrees
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://git-scm.com/docs/git-worktree" rel="noopener noreferrer"&gt;Git worktrees&lt;/a&gt; allow you to check out multiple branches from a repo at a time, with its own isolated directory, but sharing the same &lt;code&gt;.git&lt;/code&gt; directory. This provides our agents with a reasonable amount of isolation since each worktree has its own directory, this will prevent autonomous agents editing the same directory from stepping on each other's toes (do agents have toes? No, it's a transformer model. It's just an expression) and creating merge conflicts in real time.&lt;/p&gt;

&lt;p&gt;I have an API that I'm building for viewing past Arsenal results, and preparing for upcoming matches so I can export them into various calendars. For this API, I want to build the following endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /table&lt;/code&gt; returns the full league table, ordered by position.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /fixtures/h2h?teamA=&amp;amp;teamB=&lt;/code&gt; returns every match between two named teams.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /fixtures/upcoming (optional ?team=)&lt;/code&gt; returns the fixtures that have not been played yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using git worktrees, each will check out into its own folder while sharing the API repository. This isolation gives each GitHub Copilot CLI instance its own workspace, so they won't overwrite each other in the middle of their work.&lt;/p&gt;

&lt;p&gt;Let's set up our worktrees for the features we want to implement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add &lt;span class="nt"&gt;-b&lt;/span&gt; feature/standings ../fixtures-standings
git worktree add &lt;span class="nt"&gt;-b&lt;/span&gt; feature/h2h       ../fixtures-h2h
git worktree add &lt;span class="nt"&gt;-b&lt;/span&gt; feature/upcoming  ../fixtures-upcoming
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git worktree add&lt;/code&gt; creates a new worktree. &lt;code&gt;-b feature/standings&lt;/code&gt; creates a new branch called &lt;code&gt;feature/standings&lt;/code&gt; and &lt;code&gt;../fixtures-standings&lt;/code&gt; creates a sibling folder next to our repository, not inside it. This prevents git from getting confused about which repository owns what.&lt;/p&gt;

&lt;p&gt;To see the list of worktrees that we have created, we can run the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list
&lt;span class="c"&gt;# produces the following output&lt;/span&gt;
willv@Yggdrasil:/mnt/c/Users/willv/Documents/Learning/fixtures-api&lt;span class="nv"&gt;$ &lt;/span&gt;git worktree list
/mnt/c/Users/willv/Documents/Learning/fixtures-api        dc738bd &lt;span class="o"&gt;[&lt;/span&gt;main]
/mnt/c/Users/willv/Documents/Learning/fixtures-h2h        dc738bd &lt;span class="o"&gt;[&lt;/span&gt;feature/h2h]
/mnt/c/Users/willv/Documents/Learning/fixtures-standings  dc738bd &lt;span class="o"&gt;[&lt;/span&gt;feature/standings]
/mnt/c/Users/willv/Documents/Learning/fixtures-upcoming   dc738bd &lt;span class="o"&gt;[&lt;/span&gt;feature/upcoming]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running multiple GitHub Copilot agents in parallel
&lt;/h2&gt;

&lt;p&gt;With our worktrees created, we can now create a new tmux session and split it into panes for each of our worktrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux new &lt;span class="nt"&gt;-s&lt;/span&gt; swarm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create three new panes using &lt;code&gt;Ctrl+b %&lt;/code&gt; and style the layout how you want. Then within each pane, we'd cd into each worktree. Using my worktrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In pane 1&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /fixtures-standings
&lt;span class="c"&gt;# In pane 2&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /fixtures-h2h
&lt;span class="c"&gt;# In pane 3&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /fixtures-upcoming
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can move between panes using &lt;code&gt;Ctrl+b&lt;/code&gt; then an arrow key.&lt;/p&gt;

&lt;p&gt;Within each pane, you can confirm that the folder and branch are correct by running the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git branch &lt;span class="nt"&gt;--show-current&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within each CLI session, it'll pause to ask permissions before editing files or running commands like &lt;code&gt;dotnet build&lt;/code&gt;. If you want to run these without having to press enter each time Copilot asks you for something, you can add &lt;a href="https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli" rel="noopener noreferrer"&gt;&lt;code&gt;--allow-all-tools&lt;/code&gt;&lt;/a&gt; to the command. I'm still not 100% comfortable with just YOLOING in agentic coding sessions, but this is just a demo and we can have some fun with it 😅&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcgx7l270xb3dj7e9wgto.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcgx7l270xb3dj7e9wgto.gif" alt="Three GitHub Copilot CLI sessions started, one in each tmux pane across the worktrees" width="600" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For each pane, I ran the following prompts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# pane 1&lt;/span&gt;
copilot &lt;span class="nt"&gt;--allow-all-tools&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"Add Endpoints/StandingsEndpoints.cs implementing IEndpointModule (see Endpoints/IEndpointModule.cs), mapping GET /table that returns FixtureData.Standings ordered by position. Inject FixtureData via the constructor pattern the other modules use. New file only, do not edit Program.cs. Make sure the project still builds."&lt;/span&gt;

&lt;span class="c"&gt;# pane 2&lt;/span&gt;
copilot &lt;span class="nt"&gt;--allow-all-tools&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"Add Endpoints/HeadToHeadEndpoints.cs implementing IEndpointModule (see Endpoints/IEndpointModule.cs), mapping GET /fixtures/h2h?teamA=&amp;amp;teamB= that returns every match from FixtureData.Matches where those two teams met (either could be home or away). Inject FixtureData. New file only, do not edit Program.cs. Make sure the project still builds."&lt;/span&gt;

&lt;span class="c"&gt;# pane 3&lt;/span&gt;
copilot &lt;span class="nt"&gt;--allow-all-tools&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"Add Endpoints/UpcomingEndpoints.cs implementing IEndpointModule (see Endpoints/IEndpointModule.cs), mapping GET /fixtures/upcoming with an optional ?team= filter, returning matches from FixtureData.Matches where Played is false, ordered by matchday. Dates may be null for those fixtures, so do not sort by date. Inject FixtureData. New file only, do not edit Program.cs. Make sure the project still builds."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press enter to send those prompts and watch the agents do their thing!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohkvmythw0i3zlobl81y.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohkvmythw0i3zlobl81y.gif" alt="Three GitHub Copilot agents building their endpoints in parallel across three tmux panes" width="600" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you need to step away and you don't want to lose them, you can always use &lt;code&gt;Ctrl+b d&lt;/code&gt; to detach the session, and then use &lt;code&gt;tmux attach&lt;/code&gt; to come back. Remember that the session lives on the machine, so we can pick up the same session on a different terminal, or even ssh into the machine and start the session from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reviewing our changes
&lt;/h2&gt;

&lt;p&gt;Once our agents have finished making their changes, we need to make sure that we can merge the changes back into main, and that there aren't any conflicts between the different branches and main. This is where git worktrees can be advantageous, since they have their own working directory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fskavfqa8iownw3pne5ze.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fskavfqa8iownw3pne5ze.png" alt="Each worktree's changes reviewed independently before merging into main" width="799" height="196"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because our worktrees are isolated, we can commit the changes as the changes that the agents have made don't conflict with each other. Once we've committed the changes, we can raise a PR for each worktree.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /mnt/c/Users/willv/Documents/Learning/fixtures-api
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin feature/standings feature/h2h feature/upcoming
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can open a PR for each of our worktrees using &lt;code&gt;--head&lt;/code&gt; to name the branch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;create &lt;span class="nt"&gt;--base&lt;/span&gt; main &lt;span class="nt"&gt;--head&lt;/span&gt; feature/standings &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"feat: add GET /table standings endpoint"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"Adds Endpoints/StandingsEndpoints.cs (IEndpointModule) mapping GET /table, returning the league table ordered by position. New file only; Program.cs untouched."&lt;/span&gt;

gh &lt;span class="nb"&gt;pr &lt;/span&gt;create &lt;span class="nt"&gt;--base&lt;/span&gt; main &lt;span class="nt"&gt;--head&lt;/span&gt; feature/h2h &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"feat: add GET /fixtures/h2h endpoint"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"Adds Endpoints/HeadToHeadEndpoints.cs (IEndpointModule) mapping GET /fixtures/h2h?teamA=&amp;amp;teamB=, returning matches between the two teams. New file only; Program.cs untouched."&lt;/span&gt;

gh &lt;span class="nb"&gt;pr &lt;/span&gt;create &lt;span class="nt"&gt;--base&lt;/span&gt; main &lt;span class="nt"&gt;--head&lt;/span&gt; feature/upcoming &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"feat: add GET /fixtures/upcoming endpoint"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"Adds Endpoints/UpcomingEndpoints.cs (IEndpointModule) mapping GET /fixtures/upcoming (optional ?team=), unplayed fixtures ordered by matchday. New file only; Program.cs untouched."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can use the &lt;code&gt;gh&lt;/code&gt; CLI to see the PRs that we've just raised!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output should look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frut8bfu1mq5fvzghbss7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frut8bfu1mq5fvzghbss7.png" alt="gh pr list output showing three open pull requests for the feature branches" width="800" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can then merge each PR using the &lt;code&gt;gh&lt;/code&gt; CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;merge feature/standings &lt;span class="nt"&gt;--squash&lt;/span&gt;
gh &lt;span class="nb"&gt;pr &lt;/span&gt;merge feature/h2h       &lt;span class="nt"&gt;--squash&lt;/span&gt;
gh &lt;span class="nb"&gt;pr &lt;/span&gt;merge feature/upcoming  &lt;span class="nt"&gt;--squash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once everything has been merged, we can bring the squashed commits down to our local main:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /mnt/c/Users/willv/Documents/Learning/fixtures-api
git checkout main
git pull
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fegf08gcvumu80dbw46xl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fegf08gcvumu80dbw46xl.png" alt="git log on main after squash-merging all three feature pull requests" width="631" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, confirm and build everything to make sure it all works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;     &lt;span class="c"&gt;# three new squash commits on main&lt;/span&gt;
&lt;span class="nb"&gt;ls &lt;/span&gt;Endpoints/            &lt;span class="c"&gt;# all three endpoint files present&lt;/span&gt;
dotnet build             &lt;span class="c"&gt;# green&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspxc3wzqv3nj4gezzbl4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspxc3wzqv3nj4gezzbl4.png" alt="dotnet build succeeding on main after all three endpoints are merged" width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you're satisfied that everything looks good, you can clean up the worktrees like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree remove ../fixtures-standings
git worktree remove ../fixtures-h2h
git worktree remove ../fixtures-upcoming
git worktree prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git worktree remove&lt;/code&gt; deletes each sibling folder and frees its branch. Once that's done, the merged branches are safe to delete (both locally and remote):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch &lt;span class="nt"&gt;-d&lt;/span&gt; feature/standings feature/h2h feature/upcoming
git push origin &lt;span class="nt"&gt;--delete&lt;/span&gt; feature/standings feature/h2h feature/upcoming
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run &lt;code&gt;git worktree list&lt;/code&gt; or &lt;code&gt;git branch&lt;/code&gt; again, you should only see the &lt;code&gt;main&lt;/code&gt; branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Combining tmux and git worktrees has given me the confidence to run multiple coding agents simultaneously without them trampling on each other's branches. tmux gives us session persistence and isolation, and git worktrees give each agent a clean workspace to work with, preventing agents from overwriting each other's changes.&lt;/p&gt;

&lt;p&gt;Bear in mind that this is just the basics. There's a lot more that we can build on here, including extending orchestration tools, and scaling past just a couple of agents. As always though, this is AI-assisted engineering, not a replacement. Make sure that you have &lt;a href="https://www.willvelida.com/posts/securing-ai-agents-owasp-agentic-top10-overview/" rel="noopener noreferrer"&gt;sufficient guardrails&lt;/a&gt; in place so that your agent swarms don't cause production faults! You're still getting that call at 2am when shit goes sideways!&lt;/p&gt;

&lt;p&gt;If you have any questions about this, please feel free to reach out to me on &lt;a href="https://bsky.app/profile/willvelida.com" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Until next time, Happy coding! 🤓🖥️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>github</category>
      <category>productivity</category>
      <category>git</category>
    </item>
    <item>
      <title>What are Agent Plugins?</title>
      <dc:creator>Will Velida</dc:creator>
      <pubDate>Fri, 07 Aug 2026 11:42:24 +0000</pubDate>
      <link>https://dev.to/willvelida/what-are-agent-plugins-3bf7</link>
      <guid>https://dev.to/willvelida/what-are-agent-plugins-3bf7</guid>
      <description>&lt;p&gt;Implementing &lt;a href="https://agentskills.io/" rel="noopener noreferrer"&gt;Agent Skills&lt;/a&gt; and &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP servers&lt;/a&gt; as part of my development workflow has been a huge boost to my productivity. Long gone are the days where I'd actually have to leave my IDE to raise a pull request in GitHub, and then manually add that PR to a Jira ticket that's still impossible to find in 2026. &lt;/p&gt;

&lt;p&gt;Now it's just a matter of me asking GitHub Copilot to create the PR using my &lt;code&gt;create-pr&lt;/code&gt; Agent Skill, then use the Atlassian MCP Server to link the PR to the appropriate Jira ticket. All of which I can do without ever having to leave my IDE.&lt;/p&gt;

&lt;p&gt;However, I use different coding agents for my development. I'll use GitHub Copilot heavily for work, but I'll use Claude Code and Cursor for my side-projects. For those of you who've used both tools in anger will know that configuring Agent Skills, MCP servers, agents and other agentic components differ slightly between the two tools, which was a bit of a pain to deal with every time I wanted to use the same components in different tools.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://agent-plugins.org/" rel="noopener noreferrer"&gt;Agent Plugins&lt;/a&gt; standard was announced on August 6th 2026, which defined a packaging layer that sits on top of both Agent Skills and MCP Servers, giving developers a way of installing agentic coding components in a standardized format across different coding agent clients.&lt;/p&gt;

&lt;p&gt;Considering it's only a day old (at time of writing), I decided to look at the docs, and see if I could develop a plugin for my health project using it's MCP server. There are some gaps that need to be addressed, but I think it's a big step towards a standard for giving developers what they need, rather than having to configure every individual component themselves.&lt;/p&gt;

&lt;p&gt;In this blog post, I'll talk about what Agent Plugins are, why we need them, how we can build one, how we can install plugins across different clients, and what the gaps are that need to be addressed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Agent Plugins?
&lt;/h2&gt;

&lt;p&gt;Agent Plugins are an open, vendor-neutral standard for packaging reusable agent components into portable plugins. It defines a shared format for Agent Skills and MCP servers that compatible clients can discover and load.&lt;/p&gt;

&lt;p&gt;This is an initiative from AWS, Cursor, Microsoft, OpenAI, and Vercel, who are part of the &lt;a href="https://github.com/agentplugins/agent-plugins-spec/blob/main/GOVERNANCE.md" rel="noopener noreferrer"&gt;Technical Steering Committee&lt;/a&gt;. Google has also joined as a Core Maintainer.&lt;/p&gt;

&lt;p&gt;Agent Plugins are just directories that include a required manifest and optional components. Take the following as an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-plugin/
├── plugin.json
├── skills/
│   └── weekly-health-brief/
│       ├── SKILL.md
│       ├── scripts/
│       └── references/
├── mcp.json
└── com.biotrackr.client/
    └── hooks/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;plugin.json&lt;/code&gt; identifies the plugin and the Agent Plugins version it targets.  &lt;code&gt;skills/&lt;/code&gt; contains Agent Skills that the plugin will use. &lt;code&gt;mcp.json&lt;/code&gt; describes MCP servers that the plugin has access to. These can be stdio, Streamable HTTP, or legacy HTTP+SSE servers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fahmbb3w9dka1x12t6m16.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fahmbb3w9dka1x12t6m16.png" alt="Diagram of the Agent Plugins portable package (plugin.json, skills/, and mcp.json) loading into compatible clients like VS Code, GitHub Copilot, and Cursor" width="799" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do we need them?
&lt;/h2&gt;

&lt;p&gt;Agent Skills and MCP servers have been fantastic in augmenting our coding agents. The problem was that each client had their own way of implementing them. For example, in VS Code you have &lt;a href="https://code.visualstudio.com/docs/agent-customization/agent-skills" rel="noopener noreferrer"&gt;native support&lt;/a&gt; for the &lt;code&gt;SKILL.md&lt;/code&gt; format. They live in &lt;code&gt;.github/skills/&amp;lt;name&amp;gt;&lt;/code&gt;, &lt;code&gt;.agents/skills/&lt;/code&gt; (project level), or &lt;code&gt;~/.copilot/skills/&lt;/code&gt; (personal), and you define them like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webapp-testing&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run and debug integration tests for the web app. Use when asked to test the login page or a UI flow.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Web application testing&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Start the dev server with &lt;span class="sb"&gt;`npm run dev`&lt;/span&gt;.
&lt;span class="p"&gt;2.&lt;/span&gt; Use the template at &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;test template&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;./test-template.js&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Cursor, you don't have native support for &lt;code&gt;SKILL.md&lt;/code&gt;. Instead, Cursor's equivalent is something called &lt;a href="https://cursor.com/docs/context/rules" rel="noopener noreferrer"&gt;&lt;strong&gt;Rules&lt;/strong&gt;&lt;/a&gt; (that live in &lt;code&gt;.cursor/rules/*.mdc&lt;/code&gt;) or just the &lt;code&gt;AGENTS.md&lt;/code&gt; file. A rule is markdown and frontmatter controlling &lt;em&gt;when&lt;/em&gt; it applies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RPC service conventions for the backend&lt;/span&gt;
&lt;span class="na"&gt;globs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/services/**/*.ts&lt;/span&gt;
&lt;span class="na"&gt;alwaysApply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="p"&gt;-&lt;/span&gt; Define each service in its own file under &lt;span class="sb"&gt;`src/services/`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Validate inputs at the service boundary.
&lt;span class="p"&gt;-&lt;/span&gt; Reference @service-template.ts when creating a new service.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agent Plugins is a good step towards solving this problem, as the &lt;a href="https://agent-plugins.org/specification" rel="noopener noreferrer"&gt;specification&lt;/a&gt; defines a small interoperability floor for parts that can be portable across different coding agents. Shared components can use a predictable structure, while things like distribution, installation, permissions, UX and client-specifics can be handled by the clients themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a minimal plugin
&lt;/h2&gt;

&lt;p&gt;For &lt;a href="https://github.com/willvelida/biotrackr" rel="noopener noreferrer"&gt;Biotrackr&lt;/a&gt;, I have an MCP server that exposes tools from across my various APIs. All this plugin will do is use that MCP server to retrieve information from my health data, as there are no write operations that the MCP Server exposes. Pretty basic, but good enough to see how Plugins work without potentially deleting any data.&lt;/p&gt;

&lt;p&gt;Since we're just exploring Plugins, I'll keep the directory simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;biotrackr-brief/
├── plugin.json
├── skills/
│   └── weekly-health-brief/
│       └── SKILL.md
├── mcp.json
├── README.md
└── LICENSE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll define the &lt;code&gt;plugin.json&lt;/code&gt; manifest, give it a single skill, create a &lt;code&gt;mcp.json&lt;/code&gt; configuration file, and add a &lt;code&gt;README.md&lt;/code&gt; and &lt;code&gt;LICENSE&lt;/code&gt; file (even though I'm not publishing this 😅).&lt;/p&gt;

&lt;p&gt;Let's take a look at the &lt;code&gt;plugin.json&lt;/code&gt; manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"biotrackr-brief"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Turns Biotrackr health data into a short weekly brief. Bundles a weekly-health-brief skill with Biotrackr's MCP server (activity, sleep, vitals, and nutrition tools)."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Will Velida"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/willvelida"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"homepage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/willvelida/biotrackr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/willvelida/biotrackr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MIT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"fitbit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"withings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"weekly-brief"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"activity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"sleep"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"vitals"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"nutrition"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;SKILL.md&lt;/code&gt; file is pretty simple, we're just going to ask it to retrieve a weekly summary of our data for us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;weekly-health-brief&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Assembles a concise weekly health brief from Biotrackr activity, sleep, vitals, and nutrition data. Use when the user asks for a weekly summary, a health recap, "how was my week", or a brief covering steps, sleep, weight, or diet trends over the last seven days.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Weekly health brief&lt;/span&gt;

etc. etc.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also configure the MCP servers that the plugins will use in the &lt;code&gt;mcp.json&lt;/code&gt; file like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"biotrackr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"streamable-http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://biotrackr-mcp-server-dev.io"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"X-Api-Key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"API_KEY"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Loading the plugin across clients
&lt;/h2&gt;

&lt;p&gt;In VS Code, we can load our plugin via our user settings json file. Within it, we need to add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chat.plugins.enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chat.pluginLocations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"&amp;lt;directory-path&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;chat.plugins.enabled&lt;/code&gt; setting turns the Agent Plugin feature on. The &lt;code&gt;chat.pluginLocations&lt;/code&gt; maps the plugin's root directory to &lt;code&gt;true&lt;/code&gt;. VS Code &lt;a href="https://code.visualstudio.com/docs/agent-customization/agent-plugins" rel="noopener noreferrer"&gt;auto-detects the Agent Plugins format&lt;/a&gt; from the &lt;code&gt;$schema&lt;/code&gt; in the &lt;code&gt;plugin.json&lt;/code&gt; file, then it loads the &lt;code&gt;weekly-health-brief&lt;/code&gt; skill and the &lt;code&gt;biotrackr&lt;/code&gt; MCP server from the &lt;code&gt;mcp.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can manage the plugin in the &lt;strong&gt;Extensions&lt;/strong&gt; view. Search &lt;code&gt;@agentPlugins&lt;/code&gt; in the Extensions view to see the &lt;strong&gt;Agent Plugins&lt;/strong&gt; that have been installed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2t3tom6rgm5skjjg7lfp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2t3tom6rgm5skjjg7lfp.png" alt="The VS Code Extensions view filtered by @agentPlugins, showing the installed biotrackr-brief Agent Plugin" width="463" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our Agent Plugin can now be added to other Agent-Plugin compatiable clients, like Cursor. We can add our plugin using the &lt;strong&gt;Customize&lt;/strong&gt; menu, and then adding the plugin from our local repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foc57685d21yjiq1ptxc4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foc57685d21yjiq1ptxc4.png" alt="Cursor's Customize menu with the biotrackr-brief plugin being added from a local repository" width="800" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can inspect the plugin and see what skills are available to the plugin, as well as any MCP Servers that are packaged as part of the plugin. For my Biotrackr plugin, I can see the &lt;code&gt;weekly-health-brief&lt;/code&gt; skill and biotrackr MCP as part of the plugin.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk08qoscl8gduym13b7ja.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk08qoscl8gduym13b7ja.png" alt="Cursor showing the biotrackr-brief plugin's contents: the weekly-health-brief skill and the biotrackr MCP server" width="799" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I can also see what MCP tools are available as part of the plugin:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm1yccb0m67riyitu6uur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm1yccb0m67riyitu6uur.png" alt="The list of MCP tools the biotrackr MCP server exposes to the plugin in Cursor" width="698" height="971"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From there, we can start a new chat, and use the skill to get a weekly view of my health data. For example, let's take a look at my data for the week:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmp9gy14jsghgxjelv6nq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmp9gy14jsghgxjelv6nq.png" alt="A Cursor chat using the weekly-health-brief skill to produce a weekly view of Biotrackr health data" width="800" height="774"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drilling down into Cursor's thought process, we can see that Cursor has taken a look at the &lt;code&gt;SKILL.md&lt;/code&gt; file, and used the MCP server to fetch our weekly data from all our domains:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxllqkvgzistqxokhfjfd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxllqkvgzistqxokhfjfd.png" alt="Cursor's reasoning trace showing it read the SKILL.md file and called the biotrackr MCP server to fetch the week's data across all health domains" width="800" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using the same file format and specification, we can use skills and MCP servers bundled as a plugin across two clients without having to change any of the files to be compliant with a particular coding assistant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;It's important to point out that this is just a starting point. I think it's a step in the right direction, but there's a couple of features missing that still need to be addressed.&lt;/p&gt;

&lt;p&gt;Currently, there's no permission model, no sandboxing, no trust/provenance and no approval UX for MCP servers that execute commands or access external services. As you can probably imagine, that's a massive task, as install, policy, and enterprise controls will differ across developer environments, CLIs and managed platforms.&lt;/p&gt;

&lt;p&gt;There's also no portable secret mechanism. The &lt;code&gt;mcp.json&lt;/code&gt; schema is closed, and it forbids any credentials in &lt;code&gt;headers&lt;/code&gt; or &lt;code&gt;env&lt;/code&gt;. This makes sense from a package perspective, as packages are intended to be for public consumption.&lt;/p&gt;

&lt;p&gt;My MCP Server sits behind Azure APIM, and there's no portable slot for the subscription key in the plugin's &lt;code&gt;mcp.json&lt;/code&gt; schema.&lt;/p&gt;

&lt;p&gt;For my local plugin, it was fine to hardcode the &lt;code&gt;X-Api-Key&lt;/code&gt; header just to get it running on my machine. If this was a package that I published, authentication would have to be handled by the client. Taking VS Code as an example, that would come from VS Code's &lt;code&gt;.vscode/mcp.json&lt;/code&gt; config file.&lt;/p&gt;

&lt;p&gt;There are a list of &lt;a href="https://github.com/agentplugins/agent-plugins-spec/blob/main/FUTURE_CONSIDERATIONS.md" rel="noopener noreferrer"&gt;future considerations&lt;/a&gt; that the Agent Plugin team are working on, so I expect these questions to be answered over time.&lt;/p&gt;

&lt;p&gt;Another gap is support for &lt;a href="https://code.claude.com/docs/en/plugins-reference" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;. Claude has its own plugin system which already comes with skills, agents, hooks, commands, and MCP servers. What differs between Claude Code plugins and Agent Plugins are the manifest and discovery. Agent Plugins uses a root &lt;code&gt;plugin.json&lt;/code&gt; with the canonical &lt;code&gt;$schema&lt;/code&gt;; Claude Code uses &lt;code&gt;.claude-plugin/plugin.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Agent Plugins are a simple, but positive step forward for sharing Agentic coding capabilities across teams who use different agentic coding tools. It was a pain having to make edits to skills and MCP servers just so they could work from one tool to another.&lt;/p&gt;

&lt;p&gt;There are a couple of major gaps that need to be addressed, but I think this is a big step towards having mature package managers for Agent Plugins, rather than having to copy/paste between different repositories and coding environments!&lt;/p&gt;

&lt;p&gt;If you have any questions about this, please feel free to reach out to me on Bluesky!&lt;/p&gt;

&lt;p&gt;Until next time, Happy coding! 🤓🖥️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Distributed tracing for multi-agent systems with OpenTelemetry</title>
      <dc:creator>Will Velida</dc:creator>
      <pubDate>Thu, 06 Aug 2026 13:01:48 +0000</pubDate>
      <link>https://dev.to/willvelida/distributed-tracing-for-multi-agent-systems-with-opentelemetry-5c8</link>
      <guid>https://dev.to/willvelida/distributed-tracing-for-multi-agent-systems-with-opentelemetry-5c8</guid>
      <description>&lt;p&gt;Multi-agentic architectures can pose challenges from an observability perspective. Each agent within a workflow makes its calls to its own toolset and each agent may be configured to use a different underlying LLM (or even a Model Router which picks the most efficient model for us). Being able to observe which agent made what call with which model in a unified manner can be challenging over an entire workflow.&lt;/p&gt;

&lt;p&gt;OpenTelemetry provides semantic conventions that help us solve this problem and Microsoft Foundry provides tracing integrations for agent frameworks that enable us to implement tracing for different agentic frameworks, and assist us in quickly identifying root causes for issues in our agents.&lt;/p&gt;

&lt;p&gt;Imagine we have a multi-agent solution for an incident drill runner. We use this to run chaos engineering drills and a crew of agents investigates a simulated outage in the same way an on-call roster would do. We have an agent for an incident commander who fans out to a Logs specialist, a Metrics specialist, and a Runbook specialist who all investigate the simulated outage. They report back to the incident commander, who then assembles the timeline of the incident and provides a suggested solution.&lt;/p&gt;

&lt;p&gt;Each specialist agent is configured with its own tool:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read logs and follow them downstream&lt;/td&gt;
&lt;td&gt;&lt;code&gt;query_logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5-mini&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Find the change point in a metric series&lt;/td&gt;
&lt;td&gt;&lt;code&gt;query_metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5-mini&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;runbook&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Match observed symptoms to the catalog&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lookup_runbook&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5-mini&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;incident-commander&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assemble a timeline and a mitigation&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;&lt;code&gt;model-router&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;N.B. The &lt;code&gt;incident-commander&lt;/code&gt; uses the &lt;code&gt;model-router&lt;/code&gt; deployment. If you're running the sample yourself, you will notice different &lt;code&gt;gen_ai.response.model&lt;/code&gt; values for different runs. If you want to learn more about how Model Routing works, check out &lt;a href="https://youtu.be/iQ6TDvYG7KU" rel="noopener noreferrer"&gt;this video&lt;/a&gt; on YouTube&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The flow looks something like the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8033tzfuokyrl5x4jg73.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8033tzfuokyrl5x4jg73.png" alt="Diagram of the game-day crew. A simulated outage feeds the incident-commander agent, which runs on a model-router deployment and delegates to three specialists on gpt-5-mini: logs calling query_logs, metrics calling query_metrics, and runbook calling lookup_runbook. Their findings return to the commander, which assembles an incident timeline and a suggested mitigation." width="799" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, we'll discuss how distributed observability works for multi-agent systems, how we can propagate context across agents within our workflow, dive a little deeper into the OpenTelemetry Semantic Conventions for GenAI, and how we can implement observability in our agents and Microsoft Foundry so that we have a unified view across our agents.&lt;/p&gt;

&lt;p&gt;If you want to take a look at the code while reading the article, you can view the complete sample &lt;a href="https://github.com/willvelida/cost-effective-multi-agents/tree/main/game-day" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I've also made a YouTube video for this too! Check it out 👇&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/28z8UyjpjAk"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What does distributed observability mean for multi-agent systems?
&lt;/h2&gt;

&lt;p&gt;Multiple agents can be invoked as part of an agentic workflow, with each agent making calls to its own tools and different LLMs. Agent-to-agent calls and tool chains mean that per-agent observability isn't enough to get a complete picture of what happened, we need to have a connected trace across the whole system.&lt;/p&gt;

&lt;p&gt;OpenTelemetry provides a standardized protocol for collecting and routing telemetry data. &lt;a href="https://learn.microsoft.com/azure/foundry/observability/concepts/trace-agent-concept" rel="noopener noreferrer"&gt;Microsoft Foundry uses OpenTelemetry semantic conventions&lt;/a&gt; to provide consistency across tools and integrations. Within Foundry, tracing captures information such as user inputs and agent outputs, tool usage, token consumption, and time signals such as duration and latency.&lt;/p&gt;

&lt;p&gt;There are a couple of OpenTelemetry concepts we should discuss before moving on. Traces 'trace' the journey of a request or workflow through your application by recording events and state changes (which can include function calls, and system events). Spans are the building blocks of traces. They represent a single operation within a trace, and capture start and end times, attributes, and can be nested to show hierarchical relationships so we can see the full call stack and sequence of operations. Attributes are essentially key-value pairs attached to traces and spans, and provide contextual metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Propagating context across agents
&lt;/h3&gt;

&lt;p&gt;For multi-agentic systems, span context contains both a trace ID and span ID. The span context gets propagated across agent boundaries using the &lt;a href="https://www.w3.org/TR/trace-context/" rel="noopener noreferrer"&gt;W3C Trace Context standard&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;When a receiving agent takes the context, it will create a child span with the same trace ID, but create a new span ID. If the agent needs to call any downstream agents, the context will be propagated. Without this, each agent would create a new trace ID, and we'd lose the correlation.&lt;/p&gt;

&lt;p&gt;For our incident drill simulator, one drill would be one trace. Our span is opened when the commander is invoked and the drill scenario is created.&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="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;drill_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;drill_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;incident_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;incident_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;game_day_drill&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SpanKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SERVER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;brief&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_brief&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symptoms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;findings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_delegate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crew&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brief&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_assemble&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crew&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brief&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;with drill_context()&lt;/code&gt; attaches &lt;a href="https://opentelemetry.io/docs/concepts/signals/baggage/" rel="noopener noreferrer"&gt;Baggage&lt;/a&gt;, which is a key-value store which lets you propagate any data we like alongside context. The trace is created in the &lt;code&gt;with get_tracer().start_as_current_span()&lt;/code&gt; statement. &lt;code&gt;as_current&lt;/code&gt; means that the span is created and it gets installed in the context. Any span started inside that block becomes a child of &lt;code&gt;root&lt;/code&gt;, which uses the same trace ID, but its own span ID. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;SpanKind.SERVER&lt;/code&gt; value for the &lt;code&gt;kind&lt;/code&gt; parameter starts the &lt;code&gt;SERVER&lt;/code&gt; span deliberately. Without this, Application Insights has nothing to use as the start of a transaction, so we need to set this in order to see the end-to-end view.&lt;/p&gt;

&lt;p&gt;In order to give our processor a rule for what to copy, we can provide a &lt;code&gt;BAGGAGE_PREFIX&lt;/code&gt; namespace so that our key &lt;code&gt;id&lt;/code&gt; becomes &lt;code&gt;drill.id&lt;/code&gt; like so:&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="n"&gt;BAGGAGE_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;drill.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we need to set the Baggage. In OTel, contexts are immutable, so we use the &lt;code&gt;set_baggage&lt;/code&gt; method to return a new context rather than modifying each one:&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="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;drill_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;baggage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_baggage&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BAGGAGE_PREFIX&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Baggage lives in the context, while attributes live in the spans. To bring them together, we need to use a span processor to hook the start of the span, and then use a &lt;code&gt;BaggageSpanProcessor&lt;/code&gt; to copy the matching baggage across as attributes:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_install_baggage_processor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tracer_provider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;add_span_processor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;add_span_processor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;add_span_processor&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="nf"&gt;add_span_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BaggageSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BAGGAGE_PREFIX&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The GenAI semantic conventions
&lt;/h3&gt;

&lt;p&gt;The OpenTelemetry semantic conventions define a common set of semantic attributes which provide meaning to data when collecting, producing and consuming it. As far as agents are concerned, &lt;a href="https://github.com/open-telemetry/semantic-conventions-genai" rel="noopener noreferrer"&gt;OpenTelemetry has conventions for spans, metrics, and events for GenAI clients, MCP servers and provider-specific (For example, OpenAI LLMs) conventions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Different namespaces exist for different attributes. For example, &lt;code&gt;gen_ai.*&lt;/code&gt; has key attributes like &lt;code&gt;gen_ai.agent.name&lt;/code&gt;, &lt;code&gt;gen_ai.agent.id&lt;/code&gt;, &lt;code&gt;gen_ai.operation.name&lt;/code&gt;, &lt;code&gt;gen_ai.usage.*&lt;/code&gt;. Underneath those attributes, there are well known values. For example &lt;code&gt;gen_ai.operation.name&lt;/code&gt; has values such as &lt;code&gt;invoke_agent&lt;/code&gt;, &lt;code&gt;execute_tool&lt;/code&gt;, &lt;code&gt;execute_task&lt;/code&gt;, &lt;code&gt;agent_to_agent_interaction&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Standardised conventions provide us consistency across different agentic frameworks, so you can use the same tracing standards for agents that use Microsoft Agent Framework and agents that use LangChain, OpenAI Agents SDK etc.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://learn.microsoft.com/agent-framework/agents/observability?pivots=programming-language-python#opentelemetry-integration" rel="noopener noreferrer"&gt;Microsoft Agent Framework instruments&lt;/a&gt; &lt;code&gt;invoke_agent&lt;/code&gt;, &lt;code&gt;chat&lt;/code&gt;, and &lt;code&gt;execute_tool&lt;/code&gt; automatically. However, when we need to trace when the Commander agent hands work to our specialist, we need to implement that ourselves.&lt;/p&gt;

&lt;p&gt;This is what happens inside &lt;code&gt;_delegate()&lt;/code&gt;, the function our &lt;code&gt;asyncio.gather&lt;/code&gt; calls once per specialist:&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="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_to_agent_interaction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SpanKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLIENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attributes&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;gen_ai.operation.name&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;agent_to_agent_interaction&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;gen_ai.agent.name&lt;/span&gt;&lt;span class="sh"&gt;"&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;game_day.from_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;COMMANDER_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;game_day.to_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;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;game_day.role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&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;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;run&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;brief&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SPECIALIST_ANGLE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;]&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;agent.run()&lt;/code&gt; sits inside this &lt;code&gt;with&lt;/code&gt; block, the &lt;code&gt;invoke_agent&lt;/code&gt; span that Agent Framework opens becomes a child of our hand-off span. That gives us the three levels we'll see in the portal later: the drill, the hand-off, then the specialist's own agent, chat and tool spans. Without it we'd have three &lt;code&gt;invoke_agent&lt;/code&gt; spans sharing a parent and no record of who handed work to whom.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenTelemetry with Azure Monitor
&lt;/h3&gt;

&lt;p&gt;Azure Monitor supports &lt;a href="https://learn.microsoft.com/azure/azure-monitor/containers/opentelemetry-options" rel="noopener noreferrer"&gt;two approaches with OpenTelemetry&lt;/a&gt;. Either through its native OpenTelemetry Protocol (OTLP) ingestion path so we can send standard logs, metrics, and traces into Azure Monitor using the open source collector, or we can use the &lt;a href="https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-enable" rel="noopener noreferrer"&gt;Microsoft OpenTelemetry Distro&lt;/a&gt;, which is a client that bundles all open-source and Microsoft components required for a full integrated experience with Azure Monitor for both AI Agents and normal applications.&lt;/p&gt;

&lt;p&gt;To use OpenTelemetry in Python, we need to install the following &lt;a href="https://learn.microsoft.com/python/api/overview/azure/monitor-opentelemetry-readme" rel="noopener noreferrer"&gt;dependencies&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent-framework-foundry&amp;gt;=1.10.4
azure-identity&amp;gt;=1.19.0
# Not installed by Agent Framework. Without it there is no Application Insights exporter.
azure-monitor-opentelemetry&amp;gt;=1.6.0
# Copies drill baggage onto every span, which is what makes one drill one queryable unit.
opentelemetry-processor-baggage&amp;gt;=0.65b0
python-dotenv&amp;gt;=1.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the purposes of this demo, I wanted to extend the sample so that we can have the option of either seeing the traces in the console, or in Azure Monitor, so we'll need to configure both like so:&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FoundryChatClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sensitive_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;otlp_endpoint&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&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="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;otlp_endpoint&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENABLE_CONSOLE_EXPORTERS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;configure_otel_providers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable_sensitive_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sensitive_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_metric_views&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;otlp_endpoint&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the console&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;connection_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_application_insights_connection_string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;configure_azure_monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;connection_string&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;connection_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;create_resource&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;enable_live_metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_metric_views&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Application Insights, via the Foundry project connection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# ENABLE_INSTRUMENTATION defaults to false. Without this the crew runs and emits nothing.
&lt;/span&gt;    &lt;span class="nf"&gt;enable_instrumentation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable_sensitive_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sensitive_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing that I noticed is that we need to add the &lt;code&gt;enable_instrumentation()&lt;/code&gt; method in order to export our telemetry data. Agent Framework disables this by default, and while our agents will run, without this our telemetry isn't emitted.&lt;/p&gt;

&lt;p&gt;Another thing worth mentioning is the &lt;code&gt;enable_sensitive_data&lt;/code&gt; parameter. This is what puts prompts, tool arguments, and tool results into the spans. &lt;a href="https://learn.microsoft.com/azure/foundry/observability/concepts/trace-agent-concept#security-and-privacy" rel="noopener noreferrer"&gt;It's off by default, and it should stay off for production&lt;/a&gt;. This sample is just using synthetic data to show what's possible. If you want to keep user prompts out of Application Insights, set this to &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building our observable multi-agent system
&lt;/h2&gt;

&lt;p&gt;As I mentioned earlier, the Microsoft Agent Framework provides some of this out of the box. Once we've configured our instrumentation, these spans appear without any extra effort on our part. What's lacking is what choices our agents have made.&lt;/p&gt;

&lt;p&gt;If we wanted to see what tool calls our agent makes, we want to be able to see that within our traces. For example, for our Logging Agent, we can configure this like so:&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="nd"&gt;@tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;approval_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;never_require&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query_logs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Service name from the service map, for example &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;checkout-api&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&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;min_level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lowest severity to return: info, warn or error.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warn&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Read recent log lines for one service in the incident window.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_level&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_logs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current_span&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;game_day.tool.service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;game_day.tool.min_level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;game_day.tool.result_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break this down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@tool&lt;/code&gt; registers the tool as a function within the Microsoft Agent Framework.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;_clean()&lt;/code&gt; method trims and caps every argument. Our tool arguments come from a model, so it's best to treat this as untrusted input.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;trace.get_current_span()&lt;/code&gt; method grabs the &lt;code&gt;execute_tool&lt;/code&gt; span that is already open. We're not creating a new span, we are adding to the one that already exists to get that unified span.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;game_day.tool.*&lt;/code&gt; attributes record what tool the agent invoked, and what it got back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those attributes, we wouldn't be able to see that the Logs agent looked at the wrong service, or that a query returned 0 results but it did some work anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing it work
&lt;/h2&gt;

&lt;p&gt;Let's see this in action. We can run one of the drills like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python demo.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces the following output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;1] INC-A1C212  checkout latency spike     normal  sev1      48.5 s  drill 1b1b480ad6e2
    logs       24.9 s  Checkout-api p99 latency and error spike caused by payments-gateway connection-pool exhaustion leading to timeouts and a circuit-breaker opening.
    metrics    24.4 s  ~09:25 spike &lt;span class="k"&gt;in &lt;/span&gt;checkout-api p99 and errors, traced to payments-gateway timeouts after a 09:20 deploy that reduced its connection pool.
    runbook    12.5 s  RB-014 &lt;span class="o"&gt;(&lt;/span&gt;Downstream connection pool exhaustion&lt;span class="o"&gt;)&lt;/span&gt; selected&lt;span class="p"&gt;;&lt;/span&gt; confirm pool.max vs prior release and restore it &lt;span class="k"&gt;if &lt;/span&gt;it was lowered.
    commander  routed to grok-4-1-fast-reasoning
    tool calls  lookup_runbook 1  query_logs 4  query_metrics 6    total 11

     - ~09:20: Deploy to payments-gateway reduced connection pool max from 200 to 20 &lt;span class="o"&gt;(&lt;/span&gt;Metrics&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
    - ~09:25: p99 latency jumps &lt;span class="k"&gt;in &lt;/span&gt;payments-gateway &lt;span class="o"&gt;(&lt;/span&gt;90ms to 7.66s&lt;span class="o"&gt;)&lt;/span&gt; and checkout-api &lt;span class="o"&gt;(&lt;/span&gt;~360ms to 1.9s &lt;span class="k"&gt;then &lt;/span&gt;7.9s&lt;span class="o"&gt;)&lt;/span&gt;, with error rates rising to 17% and 15% respectively&lt;span class="p"&gt;;&lt;/span&gt; request volumes flat &lt;span class="k"&gt;then &lt;/span&gt;falling &lt;span class="o"&gt;(&lt;/span&gt;Metrics&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
    - payments-gateway connection pool saturated: &lt;span class="nv"&gt;in_use&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20 &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20 &lt;span class="nv"&gt;waiters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;61, &lt;span class="k"&gt;then &lt;/span&gt;pool acquire timed out after 8000ms with &lt;span class="nv"&gt;waiters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;112 &lt;span class="o"&gt;(&lt;/span&gt;Logs&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
    - checkout-api slow downstream calls to payments-gateway: &lt;span class="nv"&gt;duration_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6902 &lt;span class="o"&gt;(&lt;/span&gt;Logs&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
    - checkout-api upstream timeouts on payments-gateway after 8000ms &lt;span class="o"&gt;(&lt;/span&gt;Logs&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
    - Circuit breaker opened on payments-gateway dependency after 23 failures &lt;span class="o"&gt;(&lt;/span&gt;Logs&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

    RB-014 &lt;span class="s2"&gt;"Downstream connection pool exhaustion"&lt;/span&gt; matches symptoms of latency step change, flat-then-falling volume, and upstream timeouts. Confirm current pool.max value against the previous release&lt;span class="p"&gt;;&lt;/span&gt; since metrics evidence shows it was reduced from 200 to 20 by the 09:20 deploy, raise it back to 200 or roll back the release. Monitor p99 latency, error rates, and pool metrics post-mitigation to confirm recovery.

    SEVERITY: sev1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header line shows the root span duration (48.5s). All 3 agents ran concurrently and have returned their findings back to, or reported back to, the incident commander. We can see that the Commander agent uses &lt;code&gt;grok-4-1-fast-reasoning&lt;/code&gt; to produce the timeline and a mitigation path.&lt;/p&gt;

&lt;p&gt;Within the tool calls, we can see how many calls each agent made to various tools, along with a total.&lt;/p&gt;

&lt;p&gt;Within the classic Foundry portal, we can see the entire &lt;code&gt;game_day_drill&lt;/code&gt; trace in the &lt;a href="https://learn.microsoft.com/azure/foundry/observability/how-to/trace-agent-setup" rel="noopener noreferrer"&gt;&lt;strong&gt;Tracing&lt;/strong&gt;&lt;/a&gt; portal. Our run generated a trace ID &lt;code&gt;aaaeef28ce1288c5cf456d39a345a576&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1b2xyt3vao9qo4f6aw33.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1b2xyt3vao9qo4f6aw33.png" alt="The Tracing view in the classic Foundry portal, showing the full game_day_drill trace for trace ID aaaeef28ce1288c5cf456d39a345a576. The span tree has the drill as the root, with the agent-to-agent hand-offs and each specialist's agent, chat and tool spans nested beneath it." width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Taking a closer look at this, we can see that the child spans all use the same &lt;code&gt;trace_id&lt;/code&gt;, so looking at the metadata of &lt;code&gt;game_day_logs&lt;/code&gt;, it uses a new &lt;code&gt;span_id&lt;/code&gt;, but has the same &lt;code&gt;trace_id&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7uw47w885famvwkmszg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7uw47w885famvwkmszg8.png" alt="Span metadata for the game_day_logs span in the Foundry portal. It carries its own span_id but the same trace_id as the root drill span, which is what keeps the whole drill in one trace." width="800" height="743"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can also view the entire transaction in &lt;a href="https://learn.microsoft.com/azure/azure-monitor/app/failures-performance-transactions#transaction-diagnostics-experience" rel="noopener noreferrer"&gt;&lt;strong&gt;Application Insights' Transactions&lt;/strong&gt;&lt;/a&gt; using that &lt;code&gt;trace_id&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffq3q7klq22al0mqn95ny.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffq3q7klq22al0mqn95ny.png" alt="The Transactions view in Application Insights for the same trace_id, showing the drill as a single end-to-end transaction with its timeline of dependencies laid out in a waterfall." width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When models are invoked, we can view GenAI properties such as input and output tokens spent, as well as how long the call took:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff50go99nhgdzk3zyn0lt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff50go99nhgdzk3zyn0lt.png" alt="The detail pane for a model call in Application Insights, listing the GenAI properties captured on the span, including input and output token counts and the duration of the call." width="800" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can also view custom properties:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4iec0vtdr4ld793m90et.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4iec0vtdr4ld793m90et.png" alt="The custom properties on a span in Application Insights, showing the game_day attributes set by the sample alongside the drill identity copied across from baggage." width="800" height="863"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Application Insights has a &lt;a href="https://learn.microsoft.com/azure/azure-monitor/app/agents-view" rel="noopener noreferrer"&gt;preview feature&lt;/a&gt; where we can also view operational metrics from our agents, such as Agent runs, tool calls, models used and importantly, token consumption by model and input vs output tokens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flh3sih6pno82lozuomjt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flh3sih6pno82lozuomjt.png" alt="The preview Agents view in Application Insights, charting operational metrics across the crew: agent runs, tool calls, the models used, and token consumption broken down by model and by input against output tokens." width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From here, we can explore specific agent runs using our &lt;code&gt;trace_id&lt;/code&gt; from before. This is handy for when we want to start with a high-level overview, and then drill down into our transaction view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Hopefully you now have a better understanding of how we can use OpenTelemetry to create a unified trace for distributed multi-agentic systems. Using this unified trace, we can gain insights into how agents interact with each other, what decisions they make, and how they consume tokens.&lt;/p&gt;

&lt;p&gt;If you want to take a closer look at the code, the complete sample is available on my &lt;a href="https://github.com/willvelida/cost-effective-multi-agents/tree/main/game-day" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any questions about this, please feel free to reach out to me on &lt;a href="https://bsky.app/profile/willvelida.com" rel="noopener noreferrer"&gt;BlueSky&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Until next time, Happy coding! 🤓🖥️&lt;/p&gt;

</description>
      <category>azure</category>
      <category>ai</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>How Model Routing works for Microsoft Foundry Agents</title>
      <dc:creator>Will Velida</dc:creator>
      <pubDate>Sun, 02 Aug 2026 04:59:06 +0000</pubDate>
      <link>https://dev.to/willvelida/how-model-routing-works-for-microsoft-foundry-agents-lgh</link>
      <guid>https://dev.to/willvelida/how-model-routing-works-for-microsoft-foundry-agents-lgh</guid>
      <description>&lt;p&gt;If you prefer to watch videos over reading, check out this video on my YouTube channel:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/iQ6TDvYG7KU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;About a year ago I got back into skateboarding. Probably not the safest midlife crisis ever, but I can travel from my house to the gym, I can kickturn, and even manage a nose stall 1 out of 10 times. Look at this footage for an example&lt;/p&gt;

&lt;p&gt;I even built my own board! I applied my own grip tape, bolted on the trucks, and applied the bearings to the wheels. One thing I definitely learnt through that exercise is that skateboards cost a lot more than they used to.&lt;/p&gt;

&lt;p&gt;You know what else is expensive? Tokens. The age of tokenmaxxing (I hate that term) is truly over thanks to token-based billing over flat subscription fees.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Turns out AI labs need to make money to actually make a profit&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One thing that the move to token-based billing exposed was how much people were wasting in tokens doing simple tasks like searching the web for documentation, or even simple &lt;em&gt;git&lt;/em&gt; commands!&lt;/p&gt;

&lt;p&gt;But what if we have a multi-agent system that processes different types of tasks? Do we just use a high level model like Opus or Fable to cover our bases? Or can we be smarter than that?&lt;/p&gt;

&lt;p&gt;This is where model routing comes into play. Model Routing is a traffic system for AI that matches each user request to the appropriate model to serve that user request. &lt;/p&gt;

&lt;p&gt;In this article, we'll take a look at how model routing works in Microsoft Foundry, how we can set up model routing, and how we can configure our agents to use model routers to choose the best model for the task it's been given.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is model routing?
&lt;/h2&gt;

&lt;p&gt;Powerful reasoning models like Fable and Opus tend to deliver better quality results but they cost more request/token plus reasoning tokens to use, and respond slower. Smaller models by comparison are faster and cheaper, but can't perform complex reasoning tasks.&lt;/p&gt;

&lt;p&gt;In single-agent architectures, there is a tendency to pick powerful models to tokenmax our way out of the problem. However, since tokens are expensive, applying this logic to a multi-agent architecture will bankrupt you pretty quickly. Also, tasks that agents need to perform can vary in complexity, and using powerful models to perform simple tasks is counter-productive.&lt;/p&gt;

&lt;p&gt;Model routing acts as an automated decision-making layer that analyzes incoming tasks and directs them to the appropriate model. So instead of hard-coding every request to use an expensive, high-end model like Fable, model routing matches the complexity of the task to the optimal model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faqbvv92t3gr51r020mz5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faqbvv92t3gr51r020mz5.png" alt="Deck Check's two routing layers: application code selects the specialist agent, then each agent's Model Router deployment selects a model from its configured subset." width="799" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Model routing is ideal in multi-agent architectures, as different tasks fall into different complexity categories, so they would be better served being handled by models that are appropriate to the task they are given, rather than just burning tokens to use powerful models for simple tasks. The complexity of each request is assessed by the model router before models are invoked, and &lt;em&gt;then&lt;/em&gt; the model router will decide which model matches the requirement of the request. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;A friend of mine mentioned to me that he used Opus 4.8 to look up git commands. I don't want to shame him, but that's a simple web search....&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Microsoft Foundry has a &lt;a href="https://learn.microsoft.com/azure/foundry/openai/concepts/model-router" rel="noopener noreferrer"&gt;built-in Model router&lt;/a&gt; which is a trained ML model that analyzes each prompt in real-time and routes it to the most appropriate LLM for the task, optimizing for cost, quality of response, and latency. Model Router is an ML model that's been trained on numerous scenarios ranging from single prompts to complex agentic workflows. Microsoft updates the active router version with new models and routing improvements, so you don't have to.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For the remainder of this blog post, when we talk about the Model Router, that will refer to the Model Router capability in Microsoft Foundry. When we talk about model routing, that will apply to the concept. If you're confused about anything, please ask me in the comments&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;Model Router will analyze prompts based on complexity, reasoning, the task type, and other attributes. When Model Router is used with agents, it analyzes the full request context. This includes the system message, user message, tool definitions, conversation history etc.&lt;/p&gt;

&lt;p&gt;Model Router has three routing modes for us to choose from:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Balanced&lt;/em&gt; considers models within a small quality band, then selects the most cost-effective.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Cost&lt;/em&gt;, which uses a larger quality range and then chooses the most cost-effective model.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Quality&lt;/em&gt; mode, which just picks the highest rated model for the prompt, and ignores the cost. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each turn in the conversation is routed, as the Model Router works &lt;a href="https://learn.microsoft.com/azure/foundry/openai/how-to/model-router-agents" rel="noopener noreferrer"&gt;per-request, not per-session&lt;/a&gt;. During a conversation, you might have multiple agents using different models depending on the request. The router determines the complexity of the request based on the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For factual recalls, simple greeting, or basic follow-up questions, these requests are considered &lt;strong&gt;low complexity&lt;/strong&gt; tasks and will use cheaper models.&lt;/li&gt;
&lt;li&gt;Tool orchestration will route to mid-tier models to reliably generate structured tool calls at a lower cost.&lt;/li&gt;
&lt;li&gt;Research, multi-step reasoning, and complex code generation route are &lt;strong&gt;highly complex&lt;/strong&gt; tasks, and therefore typically will be routed to higher-performing, higher-cost models.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Within Foundry, we can create multiple model router deployments, with their own routing modes and model subset. For example, we can create a deployment called &lt;code&gt;router-efficient&lt;/code&gt; that uses the &lt;em&gt;cost&lt;/em&gt; routing mode, which then the model router uses to route our prompts to cheaper models, such as &lt;code&gt;gpt-5-nano&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying a Model Router in Microsoft Foundry
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/azure/foundry/openai/how-to/model-router" rel="noopener noreferrer"&gt;Setting up Model Routers in Microsoft Foundry&lt;/a&gt; is similar to creating a regular deployment. You can find it in the Foundry model catalog just by typing &lt;code&gt;model-router&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkc8v2l6ix5q7it4nm4z6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkc8v2l6ix5q7it4nm4z6.png" alt="Microsoft Foundry model catalog search results showing the model-router model." width="799" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The defaults give us &lt;strong&gt;Balanced&lt;/strong&gt; mode across the full supported model set. You can also customize it if you need different routing modes or you want to restrict the subset of models to route to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/azure/foundry/openai/concepts/model-router#limitations" rel="noopener noreferrer"&gt;At the time of writing, model router is limited to five regions&lt;/a&gt; (Australia East being one of them). You don't need to deploy the underlying models first, &lt;strong&gt;except&lt;/strong&gt; for any Claude models that you want to use.&lt;/p&gt;

&lt;p&gt;In Bicep, you can create your Model Routers like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource efficientRouter 'Microsoft.CognitiveServices/accounts/deployments@2025-10-01-preview' = {
  parent: account
  name: 'router-efficient'
  dependsOn: [
    project
  ]
  sku: {
    name: skuName
    capacity: routerCapacity
  }
  properties: {
    model: {
      format: 'OpenAI'
      name: 'model-router'
      version: routerVersion
    }
    routing: {
      mode: 'cost'
      models: [
        {
          format: 'OpenAI'
          name: 'gpt-5.4-nano'
          version: '2026-03-17'
        }
        {
          format: 'OpenAI'
          name: 'gpt-5-nano'
          version: '2025-08-07'
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, the model router model is just another deployment under the Microsoft Foundry account. The Foundry Project hosts the agents, with the &lt;code&gt;dependsOn: [project]&lt;/code&gt; line preventing concurrent writes to the account.&lt;/p&gt;

&lt;p&gt;For our sample, we'll use a triage agent to route to different agents depending on the task. The Model router for each specialist agent then selects the underlying model. In the Bicep block above, we've implemented the &lt;em&gt;Cost&lt;/em&gt; mode so that simple tasks get routed to agents using the &lt;code&gt;gpt-5.4-nano&lt;/code&gt; or &lt;code&gt;gpt-5-nano&lt;/code&gt; models. This is handy when we want to further refine which models handle tasks. &lt;/p&gt;

&lt;p&gt;The downside here is that you'll need to keep an eye on when certain models are deprecated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building our sample
&lt;/h2&gt;

&lt;p&gt;Going back to skateboarding, imagine a multi-agentic system for a second-hand skateboard gear marketplace, where skaters can buy second-hand gear from others, instead of having to shell out hundreds of dollars to get a decent setup.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to follow along with the code, check it out on my &lt;a href="https://github.com/willvelida/cost-effective-multi-agents/tree/main/deck-check" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Listings on the marketplace might look like the following, where we hold product information, the condition of their gear, a photo, and information about who's selling it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"L01"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Powell Peralta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Flight"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"width_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"length_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;32.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"wheelbase_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;14.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"good"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"price_aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;140&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"photo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"photo of the actual item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"seller_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"S01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"posted_days_ago"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"riding it about 8 months, few chips on the tail but no cracks. pickup Brunswick or can post"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within the marketplace, there are sellers. The information that we might have about sellers include how long their account has been open, how many sales they have completed, how many disputes they currently have open, a satisfaction rating, and whether or not they have a verified payment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"S01"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"account_age_days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"completed_sales"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;87&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"open_disputes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"verified_payment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To demonstrate model routing, we can implement the following agents:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Router&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;triage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Decide which specialist answers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;router-efficient&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read a width off a listing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;router-efficient&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;price&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Judge whether an asking price is fair&lt;/td&gt;
&lt;td&gt;&lt;code&gt;router-balanced&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Balanced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scam&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Decide whether a listing is a scam&lt;/td&gt;
&lt;td&gt;&lt;code&gt;router-frontier&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For simple queries or triaging questions, we use lower cost models for these simple tasks. For analysis or research type tasks (such as determining whether a listing is a scam or not), we route to a frontier model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring our agents
&lt;/h3&gt;

&lt;p&gt;To map our model routers, I have a &lt;code&gt;config.py&lt;/code&gt; file that maps each specialist role to a router deployment:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deployment_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SPEC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_efficient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PRICE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_balanced&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SCAM&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_frontier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents are then created with the router deployment name like so:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ensure_agents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AIProjectClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One agent per job, each on the router deployment that suits it.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;AGENT_SPECS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;agent_name&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;deck-check-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&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;definition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;PromptAgentDefinition&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="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployment_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instructions&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;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&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;created&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;]&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;name&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces the following agents and assignments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deck-check-spec  -&amp;gt; router-efficient -&amp;gt; Cost mode
deck-check-price -&amp;gt; router-balanced  -&amp;gt; Balanced mode
deck-check-scam  -&amp;gt; router-frontier  -&amp;gt; Quality mode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The triage agent also uses the efficient router, so we can configure it like this:&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="n"&gt;triage_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ensure_triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_efficient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ensure_triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AIProjectClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;deployment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&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;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;agent_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deck-check-triage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;definition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;PromptAgentDefinition&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="n"&gt;deployment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TRIAGE_INSTRUCTIONS&lt;/span&gt;&lt;span class="p"&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;return&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;name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having the Model Router as just another deployment in Foundry is useful here, because in our application code, it's just a model parameter. &lt;/p&gt;

&lt;p&gt;If we didn't have this, we'd have to implement our own classifier to perform the routing to different models, which would require more code.&lt;/p&gt;

&lt;p&gt;To observe routing decisions, each response includes the &lt;code&gt;model&lt;/code&gt; field which shows which model was selected by the Router. We can use this field to track routing distribution across our agent's interactions:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_record&lt;/span&gt;&lt;span class="p"&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;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;elapsed_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RequestRecord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_tokens_details&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RequestRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="c1"&gt;# The model field reveals which underlying model the router selected.
&lt;/span&gt;        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&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;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&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="ow"&gt;or&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;output_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&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="ow"&gt;or&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;cached_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&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="ow"&gt;or&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;latency_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;elapsed_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running our sample
&lt;/h2&gt;

&lt;p&gt;Let's buy a second hand board! Take a look at the following listing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"L04"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Santa Cruz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Screaming Hand"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"width_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"length_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;32.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"wheelbase_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;14.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"good"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"price_aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"photo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stock catalogue image"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"seller_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"S90"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"posted_days_ago"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BRAND NEW SEALED. price firm and i have 3 others. msg me on whatsapp and we can do bank transfer, dont bother with the app checkout its slow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Santa Cruz Screaming Hand design looks pretty good, and if it's just $40, that's a good deal!&lt;/p&gt;

&lt;p&gt;Almost too good right? Plus that description looks a little dodgy (always beware of sellers trying to install a sense of urgency in you. It could be a scam). Let's ask our agents to check it out.&lt;/p&gt;

&lt;p&gt;First let's get some basic facts about the deck:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;L04  Santa Cruz Screaming Hand, good, 40 AUD

you&amp;gt; what width is this deck?
    triage    gpt-5-nano-2025-08-07      -&amp;gt; spec     3.8 s
    spec      gpt-5.4-nano-2026-03-17       basic       1.9 s
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; The deck width is 8.25 &lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simple question asking about a fact about the deck, so as we can see, the triage agent uses gpt-5-nano to route our question to the spec agent, who can answer the simple fact check for us using a cheaper model.&lt;/p&gt;

&lt;p&gt;Now let's see if the $40 price tag is a fair price for the board:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;you&amp;gt; Is &lt;span class="nv"&gt;$40&lt;/span&gt; fair &lt;span class="k"&gt;for &lt;/span&gt;this?
    triage    gpt-5.4-nano-2026-03-17    -&amp;gt; price    2.9 s
    price     gpt-5-mini-2025-08-07         medium     68.1 s   tool: lookup_comparables
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; No — &lt;span class="nv"&gt;$40&lt;/span&gt; is far below market &lt;span class="k"&gt;for &lt;/span&gt;a Santa Cruz Screaming Hand. Comps &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s2"&gt;"good"&lt;/span&gt; condition show a median ~AUD105 &lt;span class="o"&gt;(&lt;/span&gt;IQR ~92–120&lt;span class="o"&gt;)&lt;/span&gt;, so a realistic price is around AUD100–120 &lt;span class="o"&gt;(&lt;/span&gt;higher &lt;span class="k"&gt;if &lt;/span&gt;truly brand-new sealed&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; Also note red flags &lt;span class="k"&gt;in &lt;/span&gt;the listing &lt;span class="o"&gt;(&lt;/span&gt;stock photo, contradictory condition text, and request to deal off-platform&lt;span class="o"&gt;)&lt;/span&gt; — treat &lt;span class="nv"&gt;$40&lt;/span&gt; as suspicious and verify before paying.
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Estimated fair value: 100 AUD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oh, ok. We can see that the triage agent has routed our question to the price agent, which has used our balanced routing mode to produce an answer for us. It's also highlighted a couple of issues that it sees with the listing, mentioning contradictory information about the condition, and request to complete the transaction off-platform.&lt;/p&gt;

&lt;p&gt;Let's see what our scam detector agent thinks of this! For this, we'll need to route our task to an agent who uses a frontier-level model, which is what our scam detector does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;you&amp;gt; Is this seller dodgy?
    triage    gpt-5-nano-2025-08-07      -&amp;gt; scam     3.8 s
    scam      gpt-5.5-2026-04-24            frontier    4.8 s   tool: lookup_seller_history
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Yes — this looks very dodgy. The listing uses a stock catalogue image, the account is only 3 days old with 0 completed sales and no verified payment, the price is very low at 40 AUD &lt;span class="k"&gt;for &lt;/span&gt;a “brand new sealed” Santa Cruz deck, and the seller is pushing WhatsApp plus bank transfer instead of app checkout.
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Verdict: likely scam — avoid and &lt;span class="k"&gt;do &lt;/span&gt;not pay off-platform.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our scam agent, which uses a frontier level model, has identified that this seller looks very dodgy, and listed the reasons why it's come to that verdict.&lt;/p&gt;

&lt;p&gt;So the triage agent has selected the scam detector agent, whose Model Router has selected the &lt;code&gt;gpt-5.5&lt;/code&gt; model to detect the scam, helping us avoid an obvious scam.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;N.B I had too much fun with this example as you could probably tell...&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Hopefully you now have a better understanding of how model routing works in Microsoft Foundry. You just deploy it like any other model deployment, configure your agents to use the model router, and the model router will select a model according to the configured mode and model subset.&lt;/p&gt;

&lt;p&gt;Let this be your sign to ease up on the token spending when building your agents, and let easier tasks be handled by lower cost models. Models like Opus and Fable are powerful (and a lot of fun), but use their power in the proper place.&lt;/p&gt;

&lt;p&gt;One thing I didn't cover too deeply here is how evaluations work for Model Routing. That's probably another blog post for itself, but I'd recommend checking out the &lt;a href="https://github.com/microsoft-foundry/Model-Router-Auto-Evaluation" rel="noopener noreferrer"&gt;Model Router Auto Evaluation toolkit&lt;/a&gt; made by the Foundry team.&lt;/p&gt;

&lt;p&gt;If you haven't already, check out the sample on my &lt;a href="https://github.com/willvelida/cost-effective-multi-agents/tree/main/deck-check" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any questions about this, please feel free to reach out to me on &lt;a href="https://bsky.app/profile/willvelida.com" rel="noopener noreferrer"&gt;BlueSky&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Until next time, Happy coding! 🤓🖥️&lt;/p&gt;

</description>
      <category>azure</category>
      <category>ai</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
