@prmichaelsen/remember-mcp 3.14.10 → 3.14.12

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.
@@ -1,197 +0,0 @@
1
- /**
2
- * Trust enforcement service — 3 configurable modes for cross-user memory access.
3
- *
4
- * - query mode (default): memories above trust threshold never returned from Weaviate
5
- * - prompt mode: all memories returned, formatted/redacted by trust level
6
- * - hybrid mode: query filter for trust 0.0, prompt filter for rest
7
- *
8
- * See agent/design/local.ghost-persona-system.md
9
- */
10
-
11
- import type { Memory } from '../types/memory.js';
12
- import type { TrustEnforcementMode } from '../types/ghost-config.js';
13
-
14
- // ─── Trust Level Thresholds ────────────────────────────────────────────────
15
-
16
- /** Trust level thresholds mapping continuous 0-1 values to discrete behavior tiers */
17
- export const TRUST_THRESHOLDS = {
18
- FULL_ACCESS: 1.0,
19
- PARTIAL_ACCESS: 0.75,
20
- SUMMARY_ONLY: 0.5,
21
- METADATA_ONLY: 0.25,
22
- EXISTENCE_ONLY: 0.0,
23
- } as const;
24
-
25
- // ─── Query-Level Enforcement ───────────────────────────────────────────────
26
-
27
- /**
28
- * Build a Weaviate filter that restricts memories by trust score.
29
- * Only returns memories where trust_score <= accessorTrustLevel.
30
- *
31
- * Trust 1.0 memories require accessor trust >= 1.0 to even appear in results.
32
- * When they do appear, formatMemoryForPrompt caps output to existence-only.
33
- *
34
- * @param collection - Weaviate collection instance
35
- * @param accessorTrustLevel - The accessor's trust level (0-1)
36
- * @returns Weaviate filter object
37
- */
38
- export function buildTrustFilter(collection: any, accessorTrustLevel: number): any {
39
- return collection.filter.byProperty('trust_score').lessThanOrEqual(accessorTrustLevel);
40
- }
41
-
42
- // ─── Prompt-Level Enforcement ──────────────────────────────────────────────
43
-
44
- /**
45
- * Formatted memory representation for prompt-level enforcement.
46
- * Content is redacted/formatted based on trust level.
47
- */
48
- export interface FormattedMemory {
49
- memory_id: string;
50
- trust_tier: string;
51
- content: string;
52
- }
53
-
54
- /**
55
- * Format a memory for inclusion in an LLM prompt, redacted by trust level.
56
- *
57
- * Trust tiers:
58
- * - 1.0 Full Access: full content, all details
59
- * - 0.75 Partial Access: content with sensitive fields redacted
60
- * - 0.5 Summary Only: title + summary, no content
61
- * - 0.25 Metadata Only: type, date, tags — no content or summary
62
- * - 0.0 Existence Only: "A memory exists about this topic"
63
- *
64
- * Trust 1.0 memories are always existence-only for cross-users, regardless of
65
- * accessor trust level. Use `isSelfAccess = true` to bypass for owner access.
66
- *
67
- * @param memory - The memory to format
68
- * @param accessorTrustLevel - The accessor's trust level (0-1)
69
- * @param isSelfAccess - True if the accessor is the memory owner (bypasses trust 1.0 cap)
70
- * @returns Formatted memory for prompt inclusion
71
- */
72
- export function formatMemoryForPrompt(memory: Memory, accessorTrustLevel: number, isSelfAccess = false): FormattedMemory {
73
- // Trust 1.0 = existence-only for cross-users (acknowledged but never revealed)
74
- if (!isSelfAccess && memory.trust >= 1.0) {
75
- return {
76
- memory_id: memory.id,
77
- trust_tier: 'Existence Only',
78
- content: 'A memory exists about this topic.',
79
- };
80
- }
81
-
82
- const tier = getTrustLevelLabel(accessorTrustLevel);
83
-
84
- if (accessorTrustLevel >= TRUST_THRESHOLDS.FULL_ACCESS) {
85
- // Full Access — all content
86
- const parts = [`[${memory.type}] ${memory.title || 'Untitled'}`];
87
- parts.push(memory.content);
88
- if (memory.summary) parts.push(`Summary: ${memory.summary}`);
89
- if (memory.tags.length > 0) parts.push(`Tags: ${memory.tags.join(', ')}`);
90
- if (memory.created_at) parts.push(`Created: ${memory.created_at}`);
91
- return { memory_id: memory.id, trust_tier: tier, content: parts.join('\n') };
92
- }
93
-
94
- if (accessorTrustLevel >= TRUST_THRESHOLDS.PARTIAL_ACCESS) {
95
- // Partial Access — redact sensitive fields
96
- const redacted = redactSensitiveFields(memory, accessorTrustLevel);
97
- const parts = [`[${redacted.type}] ${redacted.title || 'Untitled'}`];
98
- parts.push(redacted.content);
99
- if (redacted.tags.length > 0) parts.push(`Tags: ${redacted.tags.join(', ')}`);
100
- return { memory_id: memory.id, trust_tier: tier, content: parts.join('\n') };
101
- }
102
-
103
- if (accessorTrustLevel >= TRUST_THRESHOLDS.SUMMARY_ONLY) {
104
- // Summary Only — title + summary, no content body
105
- const parts = [`[${memory.type}] ${memory.title || 'Untitled'}`];
106
- if (memory.summary) {
107
- parts.push(memory.summary);
108
- } else {
109
- parts.push('(No summary available)');
110
- }
111
- return { memory_id: memory.id, trust_tier: tier, content: parts.join('\n') };
112
- }
113
-
114
- if (accessorTrustLevel >= TRUST_THRESHOLDS.METADATA_ONLY) {
115
- // Metadata Only — type, date, tags
116
- const parts = [`[${memory.type}]`];
117
- if (memory.created_at) parts.push(`Created: ${memory.created_at}`);
118
- if (memory.tags.length > 0) parts.push(`Tags: ${memory.tags.join(', ')}`);
119
- return { memory_id: memory.id, trust_tier: tier, content: parts.join('\n') };
120
- }
121
-
122
- // Existence Only — minimal hint
123
- return {
124
- memory_id: memory.id,
125
- trust_tier: tier,
126
- content: 'A memory exists about this topic.',
127
- };
128
- }
129
-
130
- // ─── Shared Utilities ──────────────────────────────────────────────────────
131
-
132
- /**
133
- * Get a human-readable label for a trust level.
134
- */
135
- export function getTrustLevelLabel(trust: number): string {
136
- if (trust >= TRUST_THRESHOLDS.FULL_ACCESS) return 'Full Access';
137
- if (trust >= TRUST_THRESHOLDS.PARTIAL_ACCESS) return 'Partial Access';
138
- if (trust >= TRUST_THRESHOLDS.SUMMARY_ONLY) return 'Summary Only';
139
- if (trust >= TRUST_THRESHOLDS.METADATA_ONLY) return 'Metadata Only';
140
- return 'Existence Only';
141
- }
142
-
143
- /**
144
- * Get LLM instruction text describing what to reveal at a given trust level.
145
- */
146
- export function getTrustInstructions(trust: number): string {
147
- if (trust >= TRUST_THRESHOLDS.FULL_ACCESS) {
148
- return 'You have full access to this memory. Share all content and details freely.';
149
- }
150
- if (trust >= TRUST_THRESHOLDS.PARTIAL_ACCESS) {
151
- return 'You have partial access. Share the main content but do not reveal sensitive personal details like exact locations, contact information, or financial data.';
152
- }
153
- if (trust >= TRUST_THRESHOLDS.SUMMARY_ONLY) {
154
- return 'You have summary-level access. Share the title and summary only. Do not reveal the full content of this memory.';
155
- }
156
- if (trust >= TRUST_THRESHOLDS.METADATA_ONLY) {
157
- return 'You have metadata-level access only. You may mention the type, date, and tags, but do not reveal any content or summary.';
158
- }
159
- return 'You may only acknowledge that a memory exists about this topic. Do not reveal any details.';
160
- }
161
-
162
- /**
163
- * Redact sensitive fields from a memory for partial access.
164
- * Returns a copy with location, context, and references cleared.
165
- */
166
- export function redactSensitiveFields(memory: Memory, _trust: number): Memory {
167
- return {
168
- ...memory,
169
- // Clear sensitive location data
170
- location: { gps: null, address: null, source: 'unavailable', confidence: 0, is_approximate: true },
171
- // Strip context details
172
- context: {
173
- ...memory.context,
174
- participants: undefined,
175
- environment: undefined,
176
- notes: undefined,
177
- },
178
- // Clear references (may contain private URLs)
179
- references: undefined,
180
- };
181
- }
182
-
183
- /**
184
- * Check whether an accessor's trust level is sufficient for a memory.
185
- * Access is granted when accessorTrust >= memoryTrust.
186
- */
187
- export function isTrustSufficient(memoryTrust: number, accessorTrust: number): boolean {
188
- return accessorTrust >= memoryTrust;
189
- }
190
-
191
- /**
192
- * Determine the enforcement mode to use.
193
- * Convenience function that returns the mode from GhostConfig or falls back to 'query'.
194
- */
195
- export function resolveEnforcementMode(mode?: TrustEnforcementMode): TrustEnforcementMode {
196
- return mode ?? 'query';
197
- }
@@ -1,312 +0,0 @@
1
- /**
2
- * Unit tests for Weaviate v3 filter builders
3
- *
4
- * Note: These tests verify filter builder logic, not exact Weaviate filter structure.
5
- * The actual Filters.and() and Filters.or() methods return Weaviate internal objects.
6
- */
7
-
8
- import {
9
- buildCombinedSearchFilters,
10
- buildMemoryOnlyFilters,
11
- buildRelationshipOnlyFilters,
12
- hasFilters,
13
- } from './weaviate-filters.js';
14
- import type { SearchFilters } from '../types/memory.js';
15
-
16
- /**
17
- * Mock Weaviate collection with filter builder
18
- */
19
- function createMockCollection() {
20
- const mockFilter = {
21
- byProperty: (property: string) => ({
22
- equal: (value: any) => ({ property, operator: 'equal', value }),
23
- greaterThanOrEqual: (value: any) => ({ property, operator: 'gte', value }),
24
- lessThanOrEqual: (value: any) => ({ property, operator: 'lte', value }),
25
- containsAny: (values: any[]) => ({ property, operator: 'containsAny', values }),
26
- }),
27
- };
28
-
29
- return {
30
- filter: mockFilter,
31
- };
32
- }
33
-
34
- describe('weaviate-filters', () => {
35
- let mockCollection: any;
36
-
37
- beforeEach(() => {
38
- mockCollection = createMockCollection();
39
- });
40
-
41
- describe('buildMemoryOnlyFilters', () => {
42
- it('should build filter with only doc_type when no other filters provided', () => {
43
- const result = buildMemoryOnlyFilters(mockCollection);
44
- expect(result).toBeDefined();
45
- expect(result.property).toBe('doc_type');
46
- expect(result.value).toBe('memory');
47
- });
48
-
49
- it('should build filter with doc_type and content type', () => {
50
- const filters: SearchFilters = {
51
- types: ['note'],
52
- };
53
-
54
- const result = buildMemoryOnlyFilters(mockCollection, filters);
55
- expect(result).toBeDefined();
56
- // Filters.and() returns Weaviate internal structure
57
- // Just verify it's defined and has the filters property
58
- expect(result.filters || result.operands).toBeDefined();
59
- });
60
-
61
- it('should build filter with weight_min', () => {
62
- const filters: SearchFilters = {
63
- weight_min: 0.5,
64
- };
65
-
66
- const result = buildMemoryOnlyFilters(mockCollection, filters);
67
- expect(result).toBeDefined();
68
- });
69
-
70
- it('should build filter with trust and date filters', () => {
71
- const filters: SearchFilters = {
72
- trust_min: 0.3,
73
- date_from: '2024-01-01',
74
- };
75
-
76
- const result = buildMemoryOnlyFilters(mockCollection, filters);
77
- expect(result).toBeDefined();
78
- });
79
-
80
- it('should build filter with tags', () => {
81
- const filters: SearchFilters = {
82
- tags: ['work', 'important'],
83
- };
84
-
85
- const result = buildMemoryOnlyFilters(mockCollection, filters);
86
- expect(result).toBeDefined();
87
- });
88
-
89
- it('should build complex filter with multiple criteria', () => {
90
- const filters: SearchFilters = {
91
- types: ['note', 'todo'],
92
- weight_min: 0.5,
93
- trust_min: 0.3,
94
- date_from: '2024-01-01',
95
- tags: ['work'],
96
- };
97
-
98
- const result = buildMemoryOnlyFilters(mockCollection, filters);
99
- expect(result).toBeDefined();
100
- });
101
- });
102
-
103
- describe('buildRelationshipOnlyFilters', () => {
104
- it('should build filter with only doc_type', () => {
105
- const result = buildRelationshipOnlyFilters(mockCollection);
106
- expect(result).toBeDefined();
107
- expect(result.property).toBe('doc_type');
108
- expect(result.value).toBe('relationship');
109
- });
110
-
111
- it('should NOT include type filter for relationships', () => {
112
- const filters: SearchFilters = {
113
- types: ['note'],
114
- };
115
-
116
- const result = buildRelationshipOnlyFilters(mockCollection, filters);
117
- expect(result).toBeDefined();
118
- expect(result.property).toBe('doc_type');
119
- });
120
-
121
- it('should build filter with weight and trust', () => {
122
- const filters: SearchFilters = {
123
- weight_min: 0.5,
124
- trust_min: 0.3,
125
- };
126
-
127
- const result = buildRelationshipOnlyFilters(mockCollection, filters);
128
- expect(result).toBeDefined();
129
- });
130
-
131
- it('should build filter with date and tags', () => {
132
- const filters: SearchFilters = {
133
- date_from: '2024-01-01',
134
- tags: ['important'],
135
- };
136
-
137
- const result = buildRelationshipOnlyFilters(mockCollection, filters);
138
- expect(result).toBeDefined();
139
- });
140
- });
141
-
142
- describe('buildCombinedSearchFilters', () => {
143
- it('should combine memory and relationship filters', () => {
144
- const result = buildCombinedSearchFilters(mockCollection);
145
- expect(result).toBeDefined();
146
- // Filters.or() returns Weaviate internal structure
147
- expect(result.filters || result.operands).toBeDefined();
148
- });
149
-
150
- it('should handle filters that apply to both types', () => {
151
- const filters: SearchFilters = {
152
- weight_min: 0.5,
153
- trust_min: 0.3,
154
- };
155
-
156
- const result = buildCombinedSearchFilters(mockCollection, filters);
157
- expect(result).toBeDefined();
158
- });
159
-
160
- it('should handle type filter (only for memories)', () => {
161
- const filters: SearchFilters = {
162
- types: ['note'],
163
- weight_min: 0.5,
164
- };
165
-
166
- const result = buildCombinedSearchFilters(mockCollection, filters);
167
- expect(result).toBeDefined();
168
- });
169
-
170
- it('should handle complex filters', () => {
171
- const filters: SearchFilters = {
172
- types: ['note'],
173
- weight_min: 0.5,
174
- weight_max: 0.9,
175
- trust_min: 0.3,
176
- date_from: '2024-01-01',
177
- date_to: '2024-12-31',
178
- tags: ['work', 'important'],
179
- };
180
-
181
- const result = buildCombinedSearchFilters(mockCollection, filters);
182
- expect(result).toBeDefined();
183
- });
184
- });
185
-
186
- describe('hasFilters', () => {
187
- it('should return true for defined filter', () => {
188
- const filter = { property: 'doc_type', operator: 'equal', value: 'memory' };
189
- expect(hasFilters(filter)).toBe(true);
190
- });
191
-
192
- it('should return false for undefined', () => {
193
- expect(hasFilters(undefined)).toBe(false);
194
- });
195
-
196
- it('should return false for null', () => {
197
- expect(hasFilters(null)).toBe(false);
198
- });
199
-
200
- it('should return true for empty object', () => {
201
- expect(hasFilters({})).toBe(true);
202
- });
203
-
204
- it('should return true for complex filter', () => {
205
- const filter = {
206
- filters: [
207
- { property: 'doc_type', operator: 'equal', value: 'memory' },
208
- { property: 'weight', operator: 'gte', value: 0.5 },
209
- ],
210
- };
211
- expect(hasFilters(filter)).toBe(true);
212
- });
213
- });
214
-
215
- describe('edge cases', () => {
216
- it('should handle empty types array', () => {
217
- const filters: SearchFilters = {
218
- types: [],
219
- };
220
-
221
- const result = buildMemoryOnlyFilters(mockCollection, filters);
222
- expect(result).toBeDefined();
223
- expect(result.property).toBe('doc_type');
224
- });
225
-
226
- it('should handle empty tags array', () => {
227
- const filters: SearchFilters = {
228
- tags: [],
229
- };
230
-
231
- const result = buildMemoryOnlyFilters(mockCollection, filters);
232
- expect(result).toBeDefined();
233
- });
234
-
235
- it('should handle weight_min of 0', () => {
236
- const filters: SearchFilters = {
237
- weight_min: 0,
238
- };
239
-
240
- const result = buildMemoryOnlyFilters(mockCollection, filters);
241
- expect(result).toBeDefined();
242
- });
243
-
244
- it('should handle trust_min of 0', () => {
245
- const filters: SearchFilters = {
246
- trust_min: 0,
247
- };
248
-
249
- const result = buildMemoryOnlyFilters(mockCollection, filters);
250
- expect(result).toBeDefined();
251
- });
252
-
253
- it('should handle weight range', () => {
254
- const filters: SearchFilters = {
255
- weight_min: 0.3,
256
- weight_max: 0.7,
257
- };
258
-
259
- const result = buildMemoryOnlyFilters(mockCollection, filters);
260
- expect(result).toBeDefined();
261
- });
262
-
263
- it('should handle trust range', () => {
264
- const filters: SearchFilters = {
265
- trust_min: 0.2,
266
- trust_max: 0.8,
267
- };
268
-
269
- const result = buildMemoryOnlyFilters(mockCollection, filters);
270
- expect(result).toBeDefined();
271
- });
272
- });
273
-
274
- describe('undefined/null filter handling', () => {
275
- it('should not create Or operator with empty operands', () => {
276
- const emptyCollection = {
277
- filter: {
278
- byProperty: () => ({
279
- equal: () => undefined,
280
- greaterThanOrEqual: () => undefined,
281
- lessThanOrEqual: () => undefined,
282
- containsAny: () => undefined,
283
- }),
284
- },
285
- };
286
-
287
- const result = buildCombinedSearchFilters(emptyCollection);
288
- expect(result).toBeUndefined();
289
- });
290
-
291
- it('should not create And operator with empty operands', () => {
292
- const emptyCollection = {
293
- filter: {
294
- byProperty: () => ({
295
- equal: () => undefined,
296
- }),
297
- },
298
- };
299
-
300
- const result = buildMemoryOnlyFilters(emptyCollection);
301
- expect(result).toBeUndefined();
302
- });
303
-
304
- it('should handle mixed valid and undefined filters', () => {
305
- const result = buildCombinedSearchFilters(mockCollection, {
306
- types: ['note'],
307
- });
308
-
309
- expect(result).toBeDefined();
310
- });
311
- });
312
- });