Skip to content

Commit 29f787f

Browse files
authored
fix(memory): require privileged dreaming config changes (#97869)
* fix(memory): gate dreaming config changes * fix(memory): document dreaming privilege gate Explain that persistent dreaming toggles require channel owner status or Gateway admin scope, while status and help remain read-only.
1 parent 05d8312 commit 29f787f

7 files changed

Lines changed: 58 additions & 18 deletions

File tree

docs/cli/memory.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ facts into `MEMORY.md`), and **REM** (reflect and surface themes).
133133

134134
- Enable with `plugins.entries.memory-core.config.dreaming.enabled: true`.
135135
- Toggle from chat with `/dreaming on|off` (or inspect with `/dreaming status`).
136+
Channel callers must be owners to change the setting; Gateway clients need
137+
`operator.admin`. Read-only status and help remain available to authorized
138+
command senders.
136139
- Dreaming runs on one managed sweep schedule (`dreaming.frequency`) and executes phases in order: light, REM, deep.
137140
- Only the deep phase writes durable memory to `MEMORY.md`.
138141
- Human-readable phase output and diary entries are written to `DREAMS.md` (or existing `dreams.md`), with optional per-phase reports in `memory/dreaming/<phase>/YYYY-MM-DD.md`.

docs/concepts/dreaming.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ Default cadence behavior:
197197
/dreaming help
198198
```
199199

200+
`/dreaming on` and `/dreaming off` change gateway-wide configuration. Channel
201+
callers must be owners, and Gateway clients must have `operator.admin`.
202+
`/dreaming status` and `/dreaming help` remain read-only.
203+
200204
## CLI workflow
201205

202206
<Tabs>

docs/tools/slash-commands.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -301,14 +301,14 @@ must be in the same identity group.
301301

302302
### Bundled plugin commands
303303

304-
| Command | Description |
305-
| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
306-
| `/dreaming [on\|off\|status\|help]` | Toggle memory dreaming. See [Dreaming](/concepts/dreaming) |
307-
| `/pair [qr\|status\|pending\|approve\|cleanup\|notify]` | Manage device pairing. See [Pairing](/channels/pairing) |
308-
| `/phone status\|arm ...\|disarm` | Temporarily arm high-risk phone node commands |
309-
| `/voice status\|list\|set <voiceId>` | Manage Talk voice config. Discord native name: `/talkvoice` |
310-
| `/card ...` | Send LINE rich card presets. See [LINE](/channels/line) |
311-
| `/codex status\|models\|threads\|resume\|compact\|review\|diagnostics\|account\|mcp\|skills` | Control the Codex app-server harness. See [Codex harness](/plugins/codex-harness) |
304+
| Command | Description |
305+
| -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
306+
| `/dreaming [on\|off\|status\|help]` | Toggle memory dreaming (owner or Gateway admin). See [Dreaming](/concepts/dreaming) |
307+
| `/pair [qr\|status\|pending\|approve\|cleanup\|notify]` | Manage device pairing. See [Pairing](/channels/pairing) |
308+
| `/phone status\|arm ...\|disarm` | Temporarily arm high-risk phone node commands |
309+
| `/voice status\|list\|set <voiceId>` | Manage Talk voice config. Discord native name: `/talkvoice` |
310+
| `/card ...` | Send LINE rich card presets. See [LINE](/channels/line) |
311+
| `/codex status\|models\|threads\|resume\|compact\|review\|diagnostics\|account\|mcp\|skills` | Control the Codex app-server harness. See [Codex harness](/plugins/codex-harness) |
312312

313313
QQBot-only: `/bot-ping`, `/bot-version`, `/bot-help`, `/bot-upgrade`, `/bot-logs`
314314

extensions/memory-core/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ describe("memory-core plugin runtime registration", () => {
101101

102102
expect(command?.name).toBe("dreaming");
103103
expect(command?.acceptsArgs).toBe(true);
104+
expect(command?.exposeSenderIsOwner).toBe(true);
104105
expect(command?.description).toContain("Enable or disable");
105106
});
106107

extensions/memory-core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ export default definePluginEntry({
209209
name: "dreaming",
210210
description: "Enable or disable memory dreaming.",
211211
acceptsArgs: true,
212+
exposeSenderIsOwner: true,
212213
handler: async (ctx) => {
213214
const { handleDreamingCommand } = await import("./src/dreaming-command.js");
214215
return await handleDreamingCommand(api, ctx);

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

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ function createHarness(initialConfig: OpenClawConfig = {}) {
6262

6363
function createCommandContext(
6464
args?: string,
65-
overrides?: Partial<Pick<PluginCommandContext, "gatewayClientScopes">>,
65+
overrides?: Partial<Pick<PluginCommandContext, "gatewayClientScopes" | "senderIsOwner">>,
6666
): PluginCommandContext {
6767
return {
6868
channel: "webchat",
@@ -71,6 +71,7 @@ function createCommandContext(
7171
args,
7272
config: {},
7373
gatewayClientScopes: overrides?.gatewayClientScopes,
74+
senderIsOwner: overrides?.senderIsOwner,
7475
requestConversationBinding: async () => ({ status: "error", message: "unsupported" }),
7576
detachConversationBinding: async () => ({ removed: false }),
7677
getCurrentConversationBinding: async () => null,
@@ -80,7 +81,7 @@ function createCommandContext(
8081
async function runDreamingCommand(
8182
harness: ReturnType<typeof createHarness>,
8283
args?: string,
83-
overrides?: Partial<Pick<PluginCommandContext, "gatewayClientScopes">>,
84+
overrides?: Partial<Pick<PluginCommandContext, "gatewayClientScopes" | "senderIsOwner">>,
8485
) {
8586
return await handleDreamingCommand(harness.api, createCommandContext(args, overrides));
8687
}
@@ -98,7 +99,18 @@ describe("memory-core /dreaming command", () => {
9899
);
99100
});
100101

101-
it("persists global enablement under plugins.entries.memory-core.config.dreaming.enabled", async () => {
102+
it("blocks non-owner external channel callers from persisting dreaming config", async () => {
103+
const harness = createHarness();
104+
105+
const result = await runDreamingCommand(harness, "off");
106+
107+
expect(result.text).toContain(
108+
"requires owner status for channel callers or operator.admin for gateway clients",
109+
);
110+
expect(harness.runtime.config.mutateConfigFile).not.toHaveBeenCalled();
111+
});
112+
113+
it("allows owner external channel callers to persist global enablement", async () => {
102114
const harness = createHarness({
103115
plugins: {
104116
entries: {
@@ -118,7 +130,9 @@ describe("memory-core /dreaming command", () => {
118130
},
119131
});
120132

121-
const result = await runDreamingCommand(harness, "off");
133+
const result = await runDreamingCommand(harness, "off", {
134+
senderIsOwner: true,
135+
});
122136

123137
expect(harness.runtime.config.mutateConfigFile).toHaveBeenCalledTimes(1);
124138
const storedDreaming = resolveStoredDreaming(harness.getRuntimeConfig());
@@ -134,7 +148,9 @@ describe("memory-core /dreaming command", () => {
134148
gatewayClientScopes: [],
135149
});
136150

137-
expect(result.text).toContain("requires operator.admin");
151+
expect(result.text).toContain(
152+
"requires owner status for channel callers or operator.admin for gateway clients",
153+
);
138154
expect(harness.runtime.config.mutateConfigFile).not.toHaveBeenCalled();
139155
});
140156

@@ -145,7 +161,9 @@ describe("memory-core /dreaming command", () => {
145161
gatewayClientScopes: ["operator.write"],
146162
});
147163

148-
expect(result.text).toContain("requires operator.admin");
164+
expect(result.text).toContain(
165+
"requires owner status for channel callers or operator.admin for gateway clients",
166+
);
149167
expect(harness.runtime.config.mutateConfigFile).not.toHaveBeenCalled();
150168
});
151169

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,14 @@ function formatUsage(includeStatus: string): string {
7777
].join("\n");
7878
}
7979

80-
function requiresAdminToMutateDreaming(gatewayClientScopes?: readonly string[]): boolean {
81-
return Array.isArray(gatewayClientScopes) && !gatewayClientScopes.includes("operator.admin");
80+
function lacksAdminOrOwnerForDreamingMutation(params: {
81+
gatewayClientScopes?: readonly string[];
82+
senderIsOwner?: boolean;
83+
}): boolean {
84+
if (Array.isArray(params.gatewayClientScopes)) {
85+
return !params.gatewayClientScopes.includes("operator.admin");
86+
}
87+
return params.senderIsOwner !== true;
8288
}
8389

8490
export async function handleDreamingCommand(api: OpenClawPluginApi, ctx: PluginCommandContext) {
@@ -98,8 +104,15 @@ export async function handleDreamingCommand(api: OpenClawPluginApi, ctx: PluginC
98104
}
99105

100106
if (firstToken === "on" || firstToken === "off") {
101-
if (requiresAdminToMutateDreaming(ctx.gatewayClientScopes)) {
102-
return { text: "⚠️ /dreaming on|off requires operator.admin for gateway clients." };
107+
if (
108+
lacksAdminOrOwnerForDreamingMutation({
109+
gatewayClientScopes: ctx.gatewayClientScopes,
110+
senderIsOwner: ctx.senderIsOwner,
111+
})
112+
) {
113+
return {
114+
text: "⚠️ /dreaming on|off requires owner status for channel callers or operator.admin for gateway clients.",
115+
};
103116
}
104117
const enabled = firstToken === "on";
105118
const committed = await api.runtime.config.mutateConfigFile({

0 commit comments

Comments
 (0)