<?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: Sagar Ghag</title>
    <description>The latest articles on DEV Community by Sagar Ghag (@ghagsagar23).</description>
    <link>https://dev.to/ghagsagar23</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%2F4016716%2F3b117c4e-7c91-4107-903e-a8468d8bdcfb.png</url>
      <title>DEV Community: Sagar Ghag</title>
      <link>https://dev.to/ghagsagar23</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ghagsagar23"/>
    <language>en</language>
    <item>
      <title>Patches Welcome #1: Apple Quietly Broke Flutter Builds, and Nobody Noticed for 10 Months</title>
      <dc:creator>Sagar Ghag</dc:creator>
      <pubDate>Thu, 05 Nov 2026 17:37:04 +0000</pubDate>
      <link>https://dev.to/ghagsagar23/patches-welcome-1-apple-quietly-broke-flutter-builds-and-nobody-noticed-for-10-months-59nf</link>
      <guid>https://dev.to/ghagsagar23/patches-welcome-1-apple-quietly-broke-flutter-builds-and-nobody-noticed-for-10-months-59nf</guid>
      <description>&lt;p&gt;&lt;code&gt;flutter build ios&lt;/code&gt; started failing on my machine with a permissions error pointing at a build output directory. The actual problem was three layers deeper: Apple silently removed GNU rsync from macOS 15.4, its replacement ignores a flag Flutter depends on, and the existing fix had been a no-op for almost a year.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I found it
&lt;/h3&gt;

&lt;p&gt;I run the Flutter SDK through Nix. The SDK lives in &lt;code&gt;/nix/store&lt;/code&gt;, which is read-only by design. That's a supported configuration. Flutter merged PR #148580 in May 2024 specifically to handle read-only SDK paths by passing &lt;code&gt;--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r&lt;/code&gt; to &lt;code&gt;rsync&lt;/code&gt; during framework copies.&lt;/p&gt;

&lt;p&gt;Worked fine until one day it didn't. The error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fatal error: lipo: can't create temporary output file:
  …/Flutter.framework/Flutter.lipo (Permission denied)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's &lt;code&gt;lipo&lt;/code&gt; complaining it can't write to a file in the &lt;em&gt;build output&lt;/em&gt; directory. Not the SDK. The build output. I spent a while staring at that before realising: the output directory inherited the source's read-only permissions because the copy step silently failed to set them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The mechanism
&lt;/h3&gt;

&lt;p&gt;I traced it to &lt;code&gt;packages/flutter_tools/lib/src/build_system/targets/darwin.dart&lt;/code&gt;. The &lt;code&gt;copyFramework&lt;/code&gt; function runs &lt;code&gt;rsync&lt;/code&gt; with &lt;code&gt;--chmod&lt;/code&gt; to make the copied framework writable before &lt;code&gt;thinFramework&lt;/code&gt; rewrites it in place with &lt;code&gt;lipo&lt;/code&gt;. The flag is there. It's correct. It does nothing.&lt;/p&gt;

&lt;p&gt;macOS 15.4 removed GNU rsync. &lt;code&gt;/usr/bin/rsync&lt;/code&gt; is now Apple's &lt;code&gt;openrsync&lt;/code&gt; fork, and &lt;code&gt;openrsync&lt;/code&gt; accepts &lt;code&gt;--chmod&lt;/code&gt;, validates it, and then never applies it. Exit code 0. No warning.&lt;/p&gt;

&lt;p&gt;I dug into Apple's open source distributions to confirm. The dispatcher source at &lt;code&gt;rsync-91.0.6&lt;/code&gt; shows two backends: &lt;code&gt;rsync_openrsync&lt;/code&gt; (default) and &lt;code&gt;rsync_samba&lt;/code&gt; (GNU). The &lt;code&gt;--chmod&lt;/code&gt; flag exists only in the samba longopts table. Between tags &lt;code&gt;rsync-91.81.3&lt;/code&gt; (2025-02-26) and &lt;code&gt;rsync-146&lt;/code&gt; (2025-04-25), Apple removed the GNU fallback entirely. That brackets macOS 15.4.&lt;/p&gt;

&lt;p&gt;Apple documented none of this. The macOS 15.4 security content doesn't mention rsync. The &lt;code&gt;openrsync&lt;/code&gt; man page still says &lt;code&gt;--chmod&lt;/code&gt; supports literal modes like &lt;code&gt;755&lt;/code&gt;. It doesn't. &lt;code&gt;chmod_parse()&lt;/code&gt; in &lt;code&gt;openrsync/main.c&lt;/code&gt; has no octal production; &lt;code&gt;755&lt;/code&gt; returns &lt;code&gt;EINVAL&lt;/code&gt;. The relative forms parse but the mode never reaches disk.&lt;/p&gt;

&lt;p&gt;I wrote a minimal repro that takes Flutter out of the equation entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/rs/src/F.framework &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;hi &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/rs/src/F.framework/bin
&lt;span class="nb"&gt;chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; a-w /tmp/rs/src/F.framework
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/rs/dst
/usr/bin/rsync &lt;span class="nt"&gt;-av&lt;/span&gt; &lt;span class="nt"&gt;--chmod&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;Du&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rwx,Dgo&lt;span class="o"&gt;=&lt;/span&gt;rx,Fu&lt;span class="o"&gt;=&lt;/span&gt;rw,Fgo&lt;span class="o"&gt;=&lt;/span&gt;r &lt;span class="se"&gt;\&lt;/span&gt;
  /tmp/rs/src/F.framework /tmp/rs/dst/
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ld&lt;/span&gt; /tmp/rs/dst/F.framework
&lt;span class="c"&gt;# dr-xr-xr-x (--chmod did nothing, exit was 0)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same flags, same source. GNU rsync 3.4.4 produces &lt;code&gt;drwxr-xr-x&lt;/code&gt;. Apple's openrsync produces &lt;code&gt;dr-xr-xr-x&lt;/code&gt;. That single variable explains the entire failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;I filed issue &lt;a href="https://github.com/flutter/flutter/issues/189645" rel="noopener noreferrer"&gt;#189645&lt;/a&gt; with the full root cause analysis, the Apple source audit, the minimal repro, and a proposed fix. Then I opened &lt;a href="https://github.com/flutter/flutter/pull/189658" rel="noopener noreferrer"&gt;PR #189658&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The fix is one idea: don't trust &lt;code&gt;rsync --chmod&lt;/code&gt;. Run &lt;code&gt;chmod -R u+w&lt;/code&gt; on the copied framework path after every &lt;code&gt;rsync&lt;/code&gt; completes. 120 lines added across &lt;code&gt;darwin.dart&lt;/code&gt;, &lt;code&gt;build_swift_package.dart&lt;/code&gt;, and three test files. Zero lines deleted.&lt;/p&gt;

&lt;p&gt;The review took two rounds with two Google engineers. Chris Bracken (&lt;code&gt;@cbracken&lt;/code&gt;) caught that I was using &lt;code&gt;printTrace&lt;/code&gt; on failure and suggested &lt;code&gt;ToolExit&lt;/code&gt; instead. Victoria Ashworth (&lt;code&gt;@vashworth&lt;/code&gt;) pushed back on that: since &lt;code&gt;chmod&lt;/code&gt; is only needed for read-only SDK environments, a build-breaking &lt;code&gt;ToolExit&lt;/code&gt; would punish normal installs. She pointed me to &lt;code&gt;_utils.logger.printWarning&lt;/code&gt; for a non-fatal warning. Both reviewers approved. Auto-submit merged it.&lt;/p&gt;

&lt;p&gt;I learned something from that exchange. My instinct was to fail loudly. Vashworth's instinct was to fail proportionally. The &lt;code&gt;chmod&lt;/code&gt; path matters on Nix and Bazel and hardened CI. On a regular &lt;code&gt;~/flutter&lt;/code&gt; install, it's a no-op that should stay quiet. She was right.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I didn't fix
&lt;/h3&gt;

&lt;p&gt;The existing tests assert that &lt;code&gt;--chmod&lt;/code&gt; is present in the rsync argument list. They don't check that the resulting files are actually writable. That's the test gap that let this regression hide for 10 months across three separate bug reports (#147142, #167823, #189645). I didn't close that gap. My PR tests that &lt;code&gt;chmod&lt;/code&gt; runs after &lt;code&gt;rsync&lt;/code&gt;, which is better, but a true end-to-end assertion on post-copy file modes would be stronger. I left that for a follow-up.&lt;/p&gt;

&lt;p&gt;I also filed the &lt;code&gt;openrsync&lt;/code&gt; &lt;code&gt;--chmod&lt;/code&gt; bug with Apple via Feedback Assistant. No response as of writing. The &lt;code&gt;--chmod&lt;/code&gt; flag is documented in Apple's man page, parses without error, and does nothing. That's been true since March 2025.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contributing to a 170k-star repo
&lt;/h3&gt;

&lt;p&gt;The issue mattered more than the PR. I spent a full day writing #189645: reading Apple's dispatcher source, testing every documented &lt;code&gt;--chmod&lt;/code&gt; form, building the macOS version matrix, linking the three prior issues that got closed as environment-specific. The PR was 120 lines. The issue was the argument that those 120 lines needed to exist.&lt;/p&gt;

&lt;p&gt;Flutter's review process is human. Two reviewers, each with a different concern. One focused on error visibility, the other on blast radius. The CLA bot, the test checker bot, and &lt;code&gt;gemini-code-assist&lt;/code&gt; all showed up too (the bot suggested wrapping &lt;code&gt;chmod&lt;/code&gt; in try-catch, which was actually reasonable). But the decisions were made by two engineers who understood the trade-offs I didn't see from the outside.&lt;/p&gt;

&lt;p&gt;If you're thinking about contributing to a large framework: file the issue first, with a repro that removes your specific setup from the equation. The Nix angle got the previous two reports closed as niche. My repro used nothing but &lt;code&gt;mkdir&lt;/code&gt;, &lt;code&gt;chmod&lt;/code&gt;, and &lt;code&gt;rsync&lt;/code&gt;. The problem stopped being about Nix and started being about macOS.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/flutter/flutter/pull/189658" rel="noopener noreferrer"&gt;PR #189658&lt;/a&gt; · &lt;a href="https://github.com/flutter/flutter/issues/189645" rel="noopener noreferrer"&gt;Issue #189645&lt;/a&gt; · &lt;a href="https://github.com/flutter/flutter" rel="noopener noreferrer"&gt;flutter/flutter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>macos</category>
      <category>opensource</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I Built a Flutter Plugin That Generates Crypto Keys Inside Your Phone's Hardware Chip</title>
      <dc:creator>Sagar Ghag</dc:creator>
      <pubDate>Thu, 05 Nov 2026 14:06:36 +0000</pubDate>
      <link>https://dev.to/ghagsagar23/i-built-a-flutter-plugin-that-generates-crypto-keys-inside-your-phones-hardware-chip-41eg</link>
      <guid>https://dev.to/ghagsagar23/i-built-a-flutter-plugin-that-generates-crypto-keys-inside-your-phones-hardware-chip-41eg</guid>
      <description>&lt;p&gt;Flutter has no native way to generate a cryptographic key inside the device's hardware security chip and sign something with it. You can store keys in the Secure Enclave on iOS or StrongBox on Android, but you have to write Swift and Kotlin yourself, bridge it through method channels, handle the three different signature formats each platform returns, and then figure out how to make the result usable for Ethereum's account abstraction standards. I spent 8 weeks doing exactly that, and the result is &lt;a href="https://pub.dev/packages/chainkey" rel="noopener noreferrer"&gt;&lt;code&gt;chainkey&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual problem
&lt;/h3&gt;

&lt;p&gt;I was building a Web3 authentication flow in Flutter. The requirement: seedless wallets. No mnemonic phrase, no "write down these 12 words." The private key lives in the Secure Enclave (iOS) or StrongBox (Android) and never leaves the chip. Every signature requires biometric presence: Face ID, Touch ID, or fingerprint.&lt;/p&gt;

&lt;p&gt;No Flutter package did this. The closest options handled key storage in software (Dart-side, exportable) or wrapped a single platform. None handled all three targets (iOS, Android, Web) behind one API, and none produced signatures in a format Ethereum smart contracts could verify.&lt;/p&gt;

&lt;h3&gt;
  
  
  What chainkey does
&lt;/h3&gt;

&lt;p&gt;Five coordinated packages. One umbrella (&lt;code&gt;chainkey&lt;/code&gt;), one interface (&lt;code&gt;chainkey_platform_interface&lt;/code&gt;), three platform implementations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;chainkey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Chainkey&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Generate a P-256 key inside the hardware enclave&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;chainkey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;generateHardwareKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;keyAlias:&lt;/span&gt; &lt;span class="s"&gt;'my_wallet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;requireUserPresence:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// biometric on every sign&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Sign a 32-byte hash (triggers Face ID / fingerprint)&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;chainkey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;signWithHardwareKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;keyAlias:&lt;/span&gt; &lt;span class="s"&gt;'my_wallet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;data:&lt;/span&gt; &lt;span class="n"&gt;transactionHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// signature.r and signature.s are already low-S normalized&lt;/span&gt;
&lt;span class="c1"&gt;// Ready for EIP-7212 precompile or ERC-4337 UserOperation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key never leaves the chip. &lt;code&gt;requireUserPresence: true&lt;/code&gt; means the OS prompts for biometric auth on every &lt;code&gt;sign&lt;/code&gt; call. You can customize the biometric prompt text on both platforms (iOS &lt;code&gt;LAContext.localizedReason&lt;/code&gt;, Android &lt;code&gt;BiometricPrompt.PromptInfo&lt;/code&gt; with title, subtitle, negative button).&lt;/p&gt;

&lt;h3&gt;
  
  
  The hard part: signature format hell
&lt;/h3&gt;

&lt;p&gt;This is the thing that ate most of the 8 weeks. Each platform returns signatures in a different format:&lt;/p&gt;

&lt;p&gt;Apple's Security framework gives you raw SEC1 point coordinates. Android's KeyStore gives you ASN.1 DER-encoded bytes. Ethereum expects 64 bytes: r concatenated with s, low-S normalized per BIP-62/EIP-2.&lt;/p&gt;

&lt;p&gt;I built four converters to bridge these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ASN.1 DER unpacker (parses the TLV structure Android returns)&lt;/li&gt;
&lt;li&gt;SEC1 point formatter (handles compressed and uncompressed public keys)&lt;/li&gt;
&lt;li&gt;SPKI parser (extracts the raw key from the SubjectPublicKeyInfo wrapper)&lt;/li&gt;
&lt;li&gt;Low-S normalizer (if s &amp;gt; n/2, replace with n - s)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four are validated against Wycheproof test vectors and the EIP-7212 reference. The Wycheproof vectors specifically caught an edge case in the DER parser where a leading zero byte in &lt;code&gt;r&lt;/code&gt; was being included in the output, producing a 33-byte value instead of 32. That would silently fail on-chain with no error message.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebAuthn passkeys (v0.4.0)
&lt;/h3&gt;

&lt;p&gt;v0.4.0 added a full WebAuthn passkey engine across all three platforms: iOS &lt;code&gt;AuthenticationServices&lt;/code&gt;, Android &lt;code&gt;CredentialManager&lt;/code&gt;, and Web &lt;code&gt;navigator.credentials&lt;/code&gt; via &lt;code&gt;dart:js_interop&lt;/code&gt;. Registration and assertion, with a binary &lt;code&gt;AuthenticatorData&lt;/code&gt; parser and a zero-copy UTF-8 &lt;code&gt;ClientDataJSON&lt;/code&gt; scanner that extracts the &lt;code&gt;challenge&lt;/code&gt; field at the byte offsets Solady and Safe contracts expect.&lt;/p&gt;

&lt;p&gt;I also wrote a CLI tool (&lt;code&gt;dart run chainkey:verify_domain&lt;/code&gt;) that validates your &lt;code&gt;apple-app-site-association&lt;/code&gt; and &lt;code&gt;assetlinks.json&lt;/code&gt; files for WebAuthn RP binding. Getting those wrong is silent on iOS (the passkey creation just fails with a generic error) and debugging it from the Dart side is painful.&lt;/p&gt;

&lt;h3&gt;
  
  
  The signature serializers (v0.5.0)
&lt;/h3&gt;

&lt;p&gt;The latest release added three contract-specific encoders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;encodeSafePasskeySignature()&lt;/code&gt; for Safe 4337 WebAuthn Signer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;encodeErc7579WebAuthnSignature()&lt;/code&gt; for ZeroDev Kernel v3 and Biconomy Nexus&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;encodeEip7212Payload()&lt;/code&gt; for the secp256r1 precompile (160-byte payload)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These exist because every smart account implementation expects a slightly different ABI encoding for the same underlying &lt;code&gt;(r, s, authenticatorData, clientDataJSON)&lt;/code&gt; tuple. Getting the encoding wrong produces a signature that looks valid locally but reverts on-chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;p&gt;Signing latency matters because it's user-facing (the biometric prompt appears and the user waits). The integration test suite measures pure crypto latency with biometric disabled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;P50: 42ms&lt;/li&gt;
&lt;li&gt;KPI target: under 350ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;42ms for a hardware enclave round-trip is fast enough that the user never perceives the signing step. The biometric prompt itself takes longer.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I haven't solved
&lt;/h3&gt;

&lt;p&gt;The package is pre-1.0. The roadmap has 14 open issues across two epics. Key rotation (what happens when the user gets a new phone and the old Secure Enclave key is gone) is the biggest unsolved problem. The current answer is "your smart contract wallet has a recovery mechanism, and that's outside the scope of this package." I'm not happy with that answer but it's honest.&lt;/p&gt;

&lt;p&gt;I also chose P-256 over secp256k1. This was deliberate: P-256 aligns with WebAuthn and the EIP-7212 precompile, which is gaining L2 adoption. But it means chainkey does not work with legacy EOA wallets. Hardware enclaves on iOS and Android don't support secp256k1 at all, so this wasn't really a choice.&lt;/p&gt;

&lt;p&gt;160/160 on pub.dev. 77 commits. 12,747 lines across Dart, Kotlin, and Swift. Apache-2.0.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pub.dev/packages/chainkey" rel="noopener noreferrer"&gt;pub.dev/packages/chainkey&lt;/a&gt;. Source at &lt;a href="https://github.com/GhagSagar23/chainkey" rel="noopener noreferrer"&gt;github.com/GhagSagar23/chainkey&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>web3</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a Face Liveness Detection Package for Flutter. 4 Other Developers Forked It Into Their Own Products.</title>
      <dc:creator>Sagar Ghag</dc:creator>
      <pubDate>Thu, 05 Nov 2026 13:20:42 +0000</pubDate>
      <link>https://dev.to/ghagsagar23/i-built-a-face-liveness-detection-package-for-flutter-4-other-developers-forked-it-into-their-own-40o9</link>
      <guid>https://dev.to/ghagsagar23/i-built-a-face-liveness-detection-package-for-flutter-4-other-developers-forked-it-into-their-own-40o9</guid>
      <description>&lt;p&gt;I needed liveness detection in a Flutter app two years ago. The options were: pay for a commercial SDK (₹8-15L/year for the ones that actually worked on Indian Android hardware), roll a web-based flow through a WebView (latency kills it on 4G), or wire up Google ML Kit's face detection and write the challenge-response logic myself.&lt;/p&gt;

&lt;p&gt;I wrote it myself. That package is &lt;a href="https://pub.dev/packages/livelyness_detection" rel="noopener noreferrer"&gt;&lt;code&gt;livelyness_detection&lt;/code&gt;&lt;/a&gt;, and it now has 40 likes on pub.dev, a 160/160 quality score, and four separate developers have built derivative packages on top of it. One of those, &lt;code&gt;m7_livelyness_detection&lt;/code&gt;, has officially discontinued itself and redirects to mine.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it actually does
&lt;/h3&gt;

&lt;p&gt;The package runs a camera feed through Google ML Kit's face detection, then walks the user through a sequence of active challenges: blink, turn left, turn right, smile. Each challenge has configurable thresholds (&lt;code&gt;BlinkDetectionThreshold&lt;/code&gt;, &lt;code&gt;SmileDetectionThreshold&lt;/code&gt;) so you can tune sensitivity per use case. The detection screen is a full-bleed camera preview with a frosted oval guide and an animated progress ring. One function call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;CapturedImage&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;LivelynessDetection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;config:&lt;/span&gt; &lt;span class="n"&gt;DetectionConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;steps:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="n"&gt;LivelynessStepItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;step:&lt;/span&gt; &lt;span class="n"&gt;LivelynessStep&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;blink&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;LivelynessStepItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;step:&lt;/span&gt; &lt;span class="n"&gt;LivelynessStep&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;turnLeft&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;LivelynessStepItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;step:&lt;/span&gt; &lt;span class="n"&gt;LivelynessStep&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;smile&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nl"&gt;startWithInfoScreen:&lt;/span&gt; &lt;span class="kc"&gt;true&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;It pops a &lt;code&gt;CapturedImage&lt;/code&gt; or &lt;code&gt;null&lt;/code&gt; if the user cancels. The captured image includes which steps completed and, since v2.1.0, a &lt;code&gt;PadResult&lt;/code&gt; with the passive anti-spoofing verdict.&lt;/p&gt;

&lt;h3&gt;
  
  
  Passive anti-spoofing, and why Phase 1 isn't enough
&lt;/h3&gt;

&lt;p&gt;v2.1.0 added passive anti-spoofing (PAD). The idea: a real face has involuntary micro-movements. Head euler angles drift, eye-open probability jitters, the bounding box wobbles. A printed photo held still has near-zero variance across all of these. The analyzer watches a 45-frame sliding window (about 1.5 seconds at 30fps) and flags faces with flat variance as spoofs.&lt;/p&gt;

&lt;p&gt;It catches flat printed photos reliably. It does not catch a hand-held printed photo, because your hand introduces enough jitter to pass. I put that in the security notes on day one because I've seen packages claim "anti-spoofing" without qualifying what that means. Phase 1 is a floor, not a ceiling.&lt;/p&gt;

&lt;p&gt;v2.2.0 added Phase 2: an opt-in TFLite two-model ensemble (MiniFASNet at crop scales 2.7 and 4.0) that runs over a method channel. Both the heuristic check and the TFLite score have to pass. Either failing is a spoof. Fail-closed: if the TFLite model fails to load or score, the verdict is spoof, not inconclusive. I haven't tested this against screen replays or 3D masks yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  The adoption I didn't plan for
&lt;/h3&gt;

&lt;p&gt;I published the first version because I needed it for a client project. The adoption came from GitHub search results and pub.dev visibility. I didn't write blog posts or pitch it in Flutter communities. Some numbers as of November 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;19 independent repositories on GitHub import the package (GitHub code search for &lt;code&gt;livelyness_detection&lt;/code&gt; in &lt;code&gt;pubspec.yaml&lt;/code&gt;, excluding my own repos)&lt;/li&gt;
&lt;li&gt;24 forks&lt;/li&gt;
&lt;li&gt;13 external contributors have opened issues or PRs&lt;/li&gt;
&lt;li&gt;10 closed issues filed by developers I've never met, reporting real bugs from production use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four derivative packages exist on pub.dev. The one that matters most is &lt;code&gt;m7_livelyness_detection&lt;/code&gt;, which was a maintained fork for a while. Its pub.dev page now reads "DISCONTINUED" with a redirect to &lt;code&gt;livelyness_detection&lt;/code&gt;. I didn't ask for that. The maintainer decided the fork wasn't worth maintaining separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 160/160 score
&lt;/h3&gt;

&lt;p&gt;pub.dev's scoring is automated: 30/30 Dart file conventions, 20/20 documentation, 20/20 platform support, 50/50 static analysis, 40/40 up-to-date dependencies. The score doesn't mean the package is good. It means the public API is documented, the analyzer is clean, null safety is correct, and platform declarations match actual platform support. I've seen packages with perfect scores that solve no real problem, and packages with 80/160 that I'd trust in production.&lt;/p&gt;

&lt;p&gt;What the score does tell you: I maintained it. The &lt;code&gt;40/40&lt;/code&gt; on dependencies means I kept them current across 7 versions over two years, which is the part most open-source packages quietly stop doing.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd do differently
&lt;/h3&gt;

&lt;p&gt;The name has a typo. &lt;code&gt;livelyness&lt;/code&gt; isn't a word; &lt;code&gt;liveliness&lt;/code&gt; is. I didn't catch it before the first pub.dev publish, and pub.dev package names are immutable. Every import, every &lt;code&gt;pubspec.yaml&lt;/code&gt; entry, every GitHub search carries the misspelling. I thought about publishing under a corrected name and deprecating the original, but 19 repos and 4 derivative packages reference the current name. The cost of a rename is higher than the cost of the typo.&lt;/p&gt;

&lt;p&gt;The v1 API used a builder pattern that made simple cases verbose. v2.0.0 was a breaking rewrite to a single &lt;code&gt;detect()&lt;/code&gt; call. Breaking changes in a package with active users are expensive to ship even when they're right. I should have started with the simpler API.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's next
&lt;/h3&gt;

&lt;p&gt;Phase 2 TFLite is opt-in and I want to make it good enough to be the default. That means testing against screen replays (a phone showing a video of a face) and cheaper 3D masks. The MiniFASNet ensemble handles printed photos well but I don't have benchmark numbers for video replays yet.&lt;/p&gt;

&lt;p&gt;The package is at &lt;a href="https://pub.dev/packages/livelyness_detection" rel="noopener noreferrer"&gt;pub.dev/packages/livelyness_detection&lt;/a&gt;. Source is at &lt;a href="https://github.com/Kryonex-Labs/livelyness_detection" rel="noopener noreferrer"&gt;github.com/Kryonex-Labs/livelyness_detection&lt;/a&gt;. Issues and PRs welcome.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>biometrics</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built ai_guardrails: Local-First Safety for Dart and Flutter AI Apps</title>
      <dc:creator>Sagar Ghag</dc:creator>
      <pubDate>Fri, 11 Sep 2026 12:12:50 +0000</pubDate>
      <link>https://dev.to/ghagsagar23/i-built-aiguardrails-local-first-safety-for-dart-and-flutter-ai-apps-2jc1</link>
      <guid>https://dev.to/ghagsagar23/i-built-aiguardrails-local-first-safety-for-dart-and-flutter-ai-apps-2jc1</guid>
      <description>&lt;p&gt;AI features in Dart and Flutter apps usually start with a provider call. That is also where a few uncomfortable questions begin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does a user prompt contain an email address, card number, or API key?&lt;/li&gt;
&lt;li&gt;Is retrieved RAG content trying to override the system prompt?&lt;/li&gt;
&lt;li&gt;Is streamed output about to leak a secret or produce unsafe tool-call arguments?&lt;/li&gt;
&lt;li&gt;Can we add these protections without sending user data to another service?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built &lt;a href="https://pub.dev/packages/ai_guardrails" rel="noopener noreferrer"&gt;&lt;code&gt;ai_guardrails&lt;/code&gt;&lt;/a&gt; to make those checks a small, local layer around any LLM integration.&lt;/p&gt;

&lt;p&gt;It is a pure-Dart, provider-agnostic package for Dart and Flutter. It has no runtime dependencies and does not make network calls by itself. Use it with OpenAI, Gemini, Anthropic, a local model, or your own HTTP gateway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic idea
&lt;/h2&gt;

&lt;p&gt;Create a scanner chain for input and output, then wrap the call you already have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:ai_guardrails/ai_guardrails.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AiGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;inputScanners:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;PiiScanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;action:&lt;/span&gt; &lt;span class="n"&gt;GuardAction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redact&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;SecretScanner&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;PromptInjectionScanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;threshold:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;InvisibleTextScanner&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nl"&gt;outputScanners:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;RepetitionScanner&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;SchemaValidator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="s"&gt;'type'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'required'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'answer'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="s"&gt;'properties'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;'answer'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'type'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'string'&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;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;input:&lt;/span&gt; &lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;llmCall:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sanitizedInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;myLlm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sanitizedInput&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Blocked at &lt;/span&gt;&lt;span class="si"&gt;${outcome.blockedStage}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;${outcome.blockReason}&lt;/span&gt;&lt;span class="s"&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;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redacting scanners run in order, so the model receives the final sanitized input. If an input scanner blocks, the provider call never happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it covers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ai_guardrails&lt;/code&gt; includes local heuristic scanners for common LLM-risk surfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PII detection and redaction across US, EU, India, Brazil, Mexico, Japan, South Korea, Canada, and Australia&lt;/li&gt;
&lt;li&gt;Secret detection for API keys, tokens, JWTs, and private-key blocks&lt;/li&gt;
&lt;li&gt;Prompt-injection, Unicode-smuggling, suspicious URL, and padding-attack detection&lt;/li&gt;
&lt;li&gt;Generated-code, SQL, HTML, JSON, URL, numeric-range, and choice validation&lt;/li&gt;
&lt;li&gt;Tool-call validation and tool-output scanning for agentic workflows&lt;/li&gt;
&lt;li&gt;RAG retrieval scanning, streaming response scanning, multi-turn escalation, and declarative conversation flows&lt;/li&gt;
&lt;li&gt;Policy profiles, JSON configuration, audit logs, metrics, OpenTelemetry-compatible tracing, benchmarking, and red-team probing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package also supports optional async scanners. Semantic checks such as fact checking, topic safety, hallucination checking, and embedding grounding use callbacks you provide, so the core package stays provider-neutral.&lt;/p&gt;

&lt;h2&gt;
  
  
  PII without sacrificing a natural response
&lt;/h2&gt;

&lt;p&gt;For many chat experiences, redacting PII before it leaves the device is the safe choice—but users still expect a natural response. &lt;code&gt;ai_guardrails&lt;/code&gt; supports a round trip for that case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AiGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;inputScanners:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PiiScanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;action:&lt;/span&gt; &lt;span class="n"&gt;GuardAction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redact&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;input:&lt;/span&gt; &lt;span class="s"&gt;'Email alice@example.com about the release.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;llmCall:&lt;/span&gt; &lt;span class="n"&gt;myLlm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;complete&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 model sees: "Email [EMAIL_1] about the release."&lt;/span&gt;
&lt;span class="c1"&gt;// outcome.output can restore the placeholder for the app.&lt;/span&gt;
&lt;span class="c1"&gt;// outcome.rawOutput preserves the model's pre-rehydration response.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That behavior is deliberate and should be used only where returning the original value to the user is appropriate. The redaction map and raw output remain available when an application needs tighter control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designed for the pipeline, not just a single prompt
&lt;/h2&gt;

&lt;p&gt;LLM safety does not end at a chat textbox. The package has first-class stages for the inputs an application assembles around a model call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Drop poisoned or unsafe RAG chunks before prompt assembly.&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;retrieval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;runRetrievalStage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrievedChunks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Scan untrusted tool results before returning them to the model.&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;toolResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;runToolOutputStage&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="n"&gt;ToolOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;toolName:&lt;/span&gt; &lt;span class="s"&gt;'search'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;content:&lt;/span&gt; &lt;span class="n"&gt;untrustedSearchResult&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 streaming applications, &lt;code&gt;StreamingAiGuard&lt;/code&gt; scans complete response segments as they arrive and ends the stream when a scanner blocks. For whole-document rules such as JSON schema validation, run a final full-output pass after the stream completes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this package is—and is not
&lt;/h2&gt;

&lt;p&gt;The built-in local scanners are deterministic heuristics. They are intentionally fast, offline, and inexpensive, but they do not understand every meaning, evasion, or cultural context. Treat them as defense in depth, not as a compliance certification or a substitute for application authorization.&lt;/p&gt;

&lt;p&gt;For semantic checks, the package exposes callback boundaries rather than embedding a provider SDK. Your app decides whether to use an on-device model, a private endpoint, or a cloud model—and owns the associated privacy and cost choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart pub add ai_guardrails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package is open source under Apache-2.0. The README includes provider examples, configuration-driven policies, and a full scanner reference.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pub.dev/packages/ai_guardrails" rel="noopener noreferrer"&gt;Package on pub.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/GhagSagar23/ai_guardrails" rel="noopener noreferrer"&gt;Source code and issue tracker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would especially love feedback from teams building Flutter copilots, RAG search, local-model experiences, and agentic tools. What safety checks are you currently reimplementing in every Dart project?&lt;/p&gt;

</description>
      <category>dart</category>
      <category>flutter</category>
      <category>ai</category>
      <category>security</category>
    </item>
  </channel>
</rss>
