dank-ai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1331 -0
- package/bin/dank +118 -0
- package/docker/Dockerfile +57 -0
- package/docker/entrypoint.js +1227 -0
- package/docker/package.json +19 -0
- package/lib/agent.js +644 -0
- package/lib/cli/build.js +43 -0
- package/lib/cli/clean.js +30 -0
- package/lib/cli/init.js +38 -0
- package/lib/cli/logs.js +122 -0
- package/lib/cli/run.js +176 -0
- package/lib/cli/status.js +125 -0
- package/lib/cli/stop.js +87 -0
- package/lib/config.js +180 -0
- package/lib/constants.js +58 -0
- package/lib/docker/manager.js +968 -0
- package/lib/index.js +26 -0
- package/lib/project.js +280 -0
- package/lib/tools/builtin.js +445 -0
- package/lib/tools/index.js +335 -0
- package/package.json +52 -0
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Container Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages Docker containers for Dank agents including:
|
|
5
|
+
* - Building agent images
|
|
6
|
+
* - Starting/stopping containers
|
|
7
|
+
* - Monitoring container health
|
|
8
|
+
* - Managing Docker resources
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const Docker = require('dockerode');
|
|
12
|
+
const fs = require('fs-extra');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const tar = require('tar');
|
|
15
|
+
const winston = require('winston');
|
|
16
|
+
const { spawn, exec } = require('child_process');
|
|
17
|
+
const { promisify } = require('util');
|
|
18
|
+
const { DOCKER_CONFIG } = require('../constants');
|
|
19
|
+
const { AgentConfig } = require('../config');
|
|
20
|
+
|
|
21
|
+
const execAsync = promisify(exec);
|
|
22
|
+
|
|
23
|
+
class DockerManager {
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.docker = new Docker(options.dockerOptions || {});
|
|
26
|
+
this.logger = options.logger || winston.createLogger({
|
|
27
|
+
level: 'info',
|
|
28
|
+
format: winston.format.simple(),
|
|
29
|
+
transports: [new winston.transports.Console()]
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.defaultBaseImageName = `${DOCKER_CONFIG.baseImagePrefix}:${DOCKER_CONFIG.defaultTag}`;
|
|
33
|
+
this.networkName = DOCKER_CONFIG.networkName;
|
|
34
|
+
this.containers = new Map();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initialize Docker environment
|
|
39
|
+
*/
|
|
40
|
+
async initialize() {
|
|
41
|
+
try {
|
|
42
|
+
// Ensure Docker is available and running
|
|
43
|
+
await this.ensureDockerAvailable();
|
|
44
|
+
|
|
45
|
+
// Check Docker connection
|
|
46
|
+
await this.docker.ping();
|
|
47
|
+
this.logger.info('Docker connection established');
|
|
48
|
+
|
|
49
|
+
// Create network if it doesn't exist
|
|
50
|
+
await this.ensureNetwork();
|
|
51
|
+
|
|
52
|
+
// Check if default base image exists, pull if not found
|
|
53
|
+
const hasBaseImage = await this.hasImage(this.defaultBaseImageName);
|
|
54
|
+
if (!hasBaseImage) {
|
|
55
|
+
this.logger.info(`Default base image '${this.defaultBaseImageName}' not found. Pulling from registry...`);
|
|
56
|
+
await this.pullBaseImage();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new Error(`Failed to initialize Docker: ${error.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Ensure Docker is installed and running
|
|
66
|
+
*/
|
|
67
|
+
async ensureDockerAvailable() {
|
|
68
|
+
try {
|
|
69
|
+
// First, try to ping Docker to see if it's running
|
|
70
|
+
await this.docker.ping();
|
|
71
|
+
this.logger.info('Docker is running and accessible');
|
|
72
|
+
return;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
this.logger.info('Docker is not accessible, checking installation...');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check if Docker is installed
|
|
78
|
+
const isInstalled = await this.isDockerInstalled();
|
|
79
|
+
|
|
80
|
+
if (!isInstalled) {
|
|
81
|
+
this.logger.info('Docker is not installed. Installing Docker...');
|
|
82
|
+
await this.installDocker();
|
|
83
|
+
} else {
|
|
84
|
+
this.logger.info('Docker is installed but not running. Starting Docker...');
|
|
85
|
+
await this.startDocker();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Wait for Docker to become available
|
|
89
|
+
await this.waitForDocker();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if Docker is installed on the system
|
|
94
|
+
*/
|
|
95
|
+
async isDockerInstalled() {
|
|
96
|
+
try {
|
|
97
|
+
await execAsync('docker --version');
|
|
98
|
+
return true;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Install Docker on the system
|
|
106
|
+
*/
|
|
107
|
+
async installDocker() {
|
|
108
|
+
const platform = process.platform;
|
|
109
|
+
|
|
110
|
+
this.logger.info(`Installing Docker for ${platform}...`);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
if (platform === 'darwin') {
|
|
114
|
+
await this.installDockerMacOS();
|
|
115
|
+
} else if (platform === 'linux') {
|
|
116
|
+
await this.installDockerLinux();
|
|
117
|
+
} else if (platform === 'win32') {
|
|
118
|
+
await this.installDockerWindows();
|
|
119
|
+
} else {
|
|
120
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.logger.info('Docker installation completed');
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new Error(`Failed to install Docker: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Install Docker on macOS
|
|
131
|
+
*/
|
|
132
|
+
async installDockerMacOS() {
|
|
133
|
+
this.logger.info('Installing Docker Desktop for macOS...');
|
|
134
|
+
|
|
135
|
+
// Check if Homebrew is available
|
|
136
|
+
try {
|
|
137
|
+
await execAsync('which brew');
|
|
138
|
+
this.logger.info('Using Homebrew to install Docker Desktop...');
|
|
139
|
+
|
|
140
|
+
// Install Docker Desktop via Homebrew
|
|
141
|
+
await this.runCommand('brew install --cask docker', 'Installing Docker Desktop via Homebrew');
|
|
142
|
+
|
|
143
|
+
} catch (error) {
|
|
144
|
+
this.logger.warn('Homebrew not found. Please install Docker Desktop manually from https://www.docker.com/products/docker-desktop/');
|
|
145
|
+
throw new Error('Docker Desktop installation requires manual intervention. Please install from https://www.docker.com/products/docker-desktop/');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Install Docker on Linux
|
|
151
|
+
*/
|
|
152
|
+
async installDockerLinux() {
|
|
153
|
+
this.logger.info('Installing Docker on Linux...');
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// Update package index
|
|
157
|
+
await this.runCommand('sudo apt-get update', 'Updating package index');
|
|
158
|
+
|
|
159
|
+
// Install prerequisites
|
|
160
|
+
await this.runCommand('sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release', 'Installing prerequisites');
|
|
161
|
+
|
|
162
|
+
// Add Docker's official GPG key
|
|
163
|
+
await this.runCommand('curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg', 'Adding Docker GPG key');
|
|
164
|
+
|
|
165
|
+
// Add Docker repository
|
|
166
|
+
await this.runCommand('echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null', 'Adding Docker repository');
|
|
167
|
+
|
|
168
|
+
// Update package index again
|
|
169
|
+
await this.runCommand('sudo apt-get update', 'Updating package index with Docker repository');
|
|
170
|
+
|
|
171
|
+
// Install Docker
|
|
172
|
+
await this.runCommand('sudo apt-get install -y docker-ce docker-ce-cli containerd.io', 'Installing Docker');
|
|
173
|
+
|
|
174
|
+
// Add current user to docker group
|
|
175
|
+
await this.runCommand('sudo usermod -aG docker $USER', 'Adding user to docker group');
|
|
176
|
+
|
|
177
|
+
this.logger.info('Docker installation completed. You may need to log out and back in for group changes to take effect.');
|
|
178
|
+
|
|
179
|
+
} catch (error) {
|
|
180
|
+
throw new Error(`Failed to install Docker on Linux: ${error.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Install Docker on Windows
|
|
186
|
+
*/
|
|
187
|
+
async installDockerWindows() {
|
|
188
|
+
this.logger.info('Installing Docker Desktop for Windows...');
|
|
189
|
+
|
|
190
|
+
// Check if Chocolatey is available
|
|
191
|
+
try {
|
|
192
|
+
await execAsync('choco --version');
|
|
193
|
+
this.logger.info('Using Chocolatey to install Docker Desktop...');
|
|
194
|
+
|
|
195
|
+
// Install Docker Desktop via Chocolatey
|
|
196
|
+
await this.runCommand('choco install docker-desktop -y', 'Installing Docker Desktop via Chocolatey');
|
|
197
|
+
|
|
198
|
+
} catch (error) {
|
|
199
|
+
this.logger.warn('Chocolatey not found. Please install Docker Desktop manually from https://www.docker.com/products/docker-desktop/');
|
|
200
|
+
throw new Error('Docker Desktop installation requires manual intervention. Please install from https://www.docker.com/products/docker-desktop/');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Start Docker service
|
|
206
|
+
*/
|
|
207
|
+
async startDocker() {
|
|
208
|
+
const platform = process.platform;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
if (platform === 'darwin') {
|
|
212
|
+
// On macOS, try to start Docker Desktop
|
|
213
|
+
await this.runCommand('open -a Docker', 'Starting Docker Desktop');
|
|
214
|
+
} else if (platform === 'linux') {
|
|
215
|
+
// On Linux, start Docker service
|
|
216
|
+
await this.runCommand('sudo systemctl start docker', 'Starting Docker service');
|
|
217
|
+
await this.runCommand('sudo systemctl enable docker', 'Enabling Docker service');
|
|
218
|
+
} else if (platform === 'win32') {
|
|
219
|
+
// On Windows, try to start Docker Desktop
|
|
220
|
+
await this.runCommand('start "" "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"', 'Starting Docker Desktop');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.logger.info('Docker service started');
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.logger.warn(`Failed to start Docker service: ${error.message}`);
|
|
226
|
+
this.logger.info('Please start Docker manually and try again');
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Wait for Docker to become available
|
|
233
|
+
*/
|
|
234
|
+
async waitForDocker() {
|
|
235
|
+
this.logger.info('Waiting for Docker to become available...');
|
|
236
|
+
|
|
237
|
+
const maxAttempts = 30; // 30 seconds
|
|
238
|
+
const delay = 1000; // 1 second
|
|
239
|
+
|
|
240
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
241
|
+
try {
|
|
242
|
+
await this.docker.ping();
|
|
243
|
+
this.logger.info('Docker is now available');
|
|
244
|
+
return;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (attempt === maxAttempts) {
|
|
247
|
+
throw new Error('Docker did not become available within the expected time');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.logger.info(`Waiting for Docker... (${attempt}/${maxAttempts})`);
|
|
251
|
+
await this.sleep(delay);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Run a command and log output
|
|
258
|
+
*/
|
|
259
|
+
async runCommand(command, description) {
|
|
260
|
+
this.logger.info(`${description}...`);
|
|
261
|
+
|
|
262
|
+
return new Promise((resolve, reject) => {
|
|
263
|
+
const child = spawn('sh', ['-c', command], {
|
|
264
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
265
|
+
shell: true
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
let stdout = '';
|
|
269
|
+
let stderr = '';
|
|
270
|
+
|
|
271
|
+
child.stdout.on('data', (data) => {
|
|
272
|
+
stdout += data.toString();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
child.stderr.on('data', (data) => {
|
|
276
|
+
stderr += data.toString();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
child.on('close', (code) => {
|
|
280
|
+
if (code === 0) {
|
|
281
|
+
this.logger.info(`${description} completed successfully`);
|
|
282
|
+
resolve({ stdout, stderr });
|
|
283
|
+
} else {
|
|
284
|
+
const error = new Error(`Command failed with exit code ${code}: ${stderr}`);
|
|
285
|
+
this.logger.error(`${description} failed: ${error.message}`);
|
|
286
|
+
reject(error);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
child.on('error', (error) => {
|
|
291
|
+
this.logger.error(`${description} failed: ${error.message}`);
|
|
292
|
+
reject(error);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Sleep utility
|
|
299
|
+
*/
|
|
300
|
+
sleep(ms) {
|
|
301
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Pull the base Docker image
|
|
306
|
+
*/
|
|
307
|
+
async pullBaseImage(baseImageName = null, options = {}) {
|
|
308
|
+
const imageName = baseImageName || this.defaultBaseImageName;
|
|
309
|
+
this.logger.info(`Pulling base Docker image: ${imageName}`);
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const stream = await this.docker.pull(imageName);
|
|
313
|
+
|
|
314
|
+
await this.followPullProgress(stream, 'Base image pull');
|
|
315
|
+
|
|
316
|
+
// Verify the image was pulled
|
|
317
|
+
const hasImage = await this.hasImage(imageName);
|
|
318
|
+
if (hasImage) {
|
|
319
|
+
this.logger.info(`Base image '${imageName}' pulled successfully`);
|
|
320
|
+
} else {
|
|
321
|
+
throw new Error(`Base image '${imageName}' was not pulled successfully`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
} catch (error) {
|
|
325
|
+
throw new Error(`Failed to pull base image: ${error.message}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Clean up existing containers from previous runs
|
|
331
|
+
*/
|
|
332
|
+
async cleanupExistingContainers(agents) {
|
|
333
|
+
this.logger.info('Cleaning up existing containers from previous runs...');
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
// Get all containers (running and stopped) that match our agent naming pattern
|
|
337
|
+
const containers = await this.docker.listContainers({ all: true });
|
|
338
|
+
|
|
339
|
+
const agentNames = agents.map(agent => agent.name.toLowerCase());
|
|
340
|
+
const containersToCleanup = containers.filter(container => {
|
|
341
|
+
// Check if container name matches our dank agent pattern
|
|
342
|
+
const containerName = container.Names[0].replace(/^\//, ''); // Remove leading slash
|
|
343
|
+
return agentNames.some(agentName =>
|
|
344
|
+
containerName.startsWith(`dank-${agentName}-`) ||
|
|
345
|
+
containerName === `dank-${agentName}`
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
if (containersToCleanup.length === 0) {
|
|
350
|
+
this.logger.info('No existing containers found to cleanup');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
this.logger.info(`Found ${containersToCleanup.length} existing containers to cleanup`);
|
|
355
|
+
|
|
356
|
+
// Stop and remove each container
|
|
357
|
+
for (const containerInfo of containersToCleanup) {
|
|
358
|
+
const container = this.docker.getContainer(containerInfo.Id);
|
|
359
|
+
const containerName = containerInfo.Names[0].replace(/^\//, '');
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
// Stop container if running
|
|
363
|
+
if (containerInfo.State === 'running') {
|
|
364
|
+
this.logger.info(`Stopping container: ${containerName}`);
|
|
365
|
+
await container.stop({ t: 10 }); // 10 second timeout
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Remove container
|
|
369
|
+
this.logger.info(`Removing container: ${containerName}`);
|
|
370
|
+
await container.remove({ force: true });
|
|
371
|
+
|
|
372
|
+
} catch (error) {
|
|
373
|
+
// Log but don't fail if we can't clean up a specific container
|
|
374
|
+
this.logger.warn(`Failed to cleanup container ${containerName}: ${error.message}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
this.logger.info('Container cleanup completed');
|
|
379
|
+
|
|
380
|
+
} catch (error) {
|
|
381
|
+
this.logger.error('Failed to cleanup existing containers:', error.message);
|
|
382
|
+
// Don't throw - we want to continue even if cleanup fails
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Build agent-specific image
|
|
388
|
+
*/
|
|
389
|
+
async buildAgentImage(agent, options = {}) {
|
|
390
|
+
const imageName = `dank-agent-${agent.name.toLowerCase()}`;
|
|
391
|
+
this.logger.info(`Building image for agent: ${agent.name}`);
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const buildContext = await this.createAgentBuildContext(agent);
|
|
395
|
+
|
|
396
|
+
const stream = await this.docker.buildImage(buildContext, {
|
|
397
|
+
t: imageName,
|
|
398
|
+
dockerfile: 'Dockerfile',
|
|
399
|
+
nocache: options.rebuild || options.noCache || false
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
await this.followBuildProgress(stream, `Agent ${agent.name} build`);
|
|
403
|
+
|
|
404
|
+
this.logger.info(`Agent image '${imageName}' built successfully`);
|
|
405
|
+
|
|
406
|
+
// Clean up build context
|
|
407
|
+
await fs.remove(buildContext);
|
|
408
|
+
|
|
409
|
+
return imageName;
|
|
410
|
+
|
|
411
|
+
} catch (error) {
|
|
412
|
+
throw new Error(`Failed to build agent image: ${error.message}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Generate handlers code from agent configuration
|
|
418
|
+
*/
|
|
419
|
+
generateHandlersCode(agent) {
|
|
420
|
+
const handlers = {};
|
|
421
|
+
|
|
422
|
+
// Add default handlers
|
|
423
|
+
handlers.output = ['(data) => console.log("Output:", data)'];
|
|
424
|
+
handlers.error = ['(error) => console.error("Error:", error)'];
|
|
425
|
+
|
|
426
|
+
// Add custom handlers from agent configuration
|
|
427
|
+
if (agent.handlers && agent.handlers.size > 0) {
|
|
428
|
+
for (const [eventName, handlerList] of agent.handlers) {
|
|
429
|
+
if (!handlers[eventName]) {
|
|
430
|
+
handlers[eventName] = [];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Convert handler functions to string representations
|
|
434
|
+
handlerList.forEach(handlerObj => {
|
|
435
|
+
if (handlerObj && typeof handlerObj.handler === 'function') {
|
|
436
|
+
// Convert function to string, handling the function properly
|
|
437
|
+
const handlerStr = handlerObj.handler.toString();
|
|
438
|
+
handlers[eventName].push(handlerStr);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Generate the JavaScript object code
|
|
445
|
+
const handlersEntries = Object.entries(handlers).map(([eventName, handlerArray]) => {
|
|
446
|
+
const handlersStr = handlerArray.join(',\n ');
|
|
447
|
+
// Quote event names that contain special characters (like colons)
|
|
448
|
+
const quotedEventName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(eventName) ? eventName : `"${eventName}"`;
|
|
449
|
+
return ` ${quotedEventName}: [\n ${handlersStr}\n ]`;
|
|
450
|
+
}).join(',\n');
|
|
451
|
+
|
|
452
|
+
return `{\n${handlersEntries}\n }`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Start agent container
|
|
457
|
+
*/
|
|
458
|
+
async startAgent(agent, options = {}) {
|
|
459
|
+
// Finalize agent configuration (auto-detect features)
|
|
460
|
+
agent.finalize();
|
|
461
|
+
|
|
462
|
+
const imageName = `dank-agent-${agent.name.toLowerCase()}`;
|
|
463
|
+
const containerName = `dank-${agent.name.toLowerCase()}-${agent.id.split('_').pop()}`;
|
|
464
|
+
const baseImageName = agent.config.docker?.baseImage || this.defaultBaseImageName;
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
// Ensure base image exists
|
|
468
|
+
const hasBaseImage = await this.hasImage(baseImageName);
|
|
469
|
+
if (!hasBaseImage) {
|
|
470
|
+
this.logger.info(`Base image '${baseImageName}' not found for agent ${agent.name}. Pulling...`);
|
|
471
|
+
await this.pullBaseImage(baseImageName);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Check if agent image exists, build if necessary
|
|
475
|
+
const hasImage = await this.hasImage(imageName);
|
|
476
|
+
if (!hasImage || options.rebuild) {
|
|
477
|
+
await this.buildAgentImage(agent, options);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Prepare container configuration
|
|
481
|
+
const containerConfig = {
|
|
482
|
+
Image: imageName,
|
|
483
|
+
name: containerName,
|
|
484
|
+
Env: this.prepareEnvironmentVariables(agent),
|
|
485
|
+
HostConfig: {
|
|
486
|
+
Memory: AgentConfig.parseMemory(agent.config.resources.memory),
|
|
487
|
+
CpuQuota: Math.floor(agent.config.resources.cpu * 100000),
|
|
488
|
+
CpuPeriod: 100000,
|
|
489
|
+
RestartPolicy: {
|
|
490
|
+
Name: 'on-failure',
|
|
491
|
+
MaximumRetryCount: agent.config.resources.maxRestarts || 3
|
|
492
|
+
},
|
|
493
|
+
NetworkMode: this.networkName,
|
|
494
|
+
...this.preparePortConfiguration(agent)
|
|
495
|
+
},
|
|
496
|
+
NetworkingConfig: {
|
|
497
|
+
EndpointsConfig: {
|
|
498
|
+
[this.networkName]: {}
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
...this.prepareExposedPorts(agent)
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// Create container
|
|
505
|
+
this.logger.info(`Creating container for agent: ${agent.name}`);
|
|
506
|
+
const container = await this.docker.createContainer(containerConfig);
|
|
507
|
+
|
|
508
|
+
// Start container
|
|
509
|
+
await container.start();
|
|
510
|
+
|
|
511
|
+
// Store container reference
|
|
512
|
+
this.containers.set(agent.name, {
|
|
513
|
+
container,
|
|
514
|
+
agent,
|
|
515
|
+
startTime: new Date(),
|
|
516
|
+
status: 'running'
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
agent.containerId = container.id;
|
|
520
|
+
agent.status = 'running';
|
|
521
|
+
|
|
522
|
+
this.logger.info(`Agent ${agent.name} started successfully (${container.id.substring(0, 12)})`);
|
|
523
|
+
|
|
524
|
+
return container;
|
|
525
|
+
|
|
526
|
+
} catch (error) {
|
|
527
|
+
agent.status = 'error';
|
|
528
|
+
throw new Error(`Failed to start agent ${agent.name}: ${error.message}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Stop agent container
|
|
534
|
+
*/
|
|
535
|
+
async stopAgent(agentName, options = {}) {
|
|
536
|
+
const containerInfo = this.containers.get(agentName);
|
|
537
|
+
if (!containerInfo) {
|
|
538
|
+
throw new Error(`Agent ${agentName} not found or not running`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
const { container, agent } = containerInfo;
|
|
543
|
+
|
|
544
|
+
this.logger.info(`Stopping agent: ${agentName}`);
|
|
545
|
+
|
|
546
|
+
if (options.force) {
|
|
547
|
+
await container.kill();
|
|
548
|
+
} else {
|
|
549
|
+
await container.stop({ t: 10 }); // 10 second timeout
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
await container.remove();
|
|
553
|
+
|
|
554
|
+
this.containers.delete(agentName);
|
|
555
|
+
agent.status = 'stopped';
|
|
556
|
+
agent.containerId = null;
|
|
557
|
+
|
|
558
|
+
this.logger.info(`Agent ${agentName} stopped successfully`);
|
|
559
|
+
|
|
560
|
+
} catch (error) {
|
|
561
|
+
throw new Error(`Failed to stop agent ${agentName}: ${error.message}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Get agent status
|
|
567
|
+
*/
|
|
568
|
+
async getAgentStatus(agentName) {
|
|
569
|
+
const containerInfo = this.containers.get(agentName);
|
|
570
|
+
if (!containerInfo) {
|
|
571
|
+
return { status: 'not_running' };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
const { container, agent, startTime } = containerInfo;
|
|
576
|
+
const containerData = await container.inspect();
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
status: containerData.State.Running ? 'running' : 'stopped',
|
|
580
|
+
containerId: container.id,
|
|
581
|
+
startTime,
|
|
582
|
+
uptime: Date.now() - startTime.getTime(),
|
|
583
|
+
health: containerData.State.Health?.Status || 'unknown',
|
|
584
|
+
restartCount: containerData.RestartCount,
|
|
585
|
+
resources: {
|
|
586
|
+
memory: agent.config.resources.memory,
|
|
587
|
+
cpu: agent.config.resources.cpu
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
} catch (error) {
|
|
592
|
+
return { status: 'error', error: error.message };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Get container logs
|
|
598
|
+
*/
|
|
599
|
+
async getAgentLogs(agentName, options = {}) {
|
|
600
|
+
const containerInfo = this.containers.get(agentName);
|
|
601
|
+
if (!containerInfo) {
|
|
602
|
+
throw new Error(`Agent ${agentName} not found`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const { container } = containerInfo;
|
|
606
|
+
|
|
607
|
+
const logStream = await container.logs({
|
|
608
|
+
stdout: true,
|
|
609
|
+
stderr: true,
|
|
610
|
+
follow: options.follow || false,
|
|
611
|
+
tail: options.tail || 100,
|
|
612
|
+
since: options.since || undefined,
|
|
613
|
+
timestamps: true
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
return logStream;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Create build context for base image
|
|
621
|
+
*/
|
|
622
|
+
async createBaseBuildContext() {
|
|
623
|
+
const contextDir = path.join(__dirname, '../../.build-context-base');
|
|
624
|
+
await fs.ensureDir(contextDir);
|
|
625
|
+
|
|
626
|
+
// Copy Docker files
|
|
627
|
+
await fs.copy(path.join(__dirname, '../../docker'), contextDir);
|
|
628
|
+
|
|
629
|
+
// Create runtime directory
|
|
630
|
+
const runtimeDir = path.join(contextDir, 'runtime');
|
|
631
|
+
await fs.ensureDir(runtimeDir);
|
|
632
|
+
|
|
633
|
+
// Create tarball
|
|
634
|
+
const tarPath = path.join(__dirname, '../../.base-build-context.tar');
|
|
635
|
+
await tar.create({
|
|
636
|
+
file: tarPath,
|
|
637
|
+
cwd: contextDir
|
|
638
|
+
}, ['.']);
|
|
639
|
+
|
|
640
|
+
return tarPath;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Create build context for agent
|
|
645
|
+
*/
|
|
646
|
+
async createAgentBuildContext(agent) {
|
|
647
|
+
const contextDir = path.join(__dirname, `../../.build-context-${agent.name}`);
|
|
648
|
+
await fs.ensureDir(contextDir);
|
|
649
|
+
|
|
650
|
+
// Get the base image for this agent
|
|
651
|
+
const baseImageName = agent.config.docker?.baseImage || this.defaultBaseImageName;
|
|
652
|
+
|
|
653
|
+
// Create Dockerfile for agent
|
|
654
|
+
const dockerfile = `FROM ${baseImageName}
|
|
655
|
+
COPY agent-code/ /app/agent-code/
|
|
656
|
+
USER dankuser
|
|
657
|
+
`;
|
|
658
|
+
|
|
659
|
+
await fs.writeFile(path.join(contextDir, 'Dockerfile'), dockerfile);
|
|
660
|
+
|
|
661
|
+
// Copy agent code if it exists
|
|
662
|
+
const agentCodeDir = path.join(contextDir, 'agent-code');
|
|
663
|
+
await fs.ensureDir(agentCodeDir);
|
|
664
|
+
|
|
665
|
+
// Create basic agent code structure
|
|
666
|
+
// Generate handlers from agent configuration
|
|
667
|
+
const handlersCode = this.generateHandlersCode(agent);
|
|
668
|
+
|
|
669
|
+
const agentCode = `
|
|
670
|
+
// Agent: ${agent.name}
|
|
671
|
+
// Generated by Dank Agent Service
|
|
672
|
+
|
|
673
|
+
module.exports = {
|
|
674
|
+
async main(context) {
|
|
675
|
+
const { llmClient, handlers, tools, config } = context;
|
|
676
|
+
console.log('Agent ${agent.name} started');
|
|
677
|
+
console.log('Available context:', Object.keys(context));
|
|
678
|
+
|
|
679
|
+
// Basic agent loop
|
|
680
|
+
setInterval(async () => {
|
|
681
|
+
try {
|
|
682
|
+
// Trigger heartbeat
|
|
683
|
+
const heartbeatHandlers = handlers.get('heartbeat') || [];
|
|
684
|
+
heartbeatHandlers.forEach(handler => {
|
|
685
|
+
try {
|
|
686
|
+
handler();
|
|
687
|
+
} catch (handlerError) {
|
|
688
|
+
console.error('Heartbeat handler error:', handlerError);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Custom agent logic would go here
|
|
693
|
+
console.log('Agent ${agent.name} heartbeat - uptime:', Math.floor(process.uptime()), 'seconds');
|
|
694
|
+
|
|
695
|
+
} catch (error) {
|
|
696
|
+
console.error('Agent loop error:', error);
|
|
697
|
+
const errorHandlers = handlers.get('error') || [];
|
|
698
|
+
errorHandlers.forEach(handler => {
|
|
699
|
+
try {
|
|
700
|
+
handler(error);
|
|
701
|
+
} catch (handlerError) {
|
|
702
|
+
console.error('Error handler failed:', handlerError);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}, 10000);
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
handlers: ${handlersCode}
|
|
710
|
+
};
|
|
711
|
+
`;
|
|
712
|
+
|
|
713
|
+
await fs.writeFile(path.join(agentCodeDir, 'index.js'), agentCode);
|
|
714
|
+
|
|
715
|
+
// Create tarball
|
|
716
|
+
const tarPath = path.join(__dirname, `../../.agent-${agent.name}-context.tar`);
|
|
717
|
+
await tar.create({
|
|
718
|
+
file: tarPath,
|
|
719
|
+
cwd: contextDir
|
|
720
|
+
}, ['.']);
|
|
721
|
+
|
|
722
|
+
return tarPath;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Prepare environment variables for container
|
|
727
|
+
*/
|
|
728
|
+
prepareEnvironmentVariables(agent) {
|
|
729
|
+
const env = AgentConfig.generateContainerEnv(agent);
|
|
730
|
+
return Object.entries(env).map(([key, value]) => `${key}=${value}`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Prepare port configuration for container
|
|
735
|
+
*/
|
|
736
|
+
preparePortConfiguration(agent) {
|
|
737
|
+
const portConfig = {};
|
|
738
|
+
const portBindings = {};
|
|
739
|
+
|
|
740
|
+
// Always bind the main agent port
|
|
741
|
+
const mainPort = agent.config.docker?.port || DOCKER_CONFIG.defaultPort;
|
|
742
|
+
portBindings[`${mainPort}/tcp`] = [{ HostPort: mainPort.toString() }];
|
|
743
|
+
|
|
744
|
+
// Also bind HTTP port if HTTP is enabled and different from main port
|
|
745
|
+
if (agent.config.http && agent.config.http.enabled) {
|
|
746
|
+
const httpPort = agent.config.http.port;
|
|
747
|
+
if (httpPort !== mainPort) {
|
|
748
|
+
portBindings[`${httpPort}/tcp`] = [{ HostPort: httpPort.toString() }];
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Always bind health check port
|
|
753
|
+
const healthPort = DOCKER_CONFIG.healthCheckPort;
|
|
754
|
+
portBindings[`${healthPort}/tcp`] = [{ HostPort: healthPort.toString() }];
|
|
755
|
+
|
|
756
|
+
portConfig.PortBindings = portBindings;
|
|
757
|
+
return portConfig;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Prepare exposed ports for container
|
|
762
|
+
*/
|
|
763
|
+
prepareExposedPorts(agent) {
|
|
764
|
+
const exposedPorts = {};
|
|
765
|
+
|
|
766
|
+
// Always expose the main agent port
|
|
767
|
+
const mainPort = agent.config.docker?.port || DOCKER_CONFIG.defaultPort;
|
|
768
|
+
exposedPorts[`${mainPort}/tcp`] = {};
|
|
769
|
+
|
|
770
|
+
// Also expose HTTP port if HTTP is enabled and different from main port
|
|
771
|
+
if (agent.config.http && agent.config.http.enabled) {
|
|
772
|
+
const httpPort = agent.config.http.port;
|
|
773
|
+
if (httpPort !== mainPort) {
|
|
774
|
+
exposedPorts[`${httpPort}/tcp`] = {};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Always expose health check port
|
|
779
|
+
const healthPort = DOCKER_CONFIG.healthCheckPort;
|
|
780
|
+
exposedPorts[`${healthPort}/tcp`] = {};
|
|
781
|
+
|
|
782
|
+
return { ExposedPorts: exposedPorts };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Ensure Docker network exists
|
|
787
|
+
*/
|
|
788
|
+
async ensureNetwork() {
|
|
789
|
+
try {
|
|
790
|
+
await this.docker.getNetwork(this.networkName).inspect();
|
|
791
|
+
this.logger.debug(`Network '${this.networkName}' already exists`);
|
|
792
|
+
} catch (error) {
|
|
793
|
+
if (error.statusCode === 404) {
|
|
794
|
+
this.logger.info(`Creating Docker network: ${this.networkName}`);
|
|
795
|
+
await this.docker.createNetwork({
|
|
796
|
+
Name: this.networkName,
|
|
797
|
+
Driver: 'bridge'
|
|
798
|
+
});
|
|
799
|
+
} else {
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Check if Docker image exists
|
|
807
|
+
*/
|
|
808
|
+
async hasImage(imageName) {
|
|
809
|
+
try {
|
|
810
|
+
await this.docker.getImage(imageName).inspect();
|
|
811
|
+
return true;
|
|
812
|
+
} catch (error) {
|
|
813
|
+
if (error.statusCode === 404) {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
throw error;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Follow build progress and log output
|
|
822
|
+
*/
|
|
823
|
+
async followBuildProgress(stream, buildName) {
|
|
824
|
+
return new Promise((resolve, reject) => {
|
|
825
|
+
this.docker.modem.followProgress(stream, (err, res) => {
|
|
826
|
+
if (err) {
|
|
827
|
+
reject(err);
|
|
828
|
+
} else {
|
|
829
|
+
resolve(res);
|
|
830
|
+
}
|
|
831
|
+
}, (event) => {
|
|
832
|
+
if (event.stream) {
|
|
833
|
+
process.stdout.write(event.stream);
|
|
834
|
+
} else if (event.status) {
|
|
835
|
+
this.logger.debug(`${buildName}: ${event.status}`);
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Follow pull progress and log output
|
|
843
|
+
*/
|
|
844
|
+
async followPullProgress(stream, pullName) {
|
|
845
|
+
return new Promise((resolve, reject) => {
|
|
846
|
+
this.docker.modem.followProgress(stream, (err, res) => {
|
|
847
|
+
if (err) {
|
|
848
|
+
reject(err);
|
|
849
|
+
} else {
|
|
850
|
+
resolve(res);
|
|
851
|
+
}
|
|
852
|
+
}, (event) => {
|
|
853
|
+
if (event.status) {
|
|
854
|
+
if (event.progress) {
|
|
855
|
+
process.stdout.write(`\r${pullName}: ${event.status} ${event.progress}`);
|
|
856
|
+
} else {
|
|
857
|
+
this.logger.info(`${pullName}: ${event.status}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Clean up Docker resources
|
|
866
|
+
*/
|
|
867
|
+
async cleanup(options = {}) {
|
|
868
|
+
this.logger.info('Cleaning up Docker resources...');
|
|
869
|
+
|
|
870
|
+
try {
|
|
871
|
+
if (options.containers || options.all) {
|
|
872
|
+
// Stop and remove all Dank containers
|
|
873
|
+
const containers = await this.docker.listContainers({
|
|
874
|
+
all: true,
|
|
875
|
+
filters: { name: ['dank-'] }
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
for (const containerInfo of containers) {
|
|
879
|
+
const container = this.docker.getContainer(containerInfo.Id);
|
|
880
|
+
try {
|
|
881
|
+
if (containerInfo.State === 'running') {
|
|
882
|
+
await container.stop();
|
|
883
|
+
}
|
|
884
|
+
await container.remove();
|
|
885
|
+
this.logger.info(`Removed container: ${containerInfo.Names[0]}`);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
this.logger.warn(`Failed to remove container ${containerInfo.Names[0]}: ${error.message}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (options.images || options.all) {
|
|
893
|
+
// Remove Dank images
|
|
894
|
+
const images = await this.docker.listImages({
|
|
895
|
+
filters: { reference: ['dank-*'] }
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
for (const imageInfo of images) {
|
|
899
|
+
const image = this.docker.getImage(imageInfo.Id);
|
|
900
|
+
try {
|
|
901
|
+
await image.remove();
|
|
902
|
+
this.logger.info(`Removed image: ${imageInfo.RepoTags?.[0] || imageInfo.Id}`);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
this.logger.warn(`Failed to remove image: ${error.message}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (options.buildContexts || options.all) {
|
|
910
|
+
// Clean up build context directories and tarballs
|
|
911
|
+
await this.cleanupBuildContexts();
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
this.logger.info('Cleanup completed');
|
|
915
|
+
|
|
916
|
+
} catch (error) {
|
|
917
|
+
throw new Error(`Cleanup failed: ${error.message}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Clean up build context directories and tarballs
|
|
923
|
+
*/
|
|
924
|
+
async cleanupBuildContexts() {
|
|
925
|
+
const projectRoot = path.join(__dirname, '../..');
|
|
926
|
+
|
|
927
|
+
try {
|
|
928
|
+
// Find all build context directories
|
|
929
|
+
const entries = await fs.readdir(projectRoot);
|
|
930
|
+
const buildContextDirs = entries.filter(entry => entry.startsWith('.build-context-'));
|
|
931
|
+
|
|
932
|
+
// Remove build context directories
|
|
933
|
+
for (const dir of buildContextDirs) {
|
|
934
|
+
const dirPath = path.join(projectRoot, dir);
|
|
935
|
+
try {
|
|
936
|
+
await fs.remove(dirPath);
|
|
937
|
+
this.logger.info(`Removed build context directory: ${dir}`);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
this.logger.warn(`Failed to remove build context directory ${dir}: ${error.message}`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Find and remove tarball files
|
|
944
|
+
const tarballs = entries.filter(entry =>
|
|
945
|
+
entry.endsWith('-context.tar') || entry.endsWith('-build-context.tar')
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
for (const tarball of tarballs) {
|
|
949
|
+
const tarballPath = path.join(projectRoot, tarball);
|
|
950
|
+
try {
|
|
951
|
+
await fs.remove(tarballPath);
|
|
952
|
+
this.logger.info(`Removed build context tarball: ${tarball}`);
|
|
953
|
+
} catch (error) {
|
|
954
|
+
this.logger.warn(`Failed to remove tarball ${tarball}: ${error.message}`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (buildContextDirs.length === 0 && tarballs.length === 0) {
|
|
959
|
+
this.logger.info('No build context files found to clean up');
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
} catch (error) {
|
|
963
|
+
this.logger.warn(`Error during build context cleanup: ${error.message}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
module.exports = { DockerManager };
|