feat(setup): per-checkout service name and docker image tag

Two NanoClaw installs on the same host used to fight over the shared `com.nanoclaw` launchd label / `nanoclaw.service` systemd unit and the `nanoclaw-agent:latest` docker tag — the second install silently rewrote the service pointer and rebuilt the image out from under the first. Introduces a deterministic per-checkout slug (sha1(projectRoot)[:8]) and namespaces everything off it:

- Service: `com.nanoclaw-v2-<slug>` / `nanoclaw-v2-<slug>.service`
- Image:   `nanoclaw-agent-v2-<slug>:latest` (base), `nanoclaw-agent-v2-<slug>:<agentGroupId>` (per-group)

New shared helpers: src/install-slug.ts (host) + setup/lib/install-slug.sh (bash). Both compute the same slug so verify/probe/add-*.sh/build.sh/container-runner all agree. Any v1 `com.nanoclaw` service left on the host stays untouched and can coexist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-23 10:10:09 +03:00
parent 4f6d62a65e
commit 7a9401ddf2
15 changed files with 156 additions and 44 deletions

View File

@@ -2,6 +2,7 @@ import os from 'os';
import path from 'path';
import { readEnvFile } from './env.js';
import { getContainerImageBase, getDefaultContainerImage } from './install-slug.js';
import { isValidTimezone } from './timezone.js';
// Read config values from .env (falls back to process.env).
@@ -22,7 +23,12 @@ export const STORE_DIR = path.resolve(PROJECT_ROOT, 'store');
export const GROUPS_DIR = path.resolve(PROJECT_ROOT, 'groups');
export const DATA_DIR = path.resolve(PROJECT_ROOT, 'data');
export const CONTAINER_IMAGE = process.env.CONTAINER_IMAGE || 'nanoclaw-agent:latest';
// Per-checkout image tag so two installs on the same host don't share
// `nanoclaw-agent:latest` and clobber each other on rebuild.
export const CONTAINER_IMAGE_BASE =
process.env.CONTAINER_IMAGE_BASE || getContainerImageBase(PROJECT_ROOT);
export const CONTAINER_IMAGE =
process.env.CONTAINER_IMAGE || getDefaultContainerImage(PROJECT_ROOT);
export const CONTAINER_TIMEOUT = parseInt(process.env.CONTAINER_TIMEOUT || '1800000', 10);
export const CONTAINER_MAX_OUTPUT_SIZE = parseInt(process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760', 10); // 10MB default
export const ONECLI_URL = process.env.ONECLI_URL || envConfig.ONECLI_URL;

View File

@@ -9,7 +9,7 @@ import path from 'path';
import { OneCLI } from '@onecli-sh/sdk';
import { CONTAINER_IMAGE, DATA_DIR, GROUPS_DIR, ONECLI_API_KEY, ONECLI_URL, TIMEZONE } from './config.js';
import { CONTAINER_IMAGE, CONTAINER_IMAGE_BASE, DATA_DIR, GROUPS_DIR, ONECLI_API_KEY, ONECLI_URL, TIMEZONE } from './config.js';
import { readContainerConfig, writeContainerConfig } from './container-config.js';
import { CONTAINER_RUNTIME_BIN, hostGatewayArgs, readonlyMountArgs, stopContainer } from './container-runtime.js';
import { composeGroupClaudeMd } from './claude-md-compose.js';
@@ -469,7 +469,7 @@ export async function buildAgentGroupImage(agentGroupId: string): Promise<void>
}
dockerfile += 'USER node\n';
const imageTag = `nanoclaw-agent:${agentGroupId}`;
const imageTag = `${CONTAINER_IMAGE_BASE}:${agentGroupId}`;
log.info('Building per-agent-group image', { agentGroupId, imageTag, apt: aptPackages, npm: npmPackages });

33
src/install-slug.ts Normal file
View File

@@ -0,0 +1,33 @@
/**
* Per-checkout install identifiers. Lets two NanoClaw installs coexist on
* one host without clobbering each other's service registration or the
* shared `nanoclaw-agent:latest` docker image tag.
*
* Slug is sha1(projectRoot)[:8] — deterministic per checkout path, stable
* across re-runs, unique enough across installs.
*/
import { createHash } from 'crypto';
export function getInstallSlug(projectRoot: string = process.cwd()): string {
return createHash('sha1').update(projectRoot).digest('hex').slice(0, 8);
}
/** launchd Label + plist basename. e.g. `com.nanoclaw-v2-ab12cd34`. */
export function getLaunchdLabel(projectRoot?: string): string {
return `com.nanoclaw-v2-${getInstallSlug(projectRoot)}`;
}
/** systemd unit name (no .service suffix). e.g. `nanoclaw-v2-ab12cd34`. */
export function getSystemdUnit(projectRoot?: string): string {
return `nanoclaw-v2-${getInstallSlug(projectRoot)}`;
}
/** Docker image base (no tag). e.g. `nanoclaw-agent-v2-ab12cd34`. */
export function getContainerImageBase(projectRoot?: string): string {
return `nanoclaw-agent-v2-${getInstallSlug(projectRoot)}`;
}
/** Default full container image reference with `:latest` tag. */
export function getDefaultContainerImage(projectRoot?: string): string {
return `${getContainerImageBase(projectRoot)}:latest`;
}