mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 13:05:01 +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.
|
- 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.
|
- 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: 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: 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: 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.
|
- 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 type { ResolvedQmdConfig } from "./backend-config.js";
|
||||||
import { parseAgentSessionKey } from "../sessions/session-key-utils.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 {
|
export function isQmdScopeAllowed(scope: ResolvedQmdConfig["scope"], sessionKey?: string): boolean {
|
||||||
if (!scope) {
|
if (!scope) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const channel = deriveQmdScopeChannel(sessionKey);
|
const parsed = parseQmdSessionScope(sessionKey);
|
||||||
const chatType = deriveQmdScopeChatType(sessionKey);
|
const channel = parsed.channel;
|
||||||
const normalizedKey = sessionKey ?? "";
|
const chatType = parsed.chatType;
|
||||||
|
const normalizedKey = parsed.normalizedKey ?? "";
|
||||||
for (const rule of scope.rules ?? []) {
|
for (const rule of scope.rules ?? []) {
|
||||||
if (!rule) {
|
if (!rule) {
|
||||||
continue;
|
continue;
|
||||||
@@ -29,38 +36,42 @@ export function isQmdScopeAllowed(scope: ResolvedQmdConfig["scope"], sessionKey?
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function deriveQmdScopeChannel(key?: string): string | undefined {
|
export function deriveQmdScopeChannel(key?: string): string | undefined {
|
||||||
if (!key) {
|
return parseQmdSessionScope(key).channel;
|
||||||
return undefined;
|
}
|
||||||
}
|
|
||||||
|
export function deriveQmdScopeChatType(key?: string): "channel" | "group" | "direct" | undefined {
|
||||||
|
return parseQmdSessionScope(key).chatType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseQmdSessionScope(key?: string): ParsedQmdSessionScope {
|
||||||
const normalized = normalizeQmdSessionKey(key);
|
const normalized = normalizeQmdSessionKey(key);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return undefined;
|
return {};
|
||||||
}
|
}
|
||||||
const parts = normalized.split(":").filter(Boolean);
|
const parts = normalized.split(":").filter(Boolean);
|
||||||
|
let chatType: ParsedQmdSessionScope["chatType"];
|
||||||
if (
|
if (
|
||||||
parts.length >= 2 &&
|
parts.length >= 2 &&
|
||||||
(parts[1] === "group" || parts[1] === "channel" || parts[1] === "direct" || parts[1] === "dm")
|
(parts[1] === "group" || parts[1] === "channel" || parts[1] === "direct" || parts[1] === "dm")
|
||||||
) {
|
) {
|
||||||
return parts[0]?.toLowerCase();
|
if (parts.includes("group")) {
|
||||||
}
|
chatType = "group";
|
||||||
return undefined;
|
} else if (parts.includes("channel")) {
|
||||||
}
|
chatType = "channel";
|
||||||
|
}
|
||||||
export function deriveQmdScopeChatType(key?: string): "channel" | "group" | "direct" | undefined {
|
return {
|
||||||
if (!key) {
|
normalizedKey: normalized,
|
||||||
return undefined;
|
channel: parts[0]?.toLowerCase(),
|
||||||
}
|
chatType: chatType ?? "direct",
|
||||||
const normalized = normalizeQmdSessionKey(key);
|
};
|
||||||
if (!normalized) {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
if (normalized.includes(":group:")) {
|
if (normalized.includes(":group:")) {
|
||||||
return "group";
|
return { normalizedKey: normalized, chatType: "group" };
|
||||||
}
|
}
|
||||||
if (normalized.includes(":channel:")) {
|
if (normalized.includes(":channel:")) {
|
||||||
return "channel";
|
return { normalizedKey: normalized, chatType: "channel" };
|
||||||
}
|
}
|
||||||
return "direct";
|
return { normalizedKey: normalized, chatType: "direct" };
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeQmdSessionKey(key: string): string | undefined {
|
function normalizeQmdSessionKey(key: string): string | undefined {
|
||||||
|
|||||||
Reference in New Issue
Block a user