@lightcone-ai/daemon 0.9.79 → 0.10.1

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/index.js +14 -14
  31. package/mcp-servers/publisher/manifest.json +16 -0
  32. package/package.json +4 -2
  33. package/src/_vendor/mcp/registry.js +327 -0
  34. package/src/agent-manager.js +761 -188
  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 -8
  39. package/src/drivers/kimi.js +80 -35
  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 -23
@@ -1,34 +1,67 @@
1
1
  import { spawn } from 'child_process';
2
- import { mkdirSync, statSync, writeFileSync, readdirSync, unlinkSync } from 'fs';
2
+ import { mkdirSync, writeFileSync, readdirSync, unlinkSync } from 'fs';
3
3
  import { homedir } from 'os';
4
4
  import path from 'path';
5
- import { buildSystemPrompt } from './drivers/claude.js';
6
- import { buildCodexSpawn, buildCodexSystemPrompt, parseCodexLine } from './drivers/codex.js';
7
- import { buildKimiSpawn, buildKimiInitMessages, parseKimiLine, encodeKimiStdin } from './drivers/kimi.js';
5
+ import { parseCodexLine, adaptCodexSystemPrompt } from './drivers/codex.js';
6
+ import { parseKimiLine, encodeKimiStdin } from './drivers/kimi.js';
8
7
  import { startSession, stopSession, stopAllSessions } from './browser-login.js';
9
- import { buildSkillMcpServers } from './mcp-config.js';
8
+ import { markInvalidatedLeases } from './governance-state.js';
9
+
10
+ const KIMI_SYSTEM_PROMPT_FILE = '.lightcone-kimi-system.md';
11
+ const KIMI_AGENT_FILE = '.lightcone-kimi-agent.yaml';
12
+ const KIMI_MCP_FILE = '.lightcone-kimi-mcp.json';
13
+
14
+ function runtimeMissingDetail(runtime) {
15
+ return `cli_missing:${runtime}`;
16
+ }
17
+
18
+ function resolveExitOfflineDetail({ code, signal, stopCause }) {
19
+ if (stopCause === 'manual_stop') return '';
20
+ if (stopCause === 'credential_revoked') return 'credential_revoked';
21
+ if (code === 0) return '';
22
+ if (signal === 'SIGTERM') return '';
23
+ if (signal === 'SIGKILL') return 'agent_timeout';
24
+ return 'spawn_session_crashed';
25
+ }
26
+
27
+ function normalizeObject(value) {
28
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
29
+ }
30
+
31
+ function replacePlaceholders(text, replacements) {
32
+ if (typeof text !== 'string') return text;
33
+ let next = text;
34
+ for (const [token, value] of Object.entries(replacements)) {
35
+ next = next.split(token).join(value);
36
+ }
37
+ return next;
38
+ }
10
39
 
11
40
  export class AgentManager {
12
41
  constructor({ serverUrl, machineApiKey }) {
13
42
  this.serverUrl = serverUrl;
14
43
  this.machineApiKey = machineApiKey;
15
- // key: `teamId:agentId` → { config, teamId, agentId, sessionId, proc }
44
+ // key: `workspaceId:agentId` → { config, workspaceId, agentId, sessionId, proc }
16
45
  this.agents = new Map();
17
46
  // key → true (spawn in progress)
18
47
  this.starting = new Set();
48
+ this.directiveRefreshTimers = new Map();
49
+ this.directiveGovernanceCache = new Map();
19
50
  }
20
51
 
21
- _key(agentId, teamId) {
22
- return `${teamId ?? ''}:${agentId}`;
52
+ _key(agentId, workspaceId) {
53
+ return `${workspaceId ?? ''}:${agentId}`;
23
54
  }
24
55
 
25
56
  handle(msg, connection) {
26
57
  switch (msg.type) {
27
58
  case 'agent:start': return this._startAgent(msg, connection);
28
- case 'agent:stop': return this._stopAgent(msg.agentId, msg.teamId, connection);
59
+ case 'agent:stop': return this._stopAgent(msg.agentId, msg.workspaceId, connection);
29
60
  case 'agent:deliver': return this._deliverMessage(msg, connection);
30
61
  case 'browser:start_login': return this._startBrowserLogin(msg, connection);
31
62
  case 'browser:stop_login': return this._stopBrowserLogin(msg);
63
+ case 'policy_invalidate': return this._handlePolicyInvalidate(msg, connection);
64
+ case 'credential_revoked': return this._handleCredentialRevoked(msg, connection);
32
65
  case 'ping': return connection.send({ type: 'pong' });
33
66
  default:
34
67
  console.log(`[AgentManager] Unhandled: ${msg.type}`);
@@ -36,6 +69,10 @@ export class AgentManager {
36
69
  }
37
70
 
38
71
  async stopAll() {
72
+ for (const timer of this.directiveRefreshTimers.values()) {
73
+ clearTimeout(timer);
74
+ }
75
+ this.directiveRefreshTimers.clear();
39
76
  for (const [, agent] of this.agents) {
40
77
  if (agent.proc) agent.proc.kill();
41
78
  }
@@ -45,16 +82,16 @@ export class AgentManager {
45
82
 
46
83
  // ── private ───────────────────────────────────────────────────────────────
47
84
 
48
- _teamWorkspaceDir(teamId) {
49
- const dir = path.join(homedir(), '.lightcone', 'workspace', teamId ?? '_global');
85
+ _workspaceRootDir(workspaceId) {
86
+ const dir = path.join(homedir(), '.lightcone', 'workspace', workspaceId ?? '_global');
50
87
  mkdirSync(path.join(dir, 'artifacts'), { recursive: true });
51
88
  mkdirSync(path.join(dir, 'notes'), { recursive: true });
52
89
  mkdirSync(path.join(dir, 'tmp'), { recursive: true });
53
90
  return dir;
54
91
  }
55
92
 
56
- _workspaceDir(agentId, teamId) {
57
- const dir = path.join(homedir(), '.lightcone', 'workspace', teamId ?? '_global', agentId);
93
+ _workspaceDir(agentId, workspaceId) {
94
+ const dir = path.join(homedir(), '.lightcone', 'workspace', workspaceId ?? '_global', agentId);
58
95
  mkdirSync(dir, { recursive: true });
59
96
  mkdirSync(path.join(dir, 'tmp'), { recursive: true });
60
97
  return dir;
@@ -78,7 +115,7 @@ export class AgentManager {
78
115
  }
79
116
 
80
117
  _formatDeliveryText(message) {
81
- return `New message in ${message.team_type === 'dm' ? 'dm from' : `#${message.team_name} from`} ${message.sender_name}: ${message.content}`;
118
+ return `New message in ${message.workspace_type === 'dm' ? 'dm from' : `#${message.workspace_name} from`} ${message.sender_name}: ${message.content}`;
82
119
  }
83
120
 
84
121
  _takePendingMessage(key) {
@@ -90,81 +127,597 @@ export class AgentManager {
90
127
  return msg;
91
128
  }
92
129
 
93
- async _startAgent({ agentId, teamId, teamName, config }, connection) {
94
- const key = this._key(agentId, teamId);
130
+ _prepareStartupMessage(key, runtime) {
131
+ if (runtime === 'codex') {
132
+ const startupMsg = this._takePendingMessage(key);
133
+ return { startupMsg, directiveMsg: startupMsg };
134
+ }
135
+ const pending = this._pendingMessages?.get(key);
136
+ return {
137
+ startupMsg: null,
138
+ directiveMsg: pending?.[0] ?? null,
139
+ };
140
+ }
141
+
142
+ _resolveDirectiveArgPath(arg, { chatBridgePath }) {
143
+ if (typeof arg !== 'string') return arg;
144
+ const normalized = arg.replaceAll('\\', '/');
145
+ if (normalized.includes('chat-bridge.js')) {
146
+ return chatBridgePath;
147
+ }
148
+ if (normalized.includes('/mcp-servers/publisher/index.js')) {
149
+ return new URL('../mcp-servers/publisher/index.js', import.meta.url).pathname;
150
+ }
151
+ if (normalized.includes('/mcp-servers/workspace-migrate/index.js')) {
152
+ return new URL('../../mcp-servers/workspace-migrate/index.js', import.meta.url).pathname;
153
+ }
154
+ if (normalized.includes('/mcp-servers/mysql/index.js')) {
155
+ return new URL('../../mcp-servers/mysql/index.js', import.meta.url).pathname;
156
+ }
157
+ if (normalized.includes('/mcp-servers/platform/index.js')) {
158
+ return new URL('../../mcp-servers/platform/index.js', import.meta.url).pathname;
159
+ }
160
+ return arg;
161
+ }
162
+
163
+ _normalizeDirectiveForLocalSpawn(directive, {
164
+ agentId,
165
+ workspaceId,
166
+ workspaceDir,
167
+ chatBridgePath,
168
+ config,
169
+ }) {
170
+ const authToken = config?.authToken || this.machineApiKey;
171
+ const userId = config?.userId ?? 'default';
172
+ const profileRoot = path.join(homedir(), '.lightcone', 'chrome-profiles');
173
+ const replacements = {
174
+ '${SERVER_URL}': this.serverUrl,
175
+ '${MACHINE_API_KEY}': authToken,
176
+ '${AGENT_ID}': agentId,
177
+ '${WORKSPACE_ID}': workspaceId ?? '',
178
+ '${WORKSPACE_DIR}': workspaceDir,
179
+ '${XHS_PROFILE_DIR}': path.join(profileRoot, `xhs-${userId}`),
180
+ '${DOUYIN_PROFILE_DIR}': path.join(profileRoot, `douyin-${userId}`),
181
+ '${KUAISHOU_PROFILE_DIR}': path.join(profileRoot, `kuaishou-${userId}`),
182
+ };
183
+
184
+ const normalized = this._replaceDirectiveValue(normalizeObject(directive), replacements);
185
+ const mcpServers = normalizeObject(normalized.mcp_servers);
186
+ const governanceLease = normalizeObject(normalized.policy_lease);
187
+ const leaseValidUntil = Date.parse(governanceLease.valid_until ?? '');
188
+ const leaseState = Number.isFinite(leaseValidUntil) && leaseValidUntil > Date.now() ? 'valid' : 'expired';
189
+ const governanceEnv = normalized.spawn_bundle_id || normalized.policy_version || normalized.directive_id
190
+ ? {
191
+ LC_GOVERNANCE_DIRECTIVE_ID: normalized.directive_id ?? '',
192
+ LC_GOVERNANCE_SPAWN_BUNDLE_ID: normalized.spawn_bundle_id ?? '',
193
+ LC_GOVERNANCE_POLICY_VERSION: normalized.policy_version ?? '',
194
+ LC_GOVERNANCE_POLICY_LEASE_ID: governanceLease.lease_id ?? '',
195
+ LC_GOVERNANCE_POLICY_LEASE_STATE: leaseState,
196
+ LC_GOVERNANCE_POLICY_LEASE_JSON: JSON.stringify(governanceLease),
197
+ LC_GOVERNANCE_MCP_CLASSIFICATION_JSON: JSON.stringify(normalizeObject(normalized.mcp_classification)),
198
+ LC_GOVERNANCE_WORKSPACE_ID: workspaceId ?? '',
199
+ }
200
+ : {};
201
+
202
+ for (const [serverKey, rawServer] of Object.entries(mcpServers)) {
203
+ const server = normalizeObject(rawServer);
204
+ const args = Array.isArray(server.args) ? server.args.map((arg) => this._resolveDirectiveArgPath(String(arg), { chatBridgePath })) : [];
205
+ const env = Object.fromEntries(Object.entries(normalizeObject(server.env)).map(([k, v]) => [k, String(v ?? '')]));
206
+ const baseEnv = {};
207
+ if (serverKey === 'chat' || serverKey === 'publisher' || serverKey === 'platform') {
208
+ baseEnv.SERVER_URL = this.serverUrl;
209
+ baseEnv.MACHINE_API_KEY = authToken;
210
+ baseEnv.AGENT_ID = agentId;
211
+ baseEnv.WORKSPACE_ID = workspaceId ?? '';
212
+ baseEnv.WORKSPACE_DIR = workspaceDir;
213
+ } else if (serverKey === 'workspace-migrate') {
214
+ baseEnv.SERVER_URL = this.serverUrl;
215
+ baseEnv.MACHINE_API_KEY = authToken;
216
+ baseEnv.AGENT_ID = agentId;
217
+ }
218
+ mcpServers[serverKey] = {
219
+ ...server,
220
+ args,
221
+ env: {
222
+ ...env,
223
+ ...baseEnv,
224
+ ...(serverKey === 'chat' ? governanceEnv : {}),
225
+ },
226
+ };
227
+ }
228
+
229
+ normalized.mcp_servers = mcpServers;
230
+ return normalized;
231
+ }
232
+
233
+ _cacheDirectiveGovernanceContract(key, normalizedDirective, workspaceId) {
234
+ const lease = normalizeObject(normalizedDirective?.policy_lease);
235
+ const cached = {
236
+ directiveId: normalizedDirective?.directive_id ?? null,
237
+ spawnBundleId: normalizedDirective?.spawn_bundle_id ?? null,
238
+ policyVersion: normalizedDirective?.policy_version ?? null,
239
+ leaseId: lease.lease_id ?? null,
240
+ workspaceId: workspaceId ?? null,
241
+ updatedAt: new Date().toISOString(),
242
+ };
243
+ this.directiveGovernanceCache.set(key, cached);
244
+ return cached;
245
+ }
246
+
247
+ _replaceDirectiveValue(value, replacements) {
248
+ if (typeof value === 'string') return replacePlaceholders(value, replacements);
249
+ if (Array.isArray(value)) return value.map((item) => this._replaceDirectiveValue(item, replacements));
250
+ if (!value || typeof value !== 'object') return value;
251
+ const next = {};
252
+ for (const [key, itemValue] of Object.entries(value)) {
253
+ next[key] = this._replaceDirectiveValue(itemValue, replacements);
254
+ }
255
+ return next;
256
+ }
257
+
258
+ _resolveDirectiveMcpServers(directive, replacements) {
259
+ const resolved = this._replaceDirectiveValue(normalizeObject(directive?.mcp_servers), replacements);
260
+ const mcpServers = {};
261
+ for (const [serverKey, rawServer] of Object.entries(resolved)) {
262
+ const server = normalizeObject(rawServer);
263
+ const command = typeof server.command === 'string' ? server.command.trim() : '';
264
+ if (!command) continue;
265
+ mcpServers[serverKey] = {
266
+ ...server,
267
+ command,
268
+ args: Array.isArray(server.args) ? server.args.map((item) => String(item)) : [],
269
+ env: Object.fromEntries(
270
+ Object.entries(normalizeObject(server.env)).map(([k, v]) => [k, String(v ?? '')])
271
+ ),
272
+ };
273
+ }
274
+ return mcpServers;
275
+ }
276
+
277
+ _buildCodexMcpArgs(mcpServers) {
278
+ const args = [];
279
+ for (const [serverKey, server] of Object.entries(mcpServers)) {
280
+ const serverArgs = Array.isArray(server.args) ? server.args.map((item) => String(item)) : [];
281
+ const envVars = normalizeObject(server.env);
282
+ const envPairs = Object.entries(envVars).map(([k, v]) => `${k}=${String(v ?? '')}`);
283
+ const keyExpr = JSON.stringify(serverKey);
284
+ const commandExpr = JSON.stringify(envPairs.length > 0 ? 'env' : server.command);
285
+ const argsExpr = JSON.stringify(envPairs.length > 0 ? [...envPairs, server.command, ...serverArgs] : serverArgs);
286
+ args.push(
287
+ '-c', `mcp_servers.${keyExpr}.command=${commandExpr}`,
288
+ '-c', `mcp_servers.${keyExpr}.args=${argsExpr}`,
289
+ '-c', `mcp_servers.${keyExpr}.enabled=true`,
290
+ );
291
+ if (server.required === true) {
292
+ args.push('-c', `mcp_servers.${keyExpr}.required=true`);
293
+ }
294
+ }
295
+ return args;
296
+ }
297
+
298
+ _createKimiConfigFiles(workspaceDir, { systemPrompt, mcpServers }) {
299
+ const systemPromptPath = path.join(workspaceDir, KIMI_SYSTEM_PROMPT_FILE);
300
+ const agentFilePath = path.join(workspaceDir, KIMI_AGENT_FILE);
301
+ const mcpConfigPath = path.join(workspaceDir, KIMI_MCP_FILE);
302
+
303
+ writeFileSync(systemPromptPath, systemPrompt ?? '', 'utf8');
304
+ writeFileSync(agentFilePath, [
305
+ 'version: 1',
306
+ 'agent:',
307
+ ' extend: default',
308
+ ` system_prompt_path: ./${KIMI_SYSTEM_PROMPT_FILE}`,
309
+ '',
310
+ ].join('\n'), 'utf8');
311
+ writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }), 'utf8');
312
+
313
+ return { systemPromptPath, agentFilePath, mcpConfigPath };
314
+ }
315
+
316
+ _buildDirectiveSpawnPlan({
317
+ directive,
318
+ runtime,
319
+ workspaceDir,
320
+ chatBridgePath,
321
+ agentId,
322
+ workspaceId,
323
+ startupMsg,
324
+ }) {
325
+ const systemPrompt = typeof directive?.system_prompt === 'string'
326
+ ? directive.system_prompt
327
+ : '';
328
+ const codexBasePrompt = adaptCodexSystemPrompt(systemPrompt);
329
+ const codexPrompt = startupMsg
330
+ ? `${codexBasePrompt}\n\nNew message received:\n\n${this._formatDeliveryText(startupMsg.message)}\n\nRespond as appropriate. Complete all required work before stopping.`
331
+ : codexBasePrompt;
332
+
333
+ const baseReplacements = {
334
+ '__CHAT_BRIDGE_PATH__': chatBridgePath,
335
+ '__WORKSPACE_DIR__': workspaceDir,
336
+ '__SERVER_URL__': this.serverUrl,
337
+ '__MACHINE_API_KEY__': this.machineApiKey,
338
+ '__AGENT_ID__': agentId,
339
+ '__WORKSPACE_ID__': workspaceId ?? '',
340
+ };
341
+ const mcpServers = this._resolveDirectiveMcpServers(directive, baseReplacements);
342
+
343
+ let kimiFiles = null;
344
+ if (runtime === 'kimi') {
345
+ kimiFiles = this._createKimiConfigFiles(workspaceDir, {
346
+ systemPrompt,
347
+ mcpServers,
348
+ });
349
+ }
350
+
351
+ const replacements = {
352
+ ...baseReplacements,
353
+ '__SYSTEM_PROMPT__': runtime === 'codex' ? codexPrompt : systemPrompt,
354
+ '__MCP_CONFIG_JSON__': JSON.stringify({ mcpServers }),
355
+ '__KIMI_AGENT_FILE__': kimiFiles?.agentFilePath ?? '',
356
+ '__KIMI_MCP_FILE__': kimiFiles?.mcpConfigPath ?? '',
357
+ };
358
+
359
+ const rawArgs = Array.isArray(directive?.spawn_args) ? directive.spawn_args : [];
360
+ const args = rawArgs.map((item) => String(this._replaceDirectiveValue(item, replacements)));
361
+
362
+ if (runtime === 'claude') {
363
+ if (!args.includes('--mcp-config')) {
364
+ args.push('--mcp-config', replacements.__MCP_CONFIG_JSON__);
365
+ }
366
+ if (!args.includes('--system-prompt') && systemPrompt.trim()) {
367
+ args.push('--system-prompt', systemPrompt);
368
+ }
369
+ } else if (runtime === 'codex') {
370
+ if (!args.some((arg) => arg.includes('mcp_servers.')) && Object.keys(mcpServers).length > 0) {
371
+ args.push(...this._buildCodexMcpArgs(mcpServers));
372
+ }
373
+ const hasPromptPlaceholder = rawArgs.some((item) => String(item).includes('__SYSTEM_PROMPT__'));
374
+ if (!hasPromptPlaceholder && codexPrompt.trim()) {
375
+ args.push(codexPrompt);
376
+ }
377
+ } else if (runtime === 'kimi') {
378
+ if (!args.includes('--agent-file') && replacements.__KIMI_AGENT_FILE__) {
379
+ args.push('--agent-file', replacements.__KIMI_AGENT_FILE__);
380
+ }
381
+ if (!args.includes('--mcp-config-file') && replacements.__KIMI_MCP_FILE__) {
382
+ args.push('--mcp-config-file', replacements.__KIMI_MCP_FILE__);
383
+ }
384
+ }
385
+
386
+ const env = {
387
+ ...process.env,
388
+ FORCE_COLOR: '0',
389
+ NO_COLOR: '1',
390
+ ...Object.fromEntries(
391
+ Object.entries(normalizeObject(directive?.env_vars)).map(([k, v]) => [k, String(v ?? '')])
392
+ ),
393
+ };
394
+
395
+ if (runtime === 'claude') {
396
+ delete env.CLAUDECODE;
397
+ }
398
+
399
+ const sessionArgIndex = args.indexOf('--session');
400
+ const kimiSessionId = runtime === 'kimi' && sessionArgIndex !== -1 ? args[sessionArgIndex + 1] ?? null : null;
401
+
402
+ return {
403
+ command: runtime,
404
+ args,
405
+ env,
406
+ kimiSessionId,
407
+ };
408
+ }
409
+
410
+ async _fetchSpawnDirective({
411
+ agentId,
412
+ workspaceId,
413
+ runtime,
414
+ workspaceDir,
415
+ chatBridgePath,
416
+ config,
417
+ triggerType = 'resume',
418
+ }) {
419
+ const res = await fetch(`${this.serverUrl}/governance/spawn-directive`, {
420
+ method: 'POST',
421
+ headers: {
422
+ 'Content-Type': 'application/json',
423
+ 'Authorization': `Bearer ${this.machineApiKey}`,
424
+ },
425
+ body: JSON.stringify({
426
+ agent_id: agentId,
427
+ workspace_id: workspaceId ?? null,
428
+ trigger: {
429
+ type: triggerType,
430
+ context: { source: 'daemon-agent-manager' },
431
+ },
432
+ runtime_context: {
433
+ cli_type: runtime,
434
+ session_id: config?.sessionId ?? null,
435
+ workspace_dir: workspaceDir,
436
+ chat_bridge_path: chatBridgePath,
437
+ },
438
+ agent_profile: {
439
+ name: config?.name ?? null,
440
+ display_name: config?.displayName ?? null,
441
+ description: config?.description ?? null,
442
+ role_prompt: config?.rolePrompt ?? null,
443
+ feishu_bot_name: config?.feishuBotName ?? null,
444
+ },
445
+ }),
446
+ });
447
+ if (!res.ok) {
448
+ const text = await res.text();
449
+ throw new Error(`spawn_directive_failed:${res.status}:${text.slice(0, 300)}`);
450
+ }
451
+ const directive = await res.json();
452
+ if (!directive || typeof directive !== 'object') {
453
+ throw new Error('spawn_directive_invalid_response');
454
+ }
455
+ return directive;
456
+ }
457
+
458
+ _clearDirectiveRefresh(key) {
459
+ const timer = this.directiveRefreshTimers.get(key);
460
+ if (timer) clearTimeout(timer);
461
+ this.directiveRefreshTimers.delete(key);
462
+ }
463
+
464
+ _scheduleDirectiveRefresh(key, context, directive) {
465
+ this._clearDirectiveRefresh(key);
466
+ if (!directive || typeof directive !== 'object') return;
467
+
468
+ let ttlMs = Number(directive.lease_ttl_ms ?? NaN);
469
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
470
+ const validUntilMs = Date.parse(directive?.policy_lease?.valid_until ?? '');
471
+ if (Number.isFinite(validUntilMs)) {
472
+ ttlMs = validUntilMs - Date.now();
473
+ }
474
+ }
475
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) return;
476
+
477
+ let refreshInMs = Math.max(250, ttlMs - 2000);
478
+ if (ttlMs <= 250) {
479
+ // Keep refresh strictly before expiry even for short leases.
480
+ refreshInMs = ttlMs > 1
481
+ ? Math.max(1, Math.min(ttlMs - 1, Math.floor(ttlMs * 0.5)))
482
+ : 1;
483
+ }
484
+ const timer = setTimeout(async () => {
485
+ if (!this.agents.has(key)) return;
486
+ try {
487
+ const nextDirective = await this._fetchSpawnDirective({
488
+ ...context,
489
+ triggerType: 'lease_refresh',
490
+ });
491
+ const agent = this.agents.get(key);
492
+ if (!agent) return;
493
+ agent.directive = nextDirective;
494
+ this._scheduleDirectiveRefresh(key, context, nextDirective);
495
+ } catch (err) {
496
+ console.log(`[AgentManager] Spawn directive refresh failed for ${context.agentId}: ${err.message}`);
497
+ // Retry later; fail-safe for transient network failures.
498
+ this._scheduleDirectiveRefresh(key, context, {
499
+ ...directive,
500
+ lease_ttl_ms: 5000,
501
+ });
502
+ }
503
+ }, refreshInMs);
504
+ this.directiveRefreshTimers.set(key, timer);
505
+ }
506
+
507
+ async _startAgent({ agentId, workspaceId, workspaceName, config }, connection) {
508
+ const key = this._key(agentId, workspaceId);
95
509
  if (this.agents.has(key) || this.starting.has(key)) {
96
- console.log(`[AgentManager] Agent ${config?.displayName ?? agentId} in team ${teamName ?? teamId} already registered`);
510
+ console.log(`[AgentManager] Agent ${config?.displayName ?? agentId} in workspace ${workspaceName ?? workspaceId} already registered`);
97
511
  return;
98
512
  }
99
513
  this.starting.add(key);
100
514
 
101
- const runtime = config.runtime ?? 'claude';
102
- const teamWorkspaceDir = this._teamWorkspaceDir(teamId);
103
- const workspaceDir = this._workspaceDir(agentId, teamId);
515
+ const requestedRuntime = String(config.runtime ?? 'claude').trim().toLowerCase() || 'claude';
516
+ this._workspaceRootDir(workspaceId);
517
+ const workspaceDir = this._workspaceDir(agentId, workspaceId);
104
518
  const chatBridgePath = new URL('./chat-bridge.js', import.meta.url).pathname;
105
- const startupMsg = runtime === 'codex' ? this._takePendingMessage(key) : null;
519
+ const failStart = (reason) => {
520
+ this._clearDirectiveRefresh(key);
521
+ this.starting.delete(key);
522
+ connection.send({ type: 'agent:status', agentId, workspaceId, status: 'inactive' });
523
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'offline', detail: reason ?? '', entries: [] });
524
+ };
106
525
 
107
- // Fetch skills for system prompt + MCP derivation (non-blocking on failure)
526
+ let directive = null;
527
+ try {
528
+ directive = await this._fetchSpawnDirective({
529
+ agentId,
530
+ workspaceId,
531
+ runtime: requestedRuntime,
532
+ workspaceDir,
533
+ chatBridgePath,
534
+ config,
535
+ });
536
+ console.log(`[AgentManager] Spawn directive loaded for ${config.displayName ?? agentId}: ${directive.directive_id ?? 'n/a'} cli=${directive.cli_type ?? requestedRuntime}`);
537
+ this._scheduleDirectiveRefresh(key, {
538
+ agentId,
539
+ workspaceId,
540
+ runtime: requestedRuntime,
541
+ workspaceDir,
542
+ chatBridgePath,
543
+ config,
544
+ }, directive);
545
+ } catch (err) {
546
+ console.log(`[AgentManager] Spawn directive fetch failed for ${agentId} (fail-closed): ${err.message}`);
547
+ failStart('spawn_directive_failed');
548
+ return;
549
+ }
550
+
551
+ const runtime = String(directive?.cli_type ?? requestedRuntime).trim().toLowerCase() || requestedRuntime;
552
+ if (!['claude', 'codex', 'kimi'].includes(runtime)) {
553
+ console.log(`[AgentManager] Unsupported runtime in directive for ${agentId}: ${runtime}`);
554
+ failStart('spawn_directive_invalid_runtime');
555
+ return;
556
+ }
557
+ if (!Array.isArray(directive?.spawn_args)) {
558
+ console.log(`[AgentManager] Invalid spawn args in directive for ${agentId}`);
559
+ failStart('spawn_directive_invalid_args');
560
+ return;
561
+ }
562
+
563
+ const { startupMsg } = this._prepareStartupMessage(key, runtime);
564
+
565
+ // Keep .skills materialization for runtime compatibility, but no longer use skills to build prompt/env/mcp.
108
566
  let skills = [];
109
567
  try {
110
568
  const res = await fetch(`${this.serverUrl}/internal/agent/${agentId}/skills`, {
111
569
  headers: { 'Authorization': `Bearer ${this.machineApiKey}` },
112
570
  });
113
571
  if (res.ok) skills = await res.json();
114
- const mcpSkills = skills.filter(s => s.mcpConfig);
115
- console.log(`[AgentManager] Skills loaded for ${config.displayName ?? agentId}: ${skills.length} total, ${mcpSkills.length} with MCP (${mcpSkills.map(s => s.name).join(', ') || 'none'})`);
572
+ console.log(`[AgentManager] Skills loaded for ${config.displayName ?? agentId}: ${skills.length}`);
116
573
  } catch (err) {
117
574
  console.log(`[AgentManager] Skills fetch failed for ${agentId} (non-fatal): ${err.message}`);
118
575
  }
576
+ this._materializeSkills(workspaceDir, skills);
119
577
 
120
- // Fetch credential grants for this agent (non-blocking on failure)
121
- let credentialGrants = [];
578
+ const requiredCredentials = Array.isArray(directive?.required_credentials)
579
+ ? directive.required_credentials.filter(Boolean)
580
+ : [];
581
+ let credentialEnvVars = {};
122
582
  try {
123
- const res = await fetch(`${this.serverUrl}/internal/agent/${agentId}/credential-grants`, {
124
- headers: { 'Authorization': `Bearer ${this.machineApiKey}` },
583
+ const res = await fetch(`${this.serverUrl}/governance/credential-broker`, {
584
+ method: 'POST',
585
+ headers: {
586
+ 'Content-Type': 'application/json',
587
+ 'Authorization': `Bearer ${this.machineApiKey}`,
588
+ },
589
+ body: JSON.stringify({
590
+ agent_id: agentId,
591
+ workspace_id: workspaceId ?? null,
592
+ required_credentials: requiredCredentials,
593
+ bundle_id: directive?.spawn_bundle_id ?? null,
594
+ }),
125
595
  });
126
- if (res.ok) credentialGrants = await res.json();
127
- console.log(`[AgentManager] Credential grants for ${config.displayName ?? agentId}: ${credentialGrants.map(g => `${g.platform}(${Object.keys(g.envVars ?? {}).join(',')})`).join(', ') || 'none'}`);
596
+ if (res.ok) {
597
+ const payload = await res.json();
598
+ credentialEnvVars = (payload?.env_vars && typeof payload.env_vars === 'object')
599
+ ? payload.env_vars
600
+ : {};
601
+ console.log(`[AgentManager] Brokered credentials for ${config.displayName ?? agentId}: ${Object.keys(credentialEnvVars).join(', ') || 'none'}`);
602
+ } else {
603
+ const text = await res.text();
604
+ if (requiredCredentials.length > 0) {
605
+ throw new Error(`credential_broker_denied:${res.status}:${text.slice(0, 200)}`);
606
+ }
607
+ console.log(`[AgentManager] Credential broker denied for ${agentId} (non-fatal): ${res.status} ${text.slice(0, 200)}`);
608
+ }
128
609
  } catch (err) {
129
- console.log(`[AgentManager] Credential grants fetch failed for ${agentId} (non-fatal): ${err.message}`);
610
+ if (requiredCredentials.length > 0) {
611
+ console.log(`[AgentManager] Credential broker failed for ${agentId} (fail-closed): ${err.message}`);
612
+ failStart('credential_broker_failed');
613
+ return;
614
+ }
615
+ console.log(`[AgentManager] Credential broker failed for ${agentId} (non-fatal): ${err.message}`);
130
616
  }
131
617
 
132
- // Materialize bound skills into .skills/ directory
133
- this._materializeSkills(workspaceDir, skills);
618
+ const directiveEnvVars = normalizeObject(directive?.env_vars);
619
+ directive.env_vars = {
620
+ ...directiveEnvVars,
621
+ ...credentialEnvVars,
622
+ };
134
623
 
135
- let proc;
624
+ const runtimeConfig = {
625
+ ...config,
626
+ authToken: this.machineApiKey,
627
+ systemPrompt: typeof directive?.system_prompt === 'string' ? directive.system_prompt : '',
628
+ envVars: normalizeObject(directive?.env_vars),
629
+ };
136
630
 
137
- if (runtime === 'kimi') {
138
- // ── Kimi CLI ──────────────────────────────────────────────────────────
139
- const kimiSpawn = buildKimiSpawn({
140
- config, agentId, teamId, workspaceDir, chatBridgePath,
141
- serverUrl: this.serverUrl, machineApiKey: this.machineApiKey, skills,
142
- credentialGrants,
631
+ let spawnPlan;
632
+ try {
633
+ spawnPlan = this._buildDirectiveSpawnPlan({
634
+ directive,
635
+ runtime,
636
+ workspaceDir,
637
+ chatBridgePath,
638
+ agentId,
639
+ workspaceId,
640
+ startupMsg,
143
641
  });
642
+ } catch (err) {
643
+ console.log(`[AgentManager] Failed to build spawn plan for ${agentId}: ${err.message}`);
644
+ failStart('spawn_plan_invalid');
645
+ return;
646
+ }
144
647
 
145
- console.log(`[AgentManager] Spawning kimi for ${config.displayName ?? agentId} team=${teamName ?? teamId ?? 'none'} (session=${kimiSpawn.sessionId})`);
648
+ console.log(`[AgentManager] Spawning ${runtime} for ${config.displayName ?? agentId} workspace=${workspaceName ?? workspaceId ?? 'none'} directive=${directive.directive_id ?? 'n/a'}`);
649
+ const stdio = runtime === 'codex'
650
+ ? ['ignore', 'pipe', 'pipe']
651
+ : ['pipe', 'pipe', 'pipe'];
652
+ const proc = spawn(spawnPlan.command, spawnPlan.args, {
653
+ cwd: workspaceDir,
654
+ env: spawnPlan.env,
655
+ stdio,
656
+ });
657
+ let spawnErrorReported = false;
146
658
 
147
- proc = spawn('kimi', kimiSpawn.args, {
148
- cwd: workspaceDir,
149
- env: kimiSpawn.env,
150
- stdio: ['pipe', 'pipe', 'pipe'],
151
- });
659
+ const reportSpawnFailure = (detail) => {
660
+ if (spawnErrorReported) return;
661
+ spawnErrorReported = true;
662
+ this._clearDirectiveRefresh(key);
663
+ this.starting.delete(key);
664
+ this.agents.delete(key);
665
+ connection.send({ type: 'agent:status', agentId, workspaceId, status: 'inactive' });
666
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'offline', detail: detail ?? 'spawn_failed', entries: [] });
667
+ };
152
668
 
153
- // Send initialize + prompt via stdin
154
- const initMsgs = buildKimiInitMessages(kimiSpawn);
155
- for (const msg of initMsgs) {
156
- proc.stdin.write(msg + '\n');
157
- }
669
+ proc.once('error', (err) => {
670
+ const detail = err?.code === 'ENOENT'
671
+ ? runtimeMissingDetail(runtime)
672
+ : 'spawn_failed';
673
+ console.log(`[AgentManager] Spawn process error for ${agentId} (${runtime}): ${err?.message ?? 'unknown error'}`);
674
+ reportSpawnFailure(detail);
675
+ });
158
676
 
159
- // Track Kimi-specific state for parsing
160
- const kimiState = { sessionId: kimiSpawn.sessionId, sessionAnnounced: false };
677
+ if (runtime === 'kimi') {
678
+ const sessionId = spawnPlan.kimiSessionId ?? null;
679
+ const kimiState = { sessionId, sessionAnnounced: false };
161
680
 
162
681
  this.agents.set(key, {
163
- config, teamId, agentId, sessionId: kimiSpawn.sessionId, proc,
164
- runtime: 'kimi', kimiState, kimiIdle: false,
682
+ config: runtimeConfig,
683
+ workspaceId,
684
+ agentId,
685
+ sessionId,
686
+ proc,
687
+ runtime: 'kimi',
688
+ kimiState,
689
+ kimiIdle: false,
690
+ directive,
691
+ requiredCredentials,
692
+ stopCause: null,
165
693
  });
166
694
  this.starting.delete(key);
167
695
 
696
+ // Kimi wire protocol requires initialize before prompt/steer frames.
697
+ const initMessages = [
698
+ JSON.stringify({
699
+ jsonrpc: '2.0',
700
+ id: `${Date.now()}-init`,
701
+ method: 'initialize',
702
+ params: {
703
+ protocol_version: '1.3',
704
+ client: { name: 'lightcone-daemon', version: '1.0.0' },
705
+ capabilities: { supports_question: false, supports_plan_mode: false },
706
+ },
707
+ }),
708
+ JSON.stringify({
709
+ jsonrpc: '2.0',
710
+ id: `${Date.now()}-prompt`,
711
+ method: 'prompt',
712
+ params: {
713
+ user_input: 'Your system prompt contains your standing instructions. Follow it now and begin listening for messages.',
714
+ },
715
+ }),
716
+ ];
717
+ for (const msg of initMessages) {
718
+ proc.stdin.write(msg + '\n');
719
+ }
720
+
168
721
  let buffer = '';
169
722
  proc.stdout.on('data', (chunk) => {
170
723
  buffer += chunk.toString();
@@ -172,38 +725,20 @@ export class AgentManager {
172
725
  buffer = lines.pop();
173
726
  for (const line of lines) {
174
727
  if (!line.trim()) continue;
175
- this._parseKimiLine(key, agentId, teamId, line, connection);
728
+ this._parseKimiLine(key, agentId, workspaceId, line, connection);
176
729
  }
177
730
  });
178
731
  } else if (runtime === 'codex') {
179
- const startupPrompt = startupMsg
180
- ? `${buildCodexSystemPrompt(config, agentId)}\n\nNew message received:\n\n${this._formatDeliveryText(startupMsg.message)}\n\nRespond as appropriate. Complete all required work before stopping.`
181
- : buildCodexSystemPrompt(config, agentId);
182
-
183
- const codexSpawn = buildCodexSpawn({
184
- config,
185
- agentId,
186
- teamId,
187
- workspaceDir,
188
- chatBridgePath,
189
- serverUrl: this.serverUrl,
190
- machineApiKey: this.machineApiKey,
191
- prompt: startupPrompt,
192
- skills,
193
- credentialGrants,
194
- });
195
-
196
- console.log(`[AgentManager] Spawning codex for ${config.displayName ?? agentId} team=${teamName ?? teamId ?? 'none'} (session=${config.sessionId ?? 'new'})`);
197
-
198
- proc = spawn('codex', codexSpawn.args, {
199
- cwd: workspaceDir,
200
- env: codexSpawn.env,
201
- stdio: ['ignore', 'pipe', 'pipe'],
202
- });
203
-
204
732
  this.agents.set(key, {
205
- config, teamId, agentId, sessionId: config.sessionId ?? null, proc,
733
+ config: runtimeConfig,
734
+ workspaceId,
735
+ agentId,
736
+ sessionId: config.sessionId ?? null,
737
+ proc,
206
738
  runtime: 'codex',
739
+ directive,
740
+ requiredCredentials,
741
+ stopCause: null,
207
742
  });
208
743
  this.starting.delete(key);
209
744
 
@@ -214,81 +749,23 @@ export class AgentManager {
214
749
  buffer = lines.pop();
215
750
  for (const line of lines) {
216
751
  if (!line.trim()) continue;
217
- this._parseCodexLine(key, agentId, teamId, line, connection);
752
+ this._parseCodexLine(key, agentId, workspaceId, line, connection);
218
753
  }
219
754
  });
220
755
  } else {
221
- // ── Claude CLI (default) ──────────────────────────────────────────────
222
- const mcpServers = {
223
- chat: {
224
- command: 'node',
225
- args: [chatBridgePath],
226
- env: {
227
- SERVER_URL: this.serverUrl,
228
- MACHINE_API_KEY: config.authToken,
229
- AGENT_ID: agentId,
230
- TEAM_ID: teamId ?? '',
231
- WORKSPACE_DIR: workspaceDir,
232
- },
233
- },
234
- };
235
-
236
- Object.assign(mcpServers, buildSkillMcpServers({
237
- skills,
238
- credentialGrants,
239
- config,
756
+ this.agents.set(key, {
757
+ config: runtimeConfig,
758
+ workspaceId,
240
759
  agentId,
241
- teamId,
242
- workspaceDir,
243
- serverUrl: this.serverUrl,
244
- authToken: config.authToken,
245
- }));
246
-
247
- const mcpConfig = { mcpServers };
248
- console.log(`[AgentManager] MCP servers for ${config.displayName ?? agentId}: ${Object.keys(mcpServers).join(', ')}`);
249
- for (const [name, mc] of Object.entries(mcpServers)) {
250
- console.log(`[AgentManager] mcp:${name} → ${mc.command} ${(mc.args ?? []).join(' ')}`);
251
- }
252
-
253
- const args = [
254
- '--print',
255
- '--allow-dangerously-skip-permissions',
256
- '--dangerously-skip-permissions',
257
- '--verbose',
258
- '--output-format', 'stream-json',
259
- '--input-format', 'stream-json',
260
- '--mcp-config', JSON.stringify(mcpConfig),
261
- '--system-prompt', buildSystemPrompt(config, agentId, skills),
262
- '--disallowed-tools', 'EnterPlanMode,ExitPlanMode',
263
- ];
264
-
265
- if (config.sessionId) {
266
- // Only resume if the session file exists locally
267
- const projectSlug = workspaceDir.replace(/[\/\.]/g, '-');
268
- const sessionFile = path.join(homedir(), '.claude', 'projects', projectSlug, `${config.sessionId}.jsonl`);
269
- try {
270
- statSync(sessionFile);
271
- args.push('--resume', config.sessionId);
272
- } catch {
273
- console.log(`[AgentManager] Session ${config.sessionId} not found locally, starting fresh`);
274
- }
275
- }
276
-
277
- const spawnEnv = { ...process.env, FORCE_COLOR: '0', ...(config.envVars ?? {}) };
278
- delete spawnEnv.CLAUDECODE;
279
-
280
- console.log(`[AgentManager] Spawning claude for ${config.displayName ?? agentId} team=${teamName ?? teamId ?? 'none'} (session=${config.sessionId ?? 'new'})`);
281
-
282
- proc = spawn('claude', args, {
283
- cwd: workspaceDir,
284
- env: spawnEnv,
285
- stdio: ['pipe', 'pipe', 'pipe'],
760
+ sessionId: config.sessionId ?? null,
761
+ proc,
762
+ runtime: 'claude',
763
+ directive,
764
+ requiredCredentials,
765
+ stopCause: null,
286
766
  });
287
-
288
- this.agents.set(key, { config, teamId, agentId, sessionId: config.sessionId ?? null, proc, runtime: 'claude' });
289
767
  this.starting.delete(key);
290
768
 
291
- // Parse stdout stream for session ID and activity updates
292
769
  let buffer = '';
293
770
  proc.stdout.on('data', (chunk) => {
294
771
  buffer += chunk.toString();
@@ -296,7 +773,7 @@ export class AgentManager {
296
773
  buffer = lines.pop();
297
774
  for (const line of lines) {
298
775
  if (!line.trim()) continue;
299
- this._parseLine(key, agentId, teamId, line, connection);
776
+ this._parseLine(key, agentId, workspaceId, line, connection);
300
777
  }
301
778
  });
302
779
  }
@@ -306,14 +783,16 @@ export class AgentManager {
306
783
  if (text) console.error(`[AgentManager][${config.displayName ?? agentId}] stderr: ${text.slice(0, 500)}`);
307
784
  });
308
785
 
309
- proc.on('exit', (code) => {
786
+ proc.on('exit', (code, signal) => {
787
+ if (spawnErrorReported) return;
310
788
  const agent = this.agents.get(key);
311
- console.log(`[AgentManager] Agent ${config.displayName ?? agentId} team=${teamName ?? teamId ?? 'none'} exited (code=${code})`);
789
+ console.log(`[AgentManager] Agent ${config.displayName ?? agentId} workspace=${workspaceName ?? workspaceId ?? 'none'} exited (code=${code}, signal=${signal ?? 'none'})`);
790
+ this._clearDirectiveRefresh(key);
312
791
  this.agents.delete(key);
313
792
 
314
793
  if (code === 0 && runtime === 'codex' && this._pendingMessages?.get(key)?.length) {
315
794
  const restartConfig = { ...config, sessionId: agent?.sessionId ?? config.sessionId ?? null };
316
- this._startAgent({ agentId, teamId, config: restartConfig }, connection);
795
+ this._startAgent({ agentId, workspaceId, config: restartConfig }, connection);
317
796
  return;
318
797
  }
319
798
 
@@ -321,14 +800,20 @@ export class AgentManager {
321
800
  if (code !== 0 && config.sessionId && !this._retried?.has(key)) {
322
801
  if (!this._retried) this._retried = new Set();
323
802
  this._retried.add(key);
324
- console.log(`[AgentManager] Retrying ${agentId} team=${teamId} without session (session may not exist locally)`);
803
+ console.log(`[AgentManager] Retrying ${agentId} workspace=${workspaceId} without session (session may not exist locally)`);
325
804
  const retryConfig = { ...config, sessionId: null };
326
- this._startAgent({ agentId, teamId, config: retryConfig }, connection);
805
+ this._startAgent({ agentId, workspaceId, config: retryConfig }, connection);
327
806
  return;
328
807
  }
329
808
 
330
- connection.send({ type: 'agent:status', agentId, teamId, status: 'inactive' });
331
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'offline', detail: '', entries: [] });
809
+ const offlineDetail = resolveExitOfflineDetail({
810
+ code,
811
+ signal,
812
+ stopCause: agent?.stopCause ?? null,
813
+ });
814
+
815
+ connection.send({ type: 'agent:status', agentId, workspaceId, status: 'inactive' });
816
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'offline', detail: offlineDetail, entries: [] });
332
817
  });
333
818
 
334
819
  // Send startup prompt
@@ -338,9 +823,9 @@ export class AgentManager {
338
823
  this._write(key, 'You have just started. Follow your startup sequence: first call read_memory with path="MEMORY.md" to load your memory index, then call check_messages.');
339
824
  }
340
825
 
341
- console.log(`[AgentManager] Agent ${config.displayName ?? agentId} is now active (team=${teamName ?? teamId ?? 'none'})`);
342
- connection.send({ type: 'agent:status', agentId, teamId, status: 'active' });
343
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'online', detail: '', entries: [] });
826
+ console.log(`[AgentManager] Agent ${config.displayName ?? agentId} is now active (workspace=${workspaceName ?? workspaceId ?? 'none'})`);
827
+ connection.send({ type: 'agent:status', agentId, workspaceId, status: 'active' });
828
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
344
829
 
345
830
  // Flush any messages that arrived while the agent was starting
346
831
  this._flushPending(key, connection);
@@ -363,34 +848,122 @@ export class AgentManager {
363
848
  return stopSession(platform);
364
849
  }
365
850
 
366
- _stopAgent(agentId, teamId, connection) {
367
- const key = this._key(agentId, teamId);
851
+ _handlePolicyInvalidate(msg, connection) {
852
+ const leaseIds = Array.isArray(msg.lease_ids) ? msg.lease_ids.filter(Boolean) : [];
853
+ const reason = msg.reason ?? 'policy_updated';
854
+ const newPolicyHash = msg.new_policy_hash ?? null;
855
+ const invalidated = markInvalidatedLeases(leaseIds, { reason, newPolicyHash });
856
+ const invalidatedSet = new Set(leaseIds);
857
+ const normalizedReason = String(reason).trim().toLowerCase();
858
+ const forceFreshSession = normalizedReason === 'policy_major_changed'
859
+ || normalizedReason === 'policy_major_change';
860
+
861
+ console.log(`[AgentManager] Received policy_invalidate: reason=${reason} leases=${invalidated}`);
862
+ connection.send({
863
+ type: 'policy_invalidate:ack',
864
+ lease_ids: leaseIds,
865
+ reason,
866
+ received_at: new Date().toISOString(),
867
+ });
868
+
869
+ let stopped = 0;
870
+ for (const [agentKey, agent] of this.agents.entries()) {
871
+ const leaseId = agent.directive?.policy_lease?.lease_id ?? null;
872
+ // Fail-closed: when no lease_id is present on agent state, treat it as affected.
873
+ const shouldStop = invalidatedSet.size === 0
874
+ || !leaseId
875
+ || invalidatedSet.has(leaseId);
876
+ if (!shouldStop) continue;
877
+
878
+ let workspaceId = agent.workspaceId ?? null;
879
+ let agentId = agent.agentId ?? null;
880
+ if (!agentId) {
881
+ const splitAt = agentKey.indexOf(':');
882
+ if (splitAt === -1) {
883
+ agentId = agentKey;
884
+ } else {
885
+ workspaceId = workspaceId ?? (agentKey.slice(0, splitAt) || null);
886
+ agentId = agentKey.slice(splitAt + 1);
887
+ }
888
+ }
889
+
890
+ if (forceFreshSession && agent.sessionId && agentId) {
891
+ agent.sessionId = null;
892
+ connection.send({ type: 'agent:session', agentId, workspaceId, sessionId: null });
893
+ }
894
+
895
+ this._clearDirectiveRefresh(agentKey);
896
+ agent.proc?.kill();
897
+ stopped += 1;
898
+ }
899
+
900
+ if (stopped > 0) {
901
+ console.log(`[AgentManager] policy_invalidate stopped ${stopped} active agent process(es)`);
902
+ }
903
+ }
904
+
905
+ _handleCredentialRevoked(msg, connection) {
906
+ const credentialIds = Array.isArray(msg.credential_ids) ? msg.credential_ids.filter(Boolean) : [];
907
+ const reason = msg.reason ?? 'credential_revoked';
908
+ console.log(`[AgentManager] Received credential_revoked: reason=${reason} credential_ids=${credentialIds.join(',') || 'none'}`);
909
+ connection.send({
910
+ type: 'credential_revoked:ack',
911
+ credential_ids: credentialIds,
912
+ reason,
913
+ received_at: new Date().toISOString(),
914
+ });
915
+
916
+ const revoked = new Set(credentialIds);
917
+ let stopped = 0;
918
+ for (const [agentKey, agent] of this.agents.entries()) {
919
+ const required = Array.isArray(agent.requiredCredentials)
920
+ ? agent.requiredCredentials.filter(Boolean)
921
+ : [];
922
+ // Fail-closed: if directive omitted required credential IDs, assume this agent may be affected.
923
+ const shouldStop = revoked.size === 0
924
+ || required.length === 0
925
+ || required.some((id) => revoked.has(id));
926
+ if (!shouldStop) continue;
927
+ this._clearDirectiveRefresh(agentKey);
928
+ agent.stopCause = 'credential_revoked';
929
+ agent.proc?.kill();
930
+ stopped += 1;
931
+ }
932
+ if (stopped > 0) {
933
+ console.log(`[AgentManager] credential_revoked stopped ${stopped} active agent process(es)`);
934
+ }
935
+ }
936
+
937
+ _stopAgent(agentId, workspaceId, connection) {
938
+ const key = this._key(agentId, workspaceId);
368
939
  const agent = this.agents.get(key);
369
940
  if (!agent) return;
370
- console.log(`[AgentManager] Stopping agent ${agentId} team=${teamId ?? 'none'}`);
941
+ console.log(`[AgentManager] Stopping agent ${agentId} workspace=${workspaceId ?? 'none'}`);
942
+ this._clearDirectiveRefresh(key);
943
+ agent.stopCause = 'manual_stop';
371
944
  agent.proc?.kill();
372
945
  // exit handler will report status
373
946
  }
374
947
 
375
948
  _deliverMessage(msg, connection) {
376
- const { agentId, teamId, seq, message } = msg;
377
- const key = this._key(agentId, teamId);
378
- connection.send({ type: 'agent:deliver:ack', agentId, teamId, seq });
949
+ const { agentId, workspaceId, seq, message } = msg;
950
+ const key = this._key(agentId, workspaceId);
951
+ connection.send({ type: 'agent:deliver:ack', agentId, workspaceId, seq });
379
952
 
380
953
  if (!this.agents.has(key) && !this.starting.has(key)) {
381
954
  // Agent not running — queue the message and request config to spawn it
382
- console.log(`[AgentManager] Agent ${agentId.slice(0,8)} team=${message.team_name ?? teamId} not running, requesting start for seq=${seq}`);
955
+ console.log(`[AgentManager] Agent ${agentId.slice(0,8)} workspace=${message.workspace_name ?? workspaceId} not running, requesting start for seq=${seq}`);
383
956
  if (!this._pendingMessages) this._pendingMessages = new Map();
384
957
  const pending = this._pendingMessages.get(key) ?? [];
385
958
  pending.push(msg);
386
959
  this._pendingMessages.set(key, pending);
387
- connection.send({ type: 'agent:request_start', agentId, teamId });
960
+ connection.send({ type: 'agent:request_start', agentId, workspaceId });
388
961
  return;
389
962
  }
390
963
 
391
964
  if (this.starting.has(key)) {
392
965
  // Spawn in progress — queue the message for delivery after start
393
- console.log(`[AgentManager] Agent ${agentId.slice(0,8)} team=${message.team_name ?? teamId} still starting, queuing seq=${seq}`);
966
+ console.log(`[AgentManager] Agent ${agentId.slice(0,8)} workspace=${message.workspace_name ?? workspaceId} still starting, queuing seq=${seq}`);
394
967
  if (!this._pendingMessages) this._pendingMessages = new Map();
395
968
  const pending = this._pendingMessages.get(key) ?? [];
396
969
  pending.push(msg);
@@ -400,7 +973,7 @@ export class AgentManager {
400
973
 
401
974
  const text = this._formatDeliveryText(message);
402
975
  const agent = this.agents.get(key);
403
- console.log(`[AgentManager] Delivering seq=${seq} to agent ${agent?.config?.displayName ?? agentId.slice(0,8)} team=${message.team_name ?? teamId}`);
976
+ console.log(`[AgentManager] Delivering seq=${seq} to agent ${agent?.config?.displayName ?? agentId.slice(0,8)} workspace=${message.workspace_name ?? workspaceId}`);
404
977
  if (agent?.runtime === 'codex') {
405
978
  if (!this._pendingMessages) this._pendingMessages = new Map();
406
979
  const pending = this._pendingMessages.get(key) ?? [];
@@ -418,10 +991,10 @@ export class AgentManager {
418
991
  if (!pending || pending.length === 0) return;
419
992
  this._pendingMessages.delete(key);
420
993
  for (const msg of pending) {
421
- const { agentId, teamId, seq, message } = msg;
994
+ const { agentId, workspaceId, seq, message } = msg;
422
995
  const text = this._formatDeliveryText(message);
423
996
  const agent = this.agents.get(key);
424
- console.log(`[AgentManager] Flushing queued seq=${seq} to agent ${agent?.config?.displayName ?? agentId.slice(0,8)} team=${message.team_name ?? teamId}`);
997
+ console.log(`[AgentManager] Flushing queued seq=${seq} to agent ${agent?.config?.displayName ?? agentId.slice(0,8)} workspace=${message.workspace_name ?? workspaceId}`);
425
998
  this._write(key, text);
426
999
  }
427
1000
  }
@@ -454,7 +1027,7 @@ export class AgentManager {
454
1027
  }
455
1028
  }
456
1029
 
457
- _parseKimiLine(key, agentId, teamId, line, connection) {
1030
+ _parseKimiLine(key, agentId, workspaceId, line, connection) {
458
1031
  const agent = this.agents.get(key);
459
1032
  if (!agent) return;
460
1033
  const events = parseKimiLine(line, agent.kimiState);
@@ -463,18 +1036,18 @@ export class AgentManager {
463
1036
  case 'session_init':
464
1037
  if (agent.sessionId !== evt.sessionId) {
465
1038
  agent.sessionId = evt.sessionId;
466
- connection.send({ type: 'agent:session', agentId, teamId, sessionId: evt.sessionId });
1039
+ connection.send({ type: 'agent:session', agentId, workspaceId, sessionId: evt.sessionId });
467
1040
  }
468
1041
  break;
469
1042
  case 'thinking':
470
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'thinking', detail: '', entries: [] });
1043
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
471
1044
  break;
472
1045
  case 'tool_call':
473
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'working', detail: evt.name, entries: [] });
1046
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'working', detail: evt.name, entries: [] });
474
1047
  break;
475
1048
  case 'turn_end':
476
1049
  agent.kimiIdle = true;
477
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'online', detail: '', entries: [] });
1050
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
478
1051
  break;
479
1052
  case 'error':
480
1053
  console.error(`[AgentManager][kimi][${agentId}] Error: ${evt.message}`);
@@ -483,7 +1056,7 @@ export class AgentManager {
483
1056
  }
484
1057
  }
485
1058
 
486
- _parseCodexLine(key, agentId, teamId, line, connection) {
1059
+ _parseCodexLine(key, agentId, workspaceId, line, connection) {
487
1060
  const agent = this.agents.get(key);
488
1061
  if (!agent) return;
489
1062
  const events = parseCodexLine(line);
@@ -492,17 +1065,17 @@ export class AgentManager {
492
1065
  case 'session_init':
493
1066
  if (agent.sessionId !== evt.sessionId) {
494
1067
  agent.sessionId = evt.sessionId;
495
- connection.send({ type: 'agent:session', agentId, teamId, sessionId: evt.sessionId });
1068
+ connection.send({ type: 'agent:session', agentId, workspaceId, sessionId: evt.sessionId });
496
1069
  }
497
1070
  break;
498
1071
  case 'thinking':
499
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'thinking', detail: '', entries: [] });
1072
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
500
1073
  break;
501
1074
  case 'tool_call':
502
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'working', detail: evt.name, entries: [] });
1075
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'working', detail: evt.name, entries: [] });
503
1076
  break;
504
1077
  case 'turn_end':
505
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'online', detail: '', entries: [] });
1078
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
506
1079
  break;
507
1080
  case 'error':
508
1081
  console.error(`[AgentManager][codex][${agentId}] Error: ${evt.message}`);
@@ -511,7 +1084,7 @@ export class AgentManager {
511
1084
  }
512
1085
  }
513
1086
 
514
- _parseLine(key, agentId, teamId, line, connection) {
1087
+ _parseLine(key, agentId, workspaceId, line, connection) {
515
1088
  let event;
516
1089
  try { event = JSON.parse(line); }
517
1090
  catch { return; }
@@ -521,7 +1094,7 @@ export class AgentManager {
521
1094
  const agent = this.agents.get(key);
522
1095
  if (agent && agent.sessionId !== event.session_id) {
523
1096
  agent.sessionId = event.session_id;
524
- connection.send({ type: 'agent:session', agentId, teamId, sessionId: event.session_id });
1097
+ connection.send({ type: 'agent:session', agentId, workspaceId, sessionId: event.session_id });
525
1098
  }
526
1099
  }
527
1100
 
@@ -536,11 +1109,11 @@ export class AgentManager {
536
1109
  console.log(`[AgentManager][${displayName}] <text> ${block.text?.slice(0, 500)}`);
537
1110
  } else if (block.type === 'tool_use') {
538
1111
  console.log(`[AgentManager][${displayName}] <tool_use> ${block.name} params=${JSON.stringify(block.input ?? {}).slice(0, 500)}`);
539
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'working', detail: block.name, entries: [] });
1112
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'working', detail: block.name, entries: [] });
540
1113
  }
541
1114
  }
542
1115
  if (!content.some(c => c.type === 'tool_use')) {
543
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'thinking', detail: '', entries: [] });
1116
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
544
1117
  }
545
1118
  } else if (event.type === 'tool') {
546
1119
  const content = event.content;
@@ -559,7 +1132,7 @@ export class AgentManager {
559
1132
  }
560
1133
  } else if (event.type === 'result') {
561
1134
  console.log(`[AgentManager][${displayName}] turn done (stop_reason=${event.stop_reason ?? '?'})`);
562
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'online', detail: '', entries: [] });
1135
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
563
1136
  }
564
1137
  }
565
1138
  }