@prmichaelsen/remember-mcp 3.0.0 → 3.13.0

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 (208) hide show
  1. package/AGENT.md +296 -250
  2. package/CHANGELOG.md +358 -0
  3. package/README.md +68 -45
  4. package/agent/commands/acp.clarification-create.md +382 -0
  5. package/agent/commands/acp.project-info.md +309 -0
  6. package/agent/commands/acp.project-remove.md +379 -0
  7. package/agent/commands/acp.project-update.md +296 -0
  8. package/agent/commands/acp.task-create.md +17 -9
  9. package/agent/commands/git.commit.md +13 -1
  10. package/agent/design/comment-memory-type.md +2 -2
  11. package/agent/design/local.collaborative-memory-sync.md +265 -0
  12. package/agent/design/local.content-flags.md +210 -0
  13. package/agent/design/local.ghost-persona-system.md +273 -0
  14. package/agent/design/local.group-acl-integration.md +338 -0
  15. package/agent/design/local.memory-acl-schema.md +352 -0
  16. package/agent/design/local.memory-collection-pattern-v2.md +348 -0
  17. package/agent/design/local.moderation-and-space-config.md +257 -0
  18. package/agent/design/local.v2-api-reference.md +621 -0
  19. package/agent/design/local.v2-migration-guide.md +191 -0
  20. package/agent/design/local.v2-usage-examples.md +265 -0
  21. package/agent/design/permissions-storage-architecture.md +11 -3
  22. package/agent/design/trust-escalation-prevention.md +9 -2
  23. package/agent/design/trust-system-implementation.md +12 -3
  24. package/agent/milestones/milestone-14-memory-collection-v2.md +182 -0
  25. package/agent/milestones/milestone-15-moderation-space-config.md +126 -0
  26. package/agent/progress.yaml +628 -49
  27. package/agent/scripts/acp.common.sh +2 -0
  28. package/agent/scripts/acp.install.sh +11 -1
  29. package/agent/scripts/acp.package-install-optimized.sh +454 -0
  30. package/agent/scripts/acp.package-install.sh +247 -300
  31. package/agent/scripts/acp.project-info.sh +218 -0
  32. package/agent/scripts/acp.project-remove.sh +302 -0
  33. package/agent/scripts/acp.project-update.sh +296 -0
  34. package/agent/scripts/acp.yaml-parser.sh +128 -10
  35. package/agent/tasks/milestone-14-memory-collection-v2/task-165-core-infrastructure-setup.md +171 -0
  36. package/agent/tasks/milestone-14-memory-collection-v2/task-166-update-remember-publish.md +191 -0
  37. package/agent/tasks/milestone-14-memory-collection-v2/task-167-update-remember-retract.md +186 -0
  38. package/agent/tasks/milestone-14-memory-collection-v2/task-168-implement-remember-revise.md +184 -0
  39. package/agent/tasks/milestone-14-memory-collection-v2/task-169-update-remember-search-space.md +179 -0
  40. package/agent/tasks/milestone-14-memory-collection-v2/task-170-update-remember-create-update.md +139 -0
  41. package/agent/tasks/milestone-14-memory-collection-v2/task-172-performance-testing-optimization.md +161 -0
  42. package/agent/tasks/milestone-14-memory-collection-v2/task-173-documentation-examples.md +258 -0
  43. package/agent/tasks/milestone-15-moderation-space-config/task-174-add-moderation-schema-fields.md +57 -0
  44. package/agent/tasks/milestone-15-moderation-space-config/task-175-create-space-config-service.md +64 -0
  45. package/agent/tasks/milestone-15-moderation-space-config/task-176-wire-moderation-publish-flow.md +45 -0
  46. package/agent/tasks/milestone-15-moderation-space-config/task-177-add-moderation-search-filters.md +70 -0
  47. package/agent/tasks/milestone-15-moderation-space-config/task-178-create-remember-moderate-tool.md +69 -0
  48. package/agent/tasks/milestone-15-moderation-space-config/task-179-documentation-integration-tests.md +58 -0
  49. package/agent/tasks/milestone-16-ghost-system/task-187-ghost-config-firestore.md +41 -0
  50. package/agent/tasks/milestone-16-ghost-system/task-188-trust-filter-integration.md +44 -0
  51. package/agent/tasks/milestone-16-ghost-system/task-189-ghost-memory-filtering.md +43 -0
  52. package/agent/tasks/milestone-16-ghost-system/task-190-ghost-config-tools.md +45 -0
  53. package/agent/tasks/milestone-16-ghost-system/task-191-escalation-firestore.md +38 -0
  54. package/agent/tasks/milestone-16-ghost-system/task-192-documentation-verification.md +39 -0
  55. package/agent/tasks/milestone-7-trust-permissions/task-180-access-result-permission-types.md +69 -0
  56. package/agent/tasks/milestone-7-trust-permissions/task-181-firestore-permissions-access-logs.md +56 -0
  57. package/agent/tasks/milestone-7-trust-permissions/task-182-trust-enforcement-service.md +68 -0
  58. package/agent/tasks/milestone-7-trust-permissions/task-183-access-control-service.md +70 -0
  59. package/agent/tasks/milestone-7-trust-permissions/task-184-permission-tools.md +79 -0
  60. package/agent/tasks/milestone-7-trust-permissions/task-185-wire-trust-into-search-query.md +55 -0
  61. package/agent/tasks/milestone-7-trust-permissions/task-186-documentation-verification.md +56 -0
  62. package/agent/tasks/task-76-fix-indexnullstate-schema-bug.md +197 -0
  63. package/dist/collections/composite-ids.d.ts +106 -0
  64. package/dist/collections/core-infrastructure.spec.d.ts +11 -0
  65. package/dist/collections/dot-notation.d.ts +106 -0
  66. package/dist/collections/tracking-arrays.d.ts +176 -0
  67. package/dist/constants/content-types.d.ts +1 -0
  68. package/dist/schema/v2-collections-comments.spec.d.ts +8 -0
  69. package/dist/schema/v2-collections.d.ts +210 -0
  70. package/dist/server-factory.d.ts +15 -0
  71. package/dist/server-factory.js +2798 -1029
  72. package/dist/server.js +2526 -1012
  73. package/dist/services/access-control.d.ts +103 -0
  74. package/dist/services/access-control.spec.d.ts +2 -0
  75. package/dist/services/credentials-provider.d.ts +24 -0
  76. package/dist/services/credentials-provider.spec.d.ts +2 -0
  77. package/dist/services/escalation.service.d.ts +22 -0
  78. package/dist/services/escalation.service.spec.d.ts +2 -0
  79. package/dist/services/ghost-config.service.d.ts +55 -0
  80. package/dist/services/ghost-config.service.spec.d.ts +2 -0
  81. package/dist/services/space-config.service.d.ts +23 -0
  82. package/dist/services/space-config.service.spec.d.ts +2 -0
  83. package/dist/services/trust-enforcement.d.ts +83 -0
  84. package/dist/services/trust-enforcement.spec.d.ts +2 -0
  85. package/dist/services/trust-validator.d.ts +43 -0
  86. package/dist/services/trust-validator.spec.d.ts +2 -0
  87. package/dist/tools/confirm-publish-moderation.spec.d.ts +8 -0
  88. package/dist/tools/confirm.d.ts +8 -1
  89. package/dist/tools/create-memory.d.ts +2 -1
  90. package/dist/tools/create-memory.spec.d.ts +10 -0
  91. package/dist/tools/create-relationship.d.ts +2 -1
  92. package/dist/tools/delete-memory.d.ts +2 -1
  93. package/dist/tools/delete-relationship.d.ts +2 -1
  94. package/dist/tools/deny.d.ts +2 -1
  95. package/dist/tools/find-similar.d.ts +2 -1
  96. package/dist/tools/get-preferences.d.ts +2 -1
  97. package/dist/tools/ghost-config.d.ts +27 -0
  98. package/dist/tools/ghost-config.spec.d.ts +2 -0
  99. package/dist/tools/moderate.d.ts +20 -0
  100. package/dist/tools/moderate.spec.d.ts +5 -0
  101. package/dist/tools/publish.d.ts +11 -3
  102. package/dist/tools/query-memory.d.ts +3 -1
  103. package/dist/tools/query-space.d.ts +4 -1
  104. package/dist/tools/retract.d.ts +29 -0
  105. package/dist/tools/revise.d.ts +45 -0
  106. package/dist/tools/revise.spec.d.ts +8 -0
  107. package/dist/tools/search-memory.d.ts +2 -1
  108. package/dist/tools/search-relationship.d.ts +2 -1
  109. package/dist/tools/search-space.d.ts +25 -5
  110. package/dist/tools/search-space.spec.d.ts +9 -0
  111. package/dist/tools/set-preference.d.ts +2 -1
  112. package/dist/tools/update-memory.d.ts +2 -1
  113. package/dist/tools/update-relationship.d.ts +2 -1
  114. package/dist/types/access-result.d.ts +48 -0
  115. package/dist/types/access-result.spec.d.ts +2 -0
  116. package/dist/types/auth.d.ts +46 -0
  117. package/dist/types/ghost-config.d.ts +36 -0
  118. package/dist/types/memory.d.ts +3 -1
  119. package/dist/types/preferences.d.ts +1 -1
  120. package/dist/utils/auth-helpers.d.ts +14 -0
  121. package/dist/utils/auth-helpers.spec.d.ts +2 -0
  122. package/dist/utils/test-data-generator.d.ts +124 -0
  123. package/dist/utils/test-data-generator.spec.d.ts +12 -0
  124. package/dist/v2-performance.e2e.d.ts +17 -0
  125. package/dist/v2-smoke.e2e.d.ts +14 -0
  126. package/dist/weaviate/client.d.ts +5 -8
  127. package/dist/weaviate/space-schema.d.ts +2 -2
  128. package/docs/performance/v2-benchmarks.md +80 -0
  129. package/jest.e2e.config.js +14 -3
  130. package/package.json +1 -1
  131. package/scripts/.collection-recreation-state.yaml +16 -0
  132. package/scripts/.gitkeep +5 -0
  133. package/scripts/README-collection-recreation.md +224 -0
  134. package/scripts/README.md +51 -0
  135. package/scripts/backup-collections.ts +543 -0
  136. package/scripts/delete-collection.ts +137 -0
  137. package/scripts/migrate-recreate-collections.ts +578 -0
  138. package/scripts/migrate-v1-to-v2.ts +1094 -0
  139. package/scripts/package-lock.json +1113 -0
  140. package/scripts/package.json +27 -0
  141. package/src/collections/composite-ids.ts +193 -0
  142. package/src/collections/core-infrastructure.spec.ts +353 -0
  143. package/src/collections/dot-notation.ts +212 -0
  144. package/src/collections/tracking-arrays.ts +298 -0
  145. package/src/constants/content-types.ts +20 -0
  146. package/src/schema/v2-collections-comments.spec.ts +141 -0
  147. package/src/schema/v2-collections.ts +433 -0
  148. package/src/server-factory.ts +89 -20
  149. package/src/server.ts +45 -17
  150. package/src/services/access-control.spec.ts +383 -0
  151. package/src/services/access-control.ts +291 -0
  152. package/src/services/credentials-provider.spec.ts +22 -0
  153. package/src/services/credentials-provider.ts +34 -0
  154. package/src/services/escalation.service.spec.ts +183 -0
  155. package/src/services/escalation.service.ts +150 -0
  156. package/src/services/ghost-config.service.spec.ts +339 -0
  157. package/src/services/ghost-config.service.ts +219 -0
  158. package/src/services/space-config.service.spec.ts +102 -0
  159. package/src/services/space-config.service.ts +79 -0
  160. package/src/services/trust-enforcement.spec.ts +309 -0
  161. package/src/services/trust-enforcement.ts +197 -0
  162. package/src/services/trust-validator.spec.ts +108 -0
  163. package/src/services/trust-validator.ts +105 -0
  164. package/src/tools/confirm-publish-moderation.spec.ts +240 -0
  165. package/src/tools/confirm.ts +869 -135
  166. package/src/tools/create-memory.spec.ts +126 -0
  167. package/src/tools/create-memory.ts +20 -27
  168. package/src/tools/create-relationship.ts +17 -8
  169. package/src/tools/delete-memory.ts +13 -6
  170. package/src/tools/delete-relationship.ts +15 -6
  171. package/src/tools/deny.ts +8 -1
  172. package/src/tools/find-similar.ts +21 -8
  173. package/src/tools/get-preferences.ts +10 -1
  174. package/src/tools/ghost-config.spec.ts +180 -0
  175. package/src/tools/ghost-config.ts +230 -0
  176. package/src/tools/moderate.spec.ts +277 -0
  177. package/src/tools/moderate.ts +219 -0
  178. package/src/tools/publish.ts +99 -41
  179. package/src/tools/query-memory.ts +28 -6
  180. package/src/tools/query-space.ts +39 -4
  181. package/src/tools/retract.ts +292 -0
  182. package/src/tools/revise.spec.ts +146 -0
  183. package/src/tools/revise.ts +283 -0
  184. package/src/tools/search-memory.ts +30 -7
  185. package/src/tools/search-relationship.ts +11 -2
  186. package/src/tools/search-space.spec.ts +341 -0
  187. package/src/tools/search-space.ts +323 -99
  188. package/src/tools/set-preference.ts +10 -1
  189. package/src/tools/update-memory.ts +16 -5
  190. package/src/tools/update-relationship.ts +10 -1
  191. package/src/types/access-result.spec.ts +193 -0
  192. package/src/types/access-result.ts +62 -0
  193. package/src/types/auth.ts +52 -0
  194. package/src/types/ghost-config.ts +46 -0
  195. package/src/types/memory.ts +9 -1
  196. package/src/types/preferences.ts +2 -2
  197. package/src/utils/auth-helpers.spec.ts +75 -0
  198. package/src/utils/auth-helpers.ts +25 -0
  199. package/src/utils/test-data-generator.spec.ts +317 -0
  200. package/src/utils/test-data-generator.ts +292 -0
  201. package/src/utils/weaviate-filters.ts +4 -4
  202. package/src/v2-performance.e2e.ts +173 -0
  203. package/src/v2-smoke.e2e.ts +401 -0
  204. package/src/weaviate/client.spec.ts +5 -5
  205. package/src/weaviate/client.ts +51 -36
  206. package/src/weaviate/schema.ts +11 -256
  207. package/src/weaviate/space-schema.spec.ts +24 -24
  208. package/src/weaviate/space-schema.ts +18 -6
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Access control service — per-memory access checks with escalation prevention.
3
+ *
4
+ * In ghost mode (default), query-level filtering handles trust at the Weaviate layer.
5
+ * This service is needed for:
6
+ * 1. Trust escalation penalty tracking
7
+ * 2. Block management
8
+ * 3. Prompt/hybrid enforcement modes (per-memory access checks)
9
+ * 4. Future direct access tools
10
+ *
11
+ * See agent/design/access-control-result-pattern.md
12
+ * See agent/design/local.ghost-persona-system.md
13
+ */
14
+
15
+ import type { Memory } from '../types/memory.js';
16
+ import type { AccessResult } from '../types/access-result.js';
17
+ import type { GhostConfig } from '../types/ghost-config.js';
18
+ import { DEFAULT_GHOST_CONFIG } from '../types/ghost-config.js';
19
+ import { isTrustSufficient } from './trust-enforcement.js';
20
+
21
+ // ─── Types ─────────────────────────────────────────────────────────────────
22
+
23
+ /** Block record for a specific (accessor, memory) pair */
24
+ export interface MemoryBlock {
25
+ blocked_at: string; // ISO 8601
26
+ reason: string;
27
+ attempt_count: number;
28
+ }
29
+
30
+ /** Attempt record for escalation tracking */
31
+ export interface AttemptRecord {
32
+ count: number;
33
+ last_attempt_at: string; // ISO 8601
34
+ }
35
+
36
+ /**
37
+ * Provider interface for GhostConfig lookups.
38
+ * In-memory stub now, Firestore implementation in M16.
39
+ */
40
+ export interface GhostConfigProvider {
41
+ getGhostConfig(ownerUserId: string): Promise<GhostConfig | null>;
42
+ }
43
+
44
+ /**
45
+ * Provider interface for block and attempt tracking.
46
+ * In-memory stub now, Firestore implementation in M16.
47
+ */
48
+ export interface EscalationStore {
49
+ getBlock(ownerUserId: string, accessorUserId: string, memoryId: string): Promise<MemoryBlock | null>;
50
+ setBlock(ownerUserId: string, accessorUserId: string, memoryId: string, block: MemoryBlock): Promise<void>;
51
+ removeBlock(ownerUserId: string, accessorUserId: string, memoryId: string): Promise<void>;
52
+ getAttempts(ownerUserId: string, accessorUserId: string, memoryId: string): Promise<AttemptRecord | null>;
53
+ incrementAttempts(ownerUserId: string, accessorUserId: string, memoryId: string): Promise<AttemptRecord>;
54
+ }
55
+
56
+ // ─── In-Memory Implementations ────────────────────────────────────────────
57
+
58
+ /** Stub GhostConfig provider — returns null (ghost not configured) */
59
+ export class StubGhostConfigProvider implements GhostConfigProvider {
60
+ private configs: Map<string, GhostConfig> = new Map();
61
+
62
+ async getGhostConfig(ownerUserId: string): Promise<GhostConfig | null> {
63
+ return this.configs.get(ownerUserId) ?? null;
64
+ }
65
+
66
+ /** Test helper: set a GhostConfig for a user */
67
+ setGhostConfig(ownerUserId: string, config: GhostConfig): void {
68
+ this.configs.set(ownerUserId, config);
69
+ }
70
+ }
71
+
72
+ /** In-memory escalation store for development/testing */
73
+ export class InMemoryEscalationStore implements EscalationStore {
74
+ private blocks: Map<string, MemoryBlock> = new Map();
75
+ private attempts: Map<string, AttemptRecord> = new Map();
76
+
77
+ private key(ownerUserId: string, accessorUserId: string, memoryId: string): string {
78
+ return `${ownerUserId}:${accessorUserId}:${memoryId}`;
79
+ }
80
+
81
+ async getBlock(ownerUserId: string, accessorUserId: string, memoryId: string): Promise<MemoryBlock | null> {
82
+ return this.blocks.get(this.key(ownerUserId, accessorUserId, memoryId)) ?? null;
83
+ }
84
+
85
+ async setBlock(ownerUserId: string, accessorUserId: string, memoryId: string, block: MemoryBlock): Promise<void> {
86
+ this.blocks.set(this.key(ownerUserId, accessorUserId, memoryId), block);
87
+ }
88
+
89
+ async removeBlock(ownerUserId: string, accessorUserId: string, memoryId: string): Promise<void> {
90
+ this.blocks.delete(this.key(ownerUserId, accessorUserId, memoryId));
91
+ }
92
+
93
+ async getAttempts(ownerUserId: string, accessorUserId: string, memoryId: string): Promise<AttemptRecord | null> {
94
+ return this.attempts.get(this.key(ownerUserId, accessorUserId, memoryId)) ?? null;
95
+ }
96
+
97
+ async incrementAttempts(ownerUserId: string, accessorUserId: string, memoryId: string): Promise<AttemptRecord> {
98
+ const k = this.key(ownerUserId, accessorUserId, memoryId);
99
+ const existing = this.attempts.get(k);
100
+ const record: AttemptRecord = {
101
+ count: (existing?.count ?? 0) + 1,
102
+ last_attempt_at: new Date().toISOString(),
103
+ };
104
+ this.attempts.set(k, record);
105
+ return record;
106
+ }
107
+ }
108
+
109
+ // ─── Constants ─────────────────────────────────────────────────────────────
110
+
111
+ /** Trust penalty applied per failed access attempt */
112
+ const TRUST_PENALTY = 0.1;
113
+
114
+ /** Number of failed attempts before blocking */
115
+ const MAX_ATTEMPTS_BEFORE_BLOCK = 3;
116
+
117
+ // ─── Core Access Control ───────────────────────────────────────────────────
118
+
119
+ /**
120
+ * Check if an accessor has permission to access a specific memory.
121
+ *
122
+ * Flow:
123
+ * 1. Self-access → always granted (owner)
124
+ * 2. Ghost not enabled → no_permission
125
+ * 3. Accessor blocked by owner → no_permission
126
+ * 4. Memory-specific block → blocked
127
+ * 5. Insufficient trust → insufficient_trust (+ penalty, possible block)
128
+ * 6. Sufficient trust → granted (trusted; trust 1.0 memories capped to existence-only by formatting layer)
129
+ */
130
+ export async function checkMemoryAccess(
131
+ accessorUserId: string,
132
+ memory: Memory,
133
+ ghostConfigProvider: GhostConfigProvider,
134
+ escalationStore: EscalationStore,
135
+ ): Promise<AccessResult> {
136
+ const ownerUserId = memory.user_id;
137
+ const memoryId = memory.id;
138
+
139
+ // 1. Self-access always granted
140
+ if (accessorUserId === ownerUserId) {
141
+ return { status: 'granted', memory, access_level: 'owner' };
142
+ }
143
+
144
+ // 2. Check if ghost is enabled for owner
145
+ const ghostConfig = await ghostConfigProvider.getGhostConfig(ownerUserId);
146
+ if (!ghostConfig || !ghostConfig.enabled) {
147
+ return { status: 'no_permission', owner_user_id: ownerUserId, accessor_user_id: accessorUserId };
148
+ }
149
+
150
+ // 3. Check if accessor is user-wide blocked
151
+ if (ghostConfig.blocked_users.includes(accessorUserId)) {
152
+ return { status: 'no_permission', owner_user_id: ownerUserId, accessor_user_id: accessorUserId };
153
+ }
154
+
155
+ // 4. Check memory-specific block
156
+ const block = await escalationStore.getBlock(ownerUserId, accessorUserId, memoryId);
157
+ if (block) {
158
+ return {
159
+ status: 'blocked',
160
+ memory_id: memoryId,
161
+ reason: block.reason,
162
+ blocked_at: block.blocked_at,
163
+ };
164
+ }
165
+
166
+ // 5. Check trust level
167
+ const accessorTrust = resolveAccessorTrustLevel(ghostConfig, accessorUserId);
168
+ const memoryTrust = memory.trust;
169
+
170
+ if (!isTrustSufficient(memoryTrust, accessorTrust)) {
171
+ // Apply escalation
172
+ const result = await handleInsufficientTrust(
173
+ ownerUserId, accessorUserId, memoryId, memoryTrust, accessorTrust, escalationStore
174
+ );
175
+ return result;
176
+ }
177
+
178
+ // 6. All checks pass (trust 1.0 memories capped to existence-only by formatting layer)
179
+ return { status: 'granted', memory, access_level: 'trusted' };
180
+ }
181
+
182
+ // ─── Trust Escalation ──────────────────────────────────────────────────────
183
+
184
+ /**
185
+ * Handle an insufficient trust access attempt.
186
+ * Applies -0.1 penalty, blocks after 3 attempts.
187
+ */
188
+ export async function handleInsufficientTrust(
189
+ ownerUserId: string,
190
+ accessorUserId: string,
191
+ memoryId: string,
192
+ requiredTrust: number,
193
+ actualTrust: number,
194
+ escalationStore: EscalationStore,
195
+ ): Promise<AccessResult> {
196
+ const attempt = await escalationStore.incrementAttempts(ownerUserId, accessorUserId, memoryId);
197
+
198
+ // Block after MAX_ATTEMPTS_BEFORE_BLOCK
199
+ if (attempt.count >= MAX_ATTEMPTS_BEFORE_BLOCK) {
200
+ const block: MemoryBlock = {
201
+ blocked_at: new Date().toISOString(),
202
+ reason: `Access blocked after ${attempt.count} unauthorized attempts`,
203
+ attempt_count: attempt.count,
204
+ };
205
+ await escalationStore.setBlock(ownerUserId, accessorUserId, memoryId, block);
206
+ return {
207
+ status: 'blocked',
208
+ memory_id: memoryId,
209
+ reason: block.reason,
210
+ blocked_at: block.blocked_at,
211
+ };
212
+ }
213
+
214
+ return {
215
+ status: 'insufficient_trust',
216
+ memory_id: memoryId,
217
+ required_trust: requiredTrust,
218
+ actual_trust: Math.max(0, actualTrust - TRUST_PENALTY),
219
+ attempts_remaining: MAX_ATTEMPTS_BEFORE_BLOCK - attempt.count,
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Check if access to a specific memory is blocked.
225
+ */
226
+ export async function isMemoryBlocked(
227
+ ownerUserId: string,
228
+ accessorUserId: string,
229
+ memoryId: string,
230
+ escalationStore: EscalationStore,
231
+ ): Promise<boolean> {
232
+ const block = await escalationStore.getBlock(ownerUserId, accessorUserId, memoryId);
233
+ return block !== null;
234
+ }
235
+
236
+ /**
237
+ * Reset a memory-specific block (e.g., via grant_access).
238
+ */
239
+ export async function resetBlock(
240
+ ownerUserId: string,
241
+ accessorUserId: string,
242
+ memoryId: string,
243
+ escalationStore: EscalationStore,
244
+ ): Promise<void> {
245
+ await escalationStore.removeBlock(ownerUserId, accessorUserId, memoryId);
246
+ }
247
+
248
+ // ─── Trust Resolution ──────────────────────────────────────────────────────
249
+
250
+ /**
251
+ * Resolve the trust level for an accessor from GhostConfig.
252
+ *
253
+ * Priority: per_user_trust → default_friend_trust → default_public_trust → 0
254
+ *
255
+ * Note: "friend" vs "public" distinction will be determined by the calling
256
+ * context in M16 (friend list, social graph). For now, non-per_user accessors
257
+ * fall through to default_public_trust.
258
+ */
259
+ export function resolveAccessorTrustLevel(ghostConfig: GhostConfig, accessorUserId: string): number {
260
+ // 1. Per-user override
261
+ if (accessorUserId in ghostConfig.per_user_trust) {
262
+ return ghostConfig.per_user_trust[accessorUserId];
263
+ }
264
+
265
+ // 2. Fall through to public trust (friend detection deferred to M16)
266
+ return ghostConfig.default_public_trust ?? 0;
267
+ }
268
+
269
+ // ─── Message Formatting ───────────────────────────────────────────────────
270
+
271
+ /**
272
+ * Format an AccessResult into a human-readable message.
273
+ */
274
+ export function formatAccessResultMessage(result: AccessResult): string {
275
+ switch (result.status) {
276
+ case 'granted':
277
+ return result.access_level === 'owner'
278
+ ? 'Access granted (owner).'
279
+ : 'Access granted (trusted).';
280
+ case 'insufficient_trust':
281
+ return `Insufficient trust level. Required: ${result.required_trust.toFixed(2)}, actual: ${result.actual_trust.toFixed(2)}. ${result.attempts_remaining} attempt(s) remaining before access is blocked.`;
282
+ case 'blocked':
283
+ return `Access blocked: ${result.reason}`;
284
+ case 'no_permission':
285
+ return 'No permission to access this user\'s memories.';
286
+ case 'not_found':
287
+ return `Memory ${result.memory_id} not found.`;
288
+ case 'deleted':
289
+ return `Memory ${result.memory_id} was deleted on ${result.deleted_at}.`;
290
+ }
291
+ }
@@ -0,0 +1,22 @@
1
+ import { StubCredentialsProvider, createCredentialsProvider } from './credentials-provider.js';
2
+
3
+ describe('StubCredentialsProvider', () => {
4
+ it('returns the correct userId', async () => {
5
+ const provider = new StubCredentialsProvider();
6
+ const creds = await provider.getCredentials('token-123', 'user-abc');
7
+ expect(creds.user_id).toBe('user-abc');
8
+ });
9
+
10
+ it('returns empty group_memberships', async () => {
11
+ const provider = new StubCredentialsProvider();
12
+ const creds = await provider.getCredentials('token-123', 'user-abc');
13
+ expect(creds.group_memberships).toEqual([]);
14
+ });
15
+ });
16
+
17
+ describe('createCredentialsProvider', () => {
18
+ it('returns a StubCredentialsProvider instance', () => {
19
+ const provider = createCredentialsProvider();
20
+ expect(provider).toBeInstanceOf(StubCredentialsProvider);
21
+ });
22
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Credentials Provider
3
+ *
4
+ * Resolves user credentials (group memberships, permissions) from an access token.
5
+ * Currently uses a stub implementation; future: HTTP provider hitting /api/credentials/agentbase.
6
+ */
7
+
8
+ import type { CredentialsProvider, UserCredentials } from '../types/auth.js';
9
+
10
+ /**
11
+ * Stub credentials provider — returns empty group memberships.
12
+ * Used until an HTTP-based provider is wired up.
13
+ */
14
+ export class StubCredentialsProvider implements CredentialsProvider {
15
+ async getCredentials(_accessToken: string, userId: string): Promise<UserCredentials> {
16
+ return {
17
+ user_id: userId,
18
+ group_memberships: [],
19
+ };
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Factory to create the appropriate credentials provider.
25
+ * Future: reads config to pick stub vs HTTP implementation.
26
+ */
27
+ export function createCredentialsProvider(): CredentialsProvider {
28
+ return new StubCredentialsProvider();
29
+ }
30
+
31
+ /**
32
+ * Singleton credentials provider instance
33
+ */
34
+ export const credentialsProvider = createCredentialsProvider();
@@ -0,0 +1,183 @@
1
+ import { FirestoreEscalationStore } from './escalation.service.js';
2
+ import * as firestoreInit from '../firestore/init';
3
+
4
+ jest.mock('../firestore/init', () => ({
5
+ getDocument: jest.fn(),
6
+ setDocument: jest.fn(),
7
+ deleteDocument: jest.fn(),
8
+ }));
9
+
10
+ jest.mock('../firestore/paths', () => ({
11
+ BASE: 'test-remember-mcp',
12
+ }));
13
+
14
+ const mockGetDocument = firestoreInit.getDocument as jest.MockedFunction<typeof firestoreInit.getDocument>;
15
+ const mockSetDocument = firestoreInit.setDocument as jest.MockedFunction<typeof firestoreInit.setDocument>;
16
+ const mockDeleteDocument = (firestoreInit as any).deleteDocument as jest.MockedFunction<any>;
17
+
18
+ describe('FirestoreEscalationStore', () => {
19
+ let store: FirestoreEscalationStore;
20
+
21
+ beforeEach(() => {
22
+ jest.clearAllMocks();
23
+ store = new FirestoreEscalationStore();
24
+ });
25
+
26
+ describe('getBlock', () => {
27
+ it('returns null when no document exists', async () => {
28
+ mockGetDocument.mockResolvedValue(null);
29
+
30
+ const result = await store.getBlock('owner-1', 'accessor-1', 'mem-1');
31
+
32
+ expect(result).toBeNull();
33
+ expect(mockGetDocument).toHaveBeenCalledWith(
34
+ 'test-remember-mcp.users/owner-1/ghost_escalation',
35
+ 'accessor-1:mem-1'
36
+ );
37
+ });
38
+
39
+ it('returns null when document exists but not blocked', async () => {
40
+ mockGetDocument.mockResolvedValue({ attempt_count: 2, blocked: false });
41
+
42
+ const result = await store.getBlock('owner-1', 'accessor-1', 'mem-1');
43
+
44
+ expect(result).toBeNull();
45
+ });
46
+
47
+ it('returns block when document has blocked flag', async () => {
48
+ mockGetDocument.mockResolvedValue({
49
+ blocked: true,
50
+ blocked_at: '2026-02-27T00:00:00.000Z',
51
+ reason: 'Too many attempts',
52
+ attempt_count: 3,
53
+ });
54
+
55
+ const result = await store.getBlock('owner-1', 'accessor-1', 'mem-1');
56
+
57
+ expect(result).toEqual({
58
+ blocked_at: '2026-02-27T00:00:00.000Z',
59
+ reason: 'Too many attempts',
60
+ attempt_count: 3,
61
+ });
62
+ });
63
+
64
+ it('returns null on Firestore error', async () => {
65
+ mockGetDocument.mockRejectedValue(new Error('Firestore unavailable'));
66
+
67
+ const result = await store.getBlock('owner-1', 'accessor-1', 'mem-1');
68
+
69
+ expect(result).toBeNull();
70
+ });
71
+ });
72
+
73
+ describe('setBlock', () => {
74
+ it('writes block to Firestore', async () => {
75
+ mockSetDocument.mockResolvedValue(undefined);
76
+
77
+ await store.setBlock('owner-1', 'accessor-1', 'mem-1', {
78
+ blocked_at: '2026-02-27T00:00:00.000Z',
79
+ reason: 'Escalation limit',
80
+ attempt_count: 3,
81
+ });
82
+
83
+ expect(mockSetDocument).toHaveBeenCalledWith(
84
+ 'test-remember-mcp.users/owner-1/ghost_escalation',
85
+ 'accessor-1:mem-1',
86
+ {
87
+ blocked: true,
88
+ blocked_at: '2026-02-27T00:00:00.000Z',
89
+ reason: 'Escalation limit',
90
+ attempt_count: 3,
91
+ accessor_user_id: 'accessor-1',
92
+ memory_id: 'mem-1',
93
+ },
94
+ { merge: true }
95
+ );
96
+ });
97
+ });
98
+
99
+ describe('removeBlock', () => {
100
+ it('deletes the escalation document', async () => {
101
+ mockDeleteDocument.mockResolvedValue(undefined);
102
+
103
+ await store.removeBlock('owner-1', 'accessor-1', 'mem-1');
104
+
105
+ expect(mockDeleteDocument).toHaveBeenCalledWith(
106
+ 'test-remember-mcp.users/owner-1/ghost_escalation',
107
+ 'accessor-1:mem-1'
108
+ );
109
+ });
110
+ });
111
+
112
+ describe('getAttempts', () => {
113
+ it('returns null when no document exists', async () => {
114
+ mockGetDocument.mockResolvedValue(null);
115
+
116
+ const result = await store.getAttempts('owner-1', 'accessor-1', 'mem-1');
117
+
118
+ expect(result).toBeNull();
119
+ });
120
+
121
+ it('returns attempt record when document exists', async () => {
122
+ mockGetDocument.mockResolvedValue({
123
+ attempt_count: 2,
124
+ last_attempt_at: '2026-02-27T01:00:00.000Z',
125
+ });
126
+
127
+ const result = await store.getAttempts('owner-1', 'accessor-1', 'mem-1');
128
+
129
+ expect(result).toEqual({
130
+ count: 2,
131
+ last_attempt_at: '2026-02-27T01:00:00.000Z',
132
+ });
133
+ });
134
+
135
+ it('returns null on Firestore error', async () => {
136
+ mockGetDocument.mockRejectedValue(new Error('Firestore unavailable'));
137
+
138
+ const result = await store.getAttempts('owner-1', 'accessor-1', 'mem-1');
139
+
140
+ expect(result).toBeNull();
141
+ });
142
+ });
143
+
144
+ describe('incrementAttempts', () => {
145
+ it('creates new attempt record when none exists', async () => {
146
+ mockGetDocument.mockResolvedValue(null);
147
+ mockSetDocument.mockResolvedValue(undefined);
148
+
149
+ const result = await store.incrementAttempts('owner-1', 'accessor-1', 'mem-1');
150
+
151
+ expect(result.count).toBe(1);
152
+ expect(result.last_attempt_at).toBeDefined();
153
+ expect(mockSetDocument).toHaveBeenCalledWith(
154
+ 'test-remember-mcp.users/owner-1/ghost_escalation',
155
+ 'accessor-1:mem-1',
156
+ expect.objectContaining({
157
+ attempt_count: 1,
158
+ accessor_user_id: 'accessor-1',
159
+ memory_id: 'mem-1',
160
+ }),
161
+ { merge: true }
162
+ );
163
+ });
164
+
165
+ it('increments existing attempt count', async () => {
166
+ mockGetDocument.mockResolvedValue({
167
+ attempt_count: 2,
168
+ last_attempt_at: '2026-02-27T00:00:00.000Z',
169
+ });
170
+ mockSetDocument.mockResolvedValue(undefined);
171
+
172
+ const result = await store.incrementAttempts('owner-1', 'accessor-1', 'mem-1');
173
+
174
+ expect(result.count).toBe(3);
175
+ expect(mockSetDocument).toHaveBeenCalledWith(
176
+ 'test-remember-mcp.users/owner-1/ghost_escalation',
177
+ 'accessor-1:mem-1',
178
+ expect.objectContaining({ attempt_count: 3 }),
179
+ { merge: true }
180
+ );
181
+ });
182
+ });
183
+ });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Firestore-backed Escalation Store
3
+ *
4
+ * Persists trust escalation tracking (attempt counts, blocks) to Firestore.
5
+ * Replaces InMemoryEscalationStore for production use.
6
+ *
7
+ * Firestore path: {BASE}.users/{ownerUserId}/ghost_escalation/{accessorUserId}:{memoryId}
8
+ *
9
+ * See agent/design/local.ghost-persona-system.md
10
+ */
11
+
12
+ import { getDocument, setDocument, deleteDocument } from '../firestore/init.js';
13
+ import { BASE } from '../firestore/paths.js';
14
+ import { logger } from '../utils/logger.js';
15
+ import type { EscalationStore, MemoryBlock, AttemptRecord } from './access-control.js';
16
+
17
+ const SERVICE = 'EscalationService';
18
+
19
+ /**
20
+ * Get Firestore collection path for escalation data.
21
+ */
22
+ function getEscalationPath(ownerUserId: string): string {
23
+ return `${BASE}.users/${ownerUserId}/ghost_escalation`;
24
+ }
25
+
26
+ /**
27
+ * Build a document ID from accessor and memory.
28
+ * Uses colon separator (safe in Firestore doc IDs).
29
+ */
30
+ function docId(accessorUserId: string, memoryId: string): string {
31
+ return `${accessorUserId}:${memoryId}`;
32
+ }
33
+
34
+ /**
35
+ * Firestore-backed escalation store for production use.
36
+ */
37
+ export class FirestoreEscalationStore implements EscalationStore {
38
+ async getBlock(ownerUserId: string, accessorUserId: string, memoryId: string): Promise<MemoryBlock | null> {
39
+ try {
40
+ const doc = await getDocument(
41
+ getEscalationPath(ownerUserId),
42
+ docId(accessorUserId, memoryId)
43
+ );
44
+
45
+ if (!doc || !doc.blocked) {
46
+ return null;
47
+ }
48
+
49
+ return {
50
+ blocked_at: doc.blocked_at,
51
+ reason: doc.reason || 'Trust escalation limit reached',
52
+ attempt_count: doc.attempt_count || 0,
53
+ };
54
+ } catch (error) {
55
+ logger.error('Failed to get block', {
56
+ service: SERVICE,
57
+ ownerUserId,
58
+ accessorUserId,
59
+ memoryId,
60
+ error: error instanceof Error ? error.message : String(error),
61
+ });
62
+ return null;
63
+ }
64
+ }
65
+
66
+ async setBlock(ownerUserId: string, accessorUserId: string, memoryId: string, block: MemoryBlock): Promise<void> {
67
+ await setDocument(
68
+ getEscalationPath(ownerUserId),
69
+ docId(accessorUserId, memoryId),
70
+ {
71
+ blocked: true,
72
+ blocked_at: block.blocked_at,
73
+ reason: block.reason,
74
+ attempt_count: block.attempt_count,
75
+ accessor_user_id: accessorUserId,
76
+ memory_id: memoryId,
77
+ },
78
+ { merge: true }
79
+ );
80
+
81
+ logger.info('User blocked from memory', {
82
+ service: SERVICE,
83
+ ownerUserId,
84
+ accessorUserId,
85
+ memoryId,
86
+ reason: block.reason,
87
+ });
88
+ }
89
+
90
+ async removeBlock(ownerUserId: string, accessorUserId: string, memoryId: string): Promise<void> {
91
+ await deleteDocument(
92
+ getEscalationPath(ownerUserId),
93
+ docId(accessorUserId, memoryId)
94
+ );
95
+
96
+ logger.info('Block removed', {
97
+ service: SERVICE,
98
+ ownerUserId,
99
+ accessorUserId,
100
+ memoryId,
101
+ });
102
+ }
103
+
104
+ async getAttempts(ownerUserId: string, accessorUserId: string, memoryId: string): Promise<AttemptRecord | null> {
105
+ try {
106
+ const doc = await getDocument(
107
+ getEscalationPath(ownerUserId),
108
+ docId(accessorUserId, memoryId)
109
+ );
110
+
111
+ if (!doc || doc.attempt_count === undefined) {
112
+ return null;
113
+ }
114
+
115
+ return {
116
+ count: doc.attempt_count,
117
+ last_attempt_at: doc.last_attempt_at,
118
+ };
119
+ } catch (error) {
120
+ logger.error('Failed to get attempts', {
121
+ service: SERVICE,
122
+ ownerUserId,
123
+ accessorUserId,
124
+ memoryId,
125
+ error: error instanceof Error ? error.message : String(error),
126
+ });
127
+ return null;
128
+ }
129
+ }
130
+
131
+ async incrementAttempts(ownerUserId: string, accessorUserId: string, memoryId: string): Promise<AttemptRecord> {
132
+ const existing = await this.getAttempts(ownerUserId, accessorUserId, memoryId);
133
+ const newCount = (existing?.count ?? 0) + 1;
134
+ const now = new Date().toISOString();
135
+
136
+ await setDocument(
137
+ getEscalationPath(ownerUserId),
138
+ docId(accessorUserId, memoryId),
139
+ {
140
+ attempt_count: newCount,
141
+ last_attempt_at: now,
142
+ accessor_user_id: accessorUserId,
143
+ memory_id: memoryId,
144
+ },
145
+ { merge: true }
146
+ );
147
+
148
+ return { count: newCount, last_attempt_at: now };
149
+ }
150
+ }