mirror of
https://github.com/lotsoftick/openclaw_client.git
synced 2026-08-14 00:48:07 +00:00
fixed pairing
This commit is contained in:
@@ -13,10 +13,24 @@ export interface AuthCredentials {
|
||||
};
|
||||
}
|
||||
|
||||
export interface SharedGatewayAuth {
|
||||
/** `gateway.auth.token` from `~/.openclaw/openclaw.json` (mode = "token"). */
|
||||
token?: string;
|
||||
/** `gateway.auth.password` from `~/.openclaw/openclaw.json` (mode = "password"). */
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface GatewayCredentials {
|
||||
device: DeviceCredentials;
|
||||
/** Device identity — required for the legacy device-auth path; optional
|
||||
* when `sharedAuth` is configured (loopback backend clients can connect
|
||||
* with a shared secret and no device pairing). */
|
||||
device: DeviceCredentials | null;
|
||||
auth: AuthCredentials;
|
||||
gatewayPort: number;
|
||||
/** Shared-secret credentials read from the OpenClaw config file. When
|
||||
* present, the connect handshake uses these instead of device-pairing,
|
||||
* so no `openclaw devices approve` step is ever required. */
|
||||
sharedAuth: SharedGatewayAuth | null;
|
||||
}
|
||||
|
||||
// ── Wire protocol ──
|
||||
|
||||
@@ -377,7 +377,14 @@ export interface AgentLimitsPatch {
|
||||
|
||||
export interface OpenclawConfig {
|
||||
agents?: OpenclawAgentsSection;
|
||||
gateway?: { port?: number };
|
||||
gateway?: {
|
||||
port?: number;
|
||||
auth?: {
|
||||
mode?: 'none' | 'token' | 'password' | 'trusted-proxy';
|
||||
token?: string;
|
||||
password?: string;
|
||||
};
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
EventListener,
|
||||
DeviceCredentials,
|
||||
AuthCredentials,
|
||||
SharedGatewayAuth,
|
||||
} from '../@types/gateway';
|
||||
import { OpenclawConfig } from '../@types/openclaw';
|
||||
import { errMsg, execErrText } from '../utils/errors';
|
||||
@@ -57,19 +58,58 @@ function signPayload(privPem: string, payload: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
function loadSharedAuthFromConfig(config: OpenclawConfig | null): SharedGatewayAuth | null {
|
||||
const auth = config?.gateway?.auth;
|
||||
if (!auth) return null;
|
||||
const token = typeof auth.token === 'string' && auth.token.length > 0 ? auth.token : undefined;
|
||||
const password =
|
||||
typeof auth.password === 'string' && auth.password.length > 0 ? auth.password : undefined;
|
||||
/* `gateway.auth.mode` may be "none" / "trusted-proxy" / unset on hosts that
|
||||
* delegate auth elsewhere. We only opt into shared-secret auth when there's
|
||||
* an actual secret to send; the mode field itself is informational here. */
|
||||
if (!token && !password) return null;
|
||||
return { token, password };
|
||||
}
|
||||
|
||||
export function loadGatewayCredentials(): GatewayCredentials | null {
|
||||
let config: OpenclawConfig | null = null;
|
||||
try {
|
||||
const configPath = path.join(OPENCLAW_HOME, 'openclaw.json');
|
||||
config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as OpenclawConfig;
|
||||
} catch (err) {
|
||||
console.warn('[gateway] could not read openclaw.json:', errMsg(err));
|
||||
return null;
|
||||
}
|
||||
|
||||
const gatewayPort = config?.gateway?.port || 18789;
|
||||
const sharedAuth = loadSharedAuthFromConfig(config);
|
||||
|
||||
/* Device files are optional when shared auth is configured. Backend
|
||||
* loopback clients (`client.id: "gateway-client"`, `client.mode: "backend"`)
|
||||
* may omit `device` entirely on direct loopback when authenticating with a
|
||||
* shared token/password — see openclaw/docs/gateway/protocol.md §Handshake. */
|
||||
let device: DeviceCredentials | null = null;
|
||||
let auth: AuthCredentials = {};
|
||||
try {
|
||||
const identityPath = path.join(OPENCLAW_HOME, 'identity', 'device.json');
|
||||
const authPath = path.join(OPENCLAW_HOME, 'identity', 'device-auth.json');
|
||||
const configPath = path.join(OPENCLAW_HOME, 'openclaw.json');
|
||||
const device = JSON.parse(fs.readFileSync(identityPath, 'utf-8')) as DeviceCredentials;
|
||||
const auth = JSON.parse(fs.readFileSync(authPath, 'utf-8')) as AuthCredentials;
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as OpenclawConfig;
|
||||
return { device, auth, gatewayPort: config.gateway?.port || 18789 };
|
||||
device = JSON.parse(fs.readFileSync(identityPath, 'utf-8')) as DeviceCredentials;
|
||||
auth = JSON.parse(fs.readFileSync(authPath, 'utf-8')) as AuthCredentials;
|
||||
} catch (err) {
|
||||
console.warn('[gateway] could not load credentials:', errMsg(err));
|
||||
if (!sharedAuth) {
|
||||
/* Only complain when device files are actually required (no shared
|
||||
* auth fallback). Otherwise their absence is expected and silent. */
|
||||
console.warn('[gateway] could not load device credentials:', errMsg(err));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sharedAuth && !device) {
|
||||
console.warn('[gateway] no credentials available (neither shared auth nor device pairing)');
|
||||
return null;
|
||||
}
|
||||
|
||||
return { device, auth, gatewayPort, sharedAuth };
|
||||
}
|
||||
|
||||
function isConnectChallenge(
|
||||
@@ -127,9 +167,12 @@ export class GatewayClient {
|
||||
return;
|
||||
}
|
||||
|
||||
const { device, auth, gatewayPort } = this.credentials;
|
||||
const { device, auth, gatewayPort, sharedAuth } = this.credentials;
|
||||
const url = `ws://127.0.0.1:${gatewayPort}`;
|
||||
console.log(`[gateway] connecting to ${url}...`);
|
||||
let authMode: 'token' | 'password' | 'device' = 'device';
|
||||
if (sharedAuth?.token) authMode = 'token';
|
||||
else if (sharedAuth?.password) authMode = 'password';
|
||||
console.log(`[gateway] connecting to ${url} (auth: ${authMode})...`);
|
||||
|
||||
const ws = new WsWebSocket(url);
|
||||
this.ws = ws;
|
||||
@@ -173,6 +216,53 @@ export class GatewayClient {
|
||||
|
||||
if (isConnectChallenge(msg)) {
|
||||
const { nonce } = msg.payload;
|
||||
const baseClient = {
|
||||
id: 'gateway-client',
|
||||
version: '1.0.0',
|
||||
platform: process.platform,
|
||||
mode: 'backend',
|
||||
};
|
||||
|
||||
/* Prefer shared-secret auth when available — backend loopback
|
||||
* clients can connect with `auth.token` / `auth.password` and
|
||||
* skip device pairing entirely (no `openclaw devices approve`
|
||||
* needed, ever). See openclaw/docs/gateway/protocol.md §Auth. */
|
||||
if (sharedAuth) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'req',
|
||||
id: crypto.randomUUID(),
|
||||
method: 'connect',
|
||||
params: {
|
||||
minProtocol: 3,
|
||||
maxProtocol: 4,
|
||||
client: baseClient,
|
||||
caps: [],
|
||||
/* `auth.password` is forwarded orthogonally; `auth.token`
|
||||
* carries the shared token in priority order. Sending
|
||||
* both is harmless on hosts configured with one. */
|
||||
auth: {
|
||||
...(sharedAuth.token ? { token: sharedAuth.token } : {}),
|
||||
...(sharedAuth.password ? { password: sharedAuth.password } : {}),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Legacy device-auth path. Keeps the v3 signed-payload format
|
||||
* for backward compat with hosts that don't have a shared
|
||||
* gateway secret configured. */
|
||||
if (!device) {
|
||||
console.error('[gateway] no device credentials and no shared auth — cannot connect');
|
||||
try {
|
||||
ws.terminate();
|
||||
} catch {
|
||||
/* idempotent */
|
||||
}
|
||||
return;
|
||||
}
|
||||
const role = 'operator';
|
||||
const scopes = auth.tokens?.operator?.scopes || [
|
||||
'operator.admin',
|
||||
@@ -203,12 +293,7 @@ export class GatewayClient {
|
||||
params: {
|
||||
minProtocol: 3,
|
||||
maxProtocol: 4,
|
||||
client: {
|
||||
id: 'gateway-client',
|
||||
version: '1.0.0',
|
||||
platform: process.platform,
|
||||
mode: 'backend',
|
||||
},
|
||||
client: baseClient,
|
||||
caps: [],
|
||||
role,
|
||||
scopes,
|
||||
@@ -507,6 +592,13 @@ export function ocSpawn(args: string[], options: SpawnOptions = {}): ChildProces
|
||||
|
||||
export async function ensureDevicePaired(): Promise<void> {
|
||||
const creds = loadGatewayCredentials();
|
||||
/* Shared-secret auth bypasses device pairing entirely — no scope-upgrade
|
||||
* approval is ever needed, so this whole bootstrap is a no-op. */
|
||||
if (creds?.sharedAuth) {
|
||||
const mode = creds.sharedAuth.token ? 'token' : 'password';
|
||||
console.log(`[setup] gateway shared-${mode} auth configured — skipping device pairing`);
|
||||
return;
|
||||
}
|
||||
const scopes = creds?.auth?.tokens?.operator?.scopes || [];
|
||||
if (scopes.includes('operator.write')) {
|
||||
console.log('[setup] device-auth already has operator.write');
|
||||
|
||||
Reference in New Issue
Block a user