bunker-admin 721b4df6c3 Add remote instance management with mTLS agent and phone-home registration
Enables the CCP to manage CML instances on remote servers via a lightweight
HTTP agent. Key components:

- ExecutionDriver abstraction (local-driver.ts / remote-driver.ts) routes
  operations to local Docker or remote agent transparently
- Remote agent package (agent/) with mTLS authentication, Docker Compose
  operations, file management, backup/upgrade delegation
- Certificate service using openssl CLI for CA management and cert issuance
- Phone-home registration: remote agents register via invite code, CCP admin
  approves, agent receives mTLS cert bundle automatically
- config.sh integration with configure_control_panel() section
- ccp-agent Docker Compose service (profile-gated)
- Frontend: AgentRegistrationsPage, InviteCodesPage, Remote Agents sidebar menu
- Security hardened: cert bundle wiped after delivery, shell injection prevention
  via execFile, command allowlist with metachar rejection, rate-limited public
  endpoints, auto-populated fingerprint pinning

Also wires ENABLE_SOCIAL/PEOPLE/ANALYTICS through env.ts, seed.ts, and
docker-compose env passthrough (from previous session).

Bunker Admin
2026-04-07 15:24:33 -06:00

70 lines
1.9 KiB
TypeScript

import fs from 'fs/promises';
import path from 'path';
import { env } from '../config/env';
import { logger } from '../utils/logger';
import { AgentError } from '../middleware/error-handler';
interface SlugEntry {
basePath: string;
composeProject: string;
registeredAt: string;
}
type Registry = Record<string, SlugEntry>;
const registryPath = () => path.join(env.AGENT_DATA_DIR, 'registry.json');
let cache: Registry | null = null;
async function loadRegistry(): Promise<Registry> {
if (cache) return cache;
try {
const data = await fs.readFile(registryPath(), 'utf-8');
cache = JSON.parse(data) as Registry;
return cache;
} catch {
cache = {};
return cache;
}
}
async function saveRegistry(registry: Registry): Promise<void> {
await fs.mkdir(env.AGENT_DATA_DIR, { recursive: true });
await fs.writeFile(registryPath(), JSON.stringify(registry, null, 2), 'utf-8');
cache = registry;
}
export async function registerSlug(slug: string, basePath: string, composeProject: string): Promise<void> {
const registry = await loadRegistry();
registry[slug] = {
basePath,
composeProject,
registeredAt: new Date().toISOString(),
};
await saveRegistry(registry);
logger.info(`[registry] Registered slug ${slug}${basePath} (project: ${composeProject})`);
}
export async function unregisterSlug(slug: string): Promise<void> {
const registry = await loadRegistry();
if (!registry[slug]) {
throw new AgentError(404, `Slug ${slug} not registered`);
}
delete registry[slug];
await saveRegistry(registry);
logger.info(`[registry] Unregistered slug ${slug}`);
}
export async function getSlugEntry(slug: string): Promise<SlugEntry> {
const registry = await loadRegistry();
const entry = registry[slug];
if (!entry) {
throw new AgentError(404, `Slug ${slug} not registered`, 'SLUG_NOT_FOUND');
}
return entry;
}
export async function listSlugs(): Promise<Record<string, SlugEntry>> {
return loadRegistry();
}