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.
- package/CHANGELOG.md +25 -0
- package/README.md +91 -32
- package/dist/channels/email.d.ts +32 -26
- package/dist/channels/email.js +239 -62
- package/dist/channels/feishu.d.ts +21 -6
- package/dist/channels/feishu.js +225 -126
- package/dist/channels/telegram.d.ts +30 -9
- package/dist/channels/telegram.js +125 -33
- package/dist/channels/websocket.d.ts +46 -3
- package/dist/channels/websocket.js +306 -37
- package/dist/channels/wechat.d.ts +33 -13
- package/dist/channels/wechat.js +229 -42
- package/dist/cli.js +1127 -19
- package/dist/core/a2a.d.ts +17 -0
- package/dist/core/a2a.js +43 -1
- package/dist/core/agent.d.ts +39 -0
- package/dist/core/agent.js +228 -3
- package/dist/core/runtime.d.ts +7 -0
- package/dist/core/runtime.js +205 -2
- package/dist/core/sandbox.d.ts +26 -0
- package/dist/core/sandbox.js +117 -0
- package/dist/core/scheduler.d.ts +52 -0
- package/dist/core/scheduler.js +168 -0
- package/dist/core/subagent.d.ts +28 -0
- package/dist/core/subagent.js +65 -0
- package/dist/core/workflow-graph.d.ts +93 -0
- package/dist/core/workflow-graph.js +247 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +134 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +183 -0
- package/dist/eval/index.d.ts +65 -0
- package/dist/eval/index.js +191 -0
- package/dist/index.d.ts +37 -6
- package/dist/index.js +75 -3
- package/dist/plugins/content-filter.d.ts +7 -0
- package/dist/plugins/content-filter.js +25 -0
- package/dist/plugins/index.d.ts +42 -0
- package/dist/plugins/index.js +108 -2
- package/dist/plugins/logger.d.ts +6 -0
- package/dist/plugins/logger.js +20 -0
- package/dist/plugins/rate-limiter.d.ts +7 -0
- package/dist/plugins/rate-limiter.js +35 -0
- package/dist/protocols/a2a/client.d.ts +25 -0
- package/dist/protocols/a2a/client.js +115 -0
- package/dist/protocols/a2a/index.d.ts +6 -0
- package/dist/protocols/a2a/index.js +12 -0
- package/dist/protocols/a2a/server.d.ts +41 -0
- package/dist/protocols/a2a/server.js +295 -0
- package/dist/protocols/a2a/types.d.ts +91 -0
- package/dist/protocols/a2a/types.js +15 -0
- package/dist/protocols/a2a/utils.d.ts +6 -0
- package/dist/protocols/a2a/utils.js +47 -0
- package/dist/protocols/agui/client.d.ts +10 -0
- package/dist/protocols/agui/client.js +75 -0
- package/dist/protocols/agui/index.d.ts +4 -0
- package/dist/protocols/agui/index.js +25 -0
- package/dist/protocols/agui/server.d.ts +37 -0
- package/dist/protocols/agui/server.js +191 -0
- package/dist/protocols/agui/types.d.ts +107 -0
- package/dist/protocols/agui/types.js +17 -0
- package/dist/protocols/index.d.ts +2 -0
- package/dist/protocols/index.js +19 -0
- package/dist/protocols/mcp/agent-tools.d.ts +11 -0
- package/dist/protocols/mcp/agent-tools.js +129 -0
- package/dist/protocols/mcp/index.d.ts +5 -0
- package/dist/protocols/mcp/index.js +11 -0
- package/dist/protocols/mcp/server.d.ts +31 -0
- package/dist/protocols/mcp/server.js +248 -0
- package/dist/protocols/mcp/types.d.ts +92 -0
- package/dist/protocols/mcp/types.js +17 -0
- package/dist/providers/index.d.ts +5 -1
- package/dist/providers/index.js +16 -9
- package/dist/publish/index.d.ts +45 -0
- package/dist/publish/index.js +350 -0
- package/dist/schema/oad.d.ts +859 -67
- package/dist/schema/oad.js +47 -3
- package/dist/security/approval.d.ts +36 -0
- package/dist/security/approval.js +113 -0
- package/dist/security/index.d.ts +4 -0
- package/dist/security/index.js +8 -0
- package/dist/security/keys.d.ts +16 -0
- package/dist/security/keys.js +117 -0
- package/dist/skills/auto-learn.d.ts +28 -0
- package/dist/skills/auto-learn.js +257 -0
- package/dist/studio/server.d.ts +63 -0
- package/dist/studio/server.js +625 -0
- package/dist/studio-ui/index.html +662 -0
- package/dist/telemetry/index.d.ts +93 -0
- package/dist/telemetry/index.js +285 -0
- package/dist/tools/builtin/datetime.d.ts +3 -0
- package/dist/tools/builtin/datetime.js +44 -0
- package/dist/tools/builtin/file.d.ts +3 -0
- package/dist/tools/builtin/file.js +151 -0
- package/dist/tools/builtin/index.d.ts +15 -0
- package/dist/tools/builtin/index.js +30 -0
- package/dist/tools/builtin/shell.d.ts +3 -0
- package/dist/tools/builtin/shell.js +43 -0
- package/dist/tools/builtin/web.d.ts +3 -0
- package/dist/tools/builtin/web.js +37 -0
- package/dist/tools/mcp-client.d.ts +24 -0
- package/dist/tools/mcp-client.js +119 -0
- package/package.json +5 -3
- package/scripts/install.ps1 +31 -0
- package/scripts/install.sh +40 -0
- package/src/channels/email.ts +351 -177
- package/src/channels/feishu.ts +349 -236
- package/src/channels/telegram.ts +212 -90
- package/src/channels/websocket.ts +399 -87
- package/src/channels/wechat.ts +329 -149
- package/src/cli.ts +1201 -20
- package/src/core/a2a.ts +60 -0
- package/src/core/agent.ts +420 -152
- package/src/core/runtime.ts +174 -0
- package/src/core/sandbox.ts +143 -0
- package/src/core/scheduler.ts +187 -0
- package/src/core/subagent.ts +98 -0
- package/src/core/workflow-graph.ts +365 -0
- package/src/daemon.ts +96 -0
- package/src/doctor.ts +156 -0
- package/src/eval/index.ts +211 -0
- package/src/eval/suites/basic.json +16 -0
- package/src/eval/suites/memory.json +12 -0
- package/src/eval/suites/safety.json +14 -0
- package/src/index.ts +65 -6
- package/src/plugins/content-filter.ts +23 -0
- package/src/plugins/index.ts +133 -2
- package/src/plugins/logger.ts +18 -0
- package/src/plugins/rate-limiter.ts +38 -0
- package/src/protocols/a2a/client.ts +132 -0
- package/src/protocols/a2a/index.ts +8 -0
- package/src/protocols/a2a/server.ts +333 -0
- package/src/protocols/a2a/types.ts +88 -0
- package/src/protocols/a2a/utils.ts +50 -0
- package/src/protocols/agui/client.ts +83 -0
- package/src/protocols/agui/index.ts +4 -0
- package/src/protocols/agui/server.ts +218 -0
- package/src/protocols/agui/types.ts +153 -0
- package/src/protocols/index.ts +2 -0
- package/src/protocols/mcp/agent-tools.ts +134 -0
- package/src/protocols/mcp/index.ts +8 -0
- package/src/protocols/mcp/server.ts +262 -0
- package/src/protocols/mcp/types.ts +69 -0
- package/src/providers/index.ts +354 -339
- package/src/publish/index.ts +376 -0
- package/src/schema/oad.ts +204 -154
- package/src/security/approval.ts +131 -0
- package/src/security/index.ts +3 -0
- package/src/security/keys.ts +87 -0
- package/src/skills/auto-learn.ts +262 -0
- package/src/studio/server.ts +629 -0
- package/src/studio-ui/index.html +662 -0
- package/src/telemetry/index.ts +324 -0
- package/src/tools/builtin/datetime.ts +41 -0
- package/src/tools/builtin/file.ts +107 -0
- package/src/tools/builtin/index.ts +28 -0
- package/src/tools/builtin/shell.ts +43 -0
- package/src/tools/builtin/web.ts +35 -0
- package/src/tools/mcp-client.ts +131 -0
- package/src/types/agent-workstation.d.ts +2 -0
- package/tests/a2a-protocol.test.ts +285 -0
- package/tests/agui-protocol.test.ts +246 -0
- package/tests/auto-learn.test.ts +105 -0
- package/tests/builtin-tools.test.ts +83 -0
- package/tests/channels/discord.test.ts +79 -0
- package/tests/channels/email.test.ts +148 -0
- package/tests/channels/feishu.test.ts +123 -0
- package/tests/channels/telegram.test.ts +129 -0
- package/tests/channels/websocket.test.ts +53 -0
- package/tests/channels/wechat.test.ts +170 -0
- package/tests/chat-cli.test.ts +160 -0
- package/tests/cli.test.ts +46 -0
- package/tests/daemon.test.ts +135 -0
- package/tests/deepbrain-wire.test.ts +234 -0
- package/tests/doctor.test.ts +38 -0
- package/tests/eval.test.ts +173 -0
- package/tests/init-role.test.ts +124 -0
- package/tests/mcp-client.test.ts +92 -0
- package/tests/mcp-server.test.ts +178 -0
- package/tests/plugin-a2a-enhanced.test.ts +230 -0
- package/tests/publish.test.ts +231 -0
- package/tests/scheduler.test.ts +200 -0
- package/tests/security-enhanced.test.ts +233 -0
- package/tests/skill-learner.test.ts +161 -0
- package/tests/studio.test.ts +229 -0
- package/tests/subagent.test.ts +193 -0
- package/tests/telegram-discord.test.ts +60 -0
- package/tests/telemetry.test.ts +186 -0
- package/tests/tools/builtin-extended.test.ts +138 -0
- package/tests/workflow-graph.test.ts +279 -0
- package/tutorial/customer-service-agent/README.md +612 -0
- package/tutorial/customer-service-agent/SOUL.md +26 -0
- package/tutorial/customer-service-agent/agent.yaml +63 -0
- package/tutorial/customer-service-agent/package.json +19 -0
- package/tutorial/customer-service-agent/src/index.ts +69 -0
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -0
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -0
- package/tutorial/customer-service-agent/tsconfig.json +14 -0
package/src/core/runtime.ts
CHANGED
|
@@ -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
|
}
|
package/src/core/sandbox.ts
CHANGED
|
@@ -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
|
+
}
|