<?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: Naveen Alavilli</title>
    <description>The latest articles on DEV Community by Naveen Alavilli (@naveen_alavilli).</description>
    <link>https://dev.to/naveen_alavilli</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%2F4140078%2Fd351ad91-80c5-45b8-8946-0e59f869e352.png</url>
      <title>DEV Community: Naveen Alavilli</title>
      <link>https://dev.to/naveen_alavilli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/naveen_alavilli"/>
    <language>en</language>
    <item>
      <title>I built a trail accessibility surveyor that runs with the radio off</title>
      <dc:creator>Naveen Alavilli</dc:creator>
      <pubDate>Sun, 11 Oct 2026 11:18:36 +0000</pubDate>
      <link>https://dev.to/naveen_alavilli/i-built-a-trail-accessibility-surveyor-that-runs-with-the-radio-off-3514</link>
      <guid>https://dev.to/naveen_alavilli/i-built-a-trail-accessibility-surveyor-that-runs-with-the-radio-off-3514</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trailworthy&lt;/strong&gt; turns a walk down a trail into an accessibility report a parks department can act on.&lt;/p&gt;

&lt;p&gt;Public trails are supposed to meet the Architectural Barriers Act Accessibility Standard. Most have never been checked against it, and the reason is boring: a survey means a clipboard, a tape measure, a copy of the standard, and someone willing to type it all up afterwards. So the surveys do not happen. And the trails nobody surveys are the trails nobody complains about, because the people who would complain gave up on them a long time ago.&lt;/p&gt;

&lt;p&gt;Trailworthy makes the field half of that job take no more effort than talking.&lt;/p&gt;

&lt;p&gt;At the trailhead you name the trail. On the trail the screen goes dark and there is exactly one button. Hold it, say what you see, let go, put the phone back in your pocket. Nothing is transcribed, nothing is uploaded, nothing asks you to look at it. The only number the app tracks while you walk is how long your screen stayed off.&lt;/p&gt;

&lt;p&gt;Back home you hit develop. Whisper transcribes each note on your device. A sentence embedding model routes each note to the right accessibility criterion. Any measurement you spoke gets checked against the actual threshold. What comes out is a cited report:&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  1. Tread obstacles - Blocking
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Citation&lt;/strong&gt; ABAAS F247.6 / FSTAG 7.7&lt;br&gt;
&lt;strong&gt;Requirement&lt;/strong&gt; Tread obstacles may not exceed 2 in high.&lt;br&gt;
&lt;strong&gt;Measured&lt;/strong&gt; 4 in (100% outside the limit)&lt;br&gt;
&lt;strong&gt;Location&lt;/strong&gt; 29.77860, -95.55140 (±6 m)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a root crosses the trail about four inches high right at the bend&lt;/p&gt;
&lt;/blockquote&gt;


&lt;/blockquote&gt;

&lt;p&gt;It is for the people who already do this work: accessibility advocates, trail volunteers, parks staff with eleven other jobs, and anyone who has turned around on a trail that was advertised as accessible and wants to say so in a form that is hard to ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://naveenalavilli.github.io/trailworthy/" rel="noopener noreferrer"&gt;https://naveenalavilli.github.io/trailworthy/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Load the models once (about 70 MB), then put the device in airplane mode and the whole survey still works. It installs as a PWA.&lt;/p&gt;

&lt;p&gt;A full generated report from a scripted set of notes is in the repo at &lt;a href="https://github.com/naveenalavilli/trailworthy/blob/main/examples/sample-report.md" rel="noopener noreferrer"&gt;&lt;code&gt;examples/sample-report.md&lt;/code&gt;&lt;/a&gt;, if you want to see the output without walking anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/naveenalavilli/trailworthy" rel="noopener noreferrer"&gt;https://github.com/naveenalavilli/trailworthy&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/criteria.js    ABAAS/FSTAG criteria, thresholds, citations, prototype phrasings
src/measure.js     pulls spoken measurements out of a transcript
src/analyze.js     routing, polarity, compliance and severity
src/report.js      the Markdown and printable report
src/worker.js      both models, off the UI thread
scripts/           icon generation, threshold calibration, the polarity experiment
tests/             runs against the real embedding model, not mocked vectors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;Two open-weight models, both in the browser through &lt;a href="https://github.com/huggingface/transformers.js" rel="noopener noreferrer"&gt;transformers.js&lt;/a&gt;, both in a web worker:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Licence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speech to text&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/Xenova/whisper-tiny.en" rel="noopener noreferrer"&gt;&lt;code&gt;Xenova/whisper-tiny.en&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Note classification&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/Xenova/all-MiniLM-L6-v2" rel="noopener noreferrer"&gt;&lt;code&gt;Xenova/all-MiniLM-L6-v2&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Classification is the interesting half. People do not talk in the language of a standard. Nobody stands on a trail and says "the cross slope exceeds 1:20". They say "it leans toward the creek", or "off camber here", or "water's cut a channel across the path". Those share no keywords with each other or with the standard, but they sit close together in embedding space. Each criterion carries a handful of example phrasings, everything gets embedded once on load, and each note goes to its nearest neighbour.&lt;/p&gt;

&lt;p&gt;Three things about that turned out to be less obvious than they looked.&lt;/p&gt;

&lt;h3&gt;
  
  
  The classifier needed a floor, and the floor needed data
&lt;/h3&gt;

&lt;p&gt;Nearest-neighbour routing always returns a nearest neighbour. Without a floor, "my daughter found a turtle by the pond" gets filed as a tread obstacle, and the report is junk.&lt;/p&gt;

&lt;p&gt;So I wrote &lt;code&gt;scripts/calibrate.mjs&lt;/code&gt;, which scores a labelled set of real observations against a set of ordinary trail chatter and prints the gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lowest real observation : 0.576
highest chatter         : 0.458
separable               : yes
suggested MATCH_FLOOR   : 0.517
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The threshold is the midpoint, and it is in the repo as a script rather than a magic number in a comment, so anyone editing the phrasings can re-derive it instead of guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embeddings are bad at negation, and that is dangerous here
&lt;/h3&gt;

&lt;p&gt;"Nothing on the sign about the grade" and "the sign lists the grade" are near neighbours. They are topically identical and mean opposite things. The first was being filed as &lt;strong&gt;compliant&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Think about what that means for this particular app. A barrier with no number attached, a missing curb ramp, no bench on a long climb, nothing on the trailhead sign, would land in the "meets the standard" pile precisely &lt;em&gt;because&lt;/em&gt; there was nothing to measure. The report would be confidently, quietly wrong in the one direction it must never be wrong in.&lt;/p&gt;

&lt;p&gt;I split the decision in two. Routing picks the criterion. A separate step decides whether the note describes that criterion being wrong or being fine, by giving each criterion two sets of phrasings, one for each. Then I tested three ways of making that call on hand-labelled notes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nearest prototype:               25/29
mean per side:                   27/29
mean + negation flip:            22/29
shipped (flip unless reassured): 28/29
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix averages similarity across each side rather than trusting the single closest phrase, then treats absence wording ("no", "nothing", "nowhere", "without") as its own signal. It only ever flips toward "barrier", and never for criteria whose compliant wording is itself negative, like "no noticeable cross slope". The experiment is in the repo.&lt;/p&gt;

&lt;p&gt;That first version scored 18/18 on its original set, and it was still wrong. Any "no" or "not" flipped a note, so "the gate is fine, no trouble getting a chair through" became a &lt;em&gt;blocking&lt;/em&gt; barrier. Adding notes where the negation lands on the problem ("not a problem", "no low branches") dropped it to 22/29. Those phrasings now switch the flip off, but never flip a note toward "fine" on their own, so the one remaining miss errs toward a barrier a human strikes out rather than one nobody sees.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebGPU built perfectly and then hung
&lt;/h3&gt;

&lt;p&gt;The obvious setup is WebGPU with a WASM fallback, and the obvious place to catch failure is where the pipeline is constructed. Neither survived a real browser.&lt;/p&gt;

&lt;p&gt;The session built. The warm-up inference returned. Every call after that hung forever. Nothing ever threw, so the fallback never fired, and the progress bar just sat at 100% looking healthy.&lt;/p&gt;

&lt;p&gt;Two changes came out of it. The fallback is now driven by a &lt;strong&gt;timed warm-up inference&lt;/strong&gt;, because a backend that builds is not a backend that works. And WASM is the default: these models are small and the work is many short sequential calls, which WASM does in 1.4 s for the whole prototype set and 31 ms for four notes. WebGPU is behind &lt;code&gt;?gpu=1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There was a fourth, more embarrassing one. I spent a while debugging a fix that had already shipped, because my service worker cached same-origin assets cache-first and was serving my own stale code back to me. That is the standard offline recipe and it pins every user to whatever build they first loaded. Everything the app ships itself is now network-first with a cache fallback, which is still completely offline on a trail but can be fixed afterwards. Two quieter holes turned up in review: a first visit cached almost nothing, because the service worker registers after the scripts load, and the ONNX runtime's 21 MB of wasm came from a CDN by default. The build now hands the service worker a full precache list, and the runtime loads from the app itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;A closed API would transcribe audio more accurately than &lt;code&gt;whisper-tiny.en&lt;/code&gt;. It still could not do this job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trails that most need surveying are the ones with no signal.&lt;/strong&gt; That is not a coincidence. Remote and under-maintained trails are both less likely to have coverage and less likely to have ever been assessed. Every note becomes a failed request exactly where the work matters most. Running the model on the device is not an optimisation here, it is the difference between the app working and not existing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data is unusually personal.&lt;/strong&gt; A survey is a precise record of where a disabled person went, how long they took, where they struggled and where they turned around. Shipping that to someone's servers to get it typed up is a bad trade. On-device inference means I am not asking anyone to trust me, because there is nothing for me to be trusted with. I do not have a server. There is no account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody is funding this.&lt;/strong&gt; This work gets done by volunteers and small advocacy groups. A per-minute transcription bill is one of the reasons it does not happen. Zero marginal cost is not a nice-to-have, it is the thing that makes a hundred surveys possible instead of three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thresholds are arguable, and should be.&lt;/strong&gt; ABAAS has conditions and exceptions, and reasonable people disagree about how a given trail should be read. Everything that encodes judgment here, the thresholds, the phrasings, the severity defaults, lives in one readable file. Fork it, change a number, re-run &lt;code&gt;npm run calibrate&lt;/code&gt;, and see what moved. A hosted classifier would make that an argument with a vendor instead of a pull request.&lt;/p&gt;

&lt;p&gt;The honest summary: open weights are what made the hard constraint (no signal, no server, no cost) satisfiable at all, and open source is what makes the judgment calls contestable by the people who actually use the trails.&lt;/p&gt;

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

&lt;p&gt;It will not tell you a trail is compliant. It reports what it heard you say, against the criterion it thinks you meant, with a confidence number attached, and the report says in plain language that a human has to verify every finding before it goes to an agency. Measurements come from a person and a tape measure, not from a model. An accessibility report that overstates its own certainty is worse than no report, because it gets one person dismissed and then everyone after them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;Entering the overall category. The project uses open-weight models and an open-source runtime rather than any partner's technology, so I am not claiming a partner category I did not use.&lt;/p&gt;




&lt;p&gt;Built in the open for Hacktoberfest Week 1. If you work for a parks department and want a survey of a specific trail, open an issue and I will go walk it.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Access Revoked. Why Can Your RAG App Still Answer?</title>
      <dc:creator>Naveen Alavilli</dc:creator>
      <pubDate>Sun, 04 Oct 2026 17:09:32 +0000</pubDate>
      <link>https://dev.to/naveen_alavilli/access-revoked-why-can-your-rag-app-still-answer-dfh</link>
      <guid>https://dev.to/naveen_alavilli/access-revoked-why-can-your-rag-app-still-answer-dfh</guid>
      <description>&lt;p&gt;At 09:00, a user asks an internal assistant about Project Cedar. The retriever checks access, finds the document, and the model produces an accurate answer.&lt;/p&gt;

&lt;p&gt;At 09:05, the user loses access to that project.&lt;/p&gt;

&lt;p&gt;At 09:06, the same question returns the same answer. The request never reaches the retriever: the answer cache handles it.&lt;/p&gt;

&lt;p&gt;Every retrieval check can be correct while the application still serves restricted information.&lt;/p&gt;

&lt;p&gt;This is a hypothetical design scenario, not a report of a production incident. It exposes a question worth asking during a RAG review: &lt;strong&gt;which copies of source information remain usable after permission changes?&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Follow the information past retrieval
&lt;/h3&gt;

&lt;p&gt;A document can leave several descendants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source document
  -&amp;gt; indexed chunks
  -&amp;gt; retrieved context
  -&amp;gt; generated answer
  -&amp;gt; answer cache
  -&amp;gt; conversation history or summary
  -&amp;gt; exported report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A permission check on the first retrieval does not automatically protect those descendants.&lt;/p&gt;

&lt;p&gt;A citation is also an incomplete dependency record. A generated answer may use a chunk without citing it. Removing the link later does not remove what the answer learned from that chunk.&lt;/p&gt;

&lt;p&gt;For application-controlled artifacts, record the sources supplied to generation, including context inherited from earlier turns. Treat the full supplied set as dependencies unless you have a sound way to establish a smaller set. This is conservative: it may invalidate some answers unnecessarily, but avoids treating a model's citation choices as an access-control decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Specify what revocation means
&lt;/h3&gt;

&lt;p&gt;Before selecting a cache TTL, decide the product contract.&lt;/p&gt;

&lt;p&gt;Does removing a user from a project stop new retrieval only? Does it also stop replay of stored answers and reuse of old conversation context? What happens to requests already running?&lt;/p&gt;

&lt;p&gt;For this example, assume the requirement is: once revocation becomes effective in the application's authorization system, subsequent requests must not serve or reuse protected content from that project.&lt;/p&gt;

&lt;p&gt;Previously downloaded files and information already seen by the user cannot be recalled. Historical conversations may have a separate retention and access policy. State that policy explicitly.&lt;/p&gt;

&lt;p&gt;For in-flight generation, choose and document a rule: authorize against a request-start snapshot, or require authorization again before delivery. A final check reduces exposure but still leaves a check-to-delivery race unless authorization changes and delivery are coordinated. Do not promise instantaneous revocation without implementing the consistency needed to support it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat a cache hit as another read
&lt;/h3&gt;

&lt;p&gt;OWASP recommends validating authorization on every request and denying access by default. Those principles apply when a cache supplies the response too. &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Authorization Cheat Sheet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A useful answer-cache record contains:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tenant and authorization scope&lt;/td&gt;
&lt;td&gt;Prevent accidental reuse across security boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query and generation configuration&lt;/td&gt;
&lt;td&gt;Identify the requested computation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source IDs and content revisions&lt;/td&gt;
&lt;td&gt;Track which inputs produced the answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission revision or epoch&lt;/td&gt;
&lt;td&gt;Detect relevant authorization changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expiration&lt;/td&gt;
&lt;td&gt;Bound retention and ordinary staleness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A user ID in the key separates users, but the same user's permissions can change. A permission epoch helps only if every relevant policy, membership, and document-ACL change advances it, and the request obtains a sufficiently current value. Hashing an old group list does not make it current.&lt;/p&gt;

&lt;p&gt;Here is control-flow pseudocode, not a complete security implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;authenticate request and establish tenant
find candidate cached answer within that tenant

if candidate exists:
    check its complete source dependencies
      against current authorization
    if dependencies are known and access is allowed:
        serve under the chosen delivery policy
    otherwise:
        do not serve the cached body

retrieve currently authorized context
generate a new answer
record all input dependencies
apply the chosen delivery policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unavailable authorization service must not silently turn a denied or unknown decision into permission to serve an old answer. A safe outcome may be an explicit temporary failure. A system can also answer from independently authorized public content, provided it does not reuse the restricted context.&lt;/p&gt;

&lt;h3&gt;
  
  
  The index has its own clock
&lt;/h3&gt;

&lt;p&gt;Refreshing the answer cache does not fix stale permissions in the index.&lt;/p&gt;

&lt;p&gt;Azure AI Search's query-time ACL/RBAC documentation, currently marked preview, explains that ACL freshness depends on ingestion. Its documented update paths include reingestion for custom push ingestion and resynchronization for particular indexer scenarios. &lt;a href="https://learn.microsoft.com/en-us/azure/search/search-query-access-control-rbac-enforcement" rel="noopener noreferrer"&gt;Microsoft: Query-time ACL and RBAC enforcement&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That creates separate delays to measure: identity/group resolution, permission propagation into the index, authorization-cache freshness, and invalidation of derived answers.&lt;/p&gt;

&lt;p&gt;A five-minute answer TTL does not establish a five-minute revocation bound if the authorization data used to regenerate that answer remains stale for longer.&lt;/p&gt;

&lt;p&gt;For stricter requirements, evaluate whether candidate results need a check against an authoritative permission service before their content reaches the model. The right design depends on the source's consistency guarantees and the latency budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the warm path and the resumed conversation
&lt;/h3&gt;

&lt;p&gt;Use synthetic documents with a distinctive marker, two test users, and controlled permission changes. Make the authorization state observable so the test knows when revocation is effective.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Experiment&lt;/th&gt;
&lt;th&gt;Expected result under this example's contract&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Warm the answer cache, revoke access, repeat the question&lt;/td&gt;
&lt;td&gt;Cached protected body is not served&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ask a paraphrase that hits a semantic cache&lt;/td&gt;
&lt;td&gt;Same authorization check applies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revoke access, then resume an old conversation&lt;/td&gt;
&lt;td&gt;Restricted chunks and derived summaries are not reused as context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep the search index deliberately stale&lt;/td&gt;
&lt;td&gt;Unauthorized context does not reach generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make authorization unavailable&lt;/td&gt;
&lt;td&gt;No fallback to the previously allowed answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leave a control user authorized&lt;/td&gt;
&lt;td&gt;The control user can still retrieve and answer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Inspect the retrieved context and outgoing model payload, not just the final response. A model declining to repeat the marker does not prove it never received the protected text. Check stored summaries and citation metadata too.&lt;/p&gt;

&lt;p&gt;For a revoked source inside a mixed answer, dropping one citation is insufficient. Regenerate from permitted inputs or withhold the derived artifact. Without trustworthy dependency tracking, rebuilding the conversation context may be safer than attempting selective cleanup.&lt;/p&gt;

&lt;p&gt;The architecture review question is concrete: &lt;strong&gt;after a permission change, which component stops the next cached answer, follow-up turn, and export from using the old access decision?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>rag</category>
      <category>security</category>
      <category>architecture</category>
      <category>ai</category>
    </item>
    <item>
      <title>Two Agents, One Record: Stop Silent Lost Updates</title>
      <dc:creator>Naveen Alavilli</dc:creator>
      <pubDate>Sun, 04 Oct 2026 02:09:26 +0000</pubDate>
      <link>https://dev.to/naveen_alavilli/two-agents-one-record-stop-silent-lost-updates-4god</link>
      <guid>https://dev.to/naveen_alavilli/two-agents-one-record-stop-silent-lost-updates-4god</guid>
      <description>&lt;p&gt;Two agents read the same support ticket. One corrects its customer-visible summary. The other changes its priority.&lt;/p&gt;

&lt;p&gt;Both send valid updates. Both receive success responses.&lt;/p&gt;

&lt;p&gt;The final record has the new priority and the old summary.&lt;/p&gt;

&lt;p&gt;No timeout. No duplicate request. One successful write silently erased another.&lt;/p&gt;

&lt;p&gt;This is a hypothetical design example. The same race can happen between an agent and a person using an ordinary web form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the interleaving
&lt;/h2&gt;

&lt;p&gt;Assume the API replaces the editable fields with the submitted document:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Agent A&lt;/th&gt;
&lt;th&gt;Agent B&lt;/th&gt;
&lt;th&gt;Stored version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Reads ticket&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Reads ticket&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Saves corrected summary&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Saves priority using its old copy&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;B's payload still contains the original summary. Each request makes sense when considered alone. Together, they lose A's work.&lt;/p&gt;

&lt;p&gt;A unique operation ID would distinguish the two writes, but both are intentional operations. Duplicate suppression does not decide whether B's starting point is still valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the starting version part of the write
&lt;/h2&gt;

&lt;p&gt;A useful update contract says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Apply these changes only if the resource still matches the version I read.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For an HTTP API that supports conditional updates, the client can retain the strong ETag returned with a read and supply it in &lt;code&gt;If-Match&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /tickets/42

&lt;/span&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;ETag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"ticket-42-v12"&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Original summary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"normal"&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;A's update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;PUT /tickets/42
If-Match: "ticket-42-v12"
Content-Type: application/json

{"summary":"Corrected summary","priority":"normal"}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After A succeeds, B's different update using the old tag fails the precondition. In this scenario, the server responds with &lt;code&gt;412 Precondition Failed&lt;/code&gt; and leaves A's version intact.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-13.1.1" rel="noopener noreferrer"&gt;RFC 9110, section 13.1.1&lt;/a&gt; defines &lt;code&gt;If-Match&lt;/code&gt;, requires strong entity-tag comparison, and describes its use in preventing lost updates. The tag above is illustrative; clients should treat the server's tag as opaque.&lt;/p&gt;

&lt;p&gt;The application must actually support and enforce this contract. Adding a header to a client cannot fix a server that ignores it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforce it where the mutation happens
&lt;/h2&gt;

&lt;p&gt;For a versioned database record, the core operation can look like this PostgreSQL-style statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The version predicate and mutation belong in the same atomic operation. A separate version check followed by an unconditional update reintroduces the race.&lt;/p&gt;

&lt;p&gt;This fragment illustrates the concurrency check; the endpoint also needs authorization and validation. No returned row means the conditional write did not happen. It does not, by itself, distinguish a stale version from a missing record or a scope mismatch.&lt;/p&gt;

&lt;p&gt;Every writer must follow the version contract, including admin scripts and background jobs. One unconditional writer can bypass the protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  A conflict needs a decision
&lt;/h2&gt;

&lt;p&gt;The dangerous recovery is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive a conflict.&lt;/li&gt;
&lt;li&gt;Fetch the latest version token.&lt;/li&gt;
&lt;li&gt;Attach it to the original stale payload.&lt;/li&gt;
&lt;li&gt;Retry.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sequence can turn a detected conflict back into a lost update. The token becomes newer while the reasoning remains stale.&lt;/p&gt;

&lt;p&gt;Instead, reload the current resource and compare it with the original observation and intended change.&lt;/p&gt;

&lt;p&gt;In this example, B wanted only to raise the priority. A policy might permit reapplying that narrow change to the current record, preserving A's summary, then submitting it conditionally again.&lt;/p&gt;

&lt;p&gt;But different fields do not automatically imply independent decisions. A changed summary might explain why raising the priority is no longer appropriate. Domain rules determine whether to rebase, recompute, or ask for review.&lt;/p&gt;

&lt;p&gt;A bounded retry policy also needs a stopping condition. Persistent contention should surface as a conflict the user can understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the race deliberately
&lt;/h2&gt;

&lt;p&gt;Avoid a test that merely launches two requests and hopes they overlap. Use a barrier or controlled sequence so both clients read version 12 before either writes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A writes, then B submits a different update based on the same old version&lt;/td&gt;
&lt;td&gt;B is rejected; A's data remains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B reloads and reapplies an allowed narrow change&lt;/td&gt;
&lt;td&gt;Both intended changes survive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B fetches a new token but reuses its old whole-document payload&lt;/td&gt;
&lt;td&gt;Recovery test fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A background writer changes the record&lt;/td&gt;
&lt;td&gt;Its write also advances the version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission is revoked between read and write&lt;/td&gt;
&lt;td&gt;Execution is denied independently of version validity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are proposed checks, not reported benchmark results.&lt;/p&gt;

&lt;p&gt;For rules spanning multiple records, a version check on one row may be insufficient. Protect the full invariant with an appropriate transaction, constraint, or coordination mechanism.&lt;/p&gt;

&lt;p&gt;The design question to bring to your next review is simple: &lt;strong&gt;what prevents a correct action based on yesterday's view from overwriting today's work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: This article was generated and published by an AI assistant at the account owner's request. The scenarios are illustrative.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>agents</category>
      <category>api</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your Agent Timed Out. Did the Action Still Happen?</title>
      <dc:creator>Naveen Alavilli</dc:creator>
      <pubDate>Sat, 03 Oct 2026 17:55:58 +0000</pubDate>
      <link>https://dev.to/naveen_alavilli/your-agent-timed-out-did-the-action-still-happen-n7b</link>
      <guid>https://dev.to/naveen_alavilli/your-agent-timed-out-did-the-action-still-happen-n7b</guid>
      <description>&lt;p&gt;An agent submits a request to publish a document. The server commits it. The response disappears before the agent receives it.&lt;/p&gt;

&lt;p&gt;The agent sees a timeout and tries again.&lt;/p&gt;

&lt;p&gt;You now have two published documents and a log that describes a successful recovery.&lt;/p&gt;

&lt;p&gt;This is a design scenario, not a benchmark result. It exposes a question worth asking before giving an agent tools that change external state: &lt;strong&gt;what does your system do when it cannot tell whether an action happened?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A timeout establishes that the caller did not receive a timely result. It does not establish that the remote operation failed. The &lt;a href="https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/" rel="noopener noreferrer"&gt;Amazon Builders' Library discussion of idempotent APIs&lt;/a&gt; explains this ambiguity and the use of caller-provided request identifiers to make retries safe.&lt;/p&gt;

&lt;p&gt;For agent workflows, I'd make that uncertainty explicit in the application contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give uncertainty a state
&lt;/h2&gt;

&lt;p&gt;A boolean &lt;code&gt;success&lt;/code&gt; field cannot describe every outcome of a remote write. Consider these states:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready&lt;/td&gt;
&lt;td&gt;The operation is authorized and durably recorded&lt;/td&gt;
&lt;td&gt;Dispatch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In flight&lt;/td&gt;
&lt;td&gt;A worker has claimed it; it may have reached the destination&lt;/td&gt;
&lt;td&gt;Await evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Succeeded&lt;/td&gt;
&lt;td&gt;The destination has confirmed the required outcome&lt;/td&gt;
&lt;td&gt;Return the stored result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed&lt;/td&gt;
&lt;td&gt;There is conclusive evidence it did not take effect&lt;/td&gt;
&lt;td&gt;Apply the failure policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;It may have taken effect, but evidence is missing&lt;/td&gt;
&lt;td&gt;Reconcile&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An expired worker lease also deserves attention: the worker might have crashed after the remote commit. Recovery must account for that possibility.&lt;/p&gt;

&lt;p&gt;The UI can say: “The request was submitted, but completion is unconfirmed. Checking its status.” That is a useful result, even when it is less satisfying than “Done.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Identify the operation before calling the model
&lt;/h2&gt;

&lt;p&gt;A tool-call ID identifies an attempt. The application needs an identity for the logical operation across attempts.&lt;/p&gt;

&lt;p&gt;For a hypothetical document workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation_id: op_8c21
actor_id: user_42
action: publish_document
target_id: doc_104
content_version: 7
request_fingerprint: &amp;lt;hash of canonical action parameters&amp;gt;
state: ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create and persist that identity in application code. Carry it through retries and restarts. If the destination supports an idempotency key, pass the same key for the same operation according to that API's contract.&lt;/p&gt;

&lt;p&gt;The request fingerprint has a different job: detect changed parameters. Reusing an operation ID with a different destination or payload should produce a conflict. A hash alone cannot express intent; two intentional operations can have identical payloads.&lt;/p&gt;

&lt;p&gt;Scope identifiers to the actor or tenant, enforce uniqueness atomically, and check the provider's retention window. An old key may stop protecting a retry after the destination expires its record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep authorization attached to the proposed action
&lt;/h2&gt;

&lt;p&gt;If a workflow requires approval, record what was approved: action, target, payload version, and applicable limits.&lt;/p&gt;

&lt;p&gt;A retry of that exact operation can reuse the recorded authorization where policy permits. A model that changes the payload has proposed a different action. It should not silently inherit approval for the old one.&lt;/p&gt;

&lt;p&gt;Recheck current permissions at execution time too. A durable approval record should not bypass a later revocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A local ledger cannot close a remote transaction
&lt;/h2&gt;

&lt;p&gt;The difficult sequence is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Record the operation locally.
2. Send the remote write.
3. The destination commits.
4. The worker crashes before recording the result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A local transaction cannot make steps 2 and 4 atomic with an unrelated service.&lt;/p&gt;

&lt;p&gt;The recovery path depends on the destination:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With suitable idempotency support, retry the same operation under that contract.&lt;/li&gt;
&lt;li&gt;With an authoritative lookup by operation ID, query it and reconcile the result.&lt;/li&gt;
&lt;li&gt;With neither, retain the unknown outcome and route it for investigation before attempting another consequential write.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An empty search result may be inconclusive if the destination is eventually consistent. A durable queue improves delivery, but the consumer still needs a strategy for duplicate attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the inconvenient boundary
&lt;/h2&gt;

&lt;p&gt;Before shipping, exercise these cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Injected condition&lt;/th&gt;
&lt;th&gt;Expected behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Response lost after remote commit&lt;/td&gt;
&lt;td&gt;Recovery resolves to the original result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two workers dispatch the same operation&lt;/td&gt;
&lt;td&gt;The destination applies one logical effect under its idempotency contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same ID, different payload&lt;/td&gt;
&lt;td&gt;Conflict before another effect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker dies after remote commit&lt;/td&gt;
&lt;td&gt;Operation enters reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lookup is temporarily stale&lt;/td&gt;
&lt;td&gt;No premature claim that nothing happened&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency retention expires&lt;/td&gt;
&lt;td&gt;Retry policy accounts for the lost protection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are tests for the surrounding application, not prompts asking the model to be more careful.&lt;/p&gt;

&lt;p&gt;A practical design review question is: &lt;strong&gt;if this write succeeds and its response vanishes, what evidence lets the next worker decide what to do?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is only “the agent will figure it out,” the recovery protocol is still unfinished.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>agents</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Built Nabsun: An Open-Source AI Browser That Works in Your Real Tabs</title>
      <dc:creator>Naveen Alavilli</dc:creator>
      <pubDate>Fri, 25 Sep 2026 00:57:06 +0000</pubDate>
      <link>https://dev.to/naveen_alavilli/i-built-nabsun-an-open-source-ai-browser-that-works-in-your-real-tabs-1ea3</link>
      <guid>https://dev.to/naveen_alavilli/i-built-nabsun-an-open-source-ai-browser-that-works-in-your-real-tabs-1ea3</guid>
      <description>&lt;p&gt;You have three documentation tabs open, a form half-filled, and the account you need already signed in.&lt;/p&gt;

&lt;p&gt;Then you ask an AI assistant for help—and start copying everything into a chat window.&lt;/p&gt;

&lt;p&gt;That gap is why I built &lt;strong&gt;Nabsun&lt;/strong&gt;, an open-source browser with an AI assistant built in. It can read the page you are looking at, compare information across tabs, and use browser tools to help complete a task in your existing session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works where your work already is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nabsun is built with Chromium and Electron. You can use its bundled local model, connect a cloud provider, or bring Codex or Claude Code into the browser. It is a developer preview, and the source is available under MIT.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nabsun.com" rel="noopener noreferrer"&gt;Website&lt;/a&gt; · &lt;a href="https://github.com/naveenalavilli/nabsun" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; · &lt;a href="https://github.com/naveenalavilli/nabsun/releases/latest" rel="noopener noreferrer"&gt;Download&lt;/a&gt; · &lt;a href="https://www.youtube.com/watch?v=mCyKPYy7OHw" rel="noopener noreferrer"&gt;Watch the demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/mCyKPYy7OHw" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Why build another browser?
&lt;/h2&gt;

&lt;p&gt;The browser already holds much of the context an assistant needs: the page you are reading, the tabs you are comparing, and the application you are signed into.&lt;/p&gt;

&lt;p&gt;Moving that work into a separate chat creates friction. You copy context over, get an answer, switch back, and translate the answer into clicks and keystrokes. A separate automation session can introduce another problem: getting it into the same state as your real session.&lt;/p&gt;

&lt;p&gt;I wanted to make that handoff smaller.&lt;/p&gt;

&lt;p&gt;In Nabsun, the assistant sits beside your tabs. The browser provides page-reading tools, action tools, and approval controls. You can watch the work happen and take over in the same tab.&lt;/p&gt;

&lt;p&gt;That is the reason to build at the browser layer: &lt;strong&gt;context, actions, and user control can live together.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What can it actually do?
&lt;/h2&gt;

&lt;p&gt;Open the assistant with &lt;strong&gt;Ctrl+Shift+A&lt;/strong&gt;. Start with the current page or give it a task involving several tabs.&lt;/p&gt;

&lt;p&gt;Here are examples of tasks to try—not promises that every model will complete every website workflow:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Example prompt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Understand a page&lt;/td&gt;
&lt;td&gt;“Summarize this documentation and point out the setup requirements.”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compare tabs&lt;/td&gt;
&lt;td&gt;“Compare the options in these tabs. Make a table and link each finding to its source.”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract information&lt;/td&gt;
&lt;td&gt;“Turn this table into structured records. Flag missing values.”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prepare a form&lt;/td&gt;
&lt;td&gt;“Fill this form using the details I provide. Stop before submitting.”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Research a question&lt;/td&gt;
&lt;td&gt;“Find the relevant documentation, read it, and explain the answer with links.”&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The assistant can read, click, type, select options, scroll, and navigate. It can ask for clarification when a choice belongs to you.&lt;/p&gt;

&lt;p&gt;Nabsun also includes the everyday browser features that make this practical: tabs, bookmarks, history, downloads, session restore, and encrypted saved passwords.&lt;/p&gt;

&lt;p&gt;The useful distinction is the move from &lt;strong&gt;discussing a task&lt;/strong&gt; to &lt;strong&gt;helping carry it out&lt;/strong&gt;, while keeping the result visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the assistant sees a page
&lt;/h2&gt;

&lt;p&gt;For developers, this is one of the more interesting parts.&lt;/p&gt;

&lt;p&gt;Nabsun provides an accessibility-derived outline of the page. Interactive elements receive opaque references that the agent can use with tools such as click, type, and select.&lt;/p&gt;

&lt;p&gt;The normal loop is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read the page → choose an element → request an action
             → apply approval settings → act → read the updated page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page bridge rejects stale references and changed element identities instead of silently using them against a different target.&lt;/p&gt;

&lt;p&gt;This gives the agent a structured way to interact with a page without relying on screen coordinates for every action. It does not make arbitrary websites reliable: iframe and shadow-root coverage are limited, and website compatibility varies.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/naveenalavilli/nabsun/blob/main/ARCHITECTURE.md" rel="noopener noreferrer"&gt;architecture notes&lt;/a&gt; explain the process boundaries, page tools, and execution loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the model that fits the task
&lt;/h2&gt;

&lt;p&gt;Nabsun separates the browser tools from the model using them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Built-in local model:&lt;/strong&gt; Qwen3 1.7B runs on your CPU without an account or API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your local stack:&lt;/strong&gt; connect Ollama or configure a different local model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud APIs:&lt;/strong&gt; configure Anthropic or OpenAI in Settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing assistant accounts:&lt;/strong&gt; connect Codex or Claude Code through &lt;strong&gt;Extensions → Assistants&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the CLI connections, Nabsun installs the official native CLI when needed and can reuse existing sign-ins. Your provider account must have access to the assistant. Credentials remain in the CLI's own store; signing out also affects that CLI outside Nabsun.&lt;/p&gt;

&lt;p&gt;The bundled model matters because you can start without opening another account or paying for hosted inference. It is suitable for simple tasks; complex research and long action sequences benefit from a stronger model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local inference works offline. Visiting websites still needs a network.&lt;/strong&gt; Cloud providers and signed-in CLI assistants use their respective services and receive the context sent to them. Ollama is local only when its endpoint is local.&lt;/p&gt;

&lt;p&gt;Optional personal context lives in &lt;code&gt;soul.md&lt;/code&gt;, a Markdown file shared only with local providers by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect your editor to your real browser
&lt;/h2&gt;

&lt;p&gt;Nabsun also exposes an &lt;strong&gt;MCP server&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That lets an external agent use Nabsun's browser tools against your existing tabs. An assistant in your development workflow can access the browser through a defined interface.&lt;/p&gt;

&lt;p&gt;Copy the configuration from:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settings → Integrations → Connect an external agent&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The sidebar and external agents use the same browser approval settings. The bridge listens on loopback and uses a fresh bearer token for each launch.&lt;/p&gt;

&lt;p&gt;Nabsun can also consume tools from MCP servers, load plugins, and load unpacked Chrome extensions. Chrome API support is partial, and there is no Chrome Web Store installation flow yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  A signed-in browser needs clear boundaries
&lt;/h2&gt;

&lt;p&gt;An assistant acting inside your session has meaningful access. The controls deserve as much attention as the capabilities.&lt;/p&gt;

&lt;p&gt;By default, reading tools run automatically. Clicking, typing, navigation, and page code require approval. Per-tool grants and Autopilot change those settings, including for connected external agents.&lt;/p&gt;

&lt;p&gt;The application UI and web pages run in separate renderers. Action targets are captured before approval and revalidated before dispatch.&lt;/p&gt;

&lt;p&gt;Those controls have limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection remains a risk.&lt;/strong&gt; Instructions on a malicious page can influence a model; approval controls do not eliminate that problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured tools restrict credential handling.&lt;/strong&gt; Password and one-time-code values are excluded from structured extraction, and typing into those fields is blocked. Arbitrary page JavaScript has broader access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugins and MCP servers are trusted code.&lt;/strong&gt; Plugins run with full Node privileges; they are not sandboxed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop cancels pending work.&lt;/strong&gt; It cannot undo an action that has already completed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nabsun has &lt;strong&gt;not had an independent security audit&lt;/strong&gt;. The &lt;a href="https://github.com/naveenalavilli/nabsun/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;security document&lt;/a&gt; describes the controls and remaining gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open source? Why MIT?
&lt;/h2&gt;

&lt;p&gt;A browser assistant can encounter private documents, internal applications, and authenticated sessions. People should be able to inspect how it handles that access.&lt;/p&gt;

&lt;p&gt;Publishing the source makes concrete questions inspectable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What context reaches the selected model?&lt;/li&gt;
&lt;li&gt;Where are credentials stored?&lt;/li&gt;
&lt;li&gt;Which actions require approval?&lt;/li&gt;
&lt;li&gt;What changes when Autopilot is enabled?&lt;/li&gt;
&lt;li&gt;What privileges does an integration receive?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open source does not automatically make software secure. It makes the implementation available for review, criticism, and improvement.&lt;/p&gt;

&lt;p&gt;MIT also gives developers room to experiment: fork the browser, change its tools, adapt the interface, or reuse parts in another project under the license's terms. I want people building agent tools to be able to work with the implementation.&lt;/p&gt;

&lt;p&gt;Nabsun's code is MIT-licensed. Bundled dependencies and model weights retain their own licenses and notices; those are documented in &lt;a href="https://github.com/naveenalavilli/nabsun/blob/main/THIRD-PARTY-NOTICES.md" rel="noopener noreferrer"&gt;third-party notices&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands today
&lt;/h2&gt;

&lt;p&gt;This is an early project, with real limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No automatic updates, profile sync, or private browsing mode.&lt;/li&gt;
&lt;li&gt;Unsigned Windows builds; macOS builds use ad-hoc signing without notarization.&lt;/li&gt;
&lt;li&gt;Apple silicon has been tested locally; Intel Mac remains unverified. Linux release packaging is not supported yet.&lt;/li&gt;
&lt;li&gt;The small bundled model can struggle with long tasks.&lt;/li&gt;
&lt;li&gt;No managed enterprise policy, central action audit log, or plugin sandbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://github.com/naveenalavilli/nabsun#readme" rel="noopener noreferrer"&gt;README&lt;/a&gt; is the place to check current setup instructions and platform support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in a few minutes
&lt;/h2&gt;

&lt;p&gt;Download an available build from &lt;a href="https://github.com/naveenalavilli/nabsun/releases/latest" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;. Windows has an installer and a portable executable. The bundled model makes the download roughly 1.4 GB; verify your download against the release's &lt;code&gt;SHA256SUMS.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Or, with Node.js, npm, and Git installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/naveenalavilli/nabsun.git
&lt;span class="nb"&gt;cd &lt;/span&gt;nabsun
npm ci
npm run fetch:model
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Electron did not download during installation, run &lt;code&gt;npm run setup&lt;/code&gt;. Linux source setup requires manual engine setup; follow the instructions printed by &lt;code&gt;fetch:model&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a first task, open a public documentation page, press &lt;strong&gt;Ctrl+Shift+A&lt;/strong&gt;, and ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Summarize this page, identify the prerequisites, and link to the sections I should read next.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then try comparing two tabs. Start with something whose answer you can check.&lt;/p&gt;

&lt;p&gt;If you want to contribute, the &lt;a href="https://github.com/naveenalavilli/nabsun/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;contributor guide&lt;/a&gt; is the starting point. Reproducible website failures, clearer permission flows, and improvements to page tools would all be useful feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is one browser task you would hand to an assistant—and what would you need to see before letting it act?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
    </item>
    <item>
      <title>The Validation Rule That Was Itself Invalid</title>
      <dc:creator>Naveen Alavilli</dc:creator>
      <pubDate>Wed, 23 Sep 2026 21:31:16 +0000</pubDate>
      <link>https://dev.to/naveen_alavilli/the-validation-rule-that-was-itself-invalid-12i7</link>
      <guid>https://dev.to/naveen_alavilli/the-validation-rule-that-was-itself-invalid-12i7</guid>
      <description>&lt;p&gt;I lost a save cycle last week to a bug that I now think is one of the most under-discussed failure modes in configurable systems: a validation rule that was itself incompletely configured.&lt;/p&gt;

&lt;p&gt;Not invalid data. An invalid &lt;em&gt;rule&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;If you have ever built or maintained a form builder, you know the shape. Forms are not code, they are data, arranged in a hierarchy that looks roughly 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;Form
└── Tab
    └── Section
        └── Question
            ├── type (text, date, number, radio, multi-select, ...)
            └── validations[]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A question carries a type and a list of validations. Text questions might accept &lt;code&gt;MaximumLength&lt;/code&gt;, &lt;code&gt;MinimumLength&lt;/code&gt;, &lt;code&gt;Email&lt;/code&gt;, &lt;code&gt;PhoneNumber&lt;/code&gt;, &lt;code&gt;Required&lt;/code&gt;. List questions bind to a shared option set so that "County" means the same 254 choices everywhere it appears. Authors compose forms in a UI, hit save, and the whole tree is persisted as a draft. Publishing is a separate, explicit step.&lt;/p&gt;

&lt;p&gt;This is a good architecture. It is also where the trap lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap
&lt;/h2&gt;

&lt;p&gt;Most validations are a single flag. &lt;code&gt;Required&lt;/code&gt; looks like one too. You add it to a question and move on.&lt;/p&gt;

&lt;p&gt;Except &lt;code&gt;Required&lt;/code&gt; was not a flag. Selecting it rendered a &lt;em&gt;second&lt;/em&gt; dropdown, with two choices, something like &lt;code&gt;Required&lt;/code&gt; and &lt;code&gt;Semirequired&lt;/code&gt; - a genuinely useful distinction when a field must be answered before publication but may be left blank during intake.&lt;/p&gt;

&lt;p&gt;That second dropdown had no default.&lt;/p&gt;

&lt;p&gt;So the rule sat there, attached to the question, looking complete in the list of validations, carrying no answer to the question "required &lt;em&gt;how&lt;/em&gt;?" I built out the rest of the form. Fifteen or so questions. Hit Save.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'Required' validation type is required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inline, per question, for every question where I had added &lt;code&gt;Required&lt;/code&gt; and not set the sub-type. The save was rejected. The error was accurate. It was just forty minutes late.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this class of bug happens
&lt;/h2&gt;

&lt;p&gt;The rule had two states that the model could not distinguish:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Not configured yet&lt;/strong&gt; - the author is mid-edit, nothing is wrong&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configured to nothing&lt;/strong&gt; - the author has moved on, and this is now broken&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both serialize identically. A &lt;code&gt;Required&lt;/code&gt; validation with a null sub-type. The only thing separating "in progress" from "broken" is whether the author intends to come back to it, and intent is not a column.&lt;/p&gt;

&lt;p&gt;So the system deferred the judgment to the only moment it could be sure the author was finished: submit. That is a reasonable engineering decision and a poor authoring experience, and both things are true at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Make the invalid state unrepresentable.&lt;/strong&gt; If &lt;code&gt;Required&lt;/code&gt; always needs a strictness, then the rule is not &lt;code&gt;Required&lt;/code&gt; plus a modifier, it is two distinct rules: &lt;code&gt;Required&lt;/code&gt; and &lt;code&gt;Semirequired&lt;/code&gt;. One dropdown, no sub-dropdown, no null state. Most "field A is mandatory when field B is set" configuration problems dissolve the moment you stop modelling the dependency and start enumerating the valid combinations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default to the safe option.&lt;/strong&gt; If enumeration is genuinely awkward, pick a default. Strict &lt;code&gt;Required&lt;/code&gt; is the conservative choice: it can only ever reject data that a laxer setting would have let through. An author who wanted &lt;code&gt;Semirequired&lt;/code&gt; will notice. An author who wanted &lt;code&gt;Required&lt;/code&gt; and got null will not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate at the point of edit.&lt;/strong&gt; If you must keep the null state, do not save it up for submit. The moment focus leaves a half-configured rule, mark it. A red dot on the question in the tree costs nothing and turns a forty-minute feedback loop into a four-second one.&lt;/p&gt;

&lt;p&gt;Any one of these would have saved the cycle. The third is the cheapest to retrofit and the least satisfying, because it treats the symptom.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that generalises
&lt;/h2&gt;

&lt;p&gt;Configuration is code without a compiler.&lt;/p&gt;

&lt;p&gt;When behaviour lives in a database rather than a source file, everything the type system used to do for free becomes someone's explicit responsibility. A half-written &lt;code&gt;if&lt;/code&gt; statement will not compile. A half-written validation rule saves cleanly, sits in a table, and waits.&lt;/p&gt;

&lt;p&gt;The instinct that serves me best here is to ask, of any config schema: &lt;strong&gt;what is the set of rows this table can hold that no author would ever have meant?&lt;/strong&gt; If that set is not empty, the schema is doing less work than it should, and the difference is being paid for at save time by whoever is holding the keyboard.&lt;/p&gt;

&lt;p&gt;Worth an hour of design. Cheaper than forty minutes and a lost form.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Working notes from building form-driven systems. If you have fought the same fight in a different builder, I would like to hear how you modelled your way out.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>codequality</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
