fix(chatbot): read the agent catalog in the declared envelope too (objectstack#4053) - #2992
Merged
Merged
Conversation
…jectstack#4053)
`GET /api/v1/ai/agents` is served by two producers — the framework dispatcher's
degraded fallback when no AI service is registered, and cloud's service-ai — and
it is one of the last SDK-addressable routes still answering outside the declared
`{ success: true, data }` envelope. useAgents read only `{ agents }` and a bare
array, so the day either producer converts, the parse misses.
That miss is unusually dangerous here, which is why it is worth getting ahead of
rather than fixing after. The catalog is not just data: useAiSurfaceEnabled gates
the ENTIRE AI surface on `agents.length > 0`, because the route is access-filtered
per caller and is therefore the only signal that is both edition- and user-aware
(ADR-0068). An empty list is the correct answer for a seat-less user or a
Community-Edition deployment with no service-ai — so a parse miss and the
legitimate hidden state are indistinguishable: no error, no 403, no log, just the
FAB, the top-bar link and the designer's "Ask AI" quietly gone for every user.
Nothing downstream would catch it.
extractAgentList now folds all four shapes to the same list — bare array,
`{ agents }`, `{ success: true, data: [...] }`, `{ success: true, data: { agents } }`
— detecting the envelope the way ObjectStackClient.unwrapResponse does (a BOOLEAN
`success`), so the two readers cannot disagree about what counts as one. It is
exported and pure so the shapes are testable without standing up the hook.
No behaviour change against any server shipping today: the shapes that worked
before parse identically. This only removes the lockstep requirement, so the
server side can convert on its own schedule instead of having to land with a
console release — the same consumer-first ordering used for SharedRecordPage in
objectstack#3983.
Nine tests; reverting to the previous two-shape read fails five of them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Defuses objectstack#4053 from the consumer side — single repo, no cross-repo coordination, safe to land before anything on the server moves.
Why this route is special
GET /api/v1/ai/agentsis served by two producers (the framework dispatcher's degraded fallback when no AI service is registered, and cloud'sservice-ai) and is one of the last SDK-addressable routes still answering outside the declared{ success: true, data }envelope.useAgentsread only{ agents }and a bare array, so the day either producer converts, the parse misses.On most routes a missed parse is a visible bug. Here it is invisible, and that asymmetry is the whole argument for doing this first.
useAiSurfaceEnabledgates the entire AI surface onagents.length > 0— deliberately, and its own header explains why: the route is access-filtered per caller, making it the only signal that is both edition- and user-aware (ADR-0068). A user without theai_seatpermission gets an empty catalog and the AI UI hides, instead of showing a button that 403s on click.So an empty list is a correct, expected answer. Which means a parse miss and the legitimate hidden state are indistinguishable:
service-ai(correct)No error, no 403, no failed request, no log line. Nothing downstream would catch it.
The change
extractAgentListfolds four shapes to one list:The envelope is detected the way
ObjectStackClient.unwrapResponsedetects it — a booleansuccess— so the two readers can't disagree about what counts as one. Pure and exported, so the shapes are testable without standing up the hook.No behaviour change against any server shipping today: the two shapes that worked before parse identically. This only removes the lockstep requirement, so the server can convert on its own schedule instead of having to land with a console release — the same consumer-first ordering I used for
SharedRecordPagein objectstack#3983.Correction to objectstack#4053
While verifying this I found the issue named the wrong exposure path, and I'll fix it there too. It said the risk sat in
client.ai.agents.list()'sbody?.agentscompensation. That SDK method has zero callers in objectui and cloud — the only cloud hits are the route-ledger declaration and its conformance test. The real exposure is this file's ownfetch.Worth calling out because the error pointed the wrong way: someone following the issue would check the SDK, see nothing calls it, conclude "low risk", and skip exactly the path that matters.
Verification
vitest run agentListShapesagentAliases·agentCapabilities(neighbours)turbo run type-check --filter=@object-ui/plugin-chatboteslintGenerated by Claude Code