Skip to content

Operator scopes cleared for API clients authenticating via shared token (no device identity) #27494

@tobalsan

Description

@tobalsan

Context

I'm building a custom agent orchestration hub that communicates with OpenClaw via the Gateway WebSocket API. The hub connects as an operator role with ["operator.read", "operator.write"] scopes, authenticating with the shared gateway token (same token configured in gateway.auth.token).

The use case is essentially "importing" an OpenClaw agent into an external orchestration tool — sending messages, reading chat history, etc.

Observed behavior

When connecting, authentication succeeds (shared auth OK), and the connection is accepted. However, any chat.send or chat.history call fails with:

errorCode=INVALID_REQUEST errorMessage=missing scope: operator.read
errorCode=INVALID_REQUEST errorMessage=missing scope: operator.write

What I traced

Looking at ws-connection/message-handler.ts, the clearUnboundScopes() function (around line 420) clears all requested scopes when there's no device identity:

const clearUnboundScopes = () => {
  if (scopes.length > 0 && !controlUiAuthPolicy.allowBypass) {
    scopes = [];
    connectParams.scopes = scopes;
  }
};
// ...
if (!device) {
  clearUnboundScopes();
}

Since API clients don't provide a device identity (no browser SubtleCrypto), scopes get wiped before the connection proceeds — even though evaluateMissingDeviceIdentity and roleCanSkipDeviceIdentity correctly allow the connection through (operator + shared auth = OK to skip device identity).

So the connection itself is fine, but the scopes needed for API operations are gone.

Question

Is there an intended way for headless/API operator clients (authenticated via shared token, no device identity) to retain their requested scopes? Or is the current behavior unintentional for this use case?

The only config-level workaround I found is gateway.controlUi.dangerouslyDisableDeviceAuth, which sets allowBypass: true and preserves scopes — but that's clearly meant for the Control UI, not API clients.

A minimal change that would fix this case would be checking sharedAuthOk in clearUnboundScopes:

if (scopes.length > 0 && !controlUiAuthPolicy.allowBypass && !sharedAuthOk) {

But I wanted to ask first whether there's a design reason scopes are cleared for deviceless connections even when shared auth succeeds, or if there's a recommended pattern I'm missing.

Environment

  • OpenClaw: latest main
  • Connection: local WebSocket, operator role, shared token auth
  • Scopes requested: ["operator.read", "operator.write"]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions