@cerefox/memory 1.4.0 → 1.6.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.
@@ -0,0 +1,38 @@
1
+ -- 0022_rls_on_document_relations.sql — close the one-table RLS gap (iteration 36).
2
+ --
3
+ -- `cerefox_document_relations` was added in iteration 29 and never added to
4
+ -- schema.sql's RLS block, so it alone among the ten tables had row-level
5
+ -- security disabled. Cerefox's model is "RLS ON with NO policies" — the
6
+ -- service-role key bypasses RLS and everything else is denied — so a table
7
+ -- without RLS is reachable by any role holding a table grant.
8
+ --
9
+ -- On projects created before Supabase stopped granting `anon` blanket
10
+ -- privileges on `public` (the maintainer's production project is one), `anon`
11
+ -- holds SELECT/INSERT/UPDATE/DELETE here. The anon / publishable key is
12
+ -- designed to be public, so that means world read AND write on this table.
13
+ -- Supabase's advisor flagged it as `rls_disabled_in_public` on 2026-08-09.
14
+ --
15
+ -- Newer projects grant `anon` nothing, so they were never exposed — which is
16
+ -- why the maintainer's staging project showed no privileges while production
17
+ -- showed all four. Both get RLS regardless: relying on the absence of a grant
18
+ -- is not the same as denying access.
19
+ --
20
+ -- Impact of the gap: the relations feature is opt-in (`relations_enabled`,
21
+ -- default false), so the table is empty on a default install and no document
22
+ -- content was ever reachable through it. Content, chunks, versions, audit log
23
+ -- and config were correctly protected throughout.
24
+ --
25
+ -- Idempotent, and safe on a table that already has RLS.
26
+
27
+ ALTER TABLE cerefox_document_relations ENABLE ROW LEVEL SECURITY;
28
+
29
+ -- Defence in depth: revoke the legacy blanket grants so the table is denied by
30
+ -- privilege as well as by RLS. Harmless where the grants were never made.
31
+ REVOKE ALL ON TABLE cerefox_document_relations FROM anon;
32
+
33
+ DO $$
34
+ BEGIN
35
+ RAISE NOTICE
36
+ 'Migration 0022: RLS enabled on cerefox_document_relations and anon '
37
+ 'grants revoked (Supabase rls_disabled_in_public). Schema version 0.11.2.';
38
+ END $$;
@@ -0,0 +1,16 @@
1
+ -- 0023_set_document_metadata.sql — metadata-only writes (#204).
2
+ --
3
+ -- Adds cerefox_set_document_metadata. No table DDL: the function itself ships
4
+ -- in rpcs.sql, which `cerefox server deploy` re-applies wholesale. This
5
+ -- migration exists so the schema version advances in step, which is what tells
6
+ -- an existing deployment it needs that redeploy.
7
+ --
8
+ -- Schema version 0.11.2 → 0.11.3. Purely additive.
9
+
10
+ DO $$
11
+ BEGIN
12
+ RAISE NOTICE
13
+ 'Migration 0023: cerefox_set_document_metadata arrives with rpcs.sql on '
14
+ 'this deploy — metadata-only writes, merge by default, JSON null removes '
15
+ 'a key (RFC 7386). Schema version 0.11.3.';
16
+ END $$;
@@ -2416,6 +2416,120 @@ $$;
2416
2416
  -- redeploy. The web UI's /api/v1/schema-version endpoint compares the bundled
2417
2417
  -- and deployed values and surfaces a 'redeploy needed' banner on mismatch.
2418
2418
 
2419
+ -- ── cerefox_set_document_metadata ────────────────────────────────────────────
2420
+ -- Change a document's metadata WITHOUT touching its content (#204).
2421
+ --
2422
+ -- Until this existed, `cerefox_ingest` was the only way to set a tag, and it
2423
+ -- requires title + content — so changing one key meant resending the whole
2424
+ -- document, reproducing every untouched character. That is the transcription
2425
+ -- risk the partial-edit tools were built to remove, and it was still fully
2426
+ -- present for metadata. Project membership had a metadata-only writer
2427
+ -- (`cerefox_set_document_projects`) all along; tags simply never got one.
2428
+ --
2429
+ -- MERGE is the default, not replace. Several agent roles write to the same
2430
+ -- documents, so a destructive default would let a caller setting `status`
2431
+ -- silently drop `type`, `seq` and `agent_role` written by someone else — a
2432
+ -- default shadowing stored state, which is the defect class behind #183 and
2433
+ -- #191. It would also force a read-modify-write, which is the cost this
2434
+ -- function exists to remove.
2435
+ --
2436
+ -- REMOVAL uses a JSON null, following RFC 7386 (JSON Merge Patch):
2437
+ -- {"status": "active", "stale_key": null} -- sets status, deletes stale_key
2438
+ -- Unambiguous because Cerefox metadata values are JSON strings by convention,
2439
+ -- so a null can never be a legitimate value. Same spirit as `cerefox_ingest`,
2440
+ -- where NULL means "not provided, keep existing" and '{}' means "clear all".
2441
+ --
2442
+ -- The merge happens inside one UPDATE against a locked row, so two agents
2443
+ -- setting different keys concurrently cannot clobber each other. Doing this
2444
+ -- client-side would reintroduce exactly that race.
2445
+ --
2446
+ -- Metadata-only: no re-chunk, no re-embed, no content version snapshot. The
2447
+ -- audit trail records it as 'update-metadata', which the CHECK already allows.
2448
+ DROP FUNCTION IF EXISTS cerefox_set_document_metadata(UUID, JSONB, BOOLEAN, TEXT, TEXT);
2449
+ CREATE FUNCTION cerefox_set_document_metadata(
2450
+ p_document_id UUID,
2451
+ p_metadata JSONB,
2452
+ p_replace BOOLEAN DEFAULT FALSE,
2453
+ p_author TEXT DEFAULT 'unknown',
2454
+ p_author_type TEXT DEFAULT 'user'
2455
+ )
2456
+ RETURNS TABLE (
2457
+ document_id UUID,
2458
+ metadata JSONB,
2459
+ keys_set INT,
2460
+ keys_removed INT
2461
+ )
2462
+ LANGUAGE plpgsql
2463
+ SECURITY DEFINER
2464
+ SET search_path = public, pg_catalog
2465
+ AS $$
2466
+ DECLARE
2467
+ v_before JSONB;
2468
+ v_after JSONB;
2469
+ v_null_keys TEXT[];
2470
+ v_title TEXT;
2471
+ v_removed INT;
2472
+ v_set INT;
2473
+ BEGIN
2474
+ IF p_metadata IS NULL OR jsonb_typeof(p_metadata) <> 'object' THEN
2475
+ RAISE EXCEPTION 'metadata must be a JSON object'
2476
+ USING ERRCODE = '22023';
2477
+ END IF;
2478
+
2479
+ -- Lock the row so a concurrent metadata write cannot interleave between
2480
+ -- the read and the merge.
2481
+ SELECT d.metadata, d.title INTO v_before, v_title
2482
+ FROM cerefox_documents d
2483
+ WHERE d.id = p_document_id AND d.deleted_at IS NULL
2484
+ FOR UPDATE;
2485
+
2486
+ IF v_title IS NULL THEN
2487
+ RAISE EXCEPTION 'Document % not found (or is deleted)', p_document_id
2488
+ USING ERRCODE = 'P0002';
2489
+ END IF;
2490
+
2491
+ -- Keys explicitly set to null are removals, never stored values.
2492
+ SELECT COALESCE(array_agg(key), ARRAY[]::TEXT[]) INTO v_null_keys
2493
+ FROM jsonb_each(p_metadata)
2494
+ WHERE value = 'null'::jsonb;
2495
+
2496
+ IF p_replace THEN
2497
+ v_after := p_metadata - v_null_keys;
2498
+ ELSE
2499
+ v_after := (COALESCE(v_before, '{}'::jsonb) || p_metadata) - v_null_keys;
2500
+ END IF;
2501
+
2502
+ UPDATE cerefox_documents
2503
+ SET metadata = v_after, updated_at = NOW()
2504
+ WHERE id = p_document_id;
2505
+
2506
+ -- Report what actually changed, not what was asked for: a caller that sets
2507
+ -- a key to the value it already had should see 0, so "nothing happened" is
2508
+ -- distinguishable from "it worked".
2509
+ SELECT count(*)::INT INTO v_removed
2510
+ FROM jsonb_object_keys(COALESCE(v_before, '{}'::jsonb)) k
2511
+ WHERE NOT v_after ? k;
2512
+
2513
+ SELECT count(*)::INT INTO v_set
2514
+ FROM jsonb_each(v_after) e
2515
+ WHERE COALESCE(v_before, '{}'::jsonb) -> e.key IS DISTINCT FROM e.value;
2516
+
2517
+ PERFORM cerefox_create_audit_entry(
2518
+ p_document_id := p_document_id,
2519
+ p_operation := 'update-metadata',
2520
+ p_author := p_author,
2521
+ p_author_type := p_author_type,
2522
+ p_description := format(
2523
+ 'Metadata %s on "%s": %s key(s) set, %s removed',
2524
+ CASE WHEN p_replace THEN 'replaced' ELSE 'merged' END,
2525
+ v_title, v_set, v_removed
2526
+ )
2527
+ );
2528
+
2529
+ RETURN QUERY SELECT p_document_id, v_after, v_set, v_removed;
2530
+ END;
2531
+ $$;
2532
+
2419
2533
  CREATE OR REPLACE FUNCTION cerefox_schema_version()
2420
2534
  RETURNS TEXT
2421
2535
  LANGUAGE sql
@@ -2425,11 +2539,14 @@ SET search_path = public, pg_catalog
2425
2539
  AS $$
2426
2540
  -- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
2427
2541
  -- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
2542
+ -- 0.11.3 (#204): cerefox_set_document_metadata — metadata-only writes.
2543
+ -- 0.11.2 (iteration 36): RLS enabled on cerefox_document_relations,
2544
+ -- which iteration 29 left off the list (Supabase rls_disabled_in_public).
2428
2545
  -- 0.11.1 (iteration 35, #197): audit CHECK accepts 'rename-section'.
2429
2546
  -- 0.11.0 supersedes 0.10.6 (v1.2.1, #191): this branch carries that fix plus
2430
2547
  -- the partial-edit surface, and both migrations (0019, 0020) are in the
2431
2548
  -- sequence, so a store deploying this gets everything from both lines.
2432
- SELECT '0.11.1'::TEXT;
2549
+ SELECT '0.11.3'::TEXT;
2433
2550
  $$;
2434
2551
 
2435
2552
  -- ── cerefox_content_format_stats ─────────────────────────────────────────────
@@ -5,7 +5,7 @@
5
5
  -- Requires extensions: vector (pgvector), uuid-ossp
6
6
  -- These are enabled at the top of db_deploy.py before this file is applied.
7
7
  --
8
- -- @version: 0.11.1
8
+ -- @version: 0.11.3
9
9
  -- The `@version` marker above is read by the schema-version-mismatch banner
10
10
  -- (see /api/v1/schema-version). Bump it whenever schema.sql OR rpcs.sql
11
11
  -- changes in a way that requires `cerefox server deploy` to be re-run —
@@ -431,6 +431,13 @@ ALTER TABLE cerefox_audit_log ENABLE ROW LEVEL SECURITY;
431
431
  ALTER TABLE cerefox_migrations ENABLE ROW LEVEL SECURITY;
432
432
  ALTER TABLE cerefox_config ENABLE ROW LEVEL SECURITY;
433
433
  ALTER TABLE cerefox_usage_log ENABLE ROW LEVEL SECURITY;
434
+ -- iteration 29 added this table and did not add it here, so it stayed
435
+ -- world-accessible on any project whose `anon` role holds the legacy
436
+ -- GRANT ... ON ALL TABLES IN SCHEMA public. Supabase's linter flagged it as
437
+ -- `rls_disabled_in_public` (2026-08-09). Every other table on this list denies
438
+ -- anon by having RLS on with NO policies; this one did not, so the model had a
439
+ -- hole exactly one table wide. See the guard test in _shared/__tests__.
440
+ ALTER TABLE cerefox_document_relations ENABLE ROW LEVEL SECURITY;
434
441
 
435
442
  -- ── Explicit Data API grants (issue #26; schema 0.8.2) ─────────────────────────
436
443
  -- Supabase is removing the implicit privileges the Data API roles get on
@@ -251,6 +251,59 @@ cerefox document edit <doc-id> --set-meta status=archived --unset-meta draft
251
251
 
252
252
  ---
253
253
 
254
+ ### `cerefox document set-metadata`
255
+
256
+ **Purpose**: change a document's metadata **without resending its content**. Before this existed, `cerefox document ingest` was the only way to set a tag, and it needs the whole document — so changing one key meant reproducing every untouched character, which is the transcription risk the partial-edit commands exist to remove.
257
+
258
+ **Merges by default.** The keys you pass are set; every other key is left alone. That means you do not need to read the document first, and you cannot accidentally drop a tag someone else set. Content, chunks and embeddings are untouched and no new version is created; the change is logged as an `update-metadata` audit entry.
259
+
260
+ CLI equivalent of the `cerefox_set_document_metadata` MCP tool. Both call the same RPC, so the semantics cannot drift between them.
261
+
262
+ **Synopsis**:
263
+
264
+ ```
265
+ cerefox document set-metadata [OPTIONS] DOCUMENT_ID
266
+ ```
267
+
268
+ **Options**:
269
+
270
+ | Flag | Meaning |
271
+ |---|---|
272
+ | `-s, --set <key=value...>` | Set a key. Repeatable. |
273
+ | `-r, --remove <key...>` | Remove a key. Repeatable. |
274
+ | `--json <object>` | A JSON object of keys to set; a `null` value removes that key. |
275
+ | `--replace` | Set the metadata to **exactly** what was given, discarding every key not listed. |
276
+ | `-a, --author <name>` | Caller identity (audit log). |
277
+ | `--author-type <type>` | `user` (default) or `agent`. |
278
+ | `--json-out` | Emit the result as JSON. |
279
+
280
+ **Examples**:
281
+
282
+ ```bash
283
+ # Add or update one tag; everything else is preserved
284
+ cerefox document set-metadata <id> --set status=active
285
+
286
+ # Several at once
287
+ cerefox document set-metadata <id> --set type=decision-log --set seq=8
288
+
289
+ # Remove a key
290
+ cerefox document set-metadata <id> --remove stale_key
291
+
292
+ # Set and remove in one call, from a script that already holds an object
293
+ cerefox document set-metadata <id> --json '{"status":"active","stale_key":null}'
294
+
295
+ # Reset a document's tags wholesale (rare)
296
+ cerefox document set-metadata <id> --replace --json '{"type":"note"}'
297
+ ```
298
+
299
+ **Values are stored as JSON strings**, matching the convention elsewhere: a `metadata_filter` matches JSONB as strings, so a stored boolean `true` would never match a filter looking for `"true"`.
300
+
301
+ **`--set key=null` is refused.** Over MCP a JSON null *removes* a key (RFC 7386), but on a command line the same text could equally mean the literal word "null" — so rather than guess, the command points you at `--remove key` (to delete) or `--json '{"key":"null"}'` (to store the word).
302
+
303
+ **Output**: reports what actually changed, not what was asked for. Setting a key to the value it already holds reports no change, so re-running a script is distinguishable from doing work.
304
+
305
+ ---
306
+
254
307
  ### `cerefox document set-projects`
255
308
 
256
309
  **Purpose**: replace a document's project memberships with **exactly** the given set (full-set replace — any project not listed is removed). This is the CLI equivalent of the `cerefox_set_document_projects` MCP tool; both share one membership-replace core, so they behave identically. Content is untouched; the change is logged as an `update-metadata` audit entry.
@@ -611,7 +664,7 @@ cerefox config set relations_enabled true # tools appear in every agent's lis
611
664
  cerefox config set relations_enabled false # hidden again; no data removed
612
665
  ```
613
666
 
614
- Agents see 12 tools with the flag off and 16 with it on. See
667
+ Agents see 13 tools with the flag off and 16 with it on. See
615
668
  [`configuration.md`](configuration.md) for the full runtime-config surface.
616
669
 
617
670
  ### `cerefox config list` / `cerefox config get` / `cerefox config set`
@@ -72,7 +72,7 @@ in the container.
72
72
  - **Easiest:** `cerefox-local configure-agent` wires it up (registers an MCP server named
73
73
  `cerefox-local` with Claude Code if the `claude` CLI is present, else prints the snippet).
74
74
  - **Manual:** point the client at `command: cerefox-local, args: ["mcp"]` (stdio). That proxies
75
- to `cerefox mcp` in the container; the same 12 core tools, identical behavior to every other path.
75
+ to `cerefox mcp` in the container; the same 13 core tools, identical behavior to every other path.
76
76
  - The cloud paths above (remote Edge Function, GPT Actions) **do not apply** to a local-only
77
77
  install — there are no Edge Functions.
78
78
 
@@ -131,7 +131,7 @@ in the container.
131
131
 
132
132
  ### What it is
133
133
 
134
- The local Cerefox MCP server runs on your machine and exposes the same 12 core tools as the remote
134
+ The local Cerefox MCP server runs on your machine and exposes the same 13 core tools as the remote
135
135
  Edge Function, communicating with clients over stdio.
136
136
 
137
137
  The local server ships as an npm package — **[`@cerefox/memory`](https://www.npmjs.com/package/@cerefox/memory)** — built with the official `@modelcontextprotocol/sdk`.
@@ -196,10 +196,11 @@ Once configured, every Path A client has these tools:
196
196
  | `cerefox_get_audit_log` | Query audit log entries with filters (document, author, operation, time range) |
197
197
  | `cerefox_list_projects` | List all projects with names and IDs. Use for discovering available projects. |
198
198
  | `cerefox_metadata_search` | Find documents by metadata key-value criteria without a text search term. Supports project, date, and content filters. |
199
+ | `cerefox_set_document_metadata` | Change a document's metadata without resending its content. **Merges** by default (keys you pass are set, others left alone); a `null` value removes a key (RFC 7386); `replace: true` sets exactly the object given. No re-chunk, no re-embed, no new version. |
199
200
  | `cerefox_set_document_projects` | Set a document's project memberships to exactly the given list (destructive replace; metadata-only, no content change). Use `cerefox_ingest` with singular `project_name` for non-destructive "add". |
200
201
  | `cerefox_get_help` | Retrieve Cerefox conventions (the same content as `AGENT_QUICK_REFERENCE.md`) over MCP. Optional `topic` parameter does a case-insensitive H2 substring match. Call this whenever you are uncertain. |
201
202
 
202
- > All 12 core tools are available on both Path A (local and remote MCP) and Path B (GPT Actions
203
+ > All 13 core tools are available on both Path A (local and remote MCP) and Path B (GPT Actions
203
204
  > via dedicated Edge Functions, except `cerefox_get_help` which is MCP-only). MCP tools use
204
205
  > `project_name` (human-readable); primitive Edge Functions (Path B) use `project_id` (UUID).
205
206
 
@@ -225,9 +226,10 @@ For the full tool reference, search Cerefox for "How AI Agents Use Cerefox".
225
226
  After setup, ask your client:
226
227
 
227
228
  > "What tools do you have available?"
228
- > Expected: 12 tools listed (`cerefox_search`, `cerefox_ingest`, `cerefox_insert`, `cerefox_edit`, `cerefox_get_document`,
229
+ > Expected: 13 tools listed (`cerefox_search`, `cerefox_ingest`, `cerefox_insert`, `cerefox_edit`, `cerefox_get_document`,
229
230
  > `cerefox_list_versions`, `cerefox_list_projects`, `cerefox_list_metadata_keys`,
230
- > `cerefox_metadata_search`, `cerefox_set_document_projects`, `cerefox_get_audit_log`,
231
+ > `cerefox_metadata_search`, `cerefox_set_document_projects`,
232
+ > `cerefox_set_document_metadata`, `cerefox_get_audit_log`,
231
233
  > `cerefox_get_help`).
232
234
 
233
235
  > "Use cerefox_search with query='second brain' and match_count=3. What did you find?"
@@ -319,7 +321,7 @@ pick it up automatically.
319
321
  to primitive Edge Functions. This means each MCP tool call costs a single Edge Function
320
322
  invocation.
321
323
 
322
- A single HTTPS URL gives any remote-capable MCP client all 12 core tools with full hybrid
324
+ A single HTTPS URL gives any remote-capable MCP client all 13 core tools with full hybrid
323
325
  search -- no Python, no `uv`, no local repository clone needed.
324
326
 
325
327
  **URL format:**
@@ -500,7 +502,7 @@ Replace `<your-project-ref>` with your Supabase project ref.
500
502
  **Step 3 — Verify:**
501
503
 
502
504
  Launch Codex and use the `/mcp` slash command to confirm the `cerefox` server is connected
503
- and all 12 tools are listed.
505
+ and all 13 tools are listed.
504
506
 
505
507
  **Notes:**
506
508
  - `bearer_token_env_var` is the **name** of the env var (e.g. `"CEREFOX_ACCESS_TOKEN"`), not the
@@ -1149,7 +1151,7 @@ user + `CEREFOX_OAUTH_OWNER_ID` pin, and register the Claude OAuth App
1149
1151
  Client Secret from the pre-registered OAuth App (setup-supabase Step 7d).
1150
1152
  5. Save. Claude runs the OAuth flow → redirects you to the **Cerefox consent page** (sign
1151
1153
  in with the owner email/password from Step 7c, then **Allow**) → returns to Claude. The
1152
- connector shows as connected with **12 tools** (16 if you have enabled document relations). (If you've approved before, Supabase
1154
+ connector shows as connected with **13 tools** (16 if you have enabled document relations). (If you've approved before, Supabase
1153
1155
  auto-consents and the page just flashes through — that's expected.)
1154
1156
  6. **Mobile**: connectors are account-level, so `CerefoxMCP` appears in the Claude mobile
1155
1157
  app automatically — run one search from your phone to confirm.
@@ -1245,6 +1247,7 @@ The agent docs are written around MCP tool names. **CLI flag names match MCP par
1245
1247
  | `cerefox_get_document` | `cerefox document get <document-id> --version-id <vid> --requestor <name>` |
1246
1248
  | `cerefox_list_versions` | `cerefox document version list <document-id> --requestor <name>` |
1247
1249
  | `cerefox_list_projects` | `cerefox project list --requestor <name>` |
1250
+ | `cerefox_set_document_metadata` | `cerefox document set-metadata <document-id> --set key=value` (also `--remove key`, `--json '{...}'`, `--replace`) |
1248
1251
  | `cerefox_set_document_projects` | `cerefox document set-projects <document-id> <name...> --author <a> --author-type user\|agent` (or `--clear`) |
1249
1252
  | `cerefox_list_metadata_keys` | `cerefox metadata keys` |
1250
1253
  | `cerefox_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --project-name <n> --requestor <name>` |
@@ -74,7 +74,7 @@ paths go through **Edge Functions**, which the free tier caps at 500,000/month:
74
74
  500K/month is generous for a single human-driven agent. But **automated or
75
75
  high-frequency agents on the remote path** can approach it. The lever: point those
76
76
  agents at the **local stdio MCP server** (`cerefox mcp`) instead of the remote Edge
77
- Function — it exposes the identical 12 core tools (plus the 4 relation tools when enabled), talks to the Data API directly, and
77
+ Function — it exposes the identical 13 core tools (plus the 4 relation tools when enabled), talks to the Data API directly, and
78
78
  costs **zero** Edge Function invocations (bonus: lower latency, and it works offline
79
79
  against a reachable database). If you do exceed the free EF quota, Supabase's Pro
80
80
  plan ($25/mo, 2M invocations included) is the next step.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cerefox/memory",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Cerefox — user-owned shared memory for AI agents. CLI + stdio MCP server + web UI + ingestion for a knowledge base on your own Supabase project (or fully self-hosted with Cerefox Local).",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/fstamatelopoulos/cerefox",