<?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>What I Learned Implementing noReactObjectTypeAsDefaultProp in Biome</title>
      <dc:creator>subaru</dc:creator>
      <pubDate>Tue, 22 Sep 2026 09:50:26 +0000</pubDate>
      <link>https://dev.to/subaruhello/what-i-learned-implementing-noreactobjecttypeasdefaultprop-in-biome-l8</link>
      <guid>https://dev.to/subaruhello/what-i-learned-implementing-noreactobjecttypeasdefaultprop-in-biome-l8</guid>
      <description>&lt;p&gt;I contributed the &lt;code&gt;noReactObjectTypeAsDefaultProp&lt;/code&gt; lint rule to &lt;a href="https://biomejs.dev/" rel="noopener noreferrer"&gt;Biome&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/biomejs/biome/issues/7656" rel="noopener noreferrer"&gt;https://github.com/biomejs/biome/issues/7656&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/biomejs/biome/pull/10634" rel="noopener noreferrer"&gt;https://github.com/biomejs/biome/pull/10634&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The issue was to port &lt;a href="https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-object-type-as-default-prop.md" rel="noopener noreferrer"&gt;eslint-plugin-react/no-object-type-as-default-prop&lt;/a&gt; to Biome. This post explains what the rule checks and what I learned about Rust while implementing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rule checks
&lt;/h2&gt;

&lt;p&gt;The rule reports expressions such as object and array literals when they are used as default values for destructured React props.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JavaScript creates a new value each time it evaluates an object or array literal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&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="c1"&gt;// false&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="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using one as a prop default therefore creates a different reference on every render. That can break memoization or make dependency-based logic run more often than intended.&lt;/p&gt;

&lt;p&gt;The rule detects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;object and array literals&lt;/li&gt;
&lt;li&gt;arrow functions and function expressions&lt;/li&gt;
&lt;li&gt;class expressions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;new&lt;/code&gt; expressions&lt;/li&gt;
&lt;li&gt;JSX elements&lt;/li&gt;
&lt;li&gt;regular-expression literals&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Symbol()&lt;/code&gt; calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Primitive defaults such as strings, numbers, and booleans remain valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading Rust before writing Rust
&lt;/h2&gt;

&lt;p&gt;Biome's contribution guide led me to state my intention on the issue before starting. I had almost no practical Rust experience, so I first read &lt;a href="https://doc.rust-lang.org/book/" rel="noopener noreferrer"&gt;The Rust Programming Language&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After reading the book, I could follow the syntax, but many of the rules were still separate pieces of knowledge. They started to connect only when I had to read Biome's code, fix compiler errors, and respond to review feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ownership and borrowing in practice
&lt;/h2&gt;

&lt;p&gt;Every value in Rust has an owner. When the owner leaves its scope, the value is dropped. Passing a &lt;code&gt;String&lt;/code&gt; to another function may move ownership.&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;fn&lt;/span&gt; &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{message}"&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;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{message}"&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;The compiler explains exactly what happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error[E0382]: borrow of moved value: `message`
  |
  |     consume(message);
  |             ------- value moved here
  |
  |     println!("{message}");
  |                ^^^^^^^ value borrowed here after move
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a function only needs to read a value, it can borrow a reference instead.&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;fn&lt;/span&gt; &lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="nf"&gt;.len&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;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;length&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;message&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{message}"&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;The output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5
hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A mutable reference uses &lt;code&gt;&amp;amp;mut&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="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;append_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="nf"&gt;.push_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;", world"&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="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;append_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{message}"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hello, world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JavaScript and Go normally let a garbage collector reclaim unused memory. Rust uses a different approach: the compiler checks ownership and borrowing rules. A garbage collector is not unsafe; Rust simply reaches memory safety through a different design.&lt;/p&gt;

&lt;p&gt;While traversing Biome's syntax tree, I often needed to inspect nodes without owning them. I gradually learned to ask whether a reference was enough instead of moving or cloning a value by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traversing Biome's syntax tree
&lt;/h2&gt;

&lt;p&gt;Biome operates on a concrete syntax tree (CST), which preserves details such as comments and whitespace. A lint rule does not search JavaScript as text. It walks typed syntax nodes.&lt;/p&gt;

&lt;p&gt;Conceptually, this rule follows this path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React component
  └─ function parameters
      └─ object binding pattern
          └─ each property
              └─ default-value expression
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real implementation must handle function declarations, arrow functions, function expressions, functions wrapped in &lt;code&gt;memo&lt;/code&gt; or &lt;code&gt;forwardRef&lt;/code&gt;, and default-exported functions. These forms use different syntax-node types. I used Biome's existing &lt;code&gt;ReactComponentInfo&lt;/code&gt; and brought the different forms to one point where the rule could obtain the props object pattern.&lt;/p&gt;

&lt;p&gt;When I did not know a node or method name, I searched existing rules with &lt;code&gt;rg&lt;/code&gt; and found examples that handled similar syntax. Learning how to navigate a large codebase was as important as learning Rust syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Option and Result fit syntax trees
&lt;/h2&gt;

&lt;p&gt;A syntax tree does not always contain the node a rule is looking for. A function may not destructure its first parameter, and a file may contain incomplete syntax.&lt;/p&gt;

&lt;p&gt;Rust represents an optional value with &lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt;: &lt;code&gt;Some(T)&lt;/code&gt; when a value exists and &lt;code&gt;None&lt;/code&gt; when it does not.&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;fn&lt;/span&gt; &lt;span class="nf"&gt;find_even&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.copied&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.find&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&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="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;find_even&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;find_even&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Some(10)
None
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt; represents success as &lt;code&gt;Ok(T)&lt;/code&gt; and a recoverable failure as &lt;code&gt;Err(E)&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="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;parse_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;num&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ParseIntError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="nf"&gt;.parse&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;These types make absence and failure visible in a function's signature. Reading them in the Rust Book was useful, but using them to traverse syntax nodes made their purpose concrete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Returning multiple diagnostics with Vec
&lt;/h2&gt;

&lt;p&gt;One component can contain several invalid defaults:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&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="kc"&gt;null&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;Stopping after the first match would make users fix the same line repeatedly. The rule therefore returns a &lt;code&gt;Vec&lt;/code&gt; of findings. A &lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;/code&gt; is a growable collection of values with the same type. An empty vector represents no diagnostics; three entries represent three diagnostics.&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;fn&lt;/span&gt; &lt;span class="nf"&gt;collect_even&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;numbers&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;number&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Understanding what ? returns from
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;?&lt;/code&gt; operator unwraps a successful &lt;code&gt;Result&lt;/code&gt; or present &lt;code&gt;Option&lt;/code&gt;. On &lt;code&gt;Err&lt;/code&gt; or &lt;code&gt;None&lt;/code&gt;, it returns early from the current function or closure.&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;fn&lt;/span&gt; &lt;span class="nf"&gt;parse_and_double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;num&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ParseIntError&lt;/span&gt;&lt;span class="o"&gt;&amp;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;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="py"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;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="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&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;The word &lt;em&gt;current&lt;/em&gt; matters. The rule processes object properties with &lt;code&gt;filter_map&lt;/code&gt;. Its closure returns an &lt;code&gt;Option&lt;/code&gt;, so &lt;code&gt;?&lt;/code&gt; can skip the current property without ending the entire rule run.&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;let&lt;/span&gt; &lt;span class="n"&gt;defaults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;object_pattern&lt;/span&gt;
    &lt;span class="nf"&gt;.properties&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.filter_map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;property&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;property&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;default_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_default_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ForbiddenDefaultKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_expression&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;default_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ForbiddenDefault&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;default_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also explains why &lt;code&gt;?&lt;/code&gt; cannot be used directly in a function returning &lt;code&gt;Vec&lt;/code&gt;. It is not merely shorthand; its control flow is tied to the return type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving behavior closer to the type
&lt;/h2&gt;

&lt;p&gt;I initially put the expression classification in a free function:&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;fn&lt;/span&gt; &lt;span class="nf"&gt;forbidden_default_kind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;expression&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;AnyJsExpression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ForbiddenDefaultKind&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// classify the expression&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After review, I moved it to &lt;code&gt;ForbiddenDefaultKind&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="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ForbiddenDefaultKind&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;from_expression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expression&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;AnyJsExpression&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// classify the expression&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;Now the type that represents the classification also knows how to derive itself from an expression. Both versions can work, but the second location makes the call site easier to read and keeps the classification detail near the type it creates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving non-React behavior into shared syntax code
&lt;/h2&gt;

&lt;p&gt;I also added parameter extraction to the React component module at first. A reviewer pointed out that the operation was not React-specific. Extracting parameters from JavaScript functions can be useful to other analyses too.&lt;/p&gt;

&lt;p&gt;I moved it to a shared extension on &lt;code&gt;AnyJsFunction&lt;/code&gt; in &lt;code&gt;biome_js_syntax&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="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;AnyJsFunction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;parenthesized_parameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JsParameters&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// normalize the different function forms&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;That review changed how I look at module boundaries. It is not enough to place code where the first caller happens to live. The operation should live at the layer that owns the concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling aliased defaults
&lt;/h2&gt;

&lt;p&gt;Object destructuring can rename a property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;localItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;localItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first implementation handled only shorthand properties such as &lt;code&gt;{ items = [] }&lt;/code&gt;. The original ESLint rule also reports aliased defaults. Its &lt;a href="https://github.com/jsx-eslint/eslint-plugin-react/blob/c99d3b274efbc593e46563358e7b77cad8d01957/lib/rules/no-object-type-as-default-prop.js#L39-L45" rel="noopener noreferrer"&gt;property handling&lt;/a&gt; does not assume shorthand syntax.&lt;/p&gt;

&lt;p&gt;Biome represents shorthand and aliased binding properties with different node types. I updated the rule to extract defaults from both &lt;code&gt;JsObjectBindingPatternShorthandProperty&lt;/code&gt; and &lt;code&gt;JsObjectBindingPatternProperty&lt;/code&gt;, then added a regression test for the aliased form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnostics are part of the implementation
&lt;/h2&gt;

&lt;p&gt;A lint rule must do more than identify a node. Its message needs to help a user decide what to do next.&lt;/p&gt;

&lt;p&gt;The review introduced me to three parts of a useful diagnostic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;what is wrong&lt;/li&gt;
&lt;li&gt;why it matters&lt;/li&gt;
&lt;li&gt;how to fix it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also replaced unexplained wording such as “reference type” with the concrete behavior: the value is newly created on every render. A technically correct message is not automatically an actionable one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in my understanding of Rust
&lt;/h2&gt;

&lt;p&gt;After reading the Rust Book, ownership, borrowing, &lt;code&gt;Option&lt;/code&gt;, &lt;code&gt;Result&lt;/code&gt;, &lt;code&gt;Vec&lt;/code&gt;, and &lt;code&gt;?&lt;/code&gt; still felt like separate language features.&lt;/p&gt;

&lt;p&gt;In Biome they form one flow. The rule borrows syntax nodes, represents missing nodes with &lt;code&gt;Option&lt;/code&gt;, receives parsing failures as &lt;code&gt;Result&lt;/code&gt;, uses &lt;code&gt;?&lt;/code&gt; inside a closure to skip non-matches, and collects findings into a &lt;code&gt;Vec&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Rust now feels less like a language with many restrictions and more like a language that makes ownership, absence, and failure visible in the code. Reading the book gave me the vocabulary. Working in a real codebase—and receiving feedback from both the compiler and reviewers—made the vocabulary useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/biomejs/biome/pull/10634" rel="noopener noreferrer"&gt;Biome PR #10634&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/biomejs/biome/issues/7656" rel="noopener noreferrer"&gt;Biome issue #7656&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html" rel="noopener noreferrer"&gt;The Rust Programming Language: Ownership&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html" rel="noopener noreferrer"&gt;The Rust Programming Language: References and Borrowing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html#the-option-enum-and-its-advantages-over-null-values" rel="noopener noreferrer"&gt;The &lt;code&gt;Option&lt;/code&gt; enum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html" rel="noopener noreferrer"&gt;Recoverable Errors with &lt;code&gt;Result&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.rust-lang.org/book/ch08-01-vectors.html" rel="noopener noreferrer"&gt;Storing Lists of Values with Vectors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator" rel="noopener noreferrer"&gt;Rust Reference: the question mark operator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This article was edited with AI assistance. I checked the technical explanations against the merged implementation, tests, review discussions, and the official Rust documentation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>opensource</category>
      <category>javascript</category>
      <category>react</category>
    </item>
    <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>
