memwarden 0.0.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/LICENSE +202 -0
- package/README.md +402 -0
- package/dist/bundle/bundle.d.ts +28 -0
- package/dist/bundle/bundle.js +85 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +593 -0
- package/dist/cli/connect.d.ts +63 -0
- package/dist/cli/connect.js +121 -0
- package/dist/cli/hook.d.ts +24 -0
- package/dist/cli/hook.js +186 -0
- package/dist/cli/tools.d.ts +47 -0
- package/dist/cli/tools.js +246 -0
- package/dist/daemon/ensure.d.ts +12 -0
- package/dist/daemon/ensure.js +54 -0
- package/dist/daemon/service.d.ts +15 -0
- package/dist/daemon/service.js +210 -0
- package/dist/embedding/index.d.ts +10 -0
- package/dist/embedding/index.js +33 -0
- package/dist/embedding/local-embedding.d.ts +14 -0
- package/dist/embedding/local-embedding.js +80 -0
- package/dist/functions/access-tracker.d.ts +13 -0
- package/dist/functions/access-tracker.js +92 -0
- package/dist/functions/audit.d.ts +46 -0
- package/dist/functions/audit.js +0 -0
- package/dist/functions/cjk-segmenter.d.ts +6 -0
- package/dist/functions/cjk-segmenter.js +120 -0
- package/dist/functions/compress-synthetic.d.ts +2 -0
- package/dist/functions/compress-synthetic.js +104 -0
- package/dist/functions/config.d.ts +68 -0
- package/dist/functions/config.js +231 -0
- package/dist/functions/conflicts.d.ts +19 -0
- package/dist/functions/conflicts.js +328 -0
- package/dist/functions/context.d.ts +3 -0
- package/dist/functions/context.js +155 -0
- package/dist/functions/dedup.d.ts +11 -0
- package/dist/functions/dedup.js +51 -0
- package/dist/functions/dejafix.d.ts +96 -0
- package/dist/functions/dejafix.js +356 -0
- package/dist/functions/doctor.d.ts +29 -0
- package/dist/functions/doctor.js +137 -0
- package/dist/functions/forget.d.ts +3 -0
- package/dist/functions/forget.js +87 -0
- package/dist/functions/hybrid-search.d.ts +17 -0
- package/dist/functions/hybrid-search.js +205 -0
- package/dist/functions/index.d.ts +32 -0
- package/dist/functions/index.js +44 -0
- package/dist/functions/keyed-mutex.d.ts +1 -0
- package/dist/functions/keyed-mutex.js +21 -0
- package/dist/functions/logger.d.ts +6 -0
- package/dist/functions/logger.js +37 -0
- package/dist/functions/memory-utils.d.ts +2 -0
- package/dist/functions/memory-utils.js +29 -0
- package/dist/functions/observe.d.ts +5 -0
- package/dist/functions/observe.js +326 -0
- package/dist/functions/paths.d.ts +1 -0
- package/dist/functions/paths.js +38 -0
- package/dist/functions/privacy.d.ts +1 -0
- package/dist/functions/privacy.js +30 -0
- package/dist/functions/provenance.d.ts +9 -0
- package/dist/functions/provenance.js +57 -0
- package/dist/functions/quantized-vector-index.d.ts +60 -0
- package/dist/functions/quantized-vector-index.js +275 -0
- package/dist/functions/receipt.d.ts +31 -0
- package/dist/functions/receipt.js +95 -0
- package/dist/functions/search-index.d.ts +27 -0
- package/dist/functions/search-index.js +217 -0
- package/dist/functions/search.d.ts +25 -0
- package/dist/functions/search.js +523 -0
- package/dist/functions/stemmer.d.ts +1 -0
- package/dist/functions/stemmer.js +110 -0
- package/dist/functions/synonyms.d.ts +1 -0
- package/dist/functions/synonyms.js +69 -0
- package/dist/functions/turboquant.d.ts +53 -0
- package/dist/functions/turboquant.js +278 -0
- package/dist/functions/types.d.ts +217 -0
- package/dist/functions/types.js +8 -0
- package/dist/functions/vector-index.d.ts +25 -0
- package/dist/functions/vector-index.js +125 -0
- package/dist/functions/vector-persistence.d.ts +14 -0
- package/dist/functions/vector-persistence.js +75 -0
- package/dist/functions/verify.d.ts +13 -0
- package/dist/functions/verify.js +104 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +219 -0
- package/dist/kernel/http.d.ts +24 -0
- package/dist/kernel/http.js +261 -0
- package/dist/kernel/index.d.ts +19 -0
- package/dist/kernel/index.js +21 -0
- package/dist/kernel/kernel.d.ts +80 -0
- package/dist/kernel/kernel.js +297 -0
- package/dist/kernel/pubsub.d.ts +21 -0
- package/dist/kernel/pubsub.js +38 -0
- package/dist/kernel/types.d.ts +139 -0
- package/dist/kernel/types.js +20 -0
- package/dist/mcp/bin.d.ts +2 -0
- package/dist/mcp/bin.js +27 -0
- package/dist/mcp/server.d.ts +34 -0
- package/dist/mcp/server.js +377 -0
- package/dist/observability/metrics.d.ts +26 -0
- package/dist/observability/metrics.js +104 -0
- package/dist/proxy/server.d.ts +30 -0
- package/dist/proxy/server.js +331 -0
- package/dist/state/kv.d.ts +41 -0
- package/dist/state/kv.js +50 -0
- package/dist/state/oplog.d.ts +25 -0
- package/dist/state/oplog.js +57 -0
- package/dist/state/schema.d.ts +60 -0
- package/dist/state/schema.js +88 -0
- package/dist/state/store-libsql.d.ts +46 -0
- package/dist/state/store-libsql.js +263 -0
- package/dist/state/store-memory.d.ts +23 -0
- package/dist/state/store-memory.js +121 -0
- package/dist/state/store.d.ts +87 -0
- package/dist/state/store.js +58 -0
- package/dist/triggers/api.d.ts +14 -0
- package/dist/triggers/api.js +510 -0
- package/dist/triggers/auth.d.ts +1 -0
- package/dist/triggers/auth.js +13 -0
- package/package.json +58 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//
|
|
2
|
+
// StateStore is the single persistence abstraction behind the StateKV
|
|
3
|
+
// contract. It preserves the EXACT observable semantics of the original
|
|
4
|
+
// two-level KV (src/state/kv.ts + src/mcp/in-memory-kv.ts):
|
|
5
|
+
//
|
|
6
|
+
// - scope and key are BOTH opaque strings, no prefix/hierarchy semantics
|
|
7
|
+
// - get -> value | null (null, never undefined, never throws on miss)
|
|
8
|
+
// - set -> upsert, last-write-wins, returns the written value
|
|
9
|
+
// - update -> read-or-{}, apply flat {type:"set", path, value} ops, write back
|
|
10
|
+
// - delete -> idempotent, no error on missing
|
|
11
|
+
// - list -> VALUES only (no keys), exact scope match, insertion order,
|
|
12
|
+
// [] on unknown scope
|
|
13
|
+
//
|
|
14
|
+
// On top of those semantics it adds two things memwarden needs beyond a plain
|
|
15
|
+
// key-value store:
|
|
16
|
+
//
|
|
17
|
+
// 1. Mutation events. set/update/delete report the affected key, the
|
|
18
|
+
// event_type, and the old/new values so the kernel can drive the
|
|
19
|
+
// registered type:"state" trigger (events.ts:108-145). The store does
|
|
20
|
+
// NOT know about triggers; it just emits and the kernel routes.
|
|
21
|
+
// 2. An append-only, hash-chained oplog of every mutation (SQLite-backed
|
|
22
|
+
// impl persists it; the memory impl mirrors it in an array). Ed25519
|
|
23
|
+
// signing lands in a later phase; for now each entry carries a SHA-256
|
|
24
|
+
// hash over its canonical bytes plus the previous entry's hash.
|
|
25
|
+
/**
|
|
26
|
+
* Apply the StateKV `update` op-list to a record, in place semantics returning
|
|
27
|
+
* the mutated record. Shared by both store implementations so their behavior
|
|
28
|
+
* is identical. Only `type:"set"` is honored (no push/inc/delete/append is
|
|
29
|
+
* ever produced by callers); `path` is a flat
|
|
30
|
+
* top-level field name, never dotted. Unknown op types are ignored.
|
|
31
|
+
*/
|
|
32
|
+
export function applyUpdateOps(current, ops) {
|
|
33
|
+
for (const op of ops) {
|
|
34
|
+
if (op.type === "set") {
|
|
35
|
+
current[op.path] = op.value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return current;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Canonical JSON for hashing/signing: deterministic key ordering so the same
|
|
42
|
+
* logical value always produces the same bytes regardless of insertion order.
|
|
43
|
+
*/
|
|
44
|
+
export function canonicalize(value) {
|
|
45
|
+
return JSON.stringify(sortValue(value));
|
|
46
|
+
}
|
|
47
|
+
function sortValue(value) {
|
|
48
|
+
if (value === null || typeof value !== "object")
|
|
49
|
+
return value;
|
|
50
|
+
if (Array.isArray(value))
|
|
51
|
+
return value.map(sortValue);
|
|
52
|
+
const obj = value;
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const key of Object.keys(obj).sort()) {
|
|
55
|
+
out[key] = sortValue(obj[key]);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ApiRequest, ISdk } from "../kernel/index.js";
|
|
2
|
+
type Response = {
|
|
3
|
+
status_code: number;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
body: unknown;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Inline auth check for handlers that receive the request directly
|
|
9
|
+
* (defense-in-depth alongside the api-auth middleware). When no secret is
|
|
10
|
+
* configured the API is open.
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkAuth(req: ApiRequest, secret: string | undefined): Response | null;
|
|
13
|
+
export declare function registerApiTriggers(sdk: ISdk, secret?: string): void;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HTTP route registrations for the core surface. Each route is a
|
|
3
|
+
// registerFunction(id, handler) + registerTrigger({type:"http", ...}) pair
|
|
4
|
+
// that validates the request body and delegates to a mem::<x> business
|
|
5
|
+
// handler via sdk.trigger (paths prefixed /memwarden, with the
|
|
6
|
+
// middleware::api-auth chain). Scope: livez, observe, context, search,
|
|
7
|
+
// verify, stats, doctor, export, import.
|
|
8
|
+
import { getSecret, getQuantBits } from "../functions/config.js";
|
|
9
|
+
import { getVectorIndex, getEmbeddingProvider } from "../functions/index.js";
|
|
10
|
+
import { QuantizedVectorIndex } from "../functions/quantized-vector-index.js";
|
|
11
|
+
import { StateKV } from "../state/kv.js";
|
|
12
|
+
import { KV } from "../state/schema.js";
|
|
13
|
+
import { metrics } from "../observability/metrics.js";
|
|
14
|
+
import { exportBundle, importBundle, isBrainBundle } from "../bundle/bundle.js";
|
|
15
|
+
import { timingSafeCompare } from "./auth.js";
|
|
16
|
+
function asNonEmptyString(value) {
|
|
17
|
+
if (typeof value !== "string")
|
|
18
|
+
return null;
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
return trimmed ? trimmed : null;
|
|
21
|
+
}
|
|
22
|
+
function parseOptionalFiniteNumber(value) {
|
|
23
|
+
if (value === undefined || value === null)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (typeof value === "number")
|
|
26
|
+
return Number.isFinite(value) ? value : null;
|
|
27
|
+
if (typeof value === "string") {
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (!trimmed)
|
|
30
|
+
return undefined;
|
|
31
|
+
const parsed = Number(trimmed);
|
|
32
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function parseOptionalPositiveInt(value) {
|
|
37
|
+
const parsed = parseOptionalFiniteNumber(value);
|
|
38
|
+
if (parsed === undefined || parsed === null)
|
|
39
|
+
return parsed;
|
|
40
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
41
|
+
return null;
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Inline auth check for handlers that receive the request directly
|
|
46
|
+
* (defense-in-depth alongside the api-auth middleware). When no secret is
|
|
47
|
+
* configured the API is open.
|
|
48
|
+
*/
|
|
49
|
+
export function checkAuth(req, secret) {
|
|
50
|
+
if (!secret)
|
|
51
|
+
return null;
|
|
52
|
+
const auth = req.headers?.["authorization"] || req.headers?.["Authorization"];
|
|
53
|
+
if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) {
|
|
54
|
+
return { status_code: 401, body: { error: "unauthorized" } };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
export function registerApiTriggers(sdk, secret) {
|
|
59
|
+
const resolvedSecret = secret ?? getSecret();
|
|
60
|
+
// --- auth middleware ----------------------------------------------
|
|
61
|
+
// Invoked by the kernel with { request: { headers } }; returns
|
|
62
|
+
// continue/respond. Absent secret = open (continue).
|
|
63
|
+
sdk.registerFunction("middleware::api-auth", async (input) => {
|
|
64
|
+
if (!resolvedSecret)
|
|
65
|
+
return { action: "continue" };
|
|
66
|
+
const headers = input?.request?.headers || {};
|
|
67
|
+
const auth = headers["authorization"] || headers["Authorization"];
|
|
68
|
+
if (typeof auth !== "string" ||
|
|
69
|
+
!timingSafeCompare(auth, `Bearer ${resolvedSecret}`)) {
|
|
70
|
+
return {
|
|
71
|
+
action: "respond",
|
|
72
|
+
response: { status_code: 401, body: { error: "unauthorized" } },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { action: "continue" };
|
|
76
|
+
});
|
|
77
|
+
// --- GET /memwarden/livez (no auth) -----------------------------
|
|
78
|
+
sdk.registerFunction("api::liveness", async () => ({
|
|
79
|
+
status_code: 200,
|
|
80
|
+
body: { status: "ok", service: "memwarden" },
|
|
81
|
+
}));
|
|
82
|
+
sdk.registerTrigger({
|
|
83
|
+
type: "http",
|
|
84
|
+
function_id: "api::liveness",
|
|
85
|
+
config: { api_path: "/memwarden/livez", http_method: "GET" },
|
|
86
|
+
});
|
|
87
|
+
// --- POST /memwarden/observe ------------------------------------
|
|
88
|
+
sdk.registerFunction("api::observe", async (req) => {
|
|
89
|
+
const body = (req.body ?? {});
|
|
90
|
+
const hookType = asNonEmptyString(body["hookType"]);
|
|
91
|
+
const sessionId = asNonEmptyString(body["sessionId"]);
|
|
92
|
+
const project = asNonEmptyString(body["project"]);
|
|
93
|
+
const cwd = asNonEmptyString(body["cwd"]);
|
|
94
|
+
const timestamp = asNonEmptyString(body["timestamp"]);
|
|
95
|
+
if (!hookType || !sessionId || !project || !cwd || !timestamp) {
|
|
96
|
+
return {
|
|
97
|
+
status_code: 400,
|
|
98
|
+
body: {
|
|
99
|
+
error: "hookType, sessionId, project, cwd, and timestamp are required strings",
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const payload = {
|
|
104
|
+
hookType: hookType,
|
|
105
|
+
sessionId,
|
|
106
|
+
project,
|
|
107
|
+
cwd,
|
|
108
|
+
timestamp,
|
|
109
|
+
data: body["data"],
|
|
110
|
+
};
|
|
111
|
+
const result = await sdk.trigger({
|
|
112
|
+
function_id: "mem::observe",
|
|
113
|
+
payload,
|
|
114
|
+
});
|
|
115
|
+
return { status_code: 201, body: result };
|
|
116
|
+
});
|
|
117
|
+
sdk.registerTrigger({
|
|
118
|
+
type: "http",
|
|
119
|
+
function_id: "api::observe",
|
|
120
|
+
config: {
|
|
121
|
+
api_path: "/memwarden/observe",
|
|
122
|
+
http_method: "POST",
|
|
123
|
+
middleware_function_ids: ["middleware::api-auth"],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
// --- POST /memwarden/context ------------------------------------
|
|
127
|
+
sdk.registerFunction("api::context", async (req) => {
|
|
128
|
+
const body = (req.body ?? {});
|
|
129
|
+
const sessionId = asNonEmptyString(body["sessionId"]);
|
|
130
|
+
const project = asNonEmptyString(body["project"]);
|
|
131
|
+
if (!sessionId || !project) {
|
|
132
|
+
return {
|
|
133
|
+
status_code: 400,
|
|
134
|
+
body: { error: "sessionId and project are required strings" },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const budget = parseOptionalPositiveInt(body["budget"]);
|
|
138
|
+
if (budget === null) {
|
|
139
|
+
return {
|
|
140
|
+
status_code: 400,
|
|
141
|
+
body: { error: "budget must be a positive integer" },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const payload = {
|
|
145
|
+
sessionId,
|
|
146
|
+
project,
|
|
147
|
+
};
|
|
148
|
+
if (budget !== undefined)
|
|
149
|
+
payload.budget = budget;
|
|
150
|
+
const result = await sdk.trigger({
|
|
151
|
+
function_id: "mem::context",
|
|
152
|
+
payload,
|
|
153
|
+
});
|
|
154
|
+
return { status_code: 200, body: result };
|
|
155
|
+
});
|
|
156
|
+
sdk.registerTrigger({
|
|
157
|
+
type: "http",
|
|
158
|
+
function_id: "api::context",
|
|
159
|
+
config: {
|
|
160
|
+
api_path: "/memwarden/context",
|
|
161
|
+
http_method: "POST",
|
|
162
|
+
middleware_function_ids: ["middleware::api-auth"],
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
// --- POST /memwarden/search -------------------------------------
|
|
166
|
+
sdk.registerFunction("api::search", async (req) => {
|
|
167
|
+
const body = (req.body ?? {});
|
|
168
|
+
if (typeof body["query"] !== "string" || !body["query"].trim()) {
|
|
169
|
+
return {
|
|
170
|
+
status_code: 400,
|
|
171
|
+
body: { error: "query is required and must be a non-empty string" },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (body["limit"] !== undefined &&
|
|
175
|
+
(!Number.isInteger(body["limit"]) || body["limit"] < 1)) {
|
|
176
|
+
return {
|
|
177
|
+
status_code: 400,
|
|
178
|
+
body: { error: "limit must be a positive integer" },
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (body["project"] !== undefined && typeof body["project"] !== "string") {
|
|
182
|
+
return {
|
|
183
|
+
status_code: 400,
|
|
184
|
+
body: { error: "project must be a string" },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (body["cwd"] !== undefined && typeof body["cwd"] !== "string") {
|
|
188
|
+
return { status_code: 400, body: { error: "cwd must be a string" } };
|
|
189
|
+
}
|
|
190
|
+
if (body["format"] !== undefined &&
|
|
191
|
+
(typeof body["format"] !== "string" ||
|
|
192
|
+
!["full", "compact", "narrative"].includes(body["format"].trim().toLowerCase()))) {
|
|
193
|
+
return {
|
|
194
|
+
status_code: 400,
|
|
195
|
+
body: { error: "format must be one of: full, compact, narrative" },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (body["token_budget"] !== undefined &&
|
|
199
|
+
(!Number.isInteger(body["token_budget"]) ||
|
|
200
|
+
body["token_budget"] < 1)) {
|
|
201
|
+
return {
|
|
202
|
+
status_code: 400,
|
|
203
|
+
body: { error: "token_budget must be a positive integer" },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// Verified Recall fails closed: safe_only needs a repo root to verify
|
|
207
|
+
// against, so reject it rather than silently returning unverified memory.
|
|
208
|
+
if (body["safe_only"] === true &&
|
|
209
|
+
(typeof body["cwd"] !== "string" || !body["cwd"].trim())) {
|
|
210
|
+
return {
|
|
211
|
+
status_code: 400,
|
|
212
|
+
body: { error: "safe_only requires cwd (a repo root to verify against)" },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const payload = { query: body["query"].trim() };
|
|
216
|
+
if (body["limit"] !== undefined)
|
|
217
|
+
payload.limit = body["limit"];
|
|
218
|
+
if (body["project"] !== undefined)
|
|
219
|
+
payload.project = body["project"];
|
|
220
|
+
if (body["cwd"] !== undefined)
|
|
221
|
+
payload.cwd = body["cwd"];
|
|
222
|
+
if (typeof body["format"] === "string")
|
|
223
|
+
payload.format = body["format"].trim().toLowerCase();
|
|
224
|
+
if (body["token_budget"] !== undefined)
|
|
225
|
+
payload.token_budget = body["token_budget"];
|
|
226
|
+
if (body["safe_only"] === true)
|
|
227
|
+
payload.safe_only = true;
|
|
228
|
+
const result = await sdk.trigger({
|
|
229
|
+
function_id: "mem::search",
|
|
230
|
+
payload,
|
|
231
|
+
});
|
|
232
|
+
return { status_code: 200, body: result };
|
|
233
|
+
});
|
|
234
|
+
sdk.registerTrigger({
|
|
235
|
+
type: "http",
|
|
236
|
+
function_id: "api::search",
|
|
237
|
+
config: {
|
|
238
|
+
api_path: "/memwarden/search",
|
|
239
|
+
http_method: "POST",
|
|
240
|
+
middleware_function_ids: ["middleware::api-auth"],
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
// --- GET /memwarden/verify --------------------------------------
|
|
244
|
+
// Tamper-evidence: show the memory store's oplog hash chain is intact.
|
|
245
|
+
// The differentiating guarantee — memory whose history is tamper-evident
|
|
246
|
+
// (detects edits/reorders; not signed, so it is evidence, not proof).
|
|
247
|
+
sdk.registerFunction("api::verify", async () => {
|
|
248
|
+
const result = (await sdk.trigger({
|
|
249
|
+
function_id: "state::verify",
|
|
250
|
+
payload: {},
|
|
251
|
+
}));
|
|
252
|
+
const count = (await sdk.trigger({
|
|
253
|
+
function_id: "state::oplog-count",
|
|
254
|
+
payload: {},
|
|
255
|
+
}));
|
|
256
|
+
return {
|
|
257
|
+
status_code: result.ok ? 200 : 409,
|
|
258
|
+
body: {
|
|
259
|
+
verified: result.ok,
|
|
260
|
+
oplogEntries: count.count,
|
|
261
|
+
...(result.ok ? {} : { brokenAt: result.brokenAt }),
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
sdk.registerTrigger({
|
|
266
|
+
type: "http",
|
|
267
|
+
function_id: "api::verify",
|
|
268
|
+
config: {
|
|
269
|
+
api_path: "/memwarden/verify",
|
|
270
|
+
http_method: "GET",
|
|
271
|
+
// Auth'd when a secret is set: oplog state is private brain metadata.
|
|
272
|
+
middleware_function_ids: ["middleware::api-auth"],
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
// --- GET /memwarden/stats ---------------------------------------
|
|
276
|
+
// Live self-custody dashboard: memory counts, the active embedding
|
|
277
|
+
// provider, and the TurboQuant compression ratio.
|
|
278
|
+
sdk.registerFunction("api::stats", async () => {
|
|
279
|
+
const kv = new StateKV(sdk);
|
|
280
|
+
const [memories, sessions] = await Promise.all([
|
|
281
|
+
kv.list(KV.memories).catch(() => []),
|
|
282
|
+
kv.list(KV.sessions).catch(() => []),
|
|
283
|
+
]);
|
|
284
|
+
const provider = getEmbeddingProvider();
|
|
285
|
+
const vec = getVectorIndex();
|
|
286
|
+
const body = {
|
|
287
|
+
memories: memories.length,
|
|
288
|
+
sessions: sessions.length,
|
|
289
|
+
vectors: vec?.size ?? 0,
|
|
290
|
+
embedding: provider
|
|
291
|
+
? { provider: provider.name, dimensions: provider.dimensions }
|
|
292
|
+
: null,
|
|
293
|
+
};
|
|
294
|
+
if (vec instanceof QuantizedVectorIndex) {
|
|
295
|
+
const { dims, paddedDims, bits, rescoreDepth } = vec.params;
|
|
296
|
+
const fullBytes = dims * 4;
|
|
297
|
+
const codeBytes = Math.ceil((paddedDims * bits) / 8) + 4; // codes + norm
|
|
298
|
+
const storedBytes = codeBytes + (rescoreDepth > 0 ? fullBytes : 0);
|
|
299
|
+
body["compression"] = {
|
|
300
|
+
algorithm: "TurboQuant",
|
|
301
|
+
bits: getQuantBits(),
|
|
302
|
+
fullBytesPerVector: fullBytes,
|
|
303
|
+
storedBytesPerVector: storedBytes,
|
|
304
|
+
ratio: Math.round((fullBytes / storedBytes) * 10) / 10,
|
|
305
|
+
rescore: rescoreDepth,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
body["compression"] = null;
|
|
310
|
+
}
|
|
311
|
+
body["performance"] = metrics.snapshot();
|
|
312
|
+
return { status_code: 200, body };
|
|
313
|
+
});
|
|
314
|
+
sdk.registerTrigger({
|
|
315
|
+
type: "http",
|
|
316
|
+
function_id: "api::stats",
|
|
317
|
+
config: {
|
|
318
|
+
api_path: "/memwarden/stats",
|
|
319
|
+
http_method: "GET",
|
|
320
|
+
// Auth'd when a secret is set: stats expose memory/session counts.
|
|
321
|
+
middleware_function_ids: ["middleware::api-auth"],
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
// --- POST /memwarden/doctor -------------------------------------
|
|
325
|
+
// The memory doctor: audit stored memories for staleness and sourcing
|
|
326
|
+
// against the live repo. The differentiating "is this safe to inject?"
|
|
327
|
+
// surface.
|
|
328
|
+
sdk.registerFunction("api::doctor", async (req) => {
|
|
329
|
+
const body = (req.body ?? {});
|
|
330
|
+
const report = await sdk.trigger({
|
|
331
|
+
function_id: "mem::doctor",
|
|
332
|
+
payload: { root: body.root, project: body.project },
|
|
333
|
+
});
|
|
334
|
+
return { status_code: 200, body: report };
|
|
335
|
+
});
|
|
336
|
+
sdk.registerTrigger({
|
|
337
|
+
type: "http",
|
|
338
|
+
function_id: "api::doctor",
|
|
339
|
+
config: {
|
|
340
|
+
api_path: "/memwarden/doctor",
|
|
341
|
+
http_method: "POST",
|
|
342
|
+
middleware_function_ids: ["middleware::api-auth"],
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
// --- POST /memwarden/forget --------------------------------------
|
|
346
|
+
// User-initiated deletion with a tamper-evident receipt. Auth'd: deleting
|
|
347
|
+
// memory is as sensitive as reading it.
|
|
348
|
+
sdk.registerFunction("api::forget", async (req) => {
|
|
349
|
+
const body = (req.body ?? {});
|
|
350
|
+
const observationId = asNonEmptyString(body.observation_id) ??
|
|
351
|
+
asNonEmptyString(body.observationId);
|
|
352
|
+
if (!observationId) {
|
|
353
|
+
return { status_code: 400, body: { error: "observation_id is required" } };
|
|
354
|
+
}
|
|
355
|
+
const result = await sdk.trigger({
|
|
356
|
+
function_id: "mem::forget",
|
|
357
|
+
payload: { observationId },
|
|
358
|
+
});
|
|
359
|
+
return { status_code: 200, body: result };
|
|
360
|
+
});
|
|
361
|
+
sdk.registerTrigger({
|
|
362
|
+
type: "http",
|
|
363
|
+
function_id: "api::forget",
|
|
364
|
+
config: {
|
|
365
|
+
api_path: "/memwarden/forget",
|
|
366
|
+
http_method: "POST",
|
|
367
|
+
middleware_function_ids: ["middleware::api-auth"],
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
// --- POST /memwarden/dejafix/lookup -----------------------------
|
|
371
|
+
// Déjà Fix: surface verified fixes for an error any agent already solved.
|
|
372
|
+
// Returns only fixes whose referenced files still hash-match (Verified
|
|
373
|
+
// Recall) — a stale fix is never returned. cwd is required: it is both the
|
|
374
|
+
// project firewall (a fix learned in repo A never leaks to repo B) and the
|
|
375
|
+
// working tree the fix is verified against.
|
|
376
|
+
sdk.registerFunction("api::dejafix-lookup", async (req) => {
|
|
377
|
+
const body = (req.body ?? {});
|
|
378
|
+
const errorText = asNonEmptyString(body.error_text) ?? asNonEmptyString(body.errorText);
|
|
379
|
+
if (!errorText) {
|
|
380
|
+
return { status_code: 400, body: { error: "error_text is required" } };
|
|
381
|
+
}
|
|
382
|
+
const cwd = asNonEmptyString(body.cwd);
|
|
383
|
+
if (!cwd) {
|
|
384
|
+
return {
|
|
385
|
+
status_code: 400,
|
|
386
|
+
body: { error: "cwd is required (the repo to verify fixes against)" },
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const result = await sdk.trigger({
|
|
390
|
+
function_id: "mem::dejafix_lookup",
|
|
391
|
+
payload: { errorText, cwd },
|
|
392
|
+
});
|
|
393
|
+
return { status_code: 200, body: result };
|
|
394
|
+
});
|
|
395
|
+
sdk.registerTrigger({
|
|
396
|
+
type: "http",
|
|
397
|
+
function_id: "api::dejafix-lookup",
|
|
398
|
+
config: {
|
|
399
|
+
api_path: "/memwarden/dejafix/lookup",
|
|
400
|
+
http_method: "POST",
|
|
401
|
+
middleware_function_ids: ["middleware::api-auth"],
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
// --- POST /memwarden/dejafix/record -----------------------------
|
|
405
|
+
// Record a {error -> root cause + fix} so any agent that hits the same error
|
|
406
|
+
// later gets it back. Referenced files are hashed now so drift is detectable.
|
|
407
|
+
sdk.registerFunction("api::dejafix-record", async (req) => {
|
|
408
|
+
const body = (req.body ?? {});
|
|
409
|
+
const fix = asNonEmptyString(body["fix"]);
|
|
410
|
+
if (!fix) {
|
|
411
|
+
return { status_code: 400, body: { error: "fix is required" } };
|
|
412
|
+
}
|
|
413
|
+
const cwd = asNonEmptyString(body["cwd"]);
|
|
414
|
+
if (!cwd) {
|
|
415
|
+
return { status_code: 400, body: { error: "cwd is required" } };
|
|
416
|
+
}
|
|
417
|
+
const errorText = asNonEmptyString(body["error_text"]) ??
|
|
418
|
+
asNonEmptyString(body["errorText"]);
|
|
419
|
+
const signature = asNonEmptyString(body["signature"]);
|
|
420
|
+
if (!errorText && !signature) {
|
|
421
|
+
return {
|
|
422
|
+
status_code: 400,
|
|
423
|
+
body: { error: "error_text or signature is required" },
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const files = Array.isArray(body["files"])
|
|
427
|
+
? body["files"].filter((f) => typeof f === "string" && f.trim().length > 0)
|
|
428
|
+
: undefined;
|
|
429
|
+
const payload = { fix, cwd };
|
|
430
|
+
if (errorText)
|
|
431
|
+
payload["errorText"] = errorText;
|
|
432
|
+
if (signature)
|
|
433
|
+
payload["signature"] = signature;
|
|
434
|
+
const rootCause = asNonEmptyString(body["root_cause"]) ??
|
|
435
|
+
asNonEmptyString(body["rootCause"]);
|
|
436
|
+
if (rootCause)
|
|
437
|
+
payload["rootCause"] = rootCause;
|
|
438
|
+
if (files && files.length > 0)
|
|
439
|
+
payload["files"] = files;
|
|
440
|
+
const tool = asNonEmptyString(body["tool"]);
|
|
441
|
+
if (tool)
|
|
442
|
+
payload["tool"] = tool;
|
|
443
|
+
const sessionId = asNonEmptyString(body["session_id"]) ??
|
|
444
|
+
asNonEmptyString(body["sessionId"]);
|
|
445
|
+
if (sessionId)
|
|
446
|
+
payload["sessionId"] = sessionId;
|
|
447
|
+
const result = await sdk.trigger({
|
|
448
|
+
function_id: "mem::dejafix_record",
|
|
449
|
+
payload,
|
|
450
|
+
});
|
|
451
|
+
return { status_code: 200, body: result };
|
|
452
|
+
});
|
|
453
|
+
sdk.registerTrigger({
|
|
454
|
+
type: "http",
|
|
455
|
+
function_id: "api::dejafix-record",
|
|
456
|
+
config: {
|
|
457
|
+
api_path: "/memwarden/dejafix/record",
|
|
458
|
+
http_method: "POST",
|
|
459
|
+
middleware_function_ids: ["middleware::api-auth"],
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
// --- GET /memwarden/export --------------------------------------
|
|
463
|
+
// Portability: a self-contained Brain Bundle the user can move between
|
|
464
|
+
// machines or agents. No vendor in the loop.
|
|
465
|
+
sdk.registerFunction("api::export", async () => {
|
|
466
|
+
const bundle = await exportBundle(new StateKV(sdk));
|
|
467
|
+
return {
|
|
468
|
+
status_code: 200,
|
|
469
|
+
body: { ...bundle, exportedAt: new Date().toISOString() },
|
|
470
|
+
};
|
|
471
|
+
});
|
|
472
|
+
sdk.registerTrigger({
|
|
473
|
+
type: "http",
|
|
474
|
+
function_id: "api::export",
|
|
475
|
+
config: {
|
|
476
|
+
api_path: "/memwarden/export",
|
|
477
|
+
http_method: "GET",
|
|
478
|
+
middleware_function_ids: ["middleware::api-auth"],
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
// --- POST /memwarden/import -------------------------------------
|
|
482
|
+
sdk.registerFunction("api::import", async (req) => {
|
|
483
|
+
const body = req.body;
|
|
484
|
+
if (!isBrainBundle(body)) {
|
|
485
|
+
return {
|
|
486
|
+
status_code: 400,
|
|
487
|
+
body: { error: "body is not a valid memwarden brain bundle" },
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const counts = await importBundle(new StateKV(sdk), body);
|
|
492
|
+
return { status_code: 200, body: { imported: counts } };
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
return {
|
|
496
|
+
status_code: 400,
|
|
497
|
+
body: { error: err instanceof Error ? err.message : String(err) },
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
sdk.registerTrigger({
|
|
502
|
+
type: "http",
|
|
503
|
+
function_id: "api::import",
|
|
504
|
+
config: {
|
|
505
|
+
api_path: "/memwarden/import",
|
|
506
|
+
http_method: "POST",
|
|
507
|
+
middleware_function_ids: ["middleware::api-auth"],
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function timingSafeCompare(a: string, b: string): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Constant-time bearer-token comparison. Both inputs are HMAC'd under a
|
|
3
|
+
// random per-process key and then compared with timingSafeEqual, so the
|
|
4
|
+
// result is independent of input length and leaks no timing about how many
|
|
5
|
+
// leading characters happened to match.
|
|
6
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
7
|
+
const COMPARE_KEY = randomBytes(32);
|
|
8
|
+
function fingerprint(value) {
|
|
9
|
+
return createHmac("sha256", COMPARE_KEY).update(value).digest();
|
|
10
|
+
}
|
|
11
|
+
export function timingSafeCompare(a, b) {
|
|
12
|
+
return timingSafeEqual(fingerprint(a), fingerprint(b));
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memwarden",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "The memory firewall for AI coding agents. Verified, self-custodied, tamper-evident — one brain across every tool.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.0.0"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"memwarden": "dist/cli/bin.js",
|
|
11
|
+
"memwarden-mcp": "dist/mcp/bin.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/saiyam1814/memwarden.git"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/saiyam1814/memwarden#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/saiyam1814/memwarden/issues"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"ai",
|
|
28
|
+
"memory",
|
|
29
|
+
"agents",
|
|
30
|
+
"mcp",
|
|
31
|
+
"claude-code",
|
|
32
|
+
"verified-recall",
|
|
33
|
+
"memory-firewall",
|
|
34
|
+
"local-first"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"dev": "tsx src/index.ts",
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"prepack": "npm run build",
|
|
40
|
+
"benchmark": "tsx benchmark/recall.ts",
|
|
41
|
+
"demo:trust": "tsx demo/trust.ts",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
},
|
|
46
|
+
"license": "Apache-2.0",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@libsql/client": "^0.15.0",
|
|
49
|
+
"zod": "^4.0.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@huggingface/transformers": "^3.8.1",
|
|
53
|
+
"@types/node": "^25.9.1",
|
|
54
|
+
"tsx": "^4.19.0",
|
|
55
|
+
"typescript": "^6.0.3",
|
|
56
|
+
"vitest": "^4.1.6"
|
|
57
|
+
}
|
|
58
|
+
}
|