feat: enhance host API authentication handling and add regression tests

This commit is contained in:
duanshuwen
2026-04-23 19:09:30 +08:00
parent 71bcc3b3c5
commit c9617a3777
10 changed files with 471 additions and 127 deletions

View File

@@ -11,6 +11,7 @@ import {
} from './route-utils';
const DEFAULT_HOST_API_PORT = 13210;
const HOST_API_UNAUTHORIZED_CODE = 'HOST_API_UNAUTHORIZED';
type StartHostApiServerOptions = {
ctx: HostApiContext;
@@ -50,21 +51,19 @@ export function startHostApiServer(options: StartHostApiServerOptions): Server {
return;
}
const bearerHeader = req.headers.authorization || '';
const bearerToken = bearerHeader.startsWith('Bearer ')
? bearerHeader.slice('Bearer '.length)
: '';
const token = (
req.headers['x-host-api-token']
|| requestUrl.searchParams.get('token')
|| bearerToken
);
const headerToken = req.headers['x-host-api-token'];
const token = typeof headerToken === 'string'
? headerToken
: Array.isArray(headerToken)
? headerToken[0]
: (requestUrl.searchParams.get('token') || '');
if (token !== hostApiToken) {
sendJsonResponse(res, 401, {
success: false,
ok: false,
error: 'Unauthorized',
code: HOST_API_UNAUTHORIZED_CODE,
error: 'Host API authentication failed',
});
return;
}

View File

@@ -18,8 +18,6 @@ import { CONFIG_KEYS } from '@runtime/lib/constants';
import { normalizeAgentSessionKey } from '@runtime/lib/models';
import type { ContentBlock, RawMessage } from '@runtime/shared/chat-model';
import type { GatewayEvent, GatewayRpcParams, RuntimeRefreshTopic } from './types';
import * as providerHandlers from './handlers/provider';
import * as skillHandlers from './handlers/skills';
import {
createInitialGatewayDiagnostics,
type GatewayDiagnosticsSnapshot,
@@ -64,6 +62,7 @@ import {
} from './reload-policy';
import { GatewayStateController, type GatewayRuntimeStatus } from './state';
import { connectGatewaySocket, waitForGatewayReady } from './ws-client';
import { dispatchGatewayRpcMethod } from './rpc-dispatch';
type RuntimeChangeBroadcast = {
topics: RuntimeRefreshTopic[];
@@ -1785,88 +1784,16 @@ export class GatewayManager extends EventEmitter {
await this.init();
}
const localDispatch = dispatchGatewayRpcMethod(
method,
params,
(event) => this.broadcast(event),
);
if (localDispatch.handled) {
return localDispatch.result;
}
switch (method) {
case 'chat.send': {
const request = params as GatewayRpcParams['chat.send'];
const sessionKey = normalizeAgentSessionKey(request.sessionKey);
const messageText = extractTextFromRawMessage(request.message);
const response = await this.rpcGateway('chat.send', {
sessionKey,
message: messageText,
deliver: false,
idempotencyKey: request.message.id || randomUUID(),
}, { timeoutMs: 30_000 });
const runId = (
isRecord(response) &&
typeof response.runId === 'string' &&
response.runId.trim()
)
? response.runId
: '';
if (!runId) {
throw new Error('OpenClaw Gateway chat.send did not return a runId');
}
return { runId };
}
case 'chat.history': {
const request = params as GatewayRpcParams['chat.history'];
const response = await this.rpcGateway('chat.history', {
sessionKey: normalizeAgentSessionKey(request.sessionKey),
limit: request.limit ?? 50,
}, { timeoutMs: 15_000 });
if (!isRecord(response) || !Array.isArray(response.messages)) {
return [];
}
return response.messages
.map((message) => normalizeGatewayRawMessage(message))
.filter((message): message is RawMessage => message !== null);
}
case 'chat.abort': {
const request = params as GatewayRpcParams['chat.abort'];
await this.rpcGateway('chat.abort', {
sessionKey: normalizeAgentSessionKey(request.sessionKey),
}, { timeoutMs: 10_000 });
return;
}
case 'session.list': {
const response = await this.rpcGateway('sessions.list', {}, { timeoutMs: 10_000 });
if (!isRecord(response) || !Array.isArray(response.sessions)) {
return [];
}
return response.sessions
.map((session) => (
isRecord(session) && typeof session.key === 'string'
? session.key
: null
))
.filter((sessionKey): sessionKey is string => Boolean(sessionKey));
}
case 'session.delete': {
const request = params as GatewayRpcParams['session.delete'];
if (normalizeAgentSessionKey(request.sessionKey).endsWith(':main')) {
return { success: false };
}
await this.rpcGateway('sessions.delete', {
key: normalizeAgentSessionKey(request.sessionKey),
deleteTranscript: true,
}, { timeoutMs: 15_000 });
return { success: true };
}
case 'provider.list':
return providerHandlers.handleProviderList();
case 'provider.getDefault':
return providerHandlers.handleProviderGetDefault();
case 'skills.status':
return skillHandlers.handleSkillsStatus();
case 'skills.update':
return skillHandlers.handleSkillsUpdate(params);
default:
throw new Error(`Unknown gateway RPC method: ${method}`);
}

View File

@@ -0,0 +1,80 @@
import { normalizeAgentSessionKey } from '@runtime/lib/models';
import type { GatewayEvent, GatewayRpcParams } from './types';
import * as chatHandlers from './handlers/chat';
import * as providerHandlers from './handlers/provider';
import * as skillHandlers from './handlers/skills';
type GatewayBroadcast = (event: GatewayEvent) => void;
export function dispatchGatewayRpcMethod(
method: string,
params: unknown,
broadcast: GatewayBroadcast,
): { handled: boolean; result?: unknown } {
switch (method) {
case 'chat.send':
return {
handled: true,
result: chatHandlers.handleChatSend(
params as GatewayRpcParams['chat.send'],
broadcast,
),
};
case 'chat.history':
return {
handled: true,
result: chatHandlers.handleChatHistory(
params as GatewayRpcParams['chat.history'],
),
};
case 'chat.abort':
return {
handled: true,
result: chatHandlers.handleChatAbort(
params as GatewayRpcParams['chat.abort'],
broadcast,
),
};
case 'session.list':
return {
handled: true,
result: chatHandlers.handleSessionList(),
};
case 'session.delete': {
const request = params as GatewayRpcParams['session.delete'];
if (normalizeAgentSessionKey(request.sessionKey).endsWith(':main')) {
return {
handled: true,
result: { success: false },
};
}
return {
handled: true,
result: chatHandlers.handleSessionDelete(request),
};
}
case 'provider.list':
return {
handled: true,
result: providerHandlers.handleProviderList(),
};
case 'provider.getDefault':
return {
handled: true,
result: providerHandlers.handleProviderGetDefault(),
};
case 'skills.status':
return {
handled: true,
result: skillHandlers.handleSkillsStatus(),
};
case 'skills.update':
return {
handled: true,
result: skillHandlers.handleSkillsUpdate(params),
};
default:
return { handled: false };
}
}