feat(FN-3107): merge fusion/fn-3107

This merge lands the managed Docker nodes feature (FN-3107) end-to-end: core schema migration for nodes with types, CRUD operations in CentralCore, and dashboard UI (AddNodeModal, NodeDetailModal) with accessibility fixes and tokenized CSS. Also included are manual PR linking (FN-3202), plugin schem

Fusion-Task-Id: FN-3107
This commit is contained in:
Fusion
2026-05-02 11:17:18 -07:00
committed by gsxdsm
parent ffb71b7912
commit d9835970e1
15 changed files with 1411 additions and 357 deletions

View File

@@ -0,0 +1,156 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { CentralCore } from "../central-core.js";
describe("CentralCore managed Docker nodes", () => {
let tempDir: string;
let central: CentralCore;
beforeEach(async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-01T10:00:00.000Z"));
tempDir = mkdtempSync(join(tmpdir(), "kb-central-docker-node-test-"));
central = new CentralCore(tempDir);
await central.init();
});
afterEach(async () => {
await central.close();
vi.useRealTimers();
vi.restoreAllMocks();
rmSync(tempDir, { recursive: true, force: true });
});
const buildInput = (name: string) => ({
nodeId: null,
name,
imageName: "runfusion/fusion",
imageTag: "latest",
hostConfig: { context: "default", tlsVerify: false },
envVars: { FUSION_NODE_NAME: name, FUSION_MODE: "managed" },
volumeMounts: [{ hostPath: "/var/lib/fusion", containerPath: "/data", mode: "rw" as const }],
resourceSizing: { memoryMB: 4096, cpus: 2, memorySwapMB: 0 },
extraClis: ["droid-cli" as const],
persistentStorage: true,
reachableUrl: "http://127.0.0.1:4041",
apiKey: "secret-key",
});
it("createManagedDockerNode creates full record with dn_ id and creating status", async () => {
const created = await central.createManagedDockerNode(buildInput("docker-a"));
expect(created.id.startsWith("dn_")).toBe(true);
expect(created.name).toBe("docker-a");
expect(created.status).toBe("creating");
expect(created.containerId).toBeNull();
expect(created.errorMessage).toBeNull();
});
it("createManagedDockerNode enforces unique names", async () => {
await central.createManagedDockerNode(buildInput("docker-unique"));
await expect(central.createManagedDockerNode(buildInput("docker-unique"))).rejects.toThrow(
"already exists with name",
);
});
it("createManagedDockerNode validates required name", async () => {
await expect(central.createManagedDockerNode(buildInput(" "))).rejects.toThrow(
"between 1 and 64 characters",
);
});
it("getManagedDockerNode returns found and undefined for missing", async () => {
const created = await central.createManagedDockerNode(buildInput("docker-get"));
await expect(central.getManagedDockerNode(created.id)).resolves.toBeDefined();
await expect(central.getManagedDockerNode("dn_missing")).resolves.toBeUndefined();
});
it("getManagedDockerNodeByName returns found and undefined for missing", async () => {
const created = await central.createManagedDockerNode(buildInput("docker-by-name"));
const found = await central.getManagedDockerNodeByName("docker-by-name");
expect(found?.id).toBe(created.id);
await expect(central.getManagedDockerNodeByName("missing-name")).resolves.toBeUndefined();
});
it("listManagedDockerNodes returns all ordered by name", async () => {
await central.createManagedDockerNode(buildInput("zeta"));
await central.createManagedDockerNode(buildInput("alpha"));
const list = await central.listManagedDockerNodes();
expect(list.map((item) => item.name)).toEqual(["alpha", "zeta"]);
});
it("updateManagedDockerNode applies partial changes and updates updatedAt", async () => {
const created = await central.createManagedDockerNode(buildInput("docker-update"));
vi.setSystemTime(new Date("2026-05-01T10:05:00.000Z"));
const updated = await central.updateManagedDockerNode(created.id, {
status: "running",
envVars: { ...created.envVars, EXTRA: "1" },
volumeMounts: [
...created.volumeMounts,
{ hostPath: "/var/log/fusion", containerPath: "/logs", mode: "ro" },
],
});
expect(updated.status).toBe("running");
expect(updated.envVars.EXTRA).toBe("1");
expect(updated.volumeMounts).toHaveLength(2);
expect(updated.updatedAt).not.toBe(created.updatedAt);
});
it("updateManagedDockerNode throws for unknown id", async () => {
await expect(central.updateManagedDockerNode("dn_missing", { status: "error" })).rejects.toThrow(
"not found",
);
});
it("deleteManagedDockerNode removes record", async () => {
const created = await central.createManagedDockerNode(buildInput("docker-delete"));
await central.deleteManagedDockerNode(created.id);
await expect(central.getManagedDockerNode(created.id)).resolves.toBeUndefined();
});
it("linkManagedDockerNodeToNode sets nodeId", async () => {
const managed = await central.createManagedDockerNode(buildInput("docker-link"));
const node = await central.registerNode({
name: "remote-link-target",
type: "remote",
url: "http://127.0.0.1:5050",
apiKey: "remote-key",
});
const linked = await central.linkManagedDockerNodeToNode(managed.id, node.id);
expect(linked.nodeId).toBe(node.id);
});
it("JSON fields round-trip through storage", async () => {
const created = await central.createManagedDockerNode({
...buildInput("docker-json"),
hostConfig: {
host: "tcp://192.168.1.50:2376",
context: "prod",
tlsVerify: true,
tlsCaPath: "/certs/ca.pem",
tlsCertPath: "/certs/cert.pem",
tlsKeyPath: "/certs/key.pem",
},
extraClis: ["claude-cli", "droid-cli"],
});
const fetched = await central.getManagedDockerNode(created.id);
expect(fetched?.hostConfig).toEqual(created.hostConfig);
expect(fetched?.envVars).toEqual(created.envVars);
expect(fetched?.volumeMounts).toEqual(created.volumeMounts);
expect(fetched?.resourceSizing).toEqual(created.resourceSizing);
expect(fetched?.extraClis).toEqual(created.extraClis);
});
});

View File

@@ -39,7 +39,7 @@ describe("CentralDatabase", () => {
it("should initialize schema version", () => {
db.init();
expect(db.getSchemaVersion()).toBe(5);
expect(db.getSchemaVersion()).toBe(6);
});
it("should seed lastModified on init", () => {
@@ -213,7 +213,7 @@ describe("CentralDatabase", () => {
db.init();
expect(db.getSchemaVersion()).toBe(5);
expect(db.getSchemaVersion()).toBe(6);
const nodeColumns = db.prepare("PRAGMA table_info(nodes)").all() as Array<{ name: string }>;
const nodeColumnNames = nodeColumns.map((column) => column.name);
@@ -278,7 +278,7 @@ describe("CentralDatabase", () => {
db.init();
expect(db.getSchemaVersion()).toBe(5);
expect(db.getSchemaVersion()).toBe(6);
const nodeColumns = db.prepare("PRAGMA table_info(nodes)").all() as Array<{ name: string }>;
const nodeColumnNames = nodeColumns.map((column) => column.name);
@@ -294,6 +294,137 @@ describe("CentralDatabase", () => {
expect(row?.versionInfo).toBeNull();
expect(row?.pluginVersions).toBeNull();
});
it("should migrate from v5 to v6 with managed Docker node schema", () => {
const now = new Date().toISOString();
db.exec(`
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
path TEXT NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'active',
isolationMode TEXT NOT NULL DEFAULT 'in-process',
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
lastActivityAt TEXT,
nodeId TEXT,
settings TEXT
);
CREATE TABLE IF NOT EXISTS nodes (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
type TEXT NOT NULL CHECK (type IN ('local', 'remote')),
url TEXT,
apiKey TEXT,
status TEXT NOT NULL DEFAULT 'offline',
capabilities TEXT,
systemMetrics TEXT,
knownPeers TEXT,
versionInfo TEXT,
pluginVersions TEXT,
maxConcurrent INTEGER NOT NULL DEFAULT 2,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS peerNodes (
id TEXT PRIMARY KEY,
nodeId TEXT NOT NULL,
peerNodeId TEXT NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'unknown',
lastSeen TEXT NOT NULL,
connectedAt TEXT NOT NULL,
UNIQUE(nodeId, peerNodeId),
FOREIGN KEY (nodeId) REFERENCES nodes(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS settingsSyncState (
nodeId TEXT NOT NULL,
remoteNodeId TEXT NOT NULL,
lastSyncedAt TEXT,
localChecksum TEXT,
remoteChecksum TEXT,
syncCount INTEGER NOT NULL DEFAULT 0,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
PRIMARY KEY (nodeId, remoteNodeId),
FOREIGN KEY (nodeId) REFERENCES nodes(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS __meta (
key TEXT PRIMARY KEY,
value TEXT
);
`);
db.prepare("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '5')").run();
db.prepare("INSERT INTO __meta (key, value) VALUES ('lastModified', ?)").run(String(Date.now()));
db.init();
expect(db.getSchemaVersion()).toBe(6);
const columns = db.prepare("PRAGMA table_info(managedDockerNodes)").all() as Array<{ name: string }>;
const columnNames = columns.map((column) => column.name);
expect(columnNames).toEqual(
expect.arrayContaining([
"id",
"nodeId",
"name",
"imageName",
"imageTag",
"containerId",
"status",
"hostConfig",
"envVars",
"volumeMounts",
"resourceSizing",
"extraClis",
"persistentStorage",
"reachableUrl",
"apiKey",
"errorMessage",
"createdAt",
"updatedAt",
]),
);
const indexes = db
.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='managedDockerNodes'")
.all() as Array<{ name: string }>;
const indexNames = indexes.map((index) => index.name);
expect(indexNames).toContain("idxManagedDockerNodesStatus");
expect(indexNames).toContain("idxManagedDockerNodesNodeId");
db.prepare(
"INSERT INTO managedDockerNodes (id, name, imageName, imageTag, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)",
).run("dn_test_defaults", "docker-defaults", "runfusion/fusion", "latest", now, now);
const row = db.prepare(
"SELECT status, hostConfig, envVars, volumeMounts, resourceSizing, extraClis FROM managedDockerNodes WHERE id = ?",
).get("dn_test_defaults") as
| {
status: string;
hostConfig: string;
envVars: string;
volumeMounts: string;
resourceSizing: string;
extraClis: string;
}
| undefined;
expect(row).toBeDefined();
expect(row?.status).toBe("creating");
expect(fromJson(row?.hostConfig, {})).toEqual({});
expect(fromJson(row?.envVars, {})).toEqual({});
expect(fromJson(row?.volumeMounts, [])).toEqual([]);
expect(fromJson(row?.resourceSizing, {})).toEqual({});
expect(fromJson(row?.extraClis, [])).toEqual([]);
});
});
describe("transactions", () => {

View File

@@ -52,6 +52,11 @@ import type {
DiscoveredNode,
NodeVersionInfo,
NodeVersionInfoInput,
DockerNodeStatus,
DockerHostConfig,
ManagedDockerNode,
ManagedDockerNodeInput,
ManagedDockerNodeUpdate,
PluginSyncResult,
VersionCompatibilityResult,
SettingsSyncPayload,
@@ -826,6 +831,212 @@ export class CentralCore extends EventEmitter<CentralCoreEvents> {
return updated;
}
/**
* Create a managed Docker node record.
*/
async createManagedDockerNode(input: ManagedDockerNodeInput): Promise<ManagedDockerNode> {
this.ensureInitialized();
const name = input.name.trim();
if (!name || name.length > 64) {
throw new Error("Managed Docker node name must be between 1 and 64 characters");
}
const existingByName = await this.getManagedDockerNodeByName(name);
if (existingByName) {
throw new Error(`Managed Docker node already exists with name: ${name}`);
}
const now = new Date().toISOString();
const node: ManagedDockerNode = {
id: `dn_${randomUUID().replace(/-/g, "").slice(0, 16)}`,
nodeId: input.nodeId ?? null,
name,
imageName: input.imageName,
imageTag: input.imageTag,
containerId: null,
status: "creating",
hostConfig: input.hostConfig,
envVars: input.envVars,
volumeMounts: input.volumeMounts,
resourceSizing: input.resourceSizing,
extraClis: input.extraClis,
persistentStorage: input.persistentStorage,
reachableUrl: input.reachableUrl ?? null,
apiKey: input.apiKey ?? null,
errorMessage: null,
createdAt: now,
updatedAt: now,
};
this.db!.prepare(
`INSERT INTO managedDockerNodes (
id, nodeId, name, imageName, imageTag, containerId, status,
hostConfig, envVars, volumeMounts, resourceSizing, extraClis,
persistentStorage, reachableUrl, apiKey, errorMessage, createdAt, updatedAt
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(
node.id,
node.nodeId,
node.name,
node.imageName,
node.imageTag,
node.containerId,
node.status,
toJson(node.hostConfig),
toJson(node.envVars),
toJson(node.volumeMounts),
toJson(node.resourceSizing),
toJson(node.extraClis),
node.persistentStorage ? 1 : 0,
node.reachableUrl,
node.apiKey,
node.errorMessage,
node.createdAt,
node.updatedAt,
);
this.db!.bumpLastModified();
return node;
}
/**
* Get a managed Docker node by ID.
*/
async getManagedDockerNode(id: string): Promise<ManagedDockerNode | undefined> {
this.ensureInitialized();
const row = this.db!.prepare("SELECT * FROM managedDockerNodes WHERE id = ?").get(id) as
| Parameters<CentralCore["rowToManagedDockerNode"]>[0]
| undefined;
return row ? this.rowToManagedDockerNode(row) : undefined;
}
/**
* Get a managed Docker node by unique name.
*/
async getManagedDockerNodeByName(name: string): Promise<ManagedDockerNode | undefined> {
this.ensureInitialized();
const row = this.db!.prepare("SELECT * FROM managedDockerNodes WHERE name = ?").get(name) as
| Parameters<CentralCore["rowToManagedDockerNode"]>[0]
| undefined;
return row ? this.rowToManagedDockerNode(row) : undefined;
}
/**
* List managed Docker nodes ordered by name.
*/
async listManagedDockerNodes(): Promise<ManagedDockerNode[]> {
this.ensureInitialized();
const rows = this.db!.prepare("SELECT * FROM managedDockerNodes ORDER BY name").all() as Array<
Parameters<CentralCore["rowToManagedDockerNode"]>[0]
>;
return rows.map((row) => this.rowToManagedDockerNode(row));
}
/**
* Update a managed Docker node.
*/
async updateManagedDockerNode(id: string, updates: ManagedDockerNodeUpdate): Promise<ManagedDockerNode> {
this.ensureInitialized();
const existing = await this.getManagedDockerNode(id);
if (!existing) {
throw new Error(`Managed Docker node not found: ${id}`);
}
const now = new Date().toISOString();
const updated: ManagedDockerNode = {
...existing,
...updates,
id: existing.id,
createdAt: existing.createdAt,
updatedAt: now,
name: updates.name ? updates.name.trim() : existing.name,
};
if (!updated.name || updated.name.length > 64) {
throw new Error("Managed Docker node name must be between 1 and 64 characters");
}
if (updated.name !== existing.name) {
const existingByName = await this.getManagedDockerNodeByName(updated.name);
if (existingByName && existingByName.id !== id) {
throw new Error(`Managed Docker node already exists with name: ${updated.name}`);
}
}
this.db!.prepare(
`UPDATE managedDockerNodes SET
nodeId = ?,
name = ?,
imageName = ?,
imageTag = ?,
containerId = ?,
status = ?,
hostConfig = ?,
envVars = ?,
volumeMounts = ?,
resourceSizing = ?,
extraClis = ?,
persistentStorage = ?,
reachableUrl = ?,
apiKey = ?,
errorMessage = ?,
updatedAt = ?
WHERE id = ?`
).run(
updated.nodeId,
updated.name,
updated.imageName,
updated.imageTag,
updated.containerId,
updated.status,
toJson(updated.hostConfig),
toJson(updated.envVars),
toJson(updated.volumeMounts),
toJson(updated.resourceSizing),
toJson(updated.extraClis),
updated.persistentStorage ? 1 : 0,
updated.reachableUrl,
updated.apiKey,
updated.errorMessage,
updated.updatedAt,
id,
);
this.db!.bumpLastModified();
return updated;
}
/**
* Delete a managed Docker node record by ID.
*/
async deleteManagedDockerNode(id: string): Promise<void> {
this.ensureInitialized();
this.db!.prepare("DELETE FROM managedDockerNodes WHERE id = ?").run(id);
this.db!.bumpLastModified();
}
/**
* Link an existing managed Docker node record to a registered mesh node.
*/
async linkManagedDockerNodeToNode(managedDockerNodeId: string, nodeId: string): Promise<ManagedDockerNode> {
this.ensureInitialized();
const node = await this.getNode(nodeId);
if (!node) {
throw new Error(`Node not found: ${nodeId}`);
}
return this.updateManagedDockerNode(managedDockerNodeId, { nodeId });
}
/**
* Check node health and update stored status.
*/
@@ -2023,6 +2234,48 @@ export class CentralCore extends EventEmitter<CentralCoreEvents> {
};
}
private rowToManagedDockerNode(row: {
id: string;
nodeId: string | null;
name: string;
imageName: string;
imageTag: string;
containerId: string | null;
status: string;
hostConfig: string;
envVars: string;
volumeMounts: string;
resourceSizing: string;
extraClis: string;
persistentStorage: number;
reachableUrl: string | null;
apiKey: string | null;
errorMessage: string | null;
createdAt: string;
updatedAt: string;
}): ManagedDockerNode {
return {
id: row.id,
nodeId: row.nodeId,
name: row.name,
imageName: row.imageName,
imageTag: row.imageTag,
containerId: row.containerId,
status: row.status as DockerNodeStatus,
hostConfig: fromJson<DockerHostConfig>(row.hostConfig) ?? {},
envVars: fromJson<Record<string, string>>(row.envVars) ?? {},
volumeMounts: fromJson<ManagedDockerNode["volumeMounts"]>(row.volumeMounts) ?? [],
resourceSizing: fromJson<ManagedDockerNode["resourceSizing"]>(row.resourceSizing) ?? {},
extraClis: fromJson<ManagedDockerNode["extraClis"]>(row.extraClis) ?? [],
persistentStorage: row.persistentStorage === 1,
reachableUrl: row.reachableUrl,
apiKey: row.apiKey,
errorMessage: row.errorMessage,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
}
private rowToPeerNode(row: {
id: string;
nodeId: string;

View File

@@ -23,7 +23,7 @@ export { toJson, toJsonNullable, fromJson };
// ── Schema Definition ───────────────────────────────────────────────────
const CENTRAL_SCHEMA_VERSION = 5;
const CENTRAL_SCHEMA_VERSION = 6;
const CENTRAL_SCHEMA_SQL = `
-- Projects table (project registry)
@@ -137,6 +137,31 @@ CREATE TABLE IF NOT EXISTS settingsSyncState (
);
CREATE INDEX IF NOT EXISTS idxSettingsSyncNode ON settingsSyncState(nodeId);
-- Managed Docker nodes table (Docker-provisioned mesh nodes)
CREATE TABLE IF NOT EXISTS managedDockerNodes (
id TEXT PRIMARY KEY,
nodeId TEXT,
name TEXT NOT NULL UNIQUE,
imageName TEXT NOT NULL,
imageTag TEXT NOT NULL,
containerId TEXT,
status TEXT NOT NULL DEFAULT 'creating',
hostConfig TEXT NOT NULL DEFAULT '{}',
envVars TEXT NOT NULL DEFAULT '{}',
volumeMounts TEXT NOT NULL DEFAULT '[]',
resourceSizing TEXT NOT NULL DEFAULT '{}',
extraClis TEXT NOT NULL DEFAULT '[]',
persistentStorage INTEGER NOT NULL DEFAULT 1,
reachableUrl TEXT,
apiKey TEXT,
errorMessage TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (nodeId) REFERENCES nodes(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idxManagedDockerNodesStatus ON managedDockerNodes(status);
CREATE INDEX IF NOT EXISTS idxManagedDockerNodesNodeId ON managedDockerNodes(nodeId);
-- Schema version tracking
CREATE TABLE IF NOT EXISTS __meta (
key TEXT PRIMARY KEY,
@@ -202,6 +227,32 @@ CREATE TABLE IF NOT EXISTS settingsSyncState (
CREATE INDEX IF NOT EXISTS idxSettingsSyncNode ON settingsSyncState(nodeId);
`;
const CENTRAL_SCHEMA_V6_MIGRATION_SQL = `
CREATE TABLE IF NOT EXISTS managedDockerNodes (
id TEXT PRIMARY KEY,
nodeId TEXT,
name TEXT NOT NULL UNIQUE,
imageName TEXT NOT NULL,
imageTag TEXT NOT NULL,
containerId TEXT,
status TEXT NOT NULL DEFAULT 'creating',
hostConfig TEXT NOT NULL DEFAULT '{}',
envVars TEXT NOT NULL DEFAULT '{}',
volumeMounts TEXT NOT NULL DEFAULT '[]',
resourceSizing TEXT NOT NULL DEFAULT '{}',
extraClis TEXT NOT NULL DEFAULT '[]',
persistentStorage INTEGER NOT NULL DEFAULT 1,
reachableUrl TEXT,
apiKey TEXT,
errorMessage TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (nodeId) REFERENCES nodes(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idxManagedDockerNodesStatus ON managedDockerNodes(status);
CREATE INDEX IF NOT EXISTS idxManagedDockerNodesNodeId ON managedDockerNodes(nodeId);
`;
// ── Central Database Class ────────────────────────────────────────────────
export class CentralDatabase {
@@ -279,6 +330,11 @@ export class CentralDatabase {
migrated = true;
}
if (currentVersion < 6) {
this.db.exec(CENTRAL_SCHEMA_V6_MIGRATION_SQL);
migrated = true;
}
if (migrated) {
this.db
.prepare("INSERT INTO __meta (key, value) VALUES ('schemaVersion', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")

View File

@@ -406,6 +406,14 @@ export type {
NodeStatus,
NodeVersionInfo,
NodeVersionInfoInput,
DockerNodeStatus,
DockerHostConfig,
DockerResourceSizing,
DockerVolumeMount,
DockerExtraCli,
ManagedDockerNode,
ManagedDockerNodeInput,
ManagedDockerNodeUpdate,
NodeDiscoveryEvent,
DiscoveryConfig,
DiscoveredNode,

View File

@@ -2467,6 +2467,99 @@ export type NodeVersionInfoInput = Omit<NodeVersionInfo, "appVersion"> & {
appVersion?: string;
};
/** Lifecycle status of a managed Docker node. */
export type DockerNodeStatus = "creating" | "running" | "stopped" | "error" | "recreating" | "deleting";
/** Docker daemon connection settings for provisioning a managed node container. */
export interface DockerHostConfig {
/** Docker host URI (for example: tcp://192.168.1.50:2376 or unix:///var/run/docker.sock). */
host?: string;
/** Named Docker context to target. */
context?: string;
/** Whether to verify Docker daemon TLS certificates. */
tlsVerify?: boolean;
/** Path to Docker daemon CA certificate. */
tlsCaPath?: string;
/** Path to Docker client certificate. */
tlsCertPath?: string;
/** Path to Docker client private key. */
tlsKeyPath?: string;
}
/** Container CPU and memory limit settings for managed Docker nodes. */
export interface DockerResourceSizing {
/** Memory limit in MB (for example: 4096). */
memoryMB?: number;
/** CPU limit (for example: 2.0). */
cpus?: number;
/** Swap limit in MB (0 = unlimited swap, Docker default behavior). */
memorySwapMB?: number;
}
/** A single bind mount definition for a managed Docker node container. */
export interface DockerVolumeMount {
/** Absolute path on the host machine. */
hostPath: string;
/** Path inside the container. */
containerPath: string;
/** Mount mode. Defaults to read/write when omitted. */
mode?: "ro" | "rw";
}
/** Optional additional CLI tools installed in the managed Docker node image. */
export type DockerExtraCli = "claude-cli" | "droid-cli";
/** Persisted definition and lifecycle metadata for a managed Docker node. */
export interface ManagedDockerNode {
/** Unique managed Docker node ID (for example: dn_abc123). */
id: string;
/** Linked mesh node ID after registration, or null while provisioning. */
nodeId: string | null;
/** Display name (unique across managed Docker nodes). */
name: string;
/** Docker image repository/name (for example: runfusion/fusion). */
imageName: string;
/** Docker image tag (for example: latest or 0.2.0). */
imageTag: string;
/** Provisioned container ID, or null before container creation. */
containerId: string | null;
/** Current managed Docker lifecycle status. */
status: DockerNodeStatus;
/** Docker daemon host/context configuration used for operations. */
hostConfig: DockerHostConfig;
/** Environment variables injected into the container. */
envVars: Record<string, string>;
/** Bind mounts configured for this container. */
volumeMounts: DockerVolumeMount[];
/** Resource limits for this container. */
resourceSizing: DockerResourceSizing;
/** Optional extra CLI tools included in provisioning. */
extraClis: DockerExtraCli[];
/** Whether storage volumes persist across container recreation. */
persistentStorage: boolean;
/** Reachable URL for mesh/node registration once running. */
reachableUrl: string | null;
/** API key for the managed node, auto-generated or user-provided. */
apiKey: string | null;
/** Last provisioning/runtime error message when status is error. */
errorMessage: string | null;
/** ISO-8601 creation timestamp. */
createdAt: string;
/** ISO-8601 last update timestamp. */
updatedAt: string;
}
/** Input for creating a managed Docker node record. */
export type ManagedDockerNodeInput = Omit<
ManagedDockerNode,
"id" | "containerId" | "status" | "createdAt" | "updatedAt" | "errorMessage"
>;
/** Partial update payload for managed Docker nodes. */
export type ManagedDockerNodeUpdate = Partial<
Omit<ManagedDockerNode, "id" | "createdAt">
>;
/** A single plugin's version information for sync comparison */
export interface PluginVersionEntry {
/** Plugin ID (matches PluginManifest.id) */