mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-18 18:29:35 +08:00
adfae67611
The agent's global Node CLIs (claude-code, agent-browser, vercel) were each
a hardcoded ARG + RUN layer in the Dockerfile, so adding or bumping one meant
editing the Dockerfile — a code reach-in every tool-installing skill had to make.
Move the tool list into container/cli-tools.json. A skill now adds a CLI by
appending a {name, version} entry (a json-merge) — the safest change shape:
deterministic, idempotent, removable. install-cli-tools.sh parses the manifest
with node (no new jq dep), writes the per-tool only-built-dependencies opt-ins,
and runs one pinned `pnpm install -g`, so the pnpm supply-chain path is unchanged.
Behavior is byte-for-byte: same opt-ins, same pinned installs. agent-browser is
now pinned (0.27.1, what `latest` last resolved to) instead of floating.
container/cli-tools.test.ts guards the seam: red if a baseline tool is dropped,
a version unpins, or the Dockerfile wiring / pnpm path is removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
62 lines
2.5 KiB
TypeScript
62 lines
2.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
// Guards the cli-tools.json seam: the global CLIs the agent invokes at runtime
|
|
// are installed from the manifest (a skill adds one with a json-merge), not
|
|
// hand-edited into the Dockerfile. These go red on a bad merge that drops a
|
|
// baseline tool, or on dewiring the Dockerfile / switching the installer off
|
|
// the pnpm supply-chain path.
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const manifest = JSON.parse(readFileSync(join(here, 'cli-tools.json'), 'utf8')) as Array<{
|
|
name: string;
|
|
version: string;
|
|
onlyBuilt?: boolean;
|
|
}>;
|
|
const dockerfile = readFileSync(join(here, 'Dockerfile'), 'utf8');
|
|
const installer = readFileSync(join(here, 'install-cli-tools.sh'), 'utf8');
|
|
|
|
describe('cli-tools manifest', () => {
|
|
it('is a non-empty array of { name, version }', () => {
|
|
expect(Array.isArray(manifest)).toBe(true);
|
|
expect(manifest.length).toBeGreaterThan(0);
|
|
for (const tool of manifest) {
|
|
expect(typeof tool.name).toBe('string');
|
|
expect(tool.name.length).toBeGreaterThan(0);
|
|
expect(typeof tool.version).toBe('string');
|
|
expect(tool.version.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('has unique tool names (json-merge is keyed on name)', () => {
|
|
const names = manifest.map((t) => t.name);
|
|
expect(new Set(names).size).toBe(names.length);
|
|
});
|
|
|
|
it('pins every version to an exact semver (no latest, no ranges — supply-chain policy)', () => {
|
|
for (const tool of manifest) {
|
|
expect(tool.version, `${tool.name} must be an exact semver, not "${tool.version}"`).toMatch(
|
|
/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('keeps the baseline CLIs the agent depends on', () => {
|
|
const names = manifest.map((t) => t.name);
|
|
for (const required of ['vercel', 'agent-browser', '@anthropic-ai/claude-code']) {
|
|
expect(names).toContain(required);
|
|
}
|
|
});
|
|
|
|
it('is wired into the Dockerfile build (COPY manifest + run installer)', () => {
|
|
expect(dockerfile).toMatch(/COPY cli-tools\.json install-cli-tools\.sh/);
|
|
expect(dockerfile).toMatch(/install-cli-tools\.sh \/tmp\/cli-tools\.json/);
|
|
});
|
|
|
|
it('installs via pnpm and writes only-built opt-ins (preserves the supply-chain path)', () => {
|
|
expect(installer).toMatch(/pnpm install -g/);
|
|
expect(installer).toMatch(/only-built-dependencies\[\]=/);
|
|
});
|
|
});
|