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 CHANGED
@@ -114,8 +114,10 @@ npx -y -p cloudflare-mcp-smart-proxy cloudmcp-connector activate codex \
114
114
  - `get_local_memory_target`:读取当前连接器工作空间的本机目标契约。
115
115
  - `verify_local_memory_target`:回读验证目标契约或一个精确条目。
116
116
  - `capture_memory`:以同一事务写入受治理的 CloudMCP memory plane 和已配置本地目标,双端回读一致后才返回成功。
117
+ - `invalidate_memory`:以 revision、原因和证据软失效一个精确条目,并把同一 canonical JSON 提交到本地与云端。
118
+ - `close_session`:把简洁交接转换为 `working + handoff`,复用与 `capture_memory` 相同的双端事务。
117
119
 
118
- `capture_memory` 只有在当前连接器通过云端鉴权、工具能力授权及精确 `memory_scope` 资源授权后才会被本地协调器暴露。它仍复用原代理的 API Key、设备签名和 CloudMCP 请求路由,不存在匿名云端写入口。
120
+ `capture_memory / invalidate_memory / close_session` 只有在当前连接器通过云端鉴权、对应工具能力授权及精确 `memory_scope` 资源授权后才会被本地协调器暴露。它们仍复用原代理的 API Key、设备签名和 CloudMCP 请求路由,不存在匿名云端写入口。`search_memory / get_memory_entry / load_context` 保持受治理云端只读工具,由同一授权链提供。
119
121
 
120
122
  本地目录不是硬编码的。以 SoloMap 为目标时,用户或 Agent 可把其记忆根目录显式传给配置工具;未安装 CloudMCP 的 SoloMap 用户继续使用原有 Markdown 机制,公共 SoloMap 插件不会默认依赖这些工具。
121
123
 
package/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 WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || process.cwd();
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 = MANAGED_CREDENTIAL?.workspaceId || process.env.CLOUDMCP_WORKSPACE_ID || process.env.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(CLOUD_URL, CLOUD_API_KEY, localTools, WORKSPACE_ROOT, connectorBridge.deviceIdentity);
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
- const transport = new StdioServerTransport();
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
- const bridgeStatus = await connectorBridge.initialize({
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-mcp-smart-proxy",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
4
4
  "description": "Smart proxy for Cloudflare MCP - routes tools to cloud or local execution",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 }
@@ -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, defaultWorkspaceId(this.workspaceRoot));
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
  }
@@ -97,6 +97,14 @@ export class LocalToolExecutor {
97
97
  case 'capture_memory':
98
98
  if (!this.memoryCoordinator) throw new Error('Local memory coordinator is not configured');
99
99
  return await this.memoryCoordinator.capture(params || {});
100
+
101
+ case 'invalidate_memory':
102
+ if (!this.memoryCoordinator) throw new Error('Local memory coordinator is not configured');
103
+ return await this.memoryCoordinator.invalidate(params || {});
104
+
105
+ case 'close_session':
106
+ if (!this.memoryCoordinator) throw new Error('Local memory coordinator is not configured');
107
+ return await this.memoryCoordinator.closeSession(params || {});
100
108
 
101
109
  default:
102
110
  throw new Error(`Unknown local tool: ${toolName}`);
@@ -136,7 +144,7 @@ export class LocalToolExecutor {
136
144
  },
137
145
  {
138
146
  name: 'capture_memory',
139
- description: 'Validate one memory and commit the same JSON entry to the configured local target and governed CloudMCP memory scope.',
147
+ description: 'Validate one governed memory and commit the same canonical JSON entry to the configured local target and CloudMCP scope.',
140
148
  inputSchema: {
141
149
  type: 'object',
142
150
  properties: {
@@ -158,6 +166,37 @@ export class LocalToolExecutor {
158
166
  required: ['content']
159
167
  }
160
168
  },
169
+ {
170
+ name: 'invalidate_memory',
171
+ description: 'Soft-invalidate one governed memory and verify the same canonical result in CloudMCP and the configured local target.',
172
+ inputSchema: {
173
+ type: 'object',
174
+ properties: {
175
+ memory_scope_id: { type: 'string' },
176
+ memory_id: { type: 'string' },
177
+ expected_revision: { type: 'number' },
178
+ reason: { type: 'string' },
179
+ evidence_refs: { type: 'array', items: { type: 'string' } }
180
+ },
181
+ required: ['memory_id', 'expected_revision', 'reason', 'evidence_refs']
182
+ }
183
+ },
184
+ {
185
+ name: 'close_session',
186
+ description: 'Persist a working handoff through the same verified local-and-cloud memory transaction.',
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ memory_scope_id: { type: 'string' },
191
+ title: { type: 'string' },
192
+ summary: { type: 'string' },
193
+ next_steps: { type: 'array', items: { type: 'string' } },
194
+ tags: { type: 'array', items: { type: 'string' } },
195
+ transaction_id: { type: 'string' }
196
+ },
197
+ required: ['summary']
198
+ }
199
+ },
161
200
  {
162
201
  name: 'read_file',
163
202
  description: 'Read a file from local filesystem',
@@ -5,6 +5,17 @@ const MEMORY_KINDS = new Set([
5
5
  'preference', 'rule', 'project_fact', 'decision', 'pattern', 'domain', 'handoff', 'observation'
6
6
  ]);
7
7
  const MEMORY_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
8
+ const MEMORY_KIND_LAYERS = new Map([
9
+ ['preference', 'stable'], ['rule', 'stable'], ['project_fact', 'stable'],
10
+ ['decision', 'stable'], ['pattern', 'stable'], ['domain', 'stable'],
11
+ ['handoff', 'working'], ['observation', 'candidate']
12
+ ]);
13
+ const SENSITIVE_MEMORY_PATTERNS = [
14
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
15
+ /\bCLOUDMCP_APPROVAL_CODE\s*=/i,
16
+ /\b(?:api[_ -]?key|access[_ -]?token|client[_ -]?secret|private[_ -]?key)\s*[:=]\s*\S{12,}/i,
17
+ /\bmcp_[A-Za-z0-9]{16,}\b/
18
+ ];
8
19
 
9
20
  function normalizeString(value, fallback = null) {
10
21
  const normalized = typeof value === 'string' ? value.trim() : '';
@@ -28,6 +39,14 @@ function normalizeStringArray(value, limit = 32) {
28
39
  ));
29
40
  }
30
41
 
42
+ export function assertNoSensitiveMemory(input = {}) {
43
+ const text = [input.title, input.content, ...(input.evidenceRefs || input.evidence_refs || [])]
44
+ .filter(Boolean).join('\n');
45
+ if (SENSITIVE_MEMORY_PATTERNS.some((pattern) => pattern.test(text))) {
46
+ throw new Error('Memory content appears to contain a secret or approval credential');
47
+ }
48
+ }
49
+
31
50
  function sortValue(value) {
32
51
  if (Array.isArray(value)) return value.map(sortValue);
33
52
  if (!value || typeof value !== 'object') return value;
@@ -58,15 +77,18 @@ export function validateMemoryCaptureInput(payload = {}) {
58
77
  if (content.length > 12000) throw new Error('Memory content exceeds 12000 characters');
59
78
  if (title && title.length > 240) throw new Error('Memory title exceeds 240 characters');
60
79
  const layer = normalizeString(payload.layer, 'candidate');
80
+ if (layer === 'stable' && !normalizeString(payload.kind)) throw new Error('Stable memory requires an explicit kind');
61
81
  const kind = normalizeString(payload.kind, layer === 'working' ? 'handoff' : 'observation');
62
82
  if (!MEMORY_LAYERS.has(layer)) throw new Error(`Unsupported memory layer: ${layer}`);
63
83
  if (!MEMORY_KINDS.has(kind)) throw new Error(`Unsupported memory kind: ${kind}`);
84
+ const requiredLayer = MEMORY_KIND_LAYERS.get(kind);
85
+ if (requiredLayer !== layer) throw new Error(`Memory kind ${kind} must use the ${requiredLayer} layer`);
64
86
  const evidenceRefs = normalizeStringArray(payload.evidence_refs || payload.evidenceRefs, 20);
65
87
  const verifiedAt = normalizeIsoDate(payload.verified_at || payload.verifiedAt);
66
- if (layer === 'stable' && evidenceRefs.length === 0 && !verifiedAt) {
67
- throw new Error('Stable memory requires evidence_refs or verified_at');
88
+ if (layer === 'stable' && evidenceRefs.length === 0) {
89
+ throw new Error('Stable memory requires at least one evidence_ref');
68
90
  }
69
- return {
91
+ const normalized = {
70
92
  title: title || 'Untitled memory',
71
93
  content,
72
94
  layer,
@@ -79,6 +101,8 @@ export function validateMemoryCaptureInput(payload = {}) {
79
101
  supersedes: normalizeStringArray(payload.supersedes, 20)
80
102
  .map((id) => validateMemoryIdentifier(id, 'supersedes entry id'))
81
103
  };
104
+ assertNoSensitiveMemory(normalized);
105
+ return normalized;
82
106
  }
83
107
 
84
108
  export function assertMemoryEntryIntegrity(entry) {
@@ -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 eligibleLocalTools = localTools.filter((tool) => tool.name !== 'capture_memory' || cloudNames.has('capture_memory'));
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