mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
docker/agent.Dockerfile hardcoded the four npm-published CLIs it installs, one of the several lists that had to be kept in step with the registry by hand. It now takes them as `ARG CLI_NPM_PACKAGES`, supplied by scripts/build-agent-image.mjs from config/clis.stock.json, with the default set to today's list so a bare `docker build` still produces the same image. The arg is expanded unquoted because word splitting is what turns the list into several arguments, which is exactly why every token is validated against ^[@A-Za-z0-9][@A-Za-z0-9/._-]*$ on the producing side; a package name carrying a space or a metacharacter is refused rather than reaching the RUN line. Verified by building the layer: four packages in, four arguments out, and the default still applies with no arg. The list is filtered on each entry's `enabled` flag — the field whose absence was the maintainer's §3 finding, where a CLI shipping disabled still got baked into every image. No stock entry is disabled today, so that assertion would pass vacuously; a unit test feeds the pure helper a fabricated disabled entry so the fix is covered now rather than the first time someone ships one. ⚠️ It reads the STOCK catalogue, never the merged registry. A user's ~/.codeman/clis.json must not change what is inside an image tagged codeman/agent:base, or two machines holding that tag hold different images. Four CLIs keep hand-written layers because the registry cannot describe what makes them special: pi's --ignore-scripts, deepseek's pnpm companion and dsh-tui profile, and the three standalone installers. Rather than extend the schema for a Docker-only benefit, the coverage test requires each to carry a written reason AND still be present, so an exclusion cannot quietly become an omission. There are two producers of this command line and there have to be — the .mjs cannot import TypeScript, and src/docker-hosts.ts builds the same argv for the in-app auto-build — so a parity test pins them together, package list, arg pairs and rendered argv. Their order is pinned too: a different order is a different RUN string and so a needless cache miss between the two build paths. docker/server.Dockerfile is deliberately NOT edited (PRs #373 and #377 both modify it); its narrower list is asserted as a declared omission list instead, so the divergence is reviewable without touching the file. Also fixes the in-app hint at index.html, which the new coverage test caught still omitting omp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015EMxQreQUZX5ZyybxAGh12
64 lines
2.9 KiB
JavaScript
64 lines
2.9 KiB
JavaScript
/**
|
|
* @fileoverview Reads the generated CLI catalogue for the Docker build.
|
|
*
|
|
* `scripts/build-agent-image.mjs` is a `.mjs` and cannot import the TypeScript registry, so it
|
|
* reads `config/clis.stock.json` (generated by `scripts/generate-cli-catalog.mts`) instead.
|
|
* The pure half lives here so `src/docker-hosts.ts`'s programmatic mirror of the same build
|
|
* command can be pinned against it by a test — those two produce the docker argv independently
|
|
* and must not drift.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const CATALOG_PATH = fileURLToPath(new URL('../../config/clis.stock.json', import.meta.url));
|
|
|
|
/**
|
|
* npm package names the AGENT image installs in its shared `npm install -g` layer.
|
|
*
|
|
* PURE: takes the parsed catalogue, returns a sorted-by-registry-order list.
|
|
*
|
|
* ⚠️ Filters on `enabled`. That is the field the earlier attempt's export omitted, which is
|
|
* how a CLI that ships disabled still had its package baked into every image.
|
|
*
|
|
* ⚠️ SPECIAL_CASES are excluded here and installed by their own hand-written Dockerfile
|
|
* layers, because the registry cannot express what makes them special — a flag, a companion
|
|
* package, or not being on npm at all. `test/docker-agent-image-coverage.test.ts` requires
|
|
* every one of them to carry a reason and to still be present in the Dockerfile, so an
|
|
* exclusion cannot quietly become an omission.
|
|
*/
|
|
export const AGENT_IMAGE_SPECIAL_CASES = {
|
|
pi: 'installed with --ignore-scripts in its own layer, so the flag cannot leak to the shared block',
|
|
deepseek: 'needs pnpm alongside it (dsh plugin, issue #352) and a dsh-tui profile install',
|
|
};
|
|
|
|
/** Tokens allowed in an npm package name reaching a Dockerfile build arg unquoted. */
|
|
const SAFE_PACKAGE = /^[@A-Za-z0-9][@A-Za-z0-9/._-]*$/;
|
|
|
|
export function agentImageNpmPackages(catalog) {
|
|
const packages = [];
|
|
for (const entry of catalog) {
|
|
if (!entry.enabled) continue;
|
|
if (entry.id in AGENT_IMAGE_SPECIAL_CASES) continue;
|
|
const pkg = entry.discovery?.install?.npmPackage;
|
|
if (!pkg) continue; // antigravity/grok/omp ship standalone installers, not npm
|
|
if (!SAFE_PACKAGE.test(pkg)) {
|
|
// The value is interpolated into a Dockerfile ARG that is expanded UNQUOTED (word
|
|
// splitting is how the list becomes several arguments), so a token with whitespace or
|
|
// shell metacharacters would change what the RUN line means.
|
|
throw new Error(`Refusing unsafe npm package name for "${entry.id}": ${JSON.stringify(pkg)}`);
|
|
}
|
|
packages.push(pkg);
|
|
}
|
|
return packages;
|
|
}
|
|
|
|
/** The `--build-arg` pairs the agent image takes. PURE. */
|
|
export function agentImageBuildArgPairs(catalog) {
|
|
return [['CLI_NPM_PACKAGES', agentImageNpmPackages(catalog).join(' ')]];
|
|
}
|
|
|
|
/** Read the committed catalogue. IO. */
|
|
export function readCatalog(path = CATALOG_PATH) {
|
|
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
}
|