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