opc-agent 2.0.0 → 2.0.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 (156) hide show
  1. package/dist/channels/email.d.ts +32 -26
  2. package/dist/channels/email.js +239 -62
  3. package/dist/channels/feishu.d.ts +21 -6
  4. package/dist/channels/feishu.js +225 -126
  5. package/dist/channels/websocket.d.ts +46 -3
  6. package/dist/channels/websocket.js +306 -37
  7. package/dist/channels/wechat.d.ts +33 -13
  8. package/dist/channels/wechat.js +229 -42
  9. package/dist/cli.js +712 -11
  10. package/dist/core/a2a.d.ts +17 -0
  11. package/dist/core/a2a.js +43 -1
  12. package/dist/core/agent.d.ts +16 -0
  13. package/dist/core/agent.js +108 -0
  14. package/dist/core/runtime.d.ts +6 -0
  15. package/dist/core/runtime.js +161 -2
  16. package/dist/core/sandbox.d.ts +26 -0
  17. package/dist/core/sandbox.js +117 -0
  18. package/dist/core/workflow-graph.d.ts +93 -0
  19. package/dist/core/workflow-graph.js +247 -0
  20. package/dist/doctor.d.ts +15 -0
  21. package/dist/doctor.js +183 -0
  22. package/dist/eval/index.d.ts +65 -0
  23. package/dist/eval/index.js +191 -0
  24. package/dist/index.d.ts +30 -6
  25. package/dist/index.js +60 -4
  26. package/dist/plugins/content-filter.d.ts +7 -0
  27. package/dist/plugins/content-filter.js +25 -0
  28. package/dist/plugins/index.d.ts +42 -0
  29. package/dist/plugins/index.js +108 -2
  30. package/dist/plugins/logger.d.ts +6 -0
  31. package/dist/plugins/logger.js +20 -0
  32. package/dist/plugins/rate-limiter.d.ts +7 -0
  33. package/dist/plugins/rate-limiter.js +35 -0
  34. package/dist/protocols/a2a/client.d.ts +25 -0
  35. package/dist/protocols/a2a/client.js +115 -0
  36. package/dist/protocols/a2a/index.d.ts +6 -0
  37. package/dist/protocols/a2a/index.js +12 -0
  38. package/dist/protocols/a2a/server.d.ts +41 -0
  39. package/dist/protocols/a2a/server.js +295 -0
  40. package/dist/protocols/a2a/types.d.ts +91 -0
  41. package/dist/protocols/a2a/types.js +15 -0
  42. package/dist/protocols/a2a/utils.d.ts +6 -0
  43. package/dist/protocols/a2a/utils.js +47 -0
  44. package/dist/protocols/agui/client.d.ts +10 -0
  45. package/dist/protocols/agui/client.js +75 -0
  46. package/dist/protocols/agui/index.d.ts +4 -0
  47. package/dist/protocols/agui/index.js +25 -0
  48. package/dist/protocols/agui/server.d.ts +37 -0
  49. package/dist/protocols/agui/server.js +191 -0
  50. package/dist/protocols/agui/types.d.ts +107 -0
  51. package/dist/protocols/agui/types.js +17 -0
  52. package/dist/protocols/index.d.ts +2 -0
  53. package/dist/protocols/index.js +19 -0
  54. package/dist/protocols/mcp/agent-tools.d.ts +11 -0
  55. package/dist/protocols/mcp/agent-tools.js +129 -0
  56. package/dist/protocols/mcp/index.d.ts +5 -0
  57. package/dist/protocols/mcp/index.js +11 -0
  58. package/dist/protocols/mcp/server.d.ts +31 -0
  59. package/dist/protocols/mcp/server.js +248 -0
  60. package/dist/protocols/mcp/types.d.ts +92 -0
  61. package/dist/protocols/mcp/types.js +17 -0
  62. package/dist/publish/index.d.ts +45 -0
  63. package/dist/publish/index.js +350 -0
  64. package/dist/schema/oad.d.ts +682 -65
  65. package/dist/schema/oad.js +36 -3
  66. package/dist/security/approval.d.ts +36 -0
  67. package/dist/security/approval.js +113 -0
  68. package/dist/security/index.d.ts +4 -0
  69. package/dist/security/index.js +8 -0
  70. package/dist/security/keys.d.ts +16 -0
  71. package/dist/security/keys.js +117 -0
  72. package/dist/studio/server.d.ts +63 -0
  73. package/dist/studio/server.js +625 -0
  74. package/dist/studio-ui/index.html +662 -0
  75. package/dist/telemetry/index.d.ts +93 -0
  76. package/dist/telemetry/index.js +285 -0
  77. package/package.json +5 -3
  78. package/scripts/install.ps1 +31 -0
  79. package/scripts/install.sh +40 -0
  80. package/src/channels/email.ts +351 -177
  81. package/src/channels/feishu.ts +349 -236
  82. package/src/channels/websocket.ts +399 -87
  83. package/src/channels/wechat.ts +329 -149
  84. package/src/cli.ts +783 -12
  85. package/src/core/a2a.ts +60 -0
  86. package/src/core/agent.ts +125 -0
  87. package/src/core/runtime.ts +127 -0
  88. package/src/core/sandbox.ts +143 -0
  89. package/src/core/workflow-graph.ts +365 -0
  90. package/src/doctor.ts +156 -0
  91. package/src/eval/index.ts +211 -0
  92. package/src/eval/suites/basic.json +16 -0
  93. package/src/eval/suites/memory.json +12 -0
  94. package/src/eval/suites/safety.json +14 -0
  95. package/src/index.ts +54 -6
  96. package/src/plugins/content-filter.ts +23 -0
  97. package/src/plugins/index.ts +133 -2
  98. package/src/plugins/logger.ts +18 -0
  99. package/src/plugins/rate-limiter.ts +38 -0
  100. package/src/protocols/a2a/client.ts +132 -0
  101. package/src/protocols/a2a/index.ts +8 -0
  102. package/src/protocols/a2a/server.ts +333 -0
  103. package/src/protocols/a2a/types.ts +88 -0
  104. package/src/protocols/a2a/utils.ts +50 -0
  105. package/src/protocols/agui/client.ts +83 -0
  106. package/src/protocols/agui/index.ts +4 -0
  107. package/src/protocols/agui/server.ts +218 -0
  108. package/src/protocols/agui/types.ts +153 -0
  109. package/src/protocols/index.ts +2 -0
  110. package/src/protocols/mcp/agent-tools.ts +134 -0
  111. package/src/protocols/mcp/index.ts +8 -0
  112. package/src/protocols/mcp/server.ts +262 -0
  113. package/src/protocols/mcp/types.ts +69 -0
  114. package/src/publish/index.ts +376 -0
  115. package/src/schema/oad.ts +39 -2
  116. package/src/security/approval.ts +131 -0
  117. package/src/security/index.ts +3 -0
  118. package/src/security/keys.ts +87 -0
  119. package/src/studio/server.ts +629 -0
  120. package/src/studio-ui/index.html +662 -0
  121. package/src/telemetry/index.ts +324 -0
  122. package/src/types/agent-workstation.d.ts +2 -0
  123. package/tests/a2a-protocol.test.ts +285 -0
  124. package/tests/agui-protocol.test.ts +246 -0
  125. package/tests/channels/discord.test.ts +79 -0
  126. package/tests/channels/email.test.ts +148 -0
  127. package/tests/channels/feishu.test.ts +123 -0
  128. package/tests/channels/telegram.test.ts +129 -0
  129. package/tests/channels/websocket.test.ts +53 -0
  130. package/tests/channels/wechat.test.ts +170 -0
  131. package/tests/chat-cli.test.ts +160 -0
  132. package/tests/daemon.test.ts +135 -0
  133. package/tests/deepbrain-wire.test.ts +234 -0
  134. package/tests/doctor.test.ts +38 -0
  135. package/tests/eval.test.ts +173 -0
  136. package/tests/init-role.test.ts +124 -0
  137. package/tests/mcp-client.test.ts +92 -0
  138. package/tests/mcp-server.test.ts +178 -0
  139. package/tests/plugin-a2a-enhanced.test.ts +230 -0
  140. package/tests/publish.test.ts +231 -0
  141. package/tests/scheduler.test.ts +200 -0
  142. package/tests/security-enhanced.test.ts +233 -0
  143. package/tests/skill-learner.test.ts +161 -0
  144. package/tests/studio.test.ts +229 -0
  145. package/tests/subagent.test.ts +63 -0
  146. package/tests/telemetry.test.ts +186 -0
  147. package/tests/tools/builtin-extended.test.ts +138 -0
  148. package/tests/workflow-graph.test.ts +279 -0
  149. package/tutorial/customer-service-agent/README.md +612 -0
  150. package/tutorial/customer-service-agent/SOUL.md +26 -0
  151. package/tutorial/customer-service-agent/agent.yaml +63 -0
  152. package/tutorial/customer-service-agent/package.json +19 -0
  153. package/tutorial/customer-service-agent/src/index.ts +69 -0
  154. package/tutorial/customer-service-agent/src/skills/faq.ts +27 -0
  155. package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -0
  156. package/tutorial/customer-service-agent/tsconfig.json +14 -0
package/src/core/a2a.ts CHANGED
@@ -3,6 +3,66 @@ import { Room } from './room';
3
3
  import type { Message, IAgent } from './types';
4
4
  import { Logger } from './logger';
5
5
 
6
+ // ── Agent Card (v1.6.0 — simplified A2A) ────────────────────
7
+
8
+ export interface AgentCard {
9
+ name: string;
10
+ description: string;
11
+ capabilities: string[];
12
+ endpoint?: string;
13
+ handler?: (message: string) => Promise<string>;
14
+ }
15
+
16
+ export class AgentCardRegistry {
17
+ private cards: Map<string, AgentCard> = new Map();
18
+ private logger = new Logger('a2a:cards');
19
+
20
+ register(card: AgentCard): void {
21
+ this.cards.set(card.name, card);
22
+ this.logger.info('AgentCard registered', { name: card.name });
23
+ }
24
+
25
+ unregister(name: string): void {
26
+ this.cards.delete(name);
27
+ }
28
+
29
+ get(name: string): AgentCard | undefined {
30
+ return this.cards.get(name);
31
+ }
32
+
33
+ find(query: string): AgentCard[] {
34
+ const lower = query.toLowerCase();
35
+ return Array.from(this.cards.values()).filter(a =>
36
+ a.name.toLowerCase().includes(lower) ||
37
+ a.description.toLowerCase().includes(lower) ||
38
+ a.capabilities.some(c => c.toLowerCase().includes(lower))
39
+ );
40
+ }
41
+
42
+ async send(agentName: string, message: string): Promise<string> {
43
+ const agent = this.cards.get(agentName);
44
+ if (!agent) throw new Error(`Agent '${agentName}' not found`);
45
+
46
+ if (agent.handler) {
47
+ return agent.handler(message);
48
+ } else if (agent.endpoint) {
49
+ const res = await fetch(agent.endpoint, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify({ message }),
53
+ });
54
+ const data = await res.json() as any;
55
+ return data.response || data.content || '';
56
+ }
57
+
58
+ throw new Error(`Agent '${agentName}' has no handler or endpoint`);
59
+ }
60
+
61
+ list(): AgentCard[] {
62
+ return Array.from(this.cards.values());
63
+ }
64
+ }
65
+
6
66
  // ── A2A Types ───────────────────────────────────────────────
7
67
 
8
68
  export interface AgentCapability {
package/src/core/agent.ts CHANGED
@@ -6,6 +6,8 @@ import { SkillLearner } from '../skills/auto-learn';
6
6
  import type { MCPTool } from '../tools/mcp';
7
7
  import { MCPToolRegistry } from '../tools/mcp';
8
8
  import { SubAgentManager, type SubAgentConfig, type SubAgentResult } from './subagent';
9
+ import { Tracer } from '../telemetry';
10
+ import type { Span as TelemetrySpan } from '../telemetry';
9
11
 
10
12
  export class BaseAgent extends EventEmitter implements IAgent {
11
13
  readonly name: string;
@@ -21,6 +23,9 @@ export class BaseAgent extends EventEmitter implements IAgent {
21
23
  private skillLearner?: SkillLearner;
22
24
  private autoLearnConfig: { enabled: boolean; minConversationLength: number; improveOnUse: boolean };
23
25
  private _subAgentManager?: SubAgentManager;
26
+ private longTermMemory?: any;
27
+ private longTermMemoryConfig: { autoLearn: boolean; autoRecall: boolean } = { autoLearn: true, autoRecall: true };
28
+ private tracer?: Tracer;
24
29
 
25
30
  constructor(options: {
26
31
  name: string;
@@ -36,6 +41,7 @@ export class BaseAgent extends EventEmitter implements IAgent {
36
41
  improveOnUse?: boolean;
37
42
  };
38
43
  maxToolRounds?: number;
44
+ tracer?: Tracer;
39
45
  }) {
40
46
  super();
41
47
  this.name = options.name;
@@ -52,6 +58,25 @@ export class BaseAgent extends EventEmitter implements IAgent {
52
58
  if (options.skillsDir) {
53
59
  this.skillLearner = new SkillLearner(options.skillsDir);
54
60
  }
61
+ this.tracer = options.tracer;
62
+ }
63
+
64
+ setLongTermMemory(brain: any, config?: { autoLearn?: boolean; autoRecall?: boolean }): void {
65
+ this.longTermMemory = brain;
66
+ if (config) {
67
+ this.longTermMemoryConfig = {
68
+ autoLearn: config.autoLearn !== false,
69
+ autoRecall: config.autoRecall !== false,
70
+ };
71
+ }
72
+ }
73
+
74
+ getLongTermMemory(): any {
75
+ return this.longTermMemory;
76
+ }
77
+
78
+ getLongTermMemoryConfig(): { autoLearn: boolean; autoRecall: boolean } {
79
+ return this.longTermMemoryConfig;
55
80
  }
56
81
 
57
82
  get state(): AgentState {
@@ -78,6 +103,14 @@ export class BaseAgent extends EventEmitter implements IAgent {
78
103
  return this.toolRegistry;
79
104
  }
80
105
 
106
+ getTracer(): Tracer | undefined {
107
+ return this.tracer;
108
+ }
109
+
110
+ setTracer(tracer: Tracer): void {
111
+ this.tracer = tracer;
112
+ }
113
+
81
114
  registerTool(tool: MCPTool): void {
82
115
  this.toolRegistry.register(tool);
83
116
  }
@@ -143,9 +176,45 @@ export class BaseAgent extends EventEmitter implements IAgent {
143
176
  async handleMessage(message: Message): Promise<Message> {
144
177
  this.emit('message:in', message);
145
178
 
179
+ // Start root span if tracer is configured
180
+ let rootSpan: TelemetrySpan | undefined;
181
+ if (this.tracer) {
182
+ rootSpan = this.tracer.startSpan('handleMessage', {
183
+ kind: 'server',
184
+ attributes: {
185
+ 'message.channel': (message.metadata?.channel as string) || 'unknown',
186
+ 'message.sender': (message.metadata?.sender as string) || 'unknown',
187
+ 'message.length': message.content.length,
188
+ },
189
+ });
190
+ this.tracer.increment('agent.messages.total', 1, { agent: this.name });
191
+ }
192
+
146
193
  const sessionId = (message.metadata?.sessionId as string) ?? 'default';
147
194
  await this.memory.addMessage(sessionId, message);
148
195
 
196
+ // === Recall from long-term memory ===
197
+ let memoryContext = '';
198
+ if (this.longTermMemory && this.longTermMemoryConfig.autoRecall) {
199
+ let memorySpan: TelemetrySpan | undefined;
200
+ if (this.tracer && rootSpan) {
201
+ memorySpan = this.tracer.startSpan('memory.recall', { parent: rootSpan, kind: 'client' });
202
+ }
203
+ try {
204
+ const recalled = await this.longTermMemory.recall(message.content);
205
+ if (recalled && (Array.isArray(recalled) ? recalled.length > 0 : true)) {
206
+ memoryContext = '\n\n[Relevant memories]\n' +
207
+ (Array.isArray(recalled)
208
+ ? recalled.map((r: any) => typeof r === 'string' ? r : r.content || r.compiled_truth || '').join('\n')
209
+ : String(recalled));
210
+ }
211
+ if (this.tracer && memorySpan) this.tracer.endSpan(memorySpan, 'ok');
212
+ } catch {
213
+ if (this.tracer && memorySpan) this.tracer.endSpan(memorySpan, 'error');
214
+ // Silent fail — don't break chat if memory fails
215
+ }
216
+ }
217
+
149
218
  const context: AgentContext = {
150
219
  agentName: this.name,
151
220
  sessionId,
@@ -172,12 +241,21 @@ export class BaseAgent extends EventEmitter implements IAgent {
172
241
 
173
242
  // Check if a learned skill matches — prepend instructions to system prompt
174
243
  let effectiveSystemPrompt = this.systemPrompt;
244
+
245
+ // Inject long-term memory context
246
+ if (memoryContext) {
247
+ effectiveSystemPrompt = effectiveSystemPrompt + memoryContext;
248
+ }
249
+
175
250
  const matchedSkill = this.skillLearner?.matchSkill(message.content);
176
251
  if (matchedSkill) {
177
252
  matchedSkill.usageCount++;
178
253
  matchedSkill.lastUsed = new Date();
179
254
  effectiveSystemPrompt = `[Learned Skill: ${matchedSkill.name}]\n${matchedSkill.instructions}\n\n${this.systemPrompt}`;
180
255
  this.emit('skill:matched', matchedSkill);
256
+ if (this.tracer && rootSpan) {
257
+ this.tracer.addEvent(rootSpan, 'skill.matched', { 'skill.name': matchedSkill.name });
258
+ }
181
259
  }
182
260
 
183
261
  // Fall back to LLM with tool use loop
@@ -186,12 +264,26 @@ export class BaseAgent extends EventEmitter implements IAgent {
186
264
  let finalResponse = '';
187
265
 
188
266
  for (let round = 0; round <= this.maxToolRounds; round++) {
267
+ let llmSpan: TelemetrySpan | undefined;
268
+ if (this.tracer && rootSpan) {
269
+ llmSpan = this.tracer.startSpan('llm.chat', {
270
+ parent: rootSpan,
271
+ kind: 'client',
272
+ attributes: { 'llm.round': round },
273
+ });
274
+ }
275
+
189
276
  const llmResponse = await this._provider.chat(
190
277
  llmMessages,
191
278
  effectiveSystemPrompt,
192
279
  { tools: tools.length > 0 ? tools : undefined },
193
280
  );
194
281
 
282
+ if (this.tracer && llmSpan) {
283
+ llmSpan.attributes['llm.response.length'] = llmResponse.length;
284
+ this.tracer.endSpan(llmSpan, 'ok');
285
+ }
286
+
195
287
  const toolCall = this.parseToolCall(llmResponse);
196
288
  if (!toolCall || tools.length === 0 || round === this.maxToolRounds) {
197
289
  finalResponse = llmResponse;
@@ -199,9 +291,23 @@ export class BaseAgent extends EventEmitter implements IAgent {
199
291
  }
200
292
 
201
293
  // Execute tool
294
+ let toolSpan: TelemetrySpan | undefined;
295
+ if (this.tracer && rootSpan) {
296
+ toolSpan = this.tracer.startSpan('tool.execute', {
297
+ parent: rootSpan,
298
+ kind: 'internal',
299
+ attributes: { 'tool.name': toolCall.name },
300
+ });
301
+ }
302
+
202
303
  const toolResult = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
203
304
  this.emit('tool:execute', toolCall.name, toolResult);
204
305
 
306
+ if (this.tracer && toolSpan) {
307
+ toolSpan.attributes['tool.result.length'] = toolResult.content?.length || 0;
308
+ this.tracer.endSpan(toolSpan, 'ok');
309
+ }
310
+
205
311
  // Add tool call and result to messages for next round
206
312
  llmMessages.push({
207
313
  id: `tool_call_${Date.now()}`,
@@ -221,6 +327,25 @@ export class BaseAgent extends EventEmitter implements IAgent {
221
327
  await this.memory.addMessage(sessionId, response);
222
328
  this.emit('message:out', response);
223
329
 
330
+ // End root telemetry span
331
+ if (this.tracer && rootSpan) {
332
+ rootSpan.attributes['response.length'] = finalResponse.length;
333
+ this.tracer.endSpan(rootSpan, 'ok');
334
+ this.tracer.histogram('agent.message.duration', rootSpan.endTime! - rootSpan.startTime, { agent: this.name });
335
+ }
336
+
337
+ // === Learn from interaction ===
338
+ if (this.longTermMemory && this.longTermMemoryConfig.autoLearn) {
339
+ try {
340
+ await this.longTermMemory.learn(
341
+ `User: ${message.content}\nAssistant: ${finalResponse}`,
342
+ { tags: ['conversation', (message.metadata?.channel as string) || 'unknown'] },
343
+ );
344
+ } catch {
345
+ // Silent fail
346
+ }
347
+ }
348
+
224
349
  // After response, check if we should learn a skill
225
350
  if (
226
351
  this.skillLearner &&
@@ -1,9 +1,17 @@
1
+ import { PluginManager } from '../plugins';
2
+ import type { Plugin } from '../plugins';
3
+ import { loggerPlugin } from '../plugins/logger';
4
+ import { createRateLimiterPlugin } from '../plugins/rate-limiter';
5
+ import { createContentFilterPlugin } from '../plugins/content-filter';
1
6
  import { BaseAgent } from './agent';
2
7
  import { loadOAD } from './config';
3
8
  import { Logger } from './logger';
4
9
  import { WebChannel } from '../channels/web';
5
10
  import { TelegramChannel } from '../channels/telegram';
6
11
  import { WebSocketChannel } from '../channels/websocket';
12
+ import { WeChatChannel } from '../channels/wechat';
13
+ import { FeishuChannel } from '../channels/feishu';
14
+ import { EmailChannel } from '../channels/email';
7
15
  import { DeepBrainMemoryStore } from '../memory/deepbrain';
8
16
  import { Analytics } from '../analytics';
9
17
  import type { OADDocument } from '../schema/oad';
@@ -30,6 +38,10 @@ export class AgentRuntime {
30
38
  private isShuttingDown = false;
31
39
  private analytics: Analytics = new Analytics();
32
40
  private scheduler: Scheduler | null = null;
41
+ private pluginManager: PluginManager = new PluginManager();
42
+ private brain: any = null;
43
+ private agentBrain: any = null;
44
+ private evolveScheduler: any = null;
33
45
 
34
46
  async loadConfig(filePath: string): Promise<OADDocument> {
35
47
  this.config = loadOAD(filePath);
@@ -108,11 +120,93 @@ export class AgentRuntime {
108
120
  } else if (ch.type === 'websocket') {
109
121
  this.agent.bindChannel(new WebSocketChannel(ch.port ?? 3002));
110
122
  this.logger.info('Bound websocket channel', { port: ch.port ?? 3002 });
123
+ } else if (ch.type === 'wechat') {
124
+ this.agent.bindChannel(new WeChatChannel({
125
+ appId: (ch.config?.appId as string) ?? process.env.WECHAT_APP_ID ?? '',
126
+ appSecret: (ch.config?.appSecret as string) ?? process.env.WECHAT_APP_SECRET ?? '',
127
+ token: (ch.config?.token as string) ?? process.env.WECHAT_TOKEN ?? '',
128
+ encodingAESKey: ch.config?.encodingAESKey as string,
129
+ port: ch.port,
130
+ }));
131
+ this.logger.info('Bound wechat channel', { port: ch.port ?? 8080 });
132
+ } else if (ch.type === 'feishu') {
133
+ this.agent.bindChannel(new FeishuChannel({
134
+ appId: (ch.config?.appId as string) ?? process.env.FEISHU_APP_ID,
135
+ appSecret: (ch.config?.appSecret as string) ?? process.env.FEISHU_APP_SECRET,
136
+ verificationToken: (ch.config?.verificationToken as string) ?? process.env.FEISHU_VERIFICATION_TOKEN,
137
+ encryptKey: ch.config?.encryptKey as string,
138
+ port: ch.port,
139
+ }));
140
+ this.logger.info('Bound feishu channel', { port: ch.port ?? 8081 });
141
+ } else if (ch.type === 'email') {
142
+ this.agent.bindChannel(new EmailChannel({
143
+ mode: (ch.config?.mode as 'webhook' | 'imap') ?? 'webhook',
144
+ smtp: ch.config?.smtp as any,
145
+ imap: ch.config?.imap as any,
146
+ webhookPort: ch.port,
147
+ filters: ch.config?.filters as any,
148
+ }));
149
+ this.logger.info('Bound email channel', { mode: ch.config?.mode ?? 'webhook', port: ch.port ?? 8082 });
111
150
  }
112
151
  }
113
152
 
114
153
  await this.agent.init();
115
154
 
155
+ // === Auto-wire DeepBrain long-term memory (Brain/AgentBrain) ===
156
+ const longTermCfg = memCfg && typeof memCfg.longTerm === 'object' ? memCfg.longTerm : null;
157
+ if (longTermCfg?.provider === 'deepbrain') {
158
+ try {
159
+ const deepbrainModule = await import(/* webpackIgnore: true */ 'deepbrain');
160
+ const BrainClass = deepbrainModule.Brain ?? deepbrainModule.default?.Brain;
161
+ const AgentBrainClass = deepbrainModule.AgentBrain ?? deepbrainModule.default?.AgentBrain;
162
+
163
+ if (BrainClass && AgentBrainClass) {
164
+ const dbConfig = longTermCfg.config ?? {};
165
+ const dbPath = (dbConfig as any).database || './data/brain.db';
166
+ const embeddingProvider = (dbConfig as any).embeddingProvider || 'ollama';
167
+
168
+ this.brain = new BrainClass({
169
+ database: dbPath,
170
+ embedding_provider: embeddingProvider,
171
+ });
172
+ await this.brain.connect();
173
+
174
+ this.agentBrain = new AgentBrainClass(this.brain, cfg.metadata.name);
175
+ this.agent.setLongTermMemory(this.agentBrain, {
176
+ autoLearn: (dbConfig as any).autoLearn !== false,
177
+ autoRecall: (dbConfig as any).autoRecall !== false,
178
+ });
179
+
180
+ this.logger.info('DeepBrain Brain/AgentBrain connected', { database: dbPath });
181
+
182
+ // Brain seed loading
183
+ const { existsSync, readFileSync, renameSync } = await import('fs');
184
+ const seedPath = './data/brain-seed.md';
185
+ if (existsSync(seedPath)) {
186
+ const seed = readFileSync(seedPath, 'utf-8');
187
+ await this.brain.put('brain-seed', seed, { type: 'seed', tags: ['seed', 'initial'] });
188
+ renameSync(seedPath, './data/brain-seed.loaded.md');
189
+ this.logger.info('Brain seed loaded');
190
+ }
191
+
192
+ // Auto-evolve scheduling
193
+ const evolveInterval = (dbConfig as any).evolveInterval;
194
+ if (evolveInterval && evolveInterval > 0) {
195
+ const AutoEvolveSchedulerClass = deepbrainModule.AutoEvolveScheduler ?? deepbrainModule.default?.AutoEvolveScheduler;
196
+ if (AutoEvolveSchedulerClass) {
197
+ this.evolveScheduler = new AutoEvolveSchedulerClass();
198
+ this.evolveScheduler.start(this.agentBrain, evolveInterval);
199
+ this.logger.info('DeepBrain auto-evolve scheduled', { interval: evolveInterval });
200
+ }
201
+ }
202
+ } else {
203
+ this.logger.warn('DeepBrain module found but Brain/AgentBrain classes not available');
204
+ }
205
+ } catch (e: any) {
206
+ this.logger.warn('DeepBrain not available (install with: npm install deepbrain)', { error: e.message });
207
+ }
208
+ }
209
+
116
210
  // Wire analytics to agent events
117
211
  this.agent.on('message:out', () => {
118
212
  // responseTime is approximated; real timing is done via skill/llm events
@@ -126,6 +220,24 @@ export class AgentRuntime {
126
220
 
127
221
  this.logger.info('Agent initialized', { name: cfg.metadata.name });
128
222
 
223
+ // Load enhanced plugins from OAD config
224
+ const pluginsCfg = (cfg.spec as any).plugins;
225
+ if (pluginsCfg && Array.isArray(pluginsCfg)) {
226
+ const builtinPlugins: Record<string, (config?: any) => Plugin> = {
227
+ 'logger': () => loggerPlugin,
228
+ 'rate-limiter': (c: any) => createRateLimiterPlugin(c?.maxPerMinute ?? 60),
229
+ 'content-filter': (c: any) => createContentFilterPlugin(c?.blocklist ?? []),
230
+ };
231
+ for (const entry of pluginsCfg) {
232
+ const factory = builtinPlugins[entry.name];
233
+ if (factory) {
234
+ this.pluginManager.registerEnhanced(factory(entry.config));
235
+ this.logger.info('Enhanced plugin loaded from config', { name: entry.name });
236
+ }
237
+ }
238
+ }
239
+ await this.pluginManager.initAll(this);
240
+
129
241
  // Initialize scheduler if jobs are configured
130
242
  const schedulerCfg = (cfg.spec as any).scheduler;
131
243
  if (schedulerCfg?.jobs && Array.isArray(schedulerCfg.jobs) && schedulerCfg.jobs.length > 0) {
@@ -178,10 +290,21 @@ export class AgentRuntime {
178
290
  async stop(): Promise<void> {
179
291
  if (!this.agent) return;
180
292
  this.logger.info('Stopping agent...');
293
+ if (this.evolveScheduler) {
294
+ try { this.evolveScheduler.stop(); } catch { /* ignore */ }
295
+ this.logger.info('DeepBrain auto-evolve stopped');
296
+ }
297
+ if (this.brain) {
298
+ try {
299
+ await this.brain.disconnect();
300
+ this.logger.info('DeepBrain disconnected');
301
+ } catch { /* ignore */ }
302
+ }
181
303
  if (this.scheduler) {
182
304
  this.scheduler.stop();
183
305
  this.logger.info('Scheduler stopped');
184
306
  }
307
+ await this.pluginManager.shutdownAll();
185
308
  await this.agent.stop();
186
309
  for (const handler of this.shutdownHandlers) {
187
310
  await handler();
@@ -227,4 +350,8 @@ export class AgentRuntime {
227
350
  getConfig(): OADDocument | null {
228
351
  return this.config;
229
352
  }
353
+
354
+ getPluginManager(): PluginManager {
355
+ return this.pluginManager;
356
+ }
230
357
  }
@@ -1,11 +1,30 @@
1
1
  import type { TrustLevelType } from '../schema/oad';
2
2
  import * as path from 'path';
3
+ import * as fs from 'fs';
3
4
 
4
5
  export interface SandboxConfig {
5
6
  trustLevel: TrustLevelType;
6
7
  agentDir: string;
7
8
  networkAllowlist?: string[];
8
9
  shellAllowed?: boolean;
10
+ allowedCommands?: string[];
11
+ blockedCommands?: string[];
12
+ maxFileSize?: number; // bytes, default 10MB
13
+ maxFiles?: number; // max files in workspace, default 1000
14
+ networkAccess?: boolean; // allow network, default true
15
+ readOnlyPaths?: string[]; // paths that can't be written
16
+ timeout?: number; // global timeout ms
17
+ }
18
+
19
+ export interface ValidationResult {
20
+ allowed: boolean;
21
+ reason?: string;
22
+ }
23
+
24
+ export interface SandboxStatus {
25
+ files: number;
26
+ totalSize: number;
27
+ violations: number;
9
28
  }
10
29
 
11
30
  export interface SandboxRestrictions {
@@ -40,6 +59,9 @@ const TRUST_RESTRICTIONS: Record<string, SandboxRestrictions> = {
40
59
  export class Sandbox {
41
60
  private config: SandboxConfig;
42
61
  private restrictions: SandboxRestrictions;
62
+ private violations: number = 0;
63
+ private maxFileSize: number;
64
+ private maxFiles: number;
43
65
 
44
66
  constructor(config: SandboxConfig) {
45
67
  this.config = config;
@@ -52,6 +74,11 @@ export class Sandbox {
52
74
  if (config.shellAllowed !== undefined) {
53
75
  this.restrictions.shell = config.shellAllowed;
54
76
  }
77
+ if (config.networkAccess === false) {
78
+ this.restrictions.network.allowed = [];
79
+ }
80
+ this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024; // 10MB
81
+ this.maxFiles = config.maxFiles ?? 1000;
55
82
  }
56
83
 
57
84
  get trustLevel(): TrustLevelType {
@@ -98,4 +125,120 @@ export class Sandbox {
98
125
  checkShellAccess(): boolean {
99
126
  return this.restrictions.shell;
100
127
  }
128
+
129
+ validateFileOp(action: 'read' | 'write' | 'delete', filePath: string): ValidationResult {
130
+ const resolved = path.resolve(filePath);
131
+
132
+ if (action === 'write' || action === 'delete') {
133
+ // Check read-only paths
134
+ if (this.config.readOnlyPaths) {
135
+ for (const ro of this.config.readOnlyPaths) {
136
+ const roResolved = path.resolve(ro);
137
+ if (resolved.startsWith(roResolved) || resolved === roResolved) {
138
+ this.violations++;
139
+ return { allowed: false, reason: `Path is read-only: ${ro}` };
140
+ }
141
+ }
142
+ }
143
+
144
+ // Check file size for writes
145
+ if (action === 'write') {
146
+ try {
147
+ if (fs.existsSync(resolved)) {
148
+ const stat = fs.statSync(resolved);
149
+ if (stat.size > this.maxFileSize) {
150
+ this.violations++;
151
+ return { allowed: false, reason: `File exceeds max size: ${this.maxFileSize} bytes` };
152
+ }
153
+ }
154
+ } catch {
155
+ // File doesn't exist yet — that's fine
156
+ }
157
+ }
158
+ }
159
+
160
+ const mode = action === 'read' ? 'read' : 'write';
161
+ if (!this.checkFileAccess(filePath, mode)) {
162
+ this.violations++;
163
+ return { allowed: false, reason: `File access denied for ${action}: ${filePath}` };
164
+ }
165
+
166
+ return { allowed: true };
167
+ }
168
+
169
+ validateCommand(command: string): ValidationResult {
170
+ if (!this.restrictions.shell) {
171
+ this.violations++;
172
+ return { allowed: false, reason: 'Shell access is disabled' };
173
+ }
174
+
175
+ // Check blocklist
176
+ if (this.config.blockedCommands) {
177
+ for (const blocked of this.config.blockedCommands) {
178
+ if (command.includes(blocked)) {
179
+ this.violations++;
180
+ return { allowed: false, reason: `Command is blocked: ${blocked}` };
181
+ }
182
+ }
183
+ }
184
+
185
+ // Check allowlist (if set, only allowed commands pass)
186
+ if (this.config.allowedCommands && this.config.allowedCommands.length > 0) {
187
+ const allowed = this.config.allowedCommands.some(a => command.startsWith(a) || command.includes(a));
188
+ if (!allowed) {
189
+ this.violations++;
190
+ return { allowed: false, reason: 'Command not in allowlist' };
191
+ }
192
+ }
193
+
194
+ return { allowed: true };
195
+ }
196
+
197
+ validateNetwork(url: string): ValidationResult {
198
+ if (this.config.networkAccess === false) {
199
+ this.violations++;
200
+ return { allowed: false, reason: 'Network access is disabled' };
201
+ }
202
+ if (!this.checkNetworkAccess(url)) {
203
+ this.violations++;
204
+ return { allowed: false, reason: `Network access denied for: ${url}` };
205
+ }
206
+ return { allowed: true };
207
+ }
208
+
209
+ getStatus(): SandboxStatus {
210
+ let files = 0;
211
+ let totalSize = 0;
212
+ try {
213
+ const agentDir = path.resolve(this.config.agentDir);
214
+ if (fs.existsSync(agentDir)) {
215
+ const countFiles = (dir: string) => {
216
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
217
+ for (const entry of entries) {
218
+ const full = path.join(dir, entry.name);
219
+ if (entry.isDirectory() && entry.name !== 'node_modules') {
220
+ countFiles(full);
221
+ } else if (entry.isFile()) {
222
+ files++;
223
+ try { totalSize += fs.statSync(full).size; } catch {}
224
+ }
225
+ }
226
+ };
227
+ countFiles(agentDir);
228
+ }
229
+ } catch {}
230
+ return { files, totalSize, violations: this.violations };
231
+ }
232
+
233
+ getViolations(): number {
234
+ return this.violations;
235
+ }
236
+
237
+ getMaxFileSize(): number {
238
+ return this.maxFileSize;
239
+ }
240
+
241
+ getMaxFiles(): number {
242
+ return this.maxFiles;
243
+ }
101
244
  }