From 31995b8bb922209f48e9a8283fc00ddaf8277ac3 Mon Sep 17 00:00:00 2001 From: Rai Butera Date: Sun, 15 Mar 2026 20:32:16 +0000 Subject: [PATCH 1/3] chore: ignore local worktrees --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/ From a9bc0bb77207206660b3ba96dd4f26aa5937f794 Mon Sep 17 00:00:00 2001 From: Rai Butera Date: Sun, 15 Mar 2026 20:35:05 +0000 Subject: [PATCH 2/3] feat: add allowedAgents config for capture and recall --- README.md | 4 ++- allowed-agents.test.ts | 57 ++++++++++++++++++++++++++++++++++++++++++ config.ts | 14 +++++++++++ hooks/capture.ts | 8 ++++++ hooks/recall.ts | 8 ++++++ 5 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 allowed-agents.test.ts 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..cb717c7 --- /dev/null +++ b/allowed-agents.test.ts @@ -0,0 +1,57 @@ +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("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..6cb61c0 100644 --- a/hooks/capture.ts +++ b/hooks/capture.ts @@ -30,6 +30,14 @@ export function buildCaptureHandler( event: Record, ctx: Record, ) => { + const sessionKey = ctx.sessionKey as string | undefined + if ( + 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..f58d38e 100644 --- a/hooks/recall.ts +++ b/hooks/recall.ts @@ -170,6 +170,14 @@ export function buildRecallHandler( event: Record, ctx?: Record, ) => { + const sessionKey = ctx?.sessionKey as string | undefined + if ( + cfg.allowedAgents?.length && + !cfg.allowedAgents.some((agentId) => sessionKey?.includes(agentId)) + ) { + return + } + const prompt = event.prompt as string | undefined if (!prompt || prompt.length < 5) return From 1473703e99f5b232d872a798017a8a20e234dca9 Mon Sep 17 00:00:00 2001 From: Rai Butera Date: Mon, 16 Mar 2026 18:53:07 +0000 Subject: [PATCH 3/3] fix(allowed-agents): prevent silent data loss when sessionKey is undefined When cfg.allowedAgents is set and sessionKey is undefined, the guard condition used optional chaining (sessionKey?.includes()) which returns undefined. Array.some() treated undefined as falsy, so .some() returned false. The negation (!false) triggered an early return, silently blocking capture/recall for sessions without a sessionKey. Fix: only apply the allowedAgents filter when sessionKey is present. Sessions without a sessionKey fall through and are processed normally, restoring prior behavior. Also adds two regression tests covering the undefined sessionKey case. --- allowed-agents.test.ts | 41 +++++++++++++++++++++++++++++++++++++++++ hooks/capture.ts | 3 ++- hooks/recall.ts | 3 ++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/allowed-agents.test.ts b/allowed-agents.test.ts index cb717c7..71b360d 100644 --- a/allowed-agents.test.ts +++ b/allowed-agents.test.ts @@ -32,6 +32,47 @@ describe("allowedAgents config", () => { 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"], diff --git a/hooks/capture.ts b/hooks/capture.ts index 6cb61c0..07330d3 100644 --- a/hooks/capture.ts +++ b/hooks/capture.ts @@ -32,8 +32,9 @@ export function buildCaptureHandler( ) => { const sessionKey = ctx.sessionKey as string | undefined if ( + sessionKey && cfg.allowedAgents?.length && - !cfg.allowedAgents.some((agentId) => sessionKey?.includes(agentId)) + !cfg.allowedAgents.some((agentId) => sessionKey.includes(agentId)) ) { return } diff --git a/hooks/recall.ts b/hooks/recall.ts index f58d38e..7092ae1 100644 --- a/hooks/recall.ts +++ b/hooks/recall.ts @@ -172,8 +172,9 @@ export function buildRecallHandler( ) => { const sessionKey = ctx?.sessionKey as string | undefined if ( + sessionKey && cfg.allowedAgents?.length && - !cfg.allowedAgents.some((agentId) => sessionKey?.includes(agentId)) + !cfg.allowedAgents.some((agentId) => sessionKey.includes(agentId)) ) { return }