Permissions & scopes
PLUR Enterprise enforces permissions server-side, on every call. The client tells the server what scope it wants to read or write; the server checks the user’s resolved scopes against that target. There is no client-side check that matters.
The model
Section titled “The model”Two structures:
- Scopes are namespaces —
global,group:acme/platform,project:acme/guardian,user:acme:alice. - Memberships are graph edges — user X belongs to group Y; group Y contains project Z.
Resolution walks the graph: starting from the authenticated user, gather every membership, then expand to every scope reachable from those memberships. The result is a set of scopes the user can read; a second pass marks which they can also write.
The graph lives in Apache AGE (a Postgres extension) and is exposed via src/db/graph.ts. Permission decisions go through src/permissions/resolver.ts.
Standard scope hierarchy
Section titled “Standard scope hierarchy”global ├── group:acme/platform │ └── group:acme/platform/backend │ └── project:acme/platform/some-repo └── group:acme/cadastreuser:acme:alice ← personal, only Alice can seeA user’s reachable set always includes global plus every group, parent group, and project they’re a member of, plus their own user: scope. Scopes they don’t belong to are simply invisible.
How recall filters
Section titled “How recall filters”When a client calls plur_recall or plur_inject:
- Server resolves the user’s readable scope set.
- Server passes that set to the engine as a filter.
- The engine only considers engrams whose
scopeis in the set.
You can’t recall what you can’t see — and you can’t tell what you can’t see exists. A user querying for “deploys” gets results from group:acme/platform (where they belong) and global, but not from scopes they aren’t members of.
How writes are gated
Section titled “How writes are gated”When a client calls plur_learn or POST /api/v1/engrams:
- Server resolves the user’s writable scope set.
- Server checks the target scope against the writable set.
- Mismatch →
403 Insufficient permission.
API keys can additionally carry an allowed_scopes restriction — a whitelist that narrows (never expands) what the owning user’s membership already permits.
Roles in PLUR Enterprise are global per user, not per-scope. Three tiers, assigned via env allowlists:
| Role | Capabilities |
|---|---|
viewer | Read-only on the admin surfaces. |
editor | Viewer + retire/pin/rescope engrams, trigger syncs. |
admin | Editor + system config: SSO providers, SCIM tokens, settings. |
Assignment priority: ROLE_USERS (explicit user:role pairs) → ADMIN_USERS (listed users get admin) → default viewer for any authenticated user.
Honest gap: per-scope role assignment (e.g. admin of one group, viewer of another) is not yet available, and neither is manual member management of groups from the dashboard. Roles are org-global; group membership comes from your IdP.
Group provisioning
Section titled “Group provisioning”Groups arrive from your identity provider and are read-only in the dashboard:
- SCIM — your IdP provisions them (Okta, Entra). See SCIM.
- GitHub/GitLab sync — when GitHub or GitLab is the IdP, org/team memberships sync automatically. The mapping is path-preserving:
org/teambecomesgroup:org/teamin PLUR.
Manual group creation and manual group→scope mapping from the dashboard are not yet available.
Pinning
Section titled “Pinning”pinned is a server-side flag on individual engrams, not a scope mechanism. Editors and admins can pin an engram so it bypasses the relevance gate at injection — useful for safety conventions and meta-rules. Every pin event is audited.
Validation
Section titled “Validation”The security posture is verified by the test suite (test/security/): postgres-roles.test.ts asserts the plur_app Postgres role can do what it must do and cannot do what it must not, alongside injection and runtime-grant suites. If you change the permission model, they must pass. They’re the contract.
What you can’t do
Section titled “What you can’t do”- Cross-org reads — orgs are isolated by construction (separate schemas, separate graphs).
- Write-up without membership — being in a group does not let you write to scopes outside your resolved set.
- Anonymous access — every request is authenticated; there is no public-read scope.