Skip to content

Commit 41e06dc

Browse files
committed
security hardening + full tool parity across MCP, REST, and OpenAPI
Security (20-point audit): - Dockerfile: run as non-root appuser - Fastify: global error handler, sanitize 5xx responses - Fastify: HSTS, X-Content-Type-Options, X-Frame-Options headers - MCP: sanitize OAuth error responses, strip /health diagnostics - MCP: /oauth/status now requires auth - OAuth: validate redirect_uri against registered URIs - JWT: enforce 32-char minimum on RM_DASHBOARD_JWT_SECRET - npm: fix all known vulnerabilities (0 remaining both repos) Tool parity (MCP + REST + OpenAPI): - MCP: add update_memory and delete_memory tools (was 9, now 11) - REST: add PUT/DELETE /agent/memories/:id with origin-ownership guard - REST: add POST /agent/memories/search (full-content text search) - REST: add POST /agent/memories/list (bulk read with content) - REST: add GET /agent/team/memories and POST /agent/team/share - OpenAPI: bump to v3.0.0 with all 13 operations for ChatGPT - Zod/JSON Schema validation limits matched across all surfaces Docs: JWT secret minimum in README, .env.example, docker-compose.yml Made-with: Cursor
1 parent 2b806e2 commit 41e06dc

11 files changed

Lines changed: 662 additions & 80 deletions

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ RM_MODEL_API_KEY=sk-your-openai-api-key
3333
# RM_AGENT_KEY_GEMINI=...
3434
# RM_AGENT_KEY_N8N=...
3535

36+
# Optional — dashboard multi-user auth
37+
# RM_DASHBOARD_SERVICE_KEY=... # Shared with dashboard. Generate: openssl rand -hex 32
38+
# RM_DASHBOARD_JWT_SECRET=... # Must match dashboard AUTH_SECRET. Minimum 32 characters.
39+
# RM_OWNER_EMAIL=you@example.com # Owner email for admin access and bootstrap
40+
3641
# Optional — public URL for OAuth discovery (required for Claude native connector)
3742
# RM_PUBLIC_URL=https://api.reflectmemory.com
3843

Dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ COPY --from=deps /app/node_modules ./node_modules
2121
COPY --from=build /app/dist ./dist
2222
COPY --from=build /app/schema.sql ./schema.sql
2323
COPY --from=build /app/openapi-agent.yaml ./openapi-agent.yaml
24-
RUN mkdir -p /data
24+
RUN mkdir -p /data && \
25+
addgroup --system appgroup && \
26+
adduser --system --ingroup appgroup appuser && \
27+
chown -R appuser:appgroup /app /data
28+
USER appuser
2529
EXPOSE 3000
2630
CMD ["node", "dist/index.js"]

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Dashboard multi-user auth (required for dashboard deployment):
5151

5252
```bash
5353
export RM_DASHBOARD_SERVICE_KEY="..." # Shared with dashboard. Generate: openssl rand -hex 32
54-
export RM_DASHBOARD_JWT_SECRET="..." # Must match dashboard AUTH_SECRET. Same value for JWT verification.
54+
export RM_DASHBOARD_JWT_SECRET="..." # Must match dashboard AUTH_SECRET. Minimum 32 characters.
5555
```
5656

5757
Multi-vendor chat (dashboard Chat tab -- enables GPT, Claude, Gemini, Perplexity, Grok):
@@ -320,6 +320,8 @@ Team tools (`read_team_memories`, `share_memory`) are available in all MCP clien
320320

321321
Run Reflect Memory locally with Docker Compose. Data stays on your machine.
322322

323+
> **Upgrading?** The container runs as a non-root user. If you have an existing `/data` volume with root-owned files, run `docker compose down && docker compose --profile isolated-hosted up --build` to rebuild. If the database fails to open, fix volume permissions: `docker run --rm -v rm_data_isolated:/data node:20-bookworm-slim chown -R 65534:65534 /data`
324+
323325
1. Clone the repo and create a `.env` file:
324326

325327
```bash

docker-compose.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ x-common-env: &common-env
99
RM_MODEL_BASE_URL: ${RM_MODEL_BASE_URL:-https://api.openai.com/v1}
1010
RM_CHAT_OPENAI_BASE_URL: ${RM_CHAT_OPENAI_BASE_URL:-https://api.openai.com/v1}
1111
RM_PUBLIC_URL: ${RM_PUBLIC_URL:-}
12-
RM_DASHBOARD_SERVICE_KEY: ${RM_DASHBOARD_SERVICE_KEY:-}
13-
RM_DASHBOARD_JWT_SECRET: ${RM_DASHBOARD_JWT_SECRET:-}
14-
RM_OWNER_EMAIL: ${RM_OWNER_EMAIL:-}
12+
RM_DASHBOARD_SERVICE_KEY: ${RM_DASHBOARD_SERVICE_KEY:-} # Generate: openssl rand -hex 32
13+
RM_DASHBOARD_JWT_SECRET: ${RM_DASHBOARD_JWT_SECRET:-} # Min 32 chars; must match dashboard AUTH_SECRET
14+
RM_OWNER_EMAIL: ${RM_OWNER_EMAIL:-} # Owner email for admin access
1515
RM_AGENT_KEY_CLAUDE: ${RM_AGENT_KEY_CLAUDE:-}
1616
RM_AGENT_KEY_CHATGPT: ${RM_AGENT_KEY_CHATGPT:-}
1717
RM_AGENT_KEY_CURSOR: ${RM_AGENT_KEY_CURSOR:-}

openapi-agent.yaml

Lines changed: 300 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
openapi: 3.1.0
22
info:
33
title: Reflect Memory Agent API
4-
version: 2.1.0
4+
version: 3.0.0
55
description: >
6-
Cross-agent memory system. Write, browse, retrieve, and query structured memories.
7-
Use /agent/memories/browse to discover available memories (lightweight, no content).
8-
Use /agent/memories/{id} for surgical full-body retrieval by ID.
9-
Use /agent/memories/by-tag for full-body retrieval by topic.
10-
Use /agent/memories/latest for the single most recent memory (strict chronological by created_at).
6+
Cross-agent memory system. Write, read, update, delete, search, browse, and query structured memories.
7+
Use updateMemory to edit an existing memory (preserves version history).
8+
Use deleteMemory to soft-delete (moves to trash, recoverable from dashboard).
9+
Use searchMemories for full-text search with full content returned.
10+
Use readMemories for bulk recent memories with full content.
11+
Use browseMemories to discover what exists (summaries only, no content).
12+
Use getMemoryById for surgical full-body retrieval by UUID.
13+
Use getMemoriesByTag for full-body retrieval by topic.
14+
Use getLatestMemory for the single most recent memory (strict chronological).
15+
Use readTeamMemories and shareMemory for team knowledge sharing.
1116
Use /query with specific filters to get AI responses grounded in relevant context.
12-
IMPORTANT: For "most recent memory chronologically" always use getLatestMemory -- never /query.
17+
IMPORTANT: For "most recent memory chronologically" always use getLatestMemory, not /query.
18+
IMPORTANT: To edit a memory, use updateMemory with the memory ID, not writeMemory.
1319
servers:
1420
- url: https://api.reflectmemory.com
1521
paths:
@@ -155,6 +161,293 @@ paths:
155161
updated_at: { type: string }
156162
"404":
157163
description: Memory not found or not accessible
164+
put:
165+
operationId: updateMemory
166+
x-openai-isConsequential: true
167+
summary: Update an existing memory by ID (full replacement)
168+
description: >
169+
Edit a memory you previously wrote. Replaces title, content, tags, and allowed_vendors.
170+
Preserves version history (old content is saved before overwriting).
171+
Use this to correct, refine, or append to a memory instead of writing duplicates.
172+
You can only update memories you created (origin must match your vendor).
173+
Trashed memories cannot be updated.
174+
parameters:
175+
- name: id
176+
in: path
177+
required: true
178+
schema:
179+
type: string
180+
description: The memory UUID to update
181+
requestBody:
182+
required: true
183+
content:
184+
application/json:
185+
schema:
186+
type: object
187+
required:
188+
- title
189+
- content
190+
- tags
191+
- allowed_vendors
192+
properties:
193+
title:
194+
type: string
195+
maxLength: 500
196+
description: "Updated title"
197+
content:
198+
type: string
199+
maxLength: 100000
200+
description: "Updated content (full replacement, not a patch)"
201+
tags:
202+
type: array
203+
items:
204+
type: string
205+
maxItems: 50
206+
description: "Updated tags"
207+
allowed_vendors:
208+
type: array
209+
items:
210+
type: string
211+
minItems: 1
212+
maxItems: 50
213+
description: "Which vendors can see this. Use ['*'] for all."
214+
responses:
215+
"200":
216+
description: Updated memory
217+
content:
218+
application/json:
219+
schema:
220+
type: object
221+
properties:
222+
id: { type: string }
223+
title: { type: string }
224+
content: { type: string }
225+
tags: { type: array, items: { type: string } }
226+
origin: { type: string }
227+
allowed_vendors: { type: array, items: { type: string } }
228+
memory_type: { type: string }
229+
created_at: { type: string }
230+
updated_at: { type: string }
231+
"403":
232+
description: Cannot update a memory you did not create
233+
"404":
234+
description: Memory not found or deleted
235+
delete:
236+
operationId: deleteMemory
237+
x-openai-isConsequential: true
238+
summary: Soft-delete a memory by ID (moves to trash)
239+
description: >
240+
Delete a memory you previously wrote. Moves it to trash (recoverable from the dashboard).
241+
You can only delete memories you created (origin must match your vendor).
242+
Already-deleted memories return 404.
243+
parameters:
244+
- name: id
245+
in: path
246+
required: true
247+
schema:
248+
type: string
249+
description: The memory UUID to delete
250+
responses:
251+
"200":
252+
description: Memory deleted (moved to trash)
253+
content:
254+
application/json:
255+
schema:
256+
type: object
257+
properties:
258+
deleted: { type: boolean }
259+
id: { type: string }
260+
title: { type: string }
261+
"403":
262+
description: Cannot delete a memory you did not create
263+
"404":
264+
description: Memory not found or already deleted
265+
/agent/memories/search:
266+
post:
267+
operationId: searchMemories
268+
x-openai-isConsequential: false
269+
summary: Search memories by text (returns full content)
270+
description: >
271+
Full-text search across memory titles and content. Returns complete memory
272+
entries (with content) matching the search term. Use for finding specific
273+
information when you know keywords but not tags or IDs.
274+
requestBody:
275+
required: true
276+
content:
277+
application/json:
278+
schema:
279+
type: object
280+
required:
281+
- term
282+
properties:
283+
term:
284+
type: string
285+
minLength: 1
286+
maxLength: 500
287+
description: "Search term to match against title and content"
288+
limit:
289+
type: integer
290+
minimum: 1
291+
maximum: 50
292+
description: "Max memories to return. Default: 10."
293+
responses:
294+
"200":
295+
description: Matching memories with full content
296+
content:
297+
application/json:
298+
schema:
299+
type: object
300+
properties:
301+
memories:
302+
type: array
303+
items:
304+
type: object
305+
properties:
306+
id: { type: string }
307+
title: { type: string }
308+
content: { type: string }
309+
tags: { type: array, items: { type: string } }
310+
origin: { type: string }
311+
allowed_vendors: { type: array, items: { type: string } }
312+
memory_type: { type: string }
313+
created_at: { type: string }
314+
updated_at: { type: string }
315+
count:
316+
type: integer
317+
/agent/memories/list:
318+
post:
319+
operationId: readMemories
320+
x-openai-isConsequential: false
321+
summary: Read recent memories (full content, bulk)
322+
description: >
323+
Get the most recent memories with full content. Use limit to control how many.
324+
For discovering what exists without loading full content, use browseMemories instead.
325+
requestBody:
326+
required: true
327+
content:
328+
application/json:
329+
schema:
330+
type: object
331+
properties:
332+
limit:
333+
type: integer
334+
minimum: 1
335+
maximum: 50
336+
description: "Max memories to return. Default: 10."
337+
responses:
338+
"200":
339+
description: Recent memories with full content
340+
content:
341+
application/json:
342+
schema:
343+
type: object
344+
properties:
345+
memories:
346+
type: array
347+
items:
348+
type: object
349+
properties:
350+
id: { type: string }
351+
title: { type: string }
352+
content: { type: string }
353+
tags: { type: array, items: { type: string } }
354+
origin: { type: string }
355+
allowed_vendors: { type: array, items: { type: string } }
356+
memory_type: { type: string }
357+
created_at: { type: string }
358+
updated_at: { type: string }
359+
count:
360+
type: integer
361+
/agent/team/memories:
362+
get:
363+
operationId: readTeamMemories
364+
x-openai-isConsequential: false
365+
summary: Read team shared memories
366+
description: >
367+
Get memories shared with your team. Returns the team knowledge pool with
368+
author attribution. Only available if you belong to a team.
369+
parameters:
370+
- name: limit
371+
in: query
372+
required: false
373+
schema:
374+
type: integer
375+
minimum: 1
376+
maximum: 50
377+
description: "Max team memories to return. Default: 20."
378+
- name: offset
379+
in: query
380+
required: false
381+
schema:
382+
type: integer
383+
minimum: 0
384+
description: "Skip this many results for pagination. Default: 0."
385+
responses:
386+
"200":
387+
description: Team shared memories
388+
content:
389+
application/json:
390+
schema:
391+
type: object
392+
properties:
393+
team_memories:
394+
type: array
395+
items:
396+
type: object
397+
properties:
398+
id: { type: string }
399+
title: { type: string }
400+
content: { type: string }
401+
tags: { type: array, items: { type: string } }
402+
origin: { type: string }
403+
memory_type: { type: string }
404+
author: { type: string }
405+
shared_at: { type: string }
406+
created_at: { type: string }
407+
total:
408+
type: integer
409+
limit:
410+
type: integer
411+
offset:
412+
type: integer
413+
has_more:
414+
type: boolean
415+
"404":
416+
description: Not a member of any team
417+
/agent/team/share:
418+
post:
419+
operationId: shareMemory
420+
x-openai-isConsequential: true
421+
summary: Share a memory with your team
422+
description: >
423+
Share one of your personal memories with your team. The memory becomes
424+
visible to all team members via readTeamMemories. You must own the memory.
425+
Already-shared memories are handled gracefully (no error).
426+
requestBody:
427+
required: true
428+
content:
429+
application/json:
430+
schema:
431+
type: object
432+
required:
433+
- memory_id
434+
properties:
435+
memory_id:
436+
type: string
437+
description: "The UUID of your memory to share with the team"
438+
responses:
439+
"200":
440+
description: Memory shared with team
441+
content:
442+
application/json:
443+
schema:
444+
type: object
445+
properties:
446+
shared: { type: boolean }
447+
memory_id: { type: string }
448+
team_id: { type: string }
449+
"404":
450+
description: Not a team member or memory not found
158451
/agent/memories/by-tag:
159452
post:
160453
operationId: getMemoriesByTag

0 commit comments

Comments
 (0)