The feature was built and the model never heard about it
This is part seven of building the live demo for my apidays Australia 2026 workshop with Claude Code. Part six ended with the on-behalf-of exchange finally working end to end, and named Agents as Principal as the next piece of work - giving the AI agent this app has been modelling in Auth0 FGA an actual identity in the token, not just a name in a policy. That work happened, and it turned out to be substantial enough to earn its own new series rather than a section in this one - I've written it up separately. This post covers everything else from the same stretch: two features that shipped correctly and were still unreachable from the chat itself, three more gaps a live click-through found the same afternoon, and a browser tab that had been quietly wrong since the day the project started.
Built at one layer, invisible at the other
I asked Claude to add two small citizen-facing capabilities: letting a caseworker re-enable a contractor (the inverse of the disable action that already existed), and letting a citizen revoke their own submitted appeal directly, without going through the AI agent at all. Both landed cleanly - correct FGA relations reused rather than duplicated, a real API route, a real database write. Both were also completely unreachable from the chat the first time either of us tried them.
The contractor-enable tool was registered on the MCP server (admin-mcp/route.ts), but the admin chat route that actually talks to the model - a separate file with its own tool definitions - never got the matching entry. The admin chatbot genuinely could not enable a contractor, not because the capability was missing, but because a Model Context Protocol server and a chat route are two different places tools get declared in this codebase, and updating one doesn't update the other.
The revoke case was more interesting, because it was deliberate rather than an oversight. Revoking an appeal was built specifically as something the citizen does themselves - never delegated to the assistant, on purpose, the same way part four established that not every capability should be agent-shaped. That meant there was never supposed to be a chat tool for it. What there was supposed to be, and wasn't, was a line in the system prompt saying so. A citizen who typed "revoke my appeal" into the chat got either silence or a guess, because the model had no idea the action existed at all, delegable or not. Deciding a capability shouldn't be agent-accessible is a real design choice. Telling the model that, so it can say "no, use the button" instead of making something up, is a separate step that has to happen anyway.

Three more gaps, the same afternoon
Live testing straight after those two fixes found three more things, none of them caught by a type check or a lint pass.
The step-up approval modal for bulk actions still only ever said "disable," even though the same caseworker_can_bulk_disable relation had already been extended to gate bulk-enable earlier in the same session. The copy was accurate when it was written. Nobody went back and looked at it once enable existed alongside disable. Fixed by making the copy generic rather than writing a separate version for each direction, since the step-up gate is one thing a caseworker requests, not two.
Amazon Nova Pro refused an entirely ordinary bulk-disable request and returned the literal string "The generated text has been blocked by our content filters" as its answer, rendered in a completely normal chat bubble. At the AI SDK level there's no distinct signal for a Bedrock content-filter refusal - no thrown error, no separate finishReason - it's just text, on the same channel a real answer comes through on. I checked the AI SDK and Bedrock documentation before deciding anything, confirmed this is a known, structural limitation rather than something fixable in the integration code, and kept Nova Pro rather than switching models over a flaky filter. The fix is a client-side string match that catches the known refusal text and renders it as an amber "this looks like a false positive, try rephrasing" banner instead of a grey reply bubble - not elegant, but it's the only signal the SDK actually gives you.
The third one started as a styling complaint and turned into a real design fix. The "Revoke appeal" button sat inline among the chat page's own golden-path buttons, which made it read as another chat action even though the whole point of the previous fix was that it isn't one - and its confirmation message rendered at the bottom of the page, disconnected from the conversation it was supposedly part of. Asked whether to fix the styling in place or move the button entirely, I chose to move it to the case page, next to the case status it actually changes. That's not a cosmetic change. It's the citizen-direct boundary the system prompt already asserts, made physically true for the first time, rather than merely stated in text a user never reads. Moving it also surfaced a genuine audit-log gap: the same FGA check gated a plain case-page view and an actual revoke, and logged both identically, with no way to tell them apart afterwards. A new distinct log event for the revoke action fixed that, on the same pattern the delegation toggle already used - the check that gates an action and the record of the action happening are two different things and need two different log entries.
A browser tab that had been wrong the whole time
A week later, once the agent-identity work above had shipped and I was testing it live, three smaller things turned up in the same afternoon that had nothing to do with authentication at all.
The new agent identity showed up in the token trace card as a raw, unlabelled string. The display-name lookup that turns an opaque client ID into something readable had only ever covered Auth0 client IDs, and an Agents as Principal identity is a different kind of identifier entirely, so it fell straight through to showing itself with no badge at all. I added both new agent names to the lookup and gave agent hops in the trace diagram a distinct colour, driven by the token's own sub_profile claim rather than just presence in a name table, so a real registered agent principal actually looks different from an ordinary client hop instead of just being labelled differently.
A signup that failed with "Something went wrong" turned out not to be a bug at all - the test account was already registered from an earlier session, and I confirmed that directly before spending any time in the Auth0 logs looking for a defect that had already been explained. Worth mentioning precisely because it's the boring, correct outcome of checking before diagnosing: stop the moment you have the actual answer, rather than continuing to investigate something that's no longer a mystery.
The third one had survived every single session in this entire project. Every page's browser tab still read "Create Next App," and the favicon was still the stock black triangle - the literal, unedited create-next-app defaults, present since the very first commit, through four separate sessions where Claude and I went back and re-checked requirements against the original brief. Nothing about a wrong page title breaks a build or fails a test. It just sits there, quietly telling every single visitor the wrong thing, in a way no review step in this whole process was ever going to catch, because nothing about it was ever an error. Fixed with a title template and a real title on all six routes, and a plain SVG icon - a simple civic-building glyph in the app's existing blue - replacing the generated favicon that had been there since the start.
![]()
An npm audit run while adding an unrelated dependency turned up something worth its own line: a critical, already-present, unpatched Next.js CVE, an unauthenticated remote code execution issue fixed two patch versions ago. It wasn't found by looking for it - it was a side effect of npm install output that nobody reads carefully unless they've just added a package for a completely different reason. Bumped the same session, once seen.
Final Thoughts
None of this stretch is one big architectural lesson the way the FGA flaw in part one or the OBO mismatch in part six were. It's smaller and more scattered on purpose - a tool that existed on the server and not in the model's own list of what it can do, a design boundary that needed stating in the system prompt as well as enforcing in code, a display gap that let a new kind of identity render as an anonymous string, and a page title that was wrong from day one because being wrong never once caused an error. The common thread across all of them is the same one this series keeps finding at a different layer each time: building something correctly and telling the model, or the reader, or the browser tab, that it exists are separate steps, and skipping the second one looks exactly like success right up until someone actually looks.