Skip to content

Commit 8ba804e

Browse files
os-zhuangclaude
andauthored
feat(auth): env-side AI-seat marker via sys_user.ai_access + resolveCtx synthesis (#2327)
* feat(auth): sys_user.ai_access → synthesize the ai_seat capability (simple AI-seat model) The env DB (Turso) is single-org and has only sys_user — so the AI seat is best modeled as a boolean on the user, not a permission set. Add `ai_access` as a better-auth additionalField (rides on the session user) and have resolveCtx synthesize the `ai_seat` capability when it is true. The existing per-agent gate (evaluateAgentAccess → requires ai_seat), catalog filter, and frontend are unchanged — they now admit a user via the field with NO permission-set grant. ADDITIVE: the permission-set path still works (a user can get ai_seat from either source), so there is no regression while the rest of the simplification (have the entitlement layer SET the field + retire the permission-set seed/junction/cap- middleware + repoint the management UI) lands. Built clean (plugin-auth + hono). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ai-seat): own ai_access via objectql SysUser def + guarded read (not a better-auth additionalField) sys_user is a better-auth-MANAGED table and better-auth SELECTs explicit columns. Declaring ai_access as a better-auth additionalField (my first cut) would make getSession query a column that may not exist on every env yet → broken auth on deploy. Safe-by-construction instead: • drop the additionalField (better-auth stays oblivious to the column); • own ai_access on the objectql SysUser object def → provisioned by boot schema-sync, and editable as a normal field (the management UI is a checkbox); • resolveCtx reads it via a GUARDED system query (.catch → []), so a missing column / failed read can only NO-OP — never break auth. Built clean (platform-objects + plugin-auth + plugin-hono-server). Amends the prior additionalField approach on this branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0885fde commit 8ba804e

3 files changed

Lines changed: 45 additions & 0 deletions

File tree

packages/platform-objects/src/identity/sys-user.object.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,19 @@ export const SysUser = ObjectSchema.create({
395395
description: 'When set, the ban auto-clears at this time.',
396396
}),
397397

398+
ai_access: Field.boolean({
399+
label: 'AI Access',
400+
defaultValue: false,
401+
group: 'Admin',
402+
description:
403+
'Whether this user holds an AI seat — grants access to the in-UI AI ' +
404+
'agents (build / ask). The framework synthesizes the `ai_seat` ' +
405+
'capability from this flag (plugin-hono-server resolveCtx). Assignment ' +
406+
'is capped by the licensed / purchased seat count (enforced by ' +
407+
'@objectstack/security-enterprise AiSeatPlugin). Owned by objectql ' +
408+
'(better-auth is oblivious to this column).',
409+
}),
410+
398411
// ── Profile ──────────────────────────────────────────────────
399412
image: Field.url({
400413
label: 'Profile Image',

packages/plugins/plugin-auth/src/auth-manager.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,14 @@ export class AuthManager {
324324
// createAdapterFactory.
325325
user: {
326326
...AUTH_USER_CONFIG,
327+
// NOTE: the env-side AI-seat marker `sys_user.ai_access` is deliberately
328+
// NOT declared as a better-auth additionalField. sys_user is a
329+
// better-auth-MANAGED table and better-auth SELECTs explicit columns, so
330+
// declaring it here would make getSession query a column that may not
331+
// exist on every env yet → broken auth. Instead the column is owned by
332+
// the objectql `SysUser` object def (provisioned by boot schema-sync)
333+
// and read by a GUARDED system query in resolveCtx (can only no-op,
334+
// never break auth). better-auth stays oblivious to the extra column.
327335
},
328336
account: {
329337
...AUTH_ACCOUNT_CONFIG,

packages/plugins/plugin-hono-server/src/hono-plugin.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,30 @@ export class HonoServerPlugin implements Plugin {
490490
/* fall back to self-only */
491491
}
492492
}
493+
// Env-side AI-seat marker (simple model). The single-org env
494+
// DB has no permission-set/org dimension for this — the seat is
495+
// the boolean `sys_user.ai_access`. Read it with a GUARDED system
496+
// query (NOT a better-auth additionalField: sys_user is
497+
// better-auth-managed and better-auth SELECTs explicit columns,
498+
// so an additionalField would make getSession query a possibly-
499+
// missing column → broken auth; a guarded read can only no-op).
500+
// When true, synthesize the `ai_seat` capability so the per-agent
501+
// gate (evaluateAgentAccess → requires `ai_seat`) admits the user
502+
// with no permission-set grant. Absent/false/missing-column →
503+
// no synthesis (deny, as before).
504+
if (!permissions.includes('ai_seat')) {
505+
try {
506+
const ql = getObjectQL();
507+
const sysCtx = { context: { isSystem: true } };
508+
const uRows = await ql?.find?.(
509+
'sys_user',
510+
{ where: { id: userId }, limit: 1, ...sysCtx } as any,
511+
).catch(() => []);
512+
if ((uRows?.[0] as any)?.ai_access === true) permissions.push('ai_seat');
513+
} catch {
514+
/* no ai_access column / query failed → no seat (safe) */
515+
}
516+
}
493517
return {
494518
userId,
495519
tenantId,

0 commit comments

Comments
 (0)