<?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: subaru</title>
    <description>The latest articles on DEV Community by subaru (@subaruhello).</description>
    <link>https://dev.to/subaruhello</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%2F4134702%2F6da862d5-3945-46db-ac3d-c381de3d430d.jpg</url>
      <title>DEV Community: subaru</title>
      <link>https://dev.to/subaruhello</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/subaruhello"/>
    <language>en</language>
    <item>
      <title>Who Decides the Tenant? A Small Rust Guard for AI Tool Calls</title>
      <dc:creator>subaru</dc:creator>
      <pubDate>Mon, 21 Sep 2026 01:28:40 +0000</pubDate>
      <link>https://dev.to/subaruhello/who-decides-the-tenant-a-small-rust-guard-for-ai-tool-calls-4pc4</link>
      <guid>https://dev.to/subaruhello/who-decides-the-tenant-a-small-rust-guard-for-ai-tool-calls-4pc4</guid>
      <description>&lt;p&gt;I have been thinking about a fairly ordinary failure mode in AI-enabled SaaS products.&lt;/p&gt;

&lt;p&gt;A support agent is helping a user from &lt;code&gt;tenant-a&lt;/code&gt;. The user asks about a contract, and the model produces a perfectly valid tool call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get_contract("contract-b")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There is nothing obviously wrong with the call. The tool exists and the argument has the right shape. The problem is that &lt;code&gt;contract-b&lt;/code&gt; belongs to &lt;code&gt;tenant-b&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A better prompt might make this happen less often. It cannot turn the prompt into an authorization boundary.&lt;/p&gt;

&lt;p&gt;I made TenantInvariant, a small experimental Rust crate, to explore where that boundary should live.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/subaru-hello" rel="noopener noreferrer"&gt;
        subaru-hello
      &lt;/a&gt; / &lt;a href="https://github.com/subaru-hello/tenant-invariant" rel="noopener noreferrer"&gt;
        tenant-invariant
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Executable tenant-isolation invariants for AI agent tool calls, written in Rust.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;TenantInvariant&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/subaru-hello/tenant-invariant/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/subaru-hello/tenant-invariant/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://crates.io/crates/tenant-invariant" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/a1824b03eedc63eca9e77b6a83905a5fbaa17ee2b7fddb507d6dede4ee21beb8/68747470733a2f2f696d672e736869656c64732e696f2f6372617465732f762f74656e616e742d696e76617269616e742e737667" alt="crates.io"&gt;&lt;/a&gt;
&lt;a href="https://crates.io/crates/tenant-invariant" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5442f9f8906810a004f12abd35efbbedf4c110ca7edd796d3b0771f6c76cdb58/68747470733a2f2f696d672e736869656c64732e696f2f6372617465732f642f74656e616e742d696e76617269616e742e737667" alt="downloads"&gt;&lt;/a&gt;
&lt;a href="https://github.com/subaru-hello/tenant-invariant/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What if an AI agent chooses a resource ID that belongs to another customer?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tenant-invariant&lt;/code&gt; is an experimental Rust library for making tenant isolation an executable invariant before an AI agent's tool call executes. The application supplies an authenticated actor and server-resolved resource ownership. The library allows same-tenant access and fails closed for cross-tenant or unknown ownership.&lt;/p&gt;
&lt;p&gt;It does not trust tenant IDs produced by the model.&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;agent proposes resource ID
  -&amp;gt; server resolves the resource owner from a trusted source
  -&amp;gt; TenantInvariant compares actor and owner
  -&amp;gt; existing authorization and tenant-scoped data operation
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;You need Git and the stable Rust toolchain. If Rust is not installed, use &lt;a href="https://www.rust-lang.org/tools/install" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;rustup&lt;/code&gt;, the installer recommended by the Rust project&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; macOS, Linux, or WSL&lt;/span&gt;
curl --proto &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;=https&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; --tlsv1.2 -sSf https://sh.rustup.rs &lt;span class="pl-k"&gt;|&lt;/span&gt; sh&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;On Windows, download and run &lt;code&gt;rustup-init.exe&lt;/code&gt; from the same official installation page. Restart the terminal…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/subaru-hello/tenant-invariant" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  The bug I had in mind
&lt;/h2&gt;

&lt;p&gt;Tool calling adds a new decision-maker to an otherwise familiar request path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user prompt
  -&amp;gt; model chooses a tool
  -&amp;gt; model supplies resource IDs
  -&amp;gt; application executes the tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is tempting to put both &lt;code&gt;tenant_id&lt;/code&gt; and &lt;code&gt;contract_id&lt;/code&gt; in the tool schema. That gives the model a neat, self-contained payload:&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;"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;"tenant-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contract_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;"contract-b"&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;It also gives the model a say in something it should not control. The tenant might have come from a system prompt, a previous tool result, or text supplied by the user. None of those sources prove authority.&lt;/p&gt;

&lt;p&gt;The rule I settled on is simple: the model can propose a resource ID, but it cannot tell the application who owns that resource.&lt;/p&gt;

&lt;p&gt;The actor's tenant comes from the authenticated server-side context. The application resolves the owner of the proposed resource from its database or another trusted service. Only those two values are compared.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI proposes a resource ID
        |
        v
server resolves the real owner
        |
        v
authenticated tenant == resource owner?
        | yes                         | no / unknown
        v                             v
normal authorization continues      deny
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I chose to deny an unknown owner as well. If the lookup fails, there is not enough information to authorize the operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The crate is deliberately small
&lt;/h2&gt;

&lt;p&gt;The actual comparison could be an &lt;code&gt;if&lt;/code&gt; statement. I did not want to hide that fact behind a large abstraction.&lt;/p&gt;

&lt;p&gt;The useful part of making it a crate is that the rule gets a name and a few types. &lt;code&gt;TenantId&lt;/code&gt; rejects an empty identifier. &lt;code&gt;ResourceOwner::Unknown&lt;/code&gt; makes a failed ownership lookup visible. &lt;code&gt;Decision&lt;/code&gt; makes the caller deal with both the allowed and denied paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tenant_invariant&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;
    &lt;span class="n"&gt;check_tenant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;,&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;ResourceOwner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// This value comes from the authenticated application context.&lt;/span&gt;
        &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenant-a"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenant ID must not be empty"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// The server looked this up using the ID proposed by the model.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ResourceOwner&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Tenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenant-b"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenant ID must not be empty"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;check_tenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&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="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Continue with normal authorization and a scoped query.&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nn"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Deny&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nd"&gt;eprintln!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"blocked: {reason:?}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can install it from crates.io:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add tenant-invariant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a runnable example in the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cargo run &lt;span class="nt"&gt;--example&lt;/span&gt; contract_lookup
&lt;span class="go"&gt;blocked: CrossTenant
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rust works nicely here because invalid or unresolved states can be represented directly instead of being hidden in strings and null values. That does not make the system secure by itself, but it makes the intended control flow harder to ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests I cared about
&lt;/h2&gt;

&lt;p&gt;The first tests were the obvious ones: a resource in the same tenant is allowed, a resource in another tenant is denied, and an unknown owner is denied.&lt;/p&gt;

&lt;p&gt;I also added scenarios for a forged tenant claim in model-generated arguments and for a batch containing resources from different tenants. The batch case is easy to get wrong. Checking the first valid item must not authorize the rest of the batch.&lt;/p&gt;

&lt;p&gt;The more interesting test is a property test. Instead of choosing a few tenant names by hand, &lt;code&gt;proptest&lt;/code&gt; generates pairs and checks the invariant directly: if the tenant IDs differ, the decision must always be &lt;code&gt;CrossTenant&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;proptest!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[test]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;different_tenants_are_always_denied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s"&gt;"[a-z0-9]{1,12}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s"&gt;"[a-z0-9]{1,12}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;prop_assume!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Actor&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="nf"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ResourceOwner&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Tenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="nd"&gt;prop_assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;check_tenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nn"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Deny&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;DenyReason&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CrossTenant&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this kind of library, the property is almost the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this sits next to OpenAI and Claude
&lt;/h2&gt;

&lt;p&gt;Both OpenAI and Anthropic already have controls around agent tools, so I wanted to understand whether this crate was duplicating them.&lt;/p&gt;

&lt;p&gt;OpenAI's Agents SDK has tool guardrails, tool filtering, and approval flows. A tool input guardrail can reject a call just before a custom function tool runs. That is a natural place to invoke a tenant check. OpenAI also notes that not every hosted tool goes through the same custom function-tool guardrail pipeline, so a remote MCP server still needs to enforce authorization itself.&lt;/p&gt;

&lt;p&gt;Anthropic describes client-side tool use as a contract: Claude returns a structured request, while the application executes the operation. For an application-specific tool, the execution boundary remains under the developer's control. That is where ownership can be resolved and checked. Anthropic's broader agent-security guidance also recommends limiting tools, permissions, data, and execution environments rather than treating prompt-injection detection as a complete defense.&lt;/p&gt;

&lt;p&gt;Those mechanisms answer related but different questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;schema validation       Is the tool call well formed?
tool policy             Should this tool be available or require approval?
authentication          Who is making the request?
tenant ownership        Does this object belong to that caller's tenant?
action authorization    May the caller perform this operation?
scoped query or RLS     Will the data layer enforce the boundary as well?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OAuth can establish the caller and the scopes granted to a token. It cannot discover that &lt;code&gt;contract-b&lt;/code&gt; belongs to another customer in my application's database. That last relationship is local business data, so the application still has to enforce it.&lt;/p&gt;

&lt;p&gt;TenantInvariant is meant to fit into that gap. It does not compete with the controls in an agent SDK or MCP host.&lt;/p&gt;

&lt;p&gt;The relevant documentation is here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://openai.github.io/openai-agents-python/guardrails/" rel="noopener noreferrer"&gt;OpenAI Agents SDK: Guardrails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openai.github.io/openai-agents-python/mcp/" rel="noopener noreferrer"&gt;OpenAI Agents SDK: Model Context Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works" rel="noopener noreferrer"&gt;Claude Platform: How tool use works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/research/trustworthy-agents" rel="noopener noreferrer"&gt;Anthropic: Trustworthy agents in practice&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;Version 0.1.0 is a small experiment, not a complete authorization system or a security guarantee.&lt;/p&gt;

&lt;p&gt;It does not authenticate a user. It does not decide whether the user may read, update, or delete a resource. It does not add a tenant predicate to a database query. It also cannot prevent a race between an ownership lookup and a later, unscoped fetch.&lt;/p&gt;

&lt;p&gt;The final operation still needs to be tenant-scoped. PostgreSQL Row-Level Security can be a useful backstop if application code gets this wrong. OpenFGA, Cedar, OPA, or an existing application policy layer may handle action-level authorization.&lt;/p&gt;

&lt;p&gt;In other words, the full path should still look more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;authenticated context
  -&amp;gt; tenant ownership check
  -&amp;gt; action authorization
  -&amp;gt; tenant-scoped query / RLS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I want to try next
&lt;/h2&gt;

&lt;p&gt;The equality check is only a starting point. I am more interested in testing the entire route from a prompt to a database operation.&lt;/p&gt;

&lt;p&gt;A useful test kit could inject cross-tenant resource IDs and mixed-tenant batches into real agent tool calls, then verify that every path is denied by the application's existing authorization stack. That would test the integration rather than only the small function shown above.&lt;/p&gt;

&lt;p&gt;If I had to summarize the project in one sentence: it keeps model-generated resource IDs on the untrusted side of the authorization boundary.&lt;/p&gt;

&lt;p&gt;I would be interested to hear where other multi-tenant SaaS or MCP implementations resolve resource ownership, and where this check lives in their request path.&lt;/p&gt;

&lt;p&gt;The crate is available on &lt;a href="https://crates.io/crates/tenant-invariant" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt;. The source, example, and scenario tests are on &lt;a href="https://github.com/subaru-hello/tenant-invariant" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. It is MIT licensed and experimental.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ai</category>
      <category>security</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
