@lightcone-ai/daemon 0.9.79 → 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.
- package/mcp-servers/mysql/index.js +13 -5
- package/mcp-servers/mysql/manifest.json +16 -0
- package/mcp-servers/official/company-fundamentals/index.js +34 -0
- package/mcp-servers/official/company-fundamentals/manifest.json +14 -0
- package/mcp-servers/official/compliance-check/index.js +49 -0
- package/mcp-servers/official/compliance-check/manifest.json +14 -0
- package/mcp-servers/official/industry-report/index.js +34 -0
- package/mcp-servers/official/industry-report/manifest.json +14 -0
- package/mcp-servers/official/market-data-query/index.js +34 -0
- package/mcp-servers/official/market-data-query/manifest.json +14 -0
- package/mcp-servers/official/portfolio-analysis/index.js +74 -0
- package/mcp-servers/official/portfolio-analysis/manifest.json +14 -0
- package/mcp-servers/official/portfolio-read/index.js +34 -0
- package/mcp-servers/official/portfolio-read/manifest.json +14 -0
- package/mcp-servers/official/research-fetch/index.js +35 -0
- package/mcp-servers/official/research-fetch/manifest.json +14 -0
- package/mcp-servers/official/risk-metrics/index.js +34 -0
- package/mcp-servers/official/risk-metrics/manifest.json +14 -0
- package/mcp-servers/official-common/fixtures.js +273 -0
- package/mcp-servers/official-common/server.js +34 -0
- package/mcp-servers/platform/manifest.json +15 -0
- package/mcp-servers/portfolio-analysis/core.js +592 -0
- package/mcp-servers/portfolio-analysis/index.js +45 -0
- package/mcp-servers/portfolio-analysis/package-lock.json +1139 -0
- package/mcp-servers/portfolio-analysis/package.json +10 -0
- package/mcp-servers/portfolio-read/core.js +330 -0
- package/mcp-servers/portfolio-read/index.js +127 -0
- package/mcp-servers/portfolio-read/package-lock.json +1243 -0
- package/mcp-servers/portfolio-read/package.json +11 -0
- package/mcp-servers/publisher/index.js +14 -14
- package/mcp-servers/publisher/manifest.json +16 -0
- package/package.json +1 -1
- package/src/agent-manager.js +761 -188
- package/src/chat-bridge.js +567 -92
- package/src/connection.js +1 -1
- package/src/drivers/claude.js +48 -45
- package/src/drivers/codex.js +110 -8
- package/src/drivers/kimi.js +80 -35
- package/src/governance-state.js +89 -0
- package/src/index.js +34 -16
- package/src/lease-window.js +8 -0
- package/src/mcp-config.js +52 -23
package/src/chat-bridge.js
CHANGED
|
@@ -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
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
66
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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} ${
|
|
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
|
|
584
|
+
// Track the workspaceId of the most recent message for memory isolation
|
|
97
585
|
const lastMsg = msgs[msgs.length - 1];
|
|
98
|
-
if (lastMsg.
|
|
586
|
+
if (lastMsg.workspace_id) currentWorkspaceId = lastMsg.workspace_id;
|
|
99
587
|
|
|
100
588
|
const text = msgs.map(m =>
|
|
101
|
-
`[${m.
|
|
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
|
|
109
|
-
target: z.string().describe('Target: #
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 (!
|
|
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('
|
|
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
|
-
`
|
|
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
|
|
664
|
+
server.tool('list_server', 'List workspaces, agents, and humans on the server', {}, async () => {
|
|
177
665
|
const data = await api('GET', '/server');
|
|
178
|
-
const
|
|
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: `
|
|
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
|
|
186
|
-
|
|
187
|
-
status: z.enum(['all', 'todo', 'in_progress', 'in_review', 'done']).optional(),
|
|
188
|
-
}, async ({
|
|
189
|
-
const params = new URLSearchParams({
|
|
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
|
|
203
|
-
|
|
204
|
-
tasks:
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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 ({
|
|
217
|
-
const data = await api('POST', '/tasks/claim', {
|
|
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
|
-
|
|
719
|
+
workspace: z.string().describe('Target: #workspace-name'),
|
|
227
720
|
task_number: z.number().describe('Task number to unclaim'),
|
|
228
|
-
}, async ({
|
|
229
|
-
await api('POST', '/tasks/unclaim', {
|
|
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
|
-
|
|
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 ({
|
|
239
|
-
await api('POST', '/tasks/update-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
|
|
245
|
-
const chParam =
|
|
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/
|
|
747
|
+
path: z.string().describe('File path, e.g. "MEMORY.md" or "notes/workspaces.md"'),
|
|
255
748
|
}, async ({ path }) => {
|
|
256
|
-
const chParam =
|
|
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 =
|
|
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
|
|
278
|
-
if (!
|
|
279
|
-
const data = await api('GET', `/
|
|
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: '
|
|
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
|
|
287
|
-
path: z.string().describe('File path relative to
|
|
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 (!
|
|
782
|
+
if (!currentWorkspaceId) return { content: [{ type: 'text', text: 'No workspace context.' }] };
|
|
290
783
|
try {
|
|
291
|
-
const data = await api('GET', `/
|
|
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
|
|
310
|
-
path: z.string().describe('File path relative to
|
|
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 (!
|
|
314
|
-
await api('PUT', `/
|
|
315
|
-
return { content: [{ type: 'text', text: `Saved to
|
|
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
|
|
319
|
-
file_path: z.string().describe('Local file path. Relative paths resolve from the current agent workspace. Absolute paths must stay inside the agent/
|
|
320
|
-
path: z.string().describe('Destination path relative to
|
|
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 (!
|
|
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', `/
|
|
329
|
-
return { content: [{ type: 'text', text: `Saved local file to
|
|
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
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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` +
|