Skip to content

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.

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.

global
├── group:acme/platform
│ └── group:acme/platform/backend
│ └── project:acme/platform/some-repo
└── group:acme/cadastre
user:acme:alice ← personal, only Alice can see

A 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.

When a client calls plur_recall or plur_inject:

  1. Server resolves the user’s readable scope set.
  2. Server passes that set to the engine as a filter.
  3. The engine only considers engrams whose scope is 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.

When a client calls plur_learn or POST /api/v1/engrams:

  1. Server resolves the user’s writable scope set.
  2. Server checks the target scope against the writable set.
  3. 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:

RoleCapabilities
viewerRead-only on the admin surfaces.
editorViewer + retire/pin/rescope engrams, trigger syncs.
adminEditor + 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.

Groups arrive from your identity provider and are read-only in the dashboard:

  1. SCIM — your IdP provisions them (Okta, Entra). See SCIM.
  2. GitHub/GitLab sync — when GitHub or GitLab is the IdP, org/team memberships sync automatically. The mapping is path-preserving: org/team becomes group:org/team in PLUR.

Manual group creation and manual group→scope mapping from the dashboard are not yet available.

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.

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.

  • 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.