Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion packages/runtime/src/dispatcher-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,13 +971,35 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
const sessionData = await api.getSession({ headers: headersInstance });
const userId: string | undefined = sessionData?.user?.id ?? sessionData?.session?.userId;
if (!userId) return undefined;
// Env-side AI-seat synthesis (ADR-0024, sys_user.ai_access boolean
// model). The per-agent gate (evaluateAgentAccess → requires the
// `ai_seat` capability) reads `req.user.permissions`, which the AI
// routes get from THIS resolver — but it previously returned an empty
// permission set, so a SEATED user (ai_access=true) was still denied
// (empty /ai/agents catalog). Mirror plugin-hono-server's data-route
// resolveCtx: read the boolean with a GUARDED system query on the
// per-request env kernel's `sys_user` and, when true, synthesize the
// `ai_seat` capability. Absent/false/missing-column/error → no seat
// (deny, unchanged) so this can only ever ADD access, never break auth.
const permissions: string[] = [];
try {
const dataEngine: any = ctx.getService('data');
if (dataEngine && typeof dataEngine.find === 'function') {
const uRows = await dataEngine
.find('sys_user', { where: { id: userId }, limit: 1 }, { context: { isSystem: true } })
.catch(() => []);
if ((uRows?.[0] as any)?.ai_access === true) permissions.push('ai_seat');
}
} catch {
/* no ai_access column / query failed → no seat (safe) */
}
return {
userId,
id: userId,
displayName: sessionData?.user?.name ?? sessionData?.user?.email ?? userId,
email: sessionData?.user?.email,
roles: [],
permissions: [],
permissions,
organizationId: sessionData?.session?.activeOrganizationId,
};
} catch {
Expand Down
34 changes: 33 additions & 1 deletion packages/runtime/src/http-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3217,14 +3217,46 @@ export class HttpDispatcher {
// for anonymous requests — the route's own `auth: true` guard
// is enforced by upstream middleware.
const ec: any = context.executionContext;
// Build the caller's permission set, then synthesize the env-side
// AI seat. The per-agent gate (evaluateAgentAccess → requires the
// `ai_seat` capability) reads `req.user.permissions`, but the
// dispatch ExecutionContext does NOT carry the seat — so a SEATED
// user (sys_user.ai_access=true) was denied (empty /ai/agents
// catalog). Read the boolean on the per-request (env) kernel via the
// env-scoped ObjectQL service and, when true, ADD `ai_seat`. Copy the
// array first so we never mutate the shared ec. Guarded system read →
// can only ever ADD access, never break auth (absent/false/error →
// no seat, unchanged).
const permissions: string[] = ec?.userId && Array.isArray(ec.permissions)
? [...ec.permissions]
: [];
if (ec?.userId && !permissions.includes('ai_seat')) {
try {
// Resolve via the DEFAULT (current per-request env) ObjectQL
// service. 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`
// (where `ai_access` lives) is served by the default service.
// Passing the id yields an empty result and the seat is lost.
const ql: any = await this.getObjectQLService();
if (ql && typeof ql.find === 'function') {
const rows = await ql
.find('sys_user', { where: { id: ec.userId }, limit: 1 }, { context: { isSystem: true } })
.catch(() => []);
if ((rows?.[0] as any)?.ai_access === true) permissions.push('ai_seat');
}
} catch {
/* no ai_access column / lookup failed → no seat (safe) */
}
}
const user = ec?.userId
? {
userId: ec.userId,
id: ec.userId,
displayName: ec.userDisplayName ?? ec.userName ?? ec.userId,
email: ec.userEmail,
roles: Array.isArray(ec.roles) ? ec.roles : [],
permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
permissions,
organizationId: ec.tenantId,
}
: undefined;
Expand Down