@mingxy/cerebro 1.20.4 → 1.20.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,508 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ // ─── Hoisted mock functions ────────────────────────────────────
4
+
5
+ const { mockResolve, mockIsEnabled, mockSetEnabled } = vi.hoisted(() => ({
6
+ mockResolve: vi.fn<() => string>().mockReturnValue("readwrite"),
7
+ mockIsEnabled: vi.fn<() => boolean>().mockReturnValue(true),
8
+ mockSetEnabled: vi.fn(),
9
+ }));
10
+
11
+ // ─── Module mocks ──────────────────────────────────────────────
12
+
13
+ vi.mock("@opencode-ai/plugin", () => {
14
+ function sb() {
15
+ const b = { describe() { return b; }, optional() { return b; } };
16
+ return b;
17
+ }
18
+ return {
19
+ tool: Object.assign((def: any) => def, {
20
+ schema: { string: sb, number: sb, array: sb, enum: sb, object: sb },
21
+ }),
22
+ };
23
+ });
24
+
25
+ vi.mock("./config.js", () => ({ resolveAgentPolicy: mockResolve }));
26
+ vi.mock("./index.js", () => ({
27
+ isAutoStoreEnabled: mockIsEnabled,
28
+ setAutoStoreEnabled: mockSetEnabled,
29
+ }));
30
+
31
+ // ─── Imports ───────────────────────────────────────────────────
32
+
33
+ import { buildTools, type ToolContext } from "./tools.js";
34
+
35
+ // ─── Constants ─────────────────────────────────────────────────
36
+
37
+ const ALL_TOOLS = [
38
+ "memory_store", "memory_search", "memory_get", "memory_update",
39
+ "memory_profile", "memory_profile_stats", "memory_list", "memory_ingest",
40
+ "memory_stats", "memory_delete", "space_create", "space_list",
41
+ "space_add_member", "memory_share", "memory_pull", "memory_reshare",
42
+ "memory_toggle",
43
+ ] as const;
44
+
45
+ type TN = typeof ALL_TOOLS[number];
46
+
47
+ const READ_TOOLS: readonly TN[] = [
48
+ "memory_search", "memory_get", "memory_profile", "memory_profile_stats",
49
+ "memory_list", "memory_stats", "space_list",
50
+ ];
51
+
52
+ const WRITE_TOOLS: readonly TN[] = [
53
+ "memory_store", "memory_update", "memory_ingest", "memory_delete",
54
+ "space_create", "space_add_member", "memory_share", "memory_pull",
55
+ "memory_reshare", "memory_toggle",
56
+ ];
57
+
58
+ const A: Record<TN, any> = {
59
+ memory_store: { content: "c", source: "s", tags: ["t1"] },
60
+ memory_search: { query: "q" },
61
+ memory_get: { id: "m1" },
62
+ memory_update: { id: "m1", content: "up" },
63
+ memory_profile: {},
64
+ memory_profile_stats: {},
65
+ memory_list: {},
66
+ memory_ingest: { messages: [{ role: "user", content: "hi" }] },
67
+ memory_stats: {},
68
+ memory_delete: { id: "m1" },
69
+ space_create: { name: "sp", space_type: "team" },
70
+ space_list: {},
71
+ space_add_member: { space_id: "s1", user_id: "u1", role: "admin" },
72
+ memory_share: { memory_id: "m1", target_space: "s1" },
73
+ memory_pull: { memory_id: "m1", source_space: "s1" },
74
+ memory_reshare: { memory_id: "m1" },
75
+ memory_toggle: { state: "on" },
76
+ };
77
+
78
+ // ─── Helpers ───────────────────────────────────────────────────
79
+
80
+ function mkClient() {
81
+ return {
82
+ createMemory: vi.fn().mockResolvedValue({ id: "m-new", tags: ["t1"] }),
83
+ searchMemories: vi.fn().mockResolvedValue([{ memory: { id: "m1", content: "hello world" }, score: 0.9 }]),
84
+ getMemory: vi.fn().mockResolvedValue({ id: "m1", content: "full content", tags: ["t"] }),
85
+ updateMemory: vi.fn().mockResolvedValue({ id: "m1" }),
86
+ getProfile: vi.fn().mockResolvedValue([{ slot: "lang", value: "ts" }]),
87
+ getProfileStats: vi.fn().mockResolvedValue({ total: 5 }),
88
+ listRecent: vi.fn().mockResolvedValue([{ id: "m1", content: "recent", category: "cases", tags: ["t"] }]),
89
+ ingestMessages: vi.fn().mockResolvedValue({ created: 1 }),
90
+ getStats: vi.fn().mockResolvedValue({ total: 100 }),
91
+ deleteMemory: vi.fn().mockResolvedValue(undefined),
92
+ createSpace: vi.fn().mockResolvedValue({ id: "s-new", name: "sp" }),
93
+ listSpaces: vi.fn().mockResolvedValue([{ id: "s1" }]),
94
+ addSpaceMember: vi.fn().mockResolvedValue({ ok: true }),
95
+ shareMemory: vi.fn().mockResolvedValue({ ok: true }),
96
+ pullMemory: vi.fn().mockResolvedValue({ ok: true }),
97
+ reshareMemory: vi.fn().mockResolvedValue({ ok: true }),
98
+ };
99
+ }
100
+
101
+ function mkCtx(p?: Partial<ToolContext>): ToolContext {
102
+ return {
103
+ agentId: "test-agent",
104
+ getSessionId: () => "sess-1",
105
+ getAgentName: () => "test-agent",
106
+ getProjectPath: () => "/test",
107
+ config: {},
108
+ ...p,
109
+ };
110
+ }
111
+
112
+ /** Parse the JSON string returned by execute() */
113
+ async function exec(tools: ReturnType<typeof buildTools>, name: TN, args?: any) {
114
+ return JSON.parse(await tools[name].execute(args ?? A[name]));
115
+ }
116
+
117
+ // ─── Tests ─────────────────────────────────────────────────────
118
+
119
+ describe("buildTools", () => {
120
+ let client: ReturnType<typeof mkClient>;
121
+ let tools: ReturnType<typeof buildTools>;
122
+
123
+ beforeEach(() => {
124
+ vi.clearAllMocks();
125
+ mockResolve.mockReturnValue("readwrite");
126
+ client = mkClient();
127
+ tools = buildTools(client, ["user:x", "project:y"], mkCtx());
128
+ });
129
+
130
+ // ────────────────── Definitions ──────────────────
131
+
132
+ describe("tool definitions", () => {
133
+ it("returns all defined tools with description + execute", () => {
134
+ expect(Object.keys(tools)).toHaveLength(ALL_TOOLS.length);
135
+ for (const name of ALL_TOOLS) {
136
+ expect(typeof tools[name].description).toBe("string");
137
+ expect(typeof tools[name].execute).toBe("function");
138
+ }
139
+ });
140
+ });
141
+
142
+ // ────────────────── Backward compat ──────────────────
143
+
144
+ describe("backward compat: no agentId / no config → allow all", () => {
145
+ it("allows WRITE when agentId is undefined", async () => {
146
+ tools = buildTools(client, [], mkCtx({ agentId: undefined, getAgentName: () => undefined }));
147
+ expect((await exec(tools, "memory_store")).ok).toBe(true);
148
+ });
149
+
150
+ it("allows READ when config is undefined", async () => {
151
+ tools = buildTools(client, [], mkCtx({ config: undefined }));
152
+ expect((await exec(tools, "memory_search")).ok).toBe(true);
153
+ });
154
+ });
155
+
156
+ // ────────────────── Permission: none ──────────────────
157
+
158
+ describe("policy 'none' → deny all 16 tools", () => {
159
+ beforeEach(() => { mockResolve.mockReturnValue("none"); });
160
+
161
+ it.each([...READ_TOOLS, ...WRITE_TOOLS] as unknown as string[])(
162
+ "denies %s", async (name: string) => {
163
+ const r = await exec(tools, name as TN);
164
+ expect(r).toMatchObject({ ok: false });
165
+ expect(r.error).toContain("Permission denied");
166
+ expect(r.error).toContain("'none'");
167
+ },
168
+ );
169
+ });
170
+
171
+ // ────────────────── Permission: readonly ──────────────────
172
+
173
+ describe("policy 'readonly'", () => {
174
+ beforeEach(() => { mockResolve.mockReturnValue("readonly"); });
175
+
176
+ it.each(READ_TOOLS as unknown as string[])("allows READ %s", async (name) => {
177
+ expect((await exec(tools, name as TN)).ok).toBe(true);
178
+ });
179
+
180
+ it.each(WRITE_TOOLS as unknown as string[])("denies WRITE %s", async (name) => {
181
+ const r = await exec(tools, name as TN);
182
+ expect(r).toMatchObject({ ok: false });
183
+ expect(r.error).toContain("Permission denied");
184
+ expect(r.error).toContain("'readonly'");
185
+ expect(r.error).toContain("'write'");
186
+ });
187
+ });
188
+
189
+ // ────────────────── Permission: readwrite ──────────────────
190
+
191
+ describe("policy 'readwrite' → allow all", () => {
192
+ it.each(ALL_TOOLS as unknown as string[])("allows %s", async (name) => {
193
+ expect((await exec(tools, name as TN)).ok).toBe(true);
194
+ });
195
+ });
196
+
197
+ // ────────────────── memory_store ──────────────────
198
+
199
+ describe("memory_store", () => {
200
+ it("success: returns id and merged tags", async () => {
201
+ const r = await exec(tools, "memory_store");
202
+ expect(r).toMatchObject({ ok: true, id: "m-new" });
203
+ expect(client.createMemory).toHaveBeenCalledWith(
204
+ "c", ["user:x", "project:y", "t1"], "s", "project",
205
+ "test-agent", "sess-1", undefined, undefined, "/test",
206
+ );
207
+ });
208
+
209
+ it("failure: returns error when client returns null", async () => {
210
+ client.createMemory.mockResolvedValue(null);
211
+ const r = await exec(tools, "memory_store");
212
+ expect(r).toMatchObject({ ok: false });
213
+ expect(r.error).toContain("unavailable");
214
+ });
215
+
216
+ it("passes scope, visibility, category when provided", async () => {
217
+ await tools.memory_store.execute({
218
+ content: "c", source: "s", tags: [], scope: "global",
219
+ visibility: "private", category: "preferences",
220
+ });
221
+ expect(client.createMemory).toHaveBeenCalledWith(
222
+ "c", ["user:x", "project:y"], "s", "global",
223
+ "test-agent", "sess-1", "private", "preferences", "/test",
224
+ );
225
+ });
226
+ });
227
+
228
+ // ────────────────── memory_search ──────────────────
229
+
230
+ describe("memory_search", () => {
231
+ it("success: returns results with truncated content", async () => {
232
+ const r = await exec(tools, "memory_search");
233
+ expect(r).toMatchObject({ ok: true, count: 1 });
234
+ expect(r.results[0]).toMatchObject({ id: "m1", score: 0.9 });
235
+ });
236
+
237
+ it("empty: returns ok with count 0", async () => {
238
+ client.searchMemories.mockResolvedValue([]);
239
+ const r = await exec(tools, "memory_search");
240
+ expect(r).toMatchObject({ ok: true, count: 0, results: [] });
241
+ });
242
+
243
+ it("passes limit and scope to client", async () => {
244
+ await tools.memory_search.execute({ query: "q", limit: 5, scope: "global" });
245
+ expect(client.searchMemories).toHaveBeenCalledWith("q", 5, "global", ["user:x", "project:y"], "/test");
246
+ });
247
+ });
248
+
249
+ // ────────────────── memory_get ──────────────────
250
+
251
+ describe("memory_get", () => {
252
+ it("success: returns full memory object", async () => {
253
+ const r = await exec(tools, "memory_get");
254
+ expect(r).toMatchObject({ ok: true, memory: { id: "m1" } });
255
+ });
256
+
257
+ it("not found: returns error when null", async () => {
258
+ client.getMemory.mockResolvedValue(null);
259
+ const r = await exec(tools, "memory_get");
260
+ expect(r).toMatchObject({ ok: false, error: "not found" });
261
+ });
262
+ });
263
+
264
+ // ────────────────── memory_update ──────────────────
265
+
266
+ describe("memory_update", () => {
267
+ it("success: returns ok with id", async () => {
268
+ const r = await exec(tools, "memory_update");
269
+ expect(r).toMatchObject({ ok: true, id: "m1" });
270
+ });
271
+
272
+ it("failure: returns error when client returns null", async () => {
273
+ client.updateMemory.mockResolvedValue(null);
274
+ const r = await exec(tools, "memory_update");
275
+ expect(r).toMatchObject({ ok: false });
276
+ expect(r.error).toContain("Failed to update");
277
+ });
278
+ });
279
+
280
+ // ────────────────── memory_profile ──────────────────
281
+
282
+ describe("memory_profile", () => {
283
+ it("success: returns preferences", async () => {
284
+ const r = await exec(tools, "memory_profile");
285
+ expect(r).toMatchObject({ ok: true, count: 1 });
286
+ });
287
+
288
+ it("empty: returns ok with empty array", async () => {
289
+ client.getProfile.mockResolvedValue([]);
290
+ const r = await exec(tools, "memory_profile");
291
+ expect(r).toMatchObject({ ok: true, count: 0, preferences: [] });
292
+ });
293
+ });
294
+
295
+ // ────────────────── memory_profile_stats ──────────────────
296
+
297
+ describe("memory_profile_stats", () => {
298
+ it("success: returns stats object", async () => {
299
+ const r = await exec(tools, "memory_profile_stats");
300
+ expect(r).toMatchObject({ ok: true, stats: { total: 5 } });
301
+ });
302
+ });
303
+
304
+ // ────────────────── memory_list ──────────────────
305
+
306
+ describe("memory_list", () => {
307
+ it("success: returns memories with truncated content", async () => {
308
+ const r = await exec(tools, "memory_list");
309
+ expect(r).toMatchObject({ ok: true, count: 1 });
310
+ expect(r.memories[0].id).toBe("m1");
311
+ });
312
+
313
+ it("empty: returns ok with empty array", async () => {
314
+ client.listRecent.mockResolvedValue([]);
315
+ const r = await exec(tools, "memory_list");
316
+ expect(r).toMatchObject({ ok: true, count: 0, memories: [] });
317
+ });
318
+
319
+ it("passes limit to client", async () => {
320
+ await tools.memory_list.execute({ limit: 5 });
321
+ expect(client.listRecent).toHaveBeenCalledWith(5);
322
+ });
323
+ });
324
+
325
+ // ────────────────── memory_ingest ──────────────────
326
+
327
+ describe("memory_ingest", () => {
328
+ it("success: returns ingestion result", async () => {
329
+ const r = await exec(tools, "memory_ingest");
330
+ expect(r).toMatchObject({ ok: true, result: { created: 1 } });
331
+ });
332
+
333
+ it("failure: returns error when client returns null", async () => {
334
+ client.ingestMessages.mockResolvedValue(null);
335
+ const r = await exec(tools, "memory_ingest");
336
+ expect(r).toMatchObject({ ok: false, error: "Ingestion failed" });
337
+ });
338
+ });
339
+
340
+ // ────────────────── memory_stats ──────────────────
341
+
342
+ describe("memory_stats", () => {
343
+ it("success: returns stats", async () => {
344
+ const r = await exec(tools, "memory_stats");
345
+ expect(r).toMatchObject({ ok: true, stats: { total: 100 } });
346
+ });
347
+
348
+ it("failure: returns error when client returns null", async () => {
349
+ client.getStats.mockResolvedValue(null);
350
+ const r = await exec(tools, "memory_stats");
351
+ expect(r).toMatchObject({ ok: false });
352
+ expect(r.error).toContain("Failed to get stats");
353
+ });
354
+ });
355
+
356
+ // ────────────────── memory_delete ──────────────────
357
+
358
+ describe("memory_delete", () => {
359
+ it("success: returns ok with id", async () => {
360
+ const r = await exec(tools, "memory_delete");
361
+ expect(r).toMatchObject({ ok: true, id: "m1" });
362
+ });
363
+
364
+ it("failure: catches error and returns failure", async () => {
365
+ client.deleteMemory.mockRejectedValue(new Error("boom"));
366
+ const r = await exec(tools, "memory_delete");
367
+ expect(r).toMatchObject({ ok: false });
368
+ expect(r.error).toContain("Failed to delete");
369
+ });
370
+ });
371
+
372
+ // ────────────────── space_create ──────────────────
373
+
374
+ describe("space_create", () => {
375
+ it("success: returns space object", async () => {
376
+ const r = await exec(tools, "space_create");
377
+ expect(r).toMatchObject({ ok: true, space: { id: "s-new" } });
378
+ });
379
+
380
+ it("failure: returns error when null", async () => {
381
+ client.createSpace.mockResolvedValue(null);
382
+ const r = await exec(tools, "space_create");
383
+ expect(r).toMatchObject({ ok: false, error: "Failed to create space" });
384
+ });
385
+ });
386
+
387
+ // ────────────────── space_list ──────────────────
388
+
389
+ describe("space_list", () => {
390
+ it("success: returns spaces array", async () => {
391
+ const r = await exec(tools, "space_list");
392
+ expect(r).toMatchObject({ ok: true, spaces: [{ id: "s1" }] });
393
+ });
394
+ });
395
+
396
+ // ────────────────── space_add_member ──────────────────
397
+
398
+ describe("space_add_member", () => {
399
+ it("success: returns result", async () => {
400
+ const r = await exec(tools, "space_add_member");
401
+ expect(r).toMatchObject({ ok: true, result: { ok: true } });
402
+ });
403
+
404
+ it("failure: returns error when null", async () => {
405
+ client.addSpaceMember.mockResolvedValue(null);
406
+ const r = await exec(tools, "space_add_member");
407
+ expect(r).toMatchObject({ ok: false, error: "Failed to add member" });
408
+ });
409
+ });
410
+
411
+ // ────────────────── memory_share ──────────────────
412
+
413
+ describe("memory_share", () => {
414
+ it("success: returns result", async () => {
415
+ const r = await exec(tools, "memory_share");
416
+ expect(r).toMatchObject({ ok: true, result: { ok: true } });
417
+ });
418
+
419
+ it("failure: returns error when null", async () => {
420
+ client.shareMemory.mockResolvedValue(null);
421
+ const r = await exec(tools, "memory_share");
422
+ expect(r).toMatchObject({ ok: false, error: "Failed to share memory" });
423
+ });
424
+ });
425
+
426
+ // ────────────────── memory_pull ──────────────────
427
+
428
+ describe("memory_pull", () => {
429
+ it("success: returns result", async () => {
430
+ const r = await exec(tools, "memory_pull");
431
+ expect(r).toMatchObject({ ok: true, result: { ok: true } });
432
+ });
433
+
434
+ it("failure: returns error when null", async () => {
435
+ client.pullMemory.mockResolvedValue(null);
436
+ const r = await exec(tools, "memory_pull");
437
+ expect(r).toMatchObject({ ok: false, error: "Failed to pull memory" });
438
+ });
439
+ });
440
+
441
+ // ────────────────── memory_reshare ──────────────────
442
+
443
+ describe("memory_reshare", () => {
444
+ it("success: returns result", async () => {
445
+ const r = await exec(tools, "memory_reshare");
446
+ expect(r).toMatchObject({ ok: true, result: { ok: true } });
447
+ });
448
+
449
+ it("failure: returns error when null", async () => {
450
+ client.reshareMemory.mockResolvedValue(null);
451
+ const r = await exec(tools, "memory_reshare");
452
+ expect(r).toMatchObject({ ok: false, error: "Failed to reshare memory" });
453
+ });
454
+ });
455
+
456
+ // ────────────────── memory_toggle ──────────────────
457
+
458
+ describe("memory_toggle", () => {
459
+ it("state='on': sets auto-store ON", async () => {
460
+ const r = await exec(tools, "memory_toggle", { state: "on" });
461
+ expect(r).toMatchObject({ ok: true, auto_store: true });
462
+ expect(mockSetEnabled).toHaveBeenCalledWith("sess-1", true);
463
+ });
464
+
465
+ it("state='off': sets auto-store OFF", async () => {
466
+ const r = await exec(tools, "memory_toggle", { state: "off" });
467
+ expect(r).toMatchObject({ ok: true, auto_store: false });
468
+ expect(mockSetEnabled).toHaveBeenCalledWith("sess-1", false);
469
+ });
470
+
471
+ it("state='ON' (uppercase): still recognized as on", async () => {
472
+ const r = await exec(tools, "memory_toggle", { state: "ON" });
473
+ expect(r).toMatchObject({ ok: true, auto_store: true });
474
+ });
475
+
476
+ it("no state: queries current status (true)", async () => {
477
+ mockIsEnabled.mockReturnValue(true);
478
+ const r = await exec(tools, "memory_toggle", {});
479
+ expect(r).toMatchObject({ ok: true, auto_store: true });
480
+ expect(mockIsEnabled).toHaveBeenCalledWith("sess-1");
481
+ });
482
+
483
+ it("no state: queries current status (false)", async () => {
484
+ mockIsEnabled.mockReturnValue(false);
485
+ const r = await exec(tools, "memory_toggle", {});
486
+ expect(r).toMatchObject({ ok: true, auto_store: false });
487
+ });
488
+
489
+ it("returns error when no session", async () => {
490
+ tools = buildTools(client, [], mkCtx({ getSessionId: () => undefined }));
491
+ const r = await exec(tools, "memory_toggle", { state: "on" });
492
+ expect(r).toMatchObject({ ok: false });
493
+ expect(r.error).toContain("No active session");
494
+ });
495
+ });
496
+
497
+ // ────────────────── JSON output format ──────────────────
498
+
499
+ describe("JSON output format", () => {
500
+ it("all tools return valid JSON with ok boolean", async () => {
501
+ for (const name of ALL_TOOLS) {
502
+ const raw = await tools[name].execute(A[name]);
503
+ const parsed = JSON.parse(raw);
504
+ expect(typeof parsed.ok).toBe("boolean");
505
+ }
506
+ });
507
+ });
508
+ });
package/src/tools.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
2
  import type { CerebroClient } from "./client.js";
3
+ import { type CerebroPluginConfig, resolveAgentPolicy } from "./config.js";
3
4
  import { isAutoStoreEnabled, setAutoStoreEnabled } from "./index.js";
4
5
 
5
6
  export interface ToolContext {
@@ -7,9 +8,25 @@ export interface ToolContext {
7
8
  getSessionId: () => string | undefined;
8
9
  getAgentName?: () => string;
9
10
  getProjectPath?: () => string | undefined;
11
+ config?: Partial<CerebroPluginConfig>;
10
12
  }
11
13
 
12
14
  export function buildTools(client: CerebroClient, containerTags: string[], context: ToolContext) {
15
+ function checkPermission(required: "read" | "write"): boolean {
16
+ const agentId = context.getAgentName?.() || context.agentId;
17
+ if (!agentId || !context.config) return true; // no policy configured → allow
18
+ const policy = resolveAgentPolicy(agentId, context.config);
19
+ if (policy === "none") return false;
20
+ if (policy === "readonly") return required === "read";
21
+ return true; // readwrite
22
+ }
23
+
24
+ function denyMessage(required: "read" | "write"): string {
25
+ const agentId = context.getAgentName?.() || context.agentId || "unknown";
26
+ const policy = (agentId && context.config) ? resolveAgentPolicy(agentId, context.config) : "readwrite";
27
+ return `Permission denied: agent '${agentId}' has '${policy}' policy, but this operation requires '${required}' access`;
28
+ }
29
+
13
30
  return {
14
31
  memory_store: tool({
15
32
  description:
@@ -70,6 +87,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
70
87
  ),
71
88
  },
72
89
  async execute(args) {
90
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
73
91
  const allTags = [...containerTags, ...(args.tags ?? [])];
74
92
  const effectiveAgentId = context.getAgentName?.() || context.agentId;
75
93
  const result = await client.createMemory(
@@ -107,6 +125,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
107
125
  .describe("Optional scope filter"),
108
126
  },
109
127
  async execute(args) {
128
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
110
129
  const results = await client.searchMemories(
111
130
  args.query,
112
131
  args.limit ?? 10,
@@ -134,6 +153,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
134
153
  id: tool.schema.string().describe("Memory ID"),
135
154
  },
136
155
  async execute(args) {
156
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
137
157
  const memory = await client.getMemory(args.id);
138
158
  if (!memory) return JSON.stringify({ ok: false, error: "not found" });
139
159
  return JSON.stringify({ ok: true, memory });
@@ -153,6 +173,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
153
173
  .describe("Replacement tags"),
154
174
  },
155
175
  async execute(args) {
176
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
156
177
  const result = await client.updateMemory(
157
178
  args.id,
158
179
  args.content,
@@ -168,6 +189,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
168
189
  "Get the user profile synthesized from stored memories. Shows preferences, patterns, and key information.",
169
190
  args: {},
170
191
  async execute() {
192
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
171
193
  const preferences = await client.getProfile();
172
194
  if (preferences.length === 0) return JSON.stringify({ ok: true, count: 0, preferences: [] });
173
195
  return JSON.stringify({ ok: true, count: preferences.length, preferences });
@@ -179,6 +201,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
179
201
  "View user profile statistics — total preferences, slot distribution, induction run counts, etc.",
180
202
  args: {},
181
203
  async execute() {
204
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
182
205
  const stats = await client.getProfileStats();
183
206
  return JSON.stringify({ ok: true, stats });
184
207
  },
@@ -194,6 +217,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
194
217
  .describe("Max memories to return (default: 20)"),
195
218
  },
196
219
  async execute(args) {
220
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
197
221
  const memories = await client.listRecent(args.limit ?? 20);
198
222
  if (memories.length === 0) return JSON.stringify({ ok: true, count: 0, memories: [] });
199
223
  const items = memories.map((m) => ({
@@ -234,6 +258,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
234
258
  .describe("Session ID to associate with the ingestion"),
235
259
  },
236
260
  async execute(args) {
261
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
237
262
  const effectiveAgentId = context.getAgentName?.() || context.agentId;
238
263
  const result = await client.ingestMessages(args.messages, {
239
264
  mode: args.mode ?? "smart",
@@ -252,6 +277,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
252
277
  "Get statistics about stored memories — counts by category, type, tier, and timeline.",
253
278
  args: {},
254
279
  async execute() {
280
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
255
281
  const stats = await client.getStats();
256
282
  if (!stats) return JSON.stringify({ ok: false, error: "Failed to get stats" });
257
283
  return JSON.stringify({ ok: true, stats });
@@ -265,6 +291,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
265
291
  id: tool.schema.string().describe("Memory ID to delete"),
266
292
  },
267
293
  async execute(args) {
294
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
268
295
  try {
269
296
  await client.deleteMemory(args.id);
270
297
  return JSON.stringify({ ok: true, id: args.id });
@@ -293,6 +320,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
293
320
  .describe("Initial members to add"),
294
321
  },
295
322
  async execute(args) {
323
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
296
324
  const result = await client.createSpace(
297
325
  args.name,
298
326
  args.space_type,
@@ -308,6 +336,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
308
336
  "List all spaces you own or are a member of.",
309
337
  args: {},
310
338
  async execute() {
339
+ if (!checkPermission("read")) return JSON.stringify({ ok: false, error: denyMessage("read") });
311
340
  const spaces = await client.listSpaces();
312
341
  return JSON.stringify({ ok: true, spaces });
313
342
  },
@@ -322,6 +351,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
322
351
  role: tool.schema.string().describe("Role: admin, member, or reader"),
323
352
  },
324
353
  async execute(args) {
354
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
325
355
  const result = await client.addSpaceMember(
326
356
  args.space_id,
327
357
  args.user_id,
@@ -340,6 +370,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
340
370
  target_space: tool.schema.string().describe("Target space ID"),
341
371
  },
342
372
  async execute(args) {
373
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
343
374
  const result = await client.shareMemory(
344
375
  args.memory_id,
345
376
  args.target_space,
@@ -361,6 +392,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
361
392
  .describe("Visibility of the pulled copy"),
362
393
  },
363
394
  async execute(args) {
395
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
364
396
  const result = await client.pullMemory(
365
397
  args.memory_id,
366
398
  args.source_space,
@@ -382,6 +414,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
382
414
  .describe("Target space containing the copy (optional)"),
383
415
  },
384
416
  async execute(args) {
417
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
385
418
  const result = await client.reshareMemory(
386
419
  args.memory_id,
387
420
  args.target_space,
@@ -401,6 +434,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
401
434
  .describe("Set to 'on' or 'off'. Omit to check current status."),
402
435
  },
403
436
  async execute(args) {
437
+ if (!checkPermission("write")) return JSON.stringify({ ok: false, error: denyMessage("write") });
404
438
  const sessionId = context.getSessionId();
405
439
  if (!sessionId) return JSON.stringify({ ok: false, error: "No active session" });
406
440