diff --git a/.gitignore b/.gitignore index 9763a2b..5f831d2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ temp/ *.tsbuildinfo .DS_Store -pnpm-lock.yaml \ No newline at end of file +pnpm-lock.yaml.worktrees/ diff --git a/README.md b/README.md index 1ea38e6..e42cb0d 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ Or configure in `~/.openclaw/openclaw.json`: | `enableCustomContainerTags` | `boolean` | `false` | Enable custom container routing. | | `customContainers` | `array` | `[]` | Custom containers with `tag` and `description`. | | `customContainerInstructions` | `string` | `""` | Instructions for AI on container routing. | +| `allowedAgents` | `string[]`| omitted | Restrict auto-capture and auto-recall to matching agent session keys. | ### Full Example @@ -117,7 +118,8 @@ Or configure in `~/.openclaw/openclaw.json`: { "tag": "work", "description": "Work-related memories" }, { "tag": "personal", "description": "Personal notes" } ], - "customContainerInstructions": "Store work tasks in 'work', personal stuff in 'personal'" + "customContainerInstructions": "Store work tasks in 'work', personal stuff in 'personal'", + "allowedAgents": ["navi", "heimerdinger"] } } } diff --git a/allowed-agents.test.ts b/allowed-agents.test.ts new file mode 100644 index 0000000..71b360d --- /dev/null +++ b/allowed-agents.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it, mock } from "bun:test" +import { parseConfig } from "./config.ts" +import { buildCaptureHandler } from "./hooks/capture.ts" +import { buildRecallHandler } from "./hooks/recall.ts" + +describe("allowedAgents config", () => { + it("parses allowedAgents from config", () => { + const cfg = parseConfig({ + apiKey: "test-key", + allowedAgents: ["navi", "heimerdinger"], + }) + + expect(cfg.allowedAgents).toEqual(["navi", "heimerdinger"]) + }) + + it("skips capture when sessionKey does not match allowedAgents", async () => { + const addMemory = mock(async () => undefined) + const handler = buildCaptureHandler( + { addMemory } as never, + parseConfig({ apiKey: "test-key", allowedAgents: ["navi"] }), + () => "agent:heimerdinger:main", + ) + + await handler( + { + success: true, + messages: [{ role: "user", content: "hello" }], + }, + { messageProvider: "discord", sessionKey: "agent:heimerdinger:main" }, + ) + + expect(addMemory).not.toHaveBeenCalled() + }) + + it("processes capture when sessionKey is undefined (no silent data loss)", async () => { + const addMemory = mock(async () => undefined) + const handler = buildCaptureHandler( + { addMemory } as never, + parseConfig({ apiKey: "test-key", allowedAgents: ["navi"] }), + () => undefined, + ) + + await handler( + { + success: true, + messages: [{ role: "user", content: "hello" }], + }, + { messageProvider: "discord" }, + ) + + expect(addMemory).toHaveBeenCalled() + }) + + it("processes recall when sessionKey is undefined (no silent data loss)", async () => { + const getProfile = mock(async () => ({ + static: ["persistent fact"], + dynamic: [], + searchResults: [], + })) + const handler = buildRecallHandler( + { getProfile } as never, + parseConfig({ apiKey: "test-key", allowedAgents: ["navi"] }), + ) + + const result = await handler( + { + prompt: "Tell me what you remember about me", + messages: [{ role: "user", content: "hello" }], + }, + { messageProvider: "discord" }, + ) + + expect(getProfile).toHaveBeenCalled() + }) + + it("skips recall when sessionKey does not match allowedAgents", async () => { + const getProfile = mock(async () => ({ + static: ["persistent fact"], + dynamic: [], + searchResults: [], + })) + const handler = buildRecallHandler( + { getProfile } as never, + parseConfig({ apiKey: "test-key", allowedAgents: ["navi"] }), + ) + + const result = await handler( + { + prompt: "Tell me what you remember about me", + messages: [{ role: "user", content: "hello" }], + }, + { messageProvider: "discord", sessionKey: "agent:heimerdinger:main" }, + ) + + expect(result).toBeUndefined() + expect(getProfile).not.toHaveBeenCalled() + }) +}) diff --git a/config.ts b/config.ts index e9d917e..f211a7a 100644 --- a/config.ts +++ b/config.ts @@ -21,6 +21,7 @@ export type SupermemoryConfig = { enableCustomContainerTags: boolean customContainers: CustomContainer[] customContainerInstructions: string + allowedAgents?: string[] } const ALLOWED_KEYS = [ @@ -36,6 +37,7 @@ const ALLOWED_KEYS = [ "enableCustomContainerTags", "customContainers", "customContainerInstructions", + "allowedAgents", ] function assertAllowedKeys( @@ -107,6 +109,13 @@ export function parseConfig(raw: unknown): SupermemoryConfig { } } + const allowedAgents = Array.isArray(cfg.allowedAgents) + ? cfg.allowedAgents.filter( + (agentId): agentId is string => + typeof agentId === "string" && agentId.trim().length > 0, + ) + : undefined + return { apiKey, containerTag: cfg.containerTag @@ -132,6 +141,7 @@ export function parseConfig(raw: unknown): SupermemoryConfig { typeof cfg.customContainerInstructions === "string" ? cfg.customContainerInstructions : "", + allowedAgents, } } @@ -162,6 +172,10 @@ export const supermemoryConfigSchema = { }, }, customContainerInstructions: { type: "string" }, + allowedAgents: { + type: "array", + items: { type: "string" }, + }, }, }, parse: parseConfig, diff --git a/hooks/capture.ts b/hooks/capture.ts index 9bfd930..07330d3 100644 --- a/hooks/capture.ts +++ b/hooks/capture.ts @@ -30,6 +30,15 @@ export function buildCaptureHandler( event: Record, ctx: Record, ) => { + const sessionKey = ctx.sessionKey as string | undefined + if ( + sessionKey && + cfg.allowedAgents?.length && + !cfg.allowedAgents.some((agentId) => sessionKey.includes(agentId)) + ) { + return + } + log.info( `agent_end fired: provider="${ctx.messageProvider}" success=${event.success}`, ) diff --git a/hooks/recall.ts b/hooks/recall.ts index 3f0fbc9..7092ae1 100644 --- a/hooks/recall.ts +++ b/hooks/recall.ts @@ -170,6 +170,15 @@ export function buildRecallHandler( event: Record, ctx?: Record, ) => { + const sessionKey = ctx?.sessionKey as string | undefined + if ( + sessionKey && + cfg.allowedAgents?.length && + !cfg.allowedAgents.some((agentId) => sessionKey.includes(agentId)) + ) { + return + } + const prompt = event.prompt as string | undefined if (!prompt || prompt.length < 5) return