mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-06 18:52:03 +08:00
31ccc61b27
Source of truth for container runtime config moves from
groups/<folder>/container.json to a new container_configs table.
The file becomes a materialized view written at spawn time.
- New container_configs table with scalar columns (provider, model,
effort, image_tag, assistant_name, max_messages_per_prompt) and
JSON columns (mcp_servers, packages_apt, packages_npm, skills,
additional_mounts)
- Startup backfill seeds DB from existing container.json files
- materializeContainerJson() replaces readContainerConfig + ensureRuntimeFields
- Self-mod handlers (install_packages, add_mcp_server) write to DB
- Provider cascade simplified: session -> container_configs -> 'claude'
- ncl groups config-{get,update,add-mcp-server,remove-mcp-server,
add-package,remove-package} custom operations
- restartAgentGroupContainers() helper for config change propagation
- Container side unchanged (still reads /workspace/agent/container.json)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
134 lines
4.6 KiB
TypeScript
134 lines
4.6 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { DATA_DIR, GROUPS_DIR } from './config.js';
|
|
import { ensureContainerConfig } from './db/container-configs.js';
|
|
import { log } from './log.js';
|
|
import type { AgentGroup } from './types.js';
|
|
|
|
const DEFAULT_SETTINGS_JSON =
|
|
JSON.stringify(
|
|
{
|
|
env: {
|
|
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
|
|
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1',
|
|
CLAUDE_CODE_DISABLE_AUTO_MEMORY: '0',
|
|
},
|
|
hooks: {
|
|
PreCompact: [
|
|
{
|
|
hooks: [
|
|
{
|
|
type: 'command',
|
|
command: 'bun /app/src/compact-instructions.ts',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
) + '\n';
|
|
|
|
/**
|
|
* Initialize the on-disk filesystem state for an agent group. Idempotent —
|
|
* every step is gated on the target not already existing, so re-running on
|
|
* an already-initialized group is a no-op.
|
|
*
|
|
* Called once per group lifetime at creation, or defensively from
|
|
* `buildMounts()` for groups that pre-date this code path.
|
|
*
|
|
* Source code and skills are shared RO mounts — not copied per-group.
|
|
* Skill symlinks are synced at spawn time by container-runner.ts.
|
|
*
|
|
* The composed `CLAUDE.md` is NOT written here — it's regenerated on every
|
|
* spawn by `composeGroupClaudeMd()` (see `claude-md-compose.ts`). Initial
|
|
* per-group instructions (if provided) seed `CLAUDE.local.md`.
|
|
*/
|
|
export function initGroupFilesystem(group: AgentGroup, opts?: { instructions?: string }): void {
|
|
const initialized: string[] = [];
|
|
|
|
// 1. groups/<folder>/ — group memory + working dir
|
|
const groupDir = path.resolve(GROUPS_DIR, group.folder);
|
|
if (!fs.existsSync(groupDir)) {
|
|
fs.mkdirSync(groupDir, { recursive: true });
|
|
initialized.push('groupDir');
|
|
}
|
|
|
|
// groups/<folder>/CLAUDE.local.md — per-group agent memory, auto-loaded by
|
|
// Claude Code. Seeded with caller-provided instructions on first creation.
|
|
const claudeLocalFile = path.join(groupDir, 'CLAUDE.local.md');
|
|
if (!fs.existsSync(claudeLocalFile)) {
|
|
const body = opts?.instructions ? opts.instructions + '\n' : '';
|
|
fs.writeFileSync(claudeLocalFile, body);
|
|
initialized.push('CLAUDE.local.md');
|
|
}
|
|
|
|
// Ensure container_configs row exists in the DB. Idempotent — no-op if
|
|
// the row already exists (e.g. created by backfill or group creation).
|
|
ensureContainerConfig(group.id);
|
|
initialized.push('container_configs');
|
|
|
|
// 2. data/v2-sessions/<id>/.claude-shared/ — Claude state + per-group skills
|
|
const claudeDir = path.join(DATA_DIR, 'v2-sessions', group.id, '.claude-shared');
|
|
if (!fs.existsSync(claudeDir)) {
|
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
initialized.push('.claude-shared');
|
|
}
|
|
|
|
const settingsFile = path.join(claudeDir, 'settings.json');
|
|
if (!fs.existsSync(settingsFile)) {
|
|
fs.writeFileSync(settingsFile, DEFAULT_SETTINGS_JSON);
|
|
initialized.push('settings.json');
|
|
} else {
|
|
ensurePreCompactHook(settingsFile, initialized);
|
|
}
|
|
|
|
// Skills directory — created empty here; symlinks are synced at spawn
|
|
// time by container-runner.ts based on container.json skills selection.
|
|
const skillsDst = path.join(claudeDir, 'skills');
|
|
if (!fs.existsSync(skillsDst)) {
|
|
fs.mkdirSync(skillsDst, { recursive: true });
|
|
initialized.push('skills/');
|
|
}
|
|
|
|
if (initialized.length > 0) {
|
|
log.info('Initialized group filesystem', {
|
|
group: group.name,
|
|
folder: group.folder,
|
|
id: group.id,
|
|
steps: initialized,
|
|
});
|
|
}
|
|
}
|
|
|
|
const PRE_COMPACT_COMMAND = 'bun /app/src/compact-instructions.ts';
|
|
|
|
/**
|
|
* Patch an existing settings.json to add the PreCompact hook if missing.
|
|
* Runs on every group init so pre-existing groups pick up the hook.
|
|
*/
|
|
function ensurePreCompactHook(settingsFile: string, initialized: string[]): void {
|
|
try {
|
|
const raw = fs.readFileSync(settingsFile, 'utf-8');
|
|
const settings = JSON.parse(raw);
|
|
|
|
// Check if there's already a PreCompact hook with our command.
|
|
const existing = settings.hooks?.PreCompact as unknown[] | undefined;
|
|
if (existing && JSON.stringify(existing).includes(PRE_COMPACT_COMMAND)) return;
|
|
|
|
// Add the hook, preserving existing hooks.
|
|
if (!settings.hooks) settings.hooks = {};
|
|
if (!settings.hooks.PreCompact) settings.hooks.PreCompact = [];
|
|
settings.hooks.PreCompact.push({
|
|
hooks: [{ type: 'command', command: PRE_COMPACT_COMMAND }],
|
|
});
|
|
|
|
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
initialized.push('settings.json (added PreCompact hook)');
|
|
} catch {
|
|
// Don't break init if settings.json is malformed — it'll use whatever's there.
|
|
}
|
|
}
|