@lightcone-ai/daemon 0.9.78 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/mcp-servers/mysql/index.js +13 -5
  2. package/mcp-servers/mysql/manifest.json +16 -0
  3. package/mcp-servers/official/company-fundamentals/index.js +34 -0
  4. package/mcp-servers/official/company-fundamentals/manifest.json +14 -0
  5. package/mcp-servers/official/compliance-check/index.js +49 -0
  6. package/mcp-servers/official/compliance-check/manifest.json +14 -0
  7. package/mcp-servers/official/industry-report/index.js +34 -0
  8. package/mcp-servers/official/industry-report/manifest.json +14 -0
  9. package/mcp-servers/official/market-data-query/index.js +34 -0
  10. package/mcp-servers/official/market-data-query/manifest.json +14 -0
  11. package/mcp-servers/official/portfolio-analysis/index.js +74 -0
  12. package/mcp-servers/official/portfolio-analysis/manifest.json +14 -0
  13. package/mcp-servers/official/portfolio-read/index.js +34 -0
  14. package/mcp-servers/official/portfolio-read/manifest.json +14 -0
  15. package/mcp-servers/official/research-fetch/index.js +35 -0
  16. package/mcp-servers/official/research-fetch/manifest.json +14 -0
  17. package/mcp-servers/official/risk-metrics/index.js +34 -0
  18. package/mcp-servers/official/risk-metrics/manifest.json +14 -0
  19. package/mcp-servers/official-common/fixtures.js +273 -0
  20. package/mcp-servers/official-common/server.js +34 -0
  21. package/mcp-servers/platform/manifest.json +15 -0
  22. package/mcp-servers/portfolio-analysis/core.js +592 -0
  23. package/mcp-servers/portfolio-analysis/index.js +45 -0
  24. package/mcp-servers/portfolio-analysis/package-lock.json +1139 -0
  25. package/mcp-servers/portfolio-analysis/package.json +10 -0
  26. package/mcp-servers/portfolio-read/core.js +330 -0
  27. package/mcp-servers/portfolio-read/index.js +127 -0
  28. package/mcp-servers/portfolio-read/package-lock.json +1243 -0
  29. package/mcp-servers/portfolio-read/package.json +11 -0
  30. package/mcp-servers/publisher/adapters/douyin.js +112 -20
  31. package/mcp-servers/publisher/index.js +84 -8
  32. package/mcp-servers/publisher/manifest.json +16 -0
  33. package/package.json +1 -1
  34. package/src/agent-manager.js +761 -187
  35. package/src/chat-bridge.js +567 -92
  36. package/src/connection.js +1 -1
  37. package/src/drivers/claude.js +48 -45
  38. package/src/drivers/codex.js +110 -7
  39. package/src/drivers/kimi.js +80 -34
  40. package/src/governance-state.js +89 -0
  41. package/src/index.js +34 -16
  42. package/src/lease-window.js +8 -0
  43. package/src/mcp-config.js +52 -21
@@ -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,80 +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
- workspaceDir,
242
- serverUrl: this.serverUrl,
243
- authToken: config.authToken,
244
- }));
245
-
246
- const mcpConfig = { mcpServers };
247
- console.log(`[AgentManager] MCP servers for ${config.displayName ?? agentId}: ${Object.keys(mcpServers).join(', ')}`);
248
- for (const [name, mc] of Object.entries(mcpServers)) {
249
- console.log(`[AgentManager] mcp:${name} → ${mc.command} ${(mc.args ?? []).join(' ')}`);
250
- }
251
-
252
- const args = [
253
- '--print',
254
- '--allow-dangerously-skip-permissions',
255
- '--dangerously-skip-permissions',
256
- '--verbose',
257
- '--output-format', 'stream-json',
258
- '--input-format', 'stream-json',
259
- '--mcp-config', JSON.stringify(mcpConfig),
260
- '--system-prompt', buildSystemPrompt(config, agentId, skills),
261
- '--disallowed-tools', 'EnterPlanMode,ExitPlanMode',
262
- ];
263
-
264
- if (config.sessionId) {
265
- // Only resume if the session file exists locally
266
- const projectSlug = workspaceDir.replace(/[\/\.]/g, '-');
267
- const sessionFile = path.join(homedir(), '.claude', 'projects', projectSlug, `${config.sessionId}.jsonl`);
268
- try {
269
- statSync(sessionFile);
270
- args.push('--resume', config.sessionId);
271
- } catch {
272
- console.log(`[AgentManager] Session ${config.sessionId} not found locally, starting fresh`);
273
- }
274
- }
275
-
276
- const spawnEnv = { ...process.env, FORCE_COLOR: '0', ...(config.envVars ?? {}) };
277
- delete spawnEnv.CLAUDECODE;
278
-
279
- console.log(`[AgentManager] Spawning claude for ${config.displayName ?? agentId} team=${teamName ?? teamId ?? 'none'} (session=${config.sessionId ?? 'new'})`);
280
-
281
- proc = spawn('claude', args, {
282
- cwd: workspaceDir,
283
- env: spawnEnv,
284
- stdio: ['pipe', 'pipe', 'pipe'],
760
+ sessionId: config.sessionId ?? null,
761
+ proc,
762
+ runtime: 'claude',
763
+ directive,
764
+ requiredCredentials,
765
+ stopCause: null,
285
766
  });
286
-
287
- this.agents.set(key, { config, teamId, agentId, sessionId: config.sessionId ?? null, proc, runtime: 'claude' });
288
767
  this.starting.delete(key);
289
768
 
290
- // Parse stdout stream for session ID and activity updates
291
769
  let buffer = '';
292
770
  proc.stdout.on('data', (chunk) => {
293
771
  buffer += chunk.toString();
@@ -295,7 +773,7 @@ export class AgentManager {
295
773
  buffer = lines.pop();
296
774
  for (const line of lines) {
297
775
  if (!line.trim()) continue;
298
- this._parseLine(key, agentId, teamId, line, connection);
776
+ this._parseLine(key, agentId, workspaceId, line, connection);
299
777
  }
300
778
  });
301
779
  }
@@ -305,14 +783,16 @@ export class AgentManager {
305
783
  if (text) console.error(`[AgentManager][${config.displayName ?? agentId}] stderr: ${text.slice(0, 500)}`);
306
784
  });
307
785
 
308
- proc.on('exit', (code) => {
786
+ proc.on('exit', (code, signal) => {
787
+ if (spawnErrorReported) return;
309
788
  const agent = this.agents.get(key);
310
- 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);
311
791
  this.agents.delete(key);
312
792
 
313
793
  if (code === 0 && runtime === 'codex' && this._pendingMessages?.get(key)?.length) {
314
794
  const restartConfig = { ...config, sessionId: agent?.sessionId ?? config.sessionId ?? null };
315
- this._startAgent({ agentId, teamId, config: restartConfig }, connection);
795
+ this._startAgent({ agentId, workspaceId, config: restartConfig }, connection);
316
796
  return;
317
797
  }
318
798
 
@@ -320,14 +800,20 @@ export class AgentManager {
320
800
  if (code !== 0 && config.sessionId && !this._retried?.has(key)) {
321
801
  if (!this._retried) this._retried = new Set();
322
802
  this._retried.add(key);
323
- 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)`);
324
804
  const retryConfig = { ...config, sessionId: null };
325
- this._startAgent({ agentId, teamId, config: retryConfig }, connection);
805
+ this._startAgent({ agentId, workspaceId, config: retryConfig }, connection);
326
806
  return;
327
807
  }
328
808
 
329
- connection.send({ type: 'agent:status', agentId, teamId, status: 'inactive' });
330
- 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: [] });
331
817
  });
332
818
 
333
819
  // Send startup prompt
@@ -337,9 +823,9 @@ export class AgentManager {
337
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.');
338
824
  }
339
825
 
340
- console.log(`[AgentManager] Agent ${config.displayName ?? agentId} is now active (team=${teamName ?? teamId ?? 'none'})`);
341
- connection.send({ type: 'agent:status', agentId, teamId, status: 'active' });
342
- 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: [] });
343
829
 
344
830
  // Flush any messages that arrived while the agent was starting
345
831
  this._flushPending(key, connection);
@@ -362,34 +848,122 @@ export class AgentManager {
362
848
  return stopSession(platform);
363
849
  }
364
850
 
365
- _stopAgent(agentId, teamId, connection) {
366
- 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);
367
939
  const agent = this.agents.get(key);
368
940
  if (!agent) return;
369
- 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';
370
944
  agent.proc?.kill();
371
945
  // exit handler will report status
372
946
  }
373
947
 
374
948
  _deliverMessage(msg, connection) {
375
- const { agentId, teamId, seq, message } = msg;
376
- const key = this._key(agentId, teamId);
377
- 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 });
378
952
 
379
953
  if (!this.agents.has(key) && !this.starting.has(key)) {
380
954
  // Agent not running — queue the message and request config to spawn it
381
- 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}`);
382
956
  if (!this._pendingMessages) this._pendingMessages = new Map();
383
957
  const pending = this._pendingMessages.get(key) ?? [];
384
958
  pending.push(msg);
385
959
  this._pendingMessages.set(key, pending);
386
- connection.send({ type: 'agent:request_start', agentId, teamId });
960
+ connection.send({ type: 'agent:request_start', agentId, workspaceId });
387
961
  return;
388
962
  }
389
963
 
390
964
  if (this.starting.has(key)) {
391
965
  // Spawn in progress — queue the message for delivery after start
392
- 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}`);
393
967
  if (!this._pendingMessages) this._pendingMessages = new Map();
394
968
  const pending = this._pendingMessages.get(key) ?? [];
395
969
  pending.push(msg);
@@ -399,7 +973,7 @@ export class AgentManager {
399
973
 
400
974
  const text = this._formatDeliveryText(message);
401
975
  const agent = this.agents.get(key);
402
- 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}`);
403
977
  if (agent?.runtime === 'codex') {
404
978
  if (!this._pendingMessages) this._pendingMessages = new Map();
405
979
  const pending = this._pendingMessages.get(key) ?? [];
@@ -417,10 +991,10 @@ export class AgentManager {
417
991
  if (!pending || pending.length === 0) return;
418
992
  this._pendingMessages.delete(key);
419
993
  for (const msg of pending) {
420
- const { agentId, teamId, seq, message } = msg;
994
+ const { agentId, workspaceId, seq, message } = msg;
421
995
  const text = this._formatDeliveryText(message);
422
996
  const agent = this.agents.get(key);
423
- 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}`);
424
998
  this._write(key, text);
425
999
  }
426
1000
  }
@@ -453,7 +1027,7 @@ export class AgentManager {
453
1027
  }
454
1028
  }
455
1029
 
456
- _parseKimiLine(key, agentId, teamId, line, connection) {
1030
+ _parseKimiLine(key, agentId, workspaceId, line, connection) {
457
1031
  const agent = this.agents.get(key);
458
1032
  if (!agent) return;
459
1033
  const events = parseKimiLine(line, agent.kimiState);
@@ -462,18 +1036,18 @@ export class AgentManager {
462
1036
  case 'session_init':
463
1037
  if (agent.sessionId !== evt.sessionId) {
464
1038
  agent.sessionId = evt.sessionId;
465
- connection.send({ type: 'agent:session', agentId, teamId, sessionId: evt.sessionId });
1039
+ connection.send({ type: 'agent:session', agentId, workspaceId, sessionId: evt.sessionId });
466
1040
  }
467
1041
  break;
468
1042
  case 'thinking':
469
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'thinking', detail: '', entries: [] });
1043
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
470
1044
  break;
471
1045
  case 'tool_call':
472
- 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: [] });
473
1047
  break;
474
1048
  case 'turn_end':
475
1049
  agent.kimiIdle = true;
476
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'online', detail: '', entries: [] });
1050
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
477
1051
  break;
478
1052
  case 'error':
479
1053
  console.error(`[AgentManager][kimi][${agentId}] Error: ${evt.message}`);
@@ -482,7 +1056,7 @@ export class AgentManager {
482
1056
  }
483
1057
  }
484
1058
 
485
- _parseCodexLine(key, agentId, teamId, line, connection) {
1059
+ _parseCodexLine(key, agentId, workspaceId, line, connection) {
486
1060
  const agent = this.agents.get(key);
487
1061
  if (!agent) return;
488
1062
  const events = parseCodexLine(line);
@@ -491,17 +1065,17 @@ export class AgentManager {
491
1065
  case 'session_init':
492
1066
  if (agent.sessionId !== evt.sessionId) {
493
1067
  agent.sessionId = evt.sessionId;
494
- connection.send({ type: 'agent:session', agentId, teamId, sessionId: evt.sessionId });
1068
+ connection.send({ type: 'agent:session', agentId, workspaceId, sessionId: evt.sessionId });
495
1069
  }
496
1070
  break;
497
1071
  case 'thinking':
498
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'thinking', detail: '', entries: [] });
1072
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
499
1073
  break;
500
1074
  case 'tool_call':
501
- 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: [] });
502
1076
  break;
503
1077
  case 'turn_end':
504
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'online', detail: '', entries: [] });
1078
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
505
1079
  break;
506
1080
  case 'error':
507
1081
  console.error(`[AgentManager][codex][${agentId}] Error: ${evt.message}`);
@@ -510,7 +1084,7 @@ export class AgentManager {
510
1084
  }
511
1085
  }
512
1086
 
513
- _parseLine(key, agentId, teamId, line, connection) {
1087
+ _parseLine(key, agentId, workspaceId, line, connection) {
514
1088
  let event;
515
1089
  try { event = JSON.parse(line); }
516
1090
  catch { return; }
@@ -520,7 +1094,7 @@ export class AgentManager {
520
1094
  const agent = this.agents.get(key);
521
1095
  if (agent && agent.sessionId !== event.session_id) {
522
1096
  agent.sessionId = event.session_id;
523
- connection.send({ type: 'agent:session', agentId, teamId, sessionId: event.session_id });
1097
+ connection.send({ type: 'agent:session', agentId, workspaceId, sessionId: event.session_id });
524
1098
  }
525
1099
  }
526
1100
 
@@ -535,11 +1109,11 @@ export class AgentManager {
535
1109
  console.log(`[AgentManager][${displayName}] <text> ${block.text?.slice(0, 500)}`);
536
1110
  } else if (block.type === 'tool_use') {
537
1111
  console.log(`[AgentManager][${displayName}] <tool_use> ${block.name} params=${JSON.stringify(block.input ?? {}).slice(0, 500)}`);
538
- 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: [] });
539
1113
  }
540
1114
  }
541
1115
  if (!content.some(c => c.type === 'tool_use')) {
542
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'thinking', detail: '', entries: [] });
1116
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
543
1117
  }
544
1118
  } else if (event.type === 'tool') {
545
1119
  const content = event.content;
@@ -558,7 +1132,7 @@ export class AgentManager {
558
1132
  }
559
1133
  } else if (event.type === 'result') {
560
1134
  console.log(`[AgentManager][${displayName}] turn done (stop_reason=${event.stop_reason ?? '?'})`);
561
- connection.send({ type: 'agent:activity', agentId, teamId, activity: 'online', detail: '', entries: [] });
1135
+ connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
562
1136
  }
563
1137
  }
564
1138
  }