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.
Files changed (119) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +402 -0
  3. package/dist/bundle/bundle.d.ts +28 -0
  4. package/dist/bundle/bundle.js +85 -0
  5. package/dist/cli/bin.d.ts +2 -0
  6. package/dist/cli/bin.js +593 -0
  7. package/dist/cli/connect.d.ts +63 -0
  8. package/dist/cli/connect.js +121 -0
  9. package/dist/cli/hook.d.ts +24 -0
  10. package/dist/cli/hook.js +186 -0
  11. package/dist/cli/tools.d.ts +47 -0
  12. package/dist/cli/tools.js +246 -0
  13. package/dist/daemon/ensure.d.ts +12 -0
  14. package/dist/daemon/ensure.js +54 -0
  15. package/dist/daemon/service.d.ts +15 -0
  16. package/dist/daemon/service.js +210 -0
  17. package/dist/embedding/index.d.ts +10 -0
  18. package/dist/embedding/index.js +33 -0
  19. package/dist/embedding/local-embedding.d.ts +14 -0
  20. package/dist/embedding/local-embedding.js +80 -0
  21. package/dist/functions/access-tracker.d.ts +13 -0
  22. package/dist/functions/access-tracker.js +92 -0
  23. package/dist/functions/audit.d.ts +46 -0
  24. package/dist/functions/audit.js +0 -0
  25. package/dist/functions/cjk-segmenter.d.ts +6 -0
  26. package/dist/functions/cjk-segmenter.js +120 -0
  27. package/dist/functions/compress-synthetic.d.ts +2 -0
  28. package/dist/functions/compress-synthetic.js +104 -0
  29. package/dist/functions/config.d.ts +68 -0
  30. package/dist/functions/config.js +231 -0
  31. package/dist/functions/conflicts.d.ts +19 -0
  32. package/dist/functions/conflicts.js +328 -0
  33. package/dist/functions/context.d.ts +3 -0
  34. package/dist/functions/context.js +155 -0
  35. package/dist/functions/dedup.d.ts +11 -0
  36. package/dist/functions/dedup.js +51 -0
  37. package/dist/functions/dejafix.d.ts +96 -0
  38. package/dist/functions/dejafix.js +356 -0
  39. package/dist/functions/doctor.d.ts +29 -0
  40. package/dist/functions/doctor.js +137 -0
  41. package/dist/functions/forget.d.ts +3 -0
  42. package/dist/functions/forget.js +87 -0
  43. package/dist/functions/hybrid-search.d.ts +17 -0
  44. package/dist/functions/hybrid-search.js +205 -0
  45. package/dist/functions/index.d.ts +32 -0
  46. package/dist/functions/index.js +44 -0
  47. package/dist/functions/keyed-mutex.d.ts +1 -0
  48. package/dist/functions/keyed-mutex.js +21 -0
  49. package/dist/functions/logger.d.ts +6 -0
  50. package/dist/functions/logger.js +37 -0
  51. package/dist/functions/memory-utils.d.ts +2 -0
  52. package/dist/functions/memory-utils.js +29 -0
  53. package/dist/functions/observe.d.ts +5 -0
  54. package/dist/functions/observe.js +326 -0
  55. package/dist/functions/paths.d.ts +1 -0
  56. package/dist/functions/paths.js +38 -0
  57. package/dist/functions/privacy.d.ts +1 -0
  58. package/dist/functions/privacy.js +30 -0
  59. package/dist/functions/provenance.d.ts +9 -0
  60. package/dist/functions/provenance.js +57 -0
  61. package/dist/functions/quantized-vector-index.d.ts +60 -0
  62. package/dist/functions/quantized-vector-index.js +275 -0
  63. package/dist/functions/receipt.d.ts +31 -0
  64. package/dist/functions/receipt.js +95 -0
  65. package/dist/functions/search-index.d.ts +27 -0
  66. package/dist/functions/search-index.js +217 -0
  67. package/dist/functions/search.d.ts +25 -0
  68. package/dist/functions/search.js +523 -0
  69. package/dist/functions/stemmer.d.ts +1 -0
  70. package/dist/functions/stemmer.js +110 -0
  71. package/dist/functions/synonyms.d.ts +1 -0
  72. package/dist/functions/synonyms.js +69 -0
  73. package/dist/functions/turboquant.d.ts +53 -0
  74. package/dist/functions/turboquant.js +278 -0
  75. package/dist/functions/types.d.ts +217 -0
  76. package/dist/functions/types.js +8 -0
  77. package/dist/functions/vector-index.d.ts +25 -0
  78. package/dist/functions/vector-index.js +125 -0
  79. package/dist/functions/vector-persistence.d.ts +14 -0
  80. package/dist/functions/vector-persistence.js +75 -0
  81. package/dist/functions/verify.d.ts +13 -0
  82. package/dist/functions/verify.js +104 -0
  83. package/dist/index.d.ts +1 -0
  84. package/dist/index.js +219 -0
  85. package/dist/kernel/http.d.ts +24 -0
  86. package/dist/kernel/http.js +261 -0
  87. package/dist/kernel/index.d.ts +19 -0
  88. package/dist/kernel/index.js +21 -0
  89. package/dist/kernel/kernel.d.ts +80 -0
  90. package/dist/kernel/kernel.js +297 -0
  91. package/dist/kernel/pubsub.d.ts +21 -0
  92. package/dist/kernel/pubsub.js +38 -0
  93. package/dist/kernel/types.d.ts +139 -0
  94. package/dist/kernel/types.js +20 -0
  95. package/dist/mcp/bin.d.ts +2 -0
  96. package/dist/mcp/bin.js +27 -0
  97. package/dist/mcp/server.d.ts +34 -0
  98. package/dist/mcp/server.js +377 -0
  99. package/dist/observability/metrics.d.ts +26 -0
  100. package/dist/observability/metrics.js +104 -0
  101. package/dist/proxy/server.d.ts +30 -0
  102. package/dist/proxy/server.js +331 -0
  103. package/dist/state/kv.d.ts +41 -0
  104. package/dist/state/kv.js +50 -0
  105. package/dist/state/oplog.d.ts +25 -0
  106. package/dist/state/oplog.js +57 -0
  107. package/dist/state/schema.d.ts +60 -0
  108. package/dist/state/schema.js +88 -0
  109. package/dist/state/store-libsql.d.ts +46 -0
  110. package/dist/state/store-libsql.js +263 -0
  111. package/dist/state/store-memory.d.ts +23 -0
  112. package/dist/state/store-memory.js +121 -0
  113. package/dist/state/store.d.ts +87 -0
  114. package/dist/state/store.js +58 -0
  115. package/dist/triggers/api.d.ts +14 -0
  116. package/dist/triggers/api.js +510 -0
  117. package/dist/triggers/auth.d.ts +1 -0
  118. package/dist/triggers/auth.js +13 -0
  119. 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
+ }