Add native host management and role-based operator authentication

Introduce an outbound Python host agent, Debian installer, durable remote
operations, immutable game-file libraries, host-aware deployments and ports,
and occupancy-safe campaigns and draining. Native hosts do not require Komodo.

Add administrator, operator and viewer accounts, protected management routes,
secure sessions, password recovery, audit history and separate telemetry tokens.
Document first-admin setup, reporter migration and live-host acceptance checks.

Validation: 1,343 TypeScript tests and 39 agent tests passed, along with type
checking, the production build, compiled SSR/API smoke tests and administrator
recovery. Real Debian/Docker T5/T6 acceptance remains unrun.
This commit is contained in:
2026-10-03 11:42:36 -04:00
parent a2e2311aa9
commit e5c0fda1ed
127 changed files with 8485 additions and 657 deletions
@@ -0,0 +1,12 @@
import { json } from '@sveltejs/kit';
import { rotateCredential } from '$lib/server/hosts/index.js';
import { authenticateAgent, hostApiError, objectBody } from '$lib/server/hosts/http.js';
export async function POST({ request }: { request: Request }) {
const host = authenticateAgent(request);
try {
const body = await objectBody(request);
if (typeof body.credential !== 'string') throw new Error('Persist a randomly generated replacement credential before requesting rotation.');
rotateCredential(host.id, body.credential);
return json({ ok: true }, { headers: { 'Cache-Control': 'no-store' } });
} catch (cause) { return hostApiError(cause); }
}
+8
View File
@@ -0,0 +1,8 @@
import { json } from '@sveltejs/kit';
import { enrollHost } from '$lib/server/hosts/index.js';
import { hostApiError, objectBody, requireAgentTransport } from '$lib/server/hosts/http.js';
export async function POST({ request }: { request: Request }) {
requireAgentTransport(request);
try { const body = await objectBody(request); return json(enrollHost(body.token, body.inventory), { headers: { 'Cache-Control': 'no-store' } }); }
catch (cause) { return hostApiError(cause); }
}
+16
View File
@@ -0,0 +1,16 @@
import { json } from '@sveltejs/kit';
import { heartbeat } from '$lib/server/hosts/index.js';
import { authenticateAgent, hostApiError, objectBody } from '$lib/server/hosts/http.js';
export async function POST({ request }: { request: Request }) {
const authenticated = authenticateAgent(request);
try {
const body = await objectBody(request);
const host = heartbeat(authenticated.id, body.inventory);
const inventory = body.inventory as Record<string, unknown>;
if (Array.isArray(inventory.libraryCaches)) {
const { updateHostLibraryCaches } = await import('$lib/server/game-files/index.js');
updateHostLibraryCaches(host.id, inventory.libraryCaches, { snapshot: true });
}
return json({ host, heartbeatSeconds: 15, serverTime: new Date().toISOString() });
} catch (cause) { return hostApiError(cause); }
}
@@ -0,0 +1,15 @@
import { json } from '@sveltejs/kit';
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { requireImportOwner, setImportManifest } from '$lib/server/game-files/index.js';
import { gameFileError, readGameJson } from '$lib/server/game-files/http.js';
export function GET({ request, params }: { request: Request; params: { id: string } }) {
try { return json(requireImportOwner(authenticateAgent(request).id, params.id)); }
catch (cause) { return gameFileError(cause); }
}
export async function PUT({ request, params }: { request: Request; params: { id: string } }) {
try {
const host = authenticateAgent(request);
const body = await readGameJson(request, 64 * 1024 * 1024);
return json(setImportManifest(host.id, params.id, body.files));
} catch (cause) { return gameFileError(cause); }
}
@@ -0,0 +1,15 @@
import { json } from '@sveltejs/kit';
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { importPublicationStatus, requestImportPublication } from '$lib/server/game-files/index.js';
import { gameFileError } from '$lib/server/game-files/http.js';
export function POST({ request, params }: { request: Request; params: { id: string } }) {
try {
const result = requestImportPublication(authenticateAgent(request).id, params.id);
return json(result, { status: result.status === 'verifying' || result.status === 'pending' ? 202 : 200 });
}
catch (cause) { return gameFileError(cause); }
}
export function GET({ request, params }: { request: Request; params: { id: string } }) {
try { return json(importPublicationStatus(authenticateAgent(request).id, params.id)); }
catch (cause) { return gameFileError(cause); }
}
@@ -0,0 +1,11 @@
import { json } from '@sveltejs/kit';
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { uploadImportFile } from '$lib/server/game-files/index.js';
import { gameFileError } from '$lib/server/game-files/http.js';
export async function PUT({ request, params }: { request: Request; params: { id: string; path: string } }) {
try {
const host = authenticateAgent(request);
await uploadImportFile(host.id, params.id, params.path, request.body);
return json({ uploaded: true });
} catch (cause) { return gameFileError(cause); }
}
@@ -0,0 +1,2 @@
import { agentReleaseAsset } from '$lib/server/hosts/releases.js';
export async function GET() { return new Response(await agentReleaseAsset('install.sh'), { headers: { 'Content-Type': 'text/x-shellscript', 'Cache-Control': 'no-cache' } }); }
@@ -0,0 +1,7 @@
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { gameFileError, serveGameFile } from '$lib/server/game-files/http.js';
export async function GET({ request, params }: { request: Request; params: { id: string; path: string } }) {
try { authenticateAgent(request); return await serveGameFile(request, params.id, params.path); }
catch (cause) { return gameFileError(cause); }
}
export const HEAD = GET;
@@ -0,0 +1,12 @@
import { json } from '@sveltejs/kit';
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { getLibraryVersion } from '$lib/server/game-files/index.js';
import { gameFileError } from '$lib/server/game-files/http.js';
export function GET({ request, params }: { request: Request; params: { id: string } }) {
try {
authenticateAgent(request);
const version = getLibraryVersion(params.id);
if (!version || version.state !== 'ready') return json({ error: 'Published library not found.' }, { status: 404 });
return json({ id: version.id, game: version.game, files: version.files, sizeBytes: version.sizeBytes, manifestSha256: version.manifestSha256 });
} catch (cause) { return gameFileError(cause); }
}
@@ -0,0 +1,11 @@
import { json } from '@sveltejs/kit';
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { updateHostLibraryCaches } from '$lib/server/game-files/index.js';
import { gameFileError, readGameJson } from '$lib/server/game-files/http.js';
export async function POST({ request, params }: { request: Request; params: { id: string } }) {
try {
const host = authenticateAgent(request); const body = await readGameJson(request);
updateHostLibraryCaches(host.id, [{ ...body, libraryVersionId: params.id }]);
return json({ ok: true });
} catch (cause) { return gameFileError(cause); }
}
@@ -0,0 +1,7 @@
import { json } from '@sveltejs/kit';
import { pendingHostOperations } from '$lib/server/hosts/index.js';
import { authenticateAgent } from '$lib/server/hosts/http.js';
export function GET({ request }: { request: Request }) {
const host = authenticateAgent(request);
return json({ operations: pendingHostOperations(host.id), serverTime: new Date().toISOString() }, { headers: { 'Cache-Control': 'no-store' } });
}
@@ -0,0 +1,8 @@
import { json } from '@sveltejs/kit';
import { reportHostOperation } from '$lib/server/hosts/index.js';
import { authenticateAgent, hostApiError, objectBody } from '$lib/server/hosts/http.js';
export async function POST({ request, params }: { request: Request; params: { id: string } }) {
const host = authenticateAgent(request);
try { const body = await objectBody(request, 128 * 1024 * 1024); const operation = reportHostOperation(host.id, params.id, { state: body.state, result: body.result, error: body.error }); return json({ operation: { id: operation.id, state: operation.state } }); }
catch (cause) { return hostApiError(cause); }
}
@@ -0,0 +1,6 @@
import { error } from '@sveltejs/kit';
import { agentReleaseAsset } from '$lib/server/hosts/releases.js';
export async function GET({ params }: { params: { file: string } }) {
if (!['ec-agent.py','manifest.json'].includes(params.file)) error(404, 'Release asset not found.');
return new Response(await agentReleaseAsset(params.file as 'ec-agent.py' | 'manifest.json'), { headers: { 'Content-Type': params.file.endsWith('.json') ? 'application/json' : 'text/plain', 'Cache-Control': 'no-cache' } });
}