@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.
- package/dist/server-factory.js +3306 -3017
- package/dist/server.js +9 -121
- package/package.json +2 -3
- package/src/services/access-control.ts +1 -1
- package/src/tools/query-memory.ts +1 -2
- package/src/tools/search-memory.ts +1 -2
- package/dist/services/trust-enforcement.d.ts +0 -83
- package/dist/services/trust-enforcement.spec.d.ts +0 -2
- package/dist/utils/weaviate-filters.d.ts +0 -56
- package/dist/utils/weaviate-filters.spec.d.ts +0 -8
- package/src/services/trust-enforcement.spec.ts +0 -309
- package/src/services/trust-enforcement.ts +0 -197
- package/src/utils/weaviate-filters.spec.ts +0 -312
- package/src/utils/weaviate-filters.ts +0 -236
package/dist/server.js
CHANGED
|
@@ -3815,118 +3815,6 @@ async function handleCreateMemory(args, userId, authContext, context) {
|
|
|
3815
3815
|
|
|
3816
3816
|
// src/tools/search-memory.ts
|
|
3817
3817
|
init_logger();
|
|
3818
|
-
|
|
3819
|
-
// src/utils/weaviate-filters.ts
|
|
3820
|
-
import { Filters as Filters6 } from "weaviate-client";
|
|
3821
|
-
function buildCombinedSearchFilters2(collection, filters) {
|
|
3822
|
-
const memoryFilters = buildDocTypeFilters2(collection, "memory", filters);
|
|
3823
|
-
const relationshipFilters = buildDocTypeFilters2(collection, "relationship", filters);
|
|
3824
|
-
const validFilters = [memoryFilters, relationshipFilters].filter((f) => f !== void 0 && f !== null);
|
|
3825
|
-
if (validFilters.length === 0) {
|
|
3826
|
-
return void 0;
|
|
3827
|
-
} else if (validFilters.length === 1) {
|
|
3828
|
-
return validFilters[0];
|
|
3829
|
-
} else {
|
|
3830
|
-
return combineFiltersWithOr2(validFilters);
|
|
3831
|
-
}
|
|
3832
|
-
}
|
|
3833
|
-
function buildDocTypeFilters2(collection, docType, filters) {
|
|
3834
|
-
const filterList = [];
|
|
3835
|
-
filterList.push(
|
|
3836
|
-
collection.filter.byProperty("doc_type").equal(docType)
|
|
3837
|
-
);
|
|
3838
|
-
if (docType === "memory" && filters?.types && filters.types.length > 0) {
|
|
3839
|
-
if (filters.types.length === 1) {
|
|
3840
|
-
filterList.push(
|
|
3841
|
-
collection.filter.byProperty("content_type").equal(filters.types[0])
|
|
3842
|
-
);
|
|
3843
|
-
} else {
|
|
3844
|
-
filterList.push(
|
|
3845
|
-
collection.filter.byProperty("content_type").containsAny(filters.types)
|
|
3846
|
-
);
|
|
3847
|
-
}
|
|
3848
|
-
}
|
|
3849
|
-
if (filters?.weight_min !== void 0) {
|
|
3850
|
-
filterList.push(
|
|
3851
|
-
collection.filter.byProperty("weight").greaterThanOrEqual(filters.weight_min)
|
|
3852
|
-
);
|
|
3853
|
-
}
|
|
3854
|
-
if (filters?.weight_max !== void 0) {
|
|
3855
|
-
filterList.push(
|
|
3856
|
-
collection.filter.byProperty("weight").lessThanOrEqual(filters.weight_max)
|
|
3857
|
-
);
|
|
3858
|
-
}
|
|
3859
|
-
if (filters?.trust_min !== void 0) {
|
|
3860
|
-
filterList.push(
|
|
3861
|
-
collection.filter.byProperty("trust_score").greaterThanOrEqual(filters.trust_min)
|
|
3862
|
-
);
|
|
3863
|
-
}
|
|
3864
|
-
if (filters?.trust_max !== void 0) {
|
|
3865
|
-
filterList.push(
|
|
3866
|
-
collection.filter.byProperty("trust_score").lessThanOrEqual(filters.trust_max)
|
|
3867
|
-
);
|
|
3868
|
-
}
|
|
3869
|
-
if (filters?.date_from) {
|
|
3870
|
-
filterList.push(
|
|
3871
|
-
collection.filter.byProperty("created_at").greaterThanOrEqual(new Date(filters.date_from))
|
|
3872
|
-
);
|
|
3873
|
-
}
|
|
3874
|
-
if (filters?.date_to) {
|
|
3875
|
-
filterList.push(
|
|
3876
|
-
collection.filter.byProperty("created_at").lessThanOrEqual(new Date(filters.date_to))
|
|
3877
|
-
);
|
|
3878
|
-
}
|
|
3879
|
-
if (filters?.tags && filters.tags.length > 0) {
|
|
3880
|
-
if (filters.tags.length === 1) {
|
|
3881
|
-
filterList.push(
|
|
3882
|
-
collection.filter.byProperty("tags").containsAny([filters.tags[0]])
|
|
3883
|
-
);
|
|
3884
|
-
} else {
|
|
3885
|
-
filterList.push(
|
|
3886
|
-
collection.filter.byProperty("tags").containsAny(filters.tags)
|
|
3887
|
-
);
|
|
3888
|
-
}
|
|
3889
|
-
}
|
|
3890
|
-
return combineFiltersWithAnd2(filterList);
|
|
3891
|
-
}
|
|
3892
|
-
function buildMemoryOnlyFilters2(collection, filters) {
|
|
3893
|
-
return buildDocTypeFilters2(collection, "memory", filters);
|
|
3894
|
-
}
|
|
3895
|
-
function combineFiltersWithAnd2(filters) {
|
|
3896
|
-
const validFilters = filters.filter((f) => f !== void 0 && f !== null);
|
|
3897
|
-
if (validFilters.length === 0) {
|
|
3898
|
-
return void 0;
|
|
3899
|
-
}
|
|
3900
|
-
if (validFilters.length === 1) {
|
|
3901
|
-
return validFilters[0];
|
|
3902
|
-
}
|
|
3903
|
-
return Filters6.and(...validFilters);
|
|
3904
|
-
}
|
|
3905
|
-
function combineFiltersWithOr2(filters) {
|
|
3906
|
-
const validFilters = filters.filter((f) => f !== void 0 && f !== null);
|
|
3907
|
-
if (validFilters.length === 0) {
|
|
3908
|
-
return void 0;
|
|
3909
|
-
}
|
|
3910
|
-
if (validFilters.length === 1) {
|
|
3911
|
-
return validFilters[0];
|
|
3912
|
-
}
|
|
3913
|
-
return Filters6.or(...validFilters);
|
|
3914
|
-
}
|
|
3915
|
-
function buildDeletedFilter2(collection, deletedFilter = "exclude") {
|
|
3916
|
-
if (deletedFilter === "exclude") {
|
|
3917
|
-
return collection.filter.byProperty("deleted_at").isNull(true);
|
|
3918
|
-
} else if (deletedFilter === "only") {
|
|
3919
|
-
return collection.filter.byProperty("deleted_at").isNull(false);
|
|
3920
|
-
}
|
|
3921
|
-
return null;
|
|
3922
|
-
}
|
|
3923
|
-
|
|
3924
|
-
// src/services/trust-enforcement.ts
|
|
3925
|
-
function buildTrustFilter2(collection, accessorTrustLevel) {
|
|
3926
|
-
return collection.filter.byProperty("trust_score").lessThanOrEqual(accessorTrustLevel);
|
|
3927
|
-
}
|
|
3928
|
-
|
|
3929
|
-
// src/tools/search-memory.ts
|
|
3930
3818
|
var searchMemoryTool = {
|
|
3931
3819
|
name: "remember_search_memory",
|
|
3932
3820
|
description: `Search memories AND relationships using hybrid semantic and keyword search.
|
|
@@ -4057,12 +3945,12 @@ async function handleSearchMemory(args, userId, authContext) {
|
|
|
4057
3945
|
const alpha = args.alpha ?? 0.7;
|
|
4058
3946
|
const limit = args.limit ?? 10;
|
|
4059
3947
|
const offset = args.offset ?? 0;
|
|
4060
|
-
const deletedFilter =
|
|
4061
|
-
const trustFilter = ghostMode ?
|
|
4062
|
-
const searchFilters = includeRelationships ?
|
|
3948
|
+
const deletedFilter = buildDeletedFilter(collection, args.deleted_filter || "exclude");
|
|
3949
|
+
const trustFilter = ghostMode ? buildTrustFilter(collection, ghostMode.accessor_trust_level) : null;
|
|
3950
|
+
const searchFilters = includeRelationships ? buildCombinedSearchFilters(collection, args.filters) : buildMemoryOnlyFilters(collection, args.filters);
|
|
4063
3951
|
const hasExplicitTypeFilter = args.filters?.types && args.filters.types.length > 0;
|
|
4064
3952
|
const ghostExclusionFilter = !hasExplicitTypeFilter ? collection.filter.byProperty("content_type").notEqual("ghost") : null;
|
|
4065
|
-
const combinedFilters =
|
|
3953
|
+
const combinedFilters = combineFiltersWithAnd([deletedFilter, trustFilter, ghostExclusionFilter, searchFilters].filter((f) => f !== null));
|
|
4066
3954
|
const searchOptions = {
|
|
4067
3955
|
alpha,
|
|
4068
3956
|
limit: limit + offset
|
|
@@ -4524,12 +4412,12 @@ async function handleQueryMemory(args, userId, authContext) {
|
|
|
4524
4412
|
const minRelevance = args.min_relevance ?? 0.6;
|
|
4525
4413
|
const includeContext = args.include_context ?? true;
|
|
4526
4414
|
const format = args.format ?? "detailed";
|
|
4527
|
-
const deletedFilter =
|
|
4528
|
-
const trustFilter = ghostMode ?
|
|
4529
|
-
const searchFilters =
|
|
4415
|
+
const deletedFilter = buildDeletedFilter(collection, args.deleted_filter || "exclude");
|
|
4416
|
+
const trustFilter = ghostMode ? buildTrustFilter(collection, ghostMode.accessor_trust_level) : null;
|
|
4417
|
+
const searchFilters = buildCombinedSearchFilters(collection, args.filters);
|
|
4530
4418
|
const hasExplicitTypeFilter = args.filters?.types && args.filters.types.length > 0;
|
|
4531
4419
|
const ghostExclusionFilter = !hasExplicitTypeFilter ? collection.filter.byProperty("content_type").notEqual("ghost") : null;
|
|
4532
|
-
const combinedFilters =
|
|
4420
|
+
const combinedFilters = combineFiltersWithAnd([deletedFilter, trustFilter, ghostExclusionFilter, searchFilters].filter((f) => f !== null));
|
|
4533
4421
|
const searchOptions = {
|
|
4534
4422
|
limit,
|
|
4535
4423
|
distance: 1 - minRelevance,
|
|
@@ -5641,7 +5529,7 @@ async function handleDeny(args, userId, authContext) {
|
|
|
5641
5529
|
}
|
|
5642
5530
|
|
|
5643
5531
|
// src/tools/search-space.ts
|
|
5644
|
-
import { Filters as
|
|
5532
|
+
import { Filters as Filters6 } from "weaviate-client";
|
|
5645
5533
|
var searchSpaceTool = {
|
|
5646
5534
|
name: "remember_search_space",
|
|
5647
5535
|
description: `Search shared spaces and/or groups to discover memories from other users.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prmichaelsen/remember-mcp",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.12",
|
|
4
4
|
"description": "Multi-tenant memory system MCP server with vector search and relationships",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -50,8 +50,7 @@
|
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
51
51
|
"@prmichaelsen/firebase-admin-sdk-v8": "^2.2.0",
|
|
52
52
|
"@prmichaelsen/mcp-auth": "^7.0.4",
|
|
53
|
-
"@prmichaelsen/remember-core": "^0.22.
|
|
54
|
-
"@prmichaelsen/remember-mcp": "^2.7.3",
|
|
53
|
+
"@prmichaelsen/remember-core": "^0.22.5",
|
|
55
54
|
"dotenv": "^16.4.5",
|
|
56
55
|
"uuid": "^13.0.0",
|
|
57
56
|
"weaviate-client": "^3.2.0"
|
|
@@ -16,7 +16,7 @@ import type { Memory } from '../types/memory.js';
|
|
|
16
16
|
import type { AccessResult } from '../types/access-result.js';
|
|
17
17
|
import type { GhostConfig } from '../types/ghost-config.js';
|
|
18
18
|
import { DEFAULT_GHOST_CONFIG } from '../types/ghost-config.js';
|
|
19
|
-
import { isTrustSufficient } from '
|
|
19
|
+
import { isTrustSufficient } from '@prmichaelsen/remember-core';
|
|
20
20
|
|
|
21
21
|
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
22
22
|
|
|
@@ -7,8 +7,7 @@ import type { Memory, SearchFilters, DeletedFilter } from '../types/memory.js';
|
|
|
7
7
|
import { getMemoryCollection } from '../weaviate/schema.js';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
9
|
import { handleToolError } from '../utils/error-handler.js';
|
|
10
|
-
import { buildCombinedSearchFilters, buildDeletedFilter, combineFiltersWithAnd } from '
|
|
11
|
-
import { buildTrustFilter } from '../services/trust-enforcement.js';
|
|
10
|
+
import { buildCombinedSearchFilters, buildDeletedFilter, combineFiltersWithAnd, buildTrustFilter } from '@prmichaelsen/remember-core';
|
|
12
11
|
import { createDebugLogger } from '../utils/debug.js';
|
|
13
12
|
import type { AuthContext } from '../types/auth.js';
|
|
14
13
|
|
|
@@ -7,8 +7,7 @@ import type { Memory, Relationship, SearchOptions, SearchResult, SearchFilters }
|
|
|
7
7
|
import { getMemoryCollection } from '../weaviate/schema.js';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
9
|
import { handleToolError } from '../utils/error-handler.js';
|
|
10
|
-
import { buildCombinedSearchFilters, buildMemoryOnlyFilters, buildDeletedFilter, combineFiltersWithAnd } from '
|
|
11
|
-
import { buildTrustFilter } from '../services/trust-enforcement.js';
|
|
10
|
+
import { buildCombinedSearchFilters, buildMemoryOnlyFilters, buildDeletedFilter, combineFiltersWithAnd, buildTrustFilter } from '@prmichaelsen/remember-core';
|
|
12
11
|
import { createDebugLogger } from '../utils/debug.js';
|
|
13
12
|
import type { AuthContext } from '../types/auth.js';
|
|
14
13
|
|
|
@@ -1,83 +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
|
-
import type { Memory } from '../types/memory.js';
|
|
11
|
-
import type { TrustEnforcementMode } from '../types/ghost-config.js';
|
|
12
|
-
/** Trust level thresholds mapping continuous 0-1 values to discrete behavior tiers */
|
|
13
|
-
export declare const TRUST_THRESHOLDS: {
|
|
14
|
-
readonly FULL_ACCESS: 1;
|
|
15
|
-
readonly PARTIAL_ACCESS: 0.75;
|
|
16
|
-
readonly SUMMARY_ONLY: 0.5;
|
|
17
|
-
readonly METADATA_ONLY: 0.25;
|
|
18
|
-
readonly EXISTENCE_ONLY: 0;
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Build a Weaviate filter that restricts memories by trust score.
|
|
22
|
-
* Only returns memories where trust_score <= accessorTrustLevel.
|
|
23
|
-
*
|
|
24
|
-
* Trust 1.0 memories require accessor trust >= 1.0 to even appear in results.
|
|
25
|
-
* When they do appear, formatMemoryForPrompt caps output to existence-only.
|
|
26
|
-
*
|
|
27
|
-
* @param collection - Weaviate collection instance
|
|
28
|
-
* @param accessorTrustLevel - The accessor's trust level (0-1)
|
|
29
|
-
* @returns Weaviate filter object
|
|
30
|
-
*/
|
|
31
|
-
export declare function buildTrustFilter(collection: any, accessorTrustLevel: number): any;
|
|
32
|
-
/**
|
|
33
|
-
* Formatted memory representation for prompt-level enforcement.
|
|
34
|
-
* Content is redacted/formatted based on trust level.
|
|
35
|
-
*/
|
|
36
|
-
export interface FormattedMemory {
|
|
37
|
-
memory_id: string;
|
|
38
|
-
trust_tier: string;
|
|
39
|
-
content: string;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Format a memory for inclusion in an LLM prompt, redacted by trust level.
|
|
43
|
-
*
|
|
44
|
-
* Trust tiers:
|
|
45
|
-
* - 1.0 Full Access: full content, all details
|
|
46
|
-
* - 0.75 Partial Access: content with sensitive fields redacted
|
|
47
|
-
* - 0.5 Summary Only: title + summary, no content
|
|
48
|
-
* - 0.25 Metadata Only: type, date, tags — no content or summary
|
|
49
|
-
* - 0.0 Existence Only: "A memory exists about this topic"
|
|
50
|
-
*
|
|
51
|
-
* Trust 1.0 memories are always existence-only for cross-users, regardless of
|
|
52
|
-
* accessor trust level. Use `isSelfAccess = true` to bypass for owner access.
|
|
53
|
-
*
|
|
54
|
-
* @param memory - The memory to format
|
|
55
|
-
* @param accessorTrustLevel - The accessor's trust level (0-1)
|
|
56
|
-
* @param isSelfAccess - True if the accessor is the memory owner (bypasses trust 1.0 cap)
|
|
57
|
-
* @returns Formatted memory for prompt inclusion
|
|
58
|
-
*/
|
|
59
|
-
export declare function formatMemoryForPrompt(memory: Memory, accessorTrustLevel: number, isSelfAccess?: boolean): FormattedMemory;
|
|
60
|
-
/**
|
|
61
|
-
* Get a human-readable label for a trust level.
|
|
62
|
-
*/
|
|
63
|
-
export declare function getTrustLevelLabel(trust: number): string;
|
|
64
|
-
/**
|
|
65
|
-
* Get LLM instruction text describing what to reveal at a given trust level.
|
|
66
|
-
*/
|
|
67
|
-
export declare function getTrustInstructions(trust: number): string;
|
|
68
|
-
/**
|
|
69
|
-
* Redact sensitive fields from a memory for partial access.
|
|
70
|
-
* Returns a copy with location, context, and references cleared.
|
|
71
|
-
*/
|
|
72
|
-
export declare function redactSensitiveFields(memory: Memory, _trust: number): Memory;
|
|
73
|
-
/**
|
|
74
|
-
* Check whether an accessor's trust level is sufficient for a memory.
|
|
75
|
-
* Access is granted when accessorTrust >= memoryTrust.
|
|
76
|
-
*/
|
|
77
|
-
export declare function isTrustSufficient(memoryTrust: number, accessorTrust: number): boolean;
|
|
78
|
-
/**
|
|
79
|
-
* Determine the enforcement mode to use.
|
|
80
|
-
* Convenience function that returns the mode from GhostConfig or falls back to 'query'.
|
|
81
|
-
*/
|
|
82
|
-
export declare function resolveEnforcementMode(mode?: TrustEnforcementMode): TrustEnforcementMode;
|
|
83
|
-
//# sourceMappingURL=trust-enforcement.d.ts.map
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Weaviate v3 Filter Builder Utilities
|
|
3
|
-
*
|
|
4
|
-
* Provides helper functions to build Weaviate v3 filters using the fluent API.
|
|
5
|
-
* Replaces old v2 filter format (path/operator/valueText) with v3 collection.filter.byProperty()
|
|
6
|
-
*/
|
|
7
|
-
import type { SearchFilters } from '../types/memory.js';
|
|
8
|
-
/**
|
|
9
|
-
* Deleted filter options
|
|
10
|
-
*/
|
|
11
|
-
export type DeletedFilter = 'exclude' | 'include' | 'only';
|
|
12
|
-
/**
|
|
13
|
-
* Build filters for searching both memories and relationships
|
|
14
|
-
* Uses OR logic: (doc_type=memory AND memory_filters) OR (doc_type=relationship AND relationship_filters)
|
|
15
|
-
*
|
|
16
|
-
* @param collection - Weaviate collection instance
|
|
17
|
-
* @param filters - Optional search filters
|
|
18
|
-
* @returns Combined filter or undefined if no filters
|
|
19
|
-
*/
|
|
20
|
-
export declare function buildCombinedSearchFilters(collection: any, filters?: SearchFilters): any;
|
|
21
|
-
/**
|
|
22
|
-
* Build filters for memory-only search (backward compatibility)
|
|
23
|
-
*
|
|
24
|
-
* @param collection - Weaviate collection instance
|
|
25
|
-
* @param filters - Optional search filters
|
|
26
|
-
* @returns Combined filter or undefined if no filters
|
|
27
|
-
*/
|
|
28
|
-
export declare function buildMemoryOnlyFilters(collection: any, filters?: SearchFilters): any;
|
|
29
|
-
/**
|
|
30
|
-
* Build filters specifically for relationship-only search
|
|
31
|
-
*
|
|
32
|
-
* @param collection - Weaviate collection instance
|
|
33
|
-
* @param filters - Optional search filters
|
|
34
|
-
* @returns Combined filter or undefined if no filters
|
|
35
|
-
*/
|
|
36
|
-
export declare function buildRelationshipOnlyFilters(collection: any, filters?: SearchFilters): any;
|
|
37
|
-
/**
|
|
38
|
-
* Combine multiple filters with AND logic
|
|
39
|
-
*
|
|
40
|
-
* @param filters - Array of filter objects
|
|
41
|
-
* @returns Combined filter or undefined
|
|
42
|
-
*/
|
|
43
|
-
export declare function combineFiltersWithAnd(filters: any[]): any;
|
|
44
|
-
/**
|
|
45
|
-
* Helper to check if a filter result is empty
|
|
46
|
-
*/
|
|
47
|
-
export declare function hasFilters(filter: any): boolean;
|
|
48
|
-
/**
|
|
49
|
-
* Build filter for deleted_at field based on deleted_filter parameter
|
|
50
|
-
*
|
|
51
|
-
* @param collection - Weaviate collection instance
|
|
52
|
-
* @param deletedFilter - Filter mode: 'exclude' (default), 'include', or 'only'
|
|
53
|
-
* @returns Filter for deleted_at field, or null if no filter needed
|
|
54
|
-
*/
|
|
55
|
-
export declare function buildDeletedFilter(collection: any, deletedFilter?: DeletedFilter): any | null;
|
|
56
|
-
//# sourceMappingURL=weaviate-filters.d.ts.map
|
|
@@ -1,8 +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
|
-
export {};
|
|
8
|
-
//# sourceMappingURL=weaviate-filters.spec.d.ts.map
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildTrustFilter,
|
|
3
|
-
formatMemoryForPrompt,
|
|
4
|
-
getTrustLevelLabel,
|
|
5
|
-
getTrustInstructions,
|
|
6
|
-
redactSensitiveFields,
|
|
7
|
-
isTrustSufficient,
|
|
8
|
-
resolveEnforcementMode,
|
|
9
|
-
TRUST_THRESHOLDS,
|
|
10
|
-
} from './trust-enforcement.js';
|
|
11
|
-
import type { Memory } from '../types/memory.js';
|
|
12
|
-
|
|
13
|
-
// ─── Test Fixtures ─────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
const mockMemory: Memory = {
|
|
16
|
-
id: 'mem-1',
|
|
17
|
-
user_id: 'user-owner',
|
|
18
|
-
doc_type: 'memory',
|
|
19
|
-
content: 'My private journal entry about my feelings.',
|
|
20
|
-
title: 'Journal Entry: Feb 2026',
|
|
21
|
-
summary: 'Reflections on the month of February.',
|
|
22
|
-
type: 'journal',
|
|
23
|
-
weight: 0.5,
|
|
24
|
-
trust: 0.75,
|
|
25
|
-
location: {
|
|
26
|
-
gps: { latitude: 37.7749, longitude: -122.4194, timestamp: '2026-01-01T00:00:00Z' },
|
|
27
|
-
address: { formatted: '123 Main St, San Francisco, CA' },
|
|
28
|
-
source: 'gps',
|
|
29
|
-
confidence: 0.9,
|
|
30
|
-
is_approximate: false,
|
|
31
|
-
},
|
|
32
|
-
context: {
|
|
33
|
-
timestamp: '2026-02-15T10:00:00Z',
|
|
34
|
-
source: { type: 'manual' },
|
|
35
|
-
participants: [{ user_id: 'user-owner', role: 'user' }],
|
|
36
|
-
environment: { device: 'laptop' },
|
|
37
|
-
notes: 'Written during morning coffee',
|
|
38
|
-
},
|
|
39
|
-
relationships: ['rel-1'],
|
|
40
|
-
access_count: 5,
|
|
41
|
-
last_accessed_at: '2026-02-20T08:00:00Z',
|
|
42
|
-
created_at: '2026-02-15T10:00:00Z',
|
|
43
|
-
updated_at: '2026-02-20T08:00:00Z',
|
|
44
|
-
version: 2,
|
|
45
|
-
tags: ['personal', 'reflection'],
|
|
46
|
-
references: ['https://private-blog.example.com/entry-1'],
|
|
47
|
-
base_weight: 0.5,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// Mock Weaviate collection
|
|
51
|
-
function createMockCollection() {
|
|
52
|
-
const filterResult = { lessThanOrEqual: jest.fn().mockReturnValue('trust_filter') };
|
|
53
|
-
return {
|
|
54
|
-
filter: {
|
|
55
|
-
byProperty: jest.fn().mockReturnValue(filterResult),
|
|
56
|
-
},
|
|
57
|
-
_filterResult: filterResult,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ─── buildTrustFilter ──────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
describe('buildTrustFilter', () => {
|
|
64
|
-
it('creates a filter for trust_score <= accessorTrustLevel', () => {
|
|
65
|
-
const collection = createMockCollection();
|
|
66
|
-
const result = buildTrustFilter(collection, 0.5);
|
|
67
|
-
expect(collection.filter.byProperty).toHaveBeenCalledWith('trust_score');
|
|
68
|
-
expect(collection._filterResult.lessThanOrEqual).toHaveBeenCalledWith(0.5);
|
|
69
|
-
expect(result).toBe('trust_filter');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('works with trust level 0', () => {
|
|
73
|
-
const collection = createMockCollection();
|
|
74
|
-
buildTrustFilter(collection, 0);
|
|
75
|
-
expect(collection._filterResult.lessThanOrEqual).toHaveBeenCalledWith(0);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('works with trust level 1 (includes trust 1.0 memories for acknowledgment)', () => {
|
|
79
|
-
const collection = createMockCollection();
|
|
80
|
-
buildTrustFilter(collection, 1.0);
|
|
81
|
-
expect(collection._filterResult.lessThanOrEqual).toHaveBeenCalledWith(1.0);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// ─── formatMemoryForPrompt ─────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
describe('formatMemoryForPrompt', () => {
|
|
88
|
-
it('returns full content at trust 1.0', () => {
|
|
89
|
-
const result = formatMemoryForPrompt(mockMemory, 1.0);
|
|
90
|
-
expect(result.trust_tier).toBe('Full Access');
|
|
91
|
-
expect(result.content).toContain(mockMemory.content);
|
|
92
|
-
expect(result.content).toContain(mockMemory.title!);
|
|
93
|
-
expect(result.content).toContain(mockMemory.summary!);
|
|
94
|
-
expect(result.content).toContain('personal, reflection');
|
|
95
|
-
expect(result.content).toContain(mockMemory.created_at);
|
|
96
|
-
expect(result.memory_id).toBe('mem-1');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('returns redacted content at trust 0.75', () => {
|
|
100
|
-
const result = formatMemoryForPrompt(mockMemory, 0.75);
|
|
101
|
-
expect(result.trust_tier).toBe('Partial Access');
|
|
102
|
-
expect(result.content).toContain(mockMemory.content);
|
|
103
|
-
expect(result.content).toContain('personal, reflection');
|
|
104
|
-
// Should not contain full summary (only in full access)
|
|
105
|
-
expect(result.content).not.toContain(mockMemory.summary!);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('returns summary only at trust 0.5', () => {
|
|
109
|
-
const result = formatMemoryForPrompt(mockMemory, 0.5);
|
|
110
|
-
expect(result.trust_tier).toBe('Summary Only');
|
|
111
|
-
expect(result.content).toContain(mockMemory.title!);
|
|
112
|
-
expect(result.content).toContain(mockMemory.summary!);
|
|
113
|
-
expect(result.content).not.toContain(mockMemory.content);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('returns (No summary available) when summary missing at trust 0.5', () => {
|
|
117
|
-
const noSummary = { ...mockMemory, summary: undefined };
|
|
118
|
-
const result = formatMemoryForPrompt(noSummary, 0.5);
|
|
119
|
-
expect(result.content).toContain('(No summary available)');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('returns metadata only at trust 0.25', () => {
|
|
123
|
-
const result = formatMemoryForPrompt(mockMemory, 0.25);
|
|
124
|
-
expect(result.trust_tier).toBe('Metadata Only');
|
|
125
|
-
expect(result.content).toContain('[journal]');
|
|
126
|
-
expect(result.content).toContain('personal, reflection');
|
|
127
|
-
expect(result.content).not.toContain(mockMemory.content);
|
|
128
|
-
expect(result.content).not.toContain(mockMemory.summary!);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('returns existence only at trust 0', () => {
|
|
132
|
-
const result = formatMemoryForPrompt(mockMemory, 0);
|
|
133
|
-
expect(result.trust_tier).toBe('Existence Only');
|
|
134
|
-
expect(result.content).toBe('A memory exists about this topic.');
|
|
135
|
-
expect(result.content).not.toContain(mockMemory.content);
|
|
136
|
-
expect(result.content).not.toContain(mockMemory.title!);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('handles memory without title gracefully', () => {
|
|
140
|
-
const noTitle = { ...mockMemory, title: undefined };
|
|
141
|
-
const result = formatMemoryForPrompt(noTitle, 1.0);
|
|
142
|
-
expect(result.content).toContain('Untitled');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('handles memory with empty tags', () => {
|
|
146
|
-
const noTags = { ...mockMemory, tags: [] };
|
|
147
|
-
const result = formatMemoryForPrompt(noTags, 1.0);
|
|
148
|
-
expect(result.content).not.toContain('Tags:');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe('trust 1.0 memories (existence-only for cross-users)', () => {
|
|
152
|
-
const trust1Memory = { ...mockMemory, trust: 1.0 };
|
|
153
|
-
|
|
154
|
-
it('returns existence-only for cross-user even at accessor trust 1.0', () => {
|
|
155
|
-
const result = formatMemoryForPrompt(trust1Memory, 1.0);
|
|
156
|
-
expect(result.trust_tier).toBe('Existence Only');
|
|
157
|
-
expect(result.content).toBe('A memory exists about this topic.');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('returns existence-only for cross-user at any trust level', () => {
|
|
161
|
-
for (const trust of [0, 0.25, 0.5, 0.75, 1.0]) {
|
|
162
|
-
const result = formatMemoryForPrompt(trust1Memory, trust);
|
|
163
|
-
expect(result.trust_tier).toBe('Existence Only');
|
|
164
|
-
expect(result.content).not.toContain(trust1Memory.content);
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('returns full content for owner (isSelfAccess = true)', () => {
|
|
169
|
-
const result = formatMemoryForPrompt(trust1Memory, 1.0, true);
|
|
170
|
-
expect(result.trust_tier).toBe('Full Access');
|
|
171
|
-
expect(result.content).toContain(trust1Memory.content);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('does not leak title, summary, or tags for cross-user', () => {
|
|
175
|
-
const result = formatMemoryForPrompt(trust1Memory, 1.0);
|
|
176
|
-
expect(result.content).not.toContain(trust1Memory.title!);
|
|
177
|
-
expect(result.content).not.toContain(trust1Memory.summary!);
|
|
178
|
-
expect(result.content).not.toContain('personal');
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// ─── getTrustLevelLabel ────────────────────────────────────────────────────
|
|
184
|
-
|
|
185
|
-
describe('getTrustLevelLabel', () => {
|
|
186
|
-
it('returns Full Access at 1.0', () => {
|
|
187
|
-
expect(getTrustLevelLabel(1.0)).toBe('Full Access');
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('returns Partial Access at 0.75', () => {
|
|
191
|
-
expect(getTrustLevelLabel(0.75)).toBe('Partial Access');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('returns Summary Only at 0.5', () => {
|
|
195
|
-
expect(getTrustLevelLabel(0.5)).toBe('Summary Only');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('returns Metadata Only at 0.25', () => {
|
|
199
|
-
expect(getTrustLevelLabel(0.25)).toBe('Metadata Only');
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('returns Existence Only at 0', () => {
|
|
203
|
-
expect(getTrustLevelLabel(0)).toBe('Existence Only');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('maps intermediate values to nearest lower threshold', () => {
|
|
207
|
-
expect(getTrustLevelLabel(0.9)).toBe('Partial Access');
|
|
208
|
-
expect(getTrustLevelLabel(0.6)).toBe('Summary Only');
|
|
209
|
-
expect(getTrustLevelLabel(0.3)).toBe('Metadata Only');
|
|
210
|
-
expect(getTrustLevelLabel(0.1)).toBe('Existence Only');
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// ─── getTrustInstructions ──────────────────────────────────────────────────
|
|
215
|
-
|
|
216
|
-
describe('getTrustInstructions', () => {
|
|
217
|
-
it('returns appropriate instructions for each tier', () => {
|
|
218
|
-
expect(getTrustInstructions(1.0)).toContain('full access');
|
|
219
|
-
expect(getTrustInstructions(0.75)).toContain('partial access');
|
|
220
|
-
expect(getTrustInstructions(0.5)).toContain('summary');
|
|
221
|
-
expect(getTrustInstructions(0.25)).toContain('metadata');
|
|
222
|
-
expect(getTrustInstructions(0)).toContain('acknowledge');
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// ─── redactSensitiveFields ─────────────────────────────────────────────────
|
|
227
|
-
|
|
228
|
-
describe('redactSensitiveFields', () => {
|
|
229
|
-
it('clears GPS and address from location', () => {
|
|
230
|
-
const redacted = redactSensitiveFields(mockMemory, 0.75);
|
|
231
|
-
expect(redacted.location.gps).toBeNull();
|
|
232
|
-
expect(redacted.location.address).toBeNull();
|
|
233
|
-
expect(redacted.location.source).toBe('unavailable');
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('clears context participants and environment', () => {
|
|
237
|
-
const redacted = redactSensitiveFields(mockMemory, 0.75);
|
|
238
|
-
expect(redacted.context.participants).toBeUndefined();
|
|
239
|
-
expect(redacted.context.environment).toBeUndefined();
|
|
240
|
-
expect(redacted.context.notes).toBeUndefined();
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('clears references', () => {
|
|
244
|
-
const redacted = redactSensitiveFields(mockMemory, 0.75);
|
|
245
|
-
expect(redacted.references).toBeUndefined();
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('preserves content and tags', () => {
|
|
249
|
-
const redacted = redactSensitiveFields(mockMemory, 0.75);
|
|
250
|
-
expect(redacted.content).toBe(mockMemory.content);
|
|
251
|
-
expect(redacted.tags).toEqual(mockMemory.tags);
|
|
252
|
-
expect(redacted.title).toBe(mockMemory.title);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('preserves core context fields', () => {
|
|
256
|
-
const redacted = redactSensitiveFields(mockMemory, 0.75);
|
|
257
|
-
expect(redacted.context.timestamp).toBe(mockMemory.context.timestamp);
|
|
258
|
-
expect(redacted.context.source).toEqual(mockMemory.context.source);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('does not mutate original memory', () => {
|
|
262
|
-
const originalLocation = mockMemory.location.gps;
|
|
263
|
-
redactSensitiveFields(mockMemory, 0.75);
|
|
264
|
-
expect(mockMemory.location.gps).toBe(originalLocation);
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// ─── isTrustSufficient ─────────────────────────────────────────────────────
|
|
269
|
-
|
|
270
|
-
describe('isTrustSufficient', () => {
|
|
271
|
-
it('returns true when accessor trust >= memory trust', () => {
|
|
272
|
-
expect(isTrustSufficient(0.5, 0.75)).toBe(true);
|
|
273
|
-
expect(isTrustSufficient(0.5, 0.5)).toBe(true);
|
|
274
|
-
expect(isTrustSufficient(0, 0)).toBe(true);
|
|
275
|
-
expect(isTrustSufficient(1.0, 1.0)).toBe(true);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('returns false when accessor trust < memory trust', () => {
|
|
279
|
-
expect(isTrustSufficient(0.75, 0.5)).toBe(false);
|
|
280
|
-
expect(isTrustSufficient(1.0, 0.99)).toBe(false);
|
|
281
|
-
expect(isTrustSufficient(0.25, 0)).toBe(false);
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
// ─── resolveEnforcementMode ────────────────────────────────────────────────
|
|
286
|
-
|
|
287
|
-
describe('resolveEnforcementMode', () => {
|
|
288
|
-
it('returns the provided mode', () => {
|
|
289
|
-
expect(resolveEnforcementMode('query')).toBe('query');
|
|
290
|
-
expect(resolveEnforcementMode('prompt')).toBe('prompt');
|
|
291
|
-
expect(resolveEnforcementMode('hybrid')).toBe('hybrid');
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it('defaults to query when undefined', () => {
|
|
295
|
-
expect(resolveEnforcementMode(undefined)).toBe('query');
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// ─── TRUST_THRESHOLDS ──────────────────────────────────────────────────────
|
|
300
|
-
|
|
301
|
-
describe('TRUST_THRESHOLDS', () => {
|
|
302
|
-
it('has correct values', () => {
|
|
303
|
-
expect(TRUST_THRESHOLDS.FULL_ACCESS).toBe(1.0);
|
|
304
|
-
expect(TRUST_THRESHOLDS.PARTIAL_ACCESS).toBe(0.75);
|
|
305
|
-
expect(TRUST_THRESHOLDS.SUMMARY_ONLY).toBe(0.5);
|
|
306
|
-
expect(TRUST_THRESHOLDS.METADATA_ONLY).toBe(0.25);
|
|
307
|
-
expect(TRUST_THRESHOLDS.EXISTENCE_ONLY).toBe(0.0);
|
|
308
|
-
});
|
|
309
|
-
});
|