mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
53dcaaedec
* feat(gateway): generic operator roles for non-maintainer access Adds gateway.roles: named role bundles over a closed capability set — sessions.others (none/view/suggest/write), an agents allowlist, and an operator-scope ceiling. Roles are person-level (additive user_profiles.role column, SQLite stays at v9); users.setRole (admin-only) assigns them. With no gateway.roles config, behavior is unchanged for solo deployments. Enforcement is deny-by-default from a host-minted actor identity (system vs operator+profileId on server-only client.internal, never accepted from the wire) and covers every entry point: WS RPCs, OpenAI-compat and Responses HTTP, tools invocation, cron, questions, usage, task suggestions, session catalog/sharing/reads. The agents allowlist gates both session creation and run-start on existing sessions. Subagent completion announce and descendant wake mint explicit system authority so role boundaries never silently drop parent notifications. The enforcement surface is expressed through a narrow policy vocabulary (operatorSessionCap, hasOperatorBoundary, authorizeSessionSharing) rather than per-handler policy internals. * fix(gateway): heal PR CI after rebase onto main - Break import cycles: extract GatewayOperatorRoleActor leaf contract; merge session-group-mutation-targets into session-sharing-target-input. - Split sessions-suggestions.test.ts (max-lines) into a visibility suite. - Add users.setRole to the 2026.8 train registry test and regenerate the Kotlin protocol client. - Startup UNAVAILABLE gating now precedes session authorization: session stores are not loaded during startup, so authorization reads would deny with a misleading non-retryable error. - sessions.assignOwner keeps its documented visibility-authorized contract when no operator role caps the caller; view/suggest-capped roles still cannot reassign foreign session ownership. - Test stubs updated for main's socket readyState guard (#128144) and the system-authority arg on channel-native resets. * test(gateway): chat.send pending-profile dispatch carries its required session target chat.send requires a non-empty sessionKey at the protocol level; the mutation pipeline now rejects targetless frames before profile-dependent dispatch, so the pending-profile test must send a realistic frame.
341 lines
19 KiB
Markdown
341 lines
19 KiB
Markdown
---
|
|
summary: "Operator roles, scopes, and approval-time checks for Gateway clients"
|
|
read_when:
|
|
- Debugging missing operator scope errors
|
|
- Reviewing device or node pairing approvals
|
|
- Adding or classifying Gateway RPC methods
|
|
title: "Operator scopes"
|
|
---
|
|
|
|
Operator scopes gate what a Gateway client can do after it authenticates.
|
|
They are a control-plane guardrail inside one trusted Gateway operator domain,
|
|
not hostile multi-tenant isolation. For strong separation between people,
|
|
teams, or machines, run separate Gateways under separate OS users or hosts.
|
|
|
|
Related: [Security](/gateway/security), [Gateway protocol](/gateway/protocol),
|
|
[Gateway pairing](/gateway/pairing), [Devices CLI](/cli/devices).
|
|
|
|
## Connection roles
|
|
|
|
Every Gateway WebSocket client connects with one role:
|
|
|
|
- `operator`: control-plane clients such as CLI, Control UI, automation, and
|
|
trusted helper processes.
|
|
- `node`: capability hosts (macOS, iOS, Android, headless) that expose
|
|
commands through `node.invoke`.
|
|
|
|
Operator RPC methods require the `operator` role; node-originated methods
|
|
require the `node` role.
|
|
|
|
## Scope levels
|
|
|
|
| Scope | Meaning |
|
|
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `operator.read` | Read-only status, lists, catalog, logs, session reads, retained audit and execution-identity diagnostics, and other non-mutating calls. |
|
|
| `operator.write` | Mutating operator actions: sending messages, invoking tools, updating talk/voice settings, node command relay. Also satisfies `operator.read`. |
|
|
| `operator.admin` | Administrative access. Satisfies every `operator.*` scope. Required for config mutation, updates, native hooks, reserved namespaces, and high-risk approvals. |
|
|
| `operator.pairing` | Device and node pairing management: list, approve, reject, remove, rotate, revoke. |
|
|
| `operator.approvals` | Exec and plugin approval APIs. |
|
|
| `operator.questions` | Listing, reading, answering, and resolving interactive questions. |
|
|
| `operator.talk` | Creating, steering, and closing Talk sessions without general Gateway write access. `operator.write` also satisfies this scope. |
|
|
| `operator.talk.secrets` | Reading Talk configuration with secrets included. |
|
|
|
|
Unknown future `operator.*` scopes require an exact match unless the caller
|
|
already holds `operator.admin`.
|
|
|
|
## Named operator roles
|
|
|
|
Team Gateways can bind authenticated user profiles to named operator roles.
|
|
Each role combines exactly three closed policies: access to other people's
|
|
sessions, agents available for session creation and agent runs, and a maximum
|
|
set of operator scopes.
|
|
|
|
```json5
|
|
{
|
|
gateway: {
|
|
roles: {
|
|
default: "guest",
|
|
definitions: {
|
|
maintainer: {
|
|
sessions: { others: "write" },
|
|
agents: "*",
|
|
scopes: ["operator.read", "operator.write", "operator.approvals"],
|
|
},
|
|
guest: {
|
|
sessions: { others: "view" },
|
|
agents: ["guest-agent"],
|
|
scopes: ["operator.read", "operator.write"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
Use the administrator-scoped `users.setRole` Gateway method with
|
|
`{ profileId, role }` to assign a configured role; set `role: null` to clear an
|
|
assignment. Assignment changes immediately invalidate and close that profile's
|
|
active Gateway connections; reconnecting applies the current role and scope
|
|
ceiling. `gateway.roles.default` is required whenever roles are configured,
|
|
must name an existing definition, and applies to profiles without a valid
|
|
assigned role. Omitting `gateway.roles` entirely leaves solo and shared-secret
|
|
deployments unchanged.
|
|
|
|
When roles are configured, identity-authenticated operator connections do not
|
|
receive reusable device or bootstrap tokens: those tokens are not bound to a
|
|
person and could bypass the role ceiling. Device-token or bootstrap-token
|
|
authentication without a verified user identity is rejected for operator
|
|
Gateway connections and HTTP requests. Reconnect through the trusted proxy or
|
|
another supported verified identity, such as Tailscale; node connections,
|
|
shared-secret/password access, and Gateways without role configuration retain
|
|
their existing behavior.
|
|
|
|
For sessions created by other people, `sessions.others` supports these values:
|
|
|
|
- `"none"`: hides foreign sessions from lists and targeted access, filters
|
|
session-level usage to visible sessions, and denies Gateway-wide `usage.cost`
|
|
because its aggregate can include hidden sessions.
|
|
- `"view"`: allows reading but does not allow mutation, even when a session is
|
|
otherwise shared.
|
|
- `"suggest"`: allows viewing and the existing suggestion flow.
|
|
- `"write"`: allows participation in foreign sessions; draft and incognito
|
|
restrictions remain in force.
|
|
|
|
A person always owns their own sessions. Explicit session membership can raise
|
|
`"view"` or `"suggest"` access for a specific session, and connections already
|
|
holding `operator.admin` retain their administrative session access.
|
|
|
|
Set `agents: "*"` to allow session creation and agent runs on every agent, list
|
|
agent IDs to allow only those agents, or use an empty array to disallow both.
|
|
The allowlist also applies when a run targets an already-existing session.
|
|
The role's `scopes` list intersects scopes granted through connection auth,
|
|
identity grants, pairing, scope upgrades, and authenticated trusted-proxy HTTP
|
|
requests. It cannot grant scopes the connection did not already receive.
|
|
Control UI plugin grants carry the authenticated profile inside a signed
|
|
cookie; plugin HTTP requests reapply the profile's current role ceiling and
|
|
reject grants without a matching durable identity when roles are enabled.
|
|
Include `operator.admin` explicitly only when that role should retain
|
|
administrative connection authority.
|
|
|
|
Named roles apply only to connections with an authenticated durable user
|
|
profile. They organize collaboration within one trusted Gateway domain and do
|
|
not replace separate Gateways when hostile-tenant isolation is required.
|
|
Diagnostic audit methods, including `audit.run.inspect`, remain shared-domain
|
|
`operator.read` surfaces and are not filtered by session role. Likewise,
|
|
`operator.write` still authorizes Gateway-wide operations such as tool
|
|
invocation, ordinary node command relay, and other write-scoped control-plane
|
|
actions; session restrictions do not turn that scope into a per-person
|
|
isolation boundary. Use separate Gateways when mutually untrusted people must
|
|
not share diagnostics or control-plane write authority.
|
|
|
|
## Identity scope grants
|
|
|
|
`gateway.auth.identityScopes` grants operator scopes to verified user
|
|
identities from trusted-proxy auth or Tailscale WhoIs:
|
|
|
|
```json5
|
|
{
|
|
gateway: {
|
|
auth: {
|
|
identityScopes: {
|
|
"admin@example.com": ["operator.admin"],
|
|
"operator@example.com": ["operator.read", "operator.write"],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
The key is the verified proxy identity or Tailscale WhoIs login. Email keys
|
|
match case-insensitively; non-email identities match exactly. Config validation
|
|
rejects scope names outside the closed set above.
|
|
|
|
Connection authority is resolved in this order:
|
|
|
|
1. For trusted-proxy Control UI connections, `x-openclaw-scopes` first caps
|
|
device enrollment or upgrade requests. Device authorization then establishes
|
|
the persistent scopes; a device-less session contributes no self-declared
|
|
scopes.
|
|
2. OpenClaw unions a matching server-side identity grant with those scopes.
|
|
3. OpenClaw applies `x-openclaw-scopes` to the final union as the session cap.
|
|
An absent header means no cap; a present-but-empty header yields no scopes.
|
|
4. If the authenticated profile has an effective named operator role,
|
|
OpenClaw intersects the result with that role's configured scope ceiling.
|
|
|
|
The result is used for both `hello.auth.scopes` and Gateway method
|
|
authorization. Identity grants are session-only: they do not create or modify
|
|
pairing records or request a device scope upgrade. Token, password, and no-auth
|
|
connections carry no verified identity and receive no grant.
|
|
Identity grants apply only to `operator`-role connections; `node`-role connections never receive them.
|
|
|
|
## Method scope is only the first gate
|
|
|
|
Each Gateway RPC has a least-privilege method scope that decides whether a
|
|
request reaches its handler. Params-aware methods derive that scope before
|
|
dispatch so authorization failures have one canonical structured response:
|
|
|
|
- `agent` needs `operator.write` for ordinary turns and `operator.admin` for
|
|
`/new` or `/reset` session lifecycle commands.
|
|
- `node.invoke` needs `operator.write` for ordinary relay commands and
|
|
`operator.admin` when relaying `browser.proxy`, `browser.proxy.upload.v1`,
|
|
`fs.listDir`, or `terminal.upload` to a node.
|
|
- The top-level `fs.listDir` RPC needs `operator.write` for Gateway-host
|
|
requests and `operator.admin` when `nodeId` targets a node. Its handler limits
|
|
non-admin Gateway-host browsing to configured agent workspaces.
|
|
- `sessions.create` needs `operator.write` for ordinary creation, including a
|
|
`projectId`, and `operator.admin` for incognito sessions or any `execNode`
|
|
request. For non-admin callers, the handler limits `cwd` to configured agent
|
|
workspaces; `projectId` cannot be combined with `cwd` or `execNode`.
|
|
- `environments.list` needs `operator.read`. Session placement methods derive
|
|
their scope from the requested target before schema validation:
|
|
`sessions.dispatch` needs `operator.write` for `deviceId` and
|
|
`operator.admin` for `profileId` or a target-less
|
|
`cloudWorkers.projectProfiles` lookup; `sessions.move` needs `operator.write`
|
|
for Gateway or device targets and `operator.admin` for profile targets;
|
|
`abandonSource: true` remains `operator.write` but is schema-valid only with
|
|
a Gateway target and runtime-valid only for an exact offline device source;
|
|
`sessions.reclaim` remains `operator.write`. Malformed dispatch params or a
|
|
malformed move target use `operator.write` so the handler can return the
|
|
precise schema error. All three methods retain session ownership,
|
|
participation, and commit-time revalidation fences. `operator.read` alone
|
|
cannot start, stop, or move a session. Cloud profile allocation and mutation,
|
|
pairing and Connect machine, raw `environments.create` or
|
|
`environments.destroy`, incognito sessions, direct `execNode` execution, and
|
|
arbitrary host or node paths remain `operator.admin`.
|
|
- `worktrees.branches` needs `operator.write`. Its handler limits non-admin
|
|
callers to workspace-contained paths or registered-project roots; other host
|
|
paths require `operator.admin`.
|
|
- `talk.config` needs `operator.read`; `includeSecrets: true` also needs
|
|
`operator.talk.secrets`.
|
|
- `talk.client.*`, `talk.session.*`, `talk.speak`, and `talk.mode` need
|
|
`operator.talk` (or the compatible broader `operator.write`).
|
|
- `sessions.patch` needs `operator.write` for session organization fields and
|
|
the per-session `model` override. Other runtime overrides, including
|
|
thinking, fast, verbose, trace, and reasoning levels, need `operator.admin`.
|
|
Persisting a selected model as the configured agent default is also
|
|
admin-only.
|
|
|
|
Project RPCs use these scopes:
|
|
|
|
| Method | Required scope and additional gate |
|
|
| -------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
| `projects.list` | `operator.read`; only callers satisfying `operator.write` receive `repoRoot` and `originUrl`. |
|
|
| `projects.add` | `operator.write` and the `controlPlaneWrite` method flag. |
|
|
| `projects.register`, `projects.remove` | `operator.admin`. |
|
|
| `projects.searchRemote` | `operator.read`. |
|
|
|
|
Some handlers then apply stricter checks based on the concrete thing being
|
|
approved or mutated:
|
|
|
|
- `device.pair.approve` is reachable with `operator.pairing`, but approving an
|
|
operator device can only mint or preserve scopes the caller already holds.
|
|
- `node.pair.approve` is reachable with `operator.pairing`, then derives extra
|
|
approval scopes from the pending node's declared command list.
|
|
- `chat.send` is a write-scoped method, but the `/config set` and
|
|
`/config unset` chat commands require `operator.admin` on top of that,
|
|
regardless of the caller's chat-send scope.
|
|
|
|
This lets lower-scope operators perform low-risk pairing actions without
|
|
making all pairing approval admin-only.
|
|
|
|
Session mutation RPCs are authorized by their negotiated operator scopes,
|
|
independent of the connecting client's `client.id` or `client.mode`. Client
|
|
identity can still affect connection and device-auth policy, but it neither
|
|
grants nor removes session mutation authority.
|
|
|
|
`audit.run.inspect` intentionally uses `operator.read`. Every client with that
|
|
scope in a Gateway operator domain may receive the retained execution-identity
|
|
context, including bounded pseudonymized references and secret-redacted display
|
|
labels. `operator.read` is not a per-user or hostile multi-tenant privacy
|
|
boundary. Operators who must keep this data separate need separate Gateway
|
|
trust domains.
|
|
|
|
## Device pairing approvals
|
|
|
|
Device pairing records are the durable source of approved roles and scopes.
|
|
An already-paired device does not get broader access silently: a reconnect
|
|
that asks for a broader role or broader scopes creates a new pending upgrade
|
|
request.
|
|
|
|
A connected limited Control UI can file that same pending request through its
|
|
**Request admin** banner without attempting a broader reconnect. The banner can
|
|
collapse into a persistent **Limited access** chip that reopens the action. The request is
|
|
bound to the signed device identity on the live connection. Approval still
|
|
comes from `device.pair.approve` and therefore requires `operator.pairing` plus
|
|
authority for every requested scope. After approval rotates the operator token,
|
|
the Gateway returns the new token only to that device's live waiter; the browser
|
|
stores it before reconnecting. Canceling the wait or disconnecting before
|
|
approval falls back to the ordinary pairing repair flow on the next connection.
|
|
|
|
The explicit exception is the administrator-capable Control UI owner profile
|
|
issued directly on the Gateway host by `openclaw dashboard` or graphical
|
|
onboarding. Its short-lived, single-use bootstrap can approve the exact closed
|
|
scope set for a fresh browser or upgrade an existing limited credential only
|
|
when it binds to that same signed browser keypair. Generic Control UI and
|
|
Telegram handoffs, mobile setup profiles, shared credentials, locality, and
|
|
caller-selected scopes do not receive this exception.
|
|
|
|
Approving a device request:
|
|
|
|
- A request with no operator role does not need operator scope approval.
|
|
- A request for a non-operator device role (for example `node`) requires
|
|
`operator.admin`, even though `device.pair.approve` itself only needs
|
|
`operator.pairing`.
|
|
- A request for `operator.read`, `operator.write`, `operator.approvals`,
|
|
`operator.questions`, `operator.pairing`, `operator.talk`, or
|
|
`operator.talk.secrets` requires
|
|
the caller to already hold that scope, or `operator.admin`.
|
|
- A request for `operator.admin` requires `operator.admin`.
|
|
- A repair request with no explicit scopes can inherit the existing operator
|
|
token's scopes; if that token is admin-scoped, approval still requires
|
|
`operator.admin`.
|
|
|
|
Non-admin shared-secret and trusted-proxy sessions can only approve
|
|
operator-device requests within their own declared operator scopes; approving
|
|
non-operator roles is admin-only even when those sessions can otherwise use
|
|
`operator.pairing`.
|
|
|
|
For paired-device token sessions, management is self-scoped unless the caller
|
|
has `operator.admin`: a non-admin caller sees only its own pairing entries, and
|
|
can approve, reject, rotate, revoke, or remove only its own device entry.
|
|
|
|
## Node pairing approvals
|
|
|
|
`node.pair.*` capability approvals are stored on the paired device record in
|
|
the shared SQLite pairing store. Gateways migrate any remaining entries from
|
|
the retired standalone `nodes/paired.json` store into those records once at
|
|
startup. See [Gateway pairing](/gateway/pairing) for details.
|
|
|
|
`node.pair.approve` derives extra required scopes from the pending request's
|
|
command list:
|
|
|
|
| Declared commands | Required scopes |
|
|
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
|
|
| none | `operator.pairing` |
|
|
| ordinary node commands | `operator.pairing` + `operator.write` |
|
|
| `system.run`, `system.run.prepare`, `system.which`, `browser.proxy`, `browser.proxy.upload.v1`, `fs.listDir`, or `system.execApprovals.get/set` | `operator.pairing` + `operator.admin` |
|
|
|
|
Here, `fs.listDir` is the node command declared for relay through `node.invoke`,
|
|
not the top-level Gateway RPC described above.
|
|
|
|
Approving a node declaration records its command surface. For `computer.act`,
|
|
the node advertises that surface only after Computer Control is enabled locally;
|
|
once the pairing update is approved, invoking it through `node.invoke` requires
|
|
write scope but not admin scope for each action. Commands classified as
|
|
dangerous or privacy-heavy still require a persistent
|
|
`gateway.nodes.commands.allow` entry in addition to pairing.
|
|
|
|
Node pairing establishes identity and trust; it does not replace a node's own
|
|
`system.run` exec approval policy.
|
|
|
|
## Shared-secret auth
|
|
|
|
Shared gateway token/password auth is treated as trusted operator access for
|
|
that Gateway. OpenAI-compatible HTTP surfaces, `/tools/invoke`, and HTTP
|
|
session-history endpoints restore the full default operator scope set for
|
|
shared-secret bearer auth, even if a caller sends narrower declared scopes.
|
|
|
|
Identity-bearing modes, such as trusted proxy auth or private-ingress `none`,
|
|
can still honor explicit declared scopes. Use separate Gateways for real trust
|
|
boundary separation.
|