@cerefox/memory 1.6.1 → 1.7.1
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/AGENT_GUIDE.md +80 -6
- package/AGENT_QUICK_REFERENCE.md +9 -7
- package/README.md +5 -3
- package/dist/bin/cerefox.js +790 -319
- package/dist/frontend/assets/index-D8E0mTnp.js +121 -0
- package/dist/frontend/assets/index-D8E0mTnp.js.map +1 -0
- package/dist/frontend/index.html +1 -1
- package/dist/server-assets/_shared/ef-meta/index.ts +3 -3
- package/dist/server-assets/_shared/mcp-tools/_utils.ts +41 -0
- package/dist/server-assets/_shared/mcp-tools/delete-document.ts +181 -0
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +4 -4
- package/dist/server-assets/_shared/mcp-tools/index.ts +8 -1
- package/dist/server-assets/_shared/mcp-tools/ingest.ts +75 -8
- package/dist/server-assets/_shared/mcp-tools/partial-edits.ts +43 -4
- package/dist/server-assets/_shared/mcp-tools/restore-document.ts +130 -0
- package/dist/server-assets/db/migrations/0024_mcp_delete_document.sql +26 -0
- package/dist/server-assets/db/migrations/0025_drop_orphaned_overloads.sql +15 -0
- package/dist/server-assets/db/migrations/0026_metadata_guard_and_dead_links.sql +46 -0
- package/dist/server-assets/db/rpcs.sql +355 -27
- package/dist/server-assets/db/schema.sql +10 -2
- package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +26 -0
- package/docs/guides/access-paths.md +24 -15
- package/docs/guides/cli.md +34 -7
- package/docs/guides/connect-agents.md +41 -17
- package/docs/guides/operational-cost.md +1 -1
- package/package.json +1 -1
- package/dist/frontend/assets/index-OqloGFwv.js +0 -121
- package/dist/frontend/assets/index-OqloGFwv.js.map +0 -1
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
sha256hex,
|
|
36
36
|
} from "./_chunker.ts";
|
|
37
37
|
import { activeEmbedderName, embedBatch, resolveEmbedderKind } from "../embeddings/index.ts";
|
|
38
|
-
import { logUsage } from "./_utils.ts";
|
|
38
|
+
import { extractConflictHashes, isMissingFunctionError, logUsage } from "./_utils.ts";
|
|
39
39
|
import { McpInvalidParams, type MCPSupabaseClient, type ToolContext, type ToolDefinition } from "./types.ts";
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -175,6 +175,28 @@ async function readDocument(
|
|
|
175
175
|
supabase: MCPSupabaseClient,
|
|
176
176
|
documentId: string,
|
|
177
177
|
): Promise<{ title: string; content: string; hash: string }> {
|
|
178
|
+
// Trashed check BEFORE anything else: a partial edit of a trashed document
|
|
179
|
+
// would otherwise pay for embeddings and then hit the RPC's CEREFOX_DELETED
|
|
180
|
+
// refusal as an unshaped error, on every retry (round-4 review). ADVISORY
|
|
181
|
+
// only — the RPC guard is authoritative — so a client without the .from()
|
|
182
|
+
// surface (test doubles) skips it rather than failing.
|
|
183
|
+
try {
|
|
184
|
+
const { data: lifecycle } = await supabase
|
|
185
|
+
.from("cerefox_documents")
|
|
186
|
+
.select("deleted_at")
|
|
187
|
+
.eq("id", documentId)
|
|
188
|
+
.maybeSingle();
|
|
189
|
+
if (lifecycle?.deleted_at) {
|
|
190
|
+
throw new McpInvalidParams(
|
|
191
|
+
`Document ${documentId} is soft-deleted (in the trash). A trashed document ` +
|
|
192
|
+
`cannot be edited — restore it first with cerefox_restore_document, then retry.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
if (e instanceof McpInvalidParams) throw e;
|
|
197
|
+
// fall through — the RPC's CEREFOX_DELETED guard still refuses the write
|
|
198
|
+
}
|
|
199
|
+
|
|
178
200
|
const { data, error } = await supabase.rpc("cerefox_get_document", {
|
|
179
201
|
p_document_id: documentId,
|
|
180
202
|
p_version_id: null,
|
|
@@ -303,8 +325,7 @@ async function applyAndWrite(
|
|
|
303
325
|
if (error) {
|
|
304
326
|
const message = error.message ?? "";
|
|
305
327
|
if (message.includes("CEREFOX_CONFLICT")) {
|
|
306
|
-
|
|
307
|
-
throw conflictError(documentId, expectedHash, current);
|
|
328
|
+
throw conflictError(documentId, expectedHash, extractConflictHashes(message).current);
|
|
308
329
|
}
|
|
309
330
|
if (message.includes("cerefox_documents_hash_unique")) {
|
|
310
331
|
// content_hash is UNIQUE store-wide, so an edit whose result matches
|
|
@@ -318,7 +339,25 @@ async function applyAndWrite(
|
|
|
318
339
|
`cerefox_search for the resulting content to find it.`,
|
|
319
340
|
);
|
|
320
341
|
}
|
|
321
|
-
if (message.includes("
|
|
342
|
+
if (message.includes("CEREFOX_UNRESOLVED_LINKS")) {
|
|
343
|
+
// Only ids INTRODUCED by this write reject (the RPC tolerates dead
|
|
344
|
+
// links the document already carried), so the diagnosis is specific.
|
|
345
|
+
const ids = message.match(/do not exist: ([^.]+)\./)?.[1] ?? "(unparsed)";
|
|
346
|
+
throw new Error(
|
|
347
|
+
`Edit rejected — this edit introduces link(s) to document id(s) that do not ` +
|
|
348
|
+
`exist: ${ids}. The UUIDs are almost certainly mangled — re-read the source ` +
|
|
349
|
+
`you copied each link from, correct the id(s), and retry. Do not retry ` +
|
|
350
|
+
`unchanged. Deliberate examples belong in code formatting (backticks).`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (message.includes("CEREFOX_DELETED")) {
|
|
354
|
+
// Race backstop: the doc was trashed between our read and the write.
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Document ${documentId} was soft-deleted while this edit was in flight. ` +
|
|
357
|
+
`Restore it first with cerefox_restore_document, then retry.`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (isMissingFunctionError(message, "cerefox_ingest_document")) {
|
|
322
361
|
throw new Error(
|
|
323
362
|
`This server is behind: partial edits need schema 0.11.0 or newer. ` +
|
|
324
363
|
`Run \`cerefox server deploy\`, then retry. (${message})`,
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cerefox_restore_document` — bring a soft-deleted document back (#210).
|
|
3
|
+
*
|
|
4
|
+
* The counterpart to `cerefox_delete_document`, added by maintainer decision
|
|
5
|
+
* (2026-08-13) reversing the earlier "restore is human-only" posture: every
|
|
6
|
+
* restore is audited with author attribution, restoring cannot destroy
|
|
7
|
+
* content, and CLI/MCP parity (`cerefox document restore` existed all along)
|
|
8
|
+
* beats a boundary the audit surface had already outgrown.
|
|
9
|
+
*
|
|
10
|
+
* What still cannot happen from here: **permanent purge**, which remains
|
|
11
|
+
* web-UI-only — the one action that actually destroys data keeps its
|
|
12
|
+
* human-in-the-loop confirmation.
|
|
13
|
+
*
|
|
14
|
+
* No `expected_content_hash`, matching the CLI contract: a trashed document
|
|
15
|
+
* cannot be rewritten while in the trash (cerefox_ingest_document refuses
|
|
16
|
+
* updates to soft-deleted documents as of 0.12.0), so what was reviewed in
|
|
17
|
+
* the trash is what comes back — there is no read-freshness to prove.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { MCPSupabaseClient } from "./types.ts";
|
|
21
|
+
|
|
22
|
+
import { isDocumentNotFoundError, isMissingFunctionError, logUsage } from "./_utils.ts";
|
|
23
|
+
import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
|
|
24
|
+
|
|
25
|
+
async function handler(
|
|
26
|
+
supabase: MCPSupabaseClient,
|
|
27
|
+
args: Record<string, unknown>,
|
|
28
|
+
ctx: ToolContext,
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
const document_id = args.document_id as string | undefined;
|
|
31
|
+
const reason = args.reason as string | undefined;
|
|
32
|
+
|
|
33
|
+
if (!document_id) throw new McpInvalidParams("document_id is required");
|
|
34
|
+
|
|
35
|
+
const author = (args.author as string | undefined) ?? (args.requestor as string | undefined);
|
|
36
|
+
// Derived from the transport, never taken from the caller: an agent must not
|
|
37
|
+
// be able to record itself as a user. Matches the delete handler.
|
|
38
|
+
const authorType = ctx.accessPath === "cli" ? "user" : "agent";
|
|
39
|
+
|
|
40
|
+
const { data, error } = await supabase.rpc("cerefox_restore_document", {
|
|
41
|
+
p_document_id: document_id,
|
|
42
|
+
p_author: author ?? "unknown",
|
|
43
|
+
p_author_type: authorType,
|
|
44
|
+
p_reason: reason ?? null,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (error) {
|
|
48
|
+
const message = error.message ?? String(error);
|
|
49
|
+
if (isMissingFunctionError(message, "cerefox_restore_document")) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`This server is behind: cerefox_restore_document needs schema 0.12.0 or newer. ` +
|
|
52
|
+
`Run \`cerefox server deploy\`, then retry. (${message})`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (isDocumentNotFoundError(error)) {
|
|
56
|
+
throw new McpInvalidParams(`Document ${document_id} not found.`);
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`RPC error: ${message}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const row = data as
|
|
62
|
+
| {
|
|
63
|
+
document_id?: string;
|
|
64
|
+
title?: string;
|
|
65
|
+
total_chars?: number;
|
|
66
|
+
restored?: boolean;
|
|
67
|
+
was_deleted?: boolean;
|
|
68
|
+
}
|
|
69
|
+
| undefined;
|
|
70
|
+
if (!row) throw new Error("cerefox_restore_document returned no data");
|
|
71
|
+
|
|
72
|
+
if (!row.restored) {
|
|
73
|
+
// Report what happened, not what was asked for: the document was never in
|
|
74
|
+
// the trash, nothing changed, no audit entry was written.
|
|
75
|
+
return (
|
|
76
|
+
`Document ${document_id} ("${row.title ?? "untitled"}") is NOT deleted — ` +
|
|
77
|
+
`there is nothing to restore. No change was made.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
logUsage(supabase, {
|
|
82
|
+
operation: "restore",
|
|
83
|
+
accessPath: ctx.accessPath,
|
|
84
|
+
requestor: args.requestor as string | undefined,
|
|
85
|
+
document_id,
|
|
86
|
+
result_count: 1,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
`Restored "${row.title ?? "untitled"}" (id: ${document_id}, ` +
|
|
91
|
+
`${row.total_chars ?? "?"} chars) from the trash. It is searchable again ` +
|
|
92
|
+
`and the restore is recorded in the audit log.\n` +
|
|
93
|
+
`Tell your user what you restored and why.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const restoreDocumentTool: ToolDefinition = {
|
|
98
|
+
name: "cerefox_restore_document",
|
|
99
|
+
description:
|
|
100
|
+
"Restore a soft-deleted document from the trash — the inverse of cerefox_delete_document. The document becomes searchable again; the restore is recorded in the audit log with your identity. Restoring a document that is not deleted is a reported no-op. Pass a short reason — like a delete's reason, it is what the human reviewing the audit trail goes on. Permanent purge has no agent surface: once a human purges a document from the web UI, it is gone and cannot be restored.",
|
|
101
|
+
annotations: {
|
|
102
|
+
title: "Restore document from trash",
|
|
103
|
+
readOnlyHint: false,
|
|
104
|
+
// Recovery only: it can bring content back, never remove it.
|
|
105
|
+
destructiveHint: false,
|
|
106
|
+
idempotentHint: true,
|
|
107
|
+
openWorldHint: false,
|
|
108
|
+
},
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: "object",
|
|
111
|
+
required: ["document_id"],
|
|
112
|
+
properties: {
|
|
113
|
+
document_id: { type: "string", description: "UUID of the soft-deleted document to restore." },
|
|
114
|
+
reason: {
|
|
115
|
+
type: "string",
|
|
116
|
+
description:
|
|
117
|
+
"Why this document is being restored. Recorded in the audit-log entry. Short and specific beats long.",
|
|
118
|
+
},
|
|
119
|
+
author: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: "Who is making this change. Recorded in the audit log.",
|
|
122
|
+
},
|
|
123
|
+
requestor: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "Name of the agent or user making this request. Recorded in the usage log.",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
handler,
|
|
130
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- 0024_mcp_delete_document.sql — MCP soft-delete parity (#208).
|
|
2
|
+
--
|
|
3
|
+
-- Reworks cerefox_delete_document: optional CAS via p_expected_content_hash
|
|
4
|
+
-- (CEREFOX_CONFLICT / PT409 on mismatch, same pattern as the ingest CAS),
|
|
5
|
+
-- p_reason appended to the audit description, JSONB return instead of VOID,
|
|
6
|
+
-- and idempotent re-delete (original deleted_at preserved, no duplicate audit
|
|
7
|
+
-- entry). No table DDL: the function itself ships in rpcs.sql, which
|
|
8
|
+
-- `cerefox server deploy` re-applies wholesale. This migration exists so the
|
|
9
|
+
-- schema version advances in step, which is what tells an existing deployment
|
|
10
|
+
-- it needs that redeploy.
|
|
11
|
+
--
|
|
12
|
+
-- Schema version 0.11.3 → 0.12.0. Restore moves OUT of the web-UI-only tier
|
|
13
|
+
-- (#210, maintainer decision): both delete and restore are agent-reachable
|
|
14
|
+
-- and audited; permanent purge is now the single web-UI-only action.
|
|
15
|
+
-- cerefox_ingest_document additionally refuses to rewrite a soft-deleted
|
|
16
|
+
-- document (restore first), which is what makes restore safe without a
|
|
17
|
+
-- freshness token.
|
|
18
|
+
|
|
19
|
+
DO $$
|
|
20
|
+
BEGIN
|
|
21
|
+
RAISE NOTICE
|
|
22
|
+
'Migration 0024: cerefox_delete_document + cerefox_restore_document '
|
|
23
|
+
'rework arrives with rpcs.sql on this deploy — CAS on delete, p_reason, '
|
|
24
|
+
'JSONB returns, honest no-ops. Backs the new cerefox_delete_document '
|
|
25
|
+
'and cerefox_restore_document MCP tools (#208, #210). Schema 0.12.0.';
|
|
26
|
+
END $$;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- 0025_drop_orphaned_overloads.sql — remove pre-author-era 1-arg overloads.
|
|
2
|
+
--
|
|
3
|
+
-- cerefox_purge_document(UUID) and cerefox_restore_document(UUID) survived
|
|
4
|
+
-- every CREATE OR REPLACE since their signatures grew (OR REPLACE only
|
|
5
|
+
-- replaces the SAME signature), leaving long-lived databases with BOTH
|
|
6
|
+
-- overloads. A named 1-arg call is then ambiguous — PostgREST PGRST203
|
|
7
|
+
-- ("could not choose the best candidate") — which is how the first
|
|
8
|
+
-- production acceptance run failed to purge its fixtures (v1.7.0).
|
|
9
|
+
-- Fresh databases never had the old signatures and are unaffected.
|
|
10
|
+
--
|
|
11
|
+
-- Schema version 0.12.0 → 0.12.1. The DROPs also run from rpcs.sql on every
|
|
12
|
+
-- deploy; this migration makes the version advance signal the redeploy.
|
|
13
|
+
|
|
14
|
+
DROP FUNCTION IF EXISTS cerefox_purge_document(UUID);
|
|
15
|
+
DROP FUNCTION IF EXISTS cerefox_restore_document(UUID);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- 0026_metadata_guard_and_dead_links.sql — #212 metadata type guards +
|
|
2
|
+
-- #214 phase-2 dead-link sweep.
|
|
3
|
+
--
|
|
4
|
+
-- RPC changes ship via rpcs.sql on this deploy: cerefox_ingest_document
|
|
5
|
+
-- rejects non-object p_metadata (the MCP layer always did; now every write
|
|
6
|
+
-- path agrees), cerefox_set_document_metadata refuses to MERGE onto a
|
|
7
|
+
-- non-object stored value (|| would produce an array; only replace=true
|
|
8
|
+
-- repairs), and two read-only RPCs arrive: cerefox_find_dead_links (whole-KB
|
|
9
|
+
-- [Text](uuid) sweep) and cerefox_metadata_health (rows with non-object
|
|
10
|
+
-- metadata, surfaced by doctor).
|
|
11
|
+
--
|
|
12
|
+
-- Schema version 0.12.1 → 0.12.2. This migration also REPORTS (not repairs)
|
|
13
|
+
-- any rows already in the non-object state, so the operator sees them at
|
|
14
|
+
-- upgrade time; repair is `cerefox document set-metadata <id> --replace`.
|
|
15
|
+
|
|
16
|
+
-- Table-level backstop (round-5 review): closes every current and future
|
|
17
|
+
-- direct writer at once, not only the RPC/CLI/web paths patched in code.
|
|
18
|
+
-- NOT VALID: legacy non-object rows survive (reported below) until repaired
|
|
19
|
+
-- with `document set-metadata --replace`, which the constraint then checks.
|
|
20
|
+
DO $$
|
|
21
|
+
BEGIN
|
|
22
|
+
IF NOT EXISTS (
|
|
23
|
+
SELECT 1 FROM pg_constraint
|
|
24
|
+
WHERE conname = 'cerefox_documents_metadata_object'
|
|
25
|
+
) THEN
|
|
26
|
+
ALTER TABLE cerefox_documents
|
|
27
|
+
ADD CONSTRAINT cerefox_documents_metadata_object
|
|
28
|
+
CHECK (jsonb_typeof(metadata) = 'object') NOT VALID;
|
|
29
|
+
END IF;
|
|
30
|
+
END $$;
|
|
31
|
+
|
|
32
|
+
DO $$
|
|
33
|
+
DECLARE
|
|
34
|
+
v_count INT;
|
|
35
|
+
BEGIN
|
|
36
|
+
SELECT count(*) INTO v_count
|
|
37
|
+
FROM cerefox_documents
|
|
38
|
+
WHERE metadata IS NOT NULL AND jsonb_typeof(metadata) <> 'object';
|
|
39
|
+
IF v_count > 0 THEN
|
|
40
|
+
RAISE NOTICE
|
|
41
|
+
'Migration 0026: % document(s) hold NON-OBJECT metadata (legacy #212 state). List them with cerefox doctor (or SELECT * FROM cerefox_metadata_health()); repair each with cerefox document set-metadata <id> --replace --json ''<object>''.',
|
|
42
|
+
v_count;
|
|
43
|
+
ELSE
|
|
44
|
+
RAISE NOTICE 'Migration 0026: metadata guards + dead-link sweep arrive with rpcs.sql. No non-object metadata rows found. Schema 0.12.2.';
|
|
45
|
+
END IF;
|
|
46
|
+
END $$;
|