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
70 lines
1.9 KiB
TypeScript
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();
|
|
}
|