Concepts/Roles and Rights

🔑 Roles and Rights

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:

  1. the entry’s own role;
  2. the role of the first rule of access.rules whose match fits the entry’s ROS name (and whose kinds, when it has any, include the entry’s kind);
  3. 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.

yaml
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_kinds locks a whole kind on every robot the app reaches: any of read, invoke, publish, assets and job_history.
  • locked locks single exposures of single robots, by slug. assets and job_history belong to the robot as a whole and are locked only as a kind.
json
{
  "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 no technician behaves like role: 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. admin inheriting [operate, maintenance] still holds operate after maintenance is 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 inherits of 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 access of 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 as collection_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 (operate for a holder of admin), 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:

yaml
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, naming assets or job_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.