Files
fusion/packages/core/src/docker-provisioning.ts
gsxdsm cca13737b6 FN-8603: reduce steady-state diagnostic log noise
Route routine core, engine, and dashboard diagnostics through debug-gated shared loggers.

- Demote steady-state diagnostic sites while preserving warnings and errors for actionable failures.
- Add cross-package severity contracts and manifest coverage for demoted log sites.
- Document logging severity guidance and add a patch changeset.

Files changed:
 .changeset/fn-8603-log-severity.md                 |  7 ++
 docs/diagnostics.md                                | 20 ++++--
 .../__tests__/log-severity-spam-contract.test.ts   | 71 ++++++++++++++++++
 packages/core/src/activity-analytics.ts            |  5 +-
 packages/core/src/ai-summarize.ts                  | 61 +++++++---------
 packages/core/src/async-mission-store.ts           |  5 +-
 packages/core/src/async-secrets-store.ts           |  7 +-
 packages/core/src/central-core.ts                  | 17 ++---
 packages/core/src/docker-provisioning.ts           | 13 ++--
 packages/core/src/index.ts                         |  1 +
 packages/core/src/master-key.ts                    |  9 ++-
 packages/core/src/memory-compaction.ts             | 29 ++++----
 packages/core/src/memory-insights.ts               |  7 +-
 packages/core/src/migration-orchestrator.ts        |  7 +-
 packages/core/src/mission-store.ts                 |  5 +-
 packages/core/src/node-discovery.ts                |  7 +-
 packages/core/src/notification/dispatcher.ts       |  9 ++-
 .../core/src/plugins/bundled-plugin-install.ts     | 11 +--
 packages/core/src/reflection-store.ts              |  5 +-
 packages/core/src/secrets-store.ts                 |  7 +-
 packages/core/src/task-store/agent-logs.ts         | 21 +++---
 packages/core/src/task-store/async-events.ts       |  5 +-
 packages/core/src/task-store/async-maintenance.ts  |  7 +-
 packages/core/src/task-store/comments-ops.ts       |  7 +-
 packages/core/src/task-store/task-mutation-ops.ts  | 11 +--
 packages/core/src/task-store/workflow-integrity.ts |  9 ++-
 packages/core/src/types/merge-policy.ts            |  5 +-
 packages/core/src/usage-events.ts                  |  5 +-
 .../__tests__/log-severity-spam-contract.test.ts   | 48 +++++++++++++
 packages/dashboard/src/ai-refine.ts                |  5 +-
 packages/dashboard/src/ai-session-diagnostics.ts   | 10 +--
 packages/dashboard/src/chat.ts                     |  8 ++-
 packages/dashboard/src/devserver-manager.ts        |  9 ++-
 packages/dashboard/src/file-service.ts             |  5 +-
 packages/dashboard/src/github-tracking-comments.ts |  7 +-
 .../dashboard/src/github-tracking-reconciler.ts    |  5 +-
 packages/dashboard/src/github-tracking-state.ts    |  5 +-
 packages/dashboard/src/gitlab-lifecycle.ts         |  5 +-
 packages/dashboard/src/insights-routes.ts          |  9 ++-
 packages/dashboard/src/issue-image-attachments.ts  |  5 +-
 packages/dashboard/src/knowledge-index.ts          |  5 +-
 packages/dashboard/src/plugin-routes.ts            |  7 +-
 packages/dashboard/src/routes/board-workflows.ts   |  5 +-
 packages/dashboard/src/routes/context.ts           |  5 +-
 .../dashboard/src/routes/register-auth-routes.ts   | 13 ++--
 .../routes/register-docker-provisioning-routes.ts  |  7 +-
 .../dashboard/src/routes/register-git-github.ts    | 21 +++---
 packages/dashboard/src/routes/register-gitlab.ts   |  7 +-
 .../src/routes/register-session-diff-routes.ts     |  9 ++-
 .../src/routes/register-settings-memory-routes.ts  |  7 +-
 .../src/routes/register-setup-activity-routes.ts   |  7 +-
 .../dashboard/src/routes/register-signal-routes.ts |  5 +-
 .../src/routes/register-task-workflow-routes.ts    | 11 +--
 packages/dashboard/src/runtime-logger.ts           | 11 +--
 packages/dashboard/src/server.ts                   |  7 +-
 packages/dashboard/src/sse.ts                      |  8 ++-
 packages/dashboard/src/terminal-service.ts         | 34 ++++-----
 packages/dashboard/src/view-chunk-manifest.ts      |  5 +-
 .../engine/src/__tests__/log-severity-manifest.ts  | 83 ++++++++++++++++++++++
 .../__tests__/log-severity-spam-contract.test.ts   | 40 ++++++++++-
 .../src/__tests__/logger-debug-gating.test.ts      |  7 +-
 packages/engine/src/goal-anchoring-audit.ts        |  5 +-
 packages/engine/src/plugin-runner.ts               | 44 ++++++------
 packages/engine/src/pty-native.ts                  |  9 ++-
 .../engine/src/runtimes/child-process-worker.ts    |  4 +-
 packages/engine/src/self-healing.ts                | 12 ++--
 packages/engine/src/worktree-hooks.ts              | 10 ++-
 67 files changed, 632 insertions(+), 250 deletions(-)

Fusion-Task-Id: FN-8603

Fusion-Task-Lineage: 53901db6-1af2-4bd7-b5ea-49507e048ef2

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 12:01:19 -07:00

339 lines
11 KiB
TypeScript

import { randomUUID } from "node:crypto";
import type Docker from "dockerode";
import type {
DockerContainerInspectResult,
DockerHostConfig,
DockerProvisionInput,
DockerProvisionResult,
} from "./types.js";
import { DockerClientService } from "./docker-client.js";
import { createLogger } from "./logger.js";
const log = createLogger("docker-provisioning");
/**
* Service for provisioning Docker-based Fusion nodes.
* Creates containers from a prebuilt image, manages their lifecycle,
* and provides start/stop/restart operations.
*/
export class DockerProvisioningService {
constructor(private readonly dockerClientService: DockerClientService) {}
/**
* Provision a new Docker container for a Fusion node.
* Pulls or validates the image, creates and starts the container,
* and returns the result with container details.
*/
async provision(input: DockerProvisionInput): Promise<DockerProvisionResult> {
const startTime = Date.now();
try {
const docker = await this.dockerClientService.getDockerInstance(input.hostConfig);
const { image, tag } = input.imageConfig;
const imageRef = `${image}:${tag}`;
// Step 1: Pull or validate image
if (input.imageConfig.pullImage) {
log.debug(`Pulling image ${imageRef}...`);
try {
const authOptions =
input.imageConfig.registryUsername || input.imageConfig.registryPassword
? {
authconfig: {
username: input.imageConfig.registryUsername ?? "",
password: input.imageConfig.registryPassword ?? "",
},
}
: undefined;
const stream = await docker.pull(imageRef, authOptions);
await new Promise<void>((resolve, reject) => {
docker.modem.followProgress(stream, (err: Error | null) =>
err ? reject(err) : resolve(),
);
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log.error(`Image pull failed: ${message}`);
return {
success: false,
error: `Failed to pull image ${imageRef}: ${message}`,
failedStage: "image-pull",
durationMs: Date.now() - startTime,
};
}
} else {
// Validate image exists locally
try {
await docker.getImage(imageRef).inspect();
} catch {
log.error(`Image ${imageRef} not found locally`);
return {
success: false,
error: `Image ${imageRef} not found locally. Set pullImage: true to pull it.`,
failedStage: "image-pull",
durationMs: Date.now() - startTime,
};
}
}
// Step 2: Generate container name
const containerName = `fusion-${input.nodeName.toLowerCase().replace(/[^a-z0-9-]/g, "-")}-${randomUUID().slice(0, 8)}`;
// Step 3: Generate API key if needed
const apiKey = input.autoGenerateApiKey
? `fn_${randomUUID().replace(/-/g, "")}`
: input.apiKey ?? "";
// Step 4: Build environment
const envArray: string[] = [
`FUSION_NODE_NAME=${input.nodeName}`,
`FUSION_API_KEY=${apiKey}`,
"FUSION_MODE=serve",
"FUSION_PORT=4040",
"FUSION_DATA_DIR=/data",
];
if (input.reachableUrl) {
envArray.push(`FUSION_REACHABLE_URL=${input.reachableUrl}`);
}
if (input.extraClis && input.extraClis.length > 0) {
envArray.push(`FUSION_EXTRA_CLIS=${input.extraClis.join(",")}`);
}
// User-provided environment appended last (allows overrides)
if (input.environment) {
envArray.push(...input.environment);
}
// Step 5: Build volume mounts
const mounts: string[] = [];
if (input.persistentVolume) {
mounts.push(`${input.persistentVolume}:/data`);
}
if (input.volumeMounts) {
mounts.push(...input.volumeMounts);
}
// Step 6: Build container create options
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createOptions: any = {
name: containerName,
Image: imageRef,
Env: envArray,
HostConfig: {
PortBindings: { "4040/tcp": [{ HostPort: "0" }] },
...(mounts.length > 0 ? { Binds: mounts } : {}),
...(input.resourceConfig?.memoryLimitMb
? { Memory: input.resourceConfig.memoryLimitMb * 1024 * 1024 }
: {}),
...(input.resourceConfig?.cpuLimit
? { NanoCpus: Math.round(input.resourceConfig.cpuLimit * 1e9) }
: {}),
...(input.resourceConfig?.memorySwapMb !== undefined
? { MemorySwap: input.resourceConfig.memorySwapMb * 1024 * 1024 }
: {}),
RestartPolicy: { Name: "unless-stopped" },
},
...(input.network
? { NetworkingConfig: { EndpointsConfig: { [input.network]: {} } } }
: {}),
Labels: {
"fusion.managed": "true",
"fusion.node-name": input.nodeName,
...(input.labels || {}),
},
ExposedPorts: { "4040/tcp": {} },
};
// Step 7: Create container
let container: Docker.Container;
try {
container = await docker.createContainer(createOptions);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log.error(`Container creation failed: ${message}`);
return {
success: false,
error: `Failed to create container: ${message}`,
failedStage: "container-create",
durationMs: Date.now() - startTime,
};
}
// Step 8: Start container
try {
await container.start();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log.error(`Container start failed: ${message}, cleaning up...`);
// Attempt cleanup
try {
await container.remove({ force: true });
} catch {
// Best effort cleanup
}
return {
success: false,
error: `Failed to start container: ${message}`,
failedStage: "container-start",
durationMs: Date.now() - startTime,
};
}
// Step 9: Inspect to get port mapping
let portMapping: string | undefined;
try {
const inspectResult = await container.inspect();
const portInfo = inspectResult.NetworkSettings?.Ports?.["4040/tcp"]?.[0];
if (portInfo?.HostPort) {
portMapping = `4040:${portInfo.HostPort}`;
}
} catch {
// Container is running, port info is nice-to-have
}
const durationMs = Date.now() - startTime;
log.debug(`Container ${containerName} provisioned successfully in ${durationMs}ms`);
return {
success: true,
containerId: container.id,
containerName,
apiKey,
portMapping,
durationMs,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log.error(`Unexpected provisioning error: ${message}`);
return {
success: false,
error: message,
failedStage: "container-create",
durationMs: Date.now() - startTime,
};
}
}
/**
* Deprovision a Docker container — stop and remove it.
*/
async deprovision(
containerId: string,
hostConfig: DockerHostConfig,
removeVolumes: boolean,
): Promise<{ success: boolean; error?: string }> {
try {
const docker = await this.dockerClientService.getDockerInstance(hostConfig);
const container = docker.getContainer(containerId);
// Stop with timeout — ignore "already stopped" errors
try {
await container.stop({ t: 10 });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes("already stopped") && !message.includes("is not running")) {
log.warn(`Container stop returned: ${message}`);
}
}
await container.remove({ force: true, v: removeVolumes });
log.debug(`Container ${containerId} deprovisioned`);
return { success: true };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log.error(`Deprovision failed for ${containerId}: ${message}`);
return { success: false, error: message };
}
}
/**
* Get the runtime status of a container.
* Creates a Docker client for the given hostConfig to query container info.
*/
async getContainerStatus(
containerId: string,
hostConfig: DockerHostConfig,
): Promise<DockerContainerInspectResult | null> {
try {
const docker = await this.dockerClientService.getDockerInstance(hostConfig);
const inspect = await docker.getContainer(containerId).inspect();
return {
id: inspect.Id,
name: (inspect.Name ?? "").replace(/^\//, ""),
status: inspect.State?.Status ?? "unknown",
image: inspect.Config?.Image ?? "",
created: inspect.Created ? Date.parse(inspect.Created) : 0,
state: {
running: Boolean(inspect.State?.Running),
paused: Boolean(inspect.State?.Paused),
restarting: Boolean(inspect.State?.Restarting),
dead: Boolean(inspect.State?.Dead),
error: inspect.State?.Error || undefined,
},
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("404") || message.toLowerCase().includes("no such container")) {
return null;
}
throw error;
}
}
/**
* Start an existing container.
*/
async startContainer(
containerId: string,
hostConfig: DockerHostConfig,
): Promise<{ success: boolean; error?: string }> {
try {
const docker = await this.dockerClientService.getDockerInstance(hostConfig);
const container = docker.getContainer(containerId);
await container.start();
return { success: true };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { success: false, error: message };
}
}
/**
* Stop a running container with a 10-second timeout.
*/
async stopContainer(
containerId: string,
hostConfig: DockerHostConfig,
): Promise<{ success: boolean; error?: string }> {
try {
const docker = await this.dockerClientService.getDockerInstance(hostConfig);
const container = docker.getContainer(containerId);
await container.stop({ t: 10 });
return { success: true };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { success: false, error: message };
}
}
/**
* Restart a container (stop then start).
*/
async restartContainer(
containerId: string,
hostConfig: DockerHostConfig,
): Promise<{ success: boolean; error?: string }> {
try {
const docker = await this.dockerClientService.getDockerInstance(hostConfig);
const container = docker.getContainer(containerId);
await container.restart();
return { success: true };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { success: false, error: message };
}
}
}