clawdie-ai/setup/controlplane.ts
Sam & ZAI 061a25f98e refactor(controlplane): rename ceo→orchestrator, company→system, agent id=AGENT_NAME
Terminology overhaul for control plane naming:
- AgentRole 'ceo' → 'orchestrator', schema constraint updated
- DEFAULT_AGENTS → getDefaultAgents(agentName) function, main agent id = AGENT_NAME
- Identity file resolution: {AGENT_NAME}.md with SOUL.md fallback
- All test mocks updated: 'ceo' → 'clawdie', 'company' → 'system'
- SOUL.md + SYSADMIN.md docs updated (Paperclip→Control Plane)
- TypeScript build clean (tsc --noEmit passes)

Remaining: DBA.md, GIT_ADMIN.md, doc/* updates, CLAWDIE.md creation
See doc/NAMING-HANDOFF.md for task checklist (Sam & Claude)
2026-04-08 10:07:41 +02:00

169 lines
6.5 KiB
TypeScript

/**
* setup/controlplane.ts
*
* Initialises the Clawdie control plane:
* 1. Creates PostgreSQL tables (idempotent — IF NOT EXISTS)
* 2. Provisions 4 default agents (CEO, Sysadmin, DBA, Git Admin)
* 3. Creates the operator account
* 4. Seeds per-agent budgets
* 5. Copies operational skills → data/skills/
*
* Safe to re-run: all writes use ON CONFLICT DO NOTHING / DO UPDATE.
*/
import fs from 'fs';
import path from 'path';
import pg from 'pg';
import { AGENT_NAME, MEMORY_DB_URL } from '../src/config.js';
import {
getDefaultAgents,
copySkills,
generatePassword,
getAgents,
getOperator,
runSchemaMigration,
upsertAgent,
upsertBudget,
upsertOperator,
} from '../src/controlplane-db.js';
import { logger } from '../src/logger.js';
import { emitStatus } from './status.js';
const LOG = 'logs/setup.log';
function appendSetupLog(line: string): void {
try {
fs.mkdirSync('logs', { recursive: true });
fs.appendFileSync(LOG, `[${new Date().toISOString()}] ${line}\n`);
} catch {
// log file is best-effort; never block setup on it
}
}
export async function run(_args: string[]): Promise<void> {
appendSetupLog('=== controlplane setup start ===');
const dbUrl = MEMORY_DB_URL;
if (!dbUrl) {
emitStatus('SETUP_CONTROLPLANE', {
STATUS: 'failed',
ERROR: 'MEMORY_DB_URL not set — cannot connect to PostgreSQL',
LOG,
});
process.exit(1);
}
const pool = new pg.Pool({ connectionString: dbUrl, max: 3 });
try {
// ── 1. Schema migration ──────────────────────────────────────────
logger.info('Running control plane schema migration...');
appendSetupLog('schema migration: starting');
await runSchemaMigration(pool);
logger.info('Schema ready');
appendSetupLog('schema migration: ready');
// ── 2. Provision default agents ──────────────────────────────────
const defaultAgents = getDefaultAgents(AGENT_NAME);
logger.info('Provisioning default agents...');
for (const agent of defaultAgents) {
await upsertAgent(pool, agent);
logger.info({ id: agent.id, role: agent.role }, 'Agent ready');
appendSetupLog(`agent ready: ${agent.id} (${agent.role})`);
}
// ── 3. Create operator account ───────────────────────────────────
const operatorName =
(process.env.CONTROLPLANE_NAME || AGENT_NAME || 'clawdie').trim();
const existing = await getOperator(pool, operatorName);
let operatorPassword: string;
if (existing) {
logger.info({ username: operatorName }, 'Operator already exists');
operatorPassword = '(existing — check OPERATOR_PASSWORD in .env)';
appendSetupLog(`operator exists: ${operatorName}`);
} else {
operatorPassword = generatePassword(32);
await upsertOperator(pool, operatorName, operatorPassword);
logger.info({ username: operatorName }, 'Operator created');
appendSetupLog(`operator created: ${operatorName} (password written to .env, not logged)`);
// Write password to .env if present
const envPath = path.join(process.cwd(), '.env');
if (fs.existsSync(envPath)) {
let envContent = fs.readFileSync(envPath, 'utf-8');
if (envContent.includes('OPERATOR_PASSWORD=')) {
envContent = envContent.replace(
/^OPERATOR_PASSWORD=.*$/m,
`OPERATOR_PASSWORD=${operatorPassword}`,
);
} else {
envContent += `\nOPERATOR_PASSWORD=${operatorPassword}\n`;
}
fs.writeFileSync(envPath, envContent, { mode: 0o600 });
logger.info('Wrote OPERATOR_PASSWORD to .env');
}
}
// ── 4. Seed budgets ──────────────────────────────────────────────
const dailyTokens = parseInt(
process.env.CONTROLPLANE_DAILY_TOKENS || '100000',
10,
);
for (const agent of defaultAgents) {
const agentTokens = Math.floor((dailyTokens * agent.budget_allocation_pct) / 100);
await upsertBudget(pool, agent.id, agentTokens);
logger.info(
{ agent: agent.id, tokens: agentTokens },
'Budget seeded',
);
}
// ── 5. Copy skills ───────────────────────────────────────────────
const skillsSrc = path.join(process.cwd(), '.agent', 'skills');
const skillsDest = path.join(process.cwd(), 'data', 'skills');
copySkills(skillsSrc, skillsDest);
const skillCount = fs
.readdirSync(skillsDest, { withFileTypes: true })
.filter(
(e) =>
e.isDirectory() &&
fs.existsSync(path.join(skillsDest, e.name, 'SKILL.md')),
).length;
logger.info({ count: skillCount, dest: skillsDest }, 'Skills copied');
// ── Summary ──────────────────────────────────────────────────────
const agents = await getAgents(pool);
emitStatus('SETUP_CONTROLPLANE', {
STATUS: 'success',
AGENTS: agents.map((a) => a.id).join(', '),
OPERATOR: operatorName,
OPERATOR_PASSWORD: existing ? '(unchanged)' : '(printed to terminal — see console output, not logged)',
DAILY_TOKENS: dailyTokens,
SKILLS_COPIED: skillCount,
DASHBOARD: `http://localhost:${process.env.CONTROLPLANE_PORT || 3100}/`,
LOG,
});
appendSetupLog(`success: agents=${agents.length} operator=${operatorName} skills=${skillCount}`);
console.log('\n✓ Control plane ready');
console.log(` Agents: ${agents.map((a) => a.id).join(', ')}`);
console.log(` Operator: ${operatorName}`);
if (!existing) {
console.log(` Password: ${operatorPassword} ← save this! (also stored in .env)`);
console.log(` ⚠ do not redirect/tee this output — password is on stdout only`);
}
console.log(` Skills: ${skillCount} copied to data/skills/`);
console.log(` Dashboard: http://localhost:${process.env.CONTROLPLANE_PORT || 3100}/\n`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
emitStatus('SETUP_CONTROLPLANE', { STATUS: 'failed', ERROR: message, LOG });
throw error;
} finally {
await pool.end();
}
}