mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 07:30:43 +00:00
fix(active-memory): require admin scope for global toggles [AI] (#78863)
* fix: gate active-memory global writes by admin scope * addressing claude review * docs: add changelog entry for PR merge
This commit is contained in:
committed by
GitHub
parent
f4b2a08c85
commit
5852f5d15c
@@ -144,6 +144,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- fix(active-memory): require admin scope for global toggles [AI]. (#78863) Thanks @pgondhi987.
|
||||
- Honor owner enforcement for native commands [AI]. (#78864) Thanks @pgondhi987.
|
||||
- Tavily: resolve dedicated `tavily_search` and `tavily_extract` tool credentials from the active runtime config snapshot, so `exec` SecretRef-backed API keys do not reach the tools unresolved. (#78610) Thanks @VACInc.
|
||||
- Gateway/sessions: clear cached skills snapshots during `/new` and `sessions.reset` so long-lived channel sessions rebuild the visible skill list after skills change. (#78873) Thanks @Evizero.
|
||||
|
||||
@@ -440,6 +440,108 @@ describe("active-memory plugin", () => {
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("blocks gateway callers without admin scope from changing global active-memory config", async () => {
|
||||
const command = registeredCommands["active-memory"];
|
||||
|
||||
for (const { args, gatewayClientScopes } of [
|
||||
{ args: "off --global", gatewayClientScopes: ["operator.write"] },
|
||||
{ args: "on --global", gatewayClientScopes: ["operator.write"] },
|
||||
{ args: "disable --global", gatewayClientScopes: ["operator.write"] },
|
||||
{ args: "enable --global", gatewayClientScopes: ["operator.write"] },
|
||||
{ args: "disabled --global", gatewayClientScopes: ["operator.write"] },
|
||||
{ args: "enabled --global", gatewayClientScopes: ["operator.write"] },
|
||||
{ args: "off --global", gatewayClientScopes: [] },
|
||||
]) {
|
||||
const result = await command.handler({
|
||||
channel: "gateway",
|
||||
isAuthorizedSender: true,
|
||||
gatewayClientScopes,
|
||||
args,
|
||||
commandBody: `/active-memory ${args}`,
|
||||
config: {},
|
||||
requestConversationBinding: async () => ({ status: "error", message: "unsupported" }),
|
||||
detachConversationBinding: async () => ({ removed: false }),
|
||||
getCurrentConversationBinding: async () => null,
|
||||
});
|
||||
|
||||
expect(result.text).toContain("global enable/disable changes require operator.admin");
|
||||
}
|
||||
|
||||
expect(api.runtime.config.replaceConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows admin-scoped gateway callers to change global active-memory config", async () => {
|
||||
const command = registeredCommands["active-memory"];
|
||||
|
||||
const result = await command.handler({
|
||||
channel: "gateway",
|
||||
isAuthorizedSender: true,
|
||||
gatewayClientScopes: ["operator.admin"],
|
||||
args: "off --global",
|
||||
commandBody: "/active-memory off --global",
|
||||
config: {},
|
||||
requestConversationBinding: async () => ({ status: "error", message: "unsupported" }),
|
||||
detachConversationBinding: async () => ({ removed: false }),
|
||||
getCurrentConversationBinding: async () => null,
|
||||
});
|
||||
|
||||
expect(result.text).toBe("Active Memory: off globally.");
|
||||
expect(api.runtime.config.replaceConfigFile).toHaveBeenCalledTimes(1);
|
||||
expect(configFile).toMatchObject({
|
||||
plugins: {
|
||||
entries: {
|
||||
"active-memory": {
|
||||
enabled: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
agents: ["main"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps write-scoped gateway callers on non-global-write active-memory paths", async () => {
|
||||
const command = registeredCommands["active-memory"];
|
||||
const sessionKey = "agent:main:write-scoped-active-memory";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-write-scoped-active-memory",
|
||||
updatedAt: 0,
|
||||
};
|
||||
|
||||
const globalStatusResult = await command.handler({
|
||||
channel: "gateway",
|
||||
isAuthorizedSender: true,
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
args: "status --global",
|
||||
commandBody: "/active-memory status --global",
|
||||
config: {},
|
||||
requestConversationBinding: async () => ({ status: "error", message: "unsupported" }),
|
||||
detachConversationBinding: async () => ({ removed: false }),
|
||||
getCurrentConversationBinding: async () => null,
|
||||
});
|
||||
|
||||
expect(globalStatusResult.text).toBe("Active Memory: on globally.");
|
||||
expect(api.runtime.config.replaceConfigFile).not.toHaveBeenCalled();
|
||||
|
||||
const sessionOffResult = await command.handler({
|
||||
channel: "gateway",
|
||||
isAuthorizedSender: true,
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
sessionKey,
|
||||
args: "off",
|
||||
commandBody: "/active-memory off",
|
||||
config: {},
|
||||
requestConversationBinding: async () => ({ status: "error", message: "unsupported" }),
|
||||
detachConversationBinding: async () => ({ removed: false }),
|
||||
getCurrentConversationBinding: async () => null,
|
||||
});
|
||||
|
||||
expect(sessionOffResult.text).toBe("Active Memory: off for this session.");
|
||||
expect(api.runtime.config.replaceConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses live runtime config for before_prompt_build enablement", async () => {
|
||||
configFile = {
|
||||
plugins: {
|
||||
|
||||
@@ -782,6 +782,13 @@ function updateActiveMemoryGlobalEnabledInConfig(
|
||||
};
|
||||
}
|
||||
|
||||
function requiresAdminToMutateActiveMemoryGlobal(gatewayClientScopes?: readonly string[]): boolean {
|
||||
return Array.isArray(gatewayClientScopes) && !gatewayClientScopes.includes("operator.admin");
|
||||
}
|
||||
|
||||
const ACTIVE_MEMORY_GLOBAL_MUTATION_ADMIN_REQUIRED_TEXT =
|
||||
"⚠️ /active-memory global enable/disable changes require operator.admin for gateway clients.";
|
||||
|
||||
function normalizePluginConfig(pluginConfig: unknown): ResolvedActiveRecallPluginConfig {
|
||||
const raw = (
|
||||
pluginConfig && typeof pluginConfig === "object" ? pluginConfig : {}
|
||||
@@ -2819,6 +2826,11 @@ export default definePluginEntry({
|
||||
text: `Active Memory: ${isActiveMemoryGloballyEnabled(currentConfig) ? "on" : "off"} globally.`,
|
||||
};
|
||||
}
|
||||
if (requiresAdminToMutateActiveMemoryGlobal(ctx.gatewayClientScopes)) {
|
||||
return {
|
||||
text: ACTIVE_MEMORY_GLOBAL_MUTATION_ADMIN_REQUIRED_TEXT,
|
||||
};
|
||||
}
|
||||
if (action === "on" || action === "enable" || action === "enabled") {
|
||||
const nextConfig = updateActiveMemoryGlobalEnabledInConfig(currentConfig, true);
|
||||
await api.runtime.config.replaceConfigFile({
|
||||
|
||||
Reference in New Issue
Block a user