Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ temp/

*.tsbuildinfo
.DS_Store
pnpm-lock.yaml
pnpm-lock.yaml.worktrees/
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"]
}
}
}
Expand Down
98 changes: 98 additions & 0 deletions allowed-agents.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
14 changes: 14 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type SupermemoryConfig = {
enableCustomContainerTags: boolean
customContainers: CustomContainer[]
customContainerInstructions: string
allowedAgents?: string[]
}

const ALLOWED_KEYS = [
Expand All @@ -36,6 +37,7 @@ const ALLOWED_KEYS = [
"enableCustomContainerTags",
"customContainers",
"customContainerInstructions",
"allowedAgents",
]

function assertAllowedKeys(
Expand Down Expand Up @@ -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
Expand All @@ -132,6 +141,7 @@ export function parseConfig(raw: unknown): SupermemoryConfig {
typeof cfg.customContainerInstructions === "string"
? cfg.customContainerInstructions
: "",
allowedAgents,
}
}

Expand Down Expand Up @@ -162,6 +172,10 @@ export const supermemoryConfigSchema = {
},
},
customContainerInstructions: { type: "string" },
allowedAgents: {
type: "array",
items: { type: "string" },
},
},
},
parse: parseConfig,
Expand Down
9 changes: 9 additions & 0 deletions hooks/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ export function buildCaptureHandler(
event: Record<string, unknown>,
ctx: Record<string, unknown>,
) => {
const sessionKey = ctx.sessionKey as string | undefined
if (
sessionKey &&
cfg.allowedAgents?.length &&
!cfg.allowedAgents.some((agentId) => sessionKey.includes(agentId))
) {
return
Comment on lines +33 to +39

This comment was marked as outdated.

}

log.info(
`agent_end fired: provider="${ctx.messageProvider}" success=${event.success}`,
)
Expand Down
9 changes: 9 additions & 0 deletions hooks/recall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ export function buildRecallHandler(
event: Record<string, unknown>,
ctx?: Record<string, unknown>,
) => {
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

Expand Down