opc-agent 1.4.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 (198) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +91 -32
  3. package/dist/channels/email.d.ts +32 -26
  4. package/dist/channels/email.js +239 -62
  5. package/dist/channels/feishu.d.ts +21 -6
  6. package/dist/channels/feishu.js +225 -126
  7. package/dist/channels/telegram.d.ts +30 -9
  8. package/dist/channels/telegram.js +125 -33
  9. package/dist/channels/websocket.d.ts +46 -3
  10. package/dist/channels/websocket.js +306 -37
  11. package/dist/channels/wechat.d.ts +33 -13
  12. package/dist/channels/wechat.js +229 -42
  13. package/dist/cli.js +1127 -19
  14. package/dist/core/a2a.d.ts +17 -0
  15. package/dist/core/a2a.js +43 -1
  16. package/dist/core/agent.d.ts +39 -0
  17. package/dist/core/agent.js +228 -3
  18. package/dist/core/runtime.d.ts +7 -0
  19. package/dist/core/runtime.js +205 -2
  20. package/dist/core/sandbox.d.ts +26 -0
  21. package/dist/core/sandbox.js +117 -0
  22. package/dist/core/scheduler.d.ts +52 -0
  23. package/dist/core/scheduler.js +168 -0
  24. package/dist/core/subagent.d.ts +28 -0
  25. package/dist/core/subagent.js +65 -0
  26. package/dist/core/workflow-graph.d.ts +93 -0
  27. package/dist/core/workflow-graph.js +247 -0
  28. package/dist/daemon.d.ts +3 -0
  29. package/dist/daemon.js +134 -0
  30. package/dist/doctor.d.ts +15 -0
  31. package/dist/doctor.js +183 -0
  32. package/dist/eval/index.d.ts +65 -0
  33. package/dist/eval/index.js +191 -0
  34. package/dist/index.d.ts +37 -6
  35. package/dist/index.js +75 -3
  36. package/dist/plugins/content-filter.d.ts +7 -0
  37. package/dist/plugins/content-filter.js +25 -0
  38. package/dist/plugins/index.d.ts +42 -0
  39. package/dist/plugins/index.js +108 -2
  40. package/dist/plugins/logger.d.ts +6 -0
  41. package/dist/plugins/logger.js +20 -0
  42. package/dist/plugins/rate-limiter.d.ts +7 -0
  43. package/dist/plugins/rate-limiter.js +35 -0
  44. package/dist/protocols/a2a/client.d.ts +25 -0
  45. package/dist/protocols/a2a/client.js +115 -0
  46. package/dist/protocols/a2a/index.d.ts +6 -0
  47. package/dist/protocols/a2a/index.js +12 -0
  48. package/dist/protocols/a2a/server.d.ts +41 -0
  49. package/dist/protocols/a2a/server.js +295 -0
  50. package/dist/protocols/a2a/types.d.ts +91 -0
  51. package/dist/protocols/a2a/types.js +15 -0
  52. package/dist/protocols/a2a/utils.d.ts +6 -0
  53. package/dist/protocols/a2a/utils.js +47 -0
  54. package/dist/protocols/agui/client.d.ts +10 -0
  55. package/dist/protocols/agui/client.js +75 -0
  56. package/dist/protocols/agui/index.d.ts +4 -0
  57. package/dist/protocols/agui/index.js +25 -0
  58. package/dist/protocols/agui/server.d.ts +37 -0
  59. package/dist/protocols/agui/server.js +191 -0
  60. package/dist/protocols/agui/types.d.ts +107 -0
  61. package/dist/protocols/agui/types.js +17 -0
  62. package/dist/protocols/index.d.ts +2 -0
  63. package/dist/protocols/index.js +19 -0
  64. package/dist/protocols/mcp/agent-tools.d.ts +11 -0
  65. package/dist/protocols/mcp/agent-tools.js +129 -0
  66. package/dist/protocols/mcp/index.d.ts +5 -0
  67. package/dist/protocols/mcp/index.js +11 -0
  68. package/dist/protocols/mcp/server.d.ts +31 -0
  69. package/dist/protocols/mcp/server.js +248 -0
  70. package/dist/protocols/mcp/types.d.ts +92 -0
  71. package/dist/protocols/mcp/types.js +17 -0
  72. package/dist/providers/index.d.ts +5 -1
  73. package/dist/providers/index.js +16 -9
  74. package/dist/publish/index.d.ts +45 -0
  75. package/dist/publish/index.js +350 -0
  76. package/dist/schema/oad.d.ts +859 -67
  77. package/dist/schema/oad.js +47 -3
  78. package/dist/security/approval.d.ts +36 -0
  79. package/dist/security/approval.js +113 -0
  80. package/dist/security/index.d.ts +4 -0
  81. package/dist/security/index.js +8 -0
  82. package/dist/security/keys.d.ts +16 -0
  83. package/dist/security/keys.js +117 -0
  84. package/dist/skills/auto-learn.d.ts +28 -0
  85. package/dist/skills/auto-learn.js +257 -0
  86. package/dist/studio/server.d.ts +63 -0
  87. package/dist/studio/server.js +625 -0
  88. package/dist/studio-ui/index.html +662 -0
  89. package/dist/telemetry/index.d.ts +93 -0
  90. package/dist/telemetry/index.js +285 -0
  91. package/dist/tools/builtin/datetime.d.ts +3 -0
  92. package/dist/tools/builtin/datetime.js +44 -0
  93. package/dist/tools/builtin/file.d.ts +3 -0
  94. package/dist/tools/builtin/file.js +151 -0
  95. package/dist/tools/builtin/index.d.ts +15 -0
  96. package/dist/tools/builtin/index.js +30 -0
  97. package/dist/tools/builtin/shell.d.ts +3 -0
  98. package/dist/tools/builtin/shell.js +43 -0
  99. package/dist/tools/builtin/web.d.ts +3 -0
  100. package/dist/tools/builtin/web.js +37 -0
  101. package/dist/tools/mcp-client.d.ts +24 -0
  102. package/dist/tools/mcp-client.js +119 -0
  103. package/package.json +5 -3
  104. package/scripts/install.ps1 +31 -0
  105. package/scripts/install.sh +40 -0
  106. package/src/channels/email.ts +351 -177
  107. package/src/channels/feishu.ts +349 -236
  108. package/src/channels/telegram.ts +212 -90
  109. package/src/channels/websocket.ts +399 -87
  110. package/src/channels/wechat.ts +329 -149
  111. package/src/cli.ts +1201 -20
  112. package/src/core/a2a.ts +60 -0
  113. package/src/core/agent.ts +420 -152
  114. package/src/core/runtime.ts +174 -0
  115. package/src/core/sandbox.ts +143 -0
  116. package/src/core/scheduler.ts +187 -0
  117. package/src/core/subagent.ts +98 -0
  118. package/src/core/workflow-graph.ts +365 -0
  119. package/src/daemon.ts +96 -0
  120. package/src/doctor.ts +156 -0
  121. package/src/eval/index.ts +211 -0
  122. package/src/eval/suites/basic.json +16 -0
  123. package/src/eval/suites/memory.json +12 -0
  124. package/src/eval/suites/safety.json +14 -0
  125. package/src/index.ts +65 -6
  126. package/src/plugins/content-filter.ts +23 -0
  127. package/src/plugins/index.ts +133 -2
  128. package/src/plugins/logger.ts +18 -0
  129. package/src/plugins/rate-limiter.ts +38 -0
  130. package/src/protocols/a2a/client.ts +132 -0
  131. package/src/protocols/a2a/index.ts +8 -0
  132. package/src/protocols/a2a/server.ts +333 -0
  133. package/src/protocols/a2a/types.ts +88 -0
  134. package/src/protocols/a2a/utils.ts +50 -0
  135. package/src/protocols/agui/client.ts +83 -0
  136. package/src/protocols/agui/index.ts +4 -0
  137. package/src/protocols/agui/server.ts +218 -0
  138. package/src/protocols/agui/types.ts +153 -0
  139. package/src/protocols/index.ts +2 -0
  140. package/src/protocols/mcp/agent-tools.ts +134 -0
  141. package/src/protocols/mcp/index.ts +8 -0
  142. package/src/protocols/mcp/server.ts +262 -0
  143. package/src/protocols/mcp/types.ts +69 -0
  144. package/src/providers/index.ts +354 -339
  145. package/src/publish/index.ts +376 -0
  146. package/src/schema/oad.ts +204 -154
  147. package/src/security/approval.ts +131 -0
  148. package/src/security/index.ts +3 -0
  149. package/src/security/keys.ts +87 -0
  150. package/src/skills/auto-learn.ts +262 -0
  151. package/src/studio/server.ts +629 -0
  152. package/src/studio-ui/index.html +662 -0
  153. package/src/telemetry/index.ts +324 -0
  154. package/src/tools/builtin/datetime.ts +41 -0
  155. package/src/tools/builtin/file.ts +107 -0
  156. package/src/tools/builtin/index.ts +28 -0
  157. package/src/tools/builtin/shell.ts +43 -0
  158. package/src/tools/builtin/web.ts +35 -0
  159. package/src/tools/mcp-client.ts +131 -0
  160. package/src/types/agent-workstation.d.ts +2 -0
  161. package/tests/a2a-protocol.test.ts +285 -0
  162. package/tests/agui-protocol.test.ts +246 -0
  163. package/tests/auto-learn.test.ts +105 -0
  164. package/tests/builtin-tools.test.ts +83 -0
  165. package/tests/channels/discord.test.ts +79 -0
  166. package/tests/channels/email.test.ts +148 -0
  167. package/tests/channels/feishu.test.ts +123 -0
  168. package/tests/channels/telegram.test.ts +129 -0
  169. package/tests/channels/websocket.test.ts +53 -0
  170. package/tests/channels/wechat.test.ts +170 -0
  171. package/tests/chat-cli.test.ts +160 -0
  172. package/tests/cli.test.ts +46 -0
  173. package/tests/daemon.test.ts +135 -0
  174. package/tests/deepbrain-wire.test.ts +234 -0
  175. package/tests/doctor.test.ts +38 -0
  176. package/tests/eval.test.ts +173 -0
  177. package/tests/init-role.test.ts +124 -0
  178. package/tests/mcp-client.test.ts +92 -0
  179. package/tests/mcp-server.test.ts +178 -0
  180. package/tests/plugin-a2a-enhanced.test.ts +230 -0
  181. package/tests/publish.test.ts +231 -0
  182. package/tests/scheduler.test.ts +200 -0
  183. package/tests/security-enhanced.test.ts +233 -0
  184. package/tests/skill-learner.test.ts +161 -0
  185. package/tests/studio.test.ts +229 -0
  186. package/tests/subagent.test.ts +193 -0
  187. package/tests/telegram-discord.test.ts +60 -0
  188. package/tests/telemetry.test.ts +186 -0
  189. package/tests/tools/builtin-extended.test.ts +138 -0
  190. package/tests/workflow-graph.test.ts +279 -0
  191. package/tutorial/customer-service-agent/README.md +612 -0
  192. package/tutorial/customer-service-agent/SOUL.md +26 -0
  193. package/tutorial/customer-service-agent/agent.yaml +63 -0
  194. package/tutorial/customer-service-agent/package.json +19 -0
  195. package/tutorial/customer-service-agent/src/index.ts +69 -0
  196. package/tutorial/customer-service-agent/src/skills/faq.ts +27 -0
  197. package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -0
  198. package/tutorial/customer-service-agent/tsconfig.json +14 -0
@@ -1,12 +1,22 @@
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';
18
+ import { Scheduler } from './scheduler';
19
+ import type { CronJob } from './scheduler';
10
20
  import type { ISkill, MemoryStore, Message } from './types';
11
21
  import type { Response } from 'express';
12
22
 
@@ -27,6 +37,11 @@ export class AgentRuntime {
27
37
  private shutdownHandlers: (() => Promise<void>)[] = [];
28
38
  private isShuttingDown = false;
29
39
  private analytics: Analytics = new Analytics();
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;
30
45
 
31
46
  async loadConfig(filePath: string): Promise<OADDocument> {
32
47
  this.config = loadOAD(filePath);
@@ -105,11 +120,93 @@ export class AgentRuntime {
105
120
  } else if (ch.type === 'websocket') {
106
121
  this.agent.bindChannel(new WebSocketChannel(ch.port ?? 3002));
107
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 });
108
150
  }
109
151
  }
110
152
 
111
153
  await this.agent.init();
112
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
+
113
210
  // Wire analytics to agent events
114
211
  this.agent.on('message:out', () => {
115
212
  // responseTime is approximated; real timing is done via skill/llm events
@@ -122,6 +219,60 @@ export class AgentRuntime {
122
219
  });
123
220
 
124
221
  this.logger.info('Agent initialized', { name: cfg.metadata.name });
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
+
241
+ // Initialize scheduler if jobs are configured
242
+ const schedulerCfg = (cfg.spec as any).scheduler;
243
+ if (schedulerCfg?.jobs && Array.isArray(schedulerCfg.jobs) && schedulerCfg.jobs.length > 0) {
244
+ this.scheduler = new Scheduler(async (job: CronJob) => {
245
+ this.logger.info('Scheduler firing job', { name: job.name, task: job.task });
246
+ if (this.agent) {
247
+ const msg: Message = {
248
+ id: `cron-${job.id}-${Date.now()}`,
249
+ role: 'user',
250
+ content: job.task,
251
+ timestamp: Date.now(),
252
+ metadata: { source: 'scheduler', jobId: job.id, jobName: job.name },
253
+ };
254
+ try {
255
+ await this.agent.handleMessage(msg);
256
+ } catch (err) {
257
+ this.logger.error('Scheduler job failed', { name: job.name, error: err instanceof Error ? err.message : String(err) });
258
+ }
259
+ }
260
+ });
261
+
262
+ for (let i = 0; i < schedulerCfg.jobs.length; i++) {
263
+ const j = schedulerCfg.jobs[i];
264
+ const id = j.id || j.name?.toLowerCase().replace(/\s+/g, '-') || `job-${i}`;
265
+ this.scheduler.addJob({
266
+ id,
267
+ name: j.name || id,
268
+ schedule: j.schedule,
269
+ task: j.task || '',
270
+ enabled: j.enabled !== false,
271
+ });
272
+ }
273
+ this.logger.info('Scheduler configured', { jobs: schedulerCfg.jobs.length });
274
+ }
275
+
125
276
  return this.agent;
126
277
  }
127
278
 
@@ -129,12 +280,31 @@ export class AgentRuntime {
129
280
  if (!this.agent) throw new Error('Agent not initialized.');
130
281
  this.setupGracefulShutdown();
131
282
  await this.agent.start();
283
+ if (this.scheduler) {
284
+ this.scheduler.start();
285
+ this.logger.info('Scheduler started');
286
+ }
132
287
  this.logger.info('Agent started');
133
288
  }
134
289
 
135
290
  async stop(): Promise<void> {
136
291
  if (!this.agent) return;
137
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
+ }
303
+ if (this.scheduler) {
304
+ this.scheduler.stop();
305
+ this.logger.info('Scheduler stopped');
306
+ }
307
+ await this.pluginManager.shutdownAll();
138
308
  await this.agent.stop();
139
309
  for (const handler of this.shutdownHandlers) {
140
310
  await handler();
@@ -180,4 +350,8 @@ export class AgentRuntime {
180
350
  getConfig(): OADDocument | null {
181
351
  return this.config;
182
352
  }
353
+
354
+ getPluginManager(): PluginManager {
355
+ return this.pluginManager;
356
+ }
183
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
  }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Simple cron scheduler — no external dependencies.
3
+ * Supports cron expressions: star, star-slash-N, M-N, M,N for minute/hour/day/month/weekday.
4
+ */
5
+
6
+ export interface CronJob {
7
+ id: string;
8
+ name: string;
9
+ schedule: string;
10
+ task: string;
11
+ enabled: boolean;
12
+ lastRun?: Date;
13
+ nextRun?: Date;
14
+ }
15
+
16
+ type CronField = { type: 'any' } | { type: 'every'; step: number } | { type: 'list'; values: number[] };
17
+
18
+ interface ParsedCron {
19
+ minute: CronField;
20
+ hour: CronField;
21
+ dayOfMonth: CronField;
22
+ month: CronField;
23
+ dayOfWeek: CronField;
24
+ }
25
+
26
+ function parseField(field: string, min: number, max: number): CronField {
27
+ if (field === '*') return { type: 'any' };
28
+ if (field.startsWith('*/')) {
29
+ const step = parseInt(field.slice(2), 10);
30
+ if (isNaN(step) || step <= 0) throw new Error(`Invalid cron step: ${field}`);
31
+ return { type: 'every', step };
32
+ }
33
+ // Could be comma-separated, each part could be a range
34
+ const values: number[] = [];
35
+ for (const part of field.split(',')) {
36
+ if (part.includes('-')) {
37
+ const [a, b] = part.split('-').map(Number);
38
+ if (isNaN(a) || isNaN(b)) throw new Error(`Invalid cron range: ${part}`);
39
+ for (let i = a; i <= b; i++) values.push(i);
40
+ } else {
41
+ const n = parseInt(part, 10);
42
+ if (isNaN(n)) throw new Error(`Invalid cron value: ${part}`);
43
+ values.push(n);
44
+ }
45
+ }
46
+ return { type: 'list', values };
47
+ }
48
+
49
+ export function parseCron(expr: string): ParsedCron {
50
+ const parts = expr.trim().split(/\s+/);
51
+ if (parts.length !== 5) throw new Error(`Invalid cron expression (need 5 fields): ${expr}`);
52
+ return {
53
+ minute: parseField(parts[0], 0, 59),
54
+ hour: parseField(parts[1], 0, 23),
55
+ dayOfMonth: parseField(parts[2], 1, 31),
56
+ month: parseField(parts[3], 1, 12),
57
+ dayOfWeek: parseField(parts[4], 0, 6),
58
+ };
59
+ }
60
+
61
+ function fieldMatches(field: CronField, value: number): boolean {
62
+ switch (field.type) {
63
+ case 'any': return true;
64
+ case 'every': return value % field.step === 0;
65
+ case 'list': return field.values.includes(value);
66
+ }
67
+ }
68
+
69
+ export function cronMatches(parsed: ParsedCron, date: Date): boolean {
70
+ return (
71
+ fieldMatches(parsed.minute, date.getMinutes()) &&
72
+ fieldMatches(parsed.hour, date.getHours()) &&
73
+ fieldMatches(parsed.dayOfMonth, date.getDate()) &&
74
+ fieldMatches(parsed.month, date.getMonth() + 1) &&
75
+ fieldMatches(parsed.dayOfWeek, date.getDay())
76
+ );
77
+ }
78
+
79
+ /** Compute approximate next run (scans forward up to 48h). */
80
+ function computeNextRun(parsed: ParsedCron, from: Date): Date | undefined {
81
+ const d = new Date(from);
82
+ d.setSeconds(0, 0);
83
+ d.setMinutes(d.getMinutes() + 1);
84
+ const limit = 48 * 60; // 48 hours in minutes
85
+ for (let i = 0; i < limit; i++) {
86
+ if (cronMatches(parsed, d)) return new Date(d);
87
+ d.setMinutes(d.getMinutes() + 1);
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ export type JobHandler = (job: CronJob) => void | Promise<void>;
93
+
94
+ export class Scheduler {
95
+ private jobs = new Map<string, CronJob>();
96
+ private parsed = new Map<string, ParsedCron>();
97
+ private timer: ReturnType<typeof setInterval> | null = null;
98
+ private handler: JobHandler;
99
+
100
+ constructor(handler: JobHandler) {
101
+ this.handler = handler;
102
+ }
103
+
104
+ addJob(job: CronJob): void {
105
+ const p = parseCron(job.schedule);
106
+ this.parsed.set(job.id, p);
107
+ job.nextRun = computeNextRun(p, new Date()) ?? undefined;
108
+ this.jobs.set(job.id, job);
109
+ }
110
+
111
+ removeJob(id: string): void {
112
+ this.jobs.delete(id);
113
+ this.parsed.delete(id);
114
+ }
115
+
116
+ enableJob(id: string): void {
117
+ const job = this.jobs.get(id);
118
+ if (job) job.enabled = true;
119
+ }
120
+
121
+ disableJob(id: string): void {
122
+ const job = this.jobs.get(id);
123
+ if (job) job.enabled = false;
124
+ }
125
+
126
+ getJobs(): CronJob[] {
127
+ return Array.from(this.jobs.values());
128
+ }
129
+
130
+ getJob(id: string): CronJob | undefined {
131
+ return this.jobs.get(id);
132
+ }
133
+
134
+ /** Run a specific job immediately */
135
+ async runJob(id: string): Promise<boolean> {
136
+ const job = this.jobs.get(id);
137
+ if (!job) return false;
138
+ job.lastRun = new Date();
139
+ await this.handler(job);
140
+ const parsed = this.parsed.get(id);
141
+ if (parsed) job.nextRun = computeNextRun(parsed, new Date());
142
+ return true;
143
+ }
144
+
145
+ start(): void {
146
+ if (this.timer) return;
147
+ // Check every 60 seconds
148
+ this.timer = setInterval(() => this.tick(), 60_000);
149
+ // Also tick immediately
150
+ this.tick();
151
+ }
152
+
153
+ stop(): void {
154
+ if (this.timer) {
155
+ clearInterval(this.timer);
156
+ this.timer = null;
157
+ }
158
+ }
159
+
160
+ private tick(): void {
161
+ const now = new Date();
162
+ for (const [id, job] of this.jobs) {
163
+ if (!job.enabled) continue;
164
+ const parsed = this.parsed.get(id);
165
+ if (!parsed) continue;
166
+ if (cronMatches(parsed, now)) {
167
+ // Avoid double-fire: check lastRun isn't same minute
168
+ if (job.lastRun) {
169
+ const last = job.lastRun;
170
+ if (last.getFullYear() === now.getFullYear() &&
171
+ last.getMonth() === now.getMonth() &&
172
+ last.getDate() === now.getDate() &&
173
+ last.getHours() === now.getHours() &&
174
+ last.getMinutes() === now.getMinutes()) {
175
+ continue;
176
+ }
177
+ }
178
+ job.lastRun = new Date(now);
179
+ job.nextRun = computeNextRun(parsed, now);
180
+ // Fire and forget (log errors)
181
+ Promise.resolve(this.handler(job)).catch((err) => {
182
+ console.error(`[scheduler] Job "${job.name}" failed:`, err);
183
+ });
184
+ }
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,98 @@
1
+ import { BaseAgent } from './agent';
2
+ import { InMemoryStore } from '../memory';
3
+ import type { Message } from './types';
4
+
5
+ export interface SubAgentConfig {
6
+ name: string;
7
+ task: string;
8
+ systemPrompt?: string;
9
+ provider?: string;
10
+ model?: string;
11
+ timeout?: number;
12
+ isolated?: boolean;
13
+ }
14
+
15
+ export interface SubAgentResult {
16
+ id: string;
17
+ name: string;
18
+ status: 'completed' | 'failed' | 'timeout';
19
+ result: string;
20
+ duration: number;
21
+ }
22
+
23
+ interface SubAgentEntry {
24
+ agent: BaseAgent;
25
+ status: string;
26
+ name: string;
27
+ }
28
+
29
+ export class SubAgentManager {
30
+ private agents: Map<string, SubAgentEntry> = new Map();
31
+
32
+ async spawn(config: SubAgentConfig, parentProvider?: any): Promise<SubAgentResult> {
33
+ const id = `sub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
34
+ const timeout = config.timeout ?? 300000;
35
+ const isolated = config.isolated !== false;
36
+
37
+ const agent = new BaseAgent({
38
+ name: config.name,
39
+ systemPrompt: config.systemPrompt ?? 'You are a helpful sub-agent.',
40
+ provider: config.provider ?? 'openai',
41
+ model: config.model,
42
+ memory: isolated ? new InMemoryStore() : undefined,
43
+ });
44
+
45
+ this.agents.set(id, { agent, status: 'running', name: config.name });
46
+
47
+ const message: Message = {
48
+ id: `msg_${Date.now()}`,
49
+ role: 'user',
50
+ content: config.task,
51
+ timestamp: Date.now(),
52
+ metadata: { subAgentId: id },
53
+ };
54
+
55
+ const start = Date.now();
56
+
57
+ try {
58
+ const result = await Promise.race([
59
+ agent.handleMessage(message),
60
+ new Promise<never>((_, reject) =>
61
+ setTimeout(() => reject(new Error('SubAgent timeout')), timeout),
62
+ ),
63
+ ]);
64
+
65
+ const duration = Date.now() - start;
66
+ this.agents.set(id, { agent, status: 'completed', name: config.name });
67
+
68
+ return { id, name: config.name, status: 'completed', result: result.content, duration };
69
+ } catch (err) {
70
+ const duration = Date.now() - start;
71
+ const isTimeout = (err as Error).message.includes('timeout');
72
+ const status = isTimeout ? 'timeout' : 'failed';
73
+ this.agents.set(id, { agent, status, name: config.name });
74
+
75
+ return { id, name: config.name, status, result: (err as Error).message, duration };
76
+ }
77
+ }
78
+
79
+ async spawnParallel(configs: SubAgentConfig[], parentProvider?: any): Promise<SubAgentResult[]> {
80
+ return Promise.all(configs.map((c) => this.spawn(c, parentProvider)));
81
+ }
82
+
83
+ list(): Array<{ id: string; name: string; status: string }> {
84
+ return Array.from(this.agents.entries()).map(([id, entry]) => ({
85
+ id,
86
+ name: entry.name,
87
+ status: entry.status,
88
+ }));
89
+ }
90
+
91
+ kill(id: string): boolean {
92
+ const entry = this.agents.get(id);
93
+ if (!entry) return false;
94
+ entry.status = 'killed';
95
+ this.agents.set(id, entry);
96
+ return true;
97
+ }
98
+ }