fix(runtime): wire ai_access→ai_seat synthesis into the AI route dispatch (per-user seat gate)#2334
Merged
Merged
Conversation
…atch (ADR-0024) The per-user AI-seat gate (evaluateAgentAccess → requires the `ai_seat` capability) reads `req.user.permissions`, but the dispatch ExecutionContext serving `/ai/*` never carried the seat — so a SEATED user (`sys_user.ai_access=true`) was denied: `/ai/agents` returned an EMPTY catalog (and in-UI build/ask would 403), even though the #525 cap correctly counted them. The synthesis lived only on plugin-hono-server's data-route resolveCtx (which is why the cap, on the data-write path, worked but the gate did not). Add the guarded synthesis to BOTH AI req.user construction paths: - http-dispatcher.ts — the `/ai/*` wildcard dispatch (the ACTIVE path): build req.user.permissions, then read sys_user.ai_access via the DEFAULT (current per-request env) ObjectQL service and push `ai_seat` when true. NB do NOT pass context.environmentId — in the cloud runtime that is the control-plane env UUID, which keys a DISTINCT empty per-scope engine; the env's own sys_user is served by the default service (this exact mismatch returned [] in testing). - dispatcher-plugin.ts resolveRequestUser — the concrete-route mount path (defensive; guarded read via the env kernel's `data` service). Guarded system read → can only ever ADD access, never break auth (absent/false/missing-column/error → no seat, unchanged). Live-verified on a local cloud+objectos stack (OS_CLOUD_AI_SEAT_MODE=enforce): seated ai_access=true → /ai/agents=[build,ask]; false → []; restore → [build,ask]; cap still 400s past the licensed seat count. Unblocks flipping OS_CLOUD_AI_SEAT_MODE=enforce. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
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.
Problem
The per-user AI-seat gate ([ADR-0024],
service-aievaluateAgentAccess→ requires theai_seatcapability) readsreq.user.permissions. But the dispatchExecutionContextthat serves the/ai/*routes never carried the seat, so a seated user (sys_user.ai_access=true) was still denied:GET /ai/agentsreturned an empty catalog for every authenticated caller (and the console hides the AI surface when the catalog is empty), while unauthenticated requests leaked the full catalog.build/askwould 403.…even though the cap (cloud#525) correctly counted the same
ai_accessbooleans. Root cause: theai_access → ai_seatsynthesis lived only inplugin-hono-server's data-routeresolveCtx— which is exactly why the cap (data-write path) worked but the gate (AI-route path) did not.Fix
Add the guarded synthesis to both AI
req.userconstruction paths in@objectstack/runtime:http-dispatcher.ts— the/ai/*wildcard dispatch (the active path). Buildreq.user.permissions, then readsys_user.ai_accessvia the default (current per-request env) ObjectQL service and pushai_seatwhen true.context.environmentId: in the cloud runtime that's the control-plane env UUID, which keys a distinct, empty per-scope engine; the env's ownsys_useris served by the default service. (Passing it returned[]in testing — diagnosed live.)dispatcher-plugin.tsresolveRequestUser— the concrete-route mount path (defensive; guarded read via the env kernel'sdataservice).Guarded system read → can only ever ADD access, never break auth (absent / false / missing-column / error → no seat, unchanged).
Verification
Live on a local cloud + objectos stack,
OS_CLOUD_AI_SEAT_MODE=enforce, driving the env REST as the seated admin (env session via thesso-openbridge):/ai/agentsai_access=true)[build, ask]✅ai_access=false)[]✅true)[build, ask]✅Cap (cloud#525) still 400s past the licensed seat count. Unblocks flipping
OS_CLOUD_AI_SEAT_MODE=enforce(the boot-verify thatai-seat-simple-modelflagged as mandatory).🤖 Generated with Claude Code