cloudflare-mcp-smart-proxy 1.5.4 → 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
@@ -106,6 +106,21 @@ npx -y -p cloudflare-mcp-smart-proxy cloudmcp-connector activate codex \
106
106
  - [CLOUDMCP_A2_EXTERNAL_CONNECTOR_CONTRACT_STANDARD_2026-03-30.md](/home/coder/project/CLOUDMCP/docs/CLOUDMCP_A2_EXTERNAL_CONNECTOR_CONTRACT_STANDARD_2026-03-30.md)
107
107
  - [CLOUDMCP_A3_OFFICIAL_REFERENCE_CONNECTOR_STANDARD_2026-03-30.md](/home/coder/project/CLOUDMCP/docs/CLOUDMCP_A3_OFFICIAL_REFERENCE_CONNECTOR_STANDARD_2026-03-30.md)
108
108
 
109
+ ## 本地记忆目标
110
+
111
+ 原有 `cloudflare-mcp-smart-proxy` 包同时提供以下本地工具:
112
+
113
+ - `configure_local_memory_target`:显式配置一个本地 JSON 记忆目录,并从当前连接器获授权的云端 scope 初始化有效条目。
114
+ - `get_local_memory_target`:读取当前连接器工作空间的本机目标契约。
115
+ - `verify_local_memory_target`:回读验证目标契约或一个精确条目。
116
+ - `capture_memory`:以同一事务写入受治理的 CloudMCP memory plane 和已配置本地目标,双端回读一致后才返回成功。
117
+ - `invalidate_memory`:以 revision、原因和证据软失效一个精确条目,并把同一 canonical JSON 提交到本地与云端。
118
+ - `close_session`:把简洁交接转换为 `working + handoff`,复用与 `capture_memory` 相同的双端事务。
119
+
120
+ `capture_memory / invalidate_memory / close_session` 只有在当前连接器通过云端鉴权、对应工具能力授权及精确 `memory_scope` 资源授权后才会被本地协调器暴露。它们仍复用原代理的 API Key、设备签名和 CloudMCP 请求路由,不存在匿名云端写入口。`search_memory / get_memory_entry / load_context` 保持受治理云端只读工具,由同一授权链提供。
121
+
122
+ 本地目录不是硬编码的。以 SoloMap 为目标时,用户或 Agent 可把其记忆根目录显式传给配置工具;未安装 CloudMCP 的 SoloMap 用户继续使用原有 Markdown 机制,公共 SoloMap 插件不会默认依赖这些工具。
123
+
109
124
  ## 常用命令
110
125
 
111
126
  ### Dry-run 渲染配置
@@ -137,6 +152,7 @@ node connector-cli.js reload codex
137
152
  node --test /home/coder/project/CLOUDMCP/tests/reference-connector-a3.test.js \
138
153
  /home/coder/project/CLOUDMCP/tests/codex-app-server-reload.test.js \
139
154
  /home/coder/project/CLOUDMCP/tests/local-proxy-device-identity.test.js \
155
+ /home/coder/project/CLOUDMCP/tests/local-proxy-memory.test.js \
140
156
  /home/coder/project/CLOUDMCP/tests/local-proxy-tool-discovery.test.js
141
157
  ```
142
158
 
package/index.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  import { sanitizeProxyError, SmartRouter } from './src/router.js';
17
17
  import { LocalToolExecutor } from './src/local-tools.js';
18
18
  import { ConnectorBridge } from './src/connector-bridge.js';
19
+ import { LocalMemoryCoordinator } from './src/local-memory-coordinator.js';
19
20
  import fs from 'fs';
20
21
 
21
22
  // 从环境变量读取配置
@@ -68,8 +69,16 @@ const connectorBridge = new ConnectorBridge({
68
69
  workspaceId: WORKSPACE_ID,
69
70
  deviceIdentityPath: DEVICE_IDENTITY_PATH
70
71
  });
71
- const localTools = new LocalToolExecutor(WORKSPACE_ROOT, connectorBridge);
72
+ const memoryCoordinator = new LocalMemoryCoordinator({
73
+ workspaceRoot: WORKSPACE_ROOT,
74
+ cloudUrl: CLOUD_URL,
75
+ clientProfileId: CLIENT_PROFILE_ID,
76
+ workspaceId: WORKSPACE_ID,
77
+ connectorId: CONNECTOR_ID
78
+ });
79
+ const localTools = new LocalToolExecutor(WORKSPACE_ROOT, connectorBridge, memoryCoordinator);
72
80
  const router = new SmartRouter(CLOUD_URL, CLOUD_API_KEY, localTools, WORKSPACE_ROOT, connectorBridge.deviceIdentity);
81
+ localTools.setCloudToolCaller((tool, params) => router.callCloudTool(tool, params));
73
82
 
74
83
  // 创建 MCP 服务器
75
84
  const server = new Server(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-mcp-smart-proxy",
3
- "version": "1.5.4",
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",
@@ -0,0 +1,392 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { execFileSync } from 'node:child_process';
6
+ import {
7
+ assertMemoryEntryIntegrity,
8
+ canonicalMemoryJson,
9
+ validateMemoryCaptureInput,
10
+ validateMemoryIdentifier
11
+ } from './memory-schema.js';
12
+
13
+ function writeJsonAtomically(filePath, value, mode = 0o600) {
14
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
15
+ const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
16
+ try {
17
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', flag: 'wx', mode });
18
+ fs.renameSync(temporaryPath, filePath);
19
+ if (process.platform !== 'win32') fs.chmodSync(filePath, mode);
20
+ } catch (error) {
21
+ if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
22
+ throw error;
23
+ }
24
+ }
25
+
26
+ function readJson(filePath) {
27
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
28
+ }
29
+
30
+ function fingerprint(parts) {
31
+ return createHash('sha256').update(parts.join('\n')).digest('hex').slice(0, 24);
32
+ }
33
+
34
+ function resolveConfiguredDirectory(directory, workspaceRoot) {
35
+ const raw = typeof directory === 'string' ? directory.trim() : '';
36
+ if (!raw) throw new Error('directory is required');
37
+ if (/[*?\[\]{}]/.test(raw) || /\$\{|%[A-Za-z_][A-Za-z0-9_]*%/.test(raw) || raw.startsWith('~')) {
38
+ throw new Error('directory must be an explicit path without globs or unresolved variables');
39
+ }
40
+ const resolved = path.resolve(workspaceRoot, raw);
41
+ const filesystemRoot = path.parse(resolved).root;
42
+ if (resolved === filesystemRoot || resolved === os.homedir()) {
43
+ throw new Error('directory must not be a filesystem root or the home directory');
44
+ }
45
+ fs.mkdirSync(resolved, { recursive: true });
46
+ return fs.realpathSync(resolved);
47
+ }
48
+
49
+ function currentGitCommit(workspaceRoot) {
50
+ try {
51
+ return execFileSync('git', ['rev-parse', 'HEAD'], {
52
+ cwd: workspaceRoot,
53
+ encoding: 'utf8',
54
+ stdio: ['ignore', 'pipe', 'ignore']
55
+ }).trim() || null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+
62
+ export class LocalMemoryCoordinator {
63
+ constructor(options = {}) {
64
+ this.workspaceRoot = path.resolve(options.workspaceRoot || process.cwd());
65
+ this.cloudUrl = String(options.cloudUrl || '').replace(/\/$/, '');
66
+ this.clientProfileId = options.clientProfileId || '';
67
+ this.workspaceId = options.workspaceId || '';
68
+ this.connectorId = options.connectorId || '';
69
+ this.homeDir = options.homeDir || os.homedir();
70
+ this.cloudToolCaller = options.cloudToolCaller || null;
71
+ const bindingFingerprint = fingerprint([
72
+ this.cloudUrl,
73
+ this.clientProfileId,
74
+ this.workspaceId,
75
+ this.connectorId,
76
+ this.workspaceRoot
77
+ ]);
78
+ this.privateRoot = path.join(this.homeDir, '.cloudmcp', 'memory-targets', bindingFingerprint);
79
+ this.contractPath = path.join(this.privateRoot, 'contract.json');
80
+ this.transactionsRoot = path.join(this.privateRoot, 'transactions');
81
+ }
82
+
83
+ setCloudToolCaller(caller) {
84
+ this.cloudToolCaller = caller;
85
+ }
86
+
87
+ async callCloud(tool, params) {
88
+ if (!this.cloudToolCaller) throw new Error('Cloud memory transport is not configured');
89
+ return await this.cloudToolCaller(tool, params);
90
+ }
91
+
92
+ getContract() {
93
+ if (!fs.existsSync(this.contractPath)) return null;
94
+ return readJson(this.contractPath);
95
+ }
96
+
97
+ async configure(params = {}) {
98
+ if (params.adapter !== 'solomap') throw new Error('Only the solomap memory adapter is currently supported');
99
+ const schemaVersion = Number(params.schema_version || params.schemaVersion || 1);
100
+ if (schemaVersion !== 1) throw new Error(`Unsupported local memory schema version: ${schemaVersion}`);
101
+ const directory = resolveConfiguredDirectory(params.directory, this.workspaceRoot);
102
+ const existing = this.getContract();
103
+ if (existing && existing.directory !== directory) {
104
+ throw new Error('A different local memory target is already active for this connector workspace');
105
+ }
106
+ const entriesRoot = path.join(directory, 'entries');
107
+ fs.mkdirSync(entriesRoot, { recursive: true });
108
+ const probePath = path.join(directory, `.cloudmcp-write-probe-${process.pid}.json`);
109
+ writeJsonAtomically(probePath, { ok: true });
110
+ const probe = readJson(probePath);
111
+ fs.unlinkSync(probePath);
112
+ if (probe.ok !== true) throw new Error('Local memory target write verification failed');
113
+
114
+ const scopesResult = await this.callCloud('list_memory_scopes', {});
115
+ const scopes = Array.isArray(scopesResult?.memory_scopes) ? scopesResult.memory_scopes : [];
116
+ if (scopes.length === 0) throw new Error('No governed CloudMCP memory scope is available');
117
+ const cloudScopeIds = scopes.map((scope) => scope.id).filter(Boolean);
118
+ const targetId = existing?.targetId || `memory_target_${randomUUID().replace(/-/g, '')}`;
119
+ const preparingContract = {
120
+ schemaVersion: 1,
121
+ targetId,
122
+ adapter: 'solomap',
123
+ format: 'cloudmcp-memory-json-v1',
124
+ workspaceId: this.workspaceId,
125
+ connectorId: this.connectorId,
126
+ cloudScopeIds,
127
+ status: 'preparing'
128
+ };
129
+ writeJsonAtomically(path.join(directory, 'contract.json'), preparingContract);
130
+ writeJsonAtomically(this.contractPath, { ...preparingContract, directory });
131
+
132
+ let hydratedEntries = 0;
133
+ for (const scopeId of cloudScopeIds) {
134
+ let offset = 0;
135
+ while (true) {
136
+ const loaded = await this.callCloud('load_context', {
137
+ memory_scope_id: scopeId,
138
+ include_layers: ['stable', 'working', 'candidate'],
139
+ limit: 50,
140
+ offset
141
+ });
142
+ const entries = Array.isArray(loaded?.entries) ? loaded.entries : [];
143
+ for (const entry of entries) {
144
+ assertMemoryEntryIntegrity(entry);
145
+ const entryPath = path.join(entriesRoot, `${entry.id}.json`);
146
+ if (fs.existsSync(entryPath)) {
147
+ const current = assertMemoryEntryIntegrity(readJson(entryPath));
148
+ if (current.canonicalHash !== entry.canonicalHash) {
149
+ throw new Error(`Local memory entry differs from CloudMCP: ${entry.id}`);
150
+ }
151
+ } else {
152
+ writeJsonAtomically(entryPath, entry);
153
+ }
154
+ hydratedEntries += 1;
155
+ }
156
+ if (!loaded?.pagination?.hasMore) break;
157
+ offset += entries.length;
158
+ if (entries.length === 0) break;
159
+ }
160
+ }
161
+ const publicContract = { ...preparingContract, status: 'active' };
162
+ writeJsonAtomically(path.join(directory, 'contract.json'), publicContract);
163
+ writeJsonAtomically(this.contractPath, { ...publicContract, directory });
164
+ return { configured: true, verified: true, contract: publicContract, hydratedEntries };
165
+ }
166
+
167
+ verify(params = {}) {
168
+ const contract = this.getContract();
169
+ if (!contract) throw new Error('Local memory target is not configured');
170
+ const marker = readJson(path.join(contract.directory, 'contract.json'));
171
+ if (marker.targetId !== contract.targetId || marker.schemaVersion !== 1 || marker.status !== 'active') {
172
+ throw new Error('Local memory target contract verification failed');
173
+ }
174
+ if (params.memory_id) {
175
+ const memoryId = validateMemoryIdentifier(params.memory_id, 'memory_id');
176
+ assertMemoryEntryIntegrity(readJson(path.join(contract.directory, 'entries', `${memoryId}.json`)));
177
+ }
178
+ return {
179
+ configured: true,
180
+ verified: true,
181
+ contract: { ...marker, directory: contract.directory },
182
+ memoryId: params.memory_id || null
183
+ };
184
+ }
185
+
186
+ async capture(params = {}) {
187
+ const contract = this.getContract();
188
+ if (!contract || contract.status !== 'active') throw new Error('Local memory target is not configured');
189
+ const input = validateMemoryCaptureInput(params);
190
+ const requestedScope = params.memory_scope_id || params.memoryScopeId || '';
191
+ if (requestedScope && !contract.cloudScopeIds.includes(requestedScope)) {
192
+ throw new Error(`Memory scope is not authorized by the local target contract: ${requestedScope}`);
193
+ }
194
+ const requestedTransactionId = params.transaction_id || params.transactionId;
195
+ const transactionId = requestedTransactionId
196
+ ? validateMemoryIdentifier(requestedTransactionId, 'transaction_id')
197
+ : `memory_txn_${randomUUID().replace(/-/g, '')}`;
198
+ const transactionPath = path.join(this.transactionsRoot, `${transactionId}.json`);
199
+ const existingTransaction = fs.existsSync(transactionPath) ? readJson(transactionPath) : null;
200
+ const requestedMemoryId = existingTransaction?.memoryId || params.memory_id || params.memoryId;
201
+ const memoryId = requestedMemoryId
202
+ ? validateMemoryIdentifier(requestedMemoryId, 'memory_id')
203
+ : `mem_${randomUUID().replace(/-/g, '')}`;
204
+ const semanticFingerprint = createHash('sha256').update(JSON.stringify(input)).digest('hex');
205
+ if (existingTransaction && existingTransaction.semanticFingerprint !== semanticFingerprint) {
206
+ throw new Error(`Memory transaction input changed: ${transactionId}`);
207
+ }
208
+ const gitCommit = existingTransaction?.gitCommit || currentGitCommit(this.workspaceRoot);
209
+ writeJsonAtomically(transactionPath, {
210
+ schemaVersion: 1,
211
+ transactionId,
212
+ memoryId,
213
+ gitCommit,
214
+ semanticFingerprint,
215
+ status: 'preparing',
216
+ createdAt: existingTransaction?.createdAt || new Date().toISOString(),
217
+ updatedAt: new Date().toISOString()
218
+ });
219
+
220
+ let stage = 'cloud_write';
221
+ try {
222
+ const cloudResult = await this.callCloud('capture_memory', {
223
+ memory_scope_id: requestedScope || undefined,
224
+ layer: input.layer,
225
+ kind: input.kind,
226
+ title: input.title,
227
+ content: input.content,
228
+ tags: input.tags,
229
+ evidence_refs: input.evidenceRefs,
230
+ verified_at: input.verifiedAt || undefined,
231
+ valid_from: input.validFrom || undefined,
232
+ valid_until: input.validUntil || undefined,
233
+ supersedes: input.supersedes,
234
+ memory_id: memoryId,
235
+ transaction_id: transactionId,
236
+ expected_revision: params.expected_revision || params.expectedRevision,
237
+ git_commit: gitCommit || undefined
238
+ });
239
+ if (cloudResult?.verified !== true) throw new Error(`Cloud memory did not confirm readback: ${memoryId}`);
240
+ const entry = assertMemoryEntryIntegrity(cloudResult.entry);
241
+ if (!contract.cloudScopeIds.includes(entry.scopeId)) {
242
+ throw new Error(`CloudMCP returned an unauthorized memory scope: ${entry.scopeId}`);
243
+ }
244
+ const supersededEntries = Array.isArray(cloudResult.superseded_entries)
245
+ ? cloudResult.superseded_entries.map((item) => assertMemoryEntryIntegrity(item))
246
+ : [];
247
+ if (supersededEntries.some((item) => item.scopeId !== entry.scopeId || item.status !== 'superseded')) {
248
+ throw new Error('CloudMCP returned an invalid superseded memory entry set');
249
+ }
250
+ stage = 'local_write';
251
+ const entryPath = path.join(contract.directory, 'entries', `${entry.id}.json`);
252
+ writeJsonAtomically(entryPath, entry);
253
+ for (const supersededEntry of supersededEntries) {
254
+ writeJsonAtomically(
255
+ path.join(contract.directory, 'entries', `${supersededEntry.id}.json`),
256
+ supersededEntry
257
+ );
258
+ }
259
+ stage = 'local_readback';
260
+ const localEntry = assertMemoryEntryIntegrity(readJson(entryPath));
261
+ if (canonicalMemoryJson(localEntry) !== canonicalMemoryJson(entry)) {
262
+ throw new Error(`Local memory readback verification failed for ${entry.id}`);
263
+ }
264
+ for (const supersededEntry of supersededEntries) {
265
+ const localSupersededEntry = assertMemoryEntryIntegrity(readJson(
266
+ path.join(contract.directory, 'entries', `${supersededEntry.id}.json`)
267
+ ));
268
+ if (canonicalMemoryJson(localSupersededEntry) !== canonicalMemoryJson(supersededEntry)) {
269
+ throw new Error(`Local memory readback verification failed for ${supersededEntry.id}`);
270
+ }
271
+ }
272
+ writeJsonAtomically(transactionPath, {
273
+ schemaVersion: 1,
274
+ transactionId,
275
+ memoryId,
276
+ semanticFingerprint,
277
+ gitCommit,
278
+ status: 'verified',
279
+ canonicalHash: entry.canonicalHash,
280
+ revision: entry.revision,
281
+ createdAt: existingTransaction?.createdAt || new Date().toISOString(),
282
+ updatedAt: new Date().toISOString()
283
+ });
284
+ return {
285
+ success: true,
286
+ memoryId: entry.id,
287
+ transactionId,
288
+ revision: entry.revision,
289
+ canonicalHash: entry.canonicalHash,
290
+ local: { persisted: true, verified: true },
291
+ cloud: { persisted: true, verified: true },
292
+ supersededMemoryIds: supersededEntries.map((item) => item.id),
293
+ entry
294
+ };
295
+ } catch (error) {
296
+ const cloudStatus = stage === 'cloud_write' ? 'unknown' : 'verified';
297
+ const localStatus = stage === 'cloud_write'
298
+ ? 'not_started'
299
+ : (stage === 'local_write' ? 'failed' : 'persisted_unverified');
300
+ try {
301
+ writeJsonAtomically(transactionPath, {
302
+ schemaVersion: 1,
303
+ transactionId,
304
+ memoryId,
305
+ semanticFingerprint,
306
+ gitCommit,
307
+ status: 'failed',
308
+ failedStage: stage,
309
+ cloudStatus,
310
+ localStatus,
311
+ createdAt: existingTransaction?.createdAt || new Date().toISOString(),
312
+ updatedAt: new Date().toISOString()
313
+ });
314
+ } catch {}
315
+ throw new Error(
316
+ `Memory transaction ${transactionId} failed at ${stage} `
317
+ + `(cloud=${cloudStatus}, local=${localStatus}): ${error?.message || 'unknown error'}`
318
+ );
319
+ }
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
+ }
392
+ }
@@ -10,9 +10,14 @@ import { SymbolTools } from './tools/symbol-tools.js';
10
10
  import { Diagnostics } from './tools/diagnostics.js';
11
11
 
12
12
  export class LocalToolExecutor {
13
- constructor(workspaceRoot, connectorBridge = null) {
13
+ constructor(workspaceRoot, connectorBridge = null, memoryCoordinator = null) {
14
14
  this.workspaceRoot = workspaceRoot;
15
15
  this.connectorBridge = connectorBridge;
16
+ this.memoryCoordinator = memoryCoordinator;
17
+ }
18
+
19
+ setCloudToolCaller(caller) {
20
+ this.memoryCoordinator?.setCloudToolCaller(caller);
16
21
  }
17
22
 
18
23
  /**
@@ -76,6 +81,30 @@ export class LocalToolExecutor {
76
81
  case 'connector_report_project_probe':
77
82
  if (!this.connectorBridge) throw new Error('Connector bridge is not configured');
78
83
  return await this.connectorBridge.reportProjectProbe(params || {});
84
+
85
+ case 'configure_local_memory_target':
86
+ if (!this.memoryCoordinator) throw new Error('Local memory coordinator is not configured');
87
+ return await this.memoryCoordinator.configure(params || {});
88
+
89
+ case 'get_local_memory_target':
90
+ if (!this.memoryCoordinator) throw new Error('Local memory coordinator is not configured');
91
+ return this.memoryCoordinator.getContract() || { configured: false };
92
+
93
+ case 'verify_local_memory_target':
94
+ if (!this.memoryCoordinator) throw new Error('Local memory coordinator is not configured');
95
+ return this.memoryCoordinator.verify(params || {});
96
+
97
+ case 'capture_memory':
98
+ if (!this.memoryCoordinator) throw new Error('Local memory coordinator is not configured');
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 || {});
79
108
 
80
109
  default:
81
110
  throw new Error(`Unknown local tool: ${toolName}`);
@@ -87,6 +116,87 @@ export class LocalToolExecutor {
87
116
  */
88
117
  listTools() {
89
118
  return [
119
+ {
120
+ name: 'configure_local_memory_target',
121
+ description: 'Configure and verify the exact local directory used for governed JSON memory writes.',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ adapter: { type: 'string', enum: ['solomap'] },
126
+ directory: { type: 'string' },
127
+ schema_version: { type: 'number', enum: [1] }
128
+ },
129
+ required: ['adapter', 'directory']
130
+ }
131
+ },
132
+ {
133
+ name: 'get_local_memory_target',
134
+ description: 'Read the active local memory target contract for this connector workspace.',
135
+ inputSchema: { type: 'object', properties: {} }
136
+ },
137
+ {
138
+ name: 'verify_local_memory_target',
139
+ description: 'Verify the active local memory target contract and optionally one exact memory entry.',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: { memory_id: { type: 'string' } }
143
+ }
144
+ },
145
+ {
146
+ name: 'capture_memory',
147
+ description: 'Validate one governed memory and commit the same canonical JSON entry to the configured local target and CloudMCP scope.',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: {
151
+ memory_scope_id: { type: 'string' },
152
+ layer: { type: 'string', enum: ['stable', 'working', 'candidate'] },
153
+ kind: { type: 'string', enum: ['preference', 'rule', 'project_fact', 'decision', 'pattern', 'domain', 'handoff', 'observation'] },
154
+ title: { type: 'string' },
155
+ content: { type: 'string' },
156
+ tags: { type: 'array', items: { type: 'string' } },
157
+ evidence_refs: { type: 'array', items: { type: 'string' } },
158
+ verified_at: { type: 'string' },
159
+ valid_from: { type: 'string' },
160
+ valid_until: { type: 'string' },
161
+ supersedes: { type: 'array', items: { type: 'string' } },
162
+ memory_id: { type: 'string' },
163
+ transaction_id: { type: 'string' },
164
+ expected_revision: { type: 'number' }
165
+ },
166
+ required: ['content']
167
+ }
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
+ },
90
200
  {
91
201
  name: 'read_file',
92
202
  description: 'Read a file from local filesystem',
@@ -0,0 +1,118 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const MEMORY_LAYERS = new Set(['stable', 'working', 'candidate']);
4
+ const MEMORY_KINDS = new Set([
5
+ 'preference', 'rule', 'project_fact', 'decision', 'pattern', 'domain', 'handoff', 'observation'
6
+ ]);
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
+ ];
19
+
20
+ function normalizeString(value, fallback = null) {
21
+ const normalized = typeof value === 'string' ? value.trim() : '';
22
+ return normalized || fallback;
23
+ }
24
+
25
+ export function validateMemoryIdentifier(value, label = 'memory_id') {
26
+ const normalized = normalizeString(value);
27
+ if (!normalized || !MEMORY_IDENTIFIER_PATTERN.test(normalized)) {
28
+ throw new Error(`${label} must be a portable identifier of 1-128 letters, digits, dots, underscores, or hyphens`);
29
+ }
30
+ return normalized;
31
+ }
32
+
33
+ function normalizeStringArray(value, limit = 32) {
34
+ return Array.from(new Set(
35
+ (Array.isArray(value) ? value : [])
36
+ .map((item) => normalizeString(item))
37
+ .filter(Boolean)
38
+ .slice(0, limit)
39
+ ));
40
+ }
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
+
50
+ function sortValue(value) {
51
+ if (Array.isArray(value)) return value.map(sortValue);
52
+ if (!value || typeof value !== 'object') return value;
53
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortValue(value[key])]));
54
+ }
55
+
56
+ export function canonicalMemoryJson(entry) {
57
+ const { canonicalHash: _canonicalHash, ...withoutHash } = entry || {};
58
+ return JSON.stringify(sortValue(withoutHash));
59
+ }
60
+
61
+ export function hashMemoryEntry(entry) {
62
+ return `sha256:${createHash('sha256').update(canonicalMemoryJson(entry)).digest('hex')}`;
63
+ }
64
+
65
+ function normalizeIsoDate(value) {
66
+ const normalized = normalizeString(value);
67
+ if (!normalized) return null;
68
+ const date = new Date(normalized);
69
+ if (!Number.isFinite(date.getTime())) throw new Error(`Invalid memory timestamp: ${normalized}`);
70
+ return date.toISOString();
71
+ }
72
+
73
+ export function validateMemoryCaptureInput(payload = {}) {
74
+ const title = normalizeString(payload.title);
75
+ const content = normalizeString(payload.content);
76
+ if (!content) throw new Error('Memory content is required');
77
+ if (content.length > 12000) throw new Error('Memory content exceeds 12000 characters');
78
+ if (title && title.length > 240) throw new Error('Memory title exceeds 240 characters');
79
+ const layer = normalizeString(payload.layer, 'candidate');
80
+ if (layer === 'stable' && !normalizeString(payload.kind)) throw new Error('Stable memory requires an explicit kind');
81
+ const kind = normalizeString(payload.kind, layer === 'working' ? 'handoff' : 'observation');
82
+ if (!MEMORY_LAYERS.has(layer)) throw new Error(`Unsupported memory layer: ${layer}`);
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`);
86
+ const evidenceRefs = normalizeStringArray(payload.evidence_refs || payload.evidenceRefs, 20);
87
+ const verifiedAt = normalizeIsoDate(payload.verified_at || payload.verifiedAt);
88
+ if (layer === 'stable' && evidenceRefs.length === 0) {
89
+ throw new Error('Stable memory requires at least one evidence_ref');
90
+ }
91
+ const normalized = {
92
+ title: title || 'Untitled memory',
93
+ content,
94
+ layer,
95
+ kind,
96
+ tags: normalizeStringArray(payload.tags, 24),
97
+ evidenceRefs,
98
+ verifiedAt,
99
+ validFrom: normalizeIsoDate(payload.valid_from || payload.validFrom),
100
+ validUntil: normalizeIsoDate(payload.valid_until || payload.validUntil),
101
+ supersedes: normalizeStringArray(payload.supersedes, 20)
102
+ .map((id) => validateMemoryIdentifier(id, 'supersedes entry id'))
103
+ };
104
+ assertNoSensitiveMemory(normalized);
105
+ return normalized;
106
+ }
107
+
108
+ export function assertMemoryEntryIntegrity(entry) {
109
+ if (!entry || entry.schemaVersion !== 1 || !entry.id || !entry.scopeId) {
110
+ throw new Error('CloudMCP returned an invalid memory entry');
111
+ }
112
+ validateMemoryIdentifier(entry.id, 'memory entry id');
113
+ const expectedHash = hashMemoryEntry(entry);
114
+ if (entry.canonicalHash !== expectedHash) {
115
+ throw new Error(`Memory canonical hash mismatch for ${entry.id}`);
116
+ }
117
+ return entry;
118
+ }
package/src/router.js CHANGED
@@ -68,7 +68,13 @@ export class SmartRouter {
68
68
  'connector_sync_profile',
69
69
  'connector_report_status',
70
70
  'connector_discover_project_probe',
71
- 'connector_report_project_probe'
71
+ 'connector_report_project_probe',
72
+ 'configure_local_memory_target',
73
+ 'get_local_memory_target',
74
+ 'verify_local_memory_target',
75
+ 'capture_memory',
76
+ 'invalidate_memory',
77
+ 'close_session'
72
78
  ],
73
79
 
74
80
  // 云端工具(需要网络或云端资源)
@@ -289,9 +295,15 @@ export class SmartRouter {
289
295
  throw new Error('CloudMCP returned no governed cloud tools');
290
296
  }
291
297
 
298
+ const cloudNames = new Set(cloudTools.map((tool) => tool.name));
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
+ ));
303
+ const localNames = new Set(eligibleLocalTools.map((tool) => tool.name));
292
304
  return [
293
- ...cloudTools.map(t => ({ ...t, source: 'cloud' })),
294
- ...localTools.map(t => ({ ...t, source: 'local' }))
305
+ ...cloudTools.filter((tool) => !localNames.has(tool.name)).map((tool) => ({ ...tool, source: 'cloud' })),
306
+ ...eligibleLocalTools.map((tool) => ({ ...tool, source: 'local' }))
295
307
  ];
296
308
  }
297
309