mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat(docker): agent base image + local build script
docker/agent.Dockerfile: node:22 + claude/codex/gemini/opencode CLIs + git/ tmux/ripgrep/curl, secret-free, OpenShift arbitrary-uid-writable HOME (gid 0). scripts/build-agent-image.mjs: local build (decision "build locally on first use"), docker/podman auto-detect, --engine/--image/--no-cache flags. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
# Codeman agent base image (built locally by scripts/build-agent-image.mjs).
|
||||||
|
#
|
||||||
|
# Contains the agent toolchain (node + the CLIs + git/tmux/ripgrep) but NO
|
||||||
|
# secrets: credentials are delivered at RUNTIME via bind mounts (~/.claude etc.)
|
||||||
|
# or name-only `docker exec --env`, never baked in, so `docker save` exports stay
|
||||||
|
# secret-free. tmux is a HARD prerequisite (the in-container tmux is what makes a
|
||||||
|
# reconnect durable), so it is installed here and probed before launch.
|
||||||
|
#
|
||||||
|
# HOME is made writable by an ARBITRARY host uid via the OpenShift "gid 0,
|
||||||
|
# group-writable" convention: on Linux we run `--user <hostUid>:0`, so the agent
|
||||||
|
# uid is the host uid (workspace files stay host-owned) while gid 0 keeps $HOME
|
||||||
|
# writable even though the uid is not the baked 1000.
|
||||||
|
FROM node:22-bookworm-slim
|
||||||
|
|
||||||
|
# Base toolchain. `curl` is needed for the hook callbacks (`curl -sk $CODEMAN_API_URL`),
|
||||||
|
# `procps` for `ps`, `tmux` for the durable in-container session.
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
git \
|
||||||
|
tmux \
|
||||||
|
ripgrep \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
less \
|
||||||
|
procps \
|
||||||
|
openssh-client \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# The agent CLIs (all four backends Codeman supports). Pinning is left to the
|
||||||
|
# rebuild cadence (see docs/docker-cases-plan.md, user-decision 2).
|
||||||
|
RUN npm install -g \
|
||||||
|
@anthropic-ai/claude-code \
|
||||||
|
@openai/codex \
|
||||||
|
@google/gemini-cli \
|
||||||
|
opencode-ai \
|
||||||
|
&& npm cache clean --force
|
||||||
|
|
||||||
|
# `agent` user (uid 1000, gid 0) with an arbitrary-uid-writable HOME.
|
||||||
|
ENV HOME=/home/agent
|
||||||
|
RUN useradd -u 1000 -g 0 -m -d /home/agent -s /bin/bash agent \
|
||||||
|
&& mkdir -p /home/agent/.npm /home/agent/.cache /home/agent/.config /home/agent/.codeman \
|
||||||
|
&& chgrp -R 0 /home/agent \
|
||||||
|
&& chmod -R g=u /home/agent
|
||||||
|
|
||||||
|
USER 1000:0
|
||||||
|
WORKDIR /home/agent
|
||||||
|
|
||||||
|
# Codeman overrides the command with `sleep infinity` at create time; this is the
|
||||||
|
# fallback so a hand-run container also idles rather than exiting.
|
||||||
|
CMD ["sleep", "infinity"]
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Build the Codeman agent base image locally (decision: "build locally on first
|
||||||
|
* use", see docs/docker-cases-plan.md). No registry account required.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/build-agent-image.mjs [--engine docker|podman] [--image <ref>] [--no-cache]
|
||||||
|
*
|
||||||
|
* Defaults: engine=docker (falls back to podman if docker is absent),
|
||||||
|
* image=codeman/agent:base
|
||||||
|
*/
|
||||||
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const REPO_ROOT = join(__dirname, '..');
|
||||||
|
const DOCKERFILE = join(REPO_ROOT, 'docker', 'agent.Dockerfile');
|
||||||
|
const DEFAULT_IMAGE = 'codeman/agent:base';
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = { image: DEFAULT_IMAGE, engine: undefined, noCache: false };
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const a = argv[i];
|
||||||
|
if (a === '--image') args.image = argv[++i];
|
||||||
|
else if (a === '--engine') args.engine = argv[++i];
|
||||||
|
else if (a === '--no-cache') args.noCache = true;
|
||||||
|
else if (a === '-h' || a === '--help') args.help = true;
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function engineAvailable(engine) {
|
||||||
|
const r = spawnSync(engine, ['--version'], { stdio: 'ignore' });
|
||||||
|
return r.status === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveEngine(preferred) {
|
||||||
|
if (preferred) {
|
||||||
|
if (!engineAvailable(preferred)) {
|
||||||
|
console.error(`[build-agent-image] engine "${preferred}" not found on PATH`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
return preferred;
|
||||||
|
}
|
||||||
|
if (engineAvailable('docker')) return 'docker';
|
||||||
|
if (engineAvailable('podman')) return 'podman';
|
||||||
|
console.error('[build-agent-image] neither docker nor podman found on PATH. Install one and retry.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
if (args.help) {
|
||||||
|
console.log('Usage: node scripts/build-agent-image.mjs [--engine docker|podman] [--image <ref>] [--no-cache]');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const engine = resolveEngine(args.engine);
|
||||||
|
const buildArgs = ['build', '-f', DOCKERFILE, '-t', args.image];
|
||||||
|
if (args.noCache) buildArgs.push('--no-cache');
|
||||||
|
buildArgs.push(REPO_ROOT);
|
||||||
|
|
||||||
|
console.log(`[build-agent-image] ${engine} ${buildArgs.join(' ')}`);
|
||||||
|
const child = spawn(engine, buildArgs, { stdio: 'inherit' });
|
||||||
|
child.on('exit', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
console.log(`\n[build-agent-image] built ${args.image}. Docker cases can now launch.`);
|
||||||
|
} else {
|
||||||
|
console.error(`\n[build-agent-image] build failed (exit ${code}).`);
|
||||||
|
}
|
||||||
|
process.exit(code ?? 1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user