-
-
Notifications
You must be signed in to change notification settings - Fork 69.5k
Operator scopes cleared for API clients authenticating via shared token (no device identity) #27494
Description
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"]