mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-14 10:41:23 +00:00
Dreaming: require admin for gateway persistence
This commit is contained in:
@@ -52,13 +52,17 @@ function createHarness(initialConfig: OpenClawConfig = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function createCommandContext(args?: string): PluginCommandContext {
|
||||
function createCommandContext(
|
||||
args?: string,
|
||||
overrides?: Partial<Pick<PluginCommandContext, "gatewayClientScopes">>,
|
||||
): PluginCommandContext {
|
||||
return {
|
||||
channel: "webchat",
|
||||
isAuthorizedSender: true,
|
||||
commandBody: args ? `/dreaming ${args}` : "/dreaming",
|
||||
args,
|
||||
config: {},
|
||||
gatewayClientScopes: overrides?.gatewayClientScopes,
|
||||
requestConversationBinding: async () => ({ status: "error", message: "unsupported" }),
|
||||
detachConversationBinding: async () => ({ removed: false }),
|
||||
getCurrentConversationBinding: async () => null,
|
||||
@@ -115,6 +119,48 @@ describe("memory-core /dreaming command", () => {
|
||||
expect(result.text).toContain("Dreaming disabled.");
|
||||
});
|
||||
|
||||
it("blocks unscoped gateway callers from persisting dreaming config", async () => {
|
||||
const { command, runtime } = createHarness();
|
||||
|
||||
const result = await command.handler(
|
||||
createCommandContext("off", {
|
||||
gatewayClientScopes: [],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.text).toContain("requires operator.admin");
|
||||
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("blocks write-scoped gateway callers from persisting dreaming config", async () => {
|
||||
const { command, runtime } = createHarness();
|
||||
|
||||
const result = await command.handler(
|
||||
createCommandContext("off", {
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.text).toContain("requires operator.admin");
|
||||
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows admin-scoped gateway callers to persist dreaming config", async () => {
|
||||
const { command, runtime, getRuntimeConfig } = createHarness();
|
||||
|
||||
const result = await command.handler(
|
||||
createCommandContext("on", {
|
||||
gatewayClientScopes: ["operator.admin"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(runtime.config.writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
expect(resolveStoredDreaming(getRuntimeConfig())).toMatchObject({
|
||||
enabled: true,
|
||||
});
|
||||
expect(result.text).toContain("Dreaming enabled.");
|
||||
});
|
||||
|
||||
it("returns status without mutating config", async () => {
|
||||
const { command, runtime } = createHarness({
|
||||
plugins: {
|
||||
|
||||
@@ -75,6 +75,10 @@ function formatUsage(includeStatus: string): string {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function requiresAdminToMutateDreaming(gatewayClientScopes?: readonly string[]): boolean {
|
||||
return Array.isArray(gatewayClientScopes) && !gatewayClientScopes.includes("operator.admin");
|
||||
}
|
||||
|
||||
export function registerDreamingCommand(api: OpenClawPluginApi): void {
|
||||
api.registerCommand({
|
||||
name: "dreaming",
|
||||
@@ -102,6 +106,9 @@ export function registerDreamingCommand(api: OpenClawPluginApi): void {
|
||||
}
|
||||
|
||||
if (firstToken === "on" || firstToken === "off") {
|
||||
if (requiresAdminToMutateDreaming(ctx.gatewayClientScopes)) {
|
||||
return { text: "⚠️ /dreaming on|off requires operator.admin for gateway clients." };
|
||||
}
|
||||
const enabled = firstToken === "on";
|
||||
const nextConfig = updateDreamingEnabledInConfig(currentConfig, enabled);
|
||||
await api.runtime.config.writeConfigFile(nextConfig);
|
||||
|
||||
@@ -207,7 +207,7 @@ function extractFirstTextBlock(payload: unknown): string | undefined {
|
||||
}
|
||||
|
||||
function createScopedCliClient(
|
||||
scopes: string[],
|
||||
scopes?: string[],
|
||||
client: Partial<{
|
||||
id: string;
|
||||
mode: string;
|
||||
@@ -1414,6 +1414,25 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
|
||||
expect(mockState.lastDispatchCtx?.CommandBody).toBe("/scopecheck");
|
||||
});
|
||||
|
||||
it("normalizes missing gateway caller scopes to an empty array before dispatch", async () => {
|
||||
createTranscriptFixture("openclaw-chat-send-missing-gateway-client-scopes-");
|
||||
mockState.finalText = "ok";
|
||||
const respond = vi.fn();
|
||||
const context = createChatContext();
|
||||
|
||||
await runNonStreamingChatSend({
|
||||
context,
|
||||
respond,
|
||||
idempotencyKey: "idem-gateway-client-scopes-missing",
|
||||
message: "/scopecheck",
|
||||
client: createScopedCliClient(),
|
||||
expectBroadcast: false,
|
||||
});
|
||||
|
||||
expect(mockState.lastDispatchCtx?.GatewayClientScopes).toEqual([]);
|
||||
expect(mockState.lastDispatchCtx?.CommandBody).toBe("/scopecheck");
|
||||
});
|
||||
|
||||
it("injects ACP system provenance into the agent-visible body", async () => {
|
||||
createTranscriptFixture("openclaw-chat-send-system-provenance-acp-");
|
||||
mockState.finalText = "ok";
|
||||
|
||||
@@ -1671,7 +1671,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
SenderId: clientInfo?.id,
|
||||
SenderName: clientInfo?.displayName,
|
||||
SenderUsername: clientInfo?.displayName,
|
||||
GatewayClientScopes: client?.connect?.scopes,
|
||||
GatewayClientScopes: client?.connect?.scopes ?? [],
|
||||
};
|
||||
|
||||
const agentId = resolveSessionAgentId({
|
||||
|
||||
Reference in New Issue
Block a user