Skip to content

Commit e8fa108

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents 0d7c399 + 6af17b3 commit e8fa108

5 files changed

Lines changed: 76 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
3131
- Gateway/startup: keep WebSocket RPC available while channels and plugin sidecars start, hold `chat.history` unavailable until startup sidecars finish so synchronous history reads cannot stall startup (reported in #63450), refresh advertised gateway methods after deferred plugin reloads, and enforce the pre-auth WebSocket upgrade budget before the no-handler 503 path so upgrade floods cannot bypass connection limits during that window. (#63480) Thanks @neeravmakwana.
3232
- Gateway/tailscale: start Tailscale exposure and the gateway update check before awaiting channel and plugin sidecar startup so remote operators are not locked out when startup sidecars stall.
3333
- QQBot/streaming: make block streaming configurable per QQ bot account via `streaming.mode` (`"partial"` | `"off"`, default `"partial"`) instead of hardcoding it off, so responses can be delivered incrementally. (#63746)
34+
- Dreaming/gateway: require `operator.admin` for persistent `/dreaming on|off` changes and treat missing gateway client scopes as unprivileged instead of silently allowing config writes. (#63872) Thanks @mbelinky.
3435

3536
## 2026.4.9
3637

extensions/memory-core/src/dreaming-command.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,17 @@ function createHarness(initialConfig: OpenClawConfig = {}) {
5252
};
5353
}
5454

55-
function createCommandContext(args?: string): PluginCommandContext {
55+
function createCommandContext(
56+
args?: string,
57+
overrides?: Partial<Pick<PluginCommandContext, "gatewayClientScopes">>,
58+
): PluginCommandContext {
5659
return {
5760
channel: "webchat",
5861
isAuthorizedSender: true,
5962
commandBody: args ? `/dreaming ${args}` : "/dreaming",
6063
args,
6164
config: {},
65+
gatewayClientScopes: overrides?.gatewayClientScopes,
6266
requestConversationBinding: async () => ({ status: "error", message: "unsupported" }),
6367
detachConversationBinding: async () => ({ removed: false }),
6468
getCurrentConversationBinding: async () => null,
@@ -115,6 +119,48 @@ describe("memory-core /dreaming command", () => {
115119
expect(result.text).toContain("Dreaming disabled.");
116120
});
117121

122+
it("blocks unscoped gateway callers from persisting dreaming config", async () => {
123+
const { command, runtime } = createHarness();
124+
125+
const result = await command.handler(
126+
createCommandContext("off", {
127+
gatewayClientScopes: [],
128+
}),
129+
);
130+
131+
expect(result.text).toContain("requires operator.admin");
132+
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
133+
});
134+
135+
it("blocks write-scoped gateway callers from persisting dreaming config", async () => {
136+
const { command, runtime } = createHarness();
137+
138+
const result = await command.handler(
139+
createCommandContext("off", {
140+
gatewayClientScopes: ["operator.write"],
141+
}),
142+
);
143+
144+
expect(result.text).toContain("requires operator.admin");
145+
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
146+
});
147+
148+
it("allows admin-scoped gateway callers to persist dreaming config", async () => {
149+
const { command, runtime, getRuntimeConfig } = createHarness();
150+
151+
const result = await command.handler(
152+
createCommandContext("on", {
153+
gatewayClientScopes: ["operator.admin"],
154+
}),
155+
);
156+
157+
expect(runtime.config.writeConfigFile).toHaveBeenCalledTimes(1);
158+
expect(resolveStoredDreaming(getRuntimeConfig())).toMatchObject({
159+
enabled: true,
160+
});
161+
expect(result.text).toContain("Dreaming enabled.");
162+
});
163+
118164
it("returns status without mutating config", async () => {
119165
const { command, runtime } = createHarness({
120166
plugins: {

extensions/memory-core/src/dreaming-command.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ function formatUsage(includeStatus: string): string {
7575
].join("\n");
7676
}
7777

78+
function requiresAdminToMutateDreaming(gatewayClientScopes?: readonly string[]): boolean {
79+
return Array.isArray(gatewayClientScopes) && !gatewayClientScopes.includes("operator.admin");
80+
}
81+
7882
export function registerDreamingCommand(api: OpenClawPluginApi): void {
7983
api.registerCommand({
8084
name: "dreaming",
@@ -102,6 +106,9 @@ export function registerDreamingCommand(api: OpenClawPluginApi): void {
102106
}
103107

104108
if (firstToken === "on" || firstToken === "off") {
109+
if (requiresAdminToMutateDreaming(ctx.gatewayClientScopes)) {
110+
return { text: "⚠️ /dreaming on|off requires operator.admin for gateway clients." };
111+
}
105112
const enabled = firstToken === "on";
106113
const nextConfig = updateDreamingEnabledInConfig(currentConfig, enabled);
107114
await api.runtime.config.writeConfigFile(nextConfig);

src/gateway/server-methods/chat.directive-tags.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ function extractFirstTextBlock(payload: unknown): string | undefined {
207207
}
208208

209209
function createScopedCliClient(
210-
scopes: string[],
210+
scopes?: string[],
211211
client: Partial<{
212212
id: string;
213213
mode: string;
@@ -1414,6 +1414,25 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
14141414
expect(mockState.lastDispatchCtx?.CommandBody).toBe("/scopecheck");
14151415
});
14161416

1417+
it("normalizes missing gateway caller scopes to an empty array before dispatch", async () => {
1418+
createTranscriptFixture("openclaw-chat-send-missing-gateway-client-scopes-");
1419+
mockState.finalText = "ok";
1420+
const respond = vi.fn();
1421+
const context = createChatContext();
1422+
1423+
await runNonStreamingChatSend({
1424+
context,
1425+
respond,
1426+
idempotencyKey: "idem-gateway-client-scopes-missing",
1427+
message: "/scopecheck",
1428+
client: createScopedCliClient(),
1429+
expectBroadcast: false,
1430+
});
1431+
1432+
expect(mockState.lastDispatchCtx?.GatewayClientScopes).toEqual([]);
1433+
expect(mockState.lastDispatchCtx?.CommandBody).toBe("/scopecheck");
1434+
});
1435+
14171436
it("injects ACP system provenance into the agent-visible body", async () => {
14181437
createTranscriptFixture("openclaw-chat-send-system-provenance-acp-");
14191438
mockState.finalText = "ok";

src/gateway/server-methods/chat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1671,7 +1671,7 @@ export const chatHandlers: GatewayRequestHandlers = {
16711671
SenderId: clientInfo?.id,
16721672
SenderName: clientInfo?.displayName,
16731673
SenderUsername: clientInfo?.displayName,
1674-
GatewayClientScopes: client?.connect?.scopes,
1674+
GatewayClientScopes: client?.connect?.scopes ?? [],
16751675
};
16761676

16771677
const agentId = resolveSessionAgentId({

0 commit comments

Comments
 (0)