diff --git a/packages/platform-objects/src/identity/sys-user.object.ts b/packages/platform-objects/src/identity/sys-user.object.ts index 8a603e822b..850d58ff26 100644 --- a/packages/platform-objects/src/identity/sys-user.object.ts +++ b/packages/platform-objects/src/identity/sys-user.object.ts @@ -395,6 +395,19 @@ export const SysUser = ObjectSchema.create({ description: 'When set, the ban auto-clears at this time.', }), + ai_access: Field.boolean({ + label: 'AI Access', + defaultValue: false, + group: 'Admin', + description: + 'Whether this user holds an AI seat — grants access to the in-UI AI ' + + 'agents (build / ask). The framework synthesizes the `ai_seat` ' + + 'capability from this flag (plugin-hono-server resolveCtx). Assignment ' + + 'is capped by the licensed / purchased seat count (enforced by ' + + '@objectstack/security-enterprise AiSeatPlugin). Owned by objectql ' + + '(better-auth is oblivious to this column).', + }), + // ── Profile ────────────────────────────────────────────────── image: Field.url({ label: 'Profile Image', diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index 30f056cfc4..c0764915fd 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -324,6 +324,14 @@ export class AuthManager { // createAdapterFactory. user: { ...AUTH_USER_CONFIG, + // NOTE: the env-side AI-seat marker `sys_user.ai_access` is deliberately + // NOT declared as a better-auth additionalField. sys_user is a + // better-auth-MANAGED table and better-auth SELECTs explicit columns, so + // declaring it here would make getSession query a column that may not + // exist on every env yet → broken auth. Instead the column is owned by + // the objectql `SysUser` object def (provisioned by boot schema-sync) + // and read by a GUARDED system query in resolveCtx (can only no-op, + // never break auth). better-auth stays oblivious to the extra column. }, account: { ...AUTH_ACCOUNT_CONFIG, diff --git a/packages/plugins/plugin-hono-server/src/hono-plugin.ts b/packages/plugins/plugin-hono-server/src/hono-plugin.ts index 4d13aebb54..7b307c74e9 100644 --- a/packages/plugins/plugin-hono-server/src/hono-plugin.ts +++ b/packages/plugins/plugin-hono-server/src/hono-plugin.ts @@ -490,6 +490,30 @@ export class HonoServerPlugin implements Plugin { /* fall back to self-only */ } } + // Env-side AI-seat marker (simple model). The single-org env + // DB has no permission-set/org dimension for this — the seat is + // the boolean `sys_user.ai_access`. Read it with a GUARDED system + // query (NOT a better-auth additionalField: sys_user is + // better-auth-managed and better-auth SELECTs explicit columns, + // so an additionalField would make getSession query a possibly- + // missing column → broken auth; a guarded read can only no-op). + // When true, synthesize the `ai_seat` capability so the per-agent + // gate (evaluateAgentAccess → requires `ai_seat`) admits the user + // with no permission-set grant. Absent/false/missing-column → + // no synthesis (deny, as before). + if (!permissions.includes('ai_seat')) { + try { + const ql = getObjectQL(); + const sysCtx = { context: { isSystem: true } }; + const uRows = await ql?.find?.( + 'sys_user', + { where: { id: userId }, limit: 1, ...sysCtx } as any, + ).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, tenantId,