mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-10 08:41:13 +00:00
fix: reserve admin gateway method prefixes
This commit is contained in:
@@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai
|
||||
- ACP/agents: inherit the target agent workspace for cross-agent ACP spawns and fall back safely when the inherited workspace no longer exists. (#58438) Thanks @zssggle-rgb.
|
||||
- ACPX/Windows: preserve backslashes and absolute `.exe` paths in Claude CLI parsing, and fail fast on wrapper-script targets with guidance to use `cmd.exe /c`, `powershell.exe -File`, or `node <script>`. (#60689)
|
||||
- Providers/OpenAI Codex: treat Codex CLI auth as the canonical source, stop persisting copied Codex OAuth secrets into `auth-profiles.json`, refresh expired Codex-managed tokens back into Codex storage, and keep OpenAI WebSocket fallback/cache paths stable across transport changes.
|
||||
- Plugins/gateway: keep reserved admin RPC namespaces (`config.*`, `exec.approvals.*`, `wizard.*`, `update.*`) admin-only even for plugin-defined methods, and warn when a plugin tries to register a narrower scope.
|
||||
- Gateway/Windows scheduled tasks: preserve Task Scheduler settings on reinstall, fail loudly when `/Run` does not start, and report fast failed restarts accurately instead of pretending they timed out after 60 seconds. (#59335) Thanks @tmimmanuel.
|
||||
- Discord: keep REST, webhook, and monitor traffic on the configured proxy, preserve component-only media sends, honor `@everyone` and `@here` mention gates, keep ACK reactions on the active account, and split voice connect/playback timeouts so auto-join is more reliable. (#57465, #60361, #60345)
|
||||
- WhatsApp: restore `channels.whatsapp.blockStreaming` and reset watchdog timeouts after reconnect so quiet chats stop falling into reconnect loops. (#60007, #60069)
|
||||
|
||||
@@ -163,6 +163,11 @@ methods:
|
||||
| `api.registerService(service)` | Background service |
|
||||
| `api.registerInteractiveHandler(registration)` | Interactive handler |
|
||||
|
||||
Reserved core admin namespaces (`config.*`, `exec.approvals.*`, `wizard.*`,
|
||||
`update.*`) always stay `operator.admin`, even if a plugin tries to assign a
|
||||
narrower gateway method scope. Prefer plugin-specific prefixes for
|
||||
plugin-owned methods.
|
||||
|
||||
### CLI registration metadata
|
||||
|
||||
`api.registerCli(registrar, opts?)` accepts two kinds of top-level metadata:
|
||||
|
||||
@@ -50,6 +50,18 @@ describe("method scope resolution", () => {
|
||||
"operator.write",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps reserved admin namespaces admin-only even if a plugin scope is narrower", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.gatewayMethodScopes = {
|
||||
"config.plugin.inspect": "operator.read",
|
||||
};
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
expect(resolveLeastPrivilegeOperatorScopesForMethod("config.plugin.inspect")).toEqual([
|
||||
"operator.admin",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("operator scope authorization", () => {
|
||||
@@ -105,6 +117,19 @@ describe("operator scope authorization", () => {
|
||||
missingScope: "operator.admin",
|
||||
});
|
||||
});
|
||||
|
||||
it("requires admin for reserved admin namespaces even if a plugin registered a narrower scope", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.gatewayMethodScopes = {
|
||||
"config.plugin.inspect": "operator.read",
|
||||
};
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
expect(authorizeOperatorScopesForMethod("config.plugin.inspect", ["operator.read"])).toEqual({
|
||||
allowed: false,
|
||||
missingScope: "operator.admin",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin approval method registration", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { isReservedAdminGatewayMethod } from "../shared/gateway-method-prefixes.js";
|
||||
|
||||
export const ADMIN_SCOPE = "operator.admin" as const;
|
||||
export const READ_SCOPE = "operator.read" as const;
|
||||
@@ -149,8 +150,6 @@ const METHOD_SCOPE_GROUPS: Record<OperatorScope, readonly string[]> = {
|
||||
],
|
||||
};
|
||||
|
||||
const ADMIN_METHOD_PREFIXES = ["exec.approvals.", "config.", "wizard.", "update."] as const;
|
||||
|
||||
const METHOD_SCOPE_BY_NAME = new Map<string, OperatorScope>(
|
||||
Object.entries(METHOD_SCOPE_GROUPS).flatMap(([scope, methods]) =>
|
||||
methods.map((method) => [method, scope as OperatorScope]),
|
||||
@@ -162,13 +161,13 @@ function resolveScopedMethod(method: string): OperatorScope | undefined {
|
||||
if (explicitScope) {
|
||||
return explicitScope;
|
||||
}
|
||||
if (isReservedAdminGatewayMethod(method)) {
|
||||
return ADMIN_SCOPE;
|
||||
}
|
||||
const pluginScope = getActivePluginRegistry()?.gatewayMethodScopes?.[method];
|
||||
if (pluginScope) {
|
||||
return pluginScope;
|
||||
}
|
||||
if (ADMIN_METHOD_PREFIXES.some((prefix) => method.startsWith(prefix))) {
|
||||
return ADMIN_SCOPE;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -964,6 +964,47 @@ describe("loadOpenClawPlugins", () => {
|
||||
expect(Object.keys(registry.gatewayHandlers)).toContain("allowed-config-path.ping");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "coerces reserved gateway method namespaces to operator.admin",
|
||||
run: () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "reserved-gateway-scope",
|
||||
filename: "reserved-gateway-scope.cjs",
|
||||
body: `module.exports = {
|
||||
id: "reserved-gateway-scope",
|
||||
register(api) {
|
||||
api.registerGatewayMethod(
|
||||
"config.plugin.inspect",
|
||||
({ respond }) => respond(true, { ok: true }),
|
||||
{ scope: "operator.read" },
|
||||
);
|
||||
},
|
||||
};`,
|
||||
});
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
workspaceDir: plugin.dir,
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["reserved-gateway-scope"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Object.keys(registry.gatewayHandlers)).toContain("config.plugin.inspect");
|
||||
expect(registry.gatewayMethodScopes?.["config.plugin.inspect"]).toBe("operator.admin");
|
||||
expect(
|
||||
registry.diagnostics.some((diag) =>
|
||||
String(diag.message).includes(
|
||||
"gateway method scope coerced to operator.admin for reserved core namespace: config.plugin.inspect",
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "limits imports to the requested plugin ids",
|
||||
run: () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
} from "../gateway/server-methods/types.js";
|
||||
import { registerInternalHook } from "../hooks/internal-hooks.js";
|
||||
import type { HookEntry } from "../hooks/types.js";
|
||||
import { isReservedAdminGatewayMethod } from "../shared/gateway-method-prefixes.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { buildPluginApi } from "./api-builder.js";
|
||||
import { registerPluginCommand, validatePluginCommandDefinition } from "./command-registration.js";
|
||||
@@ -433,9 +434,23 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
return;
|
||||
}
|
||||
registry.gatewayHandlers[trimmed] = handler;
|
||||
if (opts?.scope) {
|
||||
let effectiveScope = opts?.scope;
|
||||
if (
|
||||
effectiveScope &&
|
||||
effectiveScope !== "operator.admin" &&
|
||||
isReservedAdminGatewayMethod(trimmed)
|
||||
) {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `gateway method scope coerced to operator.admin for reserved core namespace: ${trimmed}`,
|
||||
});
|
||||
effectiveScope = "operator.admin";
|
||||
}
|
||||
if (effectiveScope) {
|
||||
registry.gatewayMethodScopes ??= {};
|
||||
registry.gatewayMethodScopes[trimmed] = opts.scope;
|
||||
registry.gatewayMethodScopes[trimmed] = effectiveScope;
|
||||
}
|
||||
record.gatewayMethods.push(trimmed);
|
||||
};
|
||||
|
||||
10
src/shared/gateway-method-prefixes.ts
Normal file
10
src/shared/gateway-method-prefixes.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const ADMIN_GATEWAY_METHOD_PREFIXES = [
|
||||
"exec.approvals.",
|
||||
"config.",
|
||||
"wizard.",
|
||||
"update.",
|
||||
] as const;
|
||||
|
||||
export function isReservedAdminGatewayMethod(method: string): boolean {
|
||||
return ADMIN_GATEWAY_METHOD_PREFIXES.some((prefix) => method.startsWith(prefix));
|
||||
}
|
||||
Reference in New Issue
Block a user