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
7 changes: 7 additions & 0 deletions .changeset/compose-vike-rbac-authz.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@gemstack/ai-autopilot': minor
---

Compose vike-rbac for roles/permissions instead of hand-rolling authz

The crud composer teaches ad-hoc role checks (`canEdit: (user) => user?.role === 'admin'`), which is fine for signed-in-vs-not but leaves the agent to hand-roll a roles/permissions schema and a permission checker the moment an app has named permissions or more than one role. The new `vike-rbac-composer` persona (wired into `vikeExtensionPersonas`, between auth and crud) teaches the agent to compose vike-rbac instead: declare permissions with `definePermissions` and `extends: ['import:vike-rbac/config:default']` (self-installs vike-auth), route every guard through the same `can(user, permission)` / `hasRole(user, role)` (the crud `canView`/`canEdit`, page guards, session scope, and vike-actions guards all delegate to it), and seed roles/permissions from the composed registry with `seedRbac()` rather than a hand-written list. vike-rbac owns the `roles`/`permissions`/`role_user`/`permission_role` tables and is the guard subject vike-admin and vike-actions are built around. No runtime change; the agent stays a black box. Part of #186. Closes #194.
16 changes: 15 additions & 1 deletion packages/ai-autopilot/src/personas/library.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
vikeCrudComposer,
vikeDataModeler,
vikeExtensionPersonas,
vikeRbacComposer,
vikeShellComposer,
} from './library.js'

Expand Down Expand Up @@ -58,6 +59,18 @@ describe('vike extension personas', () => {
assert.match(text, /ejectView\(view, \{ framework \}\)/)
})

it('vikeRbacComposer teaches composing vike-rbac, not a hand-rolled authz schema/checker', () => {
assert.equal(vikeRbacComposer.name, 'vike-rbac-composer')
const text = personaInstructions(vikeRbacComposer)
// Declare permissions + the one check everywhere, riding on vike-auth.
assert.match(text, /definePermissions/)
assert.match(text, /can\(user, permission\)/)
assert.match(text, /hasRole/)
// Seed from the registry, and do not model the RBAC tables by hand.
assert.match(text, /seedRbac/)
assert.match(text, /Do NOT model\s*\n?\s*`roles`/i)
})

it('vikeShellComposer teaches composing vike-themes/vike-layouts, not hand-rolled CSS + shell', () => {
assert.equal(vikeShellComposer.name, 'vike-shell-composer')
const text = personaInstructions(vikeShellComposer)
Expand All @@ -74,11 +87,12 @@ describe('vike extension personas', () => {
assert.match(text, /vike-toolbar\/react/)
})

it('vikeExtensionPersonas composes data + auth + crud + shell (no Prisma, no hand-rolled auth/UI/CSS)', () => {
it('vikeExtensionPersonas composes data + auth + rbac + crud + shell (no Prisma, no hand-rolled auth/UI/CSS)', () => {
const names = vikeExtensionPersonas.map(p => p.name)
assert.deepEqual(names, [
'vike-data-modeler',
'vike-auth-composer',
'vike-rbac-composer',
'vike-crud-composer',
'vike-shell-composer',
'ui-intent-designer',
Expand Down
63 changes: 57 additions & 6 deletions packages/ai-autopilot/src/personas/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,56 @@ and never re-insert fixed-id seed rows on every boot (that duplicates or crashes
real DB).`,
})

/**
* Composes `vike-rbac` for roles/permissions instead of hand-rolling an authz
* schema and a permission checker. It is the guard subject vike-admin and
* vike-actions are built around (`can()` / `hasRole()`), and it closes the gap the
* crud composer half-covers with ad-hoc `user.role` checks. Opt-in, in-workspace
* only (the packages resolve inside the vike-data workspace) — see
* `vikeExtensionPersonas`.
*/
export const vikeRbacComposer: Persona = definePersona({
name: 'vike-rbac-composer',
role: 'Composes vike-rbac for roles/permissions (can()/hasRole()) instead of hand-rolling authz',
appliesTo: ['vike-rbac'],
systemPrompt: `You compose vike-rbac for roles and permissions instead of hand-rolling an authz
schema and a permission checker. It rides on vike-auth (the user is the permission
subject) and is the same check vike-admin, page guards, and vike-actions all use.

When to reach for it: only for REAL roles/permissions. Signed-in-vs-not stays a
plain \`pageContext.user\` check (or a \`canView: (user) => !!user\`); reach for rbac
once the app has named permissions or more than one role. Do NOT model
\`roles\` / \`permissions\` / \`role_user\` / \`permission_role\` in your own schema —
vike-rbac owns those tables.

Declare the permissions and extend the config (rbac self-installs vike-auth):
\`\`\`js
import { definePermissions } from 'vike-rbac'
// +config.js: export default {
// extends: ['import:vike-rbac/config:default'],
// permissions: definePermissions([{ name: 'widgets.edit', roles: ['admin'] }]),
// defaultRoles: ['member'], // granted to a brand-new signup on first request
// }
\`\`\`

One check everywhere — \`can(user, permission)\` / \`hasRole(user, role)\` from
\`vike-rbac\`. Resolution runs in vike-auth's resolve seam, so the check is sync on
\`pageContext.user\` on every request. Route the crud composer's \`canView\` / \`canEdit\`,
page guards, session \`scope\`, and vike-actions guards through the SAME \`can()\`
instead of ad-hoc \`user.role === 'admin'\` comparisons:
\`\`\`js
import { can, hasRole } from 'vike-rbac'
if (!can(pageContext.user, 'widgets.edit')) throw render(403)
// vike-actions: guard: (ctx) => can(ctx.user, 'posts.publish')
\`\`\`

Seed from the registry, do NOT hand-write a seed list: \`seedRbac()\` / \`assignRoles()\`
from \`vike-rbac/seed\` materialize the roles/permissions/grants from the composed
\`permissions\` registry, idempotently. To guard a Telefunc RPC with the same check,
\`requirePermission('users.view')\` from \`vike-rbac/telefunc\` (one universal middleware,
via \`import:vike-rbac/telefunc-middleware:default\`).`,
})

/**
* Composes `vike-crud` (+ `vike-admin`) for the CRUD/admin UI instead of
* hand-writing list/record/form screens. Those screens are the largest chunk of
Expand Down Expand Up @@ -415,16 +465,17 @@ export const sharedPersonas: readonly Persona[] = Object.freeze([
/**
* The opt-in vike-extension stack: compose `vike-auth` for authentication, the
* universal-orm data layer for domain data (both ride one registered adapter), and
* `vike-crud` / `vike-admin` for the CRUD/admin UI derived from the schema, and
* `vike-themes` / `vike-layouts` for styling and the app shell — instead of
* hand-rolling auth, hand-installing an ORM, or hand-writing list/record/form
* screens, a CSS design system, or a layout/nav shell. Swap this in for
* {@link sharedPersonas} when composing extensions (Vike only; the extensions
* currently resolve inside the vike-data workspace).
* `vike-rbac` for roles/permissions, `vike-crud` / `vike-admin` for the CRUD/admin
* UI derived from the schema, and `vike-themes` / `vike-layouts` for styling and the
* app shell — instead of hand-rolling auth, hand-installing an ORM, or hand-writing
* an authz schema, list/record/form screens, a CSS design system, or a layout/nav
* shell. Swap this in for {@link sharedPersonas} when composing extensions (Vike
* only; the extensions currently resolve inside the vike-data workspace).
*/
export const vikeExtensionPersonas: readonly Persona[] = Object.freeze([
vikeDataModeler,
vikeAuthComposer,
vikeRbacComposer,
vikeCrudComposer,
vikeShellComposer,
uiIntentDesigner,
Expand Down
Loading