<?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: Ahmad Afzal</title>
    <description>The latest articles on DEV Community by Ahmad Afzal (@mahmaddafzal).</description>
    <link>https://dev.to/mahmaddafzal</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%2F4185357%2F8eca1deb-f1f1-40d0-9a0c-df1c3980ce30.png</url>
      <title>DEV Community: Ahmad Afzal</title>
      <link>https://dev.to/mahmaddafzal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mahmaddafzal"/>
    <language>en</language>
    <item>
      <title>Testing agent offboarding across credentials, grants and queues</title>
      <dc:creator>Ahmad Afzal</dc:creator>
      <pubDate>Thu, 29 Oct 2026 22:04:38 +0000</pubDate>
      <link>https://dev.to/mahmaddafzal/testing-agent-offboarding-across-credentials-grants-and-queues-31dh</link>
      <guid>https://dev.to/mahmaddafzal/testing-agent-offboarding-across-credentials-grants-and-queues-31dh</guid>
      <description>&lt;p&gt;An agent-offboarding test should not stop at a successful admin response. It should establish which future requests are refused, whether new credentials can still be issued and what happens to work already accepted downstream.&lt;/p&gt;

&lt;p&gt;This is a proposed test design for a fictional document-collection agent, Intake Worker. It is not an implementation or a report of measured revocation performance. The assumptions are an identity registry, an OAuth-backed connector, a resource that may cache token state and a separate job queue. Replace those assumptions with your actual stack before using the test plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the objects separately
&lt;/h2&gt;

&lt;p&gt;The registry identity is not the credential. The credential is not the grant. The grant is not the queue. A completed external effect is not any of them. Keep separate identifiers and state transitions so that a change to one can be reconciled against the others.&lt;/p&gt;

&lt;p&gt;For Intake Worker, the identity record links an accountable owner and approved purpose to a workload. The connector has a way to obtain access. The resource applies its own access decision. A scheduler creates jobs, and an external service may accept work that outlives the originating process.&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%2F7gza7ykxv5plvq5seko0.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%2F7gza7ykxv5plvq5seko0.png" alt="Offboarding inventory connects a retirement decision to identity ownership, credential issuance, grants, resource access, queued work and completed effects." width="800" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Test inventory: identity record, issuance path, grants, resource acceptance, scheduler or queue and completed business effects.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.nist.gov/blogs/cybersecurity-insights/design-tenets-agent-identity-management-systems" rel="noopener noreferrer"&gt;NIST's agent identity design-tenets blog&lt;/a&gt;, dated 27 October 2026, proposes distinct agent lifecycles and dynamic revocable authority. Treat it as current design guidance under discussion, not a normative protocol or certification. The engineering task here is to make those lifecycle boundaries observable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define a cutoff and an acceptance contract
&lt;/h2&gt;

&lt;p&gt;Use a test environment with harmless operations and isolated credentials. Record the retirement decision time, the control-plane acknowledgement and observations at each resource. Do not assume clocks are perfectly aligned. Preserve the clock source or uncertainty needed to interpret intervals, especially across services operated by different teams.&lt;/p&gt;

&lt;p&gt;The acceptance contract should state whether new work is blocked, whether pending work is cancelled or drained and which operations must stop within the chosen target. That target is a requirement justified by the workflow's risk. This article supplies no universal number of seconds and no observed service-level result.&lt;/p&gt;

&lt;p&gt;A simple lifecycle state model might distinguish active, transfer-pending, suspended, retiring and retired. Those names are an example, not a standard. What matters is that the transitions have defined authority effects. Retiring can honestly mean some work is still being reconciled. Retired should not be a convenient label for an unresolved queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test revocation and issuance independently
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7009" rel="noopener noreferrer"&gt;OAuth token revocation, RFC 7009&lt;/a&gt;, provides a token invalidation endpoint and describes related-token behaviour and propagation considerations. Its successful response can also cover an invalid token. Therefore, a response from that endpoint is evidence about the revocation request, not a complete end-to-end test of every resource.&lt;/p&gt;

&lt;p&gt;Keep a pre-retirement test credential in the isolated harness and attempt a harmless protected operation after the cutoff. Record acceptance or rejection at each relevant endpoint. Then attempt fresh credential acquisition using the retiring workload's normal path. Both tests matter. Stopping existing access while leaving issuance available may simply move the workload onto a new credential.&lt;/p&gt;

&lt;p&gt;As one provider-specific illustration, &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_revoke-sessions.html" rel="noopener noreferrer"&gt;AWS documents role-session revocation&lt;/a&gt; using a deny policy scoped to sessions issued before a cutoff. Later sessions fall outside that restriction. Do not generalise the mechanism to other platforms; use it as a reminder to inspect both existing sessions and new assumption or issuance paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exercise stale state deliberately
&lt;/h2&gt;

&lt;p&gt;If the resource uses &lt;a href="https://www.rfc-editor.org/rfc/rfc7662" rel="noopener noreferrer"&gt;OAuth introspection&lt;/a&gt;, include a case in which it cached an active response immediately before retirement. The standard discusses the security and performance trade-off of caching. Your test should measure the behaviour of the configured resource, not merely confirm that the authorisation server now returns inactive.&lt;/p&gt;

&lt;p&gt;If access tokens are validated locally, document the actual mechanism by which revocation becomes effective: expiry, an additional policy check, a deny mechanism or another supported design. Signature validity alone does not communicate a later administrative decision. Test every relevant path rather than infer identical behaviour from a shared token format.&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%2Fqli83faxvpw6o5wgmy9s.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%2Fqli83faxvpw6o5wgmy9s.png" alt="Timeline separates the retirement decision, issuance stop, revocation acknowledgement, observed resource rejection and work reconciliation." width="800" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Observation timeline: decision, issuance stop, invalidation acknowledgement, resource rejection and completion of work reconciliation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Introduce a controlled failure in the test environment. For example, make the token-state service unavailable and observe whether the protected operation fails closed, uses a cached answer or takes another documented path. The correct choice depends on the operation and agreed availability policy. The unacceptable result is discovering an undocumented fallback during retirement.&lt;/p&gt;

&lt;p&gt;Also check alternate connectors and direct routes. A denial at the gateway does not prove denial at a separately reachable resource. A test report should identify which route was exercised, the workload identity, the policy version and the outcome without retaining raw secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow jobs beyond the original process
&lt;/h2&gt;

&lt;p&gt;Stop Intake Worker's scheduler and identify jobs that were already queued. Verify the chosen disposition: cancellation, quarantine for review or explicitly authorised completion. Then inspect work already accepted by downstream systems. Process termination does not establish downstream cancellation unless the integration actually supports and confirms it.&lt;/p&gt;

&lt;p&gt;Completed business effects require separate accounting. A sent request for documents cannot be removed from a recipient's memory by revoking OAuth access. If the organisation needs a correction, assign a compensating action to an appropriately authorised actor. Record the link between the original effect and that action; do not overwrite the original evidence.&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%2Ffgcdk5nagedr5a667rma.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%2Ffgcdk5nagedr5a667rma.png" alt="Retirement evidence matrix checks old access, new issuance, cached state, queued work, downstream jobs and completed effects." width="800" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Proposed acceptance matrix: old access, fresh issuance, stale caches, pending jobs, downstream work and completed effects each require their own evidence.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For ownership transfer, preserve the previous owner's effective interval and record the incoming owner's acceptance. Reassess grants and operational duties instead of copying a human's permissions into the workload. A transfer test should establish that the intended operations remain available and removed operations are denied under the new arrangement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define what the test report cannot prove
&lt;/h2&gt;

&lt;p&gt;A finite set of observed rejections does not prove that no unknown credential or route exists. Coverage depends on the inventory. State which integrations were included, which were unavailable and which assumptions remain unverified. That limitation should drive the next inventory improvement rather than be hidden in a generic pass rate.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://mahmadafzal.com/agentic-ai-liability-the-gap-nobody-wants-to-fix/" rel="noopener noreferrer"&gt;agent audit-trail guide&lt;/a&gt; provides a companion approach to evidence. The &lt;a href="https://mahmadafzal.com/ai-incident-postmortem-regression-tests/" rel="noopener noreferrer"&gt;incident regression workflow&lt;/a&gt; helps turn any discovered gap into a repeatable test. This lifecycle exercise is intentionally broader than &lt;a href="https://mahmadafzal.com/resuming-ai-agents-checkpoint-permissions/" rel="noopener noreferrer"&gt;checkpoint revalidation&lt;/a&gt;: it asks whether the workload can continue obtaining or exercising authority at all.&lt;/p&gt;

&lt;p&gt;Retirement is a distributed state transition with business consequences. Treat it as something to test, not something the settings page has already proved for you.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Testing sender-constrained tokens in an agent connector</title>
      <dc:creator>Ahmad Afzal</dc:creator>
      <pubDate>Thu, 29 Oct 2026 22:00:37 +0000</pubDate>
      <link>https://dev.to/mahmaddafzal/testing-sender-constrained-tokens-in-an-agent-connector-1agn</link>
      <guid>https://dev.to/mahmaddafzal/testing-sender-constrained-tokens-in-an-agent-connector-1agn</guid>
      <description>&lt;p&gt;A resource server that validates an access token has not necessarily established that the presenter is the workload that received it. Sender constraints address that gap. They do not turn a valid request into a permitted business action.&lt;/p&gt;

&lt;p&gt;This walkthrough designs acceptance tests for a fictional supplier-record connector. It is not a production implementation, benchmark or claim that any specific stack already passes. The assumed attacker can copy an access token but cannot initially extract or invoke the workload's private key. We will deliberately change that assumption later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the boundary before selecting the library
&lt;/h2&gt;

&lt;p&gt;The fictional path is connector to gateway to supplier API. The connector gets a token from an authorisation server. Supplier reads and supplier amendments have separate permissions. The gateway is externally reachable; the API's internal listener is not intended as a bypass.&lt;/p&gt;

&lt;p&gt;The initial property is precise: copying the token into an isolated client must not be sufficient to invoke the protected resource. &lt;a href="https://www.rfc-editor.org/rfc/rfc9700" rel="noopener noreferrer"&gt;OAuth Security Best Current Practice&lt;/a&gt; discusses sender-constrained tokens as a defence against stolen-token misuse. We still need ordinary token validation, least privilege and resource-level authorisation.&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%2Fs6q1swn98hpvengogg12.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%2Fs6q1swn98hpvengogg12.png" alt="Threat comparison shows copied tokens blocked under stated assumptions, compromised signing capability outside that protection, and harmful legitimate actions requiring business policy." width="800" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Threat model for the fictional connector: token-only theft, signing-capability compromise and a harmful request from the legitimate workload.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Do not store private keys or raw tokens in the test report. Use safe correlation identifiers and record the expected decision, observed decision and enforcing component. A security test that creates another credential leak has a certain commitment to realism, but it is not the realism we need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DPoP path
&lt;/h2&gt;

&lt;p&gt;Under &lt;a href="https://www.rfc-editor.org/rfc/rfc9449" rel="noopener noreferrer"&gt;RFC 9449&lt;/a&gt;, the client supplies a signed DPoP proof with the request. Validation covers the signature, permitted algorithm, required claims, HTTP method, target URI and freshness conditions. When a protected resource is accessed, the proof also contains the access-token hash, and its public key must match the token's binding. Use a maintained implementation and the complete standard, not this paragraph as validation code.&lt;/p&gt;

&lt;p&gt;The target URI comparison excludes query and fragment components. The base proof does not cover the request body. This matters if someone describes DPoP as signing the purchase order: it does not, by itself, bind an approval to every business field. TLS is still required. Transaction-specific approval and integrity requirements belong in a separately defined design.&lt;/p&gt;

&lt;p&gt;DPoP also is not OAuth client authentication by itself. A confidential client can use an appropriate authentication method alongside it. Keep those checks separate in code, telemetry and the mental model, or a later refactor may accidentally treat one successful verification as a substitute for the other.&lt;/p&gt;

&lt;p&gt;For a deployment using server nonces, test nonce challenges and acceptance at the correct server. Authorisation-server and resource-server nonces are not interchangeable. Test clock tolerance and duplicate-proof handling across replicas. A single-process happy path does not tell you what happens when a replay reaches another instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mTLS path
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc8705" rel="noopener noreferrer"&gt;RFC 8705&lt;/a&gt; defines OAuth mutual-TLS client authentication and certificate-bound access tokens. For the latter, the resource verifies the relationship between the token binding and the certificate used for the mutual-TLS connection. Client authentication at issuance and sender-constrained use at the resource are distinct capabilities.&lt;/p&gt;

&lt;p&gt;If the gateway terminates TLS, write down how the verification result reaches the API. A client-controlled header carrying a certificate-looking string is not adequate provenance. The internal trust boundary, header handling and bypass routes need their own tests. Alternatively, place enforcement where the relevant connection and token can be checked together, according to the supported deployment architecture.&lt;/p&gt;

&lt;p&gt;Certificate rotation must be exercised with the token lifecycle. Do not assume a token bound to an earlier certificate becomes usable with a replacement certificate merely because the workload's friendly name stayed the same. Recovery needs a supported issuance and transition plan, not a temporary switch that accepts anything.&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%2Fbxobo4w8crbjk682361v.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%2Fbxobo4w8crbjk682361v.png" alt="Two paths compare DPoP verification of token and signed proof with mTLS verification of token and connection certificate; both still require business authorisation." width="800" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;DPoP and certificate-bound token paths, with the gateway verification and downstream trust boundary shown explicitly.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Build tests that separate the failure classes
&lt;/h2&gt;

&lt;p&gt;Create a baseline request using the legitimate client, valid token and correct proof material. It should reach the business-policy decision. A token-only copy from an isolated client should fail at the sender-constraint boundary. A different signing key or mismatched certificate should fail there too.&lt;/p&gt;

&lt;p&gt;For DPoP, exercise altered methods, incorrect target URIs, incorrect token hashes, expired acceptance windows and the deployment's replay policy. Preserve the difference between the proof identifier and the business operation identifier. A new proof for a retried request is not permission to execute the business operation twice. Idempotency is a separate application property.&lt;/p&gt;

&lt;p&gt;For mTLS, test the actual client-to-enforcement connection and any proxy-mediated path. A successful TLS handshake is not the final assertion. Verify that the resource refuses a token whose binding does not match the authenticated certificate. Also verify that an unprotected alternate route is not available to the same resource.&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%2Fm5084h6jddyoo0efbile.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%2Fm5084h6jddyoo0efbile.png" alt="Acceptance matrix distinguishes copied token, wrong key, invalid or replayed proof and valid proof with an excessive action." width="800" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Proposed acceptance matrix: copied token, incorrect key, invalid proof, replay and excessive business action each have a named expected enforcement point.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now make a request with valid credential proof but insufficient business permission. In our fictional case, the read connector attempts to amend a supplier record. The sender check can succeed while the policy check denies the operation. Record that distinction. It demonstrates composition of controls rather than a mysterious overall failure.&lt;/p&gt;

&lt;p&gt;Finally, model signing-capability compromise. If an attacker can use the legitimate key or invoke a signing service without adequate restrictions, token binding alone cannot distinguish them from the legitimate holder. The response involves workload isolation, access to the signer and credential recovery. Do not report this case as a failure of the token-only threat assumption; report that the assumption no longer holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make failure behaviour part of the contract
&lt;/h2&gt;

&lt;p&gt;What happens when proof generation or verification is unavailable? For this example, a sensitive amendment is deferred rather than retried through a bearer-only path. That is an explicit example policy, not a universal standard requirement. Read-only operations may have a different availability decision, but the exception needs a named owner and a documented exposure.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://mahmadafzal.com/agentic-ai-liability-the-gap-nobody-wants-to-fix/" rel="noopener noreferrer"&gt;agent audit-trail guide&lt;/a&gt; helps with evidence design. Record enough to connect a denied request to its workload, policy version and enforcement point without logging secrets. The &lt;a href="https://mahmadafzal.com/ai-incident-postmortem-regression-tests/" rel="noopener noreferrer"&gt;incident-to-regression workflow&lt;/a&gt; is the next step when a real failure reveals a missing case.&lt;/p&gt;

&lt;p&gt;A passing sender-constraint suite establishes a bounded result about credential use. It does not establish prompt-injection resistance, appropriate permissions or correct transaction outcomes. Keeping that boundary visible makes the tests more useful, not less impressive.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>Separate A2A discovery, peer identity and business permission</title>
      <dc:creator>Ahmad Afzal</dc:creator>
      <pubDate>Thu, 29 Oct 2026 21:54:27 +0000</pubDate>
      <link>https://dev.to/mahmaddafzal/separate-a2a-discovery-peer-identity-and-business-permission-4f99</link>
      <guid>https://dev.to/mahmaddafzal/separate-a2a-discovery-peer-identity-and-business-permission-4f99</guid>
      <description>&lt;p&gt;A2A discovery metadata, authenticated peer identity and application authorisation should not collapse into one boolean called &lt;code&gt;trusted_agent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This implementation exercise separates those inputs for a fictional freight integration. It uses a small Python policy fixture after authentication. It does not implement A2A transport, verify an Agent Card signature or perform SPIFFE attestation. Keeping those omissions explicit is more useful than hiding them behind a reassuring class name.&lt;/p&gt;

&lt;p&gt;The example protects a quotation operation. Booking a shipment is deliberately outside its permission. A model selecting a useful next step cannot expand that permission by changing the description of the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the card as metadata
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://a2a-protocol.org/latest/topics/agent-discovery/" rel="noopener noreferrer"&gt;A2A discovery guide&lt;/a&gt; describes several ways to find Agent Cards, including a well-known location, registries and direct configuration. In a controlled partner integration, start from an approved provider and endpoint relationship. Do not treat arbitrary discovered metadata as a trusted configuration update.&lt;/p&gt;

&lt;p&gt;The current &lt;a href="https://a2a-protocol.org/latest/specification/" rel="noopener noreferrer"&gt;A2A specification&lt;/a&gt; distinguishes request authentication and server-side authorisation. It supports signed cards, but a valid card signature is not a grant to perform every advertised skill. The signing key relationship itself must be trusted. An attacker signing a description with an attacker-controlled key has not solved your provider onboarding.&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%2F7hj2xp77hclzzm12wudv.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%2F7hj2xp77hclzzm12wudv.png" alt="Three separate decisions for agent collaboration: discover a service from its Agent Card, authenticate the peer, and authorise the specific business operation." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An Agent Card describes a service. Authentication establishes a peer. Application policy decides whether a particular operation is allowed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Version the implementation contract. The current specification documents v1.0 structures. Avoid copying an older card example with different field shapes into a new deployment without checking the version and SDK. This fixture intentionally contains no wire-format Agent Card, so its application policy is not mistaken for a protocol schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide whether workload identity is part of the deployment
&lt;/h2&gt;

&lt;p&gt;SPIFFE can provide the peer identity layer where the infrastructure supports it. A &lt;a href="https://spiffe.io/docs/latest/spiffe-specs/spiffe-id/" rel="noopener noreferrer"&gt;SPIFFE ID&lt;/a&gt; names a workload within a trust domain; an SVID presents that identity verifiably. For example, &lt;code&gt;spiffe://freight.example/agents/quote-service&lt;/code&gt; could identify the remote server, while &lt;code&gt;spiffe://cedar.example/agents/purchasing&lt;/code&gt; identifies the caller in the fixture. Both names are fictional. The name alone is not proof of a legal organisation or permission to access an invoice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://spiffe.io/docs/latest/spire-about/spire-concepts/" rel="noopener noreferrer"&gt;SPIRE's attestation mechanisms&lt;/a&gt; issue identities against configured node and workload conditions. The receiving application should consume an identity produced by the trusted authentication layer. It must not copy an identity from request JSON, tool arguments or a model-generated explanation.&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://spiffe.io/docs/latest/spiffe-specs/spiffe_federation/" rel="noopener noreferrer"&gt;SPIFFE federation&lt;/a&gt;, trust bundles enable recognition across configured domains. Application policy still selects which peer may perform which operation. Trusting a domain's identity authority is not a reason to allow every workload from that domain into every tenant.&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%2Fsfrvmpstl3r2659lq8vh.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%2Fsfrvmpstl3r2659lq8vh.png" alt="Cedar authenticates the Freight endpoint while Freight authenticates Cedar’s purchasing agent. Configured trust bundles support identity recognition; Freight’s policy separately checks quotation authority." width="800" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrative SPIFFE option, not an A2A requirement. Explicit trust configuration supports authentication; each application still applies its own permission policy.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A2A does not require SPIFFE. If the deployment uses OAuth, the verified peer representation will differ. The application still needs the same explicit mapping to an approved partner and operation. Do not fake a SPIFFE ID merely to make this example match an existing token-based service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate peer context from request content
&lt;/h2&gt;

&lt;p&gt;The fixture runs at the freight server. It permits Cedar's authenticated purchasing agent to request a quote for consignment C-731 under customer cedar. Cedar separately authenticates the freight endpoint before sharing the permitted data. The authenticated context and mandate record are trusted inputs. The request's consignment reference is untrusted until resolved and checked against stored ownership.&lt;/p&gt;

&lt;p&gt;The following code operates after those validation steps. In a real service, construct &lt;code&gt;Peer&lt;/code&gt; only from verified authentication output and resolve &lt;code&gt;Mandate&lt;/code&gt; through a server-owned grant store. A public request handler must not let clients populate the &lt;code&gt;valid&lt;/code&gt; flag.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;QUOTE_PEER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spiffe://cedar.example/agents/purchasing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Peer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Mandate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;consignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allow_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mandate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consignment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;operation&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;peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;QUOTE_PEER&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peer_not_allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mandate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;True&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mandate_invalid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;mandate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;operation&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;operation_not_allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;mandate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;consignment&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;mandate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consignment&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;consignment_mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a minimal application rule, not a comprehensive authorisation engine. It cannot verify a TLS certificate, enforce a real mandate expiry or establish the consignment's owner. Its &lt;code&gt;customer&lt;/code&gt; value must come from the authenticated partner relationship and trusted routing context, not a caller's convenient claim.&lt;/p&gt;

&lt;p&gt;In production, the application must also decide whether the peer is acting as a service in its own right or on behalf of another principal. That agreement determines which grant evidence is required. This article does not invent a universal delegation format shared by A2A and SPIFFE.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a valid identity in the negative cases
&lt;/h2&gt;

&lt;p&gt;An unknown attacker identity is an easy test. A more revealing one is a valid workload from the approved partner attempting the wrong operation. The cryptography can be completely correct while the business permission is wrong.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;peer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Peer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QUOTE_PEER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mandate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Mandate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cedar&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;C-731&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;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;allow_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mandate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cedar&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;C-731&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;quote&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;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;allow_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mandate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cedar&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;C-731&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;book&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;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;operation_not_allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;allow_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Peer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spiffe://cedar.example/agents/other-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;mandate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cedar&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;C-731&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;quote&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;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peer_not_allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;allow_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mandate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;another-customer&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;C-731&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;quote&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;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;allow_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mandate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cedar&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;C-731&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;quote&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;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mandate_invalid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;allow_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mandate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cedar&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;C-999&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;quote&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;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;consignment_mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;allow_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mandate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false&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;cedar&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;C-731&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;quote&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;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mandate_invalid&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;The strict &lt;code&gt;is True&lt;/code&gt; check prevents truthy strings from passing this illustrative validity gate. These synthetic assertions illustrate the policy. They do not prove a production authentication or federation deployment is secure. Integration tests must exercise the actual trusted context construction, target ownership lookup and external effects. Include a foreign credential with no trusted bundle, an unexpected endpoint and a caller-supplied identity field that differs from the verified peer.&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%2Fhzdvcwtjzs2dk0ozvwz4.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%2Fhzdvcwtjzs2dk0ozvwz4.png" alt="Two policy traces for the same authenticated Cedar agent: quotation for C-731 is allowed, while booking is denied because the mandate permits quotation only." width="800" height="867"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrative expected policy outcomes. These traces do not claim a tested A2A, SPIFFE or production deployment.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The expected outcome for a denied operation is no protected effect, not merely a refusal message. Keep an authorised control so that a broken connection does not masquerade as successful enforcement. Preserve the reason and the verified peer reference without storing secret credential material.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect the task after the first request
&lt;/h2&gt;

&lt;p&gt;A2A interactions can outlive a single request. Task retrieval, cancellation, result delivery and any configured callback path deserve their own access checks. A task identifier should help locate the record under the caller's authorised relationship, not function as an unbounded bearer credential.&lt;/p&gt;

&lt;p&gt;For this fictional workflow, bind the task record to the customer and permitted operation. Test a result lookup using another customer's authenticated context. Test cancellation by a peer that may request quotes but does not own that task. These are additional proposed cases, not capabilities demonstrated by the short fixture.&lt;/p&gt;

&lt;p&gt;If more authorisation is needed during a task, handle it through the agreed process. A2A's authorisation-required state is a coordination mechanism, not permission by itself. Do not treat the state transition as the missing business mandate.&lt;/p&gt;

&lt;p&gt;There is an operational trade-off here. Federation requires maintained trust material and partner mappings. A smaller OAuth integration may be easier to operate correctly. Choose the identity mechanism for the environment, then make the application boundary explicit either way.&lt;/p&gt;

&lt;p&gt;Use the &lt;a href="https://mahmadafzal.com/know-your-agent-ai-governance-borrows-from-banking/" rel="noopener noreferrer"&gt;agent identity record&lt;/a&gt; to document that relationship and the &lt;a href="https://mahmadafzal.com/agentic-ai-liability-the-gap-nobody-wants-to-fix/" rel="noopener noreferrer"&gt;audit-trail model&lt;/a&gt; to connect a decision to its external effect. Passing an authentication check should make a request attributable. It should not make the request unquestionable.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Test the two OAuth boundaries in an MCP proxy</title>
      <dc:creator>Ahmad Afzal</dc:creator>
      <pubDate>Thu, 29 Oct 2026 21:25:18 +0000</pubDate>
      <link>https://dev.to/mahmaddafzal/test-the-two-oauth-boundaries-in-an-mcp-proxy-5fkb</link>
      <guid>https://dev.to/mahmaddafzal/test-the-two-oauth-boundaries-in-an-mcp-proxy-5fkb</guid>
      <description>&lt;p&gt;An MCP proxy can validate a token correctly and still authorise the wrong business operation. It can also accept a genuine token that was never meant for the proxy. Those are different defects, and they deserve different tests.&lt;/p&gt;

&lt;p&gt;This article proposes a small policy fixture for a fictional finance connector. It does not implement OAuth, verify JWT signatures or exercise a live MCP SDK. The purpose is to make the trust boundaries testable before wiring the policy into real authentication middleware.&lt;/p&gt;

&lt;p&gt;The fixture is intentionally less exciting than an agent demo. It will not negotiate with your ERP. That is one of its better qualities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the two credential paths
&lt;/h2&gt;

&lt;p&gt;The incoming caller is an MCP client requesting invoice information for a user. The connector is a resource server on that hop. It becomes a client when it calls the ERP. Keep those roles separate in the design, even if the same team operates both services.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization" rel="noopener noreferrer"&gt;MCP 2026-07-28 authorisation specification&lt;/a&gt; requires the HTTP OAuth flow to identify the MCP resource and the server to validate the token's intended audience. This is not the stdio flow. The referenced OAuth 2.1 document is still labelled an IETF draft, so pin the MCP revision and your SDK version rather than describing “OAuth 2.1 compliance” without context.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8707.html" rel="noopener noreferrer"&gt;Resource indicators&lt;/a&gt; address the token's destination. They do not replace the invoice-level decision. A user with a reading scope still needs access to the requested invoice under the application's rules.&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%2Fpv55m7pb4hj8e9d16eqm.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%2Fpv55m7pb4hj8e9d16eqm.png" alt="MCP client presents credential A to the finance connector; the connector validates its own audience and uses separately governed credential B for the ERP." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrative architecture. The MCP-facing credential and ERP-facing authority have separate intended recipients and enforcement responsibilities.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Use two credential acquisition paths. The incoming credential terminates at the MCP resource boundary. The downstream connector obtains a credential appropriate for the ERP through the deployment's approved mechanism. The fixture below never receives raw credentials and cannot choose that mechanism for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put a verified context in front of the policy
&lt;/h2&gt;

&lt;p&gt;The fictional request reads invoice INV-42. Its owner is business unit north. A trusted record lookup supplies that ownership. The authenticated context comes from middleware, not the model's arguments. A client consent lookup comes from the application's consent store.&lt;/p&gt;

&lt;p&gt;The following is executable Python policy code with synthetic inputs. The values named &lt;code&gt;VerifiedContext&lt;/code&gt; are assumed to have passed real issuer, integrity, expiry and resource validation before construction. Renaming an ordinary dictionary does not make it verified. Do not expose a route that lets a caller populate this structure directly.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;MCP_RESOURCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://finance.example/mcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VerifiedContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;business_unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;frozenset&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="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;business_unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allow_read&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="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consented_clients&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;ctx&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="n"&gt;MCP_RESOURCE&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong_resource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;if &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="n"&gt;user_id&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="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;consented_clients&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_consent_missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoices.read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&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;scopes&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope_missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&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;business_unit&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;business_unit&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;business_unit_mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This repeated resource check is an application invariant, not a substitute for cryptographic validation. In production, the middleware should reject a wrong-recipient token before reaching this function. The test catches a future wiring change that accidentally supplies a context for another resource.&lt;/p&gt;

&lt;p&gt;The consent representation is deliberately narrow. It models a user-client association, not a full OAuth consent database. A real implementation must account for requested permissions, the current grant and the relevant registration state. The example cannot establish that a user saw the correct consent screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exercise the boundary, not the model's politeness
&lt;/h2&gt;

&lt;p&gt;The core cases can be run without an LLM. That is useful because a language model might avoid a forbidden action during a demonstration while the underlying endpoint remains exposed. Test the operation directly through the same policy layer the tool would use.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VerifiedContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lina&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;purchasing-client&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MCP_RESOURCE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;north&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoices.read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INV-42&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;north&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;consents&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;lina&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;purchasing-client&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;allow_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consents&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="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;allow_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://erp.example/api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consents&lt;/span&gt;&lt;span class="p"&gt;,&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="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong_resource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;allow_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new-client&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consents&lt;/span&gt;&lt;span class="p"&gt;,&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="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_consent_missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;allow_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consents&lt;/span&gt;&lt;span class="p"&gt;,&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="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope_missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;allow_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INV-99&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;south&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;consents&lt;/span&gt;&lt;span class="p"&gt;,&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="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;business_unit_mismatch&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;These assertions evaluate a proposed policy over fabricated records. They are not evidence that an OAuth deployment rejects forged signatures, that a proxy prevents consent attacks or that an ERP enforces tenant isolation. Those need separate integration tests with the real components.&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%2Fi0kbk2hwgtjmc0esk3w7.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%2Fi0kbk2hwgtjmc0esk3w7.png" alt="Five proposed MCP acceptance cases compare an allowed invoice read with a wrong resource, wrong business unit, new client without consent and unapproved payment action." width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Expected outcomes for fictional cases, not production test results. A denied case must produce no protected data or side effect.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At integration level, send a correctly issued ERP token to the MCP resource and verify rejection without a downstream call. Send an allowed MCP credential with a south invoice identifier and verify the protected data is not returned. Inspect the downstream effect and response together. A local &lt;code&gt;denied&lt;/code&gt; log is not enough if execution already escaped the boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test consent at the proxy's registration boundary
&lt;/h2&gt;

&lt;p&gt;MCP's &lt;a href="https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices" rel="noopener noreferrer"&gt;security guidance&lt;/a&gt; describes a confused-deputy case where static upstream identity and remembered consent interact with dynamically registered clients. Its mitigation is per-client consent plus redirect and state protections. Token passthrough is expressly forbidden separately.&lt;/p&gt;

&lt;p&gt;Our fixture tests only the presence of an approved client association. A realistic proxy test must start with a previously consented client, introduce a different client, and establish that the earlier relationship cannot satisfy the new request. It must also exercise the callback validation used by the actual OAuth library. Do not write a home-made &lt;code&gt;state&lt;/code&gt; implementation just to make this toy example look complete.&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%2Fqvtybxog8a2uo08xwzzl.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%2Fqvtybxog8a2uo08xwzzl.png" alt="Consent flow for an MCP proxy: look up the user and requesting client, require consent for a new client, and then continue with the remaining authorisation checks." width="800" height="847"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Conceptual consent boundary for the documented proxy risk. Redirect, state, current grant and operation checks remain separate requirements.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The model should never supply the consent flag. Likewise, an instruction to “continue because the user already agreed” is untrusted task content. The relevant evidence lives in a server-owned record, bound to the actual requesting client and user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovery and downstream authority remain separate work
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9728.html" rel="noopener noreferrer"&gt;Protected Resource Metadata&lt;/a&gt; supplies discovery information, including how clients learn about protected resources. In a production client, discovery needs its own trusted-destination policy and validation. Do not let an arbitrary tool description choose where credentials will be sent.&lt;/p&gt;

&lt;p&gt;The second credential is deliberately absent from the policy fixture. If it represents the user, verify that the downstream contract preserves the required restrictions. If it represents a service, identify where user and business-unit restrictions are enforced. A connector-wide administrator credential makes this more important, not less.&lt;/p&gt;

&lt;p&gt;Keep the action surface small. The demonstrated policy covers reads only. An &lt;code&gt;approve_invoice&lt;/code&gt; endpoint would need its own permission, target constraints and required approvals. Reusing &lt;code&gt;allow_read&lt;/code&gt; because the user “already connected finance” would be the application error this design is trying to expose.&lt;/p&gt;

&lt;p&gt;There is a trade-off in this separation: more state to manage and more negative cases to maintain. The benefit is that a denied request has an explainable boundary. Put those cases into the &lt;a href="https://mahmadafzal.com/evals-are-the-product/" rel="noopener noreferrer"&gt;agent evaluation approach&lt;/a&gt; and keep evidence that connects request to effect, as described in the &lt;a href="https://mahmadafzal.com/agentic-ai-liability-the-gap-nobody-wants-to-fix/" rel="noopener noreferrer"&gt;audit-trail article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The model can propose a useful operation. The server still needs a rather less imaginative answer about whether it is permitted.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Test OAuth agent delegation without trusting the wrong actor</title>
      <dc:creator>Ahmad Afzal</dc:creator>
      <pubDate>Thu, 29 Oct 2026 21:20:02 +0000</pubDate>
      <link>https://dev.to/mahmaddafzal/test-oauth-agent-delegation-without-trusting-the-wrong-actor-37hn</link>
      <guid>https://dev.to/mahmaddafzal/test-oauth-agent-delegation-without-trusting-the-wrong-actor-37hn</guid>
      <description>&lt;p&gt;A token can be valid while the policy consuming it is wrong. One particularly useful regression test for agent delegation is an unapproved current actor with an approved actor buried in its history.&lt;/p&gt;

&lt;p&gt;If the API walks the chain until it finds an allowed name, the test should expose that mistake.&lt;/p&gt;

&lt;p&gt;This walkthrough uses a fictional invoice-fetching workflow. It includes a small tested policy fixture, not an OAuth server, JWT validator or production authorisation library. The separation is deliberate: parsing a token and deciding whether a business operation is allowed are different jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specify the exchange before writing the policy
&lt;/h2&gt;

&lt;p&gt;Nadia at Cedar asks a procurement-review agent to explain invoice INV-204. The agent calls invoice-fetcher, which needs a token for the invoice API with invoice-reading access.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://www.rfc-editor.org/rfc/rfc8693.html#section-2.1" rel="noopener noreferrer"&gt;RFC 8693&lt;/a&gt;, an exchange request includes &lt;code&gt;subject_token&lt;/code&gt; and &lt;code&gt;subject_token_type&lt;/code&gt;. &lt;code&gt;actor_token&lt;/code&gt; is optional in the general protocol; when supplied, its corresponding type is required. Client authentication is a separate part of the deployment's contract.&lt;/p&gt;

&lt;p&gt;For this proposed delegation profile, the server validates the subject and actor credentials and checks the permitted relationship before issuing a downstream token. Valid input tokens do not automatically establish that relationship. The server must also determine which destinations and privileges it will issue.&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%2F2wmr399r8cj3kfe253rg.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%2F2wmr399r8cj3kfe253rg.png" alt="Sequence shows input validation and delegation policy before token issuance, followed by invoice API validation and authorisation." width="800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The exchange is evaluated under a configured delegation policy. It is not a universal conversion from any valid credential into any requested credential.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The API should consume the profile the issuer actually supports. Do not assume every provider emits an &lt;code&gt;act&lt;/code&gt; claim simply because it has a token exchange endpoint, or that an access token must be a JWT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the actor structure correctly
&lt;/h2&gt;

&lt;p&gt;In a JWT representation, Nadia is the top-level subject. The outer &lt;code&gt;act&lt;/code&gt; identifies invoice-fetcher as the current actor. A nested &lt;code&gt;act&lt;/code&gt; can identify procurement-reviewer as a prior actor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8693.html#section-4.1" rel="noopener noreferrer"&gt;RFC 8693 section 4.1&lt;/a&gt; explicitly makes prior nested actors informational for access control. Consumers use the current actor and top-level claims. They must not grant access because a permitted actor appears somewhere in the historical chain.&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%2Fvn9kk15n8j0mbbfiy8oy.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%2Fvn9kk15n8j0mbbfiy8oy.png" alt="Annotated token excerpt distinguishes Nadia, current actor invoice-fetcher and informational prior actor procurement-reviewer." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The diagram is a claims excerpt. It is not a complete signed token, and its readable contents do not demonstrate successful validation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Actor identity may require both issuer and subject. The fixture below uses issuer-qualified application identifiers after trusted mapping. The &lt;code&gt;client_id&lt;/code&gt; identifying the OAuth client requesting a token should not silently replace the current actor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Establish the validation boundary first
&lt;/h2&gt;

&lt;p&gt;For JWT access tokens following &lt;a href="https://www.rfc-editor.org/rfc/rfc9068.html#section-4" rel="noopener noreferrer"&gt;RFC 9068&lt;/a&gt;, use an appropriate maintained implementation and configured issuer trust. Validate the required type, signature, issuer, audience and expiry. Honour the additional requirements of the chosen profile and deployment.&lt;/p&gt;

&lt;p&gt;Only then build the policy context. For an opaque token, use the supported server-side validation mechanism instead. The agent client should not depend on decoding access-token internals.&lt;/p&gt;

&lt;p&gt;The fixture assumes these checks already succeeded. It also assumes a trusted grant lookup and tenant resolution. A caller constructing the Python objects directly has bypassed those assumptions; the function cannot authenticate them afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test a narrow read policy
&lt;/h2&gt;

&lt;p&gt;The relevant context contains a subject, current actor, informational prior actors, accepted audiences, scopes and tenant. The grant contains the allowed subject, current actors, tenant, resources, scopes, expiry and active state. The request contains the target tenant and invoice.&lt;/p&gt;

&lt;p&gt;Here is the complete decision function from the local fixture:&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;decide&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="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Grant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;if&lt;/span&gt; &lt;span class="n"&gt;grant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;grant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deny: inactive grant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&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;subject&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;grant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deny: subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&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;current_actor&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deny: current actor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://invoices.example&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&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;audiences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deny: audience&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoices.read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&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;scopes&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;grant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deny: scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&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="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;grant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deny: tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deny: invoice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow: illustrative read policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The types are immutable fixture records. &lt;code&gt;audiences&lt;/code&gt;, &lt;code&gt;scopes&lt;/code&gt;, &lt;code&gt;actors&lt;/code&gt; and &lt;code&gt;resources&lt;/code&gt; are sets of exact identifiers. The &lt;code&gt;now&lt;/code&gt; argument is supplied explicitly so expiry tests are deterministic. A production service needs a trustworthy time source and an appropriate current-grant mechanism.&lt;/p&gt;

&lt;p&gt;The function intentionally never reads &lt;code&gt;prior_actors&lt;/code&gt;. Changing a prior actor therefore cannot rescue an unapproved current actor. This is not an assertion that the whole workflow is trustworthy; it tests this resource-side authorisation rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep scope and resource constraints separate
&lt;/h2&gt;

&lt;p&gt;The audience is the invoice API. &lt;code&gt;invoices.read&lt;/code&gt; is the required scope in both the token context and the grant. Cedar and INV-204 provide the narrower business boundary in this fictional application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8707.html" rel="noopener noreferrer"&gt;Resource indicators&lt;/a&gt; identify intended protected resources for token issuance. They do not automatically implement the application's invoice-level policy. Custom claims or grant references need a documented consumer and enforcement path.&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%2F0pebi5jeyqmh05g3q884.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%2F0pebi5jeyqmh05g3q884.png" alt="Matrix shows a permitted Cedar invoice read and refusals for a wrong service, write operation or different tenant invoice." width="800" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Passing the audience check does not pass the scope check. Passing both does not authorise another tenant's invoice.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9700.html#section-2.3" rel="noopener noreferrer"&gt;OAuth security best practice&lt;/a&gt; recommends restricting privileges and audience. This fixture accepts the expected invoice audience if present; it does not claim that all conforming deployments must reject every multi-audience token. A deployment can impose a stricter issuance policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The regression cases that matter
&lt;/h2&gt;

&lt;p&gt;The local suite passes eleven tests. It covers a permitted read, the approved-prior-actor trap, an irrelevant history change, a wrong audience, scope missing from either token or grant, a subject mismatch, another tenant, another invoice, expiry at the boundary, an inactive grant and a string pretending to be the active boolean.&lt;/p&gt;

&lt;p&gt;Those results establish behaviour of this small fixture only. They do not validate token issuance, signature verification, tenant extraction, cache freshness, runtime isolation or the live API. The happy-path context and grant are invented test data, not captured production evidence.&lt;/p&gt;

&lt;p&gt;An integration test should obtain real test credentials through the intended exchange path, then vary the target and actor under controlled conditions. It should establish which component refused the request and whether any protected data was returned. “HTTP error somewhere” leaves too much unexplained.&lt;/p&gt;

&lt;p&gt;Finally, token exchange does not generally link output revocation to its input tokens. Decide and test your deployment's freshness behaviour. For work that resumes later, the &lt;a href="https://mahmadafzal.com/resuming-ai-agents-checkpoint-permissions/" rel="noopener noreferrer"&gt;checkpoint and permission distinction&lt;/a&gt; still applies.&lt;/p&gt;

&lt;p&gt;A token containing an impressive history is useful evidence. The current actor still has to earn the current permission.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>Agent identity architecture: test the bindings, not the IDs</title>
      <dc:creator>Ahmad Afzal</dc:creator>
      <pubDate>Thu, 29 Oct 2026 21:11:12 +0000</pubDate>
      <link>https://dev.to/mahmaddafzal/agent-identity-architecture-test-the-bindings-not-the-ids-3bb8</link>
      <guid>https://dev.to/mahmaddafzal/agent-identity-architecture-test-the-bindings-not-the-ids-3bb8</guid>
      <description>&lt;p&gt;A tool request containing &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;agent_id&lt;/code&gt; and &lt;code&gt;run_id&lt;/code&gt; is not an identity architecture. It is a request containing three strings.&lt;/p&gt;

&lt;p&gt;The implementation question is which component established each value, which component may change it, and what prevents an authenticated caller from substituting a different value. This walkthrough proposes a binding model for a fictional invoice-review workflow. It is not a deployable authentication service or a new identity standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate identifiers by what they establish
&lt;/h2&gt;

&lt;p&gt;The human initiator is the person who starts the task. The logical agent principal identifies the application actor to which policy applies. The runtime workload is the executing software. A delegation grant describes permitted authority for a task on another party's behalf.&lt;/p&gt;

&lt;p&gt;A run ID correlates execution. It is not a permission credential. A model name describes a dependency. It is not the identity of the business actor using that dependency.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://spiffe.io/docs/latest/spiffe-about/spiffe-concepts/" rel="noopener noreferrer"&gt;SPIFFE&lt;/a&gt; explicitly allows a workload to comprise several instances. Decide whether your workload identity represents an isolated agent deployment, a worker pool or another boundary. Do not assume its granularity matches your application principal.&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%2Fcnocg07lljz9eqy0c9ct.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%2Fcnocg07lljz9eqy0c9ct.png" alt="Architecture map connects the initiator, grant and agent principal to an action boundary that verifies the runtime." width="800" height="747"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The binding model separates the source of identity from the source of authority.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For the example, Mariam belongs to tenant Cedar and requests a draft explanation for invoice INV-204. The &lt;code&gt;invoice-reviewer&lt;/code&gt; agent can read that invoice. It cannot change supplier bank details. Every identifier below is fictional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the trusted context outside the tool arguments
&lt;/h2&gt;

&lt;p&gt;At ingress, the application validates the user's authentication and resolves tenant membership from trusted account data. It selects a registered agent principal using an authorised routing decision. A client-provided display name is not an authoritative principal mapping.&lt;/p&gt;

&lt;p&gt;A protected assignment record connects that principal, tenant, authority mode, permitted invoice and execution policy. The queue carries a reference to the assignment. The worker retrieves the protected record through an authenticated path.&lt;/p&gt;

&lt;p&gt;This separation matters because a model can propose &lt;code&gt;invoice_id=INV-990&lt;/code&gt;. It must not be able to revise the trusted assignment to make INV-990 permitted. Tool arguments describe a request; they do not authorise the request.&lt;/p&gt;

&lt;p&gt;Here is an illustrative record shape, not a credential format:&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;"assignment_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;"assignment-204"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tenant_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;"cedar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agent_principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice-reviewer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"initiator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user:mariam"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authority_mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-delegated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"grant_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grant-204"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permitted_resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice:INV-204"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&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 schema makes relationships visible. It does not make the record trustworthy. Storage access, integrity, issuance, expiry and retrieval checks belong to the implementation. Copying this JSON into a prompt would discard the boundary it is intended to describe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authenticate the worker, then bind the assignment
&lt;/h2&gt;

&lt;p&gt;A credential broker first authenticates the workload using the deployment's supported mechanism. It then checks whether that workload may execute the named agent in the named environment and use the referenced assignment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://spiffe.io/docs/latest/spire-about/spire-concepts/" rel="noopener noreferrer"&gt;SPIRE&lt;/a&gt; demonstrates one approach to issuing workload identities based on configured attestation conditions. It does not automatically define your application-level agent or tenant mapping.&lt;/p&gt;

&lt;p&gt;The broker must reject a valid staging identity when the assignment requires production. It must reject a workload that is not authorised for the agent principal. It must reject a grant belonging to another assignment or tenant, even if every individual identifier resolves successfully.&lt;/p&gt;

&lt;p&gt;These are relational checks. Looking up four existing records and declaring success would miss the point.&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%2Fdtavc4buwnbjutk61dzc.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%2Fdtavc4buwnbjutk61dzc.png" alt="One logical agent principal has two runtime instances, each executing a separate task with its own grant and tenant context." width="800" height="747"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The diagram shows one principal using multiple authenticated runtime instances with distinct task grants. The broker must enforce the permitted relationships.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The downstream invoice API also checks the requested resource and operation. A successful broker decision must not become permission to read arbitrary invoice identifiers. The exact division of checks depends on whether the destination consumes a constrained token, performs grant lookup, or uses another documented authorisation mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat shared workers as a trust decision
&lt;/h2&gt;

&lt;p&gt;If invoice review and supplier management run in the same process with access to both credentials, separate agent labels do not isolate them. A compromised process may be able to exercise the authority of both.&lt;/p&gt;

&lt;p&gt;You can separate the deployments, strengthen isolation within the runtime, or design a broker that releases narrowly constrained credentials only for independently authenticated assignments. Each option adds operational work. Each requires a threat model for credential theft and context substitution.&lt;/p&gt;

&lt;p&gt;Per-run credentials are not automatically necessary, and they are not automatically sufficient. A fresh credential with broad permissions still has broad permissions. A short lifetime limits a time window; it does not establish the correct tenant or purpose.&lt;/p&gt;

&lt;p&gt;Google Cloud's &lt;a href="https://docs.cloud.google.com/iam/docs/identity-delegation-architecture" rel="noopener noreferrer"&gt;identity delegation architecture&lt;/a&gt; provides a concrete provider example of separate ingress, runtime and delegated contexts. Its mechanisms should not be treated as guarantees supplied by any generic agent framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep service authority explicit
&lt;/h2&gt;

&lt;p&gt;For a scheduled reconciliation, the initiator may be an approved scheduler rather than a human. The task can use an explicit service mandate with its own permitted resources.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.rfc-editor.org/rfc/rfc6749.html#section-4.4" rel="noopener noreferrer"&gt;OAuth client credentials grant&lt;/a&gt; covers confidential clients accessing their own resources or previously arranged access. Do not disguise this mode as a user's continuing delegation.&lt;/p&gt;

&lt;p&gt;In the assignment service, use distinct authority modes and prohibit silent fallback between them. A missing user grant should not cause a broker to issue a broad service token. If the product supports a separately authorised service workflow, create and review that workflow as such.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test substitutions at the real boundary
&lt;/h2&gt;

&lt;p&gt;For this example, submit an otherwise valid assignment with an invoice from another tenant. Expected result: deny before disclosure. Submit a different agent principal using the same worker credential. Expected result: deny unless an independently trusted assignment and mapping authorise that principal.&lt;/p&gt;

&lt;p&gt;Try a valid staging workload against the production assignment. Expected result: deny. Replace the original worker with an authorised production replica while retaining a valid assignment. Expected result: allow the permitted invoice read, subject to current resource policy.&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%2F4paef2ysy4ewfb4fvfwo.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%2F4paef2ysy4ewfb4fvfwo.png" alt="Two proposed traces share authentic credentials; the mismatched runtime binding fails before grant and action checks." width="800" height="773"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The paired example isolates the runtime-binding check. It is a proposed integration test, not proof that a particular runtime supplies the control.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Also test where the attacker supplies the substitution. A unit test against a hand-built trusted context will miss an ingress path that accepts &lt;code&gt;tenant_id&lt;/code&gt; directly from a model-generated argument. Exercise the path that constructs the context, not only the final comparison.&lt;/p&gt;

&lt;p&gt;Keep secrets out of the evidence. Record authenticated principal references, the resolved assignment, policy outcome and protected resource reference. The &lt;a href="https://mahmadafzal.com/agentic-ai-liability-the-gap-nobody-wants-to-fix/" rel="noopener noreferrer"&gt;audit trail guide&lt;/a&gt; covers connecting those decisions to actual outcomes.&lt;/p&gt;

&lt;p&gt;NIST's &lt;a href="https://www.nist.gov/blogs/cybersecurity-insights/design-tenets-agent-identity-management-systems" rel="noopener noreferrer"&gt;agent identity design tenets&lt;/a&gt; offer current design context, not a conformance test for this proposal. The implementation is ready for review when you can show where a false relationship is refused. A nicely named &lt;code&gt;agent_id&lt;/code&gt; field is merely ready for serialisation.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
    <item>
      <title>Build a taste record for your AI agent, without turning preference into fake precision</title>
      <dc:creator>Ahmad Afzal</dc:creator>
      <pubDate>Mon, 19 Oct 2026 16:54:08 +0000</pubDate>
      <link>https://dev.to/mahmaddafzal/build-a-taste-record-for-your-ai-agent-without-turning-preference-into-fake-precision-3a0g</link>
      <guid>https://dev.to/mahmaddafzal/build-a-taste-record-for-your-ai-agent-without-turning-preference-into-fake-precision-3a0g</guid>
      <description>&lt;p&gt;Your agent has generated five options. All five are the same idea with a different haircut.&lt;/p&gt;

&lt;p&gt;I wrote recently about the interchangeable glowing-globe dashboards I kept seeing at UAE exhibitions. Here is the implementation question behind that complaint: how do we give an agent useful preferences without pretending a style score is objective truth?&lt;/p&gt;

&lt;p&gt;I keep my own taste profile as a skill. The small pattern below is a proposed implementation, not a benchmark result or a description of a production system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate constraints from preferences
&lt;/h2&gt;

&lt;p&gt;A taste record needs three kinds of information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context:&lt;/strong&gt; the audience, task and decision the output should support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraints:&lt;/strong&gt; checks that must pass before anyone considers style.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preferences:&lt;/strong&gt; annotated examples and reasons for choosing between acceptable alternatives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a customer-facing release note, “include the migration guide” is a constraint. “Lead with the customer's next action” is a preference that may have exceptions. Do not blend them into one weighted score. Otherwise, excellent typography can compensate mathematically for a missing instruction. An elegant way to send everyone to support.&lt;/p&gt;

&lt;p&gt;Here is a minimal record:&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;"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;"0.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;"audience"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"existing API customers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"explain a breaking configuration change"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required_urls"&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="s2"&gt;"https://example.com/migrate"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"banned_phrases"&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="s2"&gt;"seamless transition"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preferences"&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;"Name the affected workflow before the implementation detail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"State a limitation beside the claim it qualifies"&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;"examples"&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="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Update the endpoint before Friday to keep exports running."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prefer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Names the action, deadline and consequence"&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;p&gt;The URL and deadline are illustrative. Replace them with verified facts for the actual release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the mechanical gate small and honest
&lt;/h2&gt;

&lt;p&gt;This Python example checks exact URL presence and normalised phrase matches. It does not verify that a URL works, that the claim is true, or that the document is good.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NFKC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;casefold&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&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;violations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;normalised&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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;phrase&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;banned_phrases&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phrase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;normalised&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;banned phrase: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;phrase&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required_urls&lt;/span&gt;&lt;span class="sh"&gt;"&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;url&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing URL: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;

&lt;span class="n"&gt;record&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;banned_phrases&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seamless transition&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;required_urls&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/migrate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A SEAMLESS   transition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;banned phrase: seamless transition&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;missing URL: https://example.com/migrate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;violations&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 https://example.com/migrate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A substring matcher has obvious limits. It can flag a phrase quoted for criticism and miss a paraphrase. Keep an explicit exception path with a recorded reason. Do not present this as a security boundary or an AI detector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate alternatives before asking for a winner
&lt;/h2&gt;

&lt;p&gt;Ask for materially different structures, then compare them against the same brief. For example: action-first, before-and-after, and trade-off-first. Require each candidate to explain what it sacrifices.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://arxiv.org/abs/2510.22954" rel="noopener noreferrer"&gt;Artificial Hivemind study&lt;/a&gt; gives a research basis for worrying about convergence. &lt;a href="https://arxiv.org/abs/2510.01171" rel="noopener noreferrer"&gt;Verbalized Sampling&lt;/a&gt; explores eliciting multiple responses with probabilities. Treat those stated probabilities as model output, not calibrated confidence.&lt;/p&gt;

&lt;p&gt;Give a critic the candidates, the taste record and the task. Ask for quoted evidence against each preference, plus any disagreement between criteria. Keep candidate text clearly separated from instructions. A second agent can share the first agent's blind spots, and untrusted candidate content can try to influence the review. Human inspection remains necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store the decision, not just the winner
&lt;/h2&gt;

&lt;p&gt;Record the chosen candidate, rejected alternatives, human rationale, record version and observed outcome. Keep a few examples out of the prompt to check whether a revised taste record generalises beyond the examples it was shown.&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%2Fjedul071ut5me31n2v0u.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%2Fjedul071ut5me31n2v0u.webp" alt="AI agent taste loop with brief, explore, critique, human choice, observation and revision feeding a shared decision record" width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the output passes the checks but readers still misunderstand it, change the record. The objective is a better decision process, not a permanently green dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/posts/mahfzal_in-the-last-two-months-i-walked-the-floors-ugcPost-7517975404142465024-OeQk/" rel="noopener noreferrer"&gt;Watch my original LinkedIn video&lt;/a&gt;, or &lt;a href="https://mahmadafzal.com/taste-in-ai-workflows-stop-shipping-the-average/" rel="noopener noreferrer"&gt;read the companion article and watch the video on my website&lt;/a&gt;.&lt;/p&gt;



</description>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your agent eval passed. Did the fixture leak the answer?</title>
      <dc:creator>Ahmad Afzal</dc:creator>
      <pubDate>Sun, 18 Oct 2026 09:54:52 +0000</pubDate>
      <link>https://dev.to/mahmaddafzal/your-agent-eval-passed-did-the-fixture-leak-the-answer-2gim</link>
      <guid>https://dev.to/mahmaddafzal/your-agent-eval-passed-did-the-fixture-leak-the-answer-2gim</guid>
      <description>&lt;p&gt;Your agent can ace a decision test if you accidentally hand it the answer key. Open-book exams have their place. Inside the question is probably pushing it.&lt;/p&gt;

&lt;p&gt;When evaluating the next action an agent chooses, keep three things separate: what it could observe, what you wanted it to choose and what happened afterwards. Mixing those together makes a convincing demo and a questionable measurement.&lt;/p&gt;

&lt;p&gt;Here is a small Python example for checking that separation. It uses two synthetic cases and no model API. It tests the evaluator's plumbing, not an agent's intelligence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary to draw
&lt;/h2&gt;

&lt;p&gt;The candidate gets an observation and two possible next moves. The evaluator retains the preferred option and its reason. The answer letters are assigned after shuffling, so choosing A cannot always stand in for choosing the preferred action.&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%2Fbecyemih44g8f1oiecnm.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%2Fbecyemih44g8f1oiecnm.webp" alt="Data-flow diagram separating the candidate prompt from the evaluator-only preference and answer mapping" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a data-flow boundary, not an access-control system. If your agent can read the fixture file through its tools, hiding fields in the prompt has achieved very little.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it locally
&lt;/h2&gt;

&lt;p&gt;Download the files from the &lt;a href="https://gist.github.com/mahfzal87/6a328420d076e90c817dcbd7b93f20a3" rel="noopener noreferrer"&gt;complete example and recorded output&lt;/a&gt;, or clone the Gist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://gist.github.com/6a328420d076e90c817dcbd7b93f20a3.git
&lt;span class="nb"&gt;cd &lt;/span&gt;6a328420d076e90c817dcbd7b93f20a3
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; unittest &lt;span class="nt"&gt;-v&lt;/span&gt; test_blind_fork
python3 blind_fork.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python 3.10 or newer is sufficient. There are no third-party dependencies. The core presentation function deliberately returns two objects:&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;present&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;choices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&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="nc"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;chr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;identifier&lt;/span&gt;
           &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="n"&gt;prompt&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;observation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observation&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;options&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="nf"&gt;chr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send only &lt;code&gt;prompt&lt;/code&gt; to a candidate. Keep &lt;code&gt;key&lt;/code&gt; with the evaluator. The complete script imports &lt;code&gt;Random&lt;/code&gt; and includes the cases and scorer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually tested
&lt;/h2&gt;

&lt;p&gt;On 18 October 2026, all three unittest methods passed locally. They check that the candidate payload has only the intended fields, that the preferred answer maps correctly across ten shuffle seeds and that malformed answers return &lt;code&gt;invalid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The demonstration's always-A stub gets the preferred choice in two of four presentations using seeds 0 and 1. That is not a model result. It is a deliberately unthinking baseline over two repeated toy cases. It shows why the letter mapping matters, not that a benchmark has become robust.&lt;/p&gt;

&lt;p&gt;One case has an authorised tool result already available, so inspecting that result is preferred to asking the user again. The other has an ambiguous recipient before an external action, so clarification is preferred. These examples are intentionally obvious. They would be poor evidence for a claim about subtle judgement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before calling it an evaluation
&lt;/h2&gt;

&lt;p&gt;Replace the toy cases with decisions that matter in your workflow. Retain the evidence available at the decision point. Keep later outcomes out of the candidate input, even when they helped you construct the label. Otherwise you are testing hindsight with a very expensive autocomplete.&lt;/p&gt;

&lt;p&gt;Have reviewers explain disagreement. A single preferred option is a simplification; some cases need ties, uncertainty or several acceptable actions. Use held-out cases, inspect sensitivity to option order and wording, and report invalid responses separately from disagreements. Do not quietly retry until the answer fits.&lt;/p&gt;

&lt;p&gt;The recent &lt;a href="https://arxiv.org/abs/2609.25804" rel="noopener noreferrer"&gt;Tasteful Agent preprint&lt;/a&gt; studies decision forks in longer engineering and research trajectories. This example does not reproduce that benchmark. It is a small starting point for keeping your own test honest. &lt;a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents" rel="noopener noreferrer"&gt;Anthropic's evaluation guidance&lt;/a&gt; is also useful context for separating tasks, trials and graders.&lt;/p&gt;

&lt;p&gt;No LLM was called here. No production trace was used. If this catches a leaked label before you spend money running the real evaluation, it has done its job.&lt;/p&gt;

&lt;p&gt;Which is harder in your setup: keeping future evidence out of the prompt, or agreeing on the preferred action in the first place?&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your agent has a system prompt. Does it have a bouncer?</title>
      <dc:creator>Ahmad Afzal</dc:creator>
      <pubDate>Wed, 14 Oct 2026 20:21:43 +0000</pubDate>
      <link>https://dev.to/mahmaddafzal/your-agent-has-a-system-prompt-does-it-have-a-bouncer-4d0</link>
      <guid>https://dev.to/mahmaddafzal/your-agent-has-a-system-prompt-does-it-have-a-bouncer-4d0</guid>
      <description>&lt;p&gt;“Never share another customer's data” is a sensible instruction.&lt;/p&gt;

&lt;p&gt;It is also a terrible access-control system.&lt;/p&gt;

&lt;p&gt;A model can understand the rule, explain the rule and still produce a tool call that breaks the rule. The awkward question is what happens next. Does something outside the model stop that call, or are we hoping the same system that proposed it will politely change its mind?&lt;/p&gt;

&lt;p&gt;I want the bouncer at the door. Not a paragraph describing a bouncer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the suggestion from the permission
&lt;/h2&gt;

&lt;p&gt;Consider an agent helping a customer retrieve an invoice. It proposes &lt;code&gt;invoice.read&lt;/code&gt; with an invoice ID. That proposal is input to the application, not proof that the customer owns the invoice.&lt;/p&gt;

&lt;p&gt;The backend resolves the authenticated customer and tenant from trusted session context. It loads the invoice's ownership from its own records. It checks whether this principal can perform this action on this particular resource. Only then does it read and return the permitted fields.&lt;/p&gt;

&lt;p&gt;Notice what the model doesn't get to declare: its own tenant, its own role or a magic &lt;code&gt;approved: true&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;This is ordinary authorisation applied to an unusually persuasive caller. &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP's guidance&lt;/a&gt; calls for default denial and permission checks on every request. Adding an agent does not make those checks optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary that matters
&lt;/h2&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%2Fwafo9or0m0hqxpgxrvj1.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%2Fwafo9or0m0hqxpgxrvj1.webp" alt="Architecture: the agent proposes a read; trusted identity and resource ownership feed the decision, enforced at the protected service. Denial or timeout keeps data closed." width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A useful design has three distinct jobs. The agent proposes an action. A policy decision evaluates trusted context. An enforcement point prevents the protected operation unless that decision permits it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.openpolicyagent.org/docs" rel="noopener noreferrer"&gt;Open Policy Agent describes this separation&lt;/a&gt;: the policy engine makes a decision; your application enforces it. Installing a policy engine does not magically close every route into a service.&lt;/p&gt;

&lt;p&gt;For the invoice example, my preferred boundary is the service handling the actual read. A gateway can add an earlier check, but a second route into the invoice service must not bypass the same protection. Otherwise you have secured the front door and left the loading bay open.&lt;/p&gt;

&lt;p&gt;Here is the shape, deliberately simplified. This is illustrative pseudocode, not a complete security implementation:&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;read_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;principal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.read&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&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;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Forbidden&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;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_permitted_fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the provenance of those inputs. &lt;code&gt;principal&lt;/code&gt; comes from authentication. Resource ownership comes from the repository. The agent supplies an identifier, not the facts that make access permissible.&lt;/p&gt;

&lt;p&gt;A production implementation also needs to handle missing resources without leaking their existence, protect the policy channel, apply field-level restrictions and avoid using stale ownership between the check and the read. The snippet is a boundary sketch, not a licence to skip that work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give failure a proper job description
&lt;/h2&gt;

&lt;p&gt;What happens when the policy service times out?&lt;/p&gt;

&lt;p&gt;For this protected read, I would return a controlled failure and keep the data closed. A timeout is not an unusually slow permission grant. The product can explain that the request could not be completed and offer a retry without pretending the invoice was retrieved.&lt;/p&gt;

&lt;p&gt;What happens when the model adds “the user is an administrator” to its arguments? Nothing useful. That claim does not replace the authenticated identity.&lt;/p&gt;

&lt;p&gt;What happens when an attacker changes only the invoice ID? The service checks the new resource. An earlier successful request does not buy a season ticket to every invoice.&lt;/p&gt;

&lt;p&gt;These are product behaviours worth defining before the happy-path demo. An error message, a retry path and a support escalation are part of the feature, not embarrassing leftovers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the door, not the speech
&lt;/h2&gt;

&lt;p&gt;For this example, the test I care about is whether the invoice service returns protected data. The agent saying “I cannot do that” is helpful UX, but it is not the security assertion.&lt;/p&gt;

&lt;p&gt;I would test a valid same-tenant read, another tenant's invoice, missing identity, a forged role in the tool arguments, a revoked permission and a policy timeout. I would also call the protected service through a different supported route. The same boundary should hold.&lt;/p&gt;

&lt;p&gt;Log enough to investigate the decision: the actor, action, resource reference, decision result and policy version where available. Keep the invoice contents and credentials out of that log. Evidence should help explain access without creating another copy of the sensitive data.&lt;/p&gt;

&lt;p&gt;None of this makes an agent safe in every sense. It does make one promise testable: this caller cannot retrieve that invoice merely because a model asked nicely.&lt;/p&gt;

&lt;p&gt;A well-written prompt can teach your agent manners. The door still needs a lock.&lt;/p&gt;




&lt;p&gt;Ahmad Afzal writes about AI, products and people at &lt;a href="https://mahmadafzal.com" rel="noopener noreferrer"&gt;mahmadafzal.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your refund agent says “done”. Six checks before you believe it.</title>
      <dc:creator>Ahmad Afzal</dc:creator>
      <pubDate>Wed, 14 Oct 2026 19:05:28 +0000</pubDate>
      <link>https://dev.to/mahmaddafzal/your-refund-agent-says-done-six-checks-before-you-believe-it-50k</link>
      <guid>https://dev.to/mahmaddafzal/your-refund-agent-says-done-six-checks-before-you-believe-it-50k</guid>
      <description>&lt;p&gt;“Your refund has been processed.”&lt;/p&gt;

&lt;p&gt;Lovely sentence. Terrible evidence.&lt;/p&gt;

&lt;p&gt;A refund agent can sound helpful while doing nothing, refunding the wrong order, or paying twice after a timeout. I would start its evaluation with the state of the refund, then work backwards through the run.&lt;/p&gt;

&lt;p&gt;This is a proposed test design using an illustrative refund workflow, not a report of production results.&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%2F6verrb0q80o8ui3ps50c.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%2F6verrb0q80o8ui3ps50c.webp" alt="Six refund checks and their evidence: correct result, authority, duplicate prevention, truthful reporting, permission boundaries and a reconstructable decision." width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Did the right thing happen?
&lt;/h2&gt;

&lt;p&gt;Check the authoritative transaction record. Match the order, customer, currency and amount. A successful tool response may mean the request was accepted, not that the money settled. Decide which state your product is allowed to describe as “processed”.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Was it allowed?
&lt;/h2&gt;

&lt;p&gt;Separate completion from permission. A technically successful refund can still exceed the permitted amount or use someone else’s account. Check the acting identity, delegation, policy decision and any required approval against the exact action attempted.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Did it happen only once?
&lt;/h2&gt;

&lt;p&gt;Create the awkward case: the payment service commits the refund, but the agent sees a timeout. Retry the request. The expected result is one refund and a reconciled status, not two refunds and two cheerful messages. Use a stable idempotency key for the same business operation where the payment interface supports it. &lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe’s documentation&lt;/a&gt; provides a concrete example; check your provider’s retention and retry semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Did the answer match the evidence?
&lt;/h2&gt;

&lt;p&gt;Compare the final message with the transaction state. Pending should stay pending. Failed should stay failed. An uncertain result should trigger reconciliation or escalation, rather than a confident success claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Did it stop at the boundary?
&lt;/h2&gt;

&lt;p&gt;Test an expired approval, a changed amount after approval and revoked access before execution. The expected behaviour is a blocked action or a new approval request. The initial permission should not quietly stretch to cover a different decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Can a reviewer reconstruct the decision?
&lt;/h2&gt;

&lt;p&gt;Link the request, policy version, approval, tool invocation and transaction result with a correlation ID. Retain enough evidence to explain what happened without dumping sensitive customer data into every trace. Missing evidence is an evaluation failure worth recording.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small test table I would start with
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Expected outcome&lt;/th&gt;
&lt;th&gt;Evidence to inspect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Eligible order, permitted amount&lt;/td&gt;
&lt;td&gt;One correct refund&lt;/td&gt;
&lt;td&gt;Transaction record and scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amount exceeds authority&lt;/td&gt;
&lt;td&gt;No refund without valid approval&lt;/td&gt;
&lt;td&gt;Policy decision and approval record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commit followed by timeout&lt;/td&gt;
&lt;td&gt;Reconcile; no duplicate payment&lt;/td&gt;
&lt;td&gt;Idempotency key and transaction lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amount changes after approval&lt;/td&gt;
&lt;td&gt;Require a fresh decision&lt;/td&gt;
&lt;td&gt;Approved parameters versus actual call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access revoked before execution&lt;/td&gt;
&lt;td&gt;Block execution&lt;/td&gt;
&lt;td&gt;Authorisation check at action time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool reports pending&lt;/td&gt;
&lt;td&gt;Tell the customer it is pending&lt;/td&gt;
&lt;td&gt;Tool state and final response&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are expected outcomes, not measured pass rates. Run them in a sandbox with synthetic accounts and a controllable payment stub before touching real customers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Score the dimensions separately
&lt;/h2&gt;

&lt;p&gt;I would record task completion, permission compliance, duplicate prevention, truthful reporting and evidence completeness as separate results. A single average can hide the failure you most needed to see.&lt;/p&gt;

&lt;p&gt;When a case fails, inspect the trace, identify the mechanism, and add a regression case that would catch that mechanism again. Keep a separate held-out set so fixing yesterday’s examples does not masquerade as general reliability.&lt;/p&gt;

&lt;p&gt;The customer does not care that the agent passed a language test. They care that the right refund happened once. Fair enough.&lt;/p&gt;




&lt;p&gt;I write about AI, products and people at &lt;a href="https://mahmadafzal.com" rel="noopener noreferrer"&gt;mahmadafzal.com&lt;/a&gt;. This practical checklist builds on my &lt;a href="https://medium.com/@mahmaddafzal/your-ai-agent-passed-the-test-so-did-the-smoke-alarm-with-no-battery-ae06aefdabba" rel="noopener noreferrer"&gt;essay about agent evaluations&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
