@lightcone-ai/daemon 0.9.78 → 0.10.0

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.
Files changed (43) hide show
  1. package/mcp-servers/mysql/index.js +13 -5
  2. package/mcp-servers/mysql/manifest.json +16 -0
  3. package/mcp-servers/official/company-fundamentals/index.js +34 -0
  4. package/mcp-servers/official/company-fundamentals/manifest.json +14 -0
  5. package/mcp-servers/official/compliance-check/index.js +49 -0
  6. package/mcp-servers/official/compliance-check/manifest.json +14 -0
  7. package/mcp-servers/official/industry-report/index.js +34 -0
  8. package/mcp-servers/official/industry-report/manifest.json +14 -0
  9. package/mcp-servers/official/market-data-query/index.js +34 -0
  10. package/mcp-servers/official/market-data-query/manifest.json +14 -0
  11. package/mcp-servers/official/portfolio-analysis/index.js +74 -0
  12. package/mcp-servers/official/portfolio-analysis/manifest.json +14 -0
  13. package/mcp-servers/official/portfolio-read/index.js +34 -0
  14. package/mcp-servers/official/portfolio-read/manifest.json +14 -0
  15. package/mcp-servers/official/research-fetch/index.js +35 -0
  16. package/mcp-servers/official/research-fetch/manifest.json +14 -0
  17. package/mcp-servers/official/risk-metrics/index.js +34 -0
  18. package/mcp-servers/official/risk-metrics/manifest.json +14 -0
  19. package/mcp-servers/official-common/fixtures.js +273 -0
  20. package/mcp-servers/official-common/server.js +34 -0
  21. package/mcp-servers/platform/manifest.json +15 -0
  22. package/mcp-servers/portfolio-analysis/core.js +592 -0
  23. package/mcp-servers/portfolio-analysis/index.js +45 -0
  24. package/mcp-servers/portfolio-analysis/package-lock.json +1139 -0
  25. package/mcp-servers/portfolio-analysis/package.json +10 -0
  26. package/mcp-servers/portfolio-read/core.js +330 -0
  27. package/mcp-servers/portfolio-read/index.js +127 -0
  28. package/mcp-servers/portfolio-read/package-lock.json +1243 -0
  29. package/mcp-servers/portfolio-read/package.json +11 -0
  30. package/mcp-servers/publisher/adapters/douyin.js +112 -20
  31. package/mcp-servers/publisher/index.js +84 -8
  32. package/mcp-servers/publisher/manifest.json +16 -0
  33. package/package.json +1 -1
  34. package/src/agent-manager.js +761 -187
  35. package/src/chat-bridge.js +567 -92
  36. package/src/connection.js +1 -1
  37. package/src/drivers/claude.js +48 -45
  38. package/src/drivers/codex.js +110 -7
  39. package/src/drivers/kimi.js +80 -34
  40. package/src/governance-state.js +89 -0
  41. package/src/index.js +34 -16
  42. package/src/lease-window.js +8 -0
  43. package/src/mcp-config.js +52 -21
@@ -3,7 +3,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { z } from 'zod';
5
5
  import { readFileSync } from 'fs';
6
+ import { createHash, randomUUID } from 'crypto';
6
7
  import path, { extname } from 'path';
8
+ import { isLeaseInvalidated, clearInvalidatedLease } from './governance-state.js';
9
+ import { classifyLeaseWindow } from './lease-window.js';
7
10
 
8
11
  const cliArgs = process.argv.slice(2);
9
12
  function getArg(name) {
@@ -14,12 +17,19 @@ function getArg(name) {
14
17
  const SERVER_URL = process.env.SERVER_URL || getArg('--server-url') || 'http://localhost:8777';
15
18
  const MACHINE_API_KEY = process.env.MACHINE_API_KEY || getArg('--auth-token') || '';
16
19
  const AGENT_ID = process.env.AGENT_ID || getArg('--agent-id') || '';
17
- const TEAM_ID = process.env.TEAM_ID || getArg('--team-id') || ''; // injected per-team at spawn time
20
+ const WORKSPACE_ID = process.env.WORKSPACE_ID || getArg('--workspace-id') || ''; // injected per-workspace at spawn time
18
21
  const WORKSPACE_DIR = path.resolve(process.env.WORKSPACE_DIR || getArg('--workspace-dir') || process.cwd());
19
- const TEAM_WORKSPACE_DIR = path.dirname(WORKSPACE_DIR);
20
-
21
- // Current active teamId for memory isolation (defaults to spawn-time TEAM_ID)
22
- let currentTeamId = TEAM_ID;
22
+ const WORKSPACE_ROOT_DIR = path.dirname(WORKSPACE_DIR);
23
+ const GOVERNANCE_SPAWN_BUNDLE_ID = process.env.GOVERNANCE_SPAWN_BUNDLE_ID || getArg('--spawn-bundle-id') || null;
24
+ const GOVERNANCE_POLICY_VERSION = process.env.GOVERNANCE_POLICY_VERSION || getArg('--policy-version') || 'mvp_policy_v1';
25
+ const GOVERNANCE_POLICY_LEASE = process.env.GOVERNANCE_POLICY_LEASE || getArg('--policy-lease') || '';
26
+ const GOVERNANCE_MCP_CLASSIFICATION = process.env.GOVERNANCE_MCP_CLASSIFICATION || getArg('--mcp-classification') || '';
27
+ const GOVERNANCE_TIMEOUT_MS = Number(process.env.GOVERNANCE_TIMEOUT_MS ?? 5000);
28
+ const LEASE_GRACE_MS = Number(process.env.GOVERNANCE_LEASE_GRACE_MS ?? 5000);
29
+ const BUNDLE_EVENT_FLUSH_MS = Number(process.env.GOVERNANCE_BUNDLE_FLUSH_MS ?? 2000);
30
+
31
+ // Current active workspaceId for memory isolation (defaults to spawn-time WORKSPACE_ID)
32
+ let currentWorkspaceId = WORKSPACE_ID;
23
33
 
24
34
  const WORKSPACE_BINARY_MIME = {
25
35
  '.png': 'image/png',
@@ -62,14 +72,252 @@ function resolveLocalWorkspaceFile(filePath) {
62
72
  const resolved = path.resolve(WORKSPACE_DIR, filePath);
63
73
  if (isInsideDir(resolved, WORKSPACE_DIR)) return resolved;
64
74
 
65
- const allowedTeamRoots = ['artifacts', 'notes', 'tmp'].map(dir => path.join(TEAM_WORKSPACE_DIR, dir));
66
- if (allowedTeamRoots.some(root => isInsideDir(resolved, root))) return resolved;
75
+ const allowedWorkspaceRoots = ['artifacts', 'notes', 'tmp'].map(dir => path.join(WORKSPACE_ROOT_DIR, dir));
76
+ if (allowedWorkspaceRoots.some(root => isInsideDir(resolved, root))) return resolved;
77
+
78
+ throw new Error(`Local file must be inside the agent workspace or workspace shared artifacts/notes/tmp directories. Got: ${filePath}`);
79
+ }
80
+
81
+ const DEFAULT_TOOL_CLASSIFICATION = {
82
+ check_messages: 'local',
83
+ list_memory: 'local',
84
+ read_memory: 'local',
85
+ upload_image: 'local',
86
+ read_file_base64: 'local',
87
+
88
+ send_message: 'mandatory',
89
+ create_tasks: 'mandatory',
90
+ claim_tasks: 'mandatory',
91
+ unclaim_task: 'mandatory',
92
+ update_task_status: 'mandatory',
93
+ write_memory: 'mandatory',
94
+ write_workspace: 'mandatory',
95
+ write_workspace_file: 'mandatory',
96
+ skill_create: 'mandatory',
97
+ skill_update: 'mandatory',
98
+ request_approval: 'mandatory',
99
+ execute_approved_action: 'mandatory',
100
+
101
+ search_messages: 'cacheable',
102
+ list_server: 'cacheable',
103
+ list_tasks: 'cacheable',
104
+ list_workspace: 'cacheable',
105
+ read_workspace: 'cacheable',
106
+ skill_list: 'cacheable',
107
+ skill_read: 'cacheable',
108
+ skill_search: 'cacheable',
109
+ };
110
+
111
+ const TOOL_CLASSIFICATION = Object.freeze({
112
+ ...DEFAULT_TOOL_CLASSIFICATION,
113
+ ...(parseJsonMaybe(GOVERNANCE_MCP_CLASSIFICATION) ?? {}),
114
+ });
115
+
116
+ const CACHE_INVALIDATION_TOOLS = new Set([
117
+ 'send_message',
118
+ 'create_tasks',
119
+ 'claim_tasks',
120
+ 'unclaim_task',
121
+ 'update_task_status',
122
+ 'write_memory',
123
+ 'write_workspace',
124
+ 'write_workspace_file',
125
+ 'skill_create',
126
+ 'skill_update',
127
+ 'request_approval',
128
+ 'execute_approved_action',
129
+ ]);
130
+
131
+ const governanceContext = {
132
+ workspaceId: currentWorkspaceId || null,
133
+ spawnBundleId: GOVERNANCE_SPAWN_BUNDLE_ID,
134
+ policyVersion: GOVERNANCE_POLICY_VERSION,
135
+ lease: parseJsonMaybe(GOVERNANCE_POLICY_LEASE),
136
+ cache: new Map(),
137
+ renewalInFlight: new Set(),
138
+ };
139
+
140
+ function resetGovernanceContext(workspaceId) {
141
+ governanceContext.workspaceId = workspaceId ?? null;
142
+ governanceContext.spawnBundleId = GOVERNANCE_SPAWN_BUNDLE_ID;
143
+ governanceContext.policyVersion = GOVERNANCE_POLICY_VERSION;
144
+ governanceContext.lease = parseJsonMaybe(GOVERNANCE_POLICY_LEASE);
145
+ governanceContext.cache.clear();
146
+ governanceContext.renewalInFlight.clear();
147
+ }
148
+
149
+ function parseJsonMaybe(text) {
150
+ if (typeof text !== 'string' || !text.trim()) return null;
151
+ try { return JSON.parse(text); } catch { return null; }
152
+ }
153
+
154
+ function normalizeApiPath(apiPath) {
155
+ return (apiPath ?? '').split('?')[0] || '/';
156
+ }
157
+
158
+ function queryParamsFromPath(apiPath) {
159
+ const idx = (apiPath ?? '').indexOf('?');
160
+ if (idx === -1) return {};
161
+ const params = new URLSearchParams(apiPath.slice(idx + 1));
162
+ return Object.fromEntries(params.entries());
163
+ }
164
+
165
+ function inferToolForApi(method, apiPath, body) {
166
+ const cleanPath = normalizeApiPath(apiPath);
167
+ const query = queryParamsFromPath(apiPath);
168
+
169
+ if (method === 'GET' && cleanPath === '/receive') return 'check_messages';
170
+ if (method === 'POST' && cleanPath === '/send') return 'send_message';
171
+ if (method === 'GET' && cleanPath === '/search') return 'search_messages';
172
+ if (method === 'GET' && cleanPath === '/server') return 'list_server';
173
+ if (method === 'GET' && cleanPath === '/tasks') return 'list_tasks';
174
+ if (method === 'POST' && cleanPath === '/tasks') return 'create_tasks';
175
+ if (method === 'POST' && cleanPath === '/tasks/claim') return 'claim_tasks';
176
+ if (method === 'POST' && cleanPath === '/tasks/unclaim') return 'unclaim_task';
177
+ if (method === 'POST' && cleanPath === '/tasks/update-status') return 'update_task_status';
178
+ if (method === 'GET' && cleanPath === '/memory') return query.path ? 'read_memory' : 'list_memory';
179
+ if (method === 'PUT' && cleanPath === '/memory') return 'write_memory';
180
+ if (method === 'GET' && cleanPath === '/workspace-memory') return query.path ? 'read_workspace' : 'list_workspace';
181
+ if (method === 'PUT' && cleanPath === '/workspace-memory') {
182
+ if (typeof body?.content === 'string' && body.content.startsWith('data:')) return 'write_workspace_file';
183
+ return 'write_workspace';
184
+ }
185
+ if (method === 'POST' && cleanPath === '/upload') return 'upload_image';
186
+ if (method === 'GET' && cleanPath === '/skills') return 'skill_list';
187
+ if (method === 'GET' && cleanPath === '/skills/search') return 'skill_search';
188
+ if (method === 'GET' && cleanPath.startsWith('/skills/')) return 'skill_read';
189
+ if (method === 'POST' && cleanPath === '/skills') return 'skill_create';
190
+ if (method === 'PATCH' && cleanPath.startsWith('/skills/')) return 'skill_update';
191
+ if (method === 'POST' && cleanPath === '/actions/request') return 'request_approval';
192
+ if (method === 'POST' && /^\/actions\/[^/]+\/execute$/.test(cleanPath)) return 'execute_approved_action';
193
+ return null;
194
+ }
67
195
 
68
- throw new Error(`Local file must be inside the agent workspace or team shared artifacts/notes/tmp directories. Got: ${filePath}`);
196
+ function cacheKeyFor(method, apiPath, body, {
197
+ leaseId = null,
198
+ policyHash = null,
199
+ workspaceId = null,
200
+ } = {}) {
201
+ const payload = JSON.stringify({
202
+ method,
203
+ apiPath,
204
+ body: body ?? null,
205
+ leaseId,
206
+ policyHash,
207
+ workspaceId,
208
+ });
209
+ return createHash('sha256').update(payload, 'utf8').digest('hex');
69
210
  }
70
211
 
71
- async function api(method, path, body) {
72
- const url = `${SERVER_URL}/internal/agent/${AGENT_ID}${path}`;
212
+ function toolInputFor(apiPath, body) {
213
+ return {
214
+ ...queryParamsFromPath(apiPath),
215
+ ...(body && typeof body === 'object' ? body : {}),
216
+ };
217
+ }
218
+
219
+ function governanceReasonCode(reason) {
220
+ if (!reason) return 'governance_unavailable';
221
+ if (typeof reason === 'string') return reason;
222
+ if (typeof reason.code === 'string') return reason.code;
223
+ if (typeof reason.message === 'string') return reason.message;
224
+ return 'governance_unavailable';
225
+ }
226
+
227
+ function governanceError(reason, detail = '') {
228
+ const suffix = detail ? ` (${detail})` : '';
229
+ const retryHintSeconds = 30;
230
+ const err = new Error(`Tool temporarily unavailable: ${reason}${suffix}. retry_hint_seconds=${retryHintSeconds}`);
231
+ err.code = reason;
232
+ err.retry_hint_seconds = retryHintSeconds;
233
+ return err;
234
+ }
235
+
236
+ function invalidateLeaseCache(leaseId) {
237
+ if (!leaseId) return;
238
+ for (const [key, value] of governanceContext.cache.entries()) {
239
+ if (value.leaseId === leaseId) governanceContext.cache.delete(key);
240
+ }
241
+ if (governanceContext.lease?.lease_id === leaseId) governanceContext.lease = null;
242
+ clearInvalidatedLease(leaseId);
243
+ }
244
+
245
+ function applyPolicyLease(lease) {
246
+ if (!lease?.lease_id) return;
247
+ governanceContext.lease = lease;
248
+ }
249
+
250
+ const bundleEventQueue = [];
251
+ let bundleEventFlushTimer = null;
252
+ let bundleEventFlushInFlight = false;
253
+
254
+ function scheduleBundleEventFlush() {
255
+ if (bundleEventFlushTimer) return;
256
+ bundleEventFlushTimer = setTimeout(() => {
257
+ bundleEventFlushTimer = null;
258
+ void flushBundleEvents();
259
+ }, Math.max(100, BUNDLE_EVENT_FLUSH_MS));
260
+ }
261
+
262
+ async function postBundleEvent(event) {
263
+ const res = await fetch(`${SERVER_URL}/governance/bundle-event`, {
264
+ method: 'POST',
265
+ headers: {
266
+ 'Content-Type': 'application/json',
267
+ 'Authorization': `Bearer ${MACHINE_API_KEY}`,
268
+ },
269
+ body: JSON.stringify(event),
270
+ });
271
+ if (!res.ok) {
272
+ const text = await res.text();
273
+ throw new Error(`bundle_event_failed:${res.status}:${text.slice(0, 300)}`);
274
+ }
275
+ }
276
+
277
+ async function flushBundleEvents() {
278
+ if (bundleEventFlushInFlight) return;
279
+ if (bundleEventQueue.length === 0) return;
280
+ bundleEventFlushInFlight = true;
281
+ try {
282
+ while (bundleEventQueue.length > 0) {
283
+ const event = bundleEventQueue[0];
284
+ try {
285
+ await postBundleEvent(event);
286
+ bundleEventQueue.shift();
287
+ } catch {
288
+ break;
289
+ }
290
+ }
291
+ } finally {
292
+ bundleEventFlushInFlight = false;
293
+ if (bundleEventQueue.length > 0) scheduleBundleEventFlush();
294
+ }
295
+ }
296
+
297
+ function enqueueBundleEvent(eventType, payload = {}) {
298
+ const push = () => {
299
+ if (!governanceContext.spawnBundleId) return;
300
+ bundleEventQueue.push({
301
+ idempotency_key: randomUUID(),
302
+ spawn_bundle_id: governanceContext.spawnBundleId,
303
+ policy_version: governanceContext.policyVersion,
304
+ event_type: eventType,
305
+ payload,
306
+ occurred_at: new Date().toISOString(),
307
+ agent_id: AGENT_ID,
308
+ });
309
+ scheduleBundleEventFlush();
310
+ };
311
+
312
+ if (governanceContext.spawnBundleId) {
313
+ push();
314
+ return;
315
+ }
316
+ void ensureGovernanceContext().then(push).catch(() => {});
317
+ }
318
+
319
+ async function directApi(method, apiPath, body) {
320
+ const url = `${SERVER_URL}/internal/agent/${AGENT_ID}${apiPath}`;
73
321
  const res = await fetch(url, {
74
322
  method,
75
323
  headers: {
@@ -80,11 +328,251 @@ async function api(method, path, body) {
80
328
  });
81
329
  if (!res.ok) {
82
330
  const text = await res.text();
83
- throw new Error(`API ${method} ${path} → ${res.status}: ${text}`);
331
+ throw new Error(`API ${method} ${apiPath} → ${res.status}: ${text}`);
84
332
  }
85
333
  return res.json();
86
334
  }
87
335
 
336
+ async function callGovernance(payload, { retry = true } = {}) {
337
+ const attempts = retry ? 2 : 1;
338
+ let lastError = null;
339
+
340
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
341
+ const controller = new AbortController();
342
+ const timer = setTimeout(() => controller.abort(), GOVERNANCE_TIMEOUT_MS);
343
+ try {
344
+ const res = await fetch(`${SERVER_URL}/governance/mcp-call`, {
345
+ method: 'POST',
346
+ headers: {
347
+ 'Content-Type': 'application/json',
348
+ 'Authorization': `Bearer ${MACHINE_API_KEY}`,
349
+ },
350
+ body: JSON.stringify(payload),
351
+ signal: controller.signal,
352
+ });
353
+
354
+ const text = await res.text();
355
+ const data = parseJsonMaybe(text) ?? { reason: text };
356
+ if (!res.ok) {
357
+ const err = new Error(`governance_call_failed:${res.status}`);
358
+ err.status = res.status;
359
+ err.response = data;
360
+ throw err;
361
+ }
362
+ return data;
363
+ } catch (err) {
364
+ const reasonCode = err.response?.reason?.code;
365
+ const retriable = err.name === 'AbortError'
366
+ || err.status == null
367
+ || (err.status >= 500 && reasonCode !== 'governance_timeout');
368
+ lastError = err;
369
+ if (!retriable || attempt === attempts - 1) break;
370
+ } finally {
371
+ clearTimeout(timer);
372
+ }
373
+ }
374
+
375
+ if (lastError?.name === 'AbortError') {
376
+ throw governanceError('timeout');
377
+ }
378
+ if (lastError?.response?.reason?.code === 'governance_timeout') {
379
+ throw governanceError('timeout');
380
+ }
381
+ throw governanceError('server_error', lastError?.message ?? '');
382
+ }
383
+
384
+ async function ensureGovernanceContext() {
385
+ const workspaceId = currentWorkspaceId || WORKSPACE_ID || null;
386
+ if (governanceContext.workspaceId !== workspaceId) {
387
+ resetGovernanceContext(workspaceId);
388
+ }
389
+ if (!governanceContext.spawnBundleId || !governanceContext.policyVersion) {
390
+ throw governanceError('governance_context_missing');
391
+ }
392
+ }
393
+
394
+ async function governanceRoundTrip({ method, apiPath, body, toolName, classification, retry }) {
395
+ await ensureGovernanceContext();
396
+
397
+ const payload = {
398
+ spawn_bundle_id: governanceContext.spawnBundleId,
399
+ policy_version: governanceContext.policyVersion,
400
+ tool_name: toolName,
401
+ tool_input: toolInputFor(apiPath, body),
402
+ tool_classification: classification,
403
+ agent_id: AGENT_ID,
404
+ idempotency_key: randomUUID(),
405
+ lease_id: governanceContext.lease?.lease_id ?? null,
406
+ };
407
+
408
+ const governance = await callGovernance(payload, { retry });
409
+ if (governance.policy_lease) applyPolicyLease(governance.policy_lease);
410
+
411
+ if (governance.verdict === 'reject' || governance.verdict === 'defer_human') {
412
+ throw governanceError(governanceReasonCode(governance.reason));
413
+ }
414
+
415
+ let nextBody = body;
416
+ if (governance.verdict === 'modify' && governance.modified_input && body && typeof body === 'object') {
417
+ nextBody = { ...body, ...governance.modified_input };
418
+ }
419
+
420
+ return directApi(method, apiPath, nextBody);
421
+ }
422
+
423
+ function renewCacheInBackground({ method, apiPath, body, toolName, cacheKey }) {
424
+ if (governanceContext.renewalInFlight.has(cacheKey)) return;
425
+ governanceContext.renewalInFlight.add(cacheKey);
426
+ void governanceRoundTrip({
427
+ method,
428
+ apiPath,
429
+ body,
430
+ toolName,
431
+ classification: 'cacheable',
432
+ retry: false,
433
+ })
434
+ .then((data) => {
435
+ governanceContext.cache.set(cacheKey, {
436
+ data,
437
+ leaseId: governanceContext.lease?.lease_id ?? null,
438
+ });
439
+ })
440
+ .catch(() => {})
441
+ .finally(() => {
442
+ governanceContext.renewalInFlight.delete(cacheKey);
443
+ });
444
+ }
445
+
446
+ function auditLocalCall(toolName, apiPath, body) {
447
+ void ensureGovernanceContext()
448
+ .then(() => callGovernance({
449
+ spawn_bundle_id: governanceContext.spawnBundleId,
450
+ policy_version: governanceContext.policyVersion,
451
+ tool_name: toolName,
452
+ tool_input: toolInputFor(apiPath, body),
453
+ tool_classification: 'local',
454
+ agent_id: AGENT_ID,
455
+ idempotency_key: randomUUID(),
456
+ }, { retry: false }))
457
+ .catch(() => {});
458
+ }
459
+
460
+ async function api(method, apiPath, body) {
461
+ const toolName = inferToolForApi(method, apiPath, body);
462
+ if (!toolName) {
463
+ return directApi(method, apiPath, body);
464
+ }
465
+
466
+ const classification = TOOL_CLASSIFICATION[toolName] ?? 'local';
467
+ const traceId = randomUUID();
468
+ enqueueBundleEvent('tool_call_started', {
469
+ trace_id: traceId,
470
+ tool_name: toolName,
471
+ tool_classification: classification,
472
+ method,
473
+ api_path: normalizeApiPath(apiPath),
474
+ });
475
+
476
+ try {
477
+ if (classification === 'local') {
478
+ const data = await directApi(method, apiPath, body);
479
+ auditLocalCall(toolName, apiPath, body);
480
+ enqueueBundleEvent('tool_call_succeeded', {
481
+ trace_id: traceId,
482
+ tool_name: toolName,
483
+ tool_classification: classification,
484
+ source: 'local',
485
+ });
486
+ return data;
487
+ }
488
+
489
+ if (classification === 'cacheable') {
490
+ await ensureGovernanceContext();
491
+ const leaseId = governanceContext.lease?.lease_id ?? null;
492
+ const policyHash = governanceContext.lease?.policy_hash ?? null;
493
+ const workspaceId = governanceContext.workspaceId ?? WORKSPACE_ID ?? null;
494
+ if (leaseId && isLeaseInvalidated(leaseId)) {
495
+ invalidateLeaseCache(leaseId);
496
+ }
497
+
498
+ const cacheKey = cacheKeyFor(method, apiPath, body, { leaseId, policyHash, workspaceId });
499
+ const cacheEntry = governanceContext.cache.get(cacheKey);
500
+ const now = Date.now();
501
+ const sameLease = cacheEntry && leaseId && cacheEntry.leaseId === leaseId;
502
+ const leaseWindow = classifyLeaseWindow(governanceContext.lease?.valid_until, now, LEASE_GRACE_MS);
503
+ const hasValidLease = sameLease && leaseWindow === 'valid';
504
+ const hasGraceLease = sameLease && leaseWindow === 'grace';
505
+
506
+ if (hasValidLease) {
507
+ enqueueBundleEvent('tool_call_succeeded', {
508
+ trace_id: traceId,
509
+ tool_name: toolName,
510
+ tool_classification: classification,
511
+ source: 'cache_hit',
512
+ });
513
+ return cacheEntry.data;
514
+ }
515
+ if (hasGraceLease) {
516
+ renewCacheInBackground({ method, apiPath, body, toolName, cacheKey });
517
+ enqueueBundleEvent('tool_call_succeeded', {
518
+ trace_id: traceId,
519
+ tool_name: toolName,
520
+ tool_classification: classification,
521
+ source: 'cache_grace',
522
+ });
523
+ return cacheEntry.data;
524
+ }
525
+
526
+ const data = await governanceRoundTrip({
527
+ method,
528
+ apiPath,
529
+ body,
530
+ toolName,
531
+ classification,
532
+ retry: true,
533
+ });
534
+ governanceContext.cache.set(cacheKey, {
535
+ data,
536
+ leaseId: governanceContext.lease?.lease_id ?? null,
537
+ });
538
+ enqueueBundleEvent('tool_call_succeeded', {
539
+ trace_id: traceId,
540
+ tool_name: toolName,
541
+ tool_classification: classification,
542
+ source: 'governance_roundtrip',
543
+ });
544
+ return data;
545
+ }
546
+
547
+ const data = await governanceRoundTrip({
548
+ method,
549
+ apiPath,
550
+ body,
551
+ toolName,
552
+ classification,
553
+ retry: true,
554
+ });
555
+ if (CACHE_INVALIDATION_TOOLS.has(toolName)) {
556
+ governanceContext.cache.clear();
557
+ }
558
+ enqueueBundleEvent('tool_call_succeeded', {
559
+ trace_id: traceId,
560
+ tool_name: toolName,
561
+ tool_classification: classification,
562
+ source: 'governance_roundtrip',
563
+ });
564
+ return data;
565
+ } catch (err) {
566
+ enqueueBundleEvent('tool_call_failed', {
567
+ trace_id: traceId,
568
+ tool_name: toolName,
569
+ tool_classification: classification,
570
+ reason: err?.code ?? err?.message ?? 'unknown_error',
571
+ });
572
+ throw err;
573
+ }
574
+ }
575
+
88
576
  const server = new McpServer({ name: 'chat', version: '0.1.0' });
89
577
 
90
578
  // ── check_messages ────────────────────────────────────────────────────────────
@@ -93,20 +581,20 @@ server.tool('check_messages', 'Check for new messages in your inbox', {}, async
93
581
  const msgs = data.messages ?? [];
94
582
  if (msgs.length === 0) return { content: [{ type: 'text', text: 'No new messages.' }] };
95
583
 
96
- // Track the teamId of the most recent message for memory isolation
584
+ // Track the workspaceId of the most recent message for memory isolation
97
585
  const lastMsg = msgs[msgs.length - 1];
98
- if (lastMsg.team_id) currentTeamId = lastMsg.team_id;
586
+ if (lastMsg.workspace_id) currentWorkspaceId = lastMsg.workspace_id;
99
587
 
100
588
  const text = msgs.map(m =>
101
- `[${m.team_type === 'dm' ? `dm:@${m.team_name}` : `#${m.team_name}`}] ${m.sender_name}: ${m.content}`
589
+ `[${m.workspace_type === 'dm' ? `dm:@${m.workspace_name}` : `#${m.workspace_name}`}] ${m.sender_name}: ${m.content}`
102
590
  + (m.task_status ? ` [task #${m.task_number} ${m.task_status}]` : '')
103
591
  ).join('\n');
104
592
  return { content: [{ type: 'text', text }] };
105
593
  });
106
594
 
107
595
  // ── send_message ──────────────────────────────────────────────────────────────
108
- server.tool('send_message', 'Send a message to a team, DM, or thread', {
109
- target: z.string().describe('Target: #team-name | dm:@agentName | #team-name:shortMsgId'),
596
+ server.tool('send_message', 'Send a message to a workspace, DM, or thread', {
597
+ target: z.string().describe('Target: #workspace-name | dm:@agentName | #workspace-name:shortMsgId'),
110
598
  content: z.string().describe('Message content'),
111
599
  }, async ({ target, content }) => {
112
600
  const data = await api('POST', '/send', { target, content });
@@ -114,23 +602,23 @@ server.tool('send_message', 'Send a message to a team, DM, or thread', {
114
602
  });
115
603
 
116
604
  // ── search_messages ──────────────────────────────────────────────────────────
117
- server.tool('search_messages', 'Search messages within a specific team. You must specify the team. Use this to find relevant conversations by keyword.', {
605
+ server.tool('search_messages', 'Search messages within a specific workspace. You must specify the workspace. Use this to find relevant conversations by keyword.', {
118
606
  query: z.string().describe('Search query'),
119
- team: z.string().describe('Target team to search within, e.g. "#general", "dm:@richard". Required — you may only search teams you are a member of.'),
607
+ workspace: z.string().describe('Target workspace to search within, e.g. "#general", "dm:@richard". Required — you may only search workspaces you are a member of.'),
120
608
  limit: z.number().optional().describe('Max results (default 10, max 20)'),
121
- }, async ({ query, team, limit }) => {
609
+ }, async ({ query, workspace, limit }) => {
122
610
  const trimmed = query.trim();
123
611
  if (!trimmed) return { content: [{ type: 'text', text: 'Search query cannot be empty.' }] };
124
- if (!team?.trim()) return { content: [{ type: 'text', text: 'team is required. Specify which team to search, e.g. "#general".' }] };
612
+ if (!workspace?.trim()) return { content: [{ type: 'text', text: 'workspace is required. Specify which workspace to search, e.g. "#general".' }] };
125
613
  const params = new URLSearchParams({ q: trimmed, limit: String(Math.min(limit ?? 10, 20)) });
126
- params.set('team', team);
614
+ params.set('workspace', workspace);
127
615
  try {
128
616
  const data = await api('GET', `/search?${params}`);
129
617
  if (!data.results || data.results.length === 0)
130
618
  return { content: [{ type: 'text', text: 'No search results.' }] };
131
619
  const formatted = data.results.map((r, i) => [
132
620
  `[${i + 1}] msg=${r.id} seq=${r.seq} time=${r.createdAt}`,
133
- `team: #${r.teamName}`,
621
+ `workspace: #${r.workspaceName}`,
134
622
  `sender: @${r.senderName}${r.senderType === 'agent' ? ' (agent)' : ''}`,
135
623
  `content: ${r.snippet}`,
136
624
  ].join('\n')).join('\n\n');
@@ -173,20 +661,20 @@ server.tool('view_file', 'Download an attached image by its attachment ID and sa
173
661
  });
174
662
 
175
663
  // ── list_server ───────────────────────────────────────────────────────────────
176
- server.tool('list_server', 'List teams, agents, and humans on the server', {}, async () => {
664
+ server.tool('list_server', 'List workspaces, agents, and humans on the server', {}, async () => {
177
665
  const data = await api('GET', '/server');
178
- const teams = (data.teams ?? []).map(c => ` #${c.name}${c.joined ? ' (joined)' : ''} — ${c.description}`).join('\n');
666
+ const workspaces = (data.workspaces ?? []).map(c => ` #${c.name}${c.joined ? ' (joined)' : ''} — ${c.description}`).join('\n');
179
667
  const agents = (data.agents ?? []).map(a => ` @${a.name} [${a.status}]`).join('\n');
180
668
  const humans = (data.humans ?? []).map(h => ` @${h.name}`).join('\n');
181
- return { content: [{ type: 'text', text: `Teams:\n${teams}\n\nAgents:\n${agents}\n\nHumans:\n${humans}` }] };
669
+ return { content: [{ type: 'text', text: `Workspaces:\n${workspaces}\n\nAgents:\n${agents}\n\nHumans:\n${humans}` }] };
182
670
  });
183
671
 
184
672
  // ── list_tasks ────────────────────────────────────────────────────────────────
185
- server.tool('list_tasks', 'List tasks in a team', {
186
- team: z.string().describe('Target: #team-name'),
187
- status: z.enum(['all', 'todo', 'in_progress', 'in_review', 'done']).optional(),
188
- }, async ({ team, status }) => {
189
- const params = new URLSearchParams({ team, status: status ?? 'all' });
673
+ server.tool('list_tasks', 'List tasks in a workspace', {
674
+ workspace: z.string().describe('Target: #workspace-name'),
675
+ status: z.enum(['all', 'todo', 'in_progress', 'in_review', 'done', 'cancelled']).optional(),
676
+ }, async ({ workspace, status }) => {
677
+ const params = new URLSearchParams({ workspace, status: status ?? 'all' });
190
678
  const data = await api('GET', `/tasks?${params}`);
191
679
  const tasks = data.tasks ?? [];
192
680
  if (tasks.length === 0) return { content: [{ type: 'text', text: 'No tasks found.' }] };
@@ -199,22 +687,27 @@ server.tool('list_tasks', 'List tasks in a team', {
199
687
  });
200
688
 
201
689
  // ── create_tasks ──────────────────────────────────────────────────────────────
202
- server.tool('create_tasks', 'Create one or more tasks in a team', {
203
- team: z.string().describe('Target: #team-name'),
204
- tasks: z.array(z.object({ title: z.string() })).describe('Array of tasks to create'),
205
- }, async ({ team, tasks }) => {
206
- const data = await api('POST', '/tasks', { team, tasks });
690
+ server.tool('create_tasks', 'Create one or more tasks in a workspace', {
691
+ workspace: z.string().describe('Target: #workspace-name'),
692
+ tasks: z.array(z.object({
693
+ title: z.string(),
694
+ scenario_type: z.string().optional().describe('Optional scenario task type, e.g. research/analysis'),
695
+ priority: z.enum(['low', 'medium', 'high']).optional().describe('Task priority'),
696
+ parent_task_number: z.number().optional().describe('Optional parent task number (single parent)'),
697
+ })).describe('Array of tasks to create'),
698
+ }, async ({ workspace, tasks }) => {
699
+ const data = await api('POST', '/tasks', { workspace, tasks });
207
700
  const created = (data.tasks ?? []).map(t => `#${t.taskNumber} ${t.title}`).join('\n');
208
701
  return { content: [{ type: 'text', text: `Created:\n${created}` }] };
209
702
  });
210
703
 
211
704
  // ── claim_tasks ───────────────────────────────────────────────────────────────
212
705
  server.tool('claim_tasks', 'Claim one or more tasks to work on', {
213
- team: z.string().describe('Target: #team-name'),
706
+ workspace: z.string().describe('Target: #workspace-name'),
214
707
  task_numbers: z.array(z.number()).optional().describe('Task numbers to claim'),
215
708
  message_ids: z.array(z.string()).optional().describe('Short message IDs to claim'),
216
- }, async ({ team, task_numbers, message_ids }) => {
217
- const data = await api('POST', '/tasks/claim', { team, task_numbers, message_ids });
709
+ }, async ({ workspace, task_numbers, message_ids }) => {
710
+ const data = await api('POST', '/tasks/claim', { workspace, task_numbers, message_ids });
218
711
  const results = (data.results ?? []).map(r =>
219
712
  `#${r.taskNumber}: ${r.success ? 'claimed' : `failed (${r.reason})`}`
220
713
  ).join('\n');
@@ -223,26 +716,26 @@ server.tool('claim_tasks', 'Claim one or more tasks to work on', {
223
716
 
224
717
  // ── unclaim_task ──────────────────────────────────────────────────────────────
225
718
  server.tool('unclaim_task', 'Release a claimed task', {
226
- team: z.string().describe('Target: #team-name'),
719
+ workspace: z.string().describe('Target: #workspace-name'),
227
720
  task_number: z.number().describe('Task number to unclaim'),
228
- }, async ({ team, task_number }) => {
229
- await api('POST', '/tasks/unclaim', { team, task_number });
721
+ }, async ({ workspace, task_number }) => {
722
+ await api('POST', '/tasks/unclaim', { workspace, task_number });
230
723
  return { content: [{ type: 'text', text: `Task #${task_number} unclaimed.` }] };
231
724
  });
232
725
 
233
726
  // ── update_task_status ────────────────────────────────────────────────────────
234
727
  server.tool('update_task_status', 'Update the status of a task', {
235
- team: z.string().describe('Target: #team-name'),
728
+ workspace: z.string().describe('Target: #workspace-name'),
236
729
  task_number: z.number().describe('Task number'),
237
- status: z.enum(['todo', 'in_progress', 'in_review', 'done']).describe('New status'),
238
- }, async ({ team, task_number, status }) => {
239
- await api('POST', '/tasks/update-status', { team, task_number, status });
730
+ status: z.enum(['todo', 'in_progress', 'in_review', 'done', 'cancelled']).describe('New status'),
731
+ }, async ({ workspace, task_number, status }) => {
732
+ await api('POST', '/tasks/update-status', { workspace, task_number, status });
240
733
  return { content: [{ type: 'text', text: `Task #${task_number} → ${status}` }] };
241
734
  });
242
735
 
243
736
  // ── list_memory ───────────────────────────────────────────────────────────────
244
- server.tool('list_memory', 'List all memory files stored for this agent in the current team', {}, async () => {
245
- const chParam = currentTeamId ? `&teamId=${encodeURIComponent(currentTeamId)}` : '';
737
+ server.tool('list_memory', 'List all memory files stored for this agent in the current workspace', {}, async () => {
738
+ const chParam = currentWorkspaceId ? `&workspaceId=${encodeURIComponent(currentWorkspaceId)}` : '';
246
739
  const data = await api('GET', `/memory?_=1${chParam}`);
247
740
  const files = data.files ?? [];
248
741
  if (files.length === 0) return { content: [{ type: 'text', text: 'No memory files yet.' }] };
@@ -251,9 +744,9 @@ server.tool('list_memory', 'List all memory files stored for this agent in the c
251
744
 
252
745
  // ── read_memory ───────────────────────────────────────────────────────────────
253
746
  server.tool('read_memory', 'Read a memory file by path (e.g. "MEMORY.md" or "notes/work-log.md")', {
254
- path: z.string().describe('File path, e.g. "MEMORY.md" or "notes/teams.md"'),
747
+ path: z.string().describe('File path, e.g. "MEMORY.md" or "notes/workspaces.md"'),
255
748
  }, async ({ path }) => {
256
- const chParam = currentTeamId ? `&teamId=${encodeURIComponent(currentTeamId)}` : '';
749
+ const chParam = currentWorkspaceId ? `&workspaceId=${encodeURIComponent(currentWorkspaceId)}` : '';
257
750
  try {
258
751
  const data = await api('GET', `/memory?path=${encodeURIComponent(path)}${chParam}`);
259
752
  return { content: [{ type: 'text', text: data.content }] };
@@ -268,27 +761,27 @@ server.tool('write_memory', 'Write or update a memory file (full content replace
268
761
  path: z.string().describe('File path, e.g. "MEMORY.md" or "notes/work-log.md"'),
269
762
  content: z.string().describe('Full file content to store'),
270
763
  }, async ({ path, content }) => {
271
- const chParam = currentTeamId ? `&teamId=${encodeURIComponent(currentTeamId)}` : '';
764
+ const chParam = currentWorkspaceId ? `&workspaceId=${encodeURIComponent(currentWorkspaceId)}` : '';
272
765
  await api('PUT', `/memory?path=${encodeURIComponent(path)}${chParam}`, { content });
273
766
  return { content: [{ type: 'text', text: `Saved ${path}` }] };
274
767
  });
275
768
 
276
769
  // ── list_workspace ────────────────────────────────────────────────────────────
277
- server.tool('list_workspace', 'List all files in the shared team workspace (BRIEF.md, KNOWLEDGE.md, artifacts/, notes/)', {}, async () => {
278
- if (!currentTeamId) return { content: [{ type: 'text', text: 'No team context.' }] };
279
- const data = await api('GET', `/team-memory?teamId=${encodeURIComponent(currentTeamId)}`);
770
+ server.tool('list_workspace', 'List all files in the shared workspace (BRIEF.md, KNOWLEDGE.md, artifacts/, notes/)', {}, async () => {
771
+ if (!currentWorkspaceId) return { content: [{ type: 'text', text: 'No workspace context.' }] };
772
+ const data = await api('GET', `/workspace-memory?workspaceId=${encodeURIComponent(currentWorkspaceId)}`);
280
773
  const files = data.files ?? [];
281
- if (files.length === 0) return { content: [{ type: 'text', text: 'Team workspace is empty.' }] };
774
+ if (files.length === 0) return { content: [{ type: 'text', text: 'Workspace workspace is empty.' }] };
282
775
  return { content: [{ type: 'text', text: files.map(f => f.path).join('\n') }] };
283
776
  });
284
777
 
285
778
  // ── read_workspace ────────────────────────────────────────────────────────────
286
- server.tool('read_workspace', 'Read a file from the shared team workspace (e.g. "BRIEF.md", "KNOWLEDGE.md", "artifacts/report.html")', {
287
- path: z.string().describe('File path relative to team workspace root'),
779
+ server.tool('read_workspace', 'Read a file from the shared workspace (e.g. "BRIEF.md", "KNOWLEDGE.md", "artifacts/report.html")', {
780
+ path: z.string().describe('File path relative to workspace root'),
288
781
  }, async ({ path }) => {
289
- if (!currentTeamId) return { content: [{ type: 'text', text: 'No team context.' }] };
782
+ if (!currentWorkspaceId) return { content: [{ type: 'text', text: 'No workspace context.' }] };
290
783
  try {
291
- const data = await api('GET', `/team-memory?path=${encodeURIComponent(path)}&teamId=${encodeURIComponent(currentTeamId)}`);
784
+ const data = await api('GET', `/workspace-memory?path=${encodeURIComponent(path)}&workspaceId=${encodeURIComponent(currentWorkspaceId)}`);
292
785
  const summary = dataUrlSummary(data.content);
293
786
  if (summary) {
294
787
  return {
@@ -306,51 +799,33 @@ server.tool('read_workspace', 'Read a file from the shared team workspace (e.g.
306
799
  });
307
800
 
308
801
  // ── write_workspace ───────────────────────────────────────────────────────────
309
- server.tool('write_workspace', 'Write a file to the shared team workspace. Use this to save ALL deliverables: code, HTML, scripts, reports, data files, images — everything goes under artifacts/. Also use for KNOWLEDGE.md and shared notes. For binary files (images/PNG/JPG), encode as base64 data URL: read the file with fs.readFileSync, then format as "data:image/png;base64," + buf.toString("base64"). The server will decode and serve them correctly.', {
310
- path: z.string().describe('File path relative to team workspace root, e.g. "artifacts/result.html" or "artifacts/cover.png"'),
802
+ server.tool('write_workspace', 'Write a file to the shared workspace. Use this to save ALL deliverables: code, HTML, scripts, reports, data files, images — everything goes under artifacts/. Also use for KNOWLEDGE.md and shared notes. For binary files (images/PNG/JPG), encode as base64 data URL: read the file with fs.readFileSync, then format as "data:image/png;base64," + buf.toString("base64"). The server will decode and serve them correctly.', {
803
+ path: z.string().describe('File path relative to workspace root, e.g. "artifacts/result.html" or "artifacts/cover.png"'),
311
804
  content: z.string().describe('File content. For images: base64 data URL "data:image/png;base64,<base64data>"'),
312
805
  }, async ({ path, content }) => {
313
- if (!currentTeamId) return { content: [{ type: 'text', text: 'No team context.' }] };
314
- await api('PUT', `/team-memory?path=${encodeURIComponent(path)}&teamId=${encodeURIComponent(currentTeamId)}`, { content });
315
- return { content: [{ type: 'text', text: `Saved to team workspace: ${path}` }] };
806
+ if (!currentWorkspaceId) return { content: [{ type: 'text', text: 'No workspace context.' }] };
807
+ await api('PUT', `/workspace-memory?path=${encodeURIComponent(path)}&workspaceId=${encodeURIComponent(currentWorkspaceId)}`, { content });
808
+ return { content: [{ type: 'text', text: `Saved to workspace: ${path}` }] };
316
809
  });
317
810
 
318
- server.tool('write_workspace_file', 'Write a local file directly to the shared team workspace. Prefer this over write_workspace for images/PDFs/binary files so large base64 content never enters the model context. The source file may be a relative path under the current agent workspace, or an absolute path inside the agent workspace/team shared artifacts/notes/tmp directories.', {
319
- file_path: z.string().describe('Local file path. Relative paths resolve from the current agent workspace. Absolute paths must stay inside the agent/team workspace.'),
320
- path: z.string().describe('Destination path relative to team workspace root, e.g. "artifacts/cover.png"'),
811
+ server.tool('write_workspace_file', 'Write a local file directly to the shared workspace. Prefer this over write_workspace for images/PDFs/binary files so large base64 content never enters the model context. The source file may be a relative path under the current agent workspace, or an absolute path inside the agent workspace/workspace shared artifacts/notes/tmp directories.', {
812
+ file_path: z.string().describe('Local file path. Relative paths resolve from the current agent workspace. Absolute paths must stay inside the agent/workspace.'),
813
+ path: z.string().describe('Destination path relative to workspace root, e.g. "artifacts/cover.png"'),
321
814
  }, async ({ file_path, path }) => {
322
- if (!currentTeamId) return { content: [{ type: 'text', text: 'No team context.' }] };
815
+ if (!currentWorkspaceId) return { content: [{ type: 'text', text: 'No workspace context.' }] };
323
816
  const localPath = resolveLocalWorkspaceFile(file_path);
324
817
  const ext = extname(path || localPath).toLowerCase();
325
818
  const mime = WORKSPACE_BINARY_MIME[ext] ?? 'application/octet-stream';
326
819
  const buf = readFileSync(localPath);
327
820
  const content = `data:${mime};base64,${buf.toString('base64')}`;
328
- await api('PUT', `/team-memory?path=${encodeURIComponent(path)}&teamId=${encodeURIComponent(currentTeamId)}`, { content });
329
- return { content: [{ type: 'text', text: `Saved local file to team workspace: ${path} (${mime}, ${formatBytes(buf.length)})` }] };
821
+ await api('PUT', `/workspace-memory?path=${encodeURIComponent(path)}&workspaceId=${encodeURIComponent(currentWorkspaceId)}`, { content });
822
+ return { content: [{ type: 'text', text: `Saved local file to workspace: ${path} (${mime}, ${formatBytes(buf.length)})` }] };
330
823
  });
331
824
 
332
- // ── get_credential ───────────────────────────────────────────────────────────
333
- server.tool('get_credential',
334
- 'Retrieve decrypted credential fields for a platform granted to this agent (e.g. XHS_COOKIE for "xhs"). Use when you need to inject credentials into a browser session or external call.',
335
- {
336
- platform: z.string().describe('Platform key, e.g. "xhs", "x", "youtube"'),
337
- },
338
- async ({ platform }) => {
339
- try {
340
- const grants = await api('GET', '/credential-grants');
341
- const match = grants.find(g => g.platform === platform);
342
- if (!match) return { content: [{ type: 'text', text: `No credential found for platform "${platform}". Ask the human to connect the account via Settings → 连接外部账号.` }] };
343
- const fields = Object.entries(match.envVars).map(([k, v]) => `${k}=${v}`).join('\n');
344
- return { content: [{ type: 'text', text: fields }] };
345
- } catch (err) {
346
- return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }] };
347
- }
348
- }
349
- );
350
-
351
825
  // ── skill_list ───────────────────────────────────────────────────────────────
352
826
  server.tool('skill_list', 'List all skills available to you (platform + bound). Returns index only (name + description), not full content.', {}, async () => {
353
- const skills = await api('GET', `/skills`);
827
+ const workspaceQuery = currentWorkspaceId ? `?workspaceId=${encodeURIComponent(currentWorkspaceId)}` : '';
828
+ const skills = await api('GET', `/skills${workspaceQuery}`);
354
829
  if (!skills || skills.length === 0) return { content: [{ type: 'text', text: 'No skills available.' }] };
355
830
  const lines = skills.map(s => `- [${s.type}] **${s.name}** — ${s.description}`);
356
831
  return { content: [{ type: 'text', text: lines.join('\n') }] };
@@ -410,7 +885,7 @@ server.tool('skill_search', 'Search for skills by keyword across all accessible
410
885
  server.tool('read_file_base64',
411
886
  '读取本机文件内容,返回 base64 编码。优先使用 write_workspace_file 保存正式产出;只有外部工具明确需要 base64 字符串时才使用。',
412
887
  {
413
- file_path: z.string().describe('本机文件路径。相对路径从当前 agent workspace 解析;绝对路径必须在 agent/team workspace 内。'),
888
+ file_path: z.string().describe('本机文件路径。相对路径从当前 agent workspace 解析;绝对路径必须在 agent/workspace 内。'),
414
889
  },
415
890
  async ({ file_path }) => {
416
891
  const localPath = resolveLocalWorkspaceFile(file_path);
@@ -423,7 +898,7 @@ server.tool('read_file_base64',
423
898
  server.tool('upload_image',
424
899
  '将本机图片文件上传为临时公开 URL,用于聊天预览、二维码截图或外部平台临时访问。它不会保存正式产出;正式产出必须同时写入 artifacts/,优先使用 write_workspace_file。',
425
900
  {
426
- file_path: z.string().describe('本机图片文件路径。相对路径从当前 agent workspace 解析;绝对路径必须在 agent/team workspace 内。'),
901
+ file_path: z.string().describe('本机图片文件路径。相对路径从当前 agent workspace 解析;绝对路径必须在 agent/workspace 内。'),
427
902
  },
428
903
  async ({ file_path }) => {
429
904
  const { extname, basename } = await import('path');
@@ -451,7 +926,7 @@ server.tool('request_approval',
451
926
  action_type, platform, description,
452
927
  payload: JSON.stringify(payload),
453
928
  credential_id,
454
- team_id: currentTeamId,
929
+ workspace_id: currentWorkspaceId,
455
930
  });
456
931
  return { content: [{ type: 'text', text:
457
932
  `Approval requested (action_id="${data.id}"). Waiting for human review.\n` +