@hilbras/remembra 3.0.0 → 3.2.0

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.
package/dist/types.js CHANGED
@@ -1,12 +1,98 @@
1
1
  import { z } from "zod";
2
2
  /** The four memory types Remembra stores. */
3
3
  export const MemoryType = z.enum(["fact", "decision", "role", "history"]);
4
- export const StoreInput = z.object({
5
- type: MemoryType,
6
- content: z.string().min(1),
7
- scope: z.string().default("global"),
8
- tags: z.array(z.string()).default([]),
9
- importance: z.number().int().min(1).max(5).default(3),
10
- source: z.string().optional(),
4
+ /**
5
+ * Frontmatter schema version. Bump when the Memory format changes and
6
+ * add a migration step in store.ts (missing field in old files = v1).
7
+ */
8
+ export const SCHEMA_VERSION = 1;
9
+ /**
10
+ * Scope safety: reject `..` path segments so scope can never escape the
11
+ * storage root (P0 directory traversal fix). Absolute paths like
12
+ * `/home/user/project` are fine — path.join keeps them contained.
13
+ */
14
+ export function isSafeScope(scope) {
15
+ if (scope === "global")
16
+ return true;
17
+ const normalized = scope.replace(/[^a-zA-Z0-9._/-]/g, "_");
18
+ return !normalized.split("/").some((seg) => seg === "..");
19
+ }
20
+ // ---------------------------------------------------------------------------
21
+ // Shared input schemas (audit #13: one source of truth).
22
+ // The raw `*Shape` objects feed MCP tool inputSchemas (ZodRawShape);
23
+ // the parsed `*Input` objects validate on every transport.
24
+ // ---------------------------------------------------------------------------
25
+ export const storeInputShape = {
26
+ type: MemoryType.describe("fact | decision | role | history"),
27
+ content: z.string().min(1).describe("The memory itself, written as a standalone statement"),
28
+ scope: z
29
+ .string()
30
+ .default("global")
31
+ .describe("'global' for always-relevant memories, or a project path/id for project-scoped ones"),
32
+ tags: z.array(z.string()).default([]).describe("Keywords that boost retrieval"),
33
+ importance: z
34
+ .number()
35
+ .int()
36
+ .min(1)
37
+ .max(5)
38
+ .default(3)
39
+ .describe("1=minor, 5=critical (default 3)"),
40
+ source: z.string().optional().describe("Originating session or client"),
41
+ };
42
+ export const StoreInput = z
43
+ .object(storeInputShape)
44
+ .refine((v) => isSafeScope(v.scope), { message: "scope must not contain '..' path segments" });
45
+ export const digestInputShape = {
46
+ transcript: z
47
+ .string()
48
+ .min(1)
49
+ .describe("Conversation transcript or a detailed summary of the session"),
50
+ scope: z.string().optional().describe("Scope for extracted memories (default: global)"),
51
+ source: z.string().optional().describe("Originating session/client"),
52
+ };
53
+ export const DigestInput = z
54
+ .object(digestInputShape)
55
+ .refine((v) => v.scope === undefined || isSafeScope(v.scope), {
56
+ message: "scope must not contain '..' path segments",
57
+ });
58
+ export const searchInputShape = {
59
+ query: z.string().optional().describe("Keywords to match (omit to get a scope/recency-ranked list)"),
60
+ scope: z.string().optional().describe("Current project path or workspace id to filter by"),
61
+ type: MemoryType.optional(),
62
+ limit: z.number().int().min(1).max(50).optional(),
63
+ };
64
+ export const SearchInput = z.object(searchInputShape);
65
+ export const listInputShape = {
66
+ scope: z.string().optional(),
67
+ type: MemoryType.optional(),
68
+ includeArchived: z.boolean().optional().describe("Include archived memories (flagged)"),
69
+ };
70
+ export const ListInput = z.object(listInputShape);
71
+ export const forgetInputShape = {
72
+ id: z.string().describe("Memory id (from memory_store or memory_list)"),
73
+ };
74
+ export const ForgetInput = z.object(forgetInputShape);
75
+ /** Backup file envelope (remembra export / import). */
76
+ export const SNAPSHOT_FORMAT = "remembra-export";
77
+ export const SnapshotInput = z.object({
78
+ format: z.literal(SNAPSHOT_FORMAT),
79
+ version: z.number().int().positive(),
80
+ exportedAt: z.string(),
81
+ memories: z
82
+ .array(z.object({
83
+ id: z.string().regex(/^[a-f0-9]{8,32}$/, "invalid id"),
84
+ type: MemoryType,
85
+ content: z.string().min(1),
86
+ scope: z.string().refine(isSafeScope, { message: "scope must not contain '..'" }),
87
+ tags: z.array(z.string()),
88
+ importance: z.number().int().min(1).max(5),
89
+ createdAt: z.string(),
90
+ updatedAt: z.string(),
91
+ source: z.string().optional(),
92
+ lastSeen: z.string().optional(),
93
+ archivedAt: z.string().optional(),
94
+ embedding: z.array(z.number()).optional(),
95
+ }))
96
+ .max(100_000),
11
97
  });
12
98
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,6CAA6C;AAC7C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AA4B1E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC;IACjC,IAAI,EAAE,UAAU;IAChB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;IACnC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACrC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACrD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/docs/chatgpt.md CHANGED
@@ -19,7 +19,9 @@ curl http://localhost:8787/health
19
19
  # {"status": "ok"}
20
20
  ```
21
21
 
22
- - `REMEMBRA_API_KEY` **strongly recommended** — without it every endpoint is open.
22
+ - `REMEMBRA_API_KEY` **mandatory for public exposure** — without it the server
23
+ binds to `127.0.0.1` only, and a non-loopback `REMEMBRA_HOST` without a key
24
+ refuses to start (default-deny, see [security.md](security.md)).
23
25
  - The server binds to localhost by default. For ChatGPT to reach it you must expose it
24
26
  publicly (see [§4](#4-exposing-the-server)).
25
27
 
package/docs/clients.md CHANGED
@@ -90,6 +90,9 @@ REMEMBRA_API_KEY="your-secret" remembra --http
90
90
  | `REMEMBRA_EMBEDDINGS` | `none` | Semantic search: `openai` \| `ollama` \| `none` |
91
91
  | `REMEMBRA_ARCHIVE_AFTER_DAYS` | `90` | Unused active memory → archived |
92
92
  | `REMEMBRA_ARCHIVE_TTL_DAYS` | `365` | Archived memory → deleted |
93
+ | `REMEMBRA_HOST` | *(see security.md)* | HTTP bind address (loopback without key) |
94
+ | `REMEMBRA_MAX_BODY` | `10485760` | Max HTTP request body bytes |
95
+ | `REMEMBRA_DEBUG` | *(unset)* | `1` logs the storage root path at startup (off by default: log hygiene) |
93
96
 
94
97
  LLM/embedding key setup: see **[providers.md](providers.md)**.
95
98
 
@@ -0,0 +1,89 @@
1
+ # Security & Trust Model
2
+
3
+ Remembra v3.1.0 hardening notes — what's protected, what's a trust decision,
4
+ and how to deploy safely.
5
+
6
+ ## Trust model
7
+
8
+ ```
9
+ ┌─ Trusted ─────────────────────────────────────────────┐
10
+ │ You / your MCP clients (OpenCode, Claude Code, ...) │
11
+ │ The LLM you configure for digest/merge │
12
+ │ Files under REMEMBRA_HOME (local filesystem trust) │
13
+ └───────────────────────────────────────────────────────┘
14
+ ┌─ Semi-trusted ────────────────────────────────────────┐
15
+ │ Transcripts passed to memory_digest │
16
+ │ Anyone holding your REMEMBRA_API_KEY │
17
+ └───────────────────────────────────────────────────────┘
18
+ ┌─ Untrusted ───────────────────────────────────────────┐
19
+ │ Network peers (if HTTP is exposed) │
20
+ │ Memory content written by other users of shared AI │
21
+ └───────────────────────────────────────────────────────┘
22
+ ```
23
+
24
+ ### Role memories are instructions — the prompt-injection surface
25
+
26
+ `role` memories always surface (+1000) and are meant to be **followed**, not
27
+ merely recalled. That is the feature — and the risk:
28
+
29
+ - **Via MCP**: whoever can call `memory_store` in your session is already the
30
+ agent you're running. No additional boundary is crossed.
31
+ - **Via HTTP**: anyone with your API key can plant a global `role` that every
32
+ future session will receive as an instruction.
33
+
34
+ **Rules:**
35
+ 1. Never expose HTTP without `REMEMBRA_API_KEY` (enforced — see below).
36
+ 2. Treat the API key as root access to your assistant's standing instructions.
37
+ 3. Audit roles periodically: `memory_list { type: "role", includeArchived: true }`.
38
+ 4. If you ingest transcripts from other people (shared ChatGPT GPT, public bot),
39
+ remember the digest LLM can extract `role` items from *their* text — review
40
+ before trusting a multi-user deployment.
41
+
42
+ Retrieved memories should be treated as **data with provenance**, not commands
43
+ — but Remembra cannot enforce how the consuming model interprets them.
44
+
45
+ ## Enforced protections (3.1.0)
46
+
47
+ | Protection | Mechanism |
48
+ |------------|-----------|
49
+ | **Directory traversal (P0)** | Scopes containing `..` rejected by Zod (`StoreInput`/`DigestInput`) + `fileFor()` re-verifies the resolved path stays under `REMEMBRA_HOME` (defense in depth) |
50
+ | **Default-deny HTTP** | Without `REMEMBRA_API_KEY`: binds `127.0.0.1` only. Non-loopback `REMEMBRA_HOST` without a key → **refuses to start** |
51
+ | **Timing-safe auth** | API keys compared with `crypto.timingSafeEqual` |
52
+ | **Body size limit** | 10 MiB default (`REMEMBRA_MAX_BODY`), `413` on excess — pre-checks `Content-Length` and enforces while streaming |
53
+ | **Digest validation** | `DigestInput` Zod schema on both MCP and HTTP paths |
54
+ | **Atomic writes** | temp file + `rename()` (POSIX-atomic) — no half-written memories after a crash |
55
+ | **ID collisions** | 12-hex IDs (2⁴⁸) + existence check on store |
56
+ | **Content-Length** | Set on every response |
57
+
58
+ ## Deployment checklist
59
+
60
+ ```bash
61
+ # Local (safe default — loopback, key optional)
62
+ remembra --http
63
+
64
+ # Public (ChatGPT etc.) — key is mandatory
65
+ export REMEMBRA_API_KEY="$(openssl rand -hex 32)"
66
+ export REMEMBRA_HOST=0.0.0.0
67
+ remembra --http
68
+ # → put TLS in front (Caddy/nginx/cloudflared). Remembra speaks plain HTTP.
69
+ ```
70
+
71
+ - [ ] `REMEMBRA_API_KEY` set with a long random value (≥32 bytes)
72
+ - [ ] TLS terminator in front for any non-loopback exposure
73
+ - [ ] `REMEMBRA_HOME` lives on a filesystem you back up — use
74
+ `remembra export <file>.json` for portable snapshots (or `rsync`/git the
75
+ directory)
76
+ - [ ] Periodic `memory_list {type: "role"}` audit
77
+ - [ ] LLM/embedding keys scoped to least privilege
78
+
79
+ ## Known non-goals (current version)
80
+
81
+ - **No multi-tenancy** — one store per installation; scope isolates *projects*,
82
+ not *users*. Never share one instance between mutually untrusting users.
83
+ - **No encryption at rest** — files are plaintext markdown (by design: you can
84
+ read and edit them). Use filesystem-level encryption if needed.
85
+ - **No PII redaction** — what you store is what's written to disk.
86
+ - **No write locking / journal** — single-writer assumption; concurrent writers
87
+ from multiple machines are unsupported (atomic writes + the digest lock
88
+ protect against crashes and same-process races, not cross-machine
89
+ interleaving). `remembra export` for backups across machines.
package/docs/tools.md CHANGED
@@ -88,6 +88,17 @@ vectors. Roles never decay. Takes no arguments. See [lifecycle.md](lifecycle.md)
88
88
  Returns counts + affected ids. Also available as `POST /maintain` and the
89
89
  `remembra maintain` CLI command.
90
90
 
91
+ ## CLI commands
92
+
93
+ ```bash
94
+ remembra maintain # decay sweep + vector backfill (one-shot, prints JSON)
95
+ remembra export <file>.json # full backup snapshot incl. archived memories
96
+ remembra import <file>.json # restore; validates whole file first (atomic), idempotent
97
+ ```
98
+
99
+ Import skips existing ids and exact-duplicate contents, so running it twice —
100
+ or importing into a machine that already has the data — is always safe.
101
+
91
102
  ---
92
103
 
93
104
  ## Suggested session flow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hilbras/remembra",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "External memory for AI assistants — remember facts, decisions, roles and history across sessions. MCP server for OpenCode, Claude Code, Cline, Kimi Code and more.",
5
5
  "type": "module",
6
6
  "bin": {