init project

This commit is contained in:
root
2026-07-31 13:12:54 -04:00
parent 0da92d5e02
commit f3863f760c
7215 changed files with 1860260 additions and 1 deletions
+112
View File
@@ -0,0 +1,112 @@
import { z } from 'zod';
import { config } from '../../config.js';
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
import { dockerManager, isAllowListedImage, isLocallyBuiltImage, validateVolumeMount } from './dockerManager.js';
export function registerDockerRunContainerTool(server) {
server.registerTool('docker_run_container', {
title: 'Docker Run Container',
description: 'Run a Docker container from an image with port mappings, env vars, and workspace-confined volume mounts. ' +
'Locally built images are allowed; non-local images must be on the allow-list unless confirm: true is passed.',
inputSchema: {
image: z.string().min(1).describe('Docker image to run, e.g. "my-app:latest" or "nginx".'),
name: z.string().optional().describe('Optional container name.'),
ports: z
.record(z.number().int().positive())
.optional()
.describe('Port mapping { hostPort: containerPort }.'),
env: z.record(z.string()).optional().describe('Environment variables.'),
volumes: z
.array(z.string())
.optional()
.describe('Volume mounts in "source:target[:mode]" format. Source paths are confined to WORKSPACE_ROOT.'),
command: z.array(z.string()).optional().describe('Override the container command.'),
memory: z.string().optional().describe('Memory limit, e.g. "512m". Defaults to 512m.'),
cpus: z.string().optional().describe('CPU limit, e.g. "1.0". Defaults to 1.0.'),
confirm: z
.boolean()
.optional()
.describe('Required as true to run an image not on the allow-list.'),
},
}, async ({ image, name, ports, env, volumes, command, memory, cpus, confirm }) => {
try {
await dockerManager.ensureReachable();
if (!isLocallyBuiltImage(image) && !isAllowListedImage(image) && confirm !== true) {
return errorResult(`Image "${image}" is not locally built and not on the allow-list (${config.dockerAllowedBaseImages.join(', ')}). ` +
'Pass confirm: true to run it anyway.');
}
const portBindings = {};
const exposedPorts = {};
const portMap = {};
if (ports) {
for (const [hostPort, containerPort] of Object.entries(ports)) {
const key = `${containerPort}/tcp`;
portBindings[key] = [{ HostPort: hostPort }];
exposedPorts[key] = {};
portMap[Number(hostPort)] = containerPort;
}
}
const binds = [];
if (volumes) {
for (const spec of volumes) {
const mount = validateVolumeMount(spec);
binds.push(`${mount.source}:${mount.target}:${mount.mode}`);
}
}
const createOptions = {
Image: image,
name,
Cmd: command,
Env: env ? Object.entries(env).map(([k, v]) => `${k}=${v}`) : undefined,
ExposedPorts: Object.keys(exposedPorts).length > 0 ? exposedPorts : undefined,
HostConfig: {
PortBindings: Object.keys(portBindings).length > 0 ? portBindings : undefined,
Binds: binds.length > 0 ? binds : undefined,
Memory: parseMemory(memory ?? config.defaultContainerMemory),
NanoCpus: parseCpus(cpus ?? config.defaultContainerCpus),
// Security guardrails:
Privileged: false,
NetworkMode: 'bridge',
PidMode: undefined,
IpcMode: undefined,
},
};
const docker = dockerManager.getDocker();
const container = await docker.createContainer(createOptions);
await container.start();
const info = await container.inspect();
const managed = dockerManager.track(container.id, image, info.Name?.replace(/^\//, '') ?? container.id, portMap);
return jsonResult({ ...managed, shortId: container.id.substring(0, 12) });
}
catch (error) {
return errorResult(toErrorMessage(error));
}
});
}
function parseMemory(value) {
const match = value.match(/^([0-9.]+)([kmgt]?b?)$/i);
if (!match || !match[1])
return 512 * 1024 * 1024;
const num = Number.parseFloat(match[1]);
const unit = match[2]?.toLowerCase() ?? '';
switch (unit) {
case 'k':
case 'kb':
return num * 1024;
case 'm':
case 'mb':
return num * 1024 * 1024;
case 'g':
case 'gb':
return num * 1024 * 1024 * 1024;
case 't':
case 'tb':
return num * 1024 * 1024 * 1024 * 1024;
default:
return num;
}
}
function parseCpus(value) {
const num = Number.parseFloat(value);
return Number.isNaN(num) ? 1_000_000_000 : Math.round(num * 1_000_000_000);
}
//# sourceMappingURL=runContainer.js.map