/*
RFC-0565: dht.capacity.publish command handler. Publishes workshop capacity
(available slots, storage limit, bandwidth limit) to the DHT so placement
decisions can use real-time capacity data. Signs the capacity entry with the
operator's Ed25519 keypair.
- Do not implement placement logic — that lives in placement.ts.
- Do not implement capacity monitoring — the operator declares capacity manually.
- Capacity is self-reported by the workshop — no external measurement is performed.
- RFC-0565: initial implementation — dht.capacity.publish handler.
- RFC-1097: step 6 — compass.migrate codemod run
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
*/
import { join } from "node:path";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-shared/kernel";
import type { WorkshopCapacity } from "./types.ts";
import { workshopCapacitySchema } from "./types.ts";
import { loadDhtConfig } from "./config.ts";
import { createDhtNode, startDhtNode, stopDhtNode, dhtPut } from "./node.ts";
import { signBytes, verifyBytes } from "@warpgogol/werkstatt-shared/passport/sign";
import {
WerkstattIdentityConfigSchema,
type WerkstattIdentityConfig,
} from "@warpgogol/werkstatt-shared/passport";
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
const IDENTITY_FILENAME = "werkstatt.identity.json";
const WERKSTATT_OPERATOR_SIGNING_KEY_ENV = "WERKSTATT_OPERATOR_SIGNING_KEY";
interface DhtCapacityPublishResult {
published: boolean;
workshopId: string;
key: string;
capacity: WorkshopCapacity | null;
diagnostics?: string[];
}
async function loadIdentityConfig(
workspaceRoot: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const identityPath = join(workspaceRoot, IDENTITY_FILENAME);
const raw = await io.readFile(identityPath);
const parsed = JSON.parse(raw);
return WerkstattIdentityConfigSchema.parse(parsed);
}
function getSigningKey(): string {
const key = process.env[WERKSTATT_OPERATOR_SIGNING_KEY_ENV];
if (!key || key.length === 0) {
throw new Error(`${WERKSTATT_OPERATOR_SIGNING_KEY_ENV} environment variable is not set`);
}
return key;
}
/**
* Produce canonical UTF-8 bytes from a capacity entry (excluding signature).
*/
function capacityBytes(capacity: Omit): Uint8Array {
const sorted: Record = {};
for (const key of Object.keys(capacity).sort()) {
sorted[key] = (capacity as Record)[key];
}
return new TextEncoder().encode(JSON.stringify(sorted));
}
export async function runDhtCapacityPublish(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const { workspaceRoot } = context;
const workshopId = input.flags["workshop-id"] as string | undefined;
const availableSlots = input.flags["available-slots"] as string | undefined;
const storageLimitMb = input.flags["storage-limit-mb"] as string | undefined;
const bandwidthLimitMbps = input.flags["bandwidth-limit-mbps"] as string | undefined;
if (!workshopId) {
return {
data: {
published: false,
workshopId: "",
key: "",
capacity: null,
diagnostics: ["dht.capacity.publish: --workshop-id flag is required"],
},
exitCode: 1,
summary: "[dht.capacity.publish] --workshop-id flag is required",
nextSteps: [
{
action: "Provide the --workshop-id flag and re-run dht.capacity.publish",
kind: "required",
},
],
};
}
if (availableSlots === undefined) {
return {
data: {
published: false,
workshopId,
key: "",
capacity: null,
diagnostics: ["dht.capacity.publish: --available-slots flag is required"],
},
exitCode: 1,
summary: "[dht.capacity.publish] --available-slots flag is required",
nextSteps: [
{
action: "Provide the --available-slots flag and re-run dht.capacity.publish",
kind: "required",
},
],
};
}
const slots = parseInt(availableSlots, 10);
const storage = storageLimitMb ? parseInt(storageLimitMb, 10) : 0;
const bandwidth = bandwidthLimitMbps ? parseInt(bandwidthLimitMbps, 10) : 0;
const endpoint = (input.flags["endpoint"] as string | undefined) ?? "0.0.0.0:0";
// Load identity config
let identity: WerkstattIdentityConfig;
try {
identity = await loadIdentityConfig(workspaceRoot);
} catch {
return {
data: {
published: false,
workshopId,
key: "",
capacity: null,
diagnostics: [
`dht.capacity.publish: no ${IDENTITY_FILENAME} found — run identity.bootstrap first (RFC-0558)`,
],
},
exitCode: 1,
summary: `[dht.capacity.publish] identity not bootstrapped`,
nextSteps: [
{
action: "Run identity.bootstrap first (RFC-0558), then re-run dht.capacity.publish",
kind: "required",
},
],
};
}
// Get signing key
let signingKey: string;
try {
signingKey = getSigningKey();
} catch {
return {
data: {
published: false,
workshopId,
key: "",
capacity: null,
diagnostics: [
`dht.capacity.publish: ${WERKSTATT_OPERATOR_SIGNING_KEY_ENV} environment variable is not set`,
],
},
exitCode: 1,
summary: `[dht.capacity.publish] ${WERKSTATT_OPERATOR_SIGNING_KEY_ENV} not set`,
nextSteps: [
{
action: `Set the ${WERKSTATT_OPERATOR_SIGNING_KEY_ENV} environment variable and re-run dht.capacity.publish`,
kind: "required",
},
],
};
}
// Load DHT config
let config;
try {
config = await loadDhtConfig(workspaceRoot);
} catch {
return {
data: {
published: false,
workshopId,
key: "",
capacity: null,
diagnostics: [
`dht.capacity.publish: no werkstatt.dht.json found — run dht.node.init first`,
],
},
exitCode: 1,
summary: `[dht.capacity.publish] DHT not initialized`,
nextSteps: [
{ action: "Run dht.node.init first, then re-run dht.capacity.publish", kind: "required" },
],
};
}
// Build capacity entry
const now = new Date().toISOString();
const capacityData = {
workshopId,
endpoint,
availableSlots: slots,
storageLimitMb: storage,
bandwidthLimitMbps: bandwidth,
activeMissions: 0,
maxMissions: slots,
cpuLoad: 0,
diskFree: 0,
updatedAt: now,
};
// Sign the capacity entry
let signature: string;
try {
signature = await signBytes(signingKey, capacityBytes(capacityData));
} catch (err) {
return {
data: {
published: false,
workshopId,
key: "",
capacity: null,
diagnostics: [
`dht.capacity.publish: failed to sign capacity — ${err instanceof Error ? err.message : String(err)}`,
],
},
exitCode: 1,
summary: `[dht.capacity.publish] signing failed`,
nextSteps: [
{
action:
"Check the WERKSTATT_OPERATOR_SIGNING_KEY environment variable and re-run dht.capacity.publish",
kind: "required",
},
],
};
}
const capacity: WorkshopCapacity = { ...capacityData, signature };
// Validate
try {
workshopCapacitySchema.parse(capacity);
} catch {
return {
data: {
published: false,
workshopId,
key: "",
capacity: null,
diagnostics: [`dht.capacity.publish: capacity entry failed schema validation`],
},
exitCode: 1,
summary: `[dht.capacity.publish] schema validation failed`,
nextSteps: [
{
action: "Check the capacity entry fields and re-run dht.capacity.publish",
kind: "required",
},
],
};
}
// Create ephemeral DHT node
let node;
try {
node = await createDhtNode(config, identity);
await startDhtNode(node, config);
} catch (err) {
return {
data: {
published: false,
workshopId,
key: "",
capacity: null,
diagnostics: [
`dht.capacity.publish: failed to start DHT node — ${err instanceof Error ? err.message : String(err)}`,
],
},
exitCode: 1,
summary: `[dht.capacity.publish] DHT node startup failed`,
nextSteps: [
{
action:
"Check the DHT configuration and network connectivity, then re-run dht.capacity.publish",
kind: "required",
},
],
};
}
try {
const key = `capacity/${workshopId}`;
const valueBytes = new TextEncoder().encode(JSON.stringify(capacity));
await dhtPut(node, key, valueBytes);
return {
data: {
published: true,
workshopId,
key,
capacity,
},
exitCode: 0,
summary: `[dht.capacity.publish] published capacity for ${workshopId} (${slots} slots)`,
};
} finally {
await stopDhtNode(node);
}
}
/**
* Verify a capacity entry signature.
*/
export async function verifyCapacity(
capacity: WorkshopCapacity,
publicKeyMultibase: string,
): Promise {
const { signature, ...data } = capacity;
return verifyBytes(publicKeyMultibase, capacityBytes(data), signature);
}