Who may do what is decided in three places, and each has one job.
| Where | What it says |
|---|---|
| The organisation | Which roles exist, how they inherit, and which role holds each kind of access when nothing else is said. |
A robot’s fleetless.yaml |
Only the exceptions: the entry, the namespace or the robot-level capability that needs a different role. |
| An app | Which role each user holds, and what the app locks. A lock takes a right away and never adds one. |
Roles belong to the organisation
A role has a key, which fleetless.yaml and every assignment write, a
display name, and the roles it inherits from. A new organisation
starts with three: observe, operate (inherits observe) and admin
(inherits operate). Add your own, edit these, delete any of them.
A role inherits from any number of parents, and it holds everything it inherits, however many steps away:
| Key | Inherits | Holds |
|---|---|---|
observe |
nothing | observe |
operate |
observe |
operate, observe |
maintenance |
observe |
maintenance, observe |
admin |
operate, maintenance |
admin, operate, maintenance, observe |
A requirement that names operate is therefore met by operate and by
admin, and not by observe or maintenance. One requirement can also name
a list: holding any role on it is enough.
A key is lower-case, starts with a letter, continues with letters, digits and
underscores and is at most 63 characters; defaults is reserved. A name is 1
to 60 characters, and an organisation holds at most 50 roles. A request that
breaks one of these is 400 validation_error, and a key that another role
already has is 409 role_key_taken.
Beyond those, creating, renaming and deleting a role is accepted, with one
refusal: an inherits list that leads back to the role itself is
400 role_cycle, and the refusal names the cycle.
Roles are managed on the Roles page of the console, through /api/org/roles
and through the MCP tools console_roles_read and console_roles_write.
Defaults per kind
Every capability has a kind, and the organisation says which role holds a
kind when a robot’s fleetless.yaml says nothing.
| Kind | What it covers | Starts as |
|---|---|---|
read |
datapoints and cameras | observe |
invoke |
services and actions | operate |
publish |
publishers, which is driving the robot | operate |
assets |
the asset store: URDF and meshes | observe |
job_history |
past job runs, not only live ones | operate |
A default names one role or a list. A kind with no default belongs to nobody
unless a robot’s document says otherwise, admin included. The built-in
datapoint bridge_state has no entry of its own and follows the default of
read.
A job run names the person who started it, so whoever may read the history
learns who else has been driving the robot. That is why job_history is its
own kind rather than a part of invoke.
Change the defaults on the Roles page, with PUT /api/org/roles/defaults,
or with console_roles_write set_defaults.
Where a robot’s rights are written
For an entry of a robot, the requirement is the first of these that applies:
- the entry’s own
role; - the
roleof the first rule ofaccess.ruleswhosematchfits the entry’s ROS name (and whosekinds, when it has any, include the entry’s kind); - the organisation’s default for the entry’s kind.
For assets and job_history it is access.assets or access.job_history
of the robot, and otherwise the organisation’s default.
fleetless: 1
access:
assets: operate
rules:
- match: /arm/**
role: admin
- match: /diagnostics/*
kinds: [read]
role: maintenance
datapoints:
battery:
topic: /battery_state
type: sensor_msgs/msg/BatteryState
diag_summary:
topic: /diagnostics/summary
type: diagnostic_msgs/msg/DiagnosticArray
raw_diagnostics:
topic: /diagnostics/raw
type: diagnostic_msgs/msg/DiagnosticArray
role: admin
services:
arm_home:
ros_name: /arm/home
type: std_srvs/srv/Trigger
With the roles above, this robot asks for:
| What | Decided by | Needs |
|---|---|---|
battery |
the default for read |
observe |
diag_summary |
the second rule | maintenance |
raw_diagnostics |
its own role; the second rule fits as well and is not consulted |
admin |
arm_home |
the first rule | admin |
| assets | access.assets |
operate |
| job history | the default for job_history |
operate |
Only the published version counts. A robot with nothing published has no exposures, and its assets and job history follow the defaults.
The robot ignores role and access: the cloud enforces them, and a version
that changes only them reaches no robot. The fields themselves are in
Roles & Access.
An app assigns one role per user, and locks
An app user holds exactly one role, by key. So does every invitation, and so does the app’s default role, which a user gets when they are created, invited or register without a role named. An app with no default role refuses a create or an invite that names none.
An app user may use something on a robot when the robot is attached to the app, their role meets the requirement above, and the app has not locked it. Attaching a robot is what brings its rights along: there is nothing to tick per role and robot.
An app can only lock. A lock wins over every role, never grants anything, and applies to the app’s users and to its server keys alike.
locked_kindslocks a whole kind on every robot the app reaches: any ofread,invoke,publish,assetsandjob_history.lockedlocks single exposures of single robots, by slug.assetsandjob_historybelong to the robot as a whole and are locked only as a kind.
{
"default_role": "observe",
"locked_kinds": ["publish"],
"locked": [{ "robot_id": "0b7c5e1a-3f42-4d6b-9a18-5c2e7d90f3a4", "slugs": ["arm_home"] }]
}
Users of this app start as observe. Nobody can drive a robot through it, and
nobody can call arm_home on that one robot, whatever role they hold.
A lock that names a slug the robot does not publish, or a robot the app does
not attach, is accepted and comes back in warnings as unknown_lock_slug or
lock_robot_not_attached. The slug rename carries the locks along, and a robot
taken off the app takes its locks with it.
A write to the app’s access replaces the whole of it. It is the Access page
of the app in the console, GET and PUT /api/apps/:id/access, and the MCP
tools console_apps_read access and console_apps_write set_access.
A write sends back the revision it read, over the API and in set_access
alike. One composed from an older revision is refused as
409 target_state_conflict, rule stale, because a slug rename or a robot
taken off the app changes the access without a write of its own, and a stale
body would lift a lock it never saw. Without a revision the body replaces whatever is stored.
Unknown and deleted roles
Nothing here is strict. A role key that names no role grants nothing and never blocks anything.
- In a requirement the key is skipped.
role: [maintenance, technician]with notechnicianbehaves likerole: maintenance; a requirement that names only unknown roles can be met by no role at all. - Inheritance through a missing parent lapses, and the role’s other parents
still count.
admininheriting[operate, maintenance]still holdsoperateaftermaintenanceis deleted. - Deleting a role takes effect at once. Users who held it keep their account and can sign in, and reach nothing. Their open realtime subscriptions end.
Everything that names such a key turns into a warning, so the gap is visible:
| Where the key is named | Where the warning shows |
|---|---|
A robot’s fleetless.yaml |
The editor and the publish response: unknown_role, and rule_matches_nothing for a rule that fits nothing. |
The access of a collection in an app’s collections schema |
The schema preview, the publish response and the console: unknown_role. |
| Anywhere in the organisation | The Roles page and GET /api/org/roles: the keys that are no role, with every place that names them. |
| An app user | The app’s user list: No role, no rights. |
| An app’s default role | The app’s Access page: unknown_default_role. |
Creating a role under the same key makes those places count again.
Renaming a key is accepted. The places that name the old key keep it and
turn into warnings, unless you ask to update them: Also update N references
in the console, update_references: true in the API and in MCP.
That call answers a preview first, 409 confirmation_required: every place
with its document and count, and a confirmation token. Send the token back and
one transaction rewrites:
- app user assignments, invitations, app default roles, the
inheritsof other roles and the defaults; - each affected published
fleetless.yaml, which gets a new version that differs only in the key, and a draft, which is updated the same way; - the
accessof the collections in each affected app’s collections schema: the published schema gets a new version that differs only in the key and changes no record, and the draft is updated the same way. The new version is on the audit log ascollection_schema.published, with the role rename as its cause, and the app’s open realtime subscriptions are judged again as after a publish.
The preview lists these places with the kind collection_schema, the app, the
path in the schema (collections.notes.access.update.own[1]) and the version
the rename would give it. A rename waits a moment for an app whose collections
are busy, for instance because of a restore; if they stay busy it changes
nothing and answers 429 rate_limited with retry_after_ms, and the call can
be repeated. A schema whose text cannot be rewritten without losing it, because
an access rule is written as a YAML alias for instance, refuses the rename with
400 validation_error naming update_references and changes nothing: rename
the role without update_references and edit that schema by hand.
If anything the preview covers changes meanwhile, the token is stale and the call answers a new preview.
View as role
To see what a role gets before you rely on it, use Access → View as role in
the console, GET /api/apps/:id/effective-rights?role=<key> (or ?user=<id>),
or console_roles_read effective_rights. For each robot attached to the app
the answer lists every capability with:
- whether it is granted;
- the requirement and where it came from: the entry, a rule, the robot’s
access, or the organisation’s default; - the required role the holder inherits the right from (
operatefor a holder ofadmin), and none when the holder’s own role is named; - the app’s lock, if one applies.
It also shows the MCP datasheet: what robots.describe would list for a
session of that role, as described in
MCP for app users. A robot where nothing at all is
granted, the built-in datapoints included, is marked as not reachable, and
robots.list leaves it out for that role. A role that holds the default of
read is therefore reachable even when every capability in the list reads not
granted.
A key that names no role is not an error: nothing is granted, and the answer says so.
Two patterns
Name only exceptions. The defaults are what most entries need, so leave
role out and write one only where a robot differs. A namespace that differs
takes one rule rather than a role on every entry:
access:
rules:
- match: /arm/**
role: admin
Lock in the app, never extend. A robot’s document and the organisation’s
defaults say what a role can reach at most, and an app narrows that for its own
users. An app that needs more than the robot gives is changed at the robot’s
document or at the role, not at the app. A read-only dashboard for a customer
locks invoke and publish and changes nothing else.
What a refused caller sees
- A slug the caller’s role does not reach is
403 forbidden:'arm_home' is not granted to your role, or it does not exist on this robot.The same sentence covers a slug the robot does not have, so a caller cannot map an app they hold no rights in. - A robot the app does not list is
404 not_found, whatever the role. - A robot-level capability the role lacks is
403 capability_required, namingassetsorjob_history.
Console members are never filtered by roles. An Owner and a Developer hold no role and reach everything, as before. Roles filter app users, and locks also bind an app’s server keys. See Manage Users and Roles for how app users get in.