Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
// Admin Http Rpc tests cover index plugin behavior.
import { describe, expect, it } from "vitest";
import plugin from "./index.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
describe("admin-http-rpc plugin entry", () => {
it("stays startup-off until the plugin entry is explicitly enabled", () => {
expect(manifest.activation).toEqual({
onStartup: false,
onConfigPaths: ["plugins.entries.admin-http-rpc"],
});
expect(manifest.contracts).toEqual({
gatewayMethodDispatch: ["authenticated-request"],
});
});
it("registers one trusted gateway HTTP route", () => {
const routes: Array<Record<string, unknown>> = [];
plugin.register({
registerHttpRoute(route) {
routes.push(route as unknown as Record<string, unknown>);
},
} as Parameters<typeof plugin.register>[0]);
expect(routes).toHaveLength(1);
expect(routes[0]).toMatchObject({
path: "/api/v1/admin/rpc",
auth: "gateway",
match: "exact",
gatewayRuntimeScopeSurface: "trusted-operator",
});
});
});

View File

@@ -0,0 +1,21 @@
/**
* Admin HTTP RPC plugin entry. It exposes a trusted gateway-authenticated HTTP
* endpoint for the explicit admin method allowlist.
*/
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { handleAdminHttpRpcRequest } from "./src/handler.js";
export default definePluginEntry({
id: "admin-http-rpc",
name: "Admin HTTP RPC",
description: "Expose selected Gateway admin RPC methods over HTTP",
register(api) {
api.registerHttpRoute({
path: "/api/v1/admin/rpc",
auth: "gateway",
match: "exact",
gatewayRuntimeScopeSurface: "trusted-operator",
handler: handleAdminHttpRpcRequest,
});
},
});

View File

@@ -0,0 +1,15 @@
{
"id": "admin-http-rpc",
"activation": {
"onStartup": false,
"onConfigPaths": ["plugins.entries.admin-http-rpc"]
},
"contracts": {
"gatewayMethodDispatch": ["authenticated-request"]
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "@openclaw/admin-http-rpc",
"version": "2026.6.11",
"private": true,
"description": "OpenClaw admin HTTP RPC endpoint",
"type": "module",
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
]
}
}

View File

@@ -0,0 +1,188 @@
// Admin Http Rpc tests cover handler plugin behavior.
import { Readable } from "node:stream";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { handleAdminHttpRpcRequest } from "./handler.js";
import { listAdminHttpRpcAllowedMethods } from "./methods.js";
const { dispatchGatewayMethod } = vi.hoisted(() => ({
dispatchGatewayMethod: vi.fn(),
}));
vi.mock("openclaw/plugin-sdk/gateway-method-runtime", () => ({
dispatchGatewayMethod,
}));
type CapturedResponse = {
statusCode: number;
headers: Record<string, string | number | readonly string[]>;
body: string;
};
function createRequest(body: unknown, method = "POST") {
const req = Readable.from([typeof body === "string" ? body : JSON.stringify(body)]);
Object.assign(req, {
method,
url: "/api/v1/admin/rpc",
headers: {
"content-type": "application/json",
},
});
return req as import("node:http").IncomingMessage;
}
function createResponse() {
const captured: CapturedResponse = {
statusCode: 200,
headers: {},
body: "",
};
const res = {
get statusCode() {
return captured.statusCode;
},
set statusCode(value: number) {
captured.statusCode = value;
},
setHeader(name: string, value: string | number | readonly string[]) {
captured.headers[name.toLowerCase()] = value;
},
end(chunk?: string | Buffer) {
captured.body = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : (chunk ?? "");
},
} as import("node:http").ServerResponse;
return { res, captured };
}
async function invoke(body: unknown, method = "POST") {
const { res, captured } = createResponse();
const handled = await handleAdminHttpRpcRequest(createRequest(body, method), res);
return {
handled,
captured,
json: captured.body ? (JSON.parse(captured.body) as unknown) : undefined,
};
}
describe("admin-http-rpc plugin handler", () => {
beforeEach(() => {
dispatchGatewayMethod.mockReset();
});
it("returns the allowlist without dispatching through the Gateway", async () => {
const result = await invoke({ id: "1", method: "commands.list" });
expect(result.handled).toBe(true);
expect(result.captured.statusCode).toBe(200);
expect(result.json).toEqual({
id: "1",
ok: true,
payload: {
methods: listAdminHttpRpcAllowedMethods(),
},
});
expect(dispatchGatewayMethod).not.toHaveBeenCalled();
});
it("dispatches allowed methods through the authenticated plugin request scope", async () => {
dispatchGatewayMethod.mockResolvedValueOnce({
ok: true,
payload: { status: "ok" },
meta: { requestId: "abc" },
});
const result = await invoke({
id: "cfg",
method: "config.get",
params: { path: "gateway" },
});
expect(dispatchGatewayMethod).toHaveBeenCalledWith("config.get", { path: "gateway" });
expect(result.captured.statusCode).toBe(200);
expect(result.json).toEqual({
id: "cfg",
ok: true,
payload: { status: "ok" },
meta: { requestId: "abc" },
});
});
it.each([
["web.login.start", { force: true, timeoutMs: 1000 }],
["web.login.wait", { timeoutMs: 1000 }],
] as const)(
"allows web QR login method %s through the authenticated plugin request scope",
async (method, params) => {
dispatchGatewayMethod.mockResolvedValueOnce({
ok: true,
payload: { status: "ok" },
});
const result = await invoke({
id: "web-login",
method,
params,
});
expect(dispatchGatewayMethod).toHaveBeenCalledWith(method, params);
expect(result.captured.statusCode).toBe(200);
expect(result.json).toEqual({
id: "web-login",
ok: true,
payload: { status: "ok" },
});
},
);
it("rejects methods outside the admin HTTP RPC allowlist", async () => {
const result = await invoke({ id: "bad", method: "sessions.send" });
expect(dispatchGatewayMethod).not.toHaveBeenCalled();
expect(result.captured.statusCode).toBe(400);
expect(result.json).toEqual({
id: "bad",
ok: false,
error: {
code: "INVALID_REQUEST",
message: "admin HTTP RPC method is not supported: sessions.send",
},
});
});
it("maps Gateway errors to HTTP status codes", async () => {
dispatchGatewayMethod.mockResolvedValueOnce({
ok: false,
error: { code: "NOT_PAIRED", message: "pair first" },
});
const result = await invoke({ id: "node", method: "node.list" });
expect(result.captured.statusCode).toBe(409);
expect(result.json).toEqual({
id: "node",
ok: false,
error: { code: "NOT_PAIRED", message: "pair first" },
});
});
it("rejects invalid request bodies before dispatch", async () => {
const result = await invoke({ id: "missing" });
expect(result.captured.statusCode).toBe(400);
expect(result.json).toEqual({
ok: false,
error: {
type: "invalid_request",
message: "method must be a non-empty string",
},
});
expect(dispatchGatewayMethod).not.toHaveBeenCalled();
});
it("only accepts POST", async () => {
const result = await invoke({ method: "status" }, "GET");
expect(result.captured.statusCode).toBe(405);
expect(result.captured.headers.allow).toBe("POST");
expect(dispatchGatewayMethod).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,238 @@
/**
* HTTP handler for the Admin RPC endpoint. It validates JSON requests, enforces
* the method allowlist, dispatches gateway methods, and maps errors to HTTP.
*/
import { randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { dispatchGatewayMethod } from "openclaw/plugin-sdk/gateway-method-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { isAdminHttpRpcAllowedMethod, listAdminHttpRpcAllowedMethods } from "./methods.js";
const DEFAULT_RPC_BODY_BYTES = 1024 * 1024;
const ErrorCodes = {
AGENT_TIMEOUT: "AGENT_TIMEOUT",
APPROVAL_NOT_FOUND: "APPROVAL_NOT_FOUND",
INVALID_REQUEST: "INVALID_REQUEST",
NOT_LINKED: "NOT_LINKED",
NOT_PAIRED: "NOT_PAIRED",
UNAVAILABLE: "UNAVAILABLE",
} as const;
type RpcBody = {
id?: unknown;
method?: unknown;
params?: unknown;
};
type RpcError = {
code: string;
message: string;
details?: unknown;
retryable?: boolean;
retryAfterMs?: number;
};
type RpcResponse =
| { id: string; ok: true; payload: unknown; meta?: Record<string, unknown> }
| { id: string; ok: false; error: RpcError; meta?: Record<string, unknown> };
type ParsedRequest = {
id: string;
method: string;
params?: unknown;
};
function createError(code: string, message: string): RpcError {
return { code, message };
}
function rpcHttpStatus(response: RpcResponse): number {
if (response.ok) {
return 200;
}
switch (response.error.code) {
case ErrorCodes.INVALID_REQUEST:
return 400;
case ErrorCodes.APPROVAL_NOT_FOUND:
return 404;
case ErrorCodes.UNAVAILABLE:
return 503;
case ErrorCodes.AGENT_TIMEOUT:
return 504;
case ErrorCodes.NOT_LINKED:
case ErrorCodes.NOT_PAIRED:
return 409;
default:
return 500;
}
}
function sendJson(res: ServerResponse, status: number, body: unknown): void {
res.statusCode = status;
res.setHeader("Cache-Control", "no-store");
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(body));
}
function sendError(res: ServerResponse, status: number, error: { type: string; message: string }) {
sendJson(res, status, { ok: false, error });
}
async function readJsonBody(
req: IncomingMessage,
maxBytes: number,
): Promise<{ ok: true; value: unknown } | { ok: false; status: number; message: string }> {
const chunks: Buffer[] = [];
let totalBytes = 0;
try {
for await (const chunk of req) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
totalBytes += buffer.byteLength;
if (totalBytes > maxBytes) {
return { ok: false, status: 413, message: "Payload too large" };
}
chunks.push(buffer);
}
} catch {
return { ok: false, status: 400, message: "failed to read request body" };
}
const raw = Buffer.concat(chunks).toString("utf8");
if (!raw.trim()) {
return { ok: false, status: 400, message: "request body must be JSON" };
}
try {
return { ok: true, value: JSON.parse(raw) };
} catch {
return { ok: false, status: 400, message: "request body must be valid JSON" };
}
}
function readRpcRequestBody(body: unknown):
| { ok: true; request: ParsedRequest }
| {
ok: false;
message: string;
} {
if (!isRecord(body)) {
return { ok: false, message: "request body must be an object" };
}
const rpcBody = body as RpcBody;
if (typeof rpcBody.method !== "string" || rpcBody.method.trim().length === 0) {
return { ok: false, message: "method must be a non-empty string" };
}
const id =
typeof rpcBody.id === "string" && rpcBody.id.trim().length > 0
? rpcBody.id.trim()
: randomUUID();
return {
ok: true,
request: {
id,
method: rpcBody.method.trim(),
...(Object.hasOwn(rpcBody, "params") ? { params: rpcBody.params } : {}),
},
};
}
function methodNotAllowed(id: string, method: string): RpcResponse {
return {
id,
ok: false,
error: createError(
ErrorCodes.INVALID_REQUEST,
`admin HTTP RPC method is not supported: ${method}`,
),
};
}
function commandsList(id: string): RpcResponse {
return {
id,
ok: true,
payload: {
methods: listAdminHttpRpcAllowedMethods(),
},
};
}
async function dispatchAdminRpc(request: ParsedRequest): Promise<RpcResponse> {
try {
const response = await dispatchGatewayMethod(request.method, request.params);
if (response.ok) {
return {
id: request.id,
ok: true,
payload: response.payload,
...(response.meta ? { meta: response.meta } : {}),
};
}
return {
id: request.id,
ok: false,
error:
response.error ??
createError(ErrorCodes.UNAVAILABLE, "gateway method failed before returning a response"),
...(response.meta ? { meta: response.meta } : {}),
};
} catch {
return {
id: request.id,
ok: false,
error: createError(
ErrorCodes.UNAVAILABLE,
"gateway method failed before returning a response",
),
};
}
}
/** Handle one gateway-authenticated Admin HTTP RPC request. */
export async function handleAdminHttpRpcRequest(
req: IncomingMessage,
res: ServerResponse,
): Promise<boolean> {
if ((req.method ?? "GET").toUpperCase() !== "POST") {
res.setHeader("Allow", "POST");
sendError(res, 405, {
type: "method_not_allowed",
message: "Method Not Allowed",
});
return true;
}
const body = await readJsonBody(req, DEFAULT_RPC_BODY_BYTES);
if (!body.ok) {
sendError(res, body.status, {
type: "invalid_request",
message: body.message,
});
return true;
}
const parsed = readRpcRequestBody(body.value);
if (!parsed.ok) {
sendError(res, 400, {
type: "invalid_request",
message: parsed.message,
});
return true;
}
if (!isAdminHttpRpcAllowedMethod(parsed.request.method)) {
const response = methodNotAllowed(parsed.request.id, parsed.request.method);
sendJson(res, rpcHttpStatus(response), response);
return true;
}
if (parsed.request.method === "commands.list") {
const response = commandsList(parsed.request.id);
sendJson(res, 200, response);
return true;
}
const response = await dispatchAdminRpc(parsed.request);
sendJson(res, rpcHttpStatus(response), response);
return true;
}

View File

@@ -0,0 +1,69 @@
/**
* Method allowlist for Admin HTTP RPC. Only methods listed here can cross the
* trusted operator HTTP surface.
*/
const ADMIN_HTTP_RPC_ALLOWED_METHOD_GROUPS = {
gateway: [
"health",
"status",
"logs.tail",
"usage.status",
"usage.cost",
"gateway.restart.request",
],
discovery: ["commands.list"],
config: [
"config.get",
"config.schema",
"config.schema.lookup",
"config.set",
"config.patch",
"config.apply",
],
channels: ["channels.status", "channels.start", "channels.stop", "channels.logout"],
web: ["web.login.start", "web.login.wait"],
models: ["models.list", "models.authStatus"],
agents: ["agents.list", "agents.create", "agents.update", "agents.delete"],
approvals: [
"exec.approvals.get",
"exec.approvals.set",
"exec.approvals.node.get",
"exec.approvals.node.set",
],
cron: [
"cron.status",
"cron.list",
"cron.get",
"cron.runs",
"cron.add",
"cron.update",
"cron.remove",
"cron.run",
],
devices: ["device.pair.list", "device.pair.approve", "device.pair.reject", "device.pair.remove"],
nodes: [
"node.list",
"node.describe",
"node.pair.list",
"node.pair.approve",
"node.pair.reject",
"node.pair.remove",
"node.rename",
],
tasks: ["tasks.list", "tasks.get", "tasks.cancel"],
diagnostics: ["doctor.memory.status", "update.status"],
} as const satisfies Record<string, readonly string[]>;
const ADMIN_HTTP_RPC_ALLOWED_METHODS: ReadonlySet<string> = new Set(
Object.values(ADMIN_HTTP_RPC_ALLOWED_METHOD_GROUPS).flat(),
);
/** Return whether an admin RPC method is exposed over HTTP. */
export function isAdminHttpRpcAllowedMethod(method: string): boolean {
return ADMIN_HTTP_RPC_ALLOWED_METHODS.has(method);
}
/** List all admin RPC methods exposed over HTTP. */
export function listAdminHttpRpcAllowedMethods(): string[] {
return Array.from(ADMIN_HTTP_RPC_ALLOWED_METHODS);
}

View File

@@ -0,0 +1,16 @@
{
"extends": "../tsconfig.package-boundary.base.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["./*.ts", "./src/**/*.ts"],
"exclude": [
"./**/*.test.ts",
"./dist/**",
"./node_modules/**",
"./src/test-support/**",
"./src/**/*test-helpers.ts",
"./src/**/*test-harness.ts",
"./src/**/*test-support.ts"
]
}