merge: resolve conflict with origin/main

Keep ASSISTANT_NAME import, drop removed GROUPS_DIR import.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-23 00:03:20 +02:00
56 changed files with 2909 additions and 1539 deletions

View File

@@ -4,7 +4,6 @@
*/
import { ChildProcess, exec, spawn } from 'child_process';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {
@@ -14,8 +13,10 @@ import {
DATA_DIR,
GROUPS_DIR,
IDLE_TIMEOUT,
TIMEZONE,
} from './config.js';
import { readEnvFile } from './env.js';
import { resolveGroupFolderPath, resolveGroupIpcPath } from './group-folder.js';
import { logger } from './logger.js';
import { CONTAINER_RUNTIME_BIN, readonlyMountArgs, stopContainer } from './container-runtime.js';
import { validateAdditionalMounts } from './mount-security.js';
@@ -25,16 +26,6 @@ import { RegisteredGroup } from './types.js';
const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---';
const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---';
function getHomeDir(): string {
const home = process.env.HOME || os.homedir();
if (!home) {
throw new Error(
'Unable to determine home directory: HOME environment variable is not set and os.homedir() returned empty',
);
}
return home;
}
export interface ContainerInput {
prompt: string;
sessionId?: string;
@@ -64,27 +55,31 @@ function buildVolumeMounts(
isMain: boolean,
): VolumeMount[] {
const mounts: VolumeMount[] = [];
const homeDir = getHomeDir();
const projectRoot = process.cwd();
const groupDir = resolveGroupFolderPath(group.folder);
if (isMain) {
// Main gets the entire project root mounted
// Main gets the project root read-only. Writable paths the agent needs
// (group folder, IPC, .claude/) are mounted separately below.
// Read-only prevents the agent from modifying host application code
// (src/, dist/, package.json, etc.) which would bypass the sandbox
// entirely on next restart.
mounts.push({
hostPath: projectRoot,
containerPath: '/workspace/project',
readonly: false,
readonly: true,
});
// Main also gets its group folder as the working directory
mounts.push({
hostPath: path.join(GROUPS_DIR, group.folder),
hostPath: groupDir,
containerPath: '/workspace/group',
readonly: false,
});
} else {
// Other groups only get their own folder
mounts.push({
hostPath: path.join(GROUPS_DIR, group.folder),
hostPath: groupDir,
containerPath: '/workspace/group',
readonly: false,
});
@@ -146,7 +141,7 @@ function buildVolumeMounts(
// Per-group IPC namespace: each group gets its own IPC directory
// This prevents cross-group privilege escalation via IPC
const groupIpcDir = path.join(DATA_DIR, 'ipc', group.folder);
const groupIpcDir = resolveGroupIpcPath(group.folder);
fs.mkdirSync(path.join(groupIpcDir, 'messages'), { recursive: true });
fs.mkdirSync(path.join(groupIpcDir, 'tasks'), { recursive: true });
fs.mkdirSync(path.join(groupIpcDir, 'input'), { recursive: true });
@@ -156,13 +151,18 @@ function buildVolumeMounts(
readonly: false,
});
// Mount agent-runner source from host — recompiled on container startup.
// Bypasses sticky build cache for code changes.
// Copy agent-runner source into a per-group writable location so agents
// can customize it (add tools, change behavior) without affecting other
// groups. Recompiled on container startup via entrypoint.sh.
const agentRunnerSrc = path.join(projectRoot, 'container', 'agent-runner', 'src');
const groupAgentRunnerDir = path.join(DATA_DIR, 'sessions', group.folder, 'agent-runner-src');
if (!fs.existsSync(groupAgentRunnerDir) && fs.existsSync(agentRunnerSrc)) {
fs.cpSync(agentRunnerSrc, groupAgentRunnerDir, { recursive: true });
}
mounts.push({
hostPath: agentRunnerSrc,
hostPath: groupAgentRunnerDir,
containerPath: '/app/src',
readonly: true,
readonly: false,
});
// Additional mounts validated against external allowlist (tamper-proof from containers)
@@ -189,6 +189,9 @@ function readSecrets(): Record<string, string> {
function buildContainerArgs(mounts: VolumeMount[], containerName: string): string[] {
const args: string[] = ['run', '-i', '--rm', '--name', containerName];
// Pass host timezone so container's local time matches the user's
args.push('-e', `TZ=${TIMEZONE}`);
// Run as host user so bind-mounted files are accessible.
// Skip when running as root (uid 0), as the container's node user (uid 1000),
// or when getuid is unavailable (native Windows without WSL).
@@ -220,7 +223,7 @@ export async function runContainerAgent(
): Promise<ContainerOutput> {
const startTime = Date.now();
const groupDir = path.join(GROUPS_DIR, group.folder);
const groupDir = resolveGroupFolderPath(group.folder);
fs.mkdirSync(groupDir, { recursive: true });
const mounts = buildVolumeMounts(group, input.isMain);
@@ -251,7 +254,7 @@ export async function runContainerAgent(
'Spawning container agent',
);
const logsDir = path.join(GROUPS_DIR, group.folder, 'logs');
const logsDir = path.join(groupDir, 'logs');
fs.mkdirSync(logsDir, { recursive: true });
return new Promise((resolve) => {
@@ -595,7 +598,7 @@ export function writeTasksSnapshot(
}>,
): void {
// Write filtered tasks to the group's IPC directory
const groupIpcDir = path.join(DATA_DIR, 'ipc', groupFolder);
const groupIpcDir = resolveGroupIpcPath(groupFolder);
fs.mkdirSync(groupIpcDir, { recursive: true });
// Main sees all tasks, others only see their own
@@ -625,7 +628,7 @@ export function writeGroupsSnapshot(
groups: AvailableGroup[],
registeredJids: Set<string>,
): void {
const groupIpcDir = path.join(DATA_DIR, 'ipc', groupFolder);
const groupIpcDir = resolveGroupIpcPath(groupFolder);
fs.mkdirSync(groupIpcDir, { recursive: true });
// Main sees all groups; others see nothing (they can't activate groups)