cloudflare-mcp-smart-proxy 1.5.5 → 1.5.7
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 +3 -1
- package/connector-cli.js +3 -0
- package/index.js +27 -11
- package/package.json +1 -1
- package/src/cloud-client.js +10 -1
- package/src/connector-bridge.js +26 -7
- package/src/local-memory-coordinator.js +71 -14
- package/src/local-tools.js +40 -1
- package/src/memory-schema.js +27 -3
- package/src/project-identity.js +75 -0
- package/src/reference-connectors.js +10 -5
- package/src/router.js +11 -3
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`
|
|
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/connector-cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { reloadCodexMcpServer } from './src/codex-app-server.js';
|
|
|
9
9
|
import { installReferenceConnector, printReferenceConnectorConfig } from './src/reference-connectors.js';
|
|
10
10
|
import { DeviceIdentity } from './src/device-identity.js';
|
|
11
11
|
import { writeFileAtomically } from './src/reference-connectors.js';
|
|
12
|
+
import { discoverProjectIdentity } from './src/project-identity.js';
|
|
12
13
|
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -202,6 +203,7 @@ async function runActivation(options) {
|
|
|
202
203
|
};
|
|
203
204
|
|
|
204
205
|
const createApprovalRequest = async ({ replacesRequestId = '' } = {}) => {
|
|
206
|
+
const projectIdentity = await discoverProjectIdentity(paths.workspaceRoot);
|
|
205
207
|
const requested = await activationFetch(`${paths.cloudUrl}/connectors/activation-requests`, {
|
|
206
208
|
method: 'POST',
|
|
207
209
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -211,6 +213,7 @@ async function runActivation(options) {
|
|
|
211
213
|
publicKeyJwk: device.publicKeyJwk,
|
|
212
214
|
deviceLabel: options['device-label'] || os.hostname(),
|
|
213
215
|
workspaceLabel: path.basename(paths.workspaceRoot),
|
|
216
|
+
projectIdentity,
|
|
214
217
|
connectorId: options['connector-id'] || defaultConnectorId(options.ecosystem, paths.workspaceRoot)
|
|
215
218
|
})
|
|
216
219
|
});
|
package/index.js
CHANGED
|
@@ -38,11 +38,14 @@ function readManagedCredential() {
|
|
|
38
38
|
const MANAGED_CREDENTIAL = readManagedCredential();
|
|
39
39
|
const CLOUD_URL = MANAGED_CREDENTIAL?.cloudUrl || process.env.CLOUDFLARE_MCP_URL || process.env.MCP_URL;
|
|
40
40
|
const CLOUD_API_KEY = MANAGED_CREDENTIAL?.apiKey || process.env.CLOUDFLARE_MCP_API_KEY || process.env.MCP_API_KEY;
|
|
41
|
-
const
|
|
41
|
+
const FIXED_PROJECT_CONTEXT = process.env.CLOUDMCP_FIXED_PROJECT_CONTEXT === 'true';
|
|
42
|
+
const WORKSPACE_ROOT = FIXED_PROJECT_CONTEXT ? (process.env.WORKSPACE_ROOT || process.cwd()) : process.cwd();
|
|
42
43
|
const CLIENT_PROFILE_ID = MANAGED_CREDENTIAL?.clientProfileId || process.env.CLOUDMCP_CLIENT_PROFILE_ID || process.env.CLIENT_PROFILE_ID || '';
|
|
43
44
|
const CONNECTOR_ID = MANAGED_CREDENTIAL?.connectorId || process.env.CLOUDMCP_CONNECTOR_ID || process.env.CONNECTOR_ID || '';
|
|
44
45
|
const CONNECTOR_TYPE = MANAGED_CREDENTIAL?.connectorType || process.env.CLOUDMCP_CONNECTOR_TYPE || process.env.CONNECTOR_TYPE || 'smart_proxy';
|
|
45
|
-
const WORKSPACE_ID =
|
|
46
|
+
const WORKSPACE_ID = FIXED_PROJECT_CONTEXT
|
|
47
|
+
? (process.env.CLOUDMCP_WORKSPACE_ID || process.env.WORKSPACE_ID || '')
|
|
48
|
+
: '';
|
|
46
49
|
const DEVICE_IDENTITY_PATH = process.env.CLOUDMCP_DEVICE_IDENTITY_PATH || '';
|
|
47
50
|
const AUTO_SYNC_PROFILE = (process.env.CLOUDMCP_AUTO_SYNC_PROFILE || 'true') !== 'false';
|
|
48
51
|
const AUTO_APPLY_BRAIN = (process.env.CLOUDMCP_AUTO_APPLY_BRAIN || 'true') !== 'false';
|
|
@@ -77,7 +80,14 @@ const memoryCoordinator = new LocalMemoryCoordinator({
|
|
|
77
80
|
connectorId: CONNECTOR_ID
|
|
78
81
|
});
|
|
79
82
|
const localTools = new LocalToolExecutor(WORKSPACE_ROOT, connectorBridge, memoryCoordinator);
|
|
80
|
-
const router = new SmartRouter(
|
|
83
|
+
const router = new SmartRouter(
|
|
84
|
+
CLOUD_URL,
|
|
85
|
+
CLOUD_API_KEY,
|
|
86
|
+
localTools,
|
|
87
|
+
WORKSPACE_ROOT,
|
|
88
|
+
connectorBridge.deviceIdentity,
|
|
89
|
+
() => connectorBridge.getScopeHeaders()
|
|
90
|
+
);
|
|
81
91
|
localTools.setCloudToolCaller((tool, params) => router.callCloudTool(tool, params));
|
|
82
92
|
|
|
83
93
|
// 创建 MCP 服务器
|
|
@@ -163,7 +173,8 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
|
163
173
|
method: 'POST',
|
|
164
174
|
headers: {
|
|
165
175
|
'Authorization': `Bearer ${CLOUD_API_KEY}`,
|
|
166
|
-
'Content-Type': 'application/json'
|
|
176
|
+
'Content-Type': 'application/json',
|
|
177
|
+
...connectorBridge.getScopeHeaders()
|
|
167
178
|
},
|
|
168
179
|
body: JSON.stringify({
|
|
169
180
|
jsonrpc: '2.0',
|
|
@@ -199,7 +210,8 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
199
210
|
method: 'POST',
|
|
200
211
|
headers: {
|
|
201
212
|
'Authorization': `Bearer ${CLOUD_API_KEY}`,
|
|
202
|
-
'Content-Type': 'application/json'
|
|
213
|
+
'Content-Type': 'application/json',
|
|
214
|
+
...connectorBridge.getScopeHeaders()
|
|
203
215
|
},
|
|
204
216
|
body: JSON.stringify({
|
|
205
217
|
jsonrpc: '2.0',
|
|
@@ -240,18 +252,22 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
240
252
|
// 启动服务器
|
|
241
253
|
async function main() {
|
|
242
254
|
try {
|
|
243
|
-
|
|
244
|
-
await server.connect(transport);
|
|
245
|
-
console.error('Cloudflare MCP Smart Proxy started');
|
|
246
|
-
console.error(`Workspace root: ${WORKSPACE_ROOT}`);
|
|
247
|
-
console.error(`Cloud URL: ${CLOUD_URL}`);
|
|
255
|
+
let bridgeStatus = null;
|
|
248
256
|
if (connectorBridge.isConfigured()) {
|
|
249
|
-
|
|
257
|
+
bridgeStatus = await connectorBridge.initialize({
|
|
250
258
|
autoApplyBrain: AUTO_APPLY_BRAIN,
|
|
251
259
|
autoSyncProfile: AUTO_SYNC_PROFILE,
|
|
252
260
|
autoReportProjectProbe: AUTO_REPORT_PROJECT_PROBE,
|
|
253
261
|
autoGenerateContextPack: AUTO_GENERATE_CONTEXT_PACK
|
|
254
262
|
});
|
|
263
|
+
memoryCoordinator.workspaceId = connectorBridge.workspaceId;
|
|
264
|
+
}
|
|
265
|
+
const transport = new StdioServerTransport();
|
|
266
|
+
await server.connect(transport);
|
|
267
|
+
console.error('Cloudflare MCP Smart Proxy started');
|
|
268
|
+
console.error(`Workspace root: ${WORKSPACE_ROOT}`);
|
|
269
|
+
console.error(`Cloud URL: ${CLOUD_URL}`);
|
|
270
|
+
if (bridgeStatus) {
|
|
255
271
|
console.error(`Connector bridge profile: ${bridgeStatus.clientProfileId}`);
|
|
256
272
|
console.error(`Connector bridge workspace: ${bridgeStatus.workspaceId}`);
|
|
257
273
|
const applyResult = bridgeStatus.state?.lastBrainApplyResult;
|
package/package.json
CHANGED
package/src/cloud-client.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const CONNECTOR_CONTRACT_VERSION = 'a2.v1';
|
|
2
2
|
|
|
3
3
|
export class CloudClient {
|
|
4
|
-
constructor({ cloudUrl, cloudApiKey, deviceIdentity = null }) {
|
|
4
|
+
constructor({ cloudUrl, cloudApiKey, deviceIdentity = null, getScopeHeaders = null }) {
|
|
5
5
|
this.cloudUrl = String(cloudUrl || '').replace(/\/$/, '');
|
|
6
6
|
this.cloudApiKey = cloudApiKey || '';
|
|
7
7
|
this.deviceIdentity = deviceIdentity;
|
|
8
|
+
this.getScopeHeaders = typeof getScopeHeaders === 'function' ? getScopeHeaders : () => ({});
|
|
8
9
|
if (!this.cloudUrl) {
|
|
9
10
|
throw new Error('cloudUrl is required');
|
|
10
11
|
}
|
|
@@ -42,6 +43,7 @@ export class CloudClient {
|
|
|
42
43
|
'Authorization': `Bearer ${this.cloudApiKey}`,
|
|
43
44
|
'Content-Type': 'application/json',
|
|
44
45
|
'X-CloudMCP-Connector-Contract-Version': CONNECTOR_CONTRACT_VERSION,
|
|
46
|
+
...this.getScopeHeaders(),
|
|
45
47
|
...signedHeaders,
|
|
46
48
|
...(idempotencyKey ? { 'X-Idempotency-Key': idempotencyKey } : {})
|
|
47
49
|
},
|
|
@@ -73,6 +75,13 @@ export class CloudClient {
|
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
async resolveWorkspace({ projectIdentity }) {
|
|
79
|
+
return this.request('/connectors/workspace-resolution', {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
body: { projectIdentity }
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
76
85
|
async listConnectorStatusReports({ clientProfileId, workspaceId, connectorId, limit = 5 }) {
|
|
77
86
|
return this.request('/connectors/status-reports', {
|
|
78
87
|
query: { clientProfileId, workspaceId, connectorId, limit }
|
package/src/connector-bridge.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
|
-
import path from 'path';
|
|
3
2
|
import { CloudClient } from './cloud-client.js';
|
|
4
3
|
import { DeviceIdentity } from './device-identity.js';
|
|
5
4
|
import { discoverProjectProbe } from './project-probe-discovery.js';
|
|
6
5
|
import { detectIde, applyBrainSnapshot } from './ide-configurator.js';
|
|
6
|
+
import { discoverProjectIdentity } from './project-identity.js';
|
|
7
7
|
|
|
8
8
|
function normalizeString(value, fallback = '') {
|
|
9
9
|
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
@@ -14,10 +14,6 @@ function defaultConnectorId() {
|
|
|
14
14
|
return `connector.${os.hostname().replace(/[^a-zA-Z0-9_.-]/g, '_')}.local`;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function defaultWorkspaceId(workspaceRoot) {
|
|
18
|
-
return `workspace.${path.basename(workspaceRoot || process.cwd()).replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
17
|
function buildIdempotencyKey(...parts) {
|
|
22
18
|
return parts
|
|
23
19
|
.map((entry) => normalizeString(entry))
|
|
@@ -42,12 +38,13 @@ export class ConnectorBridge {
|
|
|
42
38
|
this.clientProfileId = normalizeString(clientProfileId);
|
|
43
39
|
this.connectorId = normalizeString(connectorId, defaultConnectorId());
|
|
44
40
|
this.connectorType = normalizeString(connectorType, 'smart_proxy');
|
|
45
|
-
this.workspaceId = normalizeString(workspaceId
|
|
41
|
+
this.workspaceId = normalizeString(workspaceId);
|
|
46
42
|
this.deviceIdentity = deviceIdentity || new DeviceIdentity({ identityPath: deviceIdentityPath });
|
|
47
43
|
this.cloudClient = cloudClient || new CloudClient({
|
|
48
44
|
cloudUrl,
|
|
49
45
|
cloudApiKey,
|
|
50
|
-
deviceIdentity: this.deviceIdentity
|
|
46
|
+
deviceIdentity: this.deviceIdentity,
|
|
47
|
+
getScopeHeaders: () => this.getScopeHeaders()
|
|
51
48
|
});
|
|
52
49
|
this.state = {
|
|
53
50
|
initializedAt: Date.now(),
|
|
@@ -66,6 +63,27 @@ export class ConnectorBridge {
|
|
|
66
63
|
return Boolean(this.clientProfileId);
|
|
67
64
|
}
|
|
68
65
|
|
|
66
|
+
getScopeHeaders() {
|
|
67
|
+
return {
|
|
68
|
+
...(this.workspaceId ? { 'X-CloudMCP-Workspace-ID': this.workspaceId } : {}),
|
|
69
|
+
...(this.clientProfileId ? { 'X-CloudMCP-Client-Profile-ID': this.clientProfileId } : {}),
|
|
70
|
+
...(this.connectorId ? { 'X-CloudMCP-Connector-ID': this.connectorId } : {})
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async resolveCurrentWorkspace() {
|
|
75
|
+
const projectIdentity = await discoverProjectIdentity(this.workspaceRoot);
|
|
76
|
+
const response = await this.cloudClient.resolveWorkspace({ projectIdentity });
|
|
77
|
+
const resolution = response?.workspaceResolution;
|
|
78
|
+
if (!resolution?.workspaceId) {
|
|
79
|
+
throw new Error('CloudMCP did not resolve a workspace for the current project');
|
|
80
|
+
}
|
|
81
|
+
this.workspaceId = resolution.workspaceId;
|
|
82
|
+
this.state.projectIdentity = projectIdentity;
|
|
83
|
+
this.state.workspaceResolution = resolution;
|
|
84
|
+
return resolution;
|
|
85
|
+
}
|
|
86
|
+
|
|
69
87
|
getBridgeStatus() {
|
|
70
88
|
return {
|
|
71
89
|
objectType: 'connector_bridge_status',
|
|
@@ -89,6 +107,7 @@ export class ConnectorBridge {
|
|
|
89
107
|
return this.getBridgeStatus();
|
|
90
108
|
}
|
|
91
109
|
|
|
110
|
+
await this.resolveCurrentWorkspace();
|
|
92
111
|
try {
|
|
93
112
|
if (typeof this.cloudClient?.registerDevice === 'function') {
|
|
94
113
|
await this.ensureDeviceRegistration();
|
|
@@ -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
|
}
|
package/src/local-tools.js
CHANGED
|
@@ -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
|
|
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',
|
package/src/memory-schema.js
CHANGED
|
@@ -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
|
|
67
|
-
throw new Error('Stable memory requires
|
|
88
|
+
if (layer === 'stable' && evidenceRefs.length === 0) {
|
|
89
|
+
throw new Error('Stable memory requires at least one evidence_ref');
|
|
68
90
|
}
|
|
69
|
-
|
|
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) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
function normalizeString(value, fallback = '') {
|
|
5
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
6
|
+
return normalized || fallback;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeAlias(value) {
|
|
10
|
+
return normalizeString(value)
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/\.git$/i, '')
|
|
13
|
+
.replace(/\/+$/, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeGitRemote(value) {
|
|
17
|
+
const remote = normalizeString(value);
|
|
18
|
+
if (!remote) return '';
|
|
19
|
+
const scpMatch = remote.match(/^(?:[^@]+@)?([^:]+):(.+)$/);
|
|
20
|
+
if (scpMatch && !remote.includes('://')) {
|
|
21
|
+
return normalizeAlias(`${scpMatch[1]}/${scpMatch[2]}`);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(remote);
|
|
25
|
+
return normalizeAlias(`${parsed.hostname}${parsed.pathname}`);
|
|
26
|
+
} catch {
|
|
27
|
+
return normalizeAlias(remote);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readText(filePath) {
|
|
32
|
+
try {
|
|
33
|
+
return await fs.readFile(filePath, 'utf8');
|
|
34
|
+
} catch {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function readPackageName(repoRoot) {
|
|
40
|
+
try {
|
|
41
|
+
const payload = JSON.parse(await fs.readFile(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
42
|
+
return normalizeAlias(payload?.name);
|
|
43
|
+
} catch {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readGitRemote(repoRoot) {
|
|
49
|
+
const gitConfig = await readText(path.join(repoRoot, '.git', 'config'));
|
|
50
|
+
const originSection = gitConfig.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/i)?.[1] || '';
|
|
51
|
+
const originUrl = originSection.match(/^\s*url\s*=\s*(.+)$/mi)?.[1] || '';
|
|
52
|
+
return normalizeGitRemote(originUrl);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function discoverProjectIdentity(workspaceRoot = process.cwd()) {
|
|
56
|
+
const repoRoot = path.resolve(workspaceRoot || process.cwd());
|
|
57
|
+
const repoName = path.basename(repoRoot);
|
|
58
|
+
const [gitRemote, packageName] = await Promise.all([
|
|
59
|
+
readGitRemote(repoRoot),
|
|
60
|
+
readPackageName(repoRoot)
|
|
61
|
+
]);
|
|
62
|
+
const aliases = Array.from(new Set([
|
|
63
|
+
gitRemote ? `git:${gitRemote}` : '',
|
|
64
|
+
packageName ? `package:${packageName}` : '',
|
|
65
|
+
`repo:${normalizeAlias(repoName)}`
|
|
66
|
+
].filter(Boolean)));
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
canonicalId: aliases[0],
|
|
70
|
+
aliases,
|
|
71
|
+
repoName,
|
|
72
|
+
gitRemote: gitRemote || null,
|
|
73
|
+
packageName: packageName || null
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -100,7 +100,8 @@ function toManagedEnv({
|
|
|
100
100
|
connectorType,
|
|
101
101
|
workspaceId,
|
|
102
102
|
deviceIdentityPath,
|
|
103
|
-
codexConfigPath
|
|
103
|
+
codexConfigPath,
|
|
104
|
+
fixedProjectContext = false
|
|
104
105
|
}) {
|
|
105
106
|
const env = {
|
|
106
107
|
CLOUDFLARE_MCP_URL: normalizeEnvValue(cloudUrl),
|
|
@@ -112,7 +113,8 @@ function toManagedEnv({
|
|
|
112
113
|
CLOUDMCP_CONNECTOR_TYPE: normalizeEnvValue(connectorType),
|
|
113
114
|
CLOUDMCP_WORKSPACE_ID: normalizeEnvValue(workspaceId),
|
|
114
115
|
CLOUDMCP_DEVICE_IDENTITY_PATH: normalizeEnvValue(deviceIdentityPath),
|
|
115
|
-
CODEX_SHARED_CONFIG_PATH: normalizeEnvValue(codexConfigPath)
|
|
116
|
+
CODEX_SHARED_CONFIG_PATH: normalizeEnvValue(codexConfigPath),
|
|
117
|
+
CLOUDMCP_FIXED_PROJECT_CONTEXT: fixedProjectContext ? 'true' : ''
|
|
116
118
|
};
|
|
117
119
|
|
|
118
120
|
return Object.fromEntries(
|
|
@@ -156,6 +158,7 @@ export function buildReferenceConnectorServer({
|
|
|
156
158
|
workspaceId = '',
|
|
157
159
|
deviceIdentityPath = '',
|
|
158
160
|
codexConfigPath = '',
|
|
161
|
+
fixedProjectContext = true,
|
|
159
162
|
packageRoot = null,
|
|
160
163
|
runtime = 'npm'
|
|
161
164
|
}) {
|
|
@@ -174,13 +177,14 @@ export function buildReferenceConnectorServer({
|
|
|
174
177
|
cloudUrl,
|
|
175
178
|
cloudApiKey,
|
|
176
179
|
credentialPath,
|
|
177
|
-
workspaceRoot: resolvedWorkspaceRoot,
|
|
180
|
+
workspaceRoot: fixedProjectContext ? resolvedWorkspaceRoot : '',
|
|
178
181
|
clientProfileId,
|
|
179
182
|
connectorId: resolvedConnectorId,
|
|
180
183
|
connectorType: resolvedConnectorType,
|
|
181
|
-
workspaceId: resolvedWorkspaceId,
|
|
184
|
+
workspaceId: fixedProjectContext ? resolvedWorkspaceId : '',
|
|
182
185
|
deviceIdentityPath,
|
|
183
|
-
codexConfigPath: normalizedEcosystem === 'codex' ? codexConfigPath : ''
|
|
186
|
+
codexConfigPath: normalizedEcosystem === 'codex' ? codexConfigPath : '',
|
|
187
|
+
fixedProjectContext
|
|
184
188
|
}),
|
|
185
189
|
enabled: true,
|
|
186
190
|
startupTimeoutSec: normalizedEcosystem === 'codex' ? DEFAULT_CODEX_STARTUP_TIMEOUT_SEC : null,
|
|
@@ -358,6 +362,7 @@ export function installReferenceConnector({
|
|
|
358
362
|
workspaceId,
|
|
359
363
|
deviceIdentityPath,
|
|
360
364
|
codexConfigPath: target.ecosystem === 'codex' ? target.targetFile : '',
|
|
365
|
+
fixedProjectContext: target.scope === 'project',
|
|
361
366
|
packageRoot,
|
|
362
367
|
runtime
|
|
363
368
|
});
|
package/src/router.js
CHANGED
|
@@ -38,12 +38,13 @@ export function sanitizeProxyError(error, params = {}) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export class SmartRouter {
|
|
41
|
-
constructor(cloudUrl, cloudApiKey, localTools, workspaceRoot = null, deviceIdentity = null) {
|
|
41
|
+
constructor(cloudUrl, cloudApiKey, localTools, workspaceRoot = null, deviceIdentity = null, getScopeHeaders = null) {
|
|
42
42
|
this.cloudUrl = cloudUrl.replace(/\/$/, ''); // 移除尾部斜杠
|
|
43
43
|
this.cloudApiKey = cloudApiKey;
|
|
44
44
|
this.localTools = localTools;
|
|
45
45
|
this.workspaceRoot = workspaceRoot || process.cwd();
|
|
46
46
|
this.deviceIdentity = deviceIdentity;
|
|
47
|
+
this.getScopeHeaders = typeof getScopeHeaders === 'function' ? getScopeHeaders : () => ({});
|
|
47
48
|
|
|
48
49
|
// 工具路由规则
|
|
49
50
|
this.routingRules = {
|
|
@@ -72,7 +73,9 @@ export class SmartRouter {
|
|
|
72
73
|
'configure_local_memory_target',
|
|
73
74
|
'get_local_memory_target',
|
|
74
75
|
'verify_local_memory_target',
|
|
75
|
-
'capture_memory'
|
|
76
|
+
'capture_memory',
|
|
77
|
+
'invalidate_memory',
|
|
78
|
+
'close_session'
|
|
76
79
|
],
|
|
77
80
|
|
|
78
81
|
// 云端工具(需要网络或云端资源)
|
|
@@ -213,6 +216,7 @@ export class SmartRouter {
|
|
|
213
216
|
headers: {
|
|
214
217
|
'Authorization': `Bearer ${this.cloudApiKey}`,
|
|
215
218
|
'Content-Type': 'application/json',
|
|
219
|
+
...this.getScopeHeaders(),
|
|
216
220
|
...signedHeaders
|
|
217
221
|
},
|
|
218
222
|
body
|
|
@@ -294,7 +298,10 @@ export class SmartRouter {
|
|
|
294
298
|
}
|
|
295
299
|
|
|
296
300
|
const cloudNames = new Set(cloudTools.map((tool) => tool.name));
|
|
297
|
-
const
|
|
301
|
+
const governedLocalMemoryTools = new Set(['capture_memory', 'invalidate_memory', 'close_session']);
|
|
302
|
+
const eligibleLocalTools = localTools.filter((tool) => (
|
|
303
|
+
!governedLocalMemoryTools.has(tool.name) || cloudNames.has(tool.name)
|
|
304
|
+
));
|
|
298
305
|
const localNames = new Set(eligibleLocalTools.map((tool) => tool.name));
|
|
299
306
|
return [
|
|
300
307
|
...cloudTools.filter((tool) => !localNames.has(tool.name)).map((tool) => ({ ...tool, source: 'cloud' })),
|
|
@@ -320,6 +327,7 @@ export class SmartRouter {
|
|
|
320
327
|
headers: {
|
|
321
328
|
'Authorization': `Bearer ${this.cloudApiKey}`,
|
|
322
329
|
'Content-Type': 'application/json',
|
|
330
|
+
...this.getScopeHeaders(),
|
|
323
331
|
...signedHeaders
|
|
324
332
|
},
|
|
325
333
|
body
|