fix: document and harden operator token handling

- document OPENCLAW_OPERATOR_TOKEN setup for R&D/Self-Improvement APIs

- compare operator tokens with timingSafeEqual

- keep unauthenticated localhost mode explicitly opt-in only
This commit is contained in:
Faisal C
2026-05-01 01:25:21 -05:00
parent f59116866f
commit 3f10fc7a38
2 changed files with 35 additions and 2 deletions
+22
View File
@@ -75,6 +75,28 @@ OPENCLAW_HOME=/opt/openclaw
npm run start
```
### R&D Council operator APIs
The Self-Improvement and R&D Council actions can write local files such as `rd-council-items.json`, `idea_ledger.json`, decision logs, and local work-order handoff state. For safety, those operator APIs require an explicit token by default:
```bash
OPENCLAW_OPERATOR_TOKEN="replace-with-a-long-random-token" npm run start
```
Then set the same token in the browser console for the local dashboard origin:
```js
localStorage.setItem("openclaw_operator_token", "replace-with-a-long-random-token")
```
For trusted local development only, you can opt out of token auth and rely on localhost checks:
```bash
OPENCLAW_ALLOW_UNAUTHENTICATED_LOCAL_OPERATOR_UI=true npm run start
```
Do not enable unauthenticated local mode on a dashboard exposed through a tunnel, shared host, reverse proxy, or public network.
## Docker Deployment
You can also deploy the dashboard using Docker:
+13 -2
View File
@@ -1,3 +1,4 @@
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
@@ -121,16 +122,26 @@ function hostnameFromHostHeader(value) {
return raw.split(':')[0];
}
function constantTimeEqual(left, right) {
const leftValue = nonEmptyString(left);
const rightValue = nonEmptyString(right);
if (!leftValue || !rightValue) return false;
const leftBuffer = Buffer.from(leftValue);
const rightBuffer = Buffer.from(rightValue);
if (leftBuffer.length !== rightBuffer.length) return false;
return crypto.timingSafeEqual(leftBuffer, rightBuffer);
}
export function validateLocalOperatorRequest(request, opts = {}) {
const expectedToken = nonEmptyString(process.env.OPENCLAW_OPERATOR_TOKEN);
const allowUnauthenticatedLocal = process.env.OPENCLAW_ALLOW_UNAUTHENTICATED_LOCAL_OPERATOR_UI === 'true' || opts.allowUnauthenticatedLocal === true;
const authHeader = nonEmptyString(request?.headers?.get?.('authorization'));
const bearerToken = authHeader?.toLowerCase().startsWith('bearer ') ? authHeader.slice(7).trim() : null;
const headerToken = nonEmptyString(request?.headers?.get?.('x-openclaw-operator-token')) || bearerToken;
if (expectedToken && headerToken === expectedToken) {
if (expectedToken && constantTimeEqual(headerToken, expectedToken)) {
return { ok: true, reason: null };
}
if (expectedToken && headerToken !== expectedToken) {
if (expectedToken && !constantTimeEqual(headerToken, expectedToken)) {
return { ok: false, status: 401, reason: 'R&D Council operator token is missing or invalid.' };
}
if (!allowUnauthenticatedLocal) {