@prmichaelsen/remember-mcp 2.2.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT.md +98 -5
- package/CHANGELOG.md +45 -0
- package/README.md +43 -3
- package/agent/commands/acp.init.md +376 -0
- package/agent/commands/acp.package-install.md +347 -0
- package/agent/commands/acp.proceed.md +311 -0
- package/agent/commands/acp.report.md +392 -0
- package/agent/commands/acp.status.md +280 -0
- package/agent/commands/acp.sync.md +323 -0
- package/agent/commands/acp.update.md +301 -0
- package/agent/commands/acp.validate.md +385 -0
- package/agent/commands/acp.version-check-for-updates.md +275 -0
- package/agent/commands/acp.version-check.md +190 -0
- package/agent/commands/acp.version-update.md +288 -0
- package/agent/commands/command.template.md +273 -0
- package/agent/design/core-memory-user-profile.md +1253 -0
- package/agent/design/ghost-profiles-pseudonymous-identity.md +194 -0
- package/agent/design/publish-tools-confirmation-flow.md +922 -0
- package/agent/milestones/milestone-10-shared-spaces.md +169 -0
- package/agent/progress.yaml +90 -4
- package/agent/scripts/install.sh +118 -0
- package/agent/scripts/update.sh +22 -10
- package/agent/scripts/version.sh +35 -0
- package/agent/tasks/task-27-implement-llm-provider-interface.md +51 -0
- package/agent/tasks/task-28-implement-llm-provider-factory.md +64 -0
- package/agent/tasks/task-29-update-config-for-llm.md +71 -0
- package/agent/tasks/task-30-implement-bedrock-provider.md +147 -0
- package/agent/tasks/task-31-implement-background-job-service.md +120 -0
- package/agent/tasks/task-32-test-llm-provider-integration.md +152 -0
- package/agent/tasks/task-34-create-confirmation-token-service.md +191 -0
- package/agent/tasks/task-35-create-space-memory-types-schema.md +183 -0
- package/agent/tasks/task-36-implement-remember-publish.md +227 -0
- package/agent/tasks/task-37-implement-remember-confirm.md +225 -0
- package/agent/tasks/task-38-implement-remember-deny.md +161 -0
- package/agent/tasks/task-39-implement-remember-search-space.md +188 -0
- package/agent/tasks/task-40-implement-remember-query-space.md +193 -0
- package/agent/tasks/task-41-configure-firestore-ttl.md +188 -0
- package/agent/tasks/task-42-create-tests-shared-spaces.md +216 -0
- package/agent/tasks/task-43-update-documentation.md +255 -0
- package/agent/tasks/task-44-implement-remember-retract.md +263 -0
- package/agent/tasks/task-45-fix-publish-false-success-bug.md +230 -0
- package/dist/llm/types.d.ts +1 -0
- package/dist/server-factory.js +1000 -1
- package/dist/server.js +1002 -3
- package/dist/services/confirmation-token.service.d.ts +99 -0
- package/dist/services/confirmation-token.service.spec.d.ts +5 -0
- package/dist/tools/confirm.d.ts +20 -0
- package/dist/tools/deny.d.ts +19 -0
- package/dist/tools/publish.d.ts +22 -0
- package/dist/tools/query-space.d.ts +28 -0
- package/dist/tools/search-space.d.ts +29 -0
- package/dist/types/space-memory.d.ts +80 -0
- package/dist/weaviate/space-schema.d.ts +59 -0
- package/dist/weaviate/space-schema.spec.d.ts +5 -0
- package/package.json +1 -1
- package/src/llm/types.ts +0 -0
- package/src/server-factory.ts +33 -0
- package/src/server.ts +33 -0
- package/src/services/confirmation-token.service.spec.ts +254 -0
- package/src/services/confirmation-token.service.ts +265 -0
- package/src/tools/confirm.ts +219 -0
- package/src/tools/create-memory.ts +7 -0
- package/src/tools/deny.ts +70 -0
- package/src/tools/publish.ts +190 -0
- package/src/tools/query-space.ts +197 -0
- package/src/tools/search-space.ts +189 -0
- package/src/types/space-memory.ts +94 -0
- package/src/weaviate/space-schema.spec.ts +131 -0
- package/src/weaviate/space-schema.ts +275 -0
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
# Generic Confirmation Flow - Token-Based Action Execution
|
|
2
|
+
|
|
3
|
+
**Concept**: Generic token-based confirmation system for any sensitive operation
|
|
4
|
+
**Created**: 2026-02-16
|
|
5
|
+
**Status**: Design Specification
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
A generic confirmation-based system for executing sensitive operations. Uses one-time tokens to ensure user consent. All parameters are captured and stored, making the flow natural and organized.
|
|
12
|
+
|
|
13
|
+
**Core Tools**:
|
|
14
|
+
1. `remember_publish` - Publish a memory to a shared space (generates token with stored params)
|
|
15
|
+
2. `remember_confirm` - Generic confirmation tool that executes any pending action
|
|
16
|
+
3. `remember_deny` - Generic denial tool for any pending action
|
|
17
|
+
4. `remember_search_space` - Search and discover memories from shared spaces
|
|
18
|
+
5. `remember_query_space` - RAG-optimized natural language queries across shared spaces
|
|
19
|
+
|
|
20
|
+
**Key Design Principles**:
|
|
21
|
+
- **ANY command requiring confirmation** generates a token and stores ALL parameters
|
|
22
|
+
- Action tools (like `remember_publish`, `remember_retract`, etc.) follow this pattern
|
|
23
|
+
- Generic `remember_confirm` executes ANY stored action
|
|
24
|
+
- Generic `remember_deny` denies ANY stored action
|
|
25
|
+
- Makes agent flow natural: request → ask user → confirm/deny
|
|
26
|
+
|
|
27
|
+
**Examples of Confirmable Actions**:
|
|
28
|
+
- `remember_publish` - Publish memory to shared space
|
|
29
|
+
- `remember_retract` - Unpublish memory from shared space (future)
|
|
30
|
+
- Any other sensitive operation requiring user confirmation
|
|
31
|
+
|
|
32
|
+
**Supported Spaces**:
|
|
33
|
+
- `the_void` - "The Void" (shared discovery space)
|
|
34
|
+
- Space ID: `the_void` (snake_case)
|
|
35
|
+
- Collection Name: `Memory_the_void`
|
|
36
|
+
- Display Name: "The Void" (can use any case/spaces)
|
|
37
|
+
- Extensible to other spaces as needed
|
|
38
|
+
|
|
39
|
+
**Naming Convention**:
|
|
40
|
+
- Display names can have spaces and mixed case: "The Void", "Public Space"
|
|
41
|
+
- Space IDs are snake_case (lowercase with underscores): `the_void`, `public_space`
|
|
42
|
+
- Collection names follow pattern: `Memory_{snake_case_id}`
|
|
43
|
+
- Conversion: "The Void" → lowercase → replace spaces → `the_void` → `Memory_the_void`
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Problem Statement
|
|
48
|
+
|
|
49
|
+
Publishing memories to shared spaces is a sensitive operation that requires explicit user consent. We need:
|
|
50
|
+
- **User confirmation** before publishing to shared collections
|
|
51
|
+
- **One-time tokens** to prevent replay attacks
|
|
52
|
+
- **Request/confirm flow** that agents can use naturally
|
|
53
|
+
- **Flexibility** to publish to different collections (void, public, etc.)
|
|
54
|
+
- **Auditability** of what was requested vs. what was confirmed
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Solution: Token-Based Confirmation Flow
|
|
59
|
+
|
|
60
|
+
### Flow Diagram
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Agent: "I'd like to publish this memory to The Void"
|
|
64
|
+
↓
|
|
65
|
+
Tool: remember_publish(memory_id, target="void")
|
|
66
|
+
↓
|
|
67
|
+
System: Validates memory, generates one-time token, stores all parameters
|
|
68
|
+
↓
|
|
69
|
+
Response: { status: "pending", token: "abc123", payload: {...} }
|
|
70
|
+
↓
|
|
71
|
+
Agent: "User, do you want to publish this to The Void?"
|
|
72
|
+
↓
|
|
73
|
+
User: "Yes" → Agent calls remember_confirm(token="abc123")
|
|
74
|
+
User: "No" → Agent calls remember_deny(token="abc123")
|
|
75
|
+
↓
|
|
76
|
+
System: Validates token, fetches memory fresh, executes or denies action
|
|
77
|
+
↓
|
|
78
|
+
Response: { status: "confirmed", payload: {...} } or { status: "denied", payload: {...} }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Architecture
|
|
84
|
+
|
|
85
|
+
### Token Management
|
|
86
|
+
|
|
87
|
+
**Storage**: Firestore collection `pending_confirmations/{user_id}/requests/{request_id}`
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
interface PendingConfirmation {
|
|
91
|
+
request_id: string;
|
|
92
|
+
user_id: string;
|
|
93
|
+
token: string; // One-time use token (UUID)
|
|
94
|
+
action: string; // 'publish_to_void', 'publish_to_public', etc.
|
|
95
|
+
target_collection: string; // 'void', 'public', etc.
|
|
96
|
+
payload: any; // The data to be published
|
|
97
|
+
created_at: Timestamp;
|
|
98
|
+
expires_at: Timestamp; // 5 minutes from creation
|
|
99
|
+
status: 'pending' | 'confirmed' | 'denied' | 'expired' | 'retracted';
|
|
100
|
+
confirmed_at?: Timestamp;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Token Properties**:
|
|
105
|
+
- One-time use (deleted after confirm/deny)
|
|
106
|
+
- Expires after 5 minutes
|
|
107
|
+
- Cryptographically random (UUID v4)
|
|
108
|
+
- Scoped to user_id
|
|
109
|
+
- Stores complete action payload
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Tool Definitions
|
|
114
|
+
|
|
115
|
+
### 1. remember_publish
|
|
116
|
+
|
|
117
|
+
Publish a memory to a shared collection. Generates a confirmation token with all parameters stored.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
{
|
|
121
|
+
name: 'remember_publish',
|
|
122
|
+
description: 'Publish a memory to a shared collection (like The Void). The memory will be COPIED (not moved) from your personal collection. Generates a confirmation token. Use remember_confirm to execute.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
memory_id: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description: 'ID of the memory from your personal collection to publish'
|
|
129
|
+
},
|
|
130
|
+
target: {
|
|
131
|
+
type: 'string',
|
|
132
|
+
description: 'Target space to publish to (snake_case ID)',
|
|
133
|
+
enum: ['the_void'],
|
|
134
|
+
default: 'the_void'
|
|
135
|
+
},
|
|
136
|
+
additional_tags: {
|
|
137
|
+
type: 'array',
|
|
138
|
+
items: { type: 'string' },
|
|
139
|
+
description: 'Additional tags for discovery (merged with original tags)',
|
|
140
|
+
default: []
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
required: ['memory_id', 'target']
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Response** (Generic format):
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"success": true,
|
|
152
|
+
"token": "550e8400-e29b-41d4-a716-446655440000",
|
|
153
|
+
"payload": {
|
|
154
|
+
"action": "publish_memory",
|
|
155
|
+
"memory_id": "uuid-original",
|
|
156
|
+
"target": "void",
|
|
157
|
+
"additional_tags": []
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Error Response**:
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"success": false,
|
|
166
|
+
"error": "Memory not found",
|
|
167
|
+
"message": "No memory found with ID: uuid-original",
|
|
168
|
+
"context": {
|
|
169
|
+
"collection_name": "Memory_User_123",
|
|
170
|
+
"collection_exists": true,
|
|
171
|
+
"query_executed": true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Note**: All parameters are stored with the token. Content is fetched fresh during confirmation. Status is implied by success flag.
|
|
177
|
+
|
|
178
|
+
### 2. remember_confirm
|
|
179
|
+
|
|
180
|
+
Generic confirmation tool that executes any pending action.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
{
|
|
184
|
+
name: 'remember_confirm',
|
|
185
|
+
description: 'Confirm and execute a pending action using the token. Works for any action that requires confirmation (publish, delete, etc.).',
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: {
|
|
189
|
+
token: {
|
|
190
|
+
type: 'string',
|
|
191
|
+
description: 'The confirmation token from the action tool'
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
required: ['token']
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Response** (Minimal - only essential info):
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"success": true,
|
|
203
|
+
"payload": {
|
|
204
|
+
"action": "publish_memory",
|
|
205
|
+
"space": "void",
|
|
206
|
+
"space_memory_id": "uuid-new-in-void"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Error Response**:
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"success": false,
|
|
215
|
+
"error": "Invalid or expired token",
|
|
216
|
+
"message": "The confirmation token is invalid, expired, or has already been used.",
|
|
217
|
+
"context": {
|
|
218
|
+
"token_found": false,
|
|
219
|
+
"token_expired": false,
|
|
220
|
+
"token_already_used": true,
|
|
221
|
+
"used_at": "2026-02-16T03:15:00Z"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Note**: Response is minimal. Agent already knows the original memory details, so only the new space_memory_id is returned.
|
|
227
|
+
|
|
228
|
+
### 3. remember_deny
|
|
229
|
+
|
|
230
|
+
Generic denial tool for any pending action.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
{
|
|
234
|
+
name: 'remember_deny',
|
|
235
|
+
description: 'Deny a pending action. The request will be marked as denied and the token invalidated. Works for any action that requires confirmation.',
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {
|
|
239
|
+
token: {
|
|
240
|
+
type: 'string',
|
|
241
|
+
description: 'The confirmation token from the action tool'
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
required: ['token']
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Response**:
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"success": true
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Error Response**:
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"success": false,
|
|
260
|
+
"error": "Invalid token",
|
|
261
|
+
"message": "Token not found or already used",
|
|
262
|
+
"context": {
|
|
263
|
+
"token_found": false,
|
|
264
|
+
"checked_requests": 5,
|
|
265
|
+
"last_valid_request": "2026-02-16T03:10:00Z"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Note**: Denial is simple - just confirms the action was denied. No payload needed.
|
|
271
|
+
|
|
272
|
+
### 4. remember_search_space
|
|
273
|
+
|
|
274
|
+
Search and discover memories from shared spaces. Same as `remember_search_memory` but with a `space` parameter.
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
{
|
|
278
|
+
name: 'remember_search_space',
|
|
279
|
+
description: 'Search shared spaces to discover thoughts, ideas, and memories. Works like remember_search_memory but searches shared spaces instead of personal memories.',
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: 'object',
|
|
282
|
+
properties: {
|
|
283
|
+
query: {
|
|
284
|
+
type: 'string',
|
|
285
|
+
description: 'Search query (semantic + keyword hybrid)'
|
|
286
|
+
},
|
|
287
|
+
space: {
|
|
288
|
+
type: 'string',
|
|
289
|
+
description: 'Which space to search',
|
|
290
|
+
enum: ['void'],
|
|
291
|
+
default: 'void'
|
|
292
|
+
},
|
|
293
|
+
// Same filters as remember_search_memory
|
|
294
|
+
content_type: { type: 'string' },
|
|
295
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
296
|
+
min_weight: { type: 'number', minimum: 0, maximum: 1 },
|
|
297
|
+
max_weight: { type: 'number', minimum: 0, maximum: 1 },
|
|
298
|
+
date_from: { type: 'string' },
|
|
299
|
+
date_to: { type: 'string' },
|
|
300
|
+
limit: { type: 'number', default: 10 },
|
|
301
|
+
offset: { type: 'number', default: 0 }
|
|
302
|
+
},
|
|
303
|
+
required: ['query', 'space']
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Note**: Uses same input schema as [`remember_search_memory`](../src/tools/search-memory.ts) plus `space` parameter.
|
|
309
|
+
|
|
310
|
+
### 5. remember_query_space
|
|
311
|
+
|
|
312
|
+
RAG-optimized queries for shared spaces. Same as `remember_query_memory` but with a `space` parameter.
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
{
|
|
316
|
+
name: 'remember_query_space',
|
|
317
|
+
description: 'Ask natural language questions about memories in shared spaces. Works like remember_query_memory but queries shared spaces.',
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: 'object',
|
|
320
|
+
properties: {
|
|
321
|
+
question: {
|
|
322
|
+
type: 'string',
|
|
323
|
+
description: 'Natural language question'
|
|
324
|
+
},
|
|
325
|
+
space: {
|
|
326
|
+
type: 'string',
|
|
327
|
+
description: 'Which space to query',
|
|
328
|
+
enum: ['void'],
|
|
329
|
+
default: 'void'
|
|
330
|
+
},
|
|
331
|
+
// Same filters as remember_query_memory
|
|
332
|
+
content_type: { type: 'string' },
|
|
333
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
334
|
+
min_weight: { type: 'number', minimum: 0, maximum: 1 },
|
|
335
|
+
date_from: { type: 'string' },
|
|
336
|
+
date_to: { type: 'string' },
|
|
337
|
+
limit: { type: 'number', default: 10 },
|
|
338
|
+
format: { type: 'string', enum: ['detailed', 'compact'], default: 'detailed' }
|
|
339
|
+
},
|
|
340
|
+
required: ['question', 'space']
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Note**: Uses same input schema as [`remember_query_memory`](../src/tools/query-memory.ts) plus `space` parameter.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Space Memory Architecture
|
|
350
|
+
|
|
351
|
+
### Memory Types
|
|
352
|
+
|
|
353
|
+
**Personal Memory**: Scoped to `user_id`, stored in `Memory_{user_id}` collections
|
|
354
|
+
```typescript
|
|
355
|
+
interface Memory {
|
|
356
|
+
id: string;
|
|
357
|
+
user_id: string; // Owner
|
|
358
|
+
content: string;
|
|
359
|
+
// ... standard fields
|
|
360
|
+
doc_type: 'memory';
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Space Memory**: Shared across users, stored in `Memory_{space_name}` collections
|
|
365
|
+
```typescript
|
|
366
|
+
interface SpaceMemory {
|
|
367
|
+
id: string;
|
|
368
|
+
space_id: string; // 'void', 'public', etc. (replaces user_id)
|
|
369
|
+
author_id: string; // Original author (for permissions)
|
|
370
|
+
ghost_id?: string; // Optional: published as ghost
|
|
371
|
+
content: string;
|
|
372
|
+
published_at: string;
|
|
373
|
+
discovery_count: number;
|
|
374
|
+
// ... standard fields
|
|
375
|
+
doc_type: 'space_memory';
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Weaviate Collections
|
|
380
|
+
|
|
381
|
+
- **Personal**: `Memory_User_123` (single user, sanitized user_id)
|
|
382
|
+
- **Spaces**: `Memory_the_void`, `Memory_public_space` (shared, snake_case space IDs)
|
|
383
|
+
- **Consistent naming**: `Memory_{identifier}` pattern
|
|
384
|
+
- User collections: `Memory_{sanitized_user_id}` (e.g., `Memory_User_123`)
|
|
385
|
+
- Space collections: `Memory_{snake_case_space_id}` (e.g., `Memory_the_void`, `Memory_public_space`)
|
|
386
|
+
|
|
387
|
+
**Display Names vs Collection IDs**:
|
|
388
|
+
- Display Name: "The Void" → Space ID: `the_void` → Collection: `Memory_the_void`
|
|
389
|
+
- Display Name: "Public Space" → Space ID: `public_space` → Collection: `Memory_public_space`
|
|
390
|
+
- Conversion: Display name → lowercase → replace spaces with underscores → prepend `Memory_`
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Implementation Details
|
|
395
|
+
|
|
396
|
+
### Token Service
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
// src/services/confirmation-token.service.ts
|
|
400
|
+
|
|
401
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
402
|
+
import { getFirestore } from '../firestore/init.js';
|
|
403
|
+
import { Timestamp } from 'firebase-admin/firestore';
|
|
404
|
+
|
|
405
|
+
export interface ConfirmationRequest {
|
|
406
|
+
// request_id is the Firestore document ID (not stored in document)
|
|
407
|
+
user_id: string;
|
|
408
|
+
token: string;
|
|
409
|
+
action: string;
|
|
410
|
+
target_collection?: string;
|
|
411
|
+
payload: any;
|
|
412
|
+
created_at: Timestamp;
|
|
413
|
+
expires_at: Timestamp;
|
|
414
|
+
status: 'pending' | 'confirmed' | 'denied' | 'expired' | 'retracted';
|
|
415
|
+
confirmed_at?: Timestamp;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export class ConfirmationTokenService {
|
|
419
|
+
private readonly EXPIRY_MINUTES = 5;
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Create a new confirmation request
|
|
423
|
+
*/
|
|
424
|
+
async createRequest(
|
|
425
|
+
userId: string,
|
|
426
|
+
action: string,
|
|
427
|
+
payload: any,
|
|
428
|
+
targetCollection?: string
|
|
429
|
+
): Promise<{ requestId: string; token: string }> {
|
|
430
|
+
const db = getFirestore();
|
|
431
|
+
const token = uuidv4();
|
|
432
|
+
|
|
433
|
+
const now = Timestamp.now();
|
|
434
|
+
const expiresAt = Timestamp.fromMillis(
|
|
435
|
+
now.toMillis() + this.EXPIRY_MINUTES * 60 * 1000
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const request: ConfirmationRequest = {
|
|
439
|
+
user_id: userId,
|
|
440
|
+
token,
|
|
441
|
+
action,
|
|
442
|
+
target_collection: targetCollection,
|
|
443
|
+
payload,
|
|
444
|
+
created_at: now,
|
|
445
|
+
expires_at: expiresAt,
|
|
446
|
+
status: 'pending',
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// Let Firestore generate the document ID (this IS the request_id)
|
|
450
|
+
const docRef = await db
|
|
451
|
+
.collection('pending_confirmations')
|
|
452
|
+
.doc(userId)
|
|
453
|
+
.collection('requests')
|
|
454
|
+
.add(request);
|
|
455
|
+
|
|
456
|
+
return { requestId: docRef.id, token };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Validate and retrieve a confirmation request
|
|
461
|
+
*/
|
|
462
|
+
async validateToken(
|
|
463
|
+
userId: string,
|
|
464
|
+
token: string
|
|
465
|
+
): Promise<ConfirmationRequest | null> {
|
|
466
|
+
const db = getFirestore();
|
|
467
|
+
|
|
468
|
+
const snapshot = await db
|
|
469
|
+
.collection('pending_confirmations')
|
|
470
|
+
.doc(userId)
|
|
471
|
+
.collection('requests')
|
|
472
|
+
.where('token', '==', token)
|
|
473
|
+
.where('status', '==', 'pending')
|
|
474
|
+
.limit(1)
|
|
475
|
+
.get();
|
|
476
|
+
|
|
477
|
+
if (snapshot.empty) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const request = snapshot.docs[0].data() as ConfirmationRequest;
|
|
482
|
+
|
|
483
|
+
// Check expiry
|
|
484
|
+
if (request.expires_at.toMillis() < Date.now()) {
|
|
485
|
+
await this.updateStatus(userId, request.request_id, 'expired');
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return request;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Confirm a request
|
|
494
|
+
*/
|
|
495
|
+
async confirmRequest(
|
|
496
|
+
userId: string,
|
|
497
|
+
token: string
|
|
498
|
+
): Promise<ConfirmationRequest | null> {
|
|
499
|
+
const request = await this.validateToken(userId, token);
|
|
500
|
+
if (!request) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
await this.updateStatus(userId, request.request_id, 'confirmed');
|
|
505
|
+
return request;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Deny a request
|
|
510
|
+
*/
|
|
511
|
+
async denyRequest(
|
|
512
|
+
userId: string,
|
|
513
|
+
token: string
|
|
514
|
+
): Promise<boolean> {
|
|
515
|
+
const request = await this.validateToken(userId, token);
|
|
516
|
+
if (!request) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
await this.updateStatus(userId, request.request_id, 'denied');
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Retract a request
|
|
526
|
+
*/
|
|
527
|
+
async retractRequest(
|
|
528
|
+
userId: string,
|
|
529
|
+
token: string
|
|
530
|
+
): Promise<boolean> {
|
|
531
|
+
const request = await this.validateToken(userId, token);
|
|
532
|
+
if (!request) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
await this.updateStatus(userId, request.request_id, 'retracted');
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Update request status
|
|
542
|
+
*/
|
|
543
|
+
private async updateStatus(
|
|
544
|
+
userId: string,
|
|
545
|
+
requestId: string,
|
|
546
|
+
status: ConfirmationRequest['status']
|
|
547
|
+
): Promise<void> {
|
|
548
|
+
const db = getFirestore();
|
|
549
|
+
|
|
550
|
+
await db
|
|
551
|
+
.collection('pending_confirmations')
|
|
552
|
+
.doc(userId)
|
|
553
|
+
.collection('requests')
|
|
554
|
+
.doc(requestId)
|
|
555
|
+
.update({
|
|
556
|
+
status,
|
|
557
|
+
confirmed_at: status === 'confirmed' ? Timestamp.now() : null,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Clean up expired requests (optional - Firestore TTL handles deletion)
|
|
563
|
+
*
|
|
564
|
+
* Note: Configure Firestore TTL policy on 'requests' collection group
|
|
565
|
+
* with 'expires_at' field for automatic deletion within 24 hours.
|
|
566
|
+
*
|
|
567
|
+
* This method is optional for immediate cleanup if needed.
|
|
568
|
+
*/
|
|
569
|
+
async cleanupExpired(): Promise<number> {
|
|
570
|
+
const db = getFirestore();
|
|
571
|
+
const now = Timestamp.now();
|
|
572
|
+
|
|
573
|
+
const snapshot = await db
|
|
574
|
+
.collectionGroup('requests')
|
|
575
|
+
.where('status', '==', 'pending')
|
|
576
|
+
.where('expires_at', '<', now)
|
|
577
|
+
.get();
|
|
578
|
+
|
|
579
|
+
const batch = db.batch();
|
|
580
|
+
snapshot.docs.forEach(doc => {
|
|
581
|
+
batch.delete(doc.ref); // Delete instead of just updating status
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
await batch.commit();
|
|
585
|
+
return snapshot.size;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export const confirmationTokenService = new ConfirmationTokenService();
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### Tool Implementation: remember_publish
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
// src/tools/publish.ts
|
|
596
|
+
|
|
597
|
+
import { confirmationTokenService } from '../services/confirmation-token.service.js';
|
|
598
|
+
import { getWeaviateClient, getMemoryCollectionName } from '../weaviate/client.js';
|
|
599
|
+
import { handleToolError } from '../utils/error-handler.js';
|
|
600
|
+
|
|
601
|
+
export const publishTool = {
|
|
602
|
+
name: 'remember_publish',
|
|
603
|
+
description: 'Publish a memory to a shared space. Generates a confirmation token.',
|
|
604
|
+
inputSchema: {
|
|
605
|
+
// ... (as defined above)
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
export async function handlePublish(
|
|
610
|
+
args: PublishArgs,
|
|
611
|
+
userId: string
|
|
612
|
+
): Promise<string> {
|
|
613
|
+
try {
|
|
614
|
+
// Verify memory exists and user owns it
|
|
615
|
+
const weaviateClient = getWeaviateClient();
|
|
616
|
+
const userCollection = weaviateClient.collections.get(getMemoryCollectionName(userId));
|
|
617
|
+
|
|
618
|
+
const memory = await userCollection.query.fetchObjectById(args.memory_id);
|
|
619
|
+
|
|
620
|
+
if (!memory) {
|
|
621
|
+
return JSON.stringify({
|
|
622
|
+
success: false,
|
|
623
|
+
error: 'Memory not found',
|
|
624
|
+
message: `No memory found with ID: ${args.memory_id}`,
|
|
625
|
+
context: {
|
|
626
|
+
collection_name: getMemoryCollectionName(userId),
|
|
627
|
+
collection_exists: true
|
|
628
|
+
}
|
|
629
|
+
}, null, 2);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Verify ownership
|
|
633
|
+
if (memory.properties.user_id !== userId) {
|
|
634
|
+
return JSON.stringify({
|
|
635
|
+
success: false,
|
|
636
|
+
error: 'Permission denied',
|
|
637
|
+
message: 'You can only publish your own memories',
|
|
638
|
+
}, null, 2);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Create payload with only memory_id (content fetched during confirmation)
|
|
642
|
+
const payload = {
|
|
643
|
+
memory_id: args.memory_id,
|
|
644
|
+
additional_tags: args.additional_tags || [],
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const { requestId, token } = await confirmationTokenService.createRequest(
|
|
648
|
+
userId,
|
|
649
|
+
'publish_memory',
|
|
650
|
+
payload,
|
|
651
|
+
args.target
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
return JSON.stringify({
|
|
655
|
+
success: true,
|
|
656
|
+
token,
|
|
657
|
+
payload: {
|
|
658
|
+
action: 'publish_memory',
|
|
659
|
+
memory_id: args.memory_id,
|
|
660
|
+
target: args.target,
|
|
661
|
+
additional_tags: payload.additional_tags
|
|
662
|
+
}
|
|
663
|
+
}, null, 2);
|
|
664
|
+
} catch (error) {
|
|
665
|
+
return handleToolError(error, 'remember_publish', {
|
|
666
|
+
userId,
|
|
667
|
+
memory_id: args.memory_id,
|
|
668
|
+
target: args.target
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### Tool Implementation: remember_confirm
|
|
675
|
+
|
|
676
|
+
```typescript
|
|
677
|
+
// src/tools/confirm.ts
|
|
678
|
+
|
|
679
|
+
import { confirmationTokenService } from '../services/confirmation-token.service.js';
|
|
680
|
+
import { getWeaviateClient, getMemoryCollectionName } from '../weaviate/client.js';
|
|
681
|
+
import { ensureSpaceCollection } from '../weaviate/space-schema.js';
|
|
682
|
+
import { handleToolError } from '../utils/error-handler.js';
|
|
683
|
+
|
|
684
|
+
export const confirmTool = {
|
|
685
|
+
name: 'remember_confirm',
|
|
686
|
+
description: 'Confirm and execute a pending action using the token.',
|
|
687
|
+
inputSchema: {
|
|
688
|
+
// ... (as defined above)
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
export async function handleConfirm(
|
|
693
|
+
args: ConfirmArgs,
|
|
694
|
+
userId: string
|
|
695
|
+
): Promise<string> {
|
|
696
|
+
try {
|
|
697
|
+
// Validate and confirm token
|
|
698
|
+
const request = await confirmationTokenService.confirmRequest(userId, args.token);
|
|
699
|
+
|
|
700
|
+
if (!request) {
|
|
701
|
+
return JSON.stringify({
|
|
702
|
+
success: false,
|
|
703
|
+
error: 'Invalid or expired token',
|
|
704
|
+
message: 'The confirmation token is invalid, expired, or has already been used.',
|
|
705
|
+
context: {
|
|
706
|
+
token_found: false,
|
|
707
|
+
token_expired: true
|
|
708
|
+
}
|
|
709
|
+
}, null, 2);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// GENERIC: Execute action based on type
|
|
713
|
+
// This is where the generic pattern delegates to action-specific executors
|
|
714
|
+
if (request.action === 'publish_memory') {
|
|
715
|
+
return await executePublishMemory(request, userId);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (request.action === 'retract_memory') {
|
|
719
|
+
return await executeRetractMemory(request, userId);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Add other action types here as needed
|
|
723
|
+
// Each action gets its own executor function
|
|
724
|
+
|
|
725
|
+
throw new Error(`Unknown action type: ${request.action}`);
|
|
726
|
+
|
|
727
|
+
} catch (error) {
|
|
728
|
+
return handleToolError(error, 'remember_confirm', { userId, token: args.token });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function executePublishMemory(request: ConfirmationRequest, userId: string): Promise<string> {
|
|
733
|
+
// Fetch the memory NOW (during confirmation, not from stored payload)
|
|
734
|
+
const weaviateClient = getWeaviateClient();
|
|
735
|
+
const userCollection = weaviateClient.collections.get(getMemoryCollectionName(userId));
|
|
736
|
+
|
|
737
|
+
const originalMemory = await userCollection.query.fetchObjectById(request.payload.memory_id);
|
|
738
|
+
|
|
739
|
+
if (!originalMemory) {
|
|
740
|
+
return JSON.stringify({
|
|
741
|
+
success: false,
|
|
742
|
+
error: 'Memory not found',
|
|
743
|
+
message: `Original memory ${request.payload.memory_id} no longer exists`,
|
|
744
|
+
}, null, 2);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Verify ownership again
|
|
748
|
+
if (originalMemory.properties.user_id !== userId) {
|
|
749
|
+
return JSON.stringify({
|
|
750
|
+
success: false,
|
|
751
|
+
error: 'Permission denied',
|
|
752
|
+
message: 'You can only publish your own memories',
|
|
753
|
+
}, null, 2);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Get target collection (generic)
|
|
757
|
+
const targetCollection = await ensureSpaceCollection(
|
|
758
|
+
weaviateClient,
|
|
759
|
+
request.target_collection || 'void'
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
// Create published memory (copy with modifications)
|
|
763
|
+
const publishedMemory = {
|
|
764
|
+
...originalMemory.properties,
|
|
765
|
+
// Override specific fields
|
|
766
|
+
user_id: request.target_collection || 'void',
|
|
767
|
+
author_id: userId, // Always attributed
|
|
768
|
+
published_at: new Date().toISOString(),
|
|
769
|
+
discovery_count: 0,
|
|
770
|
+
doc_type: `${request.target_collection}_memory`,
|
|
771
|
+
trust_level: 1.0,
|
|
772
|
+
// Merge additional tags
|
|
773
|
+
tags: [
|
|
774
|
+
...(originalMemory.properties.tags || []),
|
|
775
|
+
...(request.payload.additional_tags || [])
|
|
776
|
+
],
|
|
777
|
+
// Update timestamps
|
|
778
|
+
created_at: new Date().toISOString(),
|
|
779
|
+
updated_at: new Date().toISOString(),
|
|
780
|
+
version: 1,
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
const result = await targetCollection.data.insert(publishedMemory);
|
|
784
|
+
|
|
785
|
+
return JSON.stringify({
|
|
786
|
+
success: true,
|
|
787
|
+
payload: {
|
|
788
|
+
action: 'publish_memory',
|
|
789
|
+
space: request.target_collection,
|
|
790
|
+
space_memory_id: result
|
|
791
|
+
}
|
|
792
|
+
}, null, 2);
|
|
793
|
+
} catch (error) {
|
|
794
|
+
return handleToolError(error, 'remember_confirm', { userId, action: 'publish_memory' });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
---
|
|
800
|
+
|
|
801
|
+
## Benefits
|
|
802
|
+
|
|
803
|
+
1. **User Control**: Explicit confirmation required for sensitive operations
|
|
804
|
+
2. **Security**: One-time tokens prevent replay attacks
|
|
805
|
+
3. **Auditability**: All requests logged with status tracking
|
|
806
|
+
4. **Flexibility**: Can publish to different collections (void, public, etc.)
|
|
807
|
+
5. **Natural Flow**: Agent can explain and request confirmation naturally
|
|
808
|
+
6. **Extensible**: Token pattern can be used for other confirmable actions
|
|
809
|
+
|
|
810
|
+
---
|
|
811
|
+
|
|
812
|
+
## Trade-offs
|
|
813
|
+
|
|
814
|
+
### Pros
|
|
815
|
+
- ✅ Explicit user consent for publications
|
|
816
|
+
- ✅ Prevents accidental or malicious publications
|
|
817
|
+
- ✅ Auditable request/confirmation trail
|
|
818
|
+
- ✅ Flexible target collections
|
|
819
|
+
- ✅ Token expiry prevents stale requests
|
|
820
|
+
|
|
821
|
+
### Cons
|
|
822
|
+
- ❌ More complex than direct publication
|
|
823
|
+
- ❌ Requires two tool calls (request + confirm)
|
|
824
|
+
- ❌ Tokens need cleanup (expired requests)
|
|
825
|
+
- ❌ Additional Firestore storage for pending requests
|
|
826
|
+
|
|
827
|
+
---
|
|
828
|
+
|
|
829
|
+
## Pattern Extension - Other Confirmable Actions
|
|
830
|
+
|
|
831
|
+
This pattern can be extended to ANY sensitive operation:
|
|
832
|
+
|
|
833
|
+
### Example: remember_retract
|
|
834
|
+
|
|
835
|
+
```typescript
|
|
836
|
+
{
|
|
837
|
+
name: 'remember_retract',
|
|
838
|
+
description: 'Unpublish a memory from a shared space. Generates confirmation token. Use remember_confirm to execute.',
|
|
839
|
+
inputSchema: {
|
|
840
|
+
type: 'object',
|
|
841
|
+
properties: {
|
|
842
|
+
space_memory_id: {
|
|
843
|
+
type: 'string',
|
|
844
|
+
description: 'ID of the memory in the shared space to retract'
|
|
845
|
+
},
|
|
846
|
+
space: {
|
|
847
|
+
type: 'string',
|
|
848
|
+
description: 'Which space to retract from',
|
|
849
|
+
enum: ['void']
|
|
850
|
+
}
|
|
851
|
+
},
|
|
852
|
+
required: ['space_memory_id', 'space']
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
**Response**:
|
|
858
|
+
```json
|
|
859
|
+
{
|
|
860
|
+
"success": true,
|
|
861
|
+
"token": "uuid",
|
|
862
|
+
"payload": {
|
|
863
|
+
"action": "retract_memory",
|
|
864
|
+
"space_memory_id": "uuid-in-void",
|
|
865
|
+
"space": "void"
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
**Key Pattern**:
|
|
871
|
+
1. Any sensitive action creates a tool that generates a token
|
|
872
|
+
2. All parameters stored in Firestore with the token
|
|
873
|
+
3. Generic `remember_confirm` executes the action
|
|
874
|
+
4. Generic `remember_deny` cancels the action
|
|
875
|
+
|
|
876
|
+
---
|
|
877
|
+
|
|
878
|
+
## Future Enhancements
|
|
879
|
+
|
|
880
|
+
1. **Batch Confirmations**: Confirm multiple requests with one token
|
|
881
|
+
2. **Conditional Confirmations**: Auto-confirm based on user preferences
|
|
882
|
+
3. **Request History**: View past confirmation requests
|
|
883
|
+
4. **Timeout Warnings**: Notify when tokens are about to expire
|
|
884
|
+
|
|
885
|
+
---
|
|
886
|
+
|
|
887
|
+
## Implementation Tasks
|
|
888
|
+
|
|
889
|
+
1. Create `src/services/confirmation-token.service.ts` - Token management
|
|
890
|
+
2. Create `src/weaviate/space-schema.ts` - Generic space collection schema (Memory_Void, Memory_Public, etc.)
|
|
891
|
+
3. Create `src/types/space-memory.ts` - SpaceMemory type definitions
|
|
892
|
+
4. Create `src/tools/publish.ts` - Publish tool (generates token)
|
|
893
|
+
5. Create `src/tools/confirm.ts` - Generic confirm tool
|
|
894
|
+
6. Create `src/tools/deny.ts` - Generic deny tool
|
|
895
|
+
7. Create `src/tools/search-space.ts` - Search shared spaces
|
|
896
|
+
8. Create `src/tools/query-space.ts` - Query shared spaces
|
|
897
|
+
9. Update `src/server.ts` - Register all tools
|
|
898
|
+
10. Update `src/server-factory.ts` - Register all tools
|
|
899
|
+
11. Create unit tests for token service
|
|
900
|
+
12. Create unit tests for all tools
|
|
901
|
+
13. **Configure Firestore TTL policy** on `requests` collection group with `expires_at` field
|
|
902
|
+
14. Optional: Add manual cleanup job for immediate expiry
|
|
903
|
+
15. Update README.md with new tools
|
|
904
|
+
16. Test end-to-end flow
|
|
905
|
+
|
|
906
|
+
**Firestore TTL Configuration**:
|
|
907
|
+
- Go to Cloud Firestore Time-to-live page in GCP Console
|
|
908
|
+
- Create policy for collection group: `requests`
|
|
909
|
+
- TTL field: `expires_at`
|
|
910
|
+
- Documents automatically deleted within 24 hours after expiration
|
|
911
|
+
|
|
912
|
+
---
|
|
913
|
+
|
|
914
|
+
**Status**: Implemented (v2.3.0)
|
|
915
|
+
**Recommendation**: Token-based confirmation pattern successfully implemented and production-ready
|
|
916
|
+
|
|
917
|
+
**Implementation Notes**:
|
|
918
|
+
- All 5 tools implemented and registered in both servers
|
|
919
|
+
- Token service uses `users/{user_id}/requests` for consistency
|
|
920
|
+
- Firestore TTL configured on collection group `requests`
|
|
921
|
+
- 100% test coverage on token service and space schema
|
|
922
|
+
- Snake_case naming convention: "The Void" → `the_void` → `Memory_the_void`
|