dank-ai 1.0.31 → 1.0.33
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 +49 -50
- package/bin/dank +1 -0
- package/docker/entrypoint.js +140 -353
- package/lib/agent.js +100 -25
- package/lib/cli/init.js +1 -14
- package/lib/cli/production-build.js +6 -8
- package/lib/config.js +32 -33
- package/lib/constants.js +23 -6
- package/lib/docker/manager.js +161 -42
- package/lib/project.js +1 -4
- package/package.json +1 -1
package/lib/docker/manager.js
CHANGED
|
@@ -1140,13 +1140,35 @@ class DockerManager {
|
|
|
1140
1140
|
}
|
|
1141
1141
|
}
|
|
1142
1142
|
|
|
1143
|
+
/**
|
|
1144
|
+
* Normalize Docker image name/tag components
|
|
1145
|
+
* Docker allows: lowercase letters, digits, underscores, periods, and hyphens
|
|
1146
|
+
* Cannot start or end with period or hyphen, max 128 chars for tags
|
|
1147
|
+
*/
|
|
1148
|
+
normalizeDockerName(name) {
|
|
1149
|
+
if (!name || typeof name !== 'string') return 'invalid';
|
|
1150
|
+
const lower = String(name).toLowerCase();
|
|
1151
|
+
let sanitized = lower.replace(/[^a-z0-9_.-]/g, '-');
|
|
1152
|
+
sanitized = sanitized.replace(/\.{2,}/g, '.'); // Replace multiple periods with single
|
|
1153
|
+
sanitized = sanitized.replace(/-{2,}/g, '-'); // Replace multiple hyphens with single
|
|
1154
|
+
sanitized = sanitized.replace(/^[.-]+/, '').replace(/[.-]+$/, ''); // Remove leading/trailing . or -
|
|
1155
|
+
if (!sanitized || !/^[a-z0-9]/.test(sanitized)) sanitized = `a${sanitized || ''}`;
|
|
1156
|
+
if (sanitized.length > 128) sanitized = sanitized.slice(0, 128);
|
|
1157
|
+
return sanitized;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1143
1160
|
/**
|
|
1144
1161
|
* Build agent-specific image
|
|
1145
1162
|
*/
|
|
1146
1163
|
async buildAgentImage(agent, options = {}) {
|
|
1147
|
-
const
|
|
1164
|
+
const normalizedName = this.normalizeDockerName(agent.name);
|
|
1165
|
+
const imageName = `dank-agent-${normalizedName}`;
|
|
1148
1166
|
this.logger.info(`Building image for agent: ${agent.name}`);
|
|
1149
1167
|
|
|
1168
|
+
// Finalize agent configuration before building
|
|
1169
|
+
// This ensures ports and other configs are properly set
|
|
1170
|
+
agent.finalize();
|
|
1171
|
+
|
|
1150
1172
|
try {
|
|
1151
1173
|
const buildContext = await this.createAgentBuildContext(agent);
|
|
1152
1174
|
const dockerCmd = await this.resolveDockerCommand();
|
|
@@ -1178,6 +1200,17 @@ class DockerManager {
|
|
|
1178
1200
|
|
|
1179
1201
|
/**
|
|
1180
1202
|
* Build production image with custom naming and tagging
|
|
1203
|
+
*
|
|
1204
|
+
* @param {Object} agent - The agent to build
|
|
1205
|
+
* @param {Object} options - Build options
|
|
1206
|
+
* @param {string} [options.tag="latest"] - Image tag
|
|
1207
|
+
* @param {string} [options.registry] - Docker registry
|
|
1208
|
+
* @param {string} [options.namespace] - Docker namespace
|
|
1209
|
+
* @param {boolean} [options.tagByAgent=false] - Use agent name as tag
|
|
1210
|
+
* @param {boolean} [options.force=false] - Force rebuild without cache
|
|
1211
|
+
* @param {boolean} [options.push=false] - Push to registry after build
|
|
1212
|
+
* @param {string} [options.baseImageOverride=null] - Production-only: Override base image for all agents (replaces agent's configured base image)
|
|
1213
|
+
* @returns {Promise<Object>} Build result with imageName and pushed status
|
|
1181
1214
|
*/
|
|
1182
1215
|
async buildProductionImage(agent, options = {}) {
|
|
1183
1216
|
const {
|
|
@@ -1187,42 +1220,47 @@ class DockerManager {
|
|
|
1187
1220
|
tagByAgent = false,
|
|
1188
1221
|
force = false,
|
|
1189
1222
|
push = false,
|
|
1223
|
+
baseImageOverride = null, // Production-only: override base image for all agents
|
|
1190
1224
|
} = options;
|
|
1191
1225
|
|
|
1192
|
-
// Normalize
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
let sanitized = lower.replace(/[^a-z0-9_.-]/g, '-');
|
|
1196
|
-
sanitized = sanitized.replace(/^-+/, '').replace(/-+$/, '');
|
|
1197
|
-
if (!sanitized || !/^[a-z0-9]/.test(sanitized)) sanitized = `a${sanitized || ''}`;
|
|
1198
|
-
if (sanitized.length > 128) sanitized = sanitized.slice(0, 128);
|
|
1199
|
-
return sanitized;
|
|
1200
|
-
};
|
|
1226
|
+
// Normalize all components
|
|
1227
|
+
const normalizedAgentName = this.normalizeDockerName(agent.name);
|
|
1228
|
+
const normalizedTag = this.normalizeDockerName(tag);
|
|
1201
1229
|
|
|
1202
1230
|
//construct full repo name
|
|
1203
1231
|
let repoName;
|
|
1204
1232
|
if(!tagByAgent){
|
|
1205
1233
|
// Per-agent repository: {registry}/{namespace}/{agent-name}
|
|
1206
|
-
|
|
1207
|
-
repoName = `${registry?`${registry}/`:''}${namespace?`${namespace}/`:''}${agentRepoName}`;
|
|
1234
|
+
repoName = `${registry?`${registry}/`:''}${namespace?`${namespace}/`:''}${normalizedAgentName}`;
|
|
1208
1235
|
}else{
|
|
1209
|
-
// Common repository: {registry}/{namespace}
|
|
1236
|
+
// Common repository: {registry}/{namespace}
|
|
1210
1237
|
repoName = `${registry?`${registry}/`:''}${namespace?`${namespace}/`:''}`;
|
|
1211
1238
|
}
|
|
1212
1239
|
|
|
1213
1240
|
repoName = repoName.replace(/\/+$/, '');
|
|
1214
1241
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const finalTag = tagByAgent ? normalizeTag(agent.name) : tag;
|
|
1242
|
+
// Final tag selection - normalize both agent name tags and user-provided tags
|
|
1243
|
+
const finalTag = tagByAgent ? normalizedAgentName : normalizedTag;
|
|
1218
1244
|
const imageName = `${repoName}:${finalTag}`;
|
|
1219
1245
|
|
|
1220
1246
|
this.logger.info(
|
|
1221
1247
|
`Building production image for agent: ${agent.name} -> ${imageName}`
|
|
1222
1248
|
);
|
|
1223
1249
|
|
|
1250
|
+
// Finalize agent configuration before building
|
|
1251
|
+
// This ensures ports and other configs are properly set
|
|
1252
|
+
agent.finalize();
|
|
1253
|
+
|
|
1254
|
+
// Log base image override if provided
|
|
1255
|
+
if (baseImageOverride) {
|
|
1256
|
+
this.logger.info(`🔧 Production build: Using custom base image override: ${baseImageOverride}`);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1224
1259
|
try {
|
|
1225
|
-
const buildContext = await this.createAgentBuildContext(agent
|
|
1260
|
+
const buildContext = await this.createAgentBuildContext(agent, {
|
|
1261
|
+
isProductionBuild: true,
|
|
1262
|
+
baseImageOverride: baseImageOverride
|
|
1263
|
+
});
|
|
1226
1264
|
const dockerCmd = await this.resolveDockerCommand();
|
|
1227
1265
|
|
|
1228
1266
|
const buildCommand = [
|
|
@@ -1299,6 +1337,53 @@ class DockerManager {
|
|
|
1299
1337
|
return `{\n${handlersEntries}\n }`;
|
|
1300
1338
|
}
|
|
1301
1339
|
|
|
1340
|
+
/**
|
|
1341
|
+
* Generate routes code from agent configuration
|
|
1342
|
+
*/
|
|
1343
|
+
generateRoutesCode(agent) {
|
|
1344
|
+
const routes = {};
|
|
1345
|
+
|
|
1346
|
+
// Add routes from agent HTTP configuration
|
|
1347
|
+
if (agent.config?.http?.routes && agent.config.http.routes.size > 0) {
|
|
1348
|
+
for (const [routeKey, routeList] of agent.config.http.routes) {
|
|
1349
|
+
const [method, path] = routeKey.split(':');
|
|
1350
|
+
if (!routes[path]) {
|
|
1351
|
+
routes[path] = {};
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Convert route handlers to string representations
|
|
1355
|
+
routeList.forEach((routeObj) => {
|
|
1356
|
+
if (routeObj && typeof routeObj.handler === "function") {
|
|
1357
|
+
const handlerStr = routeObj.handler.toString();
|
|
1358
|
+
routes[path][method.toLowerCase()] = handlerStr;
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Generate the JavaScript object code
|
|
1365
|
+
if (Object.keys(routes).length === 0) {
|
|
1366
|
+
return '{}';
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const routesEntries = Object.entries(routes)
|
|
1370
|
+
.map(([path, methods]) => {
|
|
1371
|
+
const methodsEntries = Object.entries(methods)
|
|
1372
|
+
.map(([method, handler]) => {
|
|
1373
|
+
return ` ${method}: ${handler}`;
|
|
1374
|
+
})
|
|
1375
|
+
.join(",\n");
|
|
1376
|
+
// Quote paths that contain special characters
|
|
1377
|
+
const quotedPath = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(path)
|
|
1378
|
+
? path
|
|
1379
|
+
: JSON.stringify(path);
|
|
1380
|
+
return ` ${quotedPath}: {\n${methodsEntries}\n }`;
|
|
1381
|
+
})
|
|
1382
|
+
.join(",\n");
|
|
1383
|
+
|
|
1384
|
+
return `{\n${routesEntries}\n }`;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1302
1387
|
/**
|
|
1303
1388
|
* Start agent container
|
|
1304
1389
|
*/
|
|
@@ -1341,12 +1426,12 @@ class DockerManager {
|
|
|
1341
1426
|
name: containerName,
|
|
1342
1427
|
Env: this.prepareEnvironmentVariables(agent),
|
|
1343
1428
|
HostConfig: {
|
|
1344
|
-
Memory: AgentConfig.parseMemory(agent.config.
|
|
1345
|
-
CpuQuota: Math.floor(agent.config.
|
|
1429
|
+
Memory: AgentConfig.parseMemory(AgentConfig.getResourcesFromInstanceType(agent.config.instanceType).memory),
|
|
1430
|
+
CpuQuota: Math.floor(AgentConfig.getResourcesFromInstanceType(agent.config.instanceType).cpu * 100000),
|
|
1346
1431
|
CpuPeriod: 100000,
|
|
1347
1432
|
RestartPolicy: {
|
|
1348
1433
|
Name: "on-failure",
|
|
1349
|
-
MaximumRetryCount:
|
|
1434
|
+
MaximumRetryCount: 3, // Default max restarts
|
|
1350
1435
|
},
|
|
1351
1436
|
NetworkMode: this.networkName,
|
|
1352
1437
|
...this.preparePortConfiguration(agent),
|
|
@@ -1475,10 +1560,7 @@ class DockerManager {
|
|
|
1475
1560
|
uptime: Date.now() - startTime.getTime(),
|
|
1476
1561
|
health: containerData.State.Health?.Status || "unknown",
|
|
1477
1562
|
restartCount: containerData.RestartCount,
|
|
1478
|
-
resources:
|
|
1479
|
-
memory: agent.config.resources.memory,
|
|
1480
|
-
cpu: agent.config.resources.cpu,
|
|
1481
|
-
},
|
|
1563
|
+
resources: AgentConfig.getResourcesFromInstanceType(agent.config.instanceType),
|
|
1482
1564
|
};
|
|
1483
1565
|
} catch (error) {
|
|
1484
1566
|
return { status: "error", error: error.message };
|
|
@@ -1538,7 +1620,7 @@ class DockerManager {
|
|
|
1538
1620
|
/**
|
|
1539
1621
|
* Create build context for agent
|
|
1540
1622
|
*/
|
|
1541
|
-
async createAgentBuildContext(agent) {
|
|
1623
|
+
async createAgentBuildContext(agent, options = {}) {
|
|
1542
1624
|
const contextDir = path.join(
|
|
1543
1625
|
__dirname,
|
|
1544
1626
|
`../../.build-context-${agent.name}`
|
|
@@ -1546,12 +1628,44 @@ class DockerManager {
|
|
|
1546
1628
|
await fs.ensureDir(contextDir);
|
|
1547
1629
|
|
|
1548
1630
|
// Get the base image for this agent
|
|
1549
|
-
|
|
1550
|
-
|
|
1631
|
+
// Production builds can override the base image via baseImageOverride option
|
|
1632
|
+
let baseImageName;
|
|
1633
|
+
if (options.isProductionBuild && options.baseImageOverride) {
|
|
1634
|
+
baseImageName = options.baseImageOverride;
|
|
1635
|
+
this.logger.info(`🔧 Using production base image override: ${baseImageName} (instead of ${agent.config.docker?.baseImage || this.defaultBaseImageName})`);
|
|
1636
|
+
} else {
|
|
1637
|
+
baseImageName = agent.config.docker?.baseImage || this.defaultBaseImageName;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// Generate environment variables
|
|
1641
|
+
// For production builds, these need to be embedded in the image
|
|
1642
|
+
// For normal builds, they're injected at container creation time via startAgent()
|
|
1643
|
+
const env = AgentConfig.generateContainerEnv(agent);
|
|
1644
|
+
|
|
1645
|
+
// Embed env vars in Dockerfile for production builds
|
|
1646
|
+
// For normal builds, env vars are injected at container creation time
|
|
1647
|
+
const isProductionBuild = options.isProductionBuild || false;
|
|
1648
|
+
|
|
1649
|
+
let envStatements = '';
|
|
1650
|
+
if (isProductionBuild) {
|
|
1651
|
+
// Embed environment variables in Dockerfile for production builds
|
|
1652
|
+
// This ensures they're available when the image is deployed elsewhere
|
|
1653
|
+
envStatements = Object.entries(env)
|
|
1654
|
+
.map(([key, value]) => {
|
|
1655
|
+
// Escape special characters in values for Dockerfile ENV
|
|
1656
|
+
const escapedValue = String(value).replace(/\$/g, '$$$$').replace(/"/g, '\\"');
|
|
1657
|
+
return `ENV ${key}="${escapedValue}"`;
|
|
1658
|
+
})
|
|
1659
|
+
.join('\n');
|
|
1660
|
+
|
|
1661
|
+
this.logger.info(`🔌 Production build: Embedding environment variables in Dockerfile`);
|
|
1662
|
+
this.logger.info(` - DOCKER_PORT: ${env.DOCKER_PORT || 'not set'}`);
|
|
1663
|
+
}
|
|
1551
1664
|
|
|
1552
1665
|
// Create Dockerfile for agent
|
|
1553
1666
|
const dockerfile = `FROM ${baseImageName}
|
|
1554
1667
|
COPY agent-code/ /app/agent-code/
|
|
1668
|
+
${envStatements}
|
|
1555
1669
|
USER dankuser
|
|
1556
1670
|
`;
|
|
1557
1671
|
|
|
@@ -1564,6 +1678,7 @@ USER dankuser
|
|
|
1564
1678
|
// Create basic agent code structure
|
|
1565
1679
|
// Generate handlers from agent configuration
|
|
1566
1680
|
const handlersCode = this.generateHandlersCode(agent);
|
|
1681
|
+
const routesCode = this.generateRoutesCode(agent);
|
|
1567
1682
|
|
|
1568
1683
|
const agentCode = `
|
|
1569
1684
|
// Agent: ${agent.name}
|
|
@@ -1605,7 +1720,8 @@ module.exports = {
|
|
|
1605
1720
|
}, 10000);
|
|
1606
1721
|
},
|
|
1607
1722
|
|
|
1608
|
-
handlers: ${handlersCode}
|
|
1723
|
+
handlers: ${handlersCode},
|
|
1724
|
+
routes: ${routesCode}
|
|
1609
1725
|
};
|
|
1610
1726
|
`;
|
|
1611
1727
|
|
|
@@ -1632,19 +1748,21 @@ module.exports = {
|
|
|
1632
1748
|
|
|
1633
1749
|
// Always bind the main agent port
|
|
1634
1750
|
const mainPort = agent.config.docker?.port || DOCKER_CONFIG.defaultPort;
|
|
1751
|
+
this.logger.info(`🔌 Binding main agent port: ${mainPort} (from docker.port: ${agent.config.docker?.port || 'default'})`);
|
|
1635
1752
|
portBindings[`${mainPort}/tcp`] = [{ HostPort: mainPort.toString() }];
|
|
1636
1753
|
|
|
1637
|
-
//
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1754
|
+
// Only bind HTTP port if HTTP is explicitly enabled AND different from main port
|
|
1755
|
+
// Check both http.enabled and communication.httpApi.enabled to be safe
|
|
1756
|
+
const httpEnabled = agent.config.http?.enabled === true;
|
|
1757
|
+
const httpApiEnabled = agent.config.communication?.httpApi?.enabled === true;
|
|
1758
|
+
if (httpEnabled || httpApiEnabled) {
|
|
1759
|
+
const httpPort = agent.config.http?.port;
|
|
1760
|
+
if (httpPort && httpPort !== mainPort) {
|
|
1641
1761
|
portBindings[`${httpPort}/tcp`] = [{ HostPort: httpPort.toString() }];
|
|
1642
1762
|
}
|
|
1643
1763
|
}
|
|
1644
1764
|
|
|
1645
|
-
//
|
|
1646
|
-
const healthPort = DOCKER_CONFIG.healthCheckPort;
|
|
1647
|
-
portBindings[`${healthPort}/tcp`] = [{ HostPort: healthPort.toString() }];
|
|
1765
|
+
// Health check uses the same port as the main agent port, so no separate binding needed
|
|
1648
1766
|
|
|
1649
1767
|
portConfig.PortBindings = portBindings;
|
|
1650
1768
|
return portConfig;
|
|
@@ -1660,17 +1778,18 @@ module.exports = {
|
|
|
1660
1778
|
const mainPort = agent.config.docker?.port || DOCKER_CONFIG.defaultPort;
|
|
1661
1779
|
exposedPorts[`${mainPort}/tcp`] = {};
|
|
1662
1780
|
|
|
1663
|
-
//
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1781
|
+
// Only expose HTTP port if HTTP is explicitly enabled AND different from main port
|
|
1782
|
+
// Check both http.enabled and communication.httpApi.enabled to be safe
|
|
1783
|
+
const httpEnabled = agent.config.http?.enabled === true;
|
|
1784
|
+
const httpApiEnabled = agent.config.communication?.httpApi?.enabled === true;
|
|
1785
|
+
if (httpEnabled || httpApiEnabled) {
|
|
1786
|
+
const httpPort = agent.config.http?.port;
|
|
1787
|
+
if (httpPort && httpPort !== mainPort) {
|
|
1667
1788
|
exposedPorts[`${httpPort}/tcp`] = {};
|
|
1668
1789
|
}
|
|
1669
1790
|
}
|
|
1670
1791
|
|
|
1671
|
-
//
|
|
1672
|
-
const healthPort = DOCKER_CONFIG.healthCheckPort;
|
|
1673
|
-
exposedPorts[`${healthPort}/tcp`] = {};
|
|
1792
|
+
// Health check uses the same port as the main agent port, so no separate exposure needed
|
|
1674
1793
|
|
|
1675
1794
|
return { ExposedPorts: exposedPorts };
|
|
1676
1795
|
}
|
package/lib/project.js
CHANGED
|
@@ -85,10 +85,7 @@ module.exports = {
|
|
|
85
85
|
protocol: 'http',
|
|
86
86
|
port: 3000
|
|
87
87
|
})
|
|
88
|
-
.
|
|
89
|
-
memory: '512m',
|
|
90
|
-
cpu: 1
|
|
91
|
-
})
|
|
88
|
+
.setInstanceType('small')
|
|
92
89
|
// Event handlers for prompt modification and response enhancement
|
|
93
90
|
.addHandler('request_output:start', (data) => {
|
|
94
91
|
console.log('[Prompt Agent] Processing prompt:', data.conversationId);
|