cloudflare-mcp-smart-proxy 1.5.5 → 1.5.6

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/README.md CHANGED
@@ -114,8 +114,10 @@ npx -y -p cloudflare-mcp-smart-proxy cloudmcp-connector activate codex \
114
114
  - `get_local_memory_target`:读取当前连接器工作空间的本机目标契约。
115
115
  - `verify_local_memory_target`:回读验证目标契约或一个精确条目。
116
116
  - `capture_memory`:以同一事务写入受治理的 CloudMCP memory plane 和已配置本地目标,双端回读一致后才返回成功。
117
+ - `invalidate_memory`:以 revision、原因和证据软失效一个精确条目,并把同一 canonical JSON 提交到本地与云端。
118
+ - `close_session`:把简洁交接转换为 `working + handoff`,复用与 `capture_memory` 相同的双端事务。
117
119
 
118
- `capture_memory` 只有在当前连接器通过云端鉴权、工具能力授权及精确 `memory_scope` 资源授权后才会被本地协调器暴露。它仍复用原代理的 API Key、设备签名和 CloudMCP 请求路由,不存在匿名云端写入口。
120
+ `capture_memory / invalidate_memory / close_session` 只有在当前连接器通过云端鉴权、对应工具能力授权及精确 `memory_scope` 资源授权后才会被本地协调器暴露。它们仍复用原代理的 API Key、设备签名和 CloudMCP 请求路由,不存在匿名云端写入口。`search_memory / get_memory_entry / load_context` 保持受治理云端只读工具,由同一授权链提供。
119
121
 
120
122
  本地目录不是硬编码的。以 SoloMap 为目标时,用户或 Agent 可把其记忆根目录显式传给配置工具;未安装 CloudMCP 的 SoloMap 用户继续使用原有 Markdown 机制,公共 SoloMap 插件不会默认依赖这些工具。
121
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-mcp-smart-proxy",
3
- "version": "1.5.5",
3
+ "version": "1.5.6",
4
4
  "description": "Smart proxy for Cloudflare MCP - routes tools to cloud or local execution",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,13 +10,6 @@ import {
10
10
  validateMemoryIdentifier
11
11
  } from './memory-schema.js';
12
12
 
13
- const SENSITIVE_MEMORY_PATTERNS = [
14
- /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
15
- /\bCLOUDMCP_APPROVAL_CODE\s*=/i,
16
- /\b(?:api[_ -]?key|access[_ -]?token|client[_ -]?secret|private[_ -]?key)\s*[:=]\s*\S{12,}/i,
17
- /\bmcp_[A-Za-z0-9]{16,}\b/
18
- ];
19
-
20
13
  function writeJsonAtomically(filePath, value, mode = 0o600) {
21
14
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
22
15
  const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
@@ -65,12 +58,6 @@ function currentGitCommit(workspaceRoot) {
65
58
  }
66
59
  }
67
60
 
68
- function assertNoSensitiveMemory(input) {
69
- const text = [input.title, input.content, ...(input.evidenceRefs || [])].filter(Boolean).join('\n');
70
- if (SENSITIVE_MEMORY_PATTERNS.some((pattern) => pattern.test(text))) {
71
- throw new Error('Memory content appears to contain a secret or approval credential');
72
- }
73
- }
74
61
 
75
62
  export class LocalMemoryCoordinator {
76
63
  constructor(options = {}) {
@@ -200,7 +187,6 @@ export class LocalMemoryCoordinator {
200
187
  const contract = this.getContract();
201
188
  if (!contract || contract.status !== 'active') throw new Error('Local memory target is not configured');
202
189
  const input = validateMemoryCaptureInput(params);
203
- assertNoSensitiveMemory(input);
204
190
  const requestedScope = params.memory_scope_id || params.memoryScopeId || '';
205
191
  if (requestedScope && !contract.cloudScopeIds.includes(requestedScope)) {
206
192
  throw new Error(`Memory scope is not authorized by the local target contract: ${requestedScope}`);
@@ -332,4 +318,75 @@ export class LocalMemoryCoordinator {
332
318
  );
333
319
  }
334
320
  }
321
+
322
+ async invalidate(params = {}) {
323
+ const contract = this.getContract();
324
+ if (!contract || contract.status !== 'active') throw new Error('Local memory target is not configured');
325
+ const memoryId = validateMemoryIdentifier(params.memory_id || params.memoryId, 'memory_id');
326
+ const requestedScope = params.memory_scope_id || params.memoryScopeId || '';
327
+ if (requestedScope && !contract.cloudScopeIds.includes(requestedScope)) {
328
+ throw new Error(`Memory scope is not authorized by the local target contract: ${requestedScope}`);
329
+ }
330
+ let stage = 'cloud_write';
331
+ try {
332
+ const result = await this.callCloud('invalidate_memory', {
333
+ memory_scope_id: requestedScope || undefined,
334
+ memory_id: memoryId,
335
+ expected_revision: params.expected_revision || params.expectedRevision,
336
+ reason: params.reason,
337
+ evidence_refs: params.evidence_refs || params.evidenceRefs
338
+ });
339
+ if (result?.verified !== true) throw new Error(`Cloud memory did not confirm invalidation readback: ${memoryId}`);
340
+ const entry = assertMemoryEntryIntegrity(result.entry);
341
+ if (!contract.cloudScopeIds.includes(entry.scopeId) || entry.status !== 'invalidated') {
342
+ throw new Error(`CloudMCP returned an invalid memory invalidation: ${memoryId}`);
343
+ }
344
+ stage = 'local_write';
345
+ const entryPath = path.join(contract.directory, 'entries', `${entry.id}.json`);
346
+ writeJsonAtomically(entryPath, entry);
347
+ stage = 'local_readback';
348
+ const localEntry = assertMemoryEntryIntegrity(readJson(entryPath));
349
+ if (canonicalMemoryJson(localEntry) !== canonicalMemoryJson(entry)) {
350
+ throw new Error(`Local memory invalidation readback failed for ${entry.id}`);
351
+ }
352
+ return {
353
+ success: true,
354
+ memoryId: entry.id,
355
+ revision: entry.revision,
356
+ canonicalHash: entry.canonicalHash,
357
+ idempotent: result.idempotent === true,
358
+ local: { persisted: true, verified: true },
359
+ cloud: { persisted: true, verified: true },
360
+ entry
361
+ };
362
+ } catch (error) {
363
+ const cloudStatus = stage === 'cloud_write' ? 'unknown' : 'verified';
364
+ const localStatus = stage === 'cloud_write' ? 'not_started' : (stage === 'local_write' ? 'failed' : 'persisted_unverified');
365
+ throw new Error(
366
+ `Memory invalidation ${memoryId} failed at ${stage} `
367
+ + `(cloud=${cloudStatus}, local=${localStatus}): ${error?.message || 'unknown error'}`
368
+ );
369
+ }
370
+ }
371
+
372
+ async closeSession(params = {}) {
373
+ const summary = typeof params.summary === 'string' ? params.summary.trim() : '';
374
+ if (!summary) throw new Error('summary is required');
375
+ const nextSteps = (Array.isArray(params.next_steps) ? params.next_steps : [])
376
+ .map((step) => String(step).trim())
377
+ .filter(Boolean);
378
+ const content = nextSteps.length
379
+ ? `${summary}\n\nNext steps:\n${nextSteps.map((step) => `- ${String(step).trim()}`).join('\n')}`
380
+ : summary;
381
+ const captured = await this.capture({
382
+ memory_scope_id: params.memory_scope_id || params.memoryScopeId,
383
+ layer: 'working',
384
+ kind: 'handoff',
385
+ title: params.title || 'Session handoff',
386
+ content,
387
+ tags: params.tags,
388
+ transaction_id: params.transaction_id || params.transactionId
389
+ });
390
+ return { ...captured, closed: true };
391
+ }
335
392
  }
@@ -97,6 +97,14 @@ export class LocalToolExecutor {
97
97
  case 'capture_memory':
98
98
  if (!this.memoryCoordinator) throw new Error('Local memory coordinator is not configured');
99
99
  return await this.memoryCoordinator.capture(params || {});
100
+
101
+ case 'invalidate_memory':
102
+ if (!this.memoryCoordinator) throw new Error('Local memory coordinator is not configured');
103
+ return await this.memoryCoordinator.invalidate(params || {});
104
+
105
+ case 'close_session':
106
+ if (!this.memoryCoordinator) throw new Error('Local memory coordinator is not configured');
107
+ return await this.memoryCoordinator.closeSession(params || {});
100
108
 
101
109
  default:
102
110
  throw new Error(`Unknown local tool: ${toolName}`);
@@ -136,7 +144,7 @@ export class LocalToolExecutor {
136
144
  },
137
145
  {
138
146
  name: 'capture_memory',
139
- description: 'Validate one memory and commit the same JSON entry to the configured local target and governed CloudMCP memory scope.',
147
+ description: 'Validate one governed memory and commit the same canonical JSON entry to the configured local target and CloudMCP scope.',
140
148
  inputSchema: {
141
149
  type: 'object',
142
150
  properties: {
@@ -158,6 +166,37 @@ export class LocalToolExecutor {
158
166
  required: ['content']
159
167
  }
160
168
  },
169
+ {
170
+ name: 'invalidate_memory',
171
+ description: 'Soft-invalidate one governed memory and verify the same canonical result in CloudMCP and the configured local target.',
172
+ inputSchema: {
173
+ type: 'object',
174
+ properties: {
175
+ memory_scope_id: { type: 'string' },
176
+ memory_id: { type: 'string' },
177
+ expected_revision: { type: 'number' },
178
+ reason: { type: 'string' },
179
+ evidence_refs: { type: 'array', items: { type: 'string' } }
180
+ },
181
+ required: ['memory_id', 'expected_revision', 'reason', 'evidence_refs']
182
+ }
183
+ },
184
+ {
185
+ name: 'close_session',
186
+ description: 'Persist a working handoff through the same verified local-and-cloud memory transaction.',
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ memory_scope_id: { type: 'string' },
191
+ title: { type: 'string' },
192
+ summary: { type: 'string' },
193
+ next_steps: { type: 'array', items: { type: 'string' } },
194
+ tags: { type: 'array', items: { type: 'string' } },
195
+ transaction_id: { type: 'string' }
196
+ },
197
+ required: ['summary']
198
+ }
199
+ },
161
200
  {
162
201
  name: 'read_file',
163
202
  description: 'Read a file from local filesystem',
@@ -5,6 +5,17 @@ const MEMORY_KINDS = new Set([
5
5
  'preference', 'rule', 'project_fact', 'decision', 'pattern', 'domain', 'handoff', 'observation'
6
6
  ]);
7
7
  const MEMORY_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
8
+ const MEMORY_KIND_LAYERS = new Map([
9
+ ['preference', 'stable'], ['rule', 'stable'], ['project_fact', 'stable'],
10
+ ['decision', 'stable'], ['pattern', 'stable'], ['domain', 'stable'],
11
+ ['handoff', 'working'], ['observation', 'candidate']
12
+ ]);
13
+ const SENSITIVE_MEMORY_PATTERNS = [
14
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
15
+ /\bCLOUDMCP_APPROVAL_CODE\s*=/i,
16
+ /\b(?:api[_ -]?key|access[_ -]?token|client[_ -]?secret|private[_ -]?key)\s*[:=]\s*\S{12,}/i,
17
+ /\bmcp_[A-Za-z0-9]{16,}\b/
18
+ ];
8
19
 
9
20
  function normalizeString(value, fallback = null) {
10
21
  const normalized = typeof value === 'string' ? value.trim() : '';
@@ -28,6 +39,14 @@ function normalizeStringArray(value, limit = 32) {
28
39
  ));
29
40
  }
30
41
 
42
+ export function assertNoSensitiveMemory(input = {}) {
43
+ const text = [input.title, input.content, ...(input.evidenceRefs || input.evidence_refs || [])]
44
+ .filter(Boolean).join('\n');
45
+ if (SENSITIVE_MEMORY_PATTERNS.some((pattern) => pattern.test(text))) {
46
+ throw new Error('Memory content appears to contain a secret or approval credential');
47
+ }
48
+ }
49
+
31
50
  function sortValue(value) {
32
51
  if (Array.isArray(value)) return value.map(sortValue);
33
52
  if (!value || typeof value !== 'object') return value;
@@ -58,15 +77,18 @@ export function validateMemoryCaptureInput(payload = {}) {
58
77
  if (content.length > 12000) throw new Error('Memory content exceeds 12000 characters');
59
78
  if (title && title.length > 240) throw new Error('Memory title exceeds 240 characters');
60
79
  const layer = normalizeString(payload.layer, 'candidate');
80
+ if (layer === 'stable' && !normalizeString(payload.kind)) throw new Error('Stable memory requires an explicit kind');
61
81
  const kind = normalizeString(payload.kind, layer === 'working' ? 'handoff' : 'observation');
62
82
  if (!MEMORY_LAYERS.has(layer)) throw new Error(`Unsupported memory layer: ${layer}`);
63
83
  if (!MEMORY_KINDS.has(kind)) throw new Error(`Unsupported memory kind: ${kind}`);
84
+ const requiredLayer = MEMORY_KIND_LAYERS.get(kind);
85
+ if (requiredLayer !== layer) throw new Error(`Memory kind ${kind} must use the ${requiredLayer} layer`);
64
86
  const evidenceRefs = normalizeStringArray(payload.evidence_refs || payload.evidenceRefs, 20);
65
87
  const verifiedAt = normalizeIsoDate(payload.verified_at || payload.verifiedAt);
66
- if (layer === 'stable' && evidenceRefs.length === 0 && !verifiedAt) {
67
- throw new Error('Stable memory requires evidence_refs or verified_at');
88
+ if (layer === 'stable' && evidenceRefs.length === 0) {
89
+ throw new Error('Stable memory requires at least one evidence_ref');
68
90
  }
69
- return {
91
+ const normalized = {
70
92
  title: title || 'Untitled memory',
71
93
  content,
72
94
  layer,
@@ -79,6 +101,8 @@ export function validateMemoryCaptureInput(payload = {}) {
79
101
  supersedes: normalizeStringArray(payload.supersedes, 20)
80
102
  .map((id) => validateMemoryIdentifier(id, 'supersedes entry id'))
81
103
  };
104
+ assertNoSensitiveMemory(normalized);
105
+ return normalized;
82
106
  }
83
107
 
84
108
  export function assertMemoryEntryIntegrity(entry) {
package/src/router.js CHANGED
@@ -72,7 +72,9 @@ export class SmartRouter {
72
72
  'configure_local_memory_target',
73
73
  'get_local_memory_target',
74
74
  'verify_local_memory_target',
75
- 'capture_memory'
75
+ 'capture_memory',
76
+ 'invalidate_memory',
77
+ 'close_session'
76
78
  ],
77
79
 
78
80
  // 云端工具(需要网络或云端资源)
@@ -294,7 +296,10 @@ export class SmartRouter {
294
296
  }
295
297
 
296
298
  const cloudNames = new Set(cloudTools.map((tool) => tool.name));
297
- const eligibleLocalTools = localTools.filter((tool) => tool.name !== 'capture_memory' || cloudNames.has('capture_memory'));
299
+ const governedLocalMemoryTools = new Set(['capture_memory', 'invalidate_memory', 'close_session']);
300
+ const eligibleLocalTools = localTools.filter((tool) => (
301
+ !governedLocalMemoryTools.has(tool.name) || cloudNames.has(tool.name)
302
+ ));
298
303
  const localNames = new Set(eligibleLocalTools.map((tool) => tool.name));
299
304
  return [
300
305
  ...cloudTools.filter((tool) => !localNames.has(tool.name)).map((tool) => ({ ...tool, source: 'cloud' })),