mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 09:33:06 +00:00
Memory/QMD: parse scope once in qmd scope checks
This commit is contained in:
@@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord: prefer gateway guild id when logging inbound messages so cached-miss guilds do not appear as `guild=dm`. Thanks @thewilloftheshadow.
|
||||
- TUI: refactor searchable select list description layout and add regression coverage for ANSI-highlight width bounds.
|
||||
- Memory/QMD: cap QMD command output buffering to prevent memory exhaustion from pathological `qmd` command output.
|
||||
- Memory/QMD: parse qmd scope keys once per request to avoid repeated parsing in scope checks.
|
||||
- Memory/QMD: query QMD index using exact docid matches before falling back to prefix lookup for better recall correctness and index efficiency.
|
||||
- Memory/QMD: make QMD result JSON parsing resilient to noisy command output by extracting the first JSON array from noisy `stdout`.
|
||||
- Memory/QMD: pass result limits to `search`/`vsearch` commands so QMD can cap results earlier.
|
||||
|
||||
36
src/memory/qmd-scope.test.ts
Normal file
36
src/memory/qmd-scope.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ResolvedQmdConfig } from "./backend-config.js";
|
||||
import { deriveQmdScopeChannel, deriveQmdScopeChatType, isQmdScopeAllowed } from "./qmd-scope.js";
|
||||
|
||||
describe("qmd scope", () => {
|
||||
const allowDirect: ResolvedQmdConfig["scope"] = {
|
||||
default: "deny",
|
||||
rules: [{ action: "allow", match: { chatType: "direct" } }],
|
||||
};
|
||||
|
||||
it("derives channel and chat type from canonical keys once", () => {
|
||||
expect(deriveQmdScopeChannel("Workspace:group:123")).toBe("workspace");
|
||||
expect(deriveQmdScopeChatType("Workspace:group:123")).toBe("group");
|
||||
});
|
||||
|
||||
it("derives channel and chat type from stored key suffixes", () => {
|
||||
expect(deriveQmdScopeChannel("agent:agent-1:workspace:channel:chan-123")).toBe("workspace");
|
||||
expect(deriveQmdScopeChatType("agent:agent-1:workspace:channel:chan-123")).toBe("channel");
|
||||
});
|
||||
|
||||
it("treats parsed keys with no chat prefix as direct", () => {
|
||||
expect(deriveQmdScopeChannel("agent:agent-1:peer-direct")).toBeUndefined();
|
||||
expect(deriveQmdScopeChatType("agent:agent-1:peer-direct")).toBe("direct");
|
||||
expect(isQmdScopeAllowed(allowDirect, "agent:agent-1:peer-direct")).toBe(true);
|
||||
expect(isQmdScopeAllowed(allowDirect, "agent:agent-1:peer:group:abc")).toBe(false);
|
||||
});
|
||||
|
||||
it("applies scoped key-prefix checks against normalized key", () => {
|
||||
const scope: ResolvedQmdConfig["scope"] = {
|
||||
default: "deny",
|
||||
rules: [{ action: "allow", match: { keyPrefix: "workspace:" } }],
|
||||
};
|
||||
expect(isQmdScopeAllowed(scope, "agent:agent-1:workspace:group:123")).toBe(true);
|
||||
expect(isQmdScopeAllowed(scope, "agent:agent-1:other:group:123")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,20 @@
|
||||
import type { ResolvedQmdConfig } from "./backend-config.js";
|
||||
import { parseAgentSessionKey } from "../sessions/session-key-utils.js";
|
||||
|
||||
type ParsedQmdSessionScope = {
|
||||
channel?: string;
|
||||
chatType?: "channel" | "group" | "direct";
|
||||
normalizedKey?: string;
|
||||
};
|
||||
|
||||
export function isQmdScopeAllowed(scope: ResolvedQmdConfig["scope"], sessionKey?: string): boolean {
|
||||
if (!scope) {
|
||||
return true;
|
||||
}
|
||||
const channel = deriveQmdScopeChannel(sessionKey);
|
||||
const chatType = deriveQmdScopeChatType(sessionKey);
|
||||
const normalizedKey = sessionKey ?? "";
|
||||
const parsed = parseQmdSessionScope(sessionKey);
|
||||
const channel = parsed.channel;
|
||||
const chatType = parsed.chatType;
|
||||
const normalizedKey = parsed.normalizedKey ?? "";
|
||||
for (const rule of scope.rules ?? []) {
|
||||
if (!rule) {
|
||||
continue;
|
||||
@@ -29,38 +36,42 @@ export function isQmdScopeAllowed(scope: ResolvedQmdConfig["scope"], sessionKey?
|
||||
}
|
||||
|
||||
export function deriveQmdScopeChannel(key?: string): string | undefined {
|
||||
if (!key) {
|
||||
return undefined;
|
||||
}
|
||||
return parseQmdSessionScope(key).channel;
|
||||
}
|
||||
|
||||
export function deriveQmdScopeChatType(key?: string): "channel" | "group" | "direct" | undefined {
|
||||
return parseQmdSessionScope(key).chatType;
|
||||
}
|
||||
|
||||
function parseQmdSessionScope(key?: string): ParsedQmdSessionScope {
|
||||
const normalized = normalizeQmdSessionKey(key);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
return {};
|
||||
}
|
||||
const parts = normalized.split(":").filter(Boolean);
|
||||
let chatType: ParsedQmdSessionScope["chatType"];
|
||||
if (
|
||||
parts.length >= 2 &&
|
||||
(parts[1] === "group" || parts[1] === "channel" || parts[1] === "direct" || parts[1] === "dm")
|
||||
) {
|
||||
return parts[0]?.toLowerCase();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function deriveQmdScopeChatType(key?: string): "channel" | "group" | "direct" | undefined {
|
||||
if (!key) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeQmdSessionKey(key);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
if (parts.includes("group")) {
|
||||
chatType = "group";
|
||||
} else if (parts.includes("channel")) {
|
||||
chatType = "channel";
|
||||
}
|
||||
return {
|
||||
normalizedKey: normalized,
|
||||
channel: parts[0]?.toLowerCase(),
|
||||
chatType: chatType ?? "direct",
|
||||
};
|
||||
}
|
||||
if (normalized.includes(":group:")) {
|
||||
return "group";
|
||||
return { normalizedKey: normalized, chatType: "group" };
|
||||
}
|
||||
if (normalized.includes(":channel:")) {
|
||||
return "channel";
|
||||
return { normalizedKey: normalized, chatType: "channel" };
|
||||
}
|
||||
return "direct";
|
||||
return { normalizedKey: normalized, chatType: "direct" };
|
||||
}
|
||||
|
||||
function normalizeQmdSessionKey(key: string): string | undefined {
|
||||
|
||||
Reference in New Issue
Block a user