Roles and features
Built-in and custom roles, feature grants, and priority-gated assignment.
Roles in Neuralis govern humans and agents with one vocabulary: the same grants that decide which tabs a user sees decide which tools, skills, and files an agent's model is even told about, and agent provisioning is gated exactly like inviting a colleague — nobody can mint an agent stronger than themselves.
Authorization has two orthogonal dimensions. Feature grants answer "what can this role do" — every route, tool, and contribution declares the feature it requires, checked server-side. Role priority answers a different question: "what strength of role may this caller hand out". Both are defined per project on the project's role definitions.
Built-in roles
Five built-in roles anchor the scale. Their priorities are immutable; their feature grants are project-editable like any role's.
| Role | Priority | Agent access | Can invite | Can manage roles | Default grants |
|---|---|---|---|---|---|
owner | 1 | all agents | yes | yes | * (wildcard) |
admin | 2 | all agents | yes | yes | every first-party non-platform.* feature, enumerated |
manager | 10 | all agents | no | no | curated feature set |
member | 20 | own agents | no | no | curated feature set |
viewer | 30 | view only | no | no | curated feature set |
The gaps between 2, 10, 20 and 30 are deliberate — see role priority.
The agents column is part of the role definition: * grants access to
every agent in the project, own restricts a user to agents they created or
are assigned to, and view is read-only. See
multi-tenancy for agent ownership.
Custom roles
Projects can define additional roles. A custom role is a normal role definition — its own feature grants, agent access, and an explicitly declared priority. Two rules keep custom roles safe:
- the name is cosmetic, the priority is enforced — calling a role "Director" grants nothing by itself;
- a role label with no declared priority resolves to the weakest possible strength, so an undeclared custom role can always be assigned but can never be an escalation.
Feature grants
A feature is a string key such as core.execute, drive.read, or
platform.config. The semantics are uniform across the platform:
- a role's
grantedFeaturesarray lists what it holds; the'*'wildcard passes every check, - a surface that requires multiple features requires all of them,
- a surface that declares no required features is ungated,
- a feature id that no loaded package provides is never granted to any role except a wildcard holder — so requiring an unknown feature is wildcard-holder-only by construction, deny-by-default.
Running a shell needs two ids, not one
core.execute is the id for running an agent — the chat loop, conversations,
the execute tool itself. Opening a shell additionally needs the entry
feature of the plane it opens on:
| Plane | Entry feature | Notes |
|---|---|---|
| The app container | exec.container | Granted wherever core.execute is. A packaged skill's shell scripts therefore need BOTH — core.execute to run the tool, exec.container to open the shell. |
| The app container, unrestricted | exec.unconfined | Not an entry but a bypass: it skips the path policy and the OS sandbox inside the container, and opens the container plane by itself. Owner/admin by default. |
| The operator's host machine | exec.host | Admin tier and never a bypass; an operator must also have provisioned the host broker. |
| A virtual desktop (or any other exec-capable source) | Whatever that source's connector DECLARES | Each connector names its own entry id in its manifest; the desktop's is exec.machine. |
An entry feature grants nothing by itself. After it, the source's own path
policy decides whether that caller may start a command in that directory — the
same exec bit the Files UI shows, per role, user and agent.
The two tiers
The prefix states where a decision ACTS, so a feature's blast radius is readable from its id:
project.*— the decision acts inside your current project. It never reads or writes another tenant.platform.*— the decision crosses the project boundary: another tenant's data, a platform-global file, cross-user records, or the platform-wide credential scope.
No platform.* feature carries a default role grant. They are grantable —
an owner can hand one to any role — but nothing seeds them.
The split is what makes the project tier safe to grant broadly. It replaced a single administration namespace in which one id simultaneously opened the Users tab, the cross-tenant project list, the platform audit log and the whole cross-scope guard. Those are four independent powers and are now four independently grantable ids.
What the seeded roles hold
The owner role holds the '*' wildcard.
The admin role holds the enumerated union of every first-party
non-platform.* feature, derived from the package manifests rather than
hand-listed. So an admin is the strongest role inside a project, and does not
cross the tenant boundary by default: they cannot see a project they are not a
member of, read the platform audit log, patch platform settings, or act on
another member's scope.
Two consequences worth knowing:
- Because the admin list is enumerated rather than a wildcard, an admin does not automatically inherit features contributed by a project-dropped package. Those features stay listed in the role editor and an owner can grant them by hand. This is deliberate — a dropped package must not be able to grant itself onto the strongest in-project role.
- The wildcard itself is not restricted: a role that holds
'*'can still grant'*'to another role. What changed is that the seededadminrole no longer ships with it.
Governance is a flag, not a feature
Editing a project's role map, members, limits or agent ownership is gated by the
role's stored canManageRoles flag — not by a feature. Governance must not be
self-grantable through a capability toggle. A role granted project.roles can
READ the role map; rewriting it additionally requires the flag.
Checks happen server-side before data access; UI affordances (hidden tabs, filtered lists) mirror the same grants but are never the defense. Feature gating is also applied to what agents see: skills, instructions, and tools from a package the caller cannot use are absent from the model's context entirely (see the security model).
Role priority
Priority is one ordinal number per role, lower = stronger. The built-in
roles anchor the scale — owner 1, admin 2, manager 10, member 20, viewer 30 —
and a role you define declares its own number anywhere in 1..99. The gaps are
deliberate: a custom role slots between two built-ins without renumbering
anything. The assignment rule:
A caller may assign a role only if the target role's priority is greater than or equal to the caller's own.
So a member (priority 20) can hand out member or viewer, but cannot
invite an admin or mint an admin-strength agent; an admin (priority 2)
cannot promote anyone to owner. The rule is enforced server-side at every
assignment surface — project invites, role changes and agent provisioning — by
one shared predicate; the admin UI additionally clamps its pickers to assignable
roles, but the server decision is authoritative. Disallowed assignments return
403.
Member strength is always derived from the member's role; there is no per-member priority override. Removing a member is governed by the same rule: a caller cannot remove someone currently stronger than themselves.
Editing a role definition
Changing what a role is — its grants, its flags, its own priority — is a stricter act than handing that role to someone, and it follows two rules of its own:
Strength. You may edit only roles strictly weaker than your own priority. A role at or above your strength cannot be modified and cannot be deleted — including your own role.
Conservation. You may grant only, and revoke only, features you hold yourself.
The strictness on your own role is deliberate: it makes locking yourself out of your own project structurally impossible rather than merely unlikely. The practical consequence is that governance flows strictly downward — a role is tuned by something stronger than itself, and the strongest role in a project is fixed once the project is created. Defining a new role is the one place equality is allowed, so a second owner-strength role remains possible.
Conservation is symmetric on purpose. Being unable to hand out authority you lack is the obvious half; being unable to destroy authority you lack is the half that stops a project administrator from quietly stripping capabilities from a role above them. Deleting a role runs the same check over everything that role held, so deletion is not a way around it.
Both role and membership maps are submitted whole, so leaving an entry out is a deletion and is authorized as one.
The 1..99 range is a hard floor, not a convention: both role write paths — the
project PATCH and the admin Config → Roles editor — reject anything outside it
through one shared body, including 0, 100, a fraction, and a numeric string.
Built-in priorities cannot be changed at all. That shared body is also where the
two editing rules above live, so the two surfaces cannot answer differently for
the same request. Existing projects created
before the scale moved are migrated on first read; a custom role keeps its
strength relative to the built-ins around it, and where the old number is
ambiguous the migration always resolves it toward the weaker end.
Who created a project is not who governs it
The project record keeps a creator field for provenance and display, and it
grants nothing. Everything that used to key on it now keys on strength or on a
feature: archiving, restoring and permanently deleting a project — and changing
its name or description, which every member's agents read — require an
owner-strength role (priority 1 or stronger) in that project, while acting
on a platform user's account — disabling, re-enabling, resetting the password,
renaming or deleting it — requires governing that user in every project they
belong to (a member there with the invite flag and a role at least as strong as
theirs), and deleting additionally needs the platform.users feature.
Because no role is seeded with platform.users, its holders out of the box are
the wildcard (owner-strength) roles, until an owner grants it deliberately. So
transferring governance is just a role change, and a creator who was later
demoted keeps no residual powers — only the creator record itself cannot be
deleted while it is some project's creator.
A custom role you define at priority 1 governs like the built-in owner across
the layers that decide capability: feature grants, priority thresholds and
route guards all read strength and grants, never the name. One boundary is
worth knowing before you rely on it — per-source
URI policies may carry per-role path
overrides that are keyed on the role name. A custom role matches none of
the built-in keys there, so it falls back to the source's default permissions
instead of inheriting an owner-keyed override. That resolves fail-closed —
less path access than owner, never more — but it does mean a custom
owner-strength role needs its path overrides granted explicitly on the sources
that define them.
Governance of the project record itself — members, roles, agent ownership and
limits — is a separate per-role flag (canManageRoles) rather than a
feature, so it can never be self-granted through a capability toggle. The
built-in owner and admin ship with it; any role you define can be given it,
and a role you configure without it does not get it back by being named
something familiar.
How packages contribute features
Packages own their feature vocabulary — nothing is hardcoded in the host or the admin UI:
- A package's manifest declares the features it provides, either as bare
ids or as
{ id, title, description }objects that drive the labels and tooltips in the roles editor. - A package may declare default role grants — features that should land on specific roles when the package is installed. They merge into the project's roles once per package version, so an admin's later manual revoke is not silently re-applied on the next restart.
- Uninstalling a project package revokes the features it granted (except any feature another loaded package still provides).
- The roles editor consumes a runtime feature catalog built from the packages installed in that project, including which tools, skills, commands, and widgets each feature gates. Roles are project-level, so the catalog is too: another project's packages, the feature titles and descriptions they declare, and the contribution names they list as consumers are all absent — a package appears only once it is installed here.
The package-side contract — declaring providesFeatures, requiring features
on routes and tools, and gating contributions — is documented in
features and access. The admin
screens for editing roles are covered under
the admin package.