src/services/local-n8n-sidecar-installer.js
Rama main · réplica de lectura del repositorio original.
import { createHash, randomBytes as createRandomBytes, randomUUID } from "node:crypto";
import * as defaultFileSystem from "node:fs/promises";
import { homedir } from "node:os";
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
import {
LOCAL_N8N_SIDECAR_ENDPOINT,
LOCAL_N8N_SIDECAR_HOSTNAME,
LOCAL_N8N_SIDECAR_TARGET,
createLocalN8nSidecarComposeFile,
createLocalN8nSidecarDockerfile,
createLocalN8nSidecarDockerignore,
normalizeLocalN8nSidecarPlan,
validateDockerObjectId,
} from "../domain/local-n8n-sidecar.js";
import {
safeSiwcCatalogFailure, safeSiwcRuntimeFailure,
validateSiwcAuthBinding, validateSiwcRegistrationId,
} from "../domain/safety.js";
import { validateDockerName } from "../domain/validation.js";
import { readCodexImagesCliResult } from "../domain/codex-images.js";
import {
runLocalProcess,
lockDownLocalPath,
validateLocalDockerHost,
} from "../infrastructure/local-process.js";
import { collectSiwcRuntimeAssets } from "./siwc-runtime-assets.js";
import {
finishAuthHandoff, prepareAuthHandoff, readAuthHandoff, readPendingAuthHandoff, readRegistration, listRegistrations,
validateSiwcBackgroundConsent, validateSiwcHostId,
} from "./siwc-session.mjs";
import {
acquireLocalIntegrationLifecycleLock,
settleLocalIntegrationLifecycleOperation,
fingerprintLocalSiwcFiles, fingerprintLocalSiwcResources, localSiwcFinalizationFailure,
localSiwcStagingPath, readLocalSiwcStaging, readLocalSiwcResumeAuthBinding,
assertNoLocalSiwcOneOffContainers,
} from "./local-integration-lifecycle-lock.js";
import { usageRecordFromCli } from "./model-discovery.mjs";
const COMPOSE_FILENAME = "docker-compose.yml";
const MANAGED_MARKER = ".managed-by-relmio.json";
const ROOT_MARKER = ".managed-by-relmio-root.json";
const ROOT_MARKER_SCHEMA_VERSION = 1;
const MARKER_SCHEMA_VERSION = 2;
const PROJECT_PREFIX = "relmio-n8n-openai-oauth";
const SERVICE_NAME = "openai-oauth";
const MAX_DISCOVERED_CONTAINERS = 100;
const MAX_DOCKER_METADATA_BYTES = 1024 * 1024;
const DOCKER_IMAGE_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
const DOCKER_SELECTION_VARIABLES = new Set([
"BUILDKIT_HOST",
"DOCKER_CERT_PATH",
"DOCKER_CONFIG",
"DOCKER_CONTEXT",
"DOCKER_HOST",
"DOCKER_TLS_VERIFY",
]);
const SIWC_ASSET_PATHS = new Set([
"gateway/openai-oauth-sidecar.mjs", "gateway/codex-chat.js",
"gateway/codex-app-server.mjs", "services/siwc-session.mjs",
"services/siwc-handoff.mjs", "infrastructure/local-process.js",
"services/local-integration-lifecycle-lock.js", "services/codex-images.mjs", "services/model-discovery.mjs",
"infrastructure/process-identity.js",
]);
const OFFICIAL_N8N_IMAGE = /^(?:(?:(?:docker\.n8n\.io|docker\.io)\/)?n8nio\/n8n)(?::[A-Za-z0-9_.-]{1,128})?(?:@sha256:[a-f0-9]{64})?$/u;
const VERIFIER_SCRIPT = [
'(async()=>{let key="";for await(const chunk of process.stdin){key+=chunk;',
'if(key.length>128)process.exit(1)}',
'const headers={Authorization:"Bearer "+key};',
'const [health,models]=await Promise.all([fetch("http://n8n-openai-oauth:10531/health"),',
'fetch("http://n8n-openai-oauth:10531/v1/models",{headers})]);',
'if(!health.ok||!models.ok)process.exit(1);',
'process.stdout.write(JSON.stringify(await models.json()))})().catch(()=>process.exit(1));',
].join("");
function isMissing(error) {
return error?.code === "ENOENT";
}
function validateDockerImageDigest(value) {
if (typeof value !== "string" || !DOCKER_IMAGE_DIGEST_PATTERN.test(value)) {
throw new TypeError("Local sidecar runtime image identity is invalid.");
}
return value;
}
function assertSupportedPlatform(platform) {
if (typeof platform !== "string" || platform.length === 0) {
throw new TypeError("The local platform is invalid.");
}
}
function rejectDockerEnvironmentOverrides(env) {
if (!env || typeof env !== "object" || Array.isArray(env)) {
throw new TypeError("The local process environment is invalid.");
}
for (const [name, value] of Object.entries(env)) {
if (
DOCKER_SELECTION_VARIABLES.has(name.toUpperCase()) &&
typeof value === "string" &&
value !== ""
) {
throw new Error(
"Relmio local sidecars require the selected Docker context without a Docker environment override.",
);
}
}
}
function validateAbsolutePath(value) {
if (
typeof value !== "string" ||
value.trim() === "" ||
value.includes("\0") ||
!isAbsolute(value)
) {
throw new TypeError("Relmio local storage path is invalid.");
}
return resolve(value);
}
function validateManagedBase(value) {
const resolved = validateAbsolutePath(value);
if (basename(resolved) !== ".relmio") {
throw new TypeError("Relmio local storage path is invalid.");
}
return resolved;
}
function validateInstallRoot(value) {
const resolved = validateAbsolutePath(value);
if (
basename(resolved) !== LOCAL_N8N_SIDECAR_TARGET ||
basename(dirname(resolved)) !== "local" ||
basename(resolve(resolved, "..", "..")) !== ".relmio"
) {
throw new TypeError("The local n8n sidecar install directory is invalid.");
}
return resolved;
}
export async function resolveLocalN8nSidecarInstallRoot({
env = process.env,
homeDirectory = homedir(),
fileSystem = defaultFileSystem,
platform = process.platform,
} = {}) {
assertSupportedPlatform(platform);
const configuredHome =
typeof env.RELMIO_HOME === "string" && env.RELMIO_HOME.trim() !== ""
? env.RELMIO_HOME
: resolve(homeDirectory, ".relmio");
const requestedHome = validateManagedBase(configuredHome);
const requestedParent = dirname(requestedHome);
let canonicalParent;
try {
canonicalParent = await fileSystem.realpath(requestedParent);
} catch {
throw new Error("The parent of the Relmio local storage directory is invalid.");
}
if (canonicalParent !== resolve(requestedParent)) {
throw new Error(
"Relmio refuses a local storage path with a symbolic-link ancestor.",
);
}
return join(canonicalParent, ".relmio", "local", LOCAL_N8N_SIDECAR_TARGET);
}
function localN8nSidecarSnapshot(marker, account = null) {
const migrationRequired = marker.schemaVersion !== MARKER_SCHEMA_VERSION;
const connected = !migrationRequired && account?.ownership === "owned" &&
account?.session === "connected" && account?.planEnabled === true;
return {
target: LOCAL_N8N_SIDECAR_TARGET,
endpoint: LOCAL_N8N_SIDECAR_ENDPOINT,
auth: {
configured: connected,
disclosure: "server-managed",
...(!migrationRequired && account ? { account } : {}),
},
...(!migrationRequired ? { registrationId: marker.registrationId } : {}),
migrationRequired,
canRefreshCredential: false,
canRemove: true,
};
}
function parseSidecarStatusRecord(output, marker) {
let records;
try {
const parsed = JSON.parse(output);
records = Array.isArray(parsed) ? parsed : [parsed];
} catch {
throw new Error("The local sidecar status metadata is invalid.");
}
const record = records[0];
if (
records.length !== 1 ||
record?.Name !== `${marker.projectName}-${SERVICE_NAME}-1` ||
record?.Service !== SERVICE_NAME ||
typeof record?.State !== "string" ||
typeof record?.Health !== "string"
) {
throw new Error("The local sidecar status metadata is invalid.");
}
assertNoPublishedHostPort(output);
return record;
}
async function verifyWindowsSidecarStatusPathSecurity({
fileSystem,
installRoot,
platform,
lockDownPath,
}) {
if (platform !== "win32") return;
const relmioHome = resolve(installRoot, "..", "..");
for (const path of [relmioHome, join(relmioHome, "local"), installRoot]) {
assertDirectory(await lstatIfExists(fileSystem, path));
await lockDownPath(path, { platform, verifyOnly: true });
}
for (const path of [
join(relmioHome, ROOT_MARKER),
join(installRoot, MANAGED_MARKER),
join(installRoot, COMPOSE_FILENAME),
]) {
const metadata = await lstatIfExists(fileSystem, path);
if (!metadata?.isFile?.() || metadata.isSymbolicLink()) {
throw new Error("Relmio refuses an unsafe local sidecar managed file.");
}
await lockDownPath(path, {
platform,
kind: "file",
verifyOnly: true,
verifyEffectiveOwnerOnly: true,
});
}
}
export async function getLocalN8nSidecarStatus({
env = process.env,
homeDirectory = homedir(),
fileSystem = defaultFileSystem,
runProcess = runLocalProcess,
platform = process.platform,
lockDownPath = lockDownLocalPath,
} = {}) {
const absent = {
target: LOCAL_N8N_SIDECAR_TARGET,
managed: false,
state: "absent",
};
const unavailable = {
target: LOCAL_N8N_SIDECAR_TARGET,
managed: false,
state: "unavailable",
};
try {
const installRoot = await resolveLocalN8nSidecarInstallRoot({
env,
homeDirectory,
fileSystem,
platform,
});
const staged = await readLocalSiwcStaging({ fileSystem, installRoot, platform, lockDownPath });
if (staged && staged.checkpoint.stage !== "completed") {
await fingerprintLocalSiwcFiles({ fileSystem, installRoot, platform, lockDownPath });
const { installId, registrationId, stage } = staged.checkpoint;
return { target: LOCAL_N8N_SIDECAR_TARGET, managed: true, state: "staged",
staging: { installId, registrationId, stage } };
}
if (await lstatIfExists(fileSystem, installRoot)) {
await verifyWindowsSidecarStatusPathSecurity({
fileSystem,
installRoot,
platform,
lockDownPath,
});
}
const managed = await inspectManagedInstall({ fileSystem, installRoot });
if (!managed.marker) return absent;
const marker = validateMarker(managed.marker);
if (platform === "win32" && marker.schemaVersion === MARKER_SCHEMA_VERSION) {
for (const relative of [
"Dockerfile", ".dockerignore", "package.json", "package-lock.json",
"services/siwc-session.mjs", "services/siwc-handoff.mjs",
"gateway/openai-oauth-sidecar.mjs", "infrastructure/local-process.js",
"services/local-integration-lifecycle-lock.js", "services/codex-images.mjs", "services/model-discovery.mjs",
"infrastructure/process-identity.js",
]) {
const path = join(installRoot, relative);
const metadata = await lstatIfExists(fileSystem, path);
// Installs made before the image add-on or model discovery lack these files; verify them whenever present.
if (!metadata && ["services/codex-images.mjs", "services/model-discovery.mjs"].includes(relative)) continue;
if (!metadata?.isFile?.() || metadata.isSymbolicLink()) {
throw new Error("The installed SIWC runtime asset is missing or unsafe.");
}
await lockDownPath(path, {
platform, kind: "file", verifyOnly: true, verifyEffectiveOwnerOnly: true,
});
}
}
if (marker.schemaVersion === MARKER_SCHEMA_VERSION) {
await assertManagedSiwcCompose(fileSystem, installRoot, marker);
}
const selectedDockerHost = await resolveLocalDockerHost({
runProcess,
cwd: installRoot,
env,
platform,
});
if (selectedDockerHost !== marker.dockerHost) return unavailable;
if (marker.schemaVersion === MARKER_SCHEMA_VERSION && marker.legacyInstallId) {
const migrationState = await readLegacyN8nMigrationState(
fileSystem, installRoot, marker.legacyInstallId,
);
if (migrationState !== "transferred") {
return {
target: LOCAL_N8N_SIDECAR_TARGET, managed: true, state: "partial",
snapshot: {
target: LOCAL_N8N_SIDECAR_TARGET,
endpoint: LOCAL_N8N_SIDECAR_ENDPOINT,
auth: { configured: false, disclosure: "server-managed" },
canRefreshCredential: false, canRemove: true,
migrationRequired: !marker.previousWasSiwc,
replacementRequired: !!marker.previousWasSiwc,
migrationState: migrationState ?? "incomplete",
legacyResourcesPreserved: true,
},
};
}
}
await attestPlanAndAlias({
plan: marker,
runProcess,
cwd: installRoot,
installId: marker.installId,
projectName: marker.projectName,
});
const ownership = await attestProjectOwnership({
runProcess,
cwd: installRoot,
dockerHost: marker.dockerHost,
installId: marker.installId,
projectName: marker.projectName,
returnDetails: true,
volumeName: marker.schemaVersion === MARKER_SCHEMA_VERSION ? "siwc-store" : "oauth-auth",
});
if (!ownership.exact) {
return {
target: LOCAL_N8N_SIDECAR_TARGET,
managed: true,
state: "partial",
};
}
if (marker.schemaVersion !== MARKER_SCHEMA_VERSION) {
const migrationState = await readLegacyN8nMigrationState(
fileSystem, installRoot, marker.installId,
);
if (migrationState === "transferred") {
throw new Error("The legacy n8n migration metadata is inconsistent.");
}
return {
target: LOCAL_N8N_SIDECAR_TARGET,
managed: true, state: migrationState ? "partial" : "legacy",
snapshot: {
...localN8nSidecarSnapshot(marker),
...(migrationState ? { migrationState, legacyResourcesPreserved: true } : {}),
},
};
}
const publication = await runOrThrow(
runProcess,
{
file: "docker",
args: createComposeArgs(marker.projectName, [
"ps",
"--all",
"--format",
"json",
SERVICE_NAME,
]),
cwd: installRoot,
dockerHost: marker.dockerHost,
},
"Local sidecar inventory publication check",
);
const statusRecord = parseSidecarStatusRecord(publication.stdout, marker);
const runtime = await inspectOwnedSidecarRuntime({
runProcess,
installRoot,
marker,
});
let account = null;
if (runtime?.running && !runtime.paused &&
statusRecord.State === "running") {
try {
account = (await runSiwcCli({
runProcess, installRoot, marker, command: "account-live",
})).account;
if (account?.registrationId !== marker.registrationId ||
account?.ownerHostId !== marker.ownerHostId ||
account?.ownerRuntimeId !== marker.installId ||
account?.ownership !== "owned") {
throw new Error("The installed SIWC account cannot be attested.");
}
} catch {
return {
target: LOCAL_N8N_SIDECAR_TARGET,
managed: true, state: "partial",
snapshot: localN8nSidecarSnapshot(marker),
};
}
}
const snapshot = localN8nSidecarSnapshot(marker, account);
if (!runtime) {
return {
target: LOCAL_N8N_SIDECAR_TARGET,
managed: true,
state: "partial",
};
}
if (runtime.paused) {
return {
target: LOCAL_N8N_SIDECAR_TARGET,
managed: true,
state: "partial",
};
}
if (!runtime.running) {
return ["created", "exited"].includes(statusRecord.State) &&
!["starting", "unhealthy"].includes(statusRecord.Health)
? {
target: LOCAL_N8N_SIDECAR_TARGET,
managed: true,
state: "stopped",
snapshot,
}
: {
target: LOCAL_N8N_SIDECAR_TARGET,
managed: true,
state: "partial",
};
}
if (
statusRecord.State !== "running" ||
statusRecord.Health !== "healthy" ||
runtime.health !== "healthy"
) {
return {
target: LOCAL_N8N_SIDECAR_TARGET,
managed: true,
state: "partial",
};
}
await verifyRunningSidecar({
runProcess,
installRoot,
plan: marker,
installId: marker.installId,
projectName: marker.projectName,
});
return {
target: LOCAL_N8N_SIDECAR_TARGET,
managed: true,
state: "healthy",
snapshot,
};
} catch {
return unavailable;
}
}
// Read-only: the installed local sidecar's stored request counts, as usageRecordFromCli reads them, or
// undefined when no running SIWC sidecar owned by this install can be attested. Nothing is changed.
export async function getLocalN8nSidecarUsage({
env = process.env,
homeDirectory = homedir(),
fileSystem = defaultFileSystem,
runProcess = runLocalProcess,
platform = process.platform,
lockDownPath = lockDownLocalPath,
} = {}) {
try {
const installRoot = await resolveLocalN8nSidecarInstallRoot({ env, homeDirectory, fileSystem, platform });
if (!await lstatIfExists(fileSystem, installRoot)) return undefined;
await verifyWindowsSidecarStatusPathSecurity({ fileSystem, installRoot, platform, lockDownPath });
const managed = await inspectManagedInstall({ fileSystem, installRoot });
if (!managed.marker) return undefined;
const marker = validateMarker(managed.marker);
await assertManagedSiwcCompose(fileSystem, installRoot, marker);
if (await resolveLocalDockerHost({ runProcess, cwd: installRoot, env, platform }) !== marker.dockerHost) return undefined;
const runtime = await inspectOwnedSidecarRuntime({ runProcess, installRoot, marker });
if (!runtime?.running || runtime.paused) return undefined;
await attestProjectOwnership({ runProcess, cwd: installRoot, dockerHost: marker.dockerHost,
installId: marker.installId, projectName: marker.projectName });
return usageRecordFromCli(await runProcess({
file: "docker",
args: createComposeArgs(marker.projectName, [
"exec", "-T", SERVICE_NAME, "node", "/app/services/model-discovery.mjs", "usage",
]),
cwd: installRoot,
dockerHost: marker.dockerHost,
}), marker.registrationId);
} catch {
return undefined;
}
}
async function resolveLocalDockerHost({ runProcess, cwd, env, platform }) {
assertSupportedPlatform(platform);
rejectDockerEnvironmentOverrides(env);
const context = await runProcess({
file: "docker",
args: [
"context",
"inspect",
"--format",
"{{json .Endpoints.docker.Host}}",
],
cwd,
});
if (context.code !== 0) {
throw new Error("The selected Docker context could not be inspected.");
}
let candidate;
try {
candidate = JSON.parse(context.stdout.trim());
} catch {
throw new Error("The selected Docker context is not a local Docker daemon.");
}
let dockerHost;
try {
dockerHost = validateLocalDockerHost(candidate, { platform });
} catch {
throw new Error("The selected Docker context is not a local Docker daemon.");
}
if (platform === "win32" && dockerHost.startsWith("npipe:")) {
const selectedContext = await runProcess({ file: "docker", args: ["context", "show"], cwd });
if (selectedContext.code !== 0 || selectedContext.stdout.trim() !== "desktop-linux") {
throw new Error("Docker Desktop's local Linux engine must be selected.");
}
}
return dockerHost;
}
async function runOrThrow(runProcess, spec, label) {
const result = await runProcess(spec);
if (result.code !== 0) {
throw new Error(`${label} failed.`);
}
return result;
}
function parseJson(value, label) {
if (
typeof value !== "string" ||
value.length === 0 ||
Buffer.byteLength(value) > MAX_DOCKER_METADATA_BYTES
) {
throw new Error(`${label} returned invalid Docker metadata.`);
}
try {
return JSON.parse(value);
} catch {
throw new Error(`${label} returned invalid Docker metadata.`);
}
}
function parseJsonLines(value, label) {
if (typeof value !== "string" || Buffer.byteLength(value) > MAX_DOCKER_METADATA_BYTES) {
throw new Error(`${label} returned invalid Docker metadata.`);
}
if (value.trim() === "") {
return [];
}
return value
.trim()
.split("\n")
.map((line) => parseJson(line, label));
}
function validateVersion(value, label) {
const normalized = typeof value === "string" ? value.trim() : "";
if (!/^[A-Za-z0-9.+-]{1,64}$/u.test(normalized)) {
throw new Error(`${label} returned an invalid version.`);
}
return normalized;
}
function isOfficialN8nImage(value) {
return typeof value === "string" && OFFICIAL_N8N_IMAGE.test(value);
}
function parseContainerName(value) {
const normalized = typeof value === "string" && value.startsWith("/")
? value.slice(1)
: value;
return validateDockerName(normalized);
}
async function inspectContainer({ runProcess, cwd, dockerHost, containerId }) {
const result = await runOrThrow(
runProcess,
{
file: "docker",
args: ["container", "inspect", "--format", "{{json .}}", containerId],
cwd,
dockerHost,
},
"Docker container inspection",
);
return parseJson(result.stdout.trim(), "Docker container inspection");
}
async function inspectNetwork({ runProcess, cwd, dockerHost, networkName }) {
const result = await runOrThrow(
runProcess,
{
file: "docker",
args: ["network", "inspect", "--format", "{{json .}}", networkName],
cwd,
dockerHost,
},
"Docker network inspection",
);
return parseJson(result.stdout.trim(), "Docker network inspection");
}
function validateDiscoveredN8nContainer(inspected) {
const containerId = validateDockerObjectId(inspected?.Id, "n8n container");
const containerName = parseContainerName(inspected?.Name);
const image = inspected?.Config?.Image;
if (!isOfficialN8nImage(image) || inspected?.State?.Running !== true) {
throw new Error("The selected container is not a running official n8n container.");
}
if (
!inspected.NetworkSettings?.Networks ||
typeof inspected.NetworkSettings.Networks !== "object" ||
Array.isArray(inspected.NetworkSettings.Networks)
) {
throw new Error("The selected n8n container network metadata is invalid.");
}
return { containerId, containerName, image };
}
function validateSelectedNetwork(inspected, { n8nContainerId, expectedName }) {
const dockerNetworkId = validateDockerObjectId(
inspected?.Id,
"Docker network",
);
const networkName = validateDockerName(inspected?.Name);
if (
networkName !== expectedName ||
inspected?.Driver !== "bridge" ||
inspected?.Scope !== "local" ||
inspected?.Internal !== false ||
!inspected?.Containers ||
typeof inspected.Containers !== "object" ||
Array.isArray(inspected.Containers) ||
(n8nContainerId !== undefined && !Object.prototype.hasOwnProperty.call(inspected.Containers, n8nContainerId))
) {
if (inspected?.Internal === true) {
throw new Error(
"The selected n8n Docker network has no outbound Internet access. Choose a non-internal Docker network and review a fresh plan.",
);
}
throw new Error("The selected n8n Docker network is invalid.");
}
const labels = inspected.Labels;
return {
dockerNetworkId,
networkName,
disposable:
labels &&
typeof labels === "object" &&
labels["com.relmio.disposable"] === "true",
};
}
export async function discoverLocalN8nSidecarTargets({
runProcess = runLocalProcess,
cwd = process.cwd(),
env = process.env,
platform = process.platform,
} = {}) {
const dockerHost = await resolveLocalDockerHost({
runProcess,
cwd,
env,
platform,
});
const docker = await runOrThrow(
runProcess,
{
file: "docker",
args: ["version", "--format", "{{.Server.Version}}"],
cwd,
dockerHost,
},
"Docker version check",
);
const compose = await runOrThrow(
runProcess,
{
file: "docker",
args: ["compose", "version", "--short"],
cwd,
dockerHost,
},
"Docker Compose version check",
);
const listed = await runOrThrow(
runProcess,
{
file: "docker",
args: ["ps", "--filter", "status=running", "--format", "{{json .}}"],
cwd,
dockerHost,
},
"n8n container discovery",
);
const candidates = parseJsonLines(listed.stdout, "n8n container discovery")
.filter((entry) => isOfficialN8nImage(entry?.Image))
.slice(0, MAX_DISCOVERED_CONTAINERS);
const containers = [];
for (const candidate of candidates) {
if (typeof candidate?.ID !== "string" || !/^[a-f0-9]{12,64}$/u.test(candidate.ID)) {
continue;
}
const inspected = await inspectContainer({
runProcess,
cwd,
dockerHost,
containerId: candidate.ID,
});
const container = validateDiscoveredN8nContainer(inspected);
const networks = [];
for (const networkName of Object.keys(inspected.NetworkSettings.Networks)) {
let safeNetworkName;
try {
safeNetworkName = validateDockerName(networkName);
} catch {
continue;
}
if (["bridge", "host", "none"].includes(safeNetworkName)) {
continue;
}
const network = await inspectNetwork({
runProcess,
cwd,
dockerHost,
networkName: safeNetworkName,
});
try {
networks.push(
validateSelectedNetwork(network, {
n8nContainerId: container.containerId,
expectedName: safeNetworkName,
}),
);
} catch {
// Discovery omits networks that cannot safely host the private sidecar.
}
}
if (networks.length > 0) {
containers.push({ ...container, networks });
}
}
return {
dockerAvailable: true,
dockerVersion: validateVersion(docker.stdout, "Docker"),
composeVersion: validateVersion(compose.stdout, "Docker Compose"),
dockerHost,
containers,
};
}
async function lstatIfExists(fileSystem, path) {
try {
return await fileSystem.lstat(path);
} catch (error) {
if (isMissing(error)) {
return null;
}
throw new Error("Relmio could not inspect its local managed directory.");
}
}
function assertDirectory(metadata) {
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
throw new Error("Relmio refuses an unsafe local managed directory.");
}
}
async function writeManagedFile(fileSystem, path, contents, mode) {
const existing = await lstatIfExists(fileSystem, path);
if (existing && (existing.isSymbolicLink() || !existing.isFile())) {
throw new Error("Relmio refuses to replace a non-file in its managed directory.");
}
const temporaryPath = `${path}.tmp-${randomUUID()}`;
try {
await fileSystem.writeFile(temporaryPath, contents, { flag: "wx", mode });
await fileSystem.chmod(temporaryPath, mode);
await fileSystem.rename(temporaryPath, path);
await fileSystem.chmod(path, mode);
} catch {
try {
await fileSystem.unlink(temporaryPath);
} catch {
// The temporary file may not have been created.
}
throw new Error("Relmio could not write its local n8n sidecar files.");
}
}
async function ensurePrivateDirectory(fileSystem, path, platform, lockDownPath) {
const metadata = await lstatIfExists(fileSystem, path);
if (metadata) {
assertDirectory(metadata);
} else {
await fileSystem.mkdir(path, { mode: 0o700 });
}
await fileSystem.chmod(path, 0o700);
if (platform === "win32") await lockDownPath(path, { platform });
}
async function inspectManagedInstall({ fileSystem, installRoot, staging }) {
const relmioHome = resolve(installRoot, "..", "..");
const localRoot = join(relmioHome, "local");
const homeMetadata = await lstatIfExists(fileSystem, relmioHome);
if (!homeMetadata) {
return { baseExists: false, marker: null, deploymentMode: "installed" };
}
assertDirectory(homeMetadata);
const rootMarkerMetadata = await lstatIfExists(
fileSystem,
join(relmioHome, ROOT_MARKER),
);
if (
!rootMarkerMetadata ||
rootMarkerMetadata.isSymbolicLink() ||
!rootMarkerMetadata.isFile()
) {
throw new Error(
"The Relmio local storage directory is not an owned managed root. Nothing was changed.",
);
}
let rootMarker;
try {
rootMarker = JSON.parse(
await fileSystem.readFile(join(relmioHome, ROOT_MARKER), "utf8"),
);
} catch {
throw new Error("The Relmio local managed-root marker is invalid.");
}
if (
rootMarker?.schemaVersion !== ROOT_MARKER_SCHEMA_VERSION ||
rootMarker?.kind !== "relmio-local-root"
) {
throw new Error("The Relmio local managed-root marker is invalid.");
}
for (const path of [localRoot, installRoot]) {
const metadata = await lstatIfExists(fileSystem, path);
if (metadata) {
assertDirectory(metadata);
}
}
const installMetadata = await lstatIfExists(fileSystem, installRoot);
if (!installMetadata) {
return { baseExists: true, marker: null, deploymentMode: "installed" };
}
if (staging && !(await lstatIfExists(fileSystem, join(installRoot, MANAGED_MARKER)))) {
return { baseExists: true, marker: null, deploymentMode: "installed" };
}
const markerMetadata = await lstatIfExists(
fileSystem,
join(installRoot, MANAGED_MARKER),
);
if (
!markerMetadata ||
markerMetadata.isSymbolicLink() ||
!markerMetadata.isFile()
) {
throw new Error(
"The local n8n sidecar directory is unmanaged. Nothing was overwritten.",
);
}
let marker;
try {
marker = JSON.parse(
await fileSystem.readFile(join(installRoot, MANAGED_MARKER), "utf8"),
);
validateMarker(marker);
} catch {
throw new Error("The local n8n sidecar managed marker is invalid.");
}
return { baseExists: true, marker, deploymentMode: "updated" };
}
function validateMarker(marker) {
const installId =
typeof marker?.installId === "string" && /^[a-f0-9]{32}$/u.test(marker.installId)
? marker.installId
: null;
const expectedProjectName = installId ? `${PROJECT_PREFIX}-${installId}` : null;
if (
![1, MARKER_SCHEMA_VERSION].includes(marker?.schemaVersion) ||
marker?.kind !== "relmio-local-n8n-sidecar" ||
marker?.target !== LOCAL_N8N_SIDECAR_TARGET ||
marker?.projectName !== expectedProjectName
) {
throw new TypeError("The local n8n sidecar marker is invalid.");
}
validateLocalDockerHost(marker.dockerHost);
validateDockerObjectId(marker.n8nContainerId, "n8n container");
validateDockerName(marker.n8nContainerName);
validateDockerObjectId(marker.dockerNetworkId, "Docker network");
validateDockerName(marker.networkName);
if (marker.schemaVersion === MARKER_SCHEMA_VERSION) {
validateSiwcRegistrationId(marker.registrationId);
validateSiwcHostId(marker.ownerHostId);
if (typeof marker.clientId !== "string" || !/^[!-~]{1,256}$/u.test(marker.clientId) ||
typeof marker.tokenSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(marker.tokenSha256)) {
throw new TypeError("The local n8n sidecar marker is invalid.");
}
if (marker.legacyInstallId !== undefined) {
if (typeof marker.legacyInstallId !== "string" ||
!/^[a-f0-9]{32}$/u.test(marker.legacyInstallId) ||
marker.legacyInstallId === installId) {
throw new TypeError("The local n8n migration marker is invalid.");
}
}
if (marker.previousWasSiwc !== undefined &&
(marker.previousWasSiwc !== true || !marker.legacyInstallId)) {
throw new TypeError("The local n8n replacement marker is invalid.");
}
}
return marker;
}
async function readLegacyN8nMigrationState(fileSystem, installRoot, installId) {
const legacyRoot = join(installRoot, "legacy");
const parent = await lstatIfExists(fileSystem, legacyRoot);
if (!parent) return null;
assertDirectory(parent);
const archive = join(legacyRoot, installId);
const metadata = await lstatIfExists(fileSystem, archive);
if (!metadata) return null;
assertDirectory(metadata);
const journalPath = join(archive, "migration.json");
const journal = await lstatIfExists(fileSystem, journalPath);
if (!journal) return "incomplete";
if (!journal.isFile() || journal.isSymbolicLink() || journal.size > 4096) {
throw new Error("The local n8n migration journal is unsafe.");
}
let record;
try { record = JSON.parse(await fileSystem.readFile(journalPath, "utf8")); }
catch { throw new Error("The local n8n migration journal is invalid."); }
if (record?.schemaVersion !== 1 ||
record?.reviewedLegacy?.installId !== installId ||
!["prepared", "stopped", "transferred"].includes(record?.state)) {
throw new Error("The local n8n migration journal needs manual inspection.");
}
return record.state;
}
async function assertManagedSiwcCompose(fileSystem, installRoot, marker) {
if (marker.schemaVersion !== MARKER_SCHEMA_VERSION) {
throw new Error("The legacy bridge needs a fresh SIWC sign-in and reviewed migration.");
}
const path = join(installRoot, COMPOSE_FILENAME);
const metadata = await lstatIfExists(fileSystem, path);
if (!metadata?.isFile?.() || metadata.isSymbolicLink()) {
throw new Error("The installed SIWC Compose file is missing or unsafe.");
}
const expected = createLocalN8nSidecarComposeFile({
installId: marker.installId,
networkName: marker.networkName,
registrationId: marker.registrationId,
tokenSha256: marker.tokenSha256,
});
if (await fileSystem.readFile(path, "utf8") !== expected) {
throw new Error("The installed SIWC Compose file changed; the owned service was not mutated.");
}
}
async function initializeManagedDirectories({
fileSystem,
installRoot,
baseExists,
platform,
lockDownPath,
}) {
const relmioHome = resolve(installRoot, "..", "..");
if (!baseExists) {
let createdIdentity = null;
try {
await fileSystem.mkdir(relmioHome, { mode: 0o700 });
createdIdentity = await captureFreshDirectoryIdentity(fileSystem, relmioHome);
await fileSystem.chmod(relmioHome, 0o700);
if (platform === "win32") await lockDownPath(relmioHome, { platform });
await writeManagedFile(
fileSystem,
join(relmioHome, ROOT_MARKER),
`${JSON.stringify({
schemaVersion: ROOT_MARKER_SCHEMA_VERSION,
kind: "relmio-local-root",
})}\n`,
0o600,
);
} catch (error) {
await removeFreshEmptyManagedRoot({
fileSystem,
relmioHome,
createdIdentity,
});
throw error;
}
}
await fileSystem.chmod(relmioHome, 0o700);
if (platform === "win32") await lockDownPath(relmioHome, { platform });
await ensurePrivateDirectory(fileSystem, join(relmioHome, "local"), platform, lockDownPath);
await ensurePrivateDirectory(fileSystem, installRoot, platform, lockDownPath);
}
function isDirectoryIdentity(metadata, identity) {
return (
metadata !== null &&
!metadata.isSymbolicLink() &&
metadata.isDirectory() &&
identity !== null &&
metadata.dev === identity.dev &&
metadata.ino === identity.ino
);
}
async function captureFreshDirectoryIdentity(fileSystem, path) {
const metadata = await lstatIfExists(fileSystem, path);
if (!metadata || metadata.isSymbolicLink() || !metadata.isDirectory()) {
throw new Error("Relmio could not verify its newly created local storage directory.");
}
return { dev: metadata.dev, ino: metadata.ino };
}
async function removeFreshEmptyManagedRoot({
fileSystem,
relmioHome,
createdIdentity,
}) {
if (!createdIdentity) return;
try {
let metadata = await lstatIfExists(fileSystem, relmioHome);
if (!isDirectoryIdentity(metadata, createdIdentity)) return;
if ((await fileSystem.readdir(relmioHome)).length !== 0) return;
metadata = await lstatIfExists(fileSystem, relmioHome);
if (!isDirectoryIdentity(metadata, createdIdentity)) return;
await fileSystem.rmdir(relmioHome);
} catch {
// Only an exact, still-empty root may be removed during recovery.
}
}
async function acquireSidecarLock({
fileSystem,
getProcessIdentity,
installRoot,
lockDownPath,
now,
platform,
}) {
const safeInstallRoot = validateInstallRoot(installRoot);
const lockPath = join(
dirname(resolve(safeInstallRoot, "..", "..")),
".relmio-local-n8n-openai-oauth.lock",
);
return acquireLocalIntegrationLifecycleLock({
fileSystem,
getProcessIdentity,
lockDownPath,
lockPath,
now,
// Lock ownership is local-process state, not the caller's Docker fixture.
platform: process.platform,
label: "local n8n OAuth sidecar operation lock",
});
}
function requireOwnershipLabels(labels, { installId, projectName, service }) {
if (!labels || typeof labels !== "object" || Array.isArray(labels)) {
throw new Error("The local sidecar Docker ownership metadata is invalid.");
}
if (
labels["com.docker.compose.project"] !== projectName ||
labels["io.relmio.managed"] !== "true" ||
labels["io.relmio.target"] !== LOCAL_N8N_SIDECAR_TARGET ||
labels["io.relmio.install"] !== installId ||
(service && labels["com.docker.compose.service"] !== SERVICE_NAME)
) {
throw new Error(
"A Docker resource uses this Relmio project identity without matching ownership. Nothing was changed.",
);
}
}
async function attestProjectOwnership({
runProcess,
cwd,
dockerHost,
installId,
projectName,
returnDetails = false,
volumeName = "siwc-store",
}) {
const projectFilter = `label=com.docker.compose.project=${projectName}`;
const containers = await runOrThrow(
runProcess,
{
file: "docker",
args: [
"ps",
"--all",
"--no-trunc",
"--filter",
projectFilter,
"--format",
"{{json .}}",
],
cwd,
dockerHost,
},
"Local sidecar container ownership check",
);
const containerRows = parseJsonLines(
containers.stdout,
"Local sidecar container ownership check",
);
if (containerRows.length > MAX_DISCOVERED_CONTAINERS) {
throw new Error("The local sidecar container ownership check failed closed.");
}
for (const row of containerRows) {
const containerId = validateDockerObjectId(
row?.ID,
"local sidecar container",
);
const inspected = await inspectContainer({
runProcess,
cwd,
dockerHost,
containerId,
});
if (inspected?.Id !== containerId) {
throw new Error("The local sidecar Docker ownership metadata is invalid.");
}
requireOwnershipLabels(inspected.Config?.Labels, {
installId,
projectName,
service: true,
});
}
const volumes = await runOrThrow(
runProcess,
{
file: "docker",
args: ["volume", "ls", "--filter", projectFilter, "--format", "{{json .}}"],
cwd,
dockerHost,
},
"Local sidecar volume ownership check",
);
const volumeRows = parseJsonLines(
volumes.stdout,
"Local sidecar volume ownership check",
);
if (volumeRows.length > MAX_DISCOVERED_CONTAINERS) {
throw new Error("The local sidecar volume ownership check failed closed.");
}
const volumeNames = [];
for (const row of volumeRows) {
const volumeName = validateDockerName(row?.Name);
volumeNames.push(volumeName);
const inspected = await runOrThrow(
runProcess,
{
file: "docker",
args: [
"volume",
"inspect",
"--format",
"{{json .Labels}}",
volumeName,
],
cwd,
dockerHost,
},
"Local sidecar volume ownership inspection",
);
requireOwnershipLabels(
parseJson(
inspected.stdout.trim(),
"Local sidecar volume ownership inspection",
),
{ installId, projectName, service: false },
);
}
const imagePresent = await inspectOwnedImageIfPresent({
runProcess,
cwd,
dockerHost,
installId,
projectName,
});
if (returnDetails) {
return {
exact:
containerRows.length === 1 &&
volumeNames.length === 1 &&
volumeNames[0] === `${projectName}_${volumeName}` &&
imagePresent,
};
}
}
async function inspectOwnedImageIfPresent({
runProcess,
cwd,
dockerHost,
installId,
projectName,
}) {
const imageName = `${projectName}:local`;
const listed = await runOrThrow(
runProcess,
{
file: "docker",
args: [
"image",
"ls",
"--filter",
`reference=${imageName}`,
"--format",
"{{json .}}",
],
cwd,
dockerHost,
},
"Local sidecar image existence check",
);
const rows = parseJsonLines(
listed.stdout,
"Local sidecar image existence check",
);
if (rows.length === 0) {
return false;
}
if (
rows.length !== 1 ||
rows[0]?.Repository !== projectName ||
rows[0]?.Tag !== "local"
) {
throw new Error("The local sidecar image existence check failed closed.");
}
const image = await runOrThrow(
runProcess,
{
file: "docker",
args: [
"image",
"inspect",
"--format",
"{{json .Config.Labels}}",
imageName,
],
cwd,
dockerHost,
},
"Local sidecar image ownership check",
);
const labels = parseJson(
image.stdout.trim(),
"Local sidecar image ownership check",
);
if (
!labels ||
labels["io.relmio.managed"] !== "true" ||
labels["io.relmio.target"] !== LOCAL_N8N_SIDECAR_TARGET ||
labels["io.relmio.install"] !== installId
) {
throw new Error(
"A Docker image uses this Relmio project identity without matching ownership. Nothing was changed.",
);
}
return true;
}
function labelsMatchOwnedSidecar(labels, { installId, projectName }) {
return (
labels?.["com.docker.compose.project"] === projectName &&
labels?.["com.docker.compose.service"] === SERVICE_NAME &&
labels?.["io.relmio.managed"] === "true" &&
labels?.["io.relmio.target"] === LOCAL_N8N_SIDECAR_TARGET &&
labels?.["io.relmio.install"] === installId
);
}
async function attestPlanAndAlias({
plan,
runProcess,
cwd,
installId,
projectName,
attestN8n = true,
}) {
let n8n;
if (attestN8n) {
n8n = await inspectContainer({
runProcess,
cwd,
dockerHost: plan.dockerHost,
containerId: plan.n8nContainerId,
});
const normalizedN8n = validateDiscoveredN8nContainer(n8n);
const n8nNetwork = n8n.NetworkSettings.Networks?.[plan.networkName];
if (
normalizedN8n.containerId !== plan.n8nContainerId ||
normalizedN8n.containerName !== plan.n8nContainerName ||
n8nNetwork?.NetworkID !== plan.dockerNetworkId
) {
throw new Error(
"The selected n8n container changed. Create and confirm a fresh plan.",
);
}
}
const network = await inspectNetwork({
runProcess,
cwd,
dockerHost: plan.dockerHost,
networkName: plan.networkName,
});
const normalizedNetwork = validateSelectedNetwork(network, {
n8nContainerId: attestN8n ? plan.n8nContainerId : undefined,
expectedName: plan.networkName,
});
if (normalizedNetwork.dockerNetworkId !== plan.dockerNetworkId) {
throw new Error(
"The selected n8n Docker network changed. Create and confirm a fresh plan.",
);
}
const connectedIds = Object.keys(network.Containers);
if (connectedIds.length > MAX_DISCOVERED_CONTAINERS) {
throw new Error("The selected n8n Docker network is too large to attest safely.");
}
for (const connectedId of connectedIds) {
validateDockerObjectId(connectedId, "connected container");
const connected = attestN8n && connectedId === plan.n8nContainerId
? n8n
: await inspectContainer({
runProcess,
cwd,
dockerHost: plan.dockerHost,
containerId: connectedId,
});
const connectedName = parseContainerName(connected?.Name);
const endpointName = parseContainerName(
network.Containers?.[connectedId]?.Name,
);
const networkState = connected.NetworkSettings?.Networks?.[plan.networkName];
const ownsReservedIdentity = labelsMatchOwnedSidecar(
connected.Config?.Labels,
{ installId, projectName },
);
if (
(connectedName === LOCAL_N8N_SIDECAR_HOSTNAME ||
endpointName === LOCAL_N8N_SIDECAR_HOSTNAME ||
(Array.isArray(networkState?.Aliases) &&
networkState.Aliases.includes(LOCAL_N8N_SIDECAR_HOSTNAME))) &&
!ownsReservedIdentity
) {
throw new Error(
"The n8n-openai-oauth Docker network alias has a collision. Nothing was changed.",
);
}
}
}
function createComposeArgs(projectName, suffix) {
if (!new RegExp(`^${PROJECT_PREFIX}-[a-f0-9]{32}$`, "u").test(projectName)) {
throw new TypeError("The local sidecar Docker project identity is invalid.");
}
return [
"compose",
"--project-name",
projectName,
"--file",
COMPOSE_FILENAME,
...suffix,
];
}
async function runSiwcCli({ runProcess, installRoot, marker, command, input }) {
if (!["host", "accept", "receipt", "account", "account-live", "sign-out", "disable-plan", "enable-plan"].includes(command)) {
throw new TypeError("The SIWC installation operation is invalid.");
}
if (["accept", "receipt"].includes(command)) validateSiwcRegistrationId(marker.registrationId);
if (command === "receipt" && (!Buffer.isBuffer(input) || input.length > 8192)) {
throw new TypeError("The handoff receipt request is invalid.");
}
const result = await runOrThrow(runProcess, {
file: "docker",
args: command === "account-live"
? createComposeArgs(marker.projectName, [
"exec", "-T", SERVICE_NAME, "node", "/app/services/siwc-handoff.mjs", "account",
])
: createComposeArgs(marker.projectName, [
"run", "--rm", "--no-deps", "-T",
...(["accept", "receipt"].includes(command)
? ["--env", `RELMIO_REGISTRATION_ID=${marker.registrationId}`] : []),
"--entrypoint", "node", SERVICE_NAME, "/app/services/siwc-handoff.mjs", command,
]),
cwd: installRoot,
dockerHost: marker.dockerHost,
...(input === undefined ? {} : { input }),
}, "Local SIWC installation operation");
if (Buffer.byteLength(result.stdout) > 4096) {
throw new Error("The SIWC installation returned excessive status data.");
}
return parseJson(result.stdout, "SIWC installation status");
}
function parseModels(output) {
const parsed = parseJson(output.trim(), "Local OAuth model check");
if (!Array.isArray(parsed?.data)) {
throw new Error("The local OAuth model response could not be verified.");
}
const models = parsed.data
.map((entry) => entry?.id)
.filter(
(id) =>
typeof id === "string" &&
id.length > 0 &&
id.length <= 128 &&
/^[A-Za-z0-9_.:-]+$/u.test(id),
);
return models;
}
function assertNoPublishedHostPort(output) {
let services;
try {
const parsed = JSON.parse(output);
services = Array.isArray(parsed) ? parsed : [parsed];
} catch {
throw new Error("The local sidecar published-port safety check failed.");
}
if (services.length !== 1 || !Array.isArray(services[0]?.Publishers)) {
throw new Error("The local sidecar published-port safety check failed.");
}
for (const publisher of services[0].Publishers) {
if (
!publisher ||
!Number.isInteger(publisher.PublishedPort) ||
typeof publisher.URL !== "string" ||
publisher.PublishedPort !== 0 ||
publisher.URL !== ""
) {
throw new Error(
"Safety check failed: the local sidecar published an unexpected host port.",
);
}
}
}
async function verifyRunningSidecar({
runProcess,
installRoot,
plan,
installId,
projectName,
}) {
const running = await runOrThrow(
runProcess,
{
file: "docker",
args: createComposeArgs(projectName, [
"ps",
"--status",
"running",
"--services",
SERVICE_NAME,
]),
cwd: installRoot,
dockerHost: plan.dockerHost,
},
"Local sidecar status check",
);
if (!running.stdout.split(/\s+/u).includes(SERVICE_NAME)) {
throw new Error("The local n8n sidecar did not reach the running state.");
}
const publication = await runOrThrow(
runProcess,
{
file: "docker",
args: createComposeArgs(projectName, [
"ps",
"--format",
"json",
SERVICE_NAME,
]),
cwd: installRoot,
dockerHost: plan.dockerHost,
},
"Local sidecar publication check",
);
assertNoPublishedHostPort(publication.stdout);
const containerIdResult = await runOrThrow(
runProcess,
{
file: "docker",
args: createComposeArgs(projectName, ["ps", "-q", SERVICE_NAME]),
cwd: installRoot,
dockerHost: plan.dockerHost,
},
"Local sidecar identity check",
);
const containerId = validateDockerObjectId(
containerIdResult.stdout.trim(),
"local sidecar container",
);
const sidecar = await inspectContainer({
runProcess,
cwd: installRoot,
dockerHost: plan.dockerHost,
containerId,
});
const sidecarNetwork = sidecar.NetworkSettings?.Networks?.[plan.networkName];
if (
sidecar?.Id !== containerId ||
sidecar?.State?.Running !== true ||
!labelsMatchOwnedSidecar(sidecar.Config?.Labels, {
installId,
projectName,
}) ||
sidecarNetwork?.NetworkID !== plan.dockerNetworkId ||
!Array.isArray(sidecarNetwork?.Aliases) ||
!sidecarNetwork.Aliases.includes(LOCAL_N8N_SIDECAR_HOSTNAME)
) {
throw new Error("The local sidecar Docker identity could not be verified.");
}
const ports = sidecar.NetworkSettings?.Ports;
if (
!ports ||
typeof ports !== "object" ||
Array.isArray(ports) ||
Object.values(ports).some((bindings) => bindings !== null)
) {
throw new Error("The local sidecar published-port safety check failed.");
}
}
async function verifySidecarCatalog({
runProcess, installRoot, plan, projectName, clientCredential,
}) {
const verifier = await runOrThrow(
runProcess,
{
file: "docker",
args: createComposeArgs(projectName, [
"run",
"--rm",
"--no-deps",
"-T",
"--entrypoint",
"node",
SERVICE_NAME,
"-e",
VERIFIER_SCRIPT,
]),
cwd: installRoot,
dockerHost: plan.dockerHost,
input: clientCredential,
},
"Local sidecar private-network verification",
);
return parseModels(verifier.stdout);
}
async function inspectOwnedSidecarRuntime({
runProcess,
installRoot,
marker,
}) {
const containerIdResult = await runOrThrow(
runProcess,
{
file: "docker",
args: createComposeArgs(marker.projectName, [
"ps",
"--all",
"-q",
SERVICE_NAME,
]),
cwd: installRoot,
dockerHost: marker.dockerHost,
},
"Local sidecar refresh identity check",
);
if (containerIdResult.stdout.trim() === "") {
return null;
}
const containerId = validateDockerObjectId(
containerIdResult.stdout.trim(),
"local sidecar container",
);
const sidecar = await inspectContainer({
runProcess,
cwd: installRoot,
dockerHost: marker.dockerHost,
containerId,
});
const sidecarNetwork = sidecar.NetworkSettings?.Networks?.[marker.networkName];
const ports = sidecar.NetworkSettings?.Ports;
let imageId;
try {
imageId = validateDockerImageDigest(sidecar?.Image);
} catch {
throw new Error("The local sidecar refresh identity could not be verified.");
}
if (
sidecar?.Id !== containerId ||
sidecar?.Config?.Image !== `${marker.projectName}:local` ||
typeof sidecar?.State?.Running !== "boolean" ||
typeof sidecar?.State?.Paused !== "boolean" ||
!labelsMatchOwnedSidecar(sidecar.Config?.Labels, {
installId: marker.installId,
projectName: marker.projectName,
}) ||
(sidecarNetwork && sidecarNetwork.NetworkID !== marker.dockerNetworkId) ||
(sidecar.State.Running && (
sidecarNetwork?.NetworkID !== marker.dockerNetworkId ||
!Array.isArray(sidecarNetwork?.Aliases) ||
!sidecarNetwork.Aliases.includes(LOCAL_N8N_SIDECAR_HOSTNAME)
)) ||
(sidecar.State.Running && (!ports || typeof ports !== "object" || Array.isArray(ports))) ||
(ports && (typeof ports !== "object" || Array.isArray(ports) ||
Object.values(ports).some((bindings) => bindings !== null)))
) {
throw new Error("The local sidecar refresh identity could not be verified.");
}
const logicalVolume = marker.schemaVersion === MARKER_SCHEMA_VERSION ? "siwc-store" : "oauth-auth";
const destination = marker.schemaVersion === MARKER_SCHEMA_VERSION ? "/home/node/.relmio-siwc" : "/home/node/.codex";
const mounts = sidecar.Mounts;
if (!Array.isArray(mounts) || mounts.length !== 1 ||
mounts[0].Type !== "volume" || mounts[0].Name !== `${marker.projectName}_${logicalVolume}` ||
mounts[0].Destination !== destination || mounts[0].RW !== true) {
throw new Error("The owned sidecar credential mount changed.");
}
return Object.freeze({
containerId,
imageId,
credentialMount: { type: mounts[0].Type, name: mounts[0].Name, destination: mounts[0].Destination },
health: sidecar.State.Health?.Status,
paused: sidecar.State.Paused,
running: sidecar.State.Running,
});
}
async function cleanupSidecarProject({
runProcess,
installRoot,
dockerHost,
projectName,
}) {
await runOrThrow(
runProcess,
{
file: "docker",
args: createComposeArgs(projectName, [
"down",
"--volumes",
"--remove-orphans",
]),
cwd: installRoot,
dockerHost,
},
"Local sidecar cleanup",
);
const remaining = await runOrThrow(
runProcess,
{
file: "docker",
args: createComposeArgs(projectName, ["ps", "-q", SERVICE_NAME]),
cwd: installRoot,
dockerHost,
},
"Local sidecar cleanup verification",
);
if (remaining.stdout.trim() !== "") {
throw new Error("Relmio could not confirm local sidecar cleanup.");
}
const projectFilter = `label=com.docker.compose.project=${projectName}`;
for (const [resource, args, label] of [
[
"container",
["container", "ls", "--all", "--filter", projectFilter, "--format", "{{json .}}"],
"containers",
],
[
"volume",
["volume", "ls", "--filter", projectFilter, "--format", "{{json .}}"],
"credential volumes",
],
]) {
const result = await runOrThrow(
runProcess,
{ file: "docker", args, cwd: installRoot, dockerHost },
`Local sidecar ${resource} cleanup verification`,
);
const records = parseJsonLines(
result.stdout,
`Local sidecar ${resource} cleanup verification`,
);
if (records.length !== 0) {
throw new Error(
`Relmio could not confirm removal of its local sidecar ${label}. The managed directory was kept.`,
);
}
}
}
async function validateReviewedLocalAccount(registration, binding) {
const record = await readRegistration(registration);
if (
record?.registrationId !== binding.registrationId ||
record?.clientId !== binding.clientId ||
record?.generation !== binding.generation ||
record?.owner?.hostId !== binding.ownerHostId ||
record?.owner?.runtimeId !== binding.ownerRuntimeId ||
record?.handoff?.state !== "owned" ||
!record.planEnabled ||
!record.session?.refreshToken ||
!record.session?.scopes?.includes("chatgpt.tokens.use.direct")
) throw new Error("The selected SIWC account changed. Review the installation again.");
}
async function reviewOwnedN8nReplacement(
{ plan, existingSiwc = false },
{
fileSystem = defaultFileSystem, env = process.env,
homeDirectory = homedir(), runProcess = runLocalProcess,
platform = process.platform, lockDownPath = lockDownLocalPath,
} = {},
) {
assertSupportedPlatform(platform);
rejectDockerEnvironmentOverrides(env);
const selected = normalizeLocalN8nSidecarPlan(plan);
const installRoot = await resolveLocalN8nSidecarInstallRoot({
env, homeDirectory, fileSystem, platform,
});
await verifyWindowsSidecarStatusPathSecurity({
fileSystem, installRoot, platform, lockDownPath,
});
const managed = await inspectManagedInstall({ fileSystem, installRoot });
const marker = validateMarker(managed.marker);
if (marker.schemaVersion !== (existingSiwc ? MARKER_SCHEMA_VERSION : 1) ||
marker.dockerHost !== selected.dockerHost ||
marker.n8nContainerId !== selected.n8nContainerId ||
marker.dockerNetworkId !== selected.dockerNetworkId ||
marker.networkName !== selected.networkName ||
marker.n8nContainerName !== selected.n8nContainerName) {
throw new Error("The selected n8n target or old bridge changed before migration.");
}
const selectedDockerHost = await resolveLocalDockerHost({
runProcess, cwd: installRoot, env, platform,
});
if (selectedDockerHost !== marker.dockerHost) {
throw new Error("The selected local Docker context changed.");
}
await attestPlanAndAlias({
plan: selected, runProcess, cwd: installRoot,
installId: marker.installId, projectName: marker.projectName,
});
const ownership = await attestProjectOwnership({
runProcess, cwd: installRoot, dockerHost: marker.dockerHost,
installId: marker.installId, projectName: marker.projectName,
returnDetails: true, volumeName: existingSiwc ? "siwc-store" : "oauth-auth",
});
if (!ownership.exact) {
throw new Error("The old bridge container, image or credential volume is not exactly owned.");
}
const runtime = await inspectOwnedSidecarRuntime({
runProcess, installRoot, marker,
});
if (!runtime || runtime.paused) {
throw new Error("The old bridge refresh writer is missing or ambiguous.");
}
const logicalVolume = existingSiwc ? "siwc-store" : "oauth-auth";
const effective = parseJson((await runOrThrow(runProcess, {
file: "docker", args: createComposeArgs(marker.projectName, ["config", "--format", "json"]),
cwd: installRoot, dockerHost: marker.dockerHost,
}, "Owned sidecar effective Compose check")).stdout, "Owned sidecar effective Compose check");
const current = effective?.services?.[SERVICE_NAME];
if (Object.keys(effective?.services ?? {}).length !== (existingSiwc ? 2 : 1) ||
current?.image !== `${marker.projectName}:local` ||
current?.volumes?.length !== 1 || current.volumes[0]?.type !== "volume" ||
current.volumes[0]?.source !== logicalVolume ||
current.volumes[0]?.target !== runtime.credentialMount.destination ||
current.volumes[0]?.read_only === true ||
effective?.volumes?.[logicalVolume]?.name !== runtime.credentialMount.name) {
throw new Error("The owned sidecar effective credential mapping changed.");
}
const volumeIdentity = await readOwnedCredentialVolumeIdentity({
runProcess, installRoot, marker, volumeName: runtime.credentialMount.name,
});
return Object.freeze({
installId: marker.installId, projectName: marker.projectName,
dockerHost: marker.dockerHost,
n8nContainerId: marker.n8nContainerId,
dockerNetworkId: marker.dockerNetworkId,
networkName: marker.networkName,
containerId: runtime.containerId, imageId: runtime.imageId,
volumeName: runtime.credentialMount.name,
credentialMount: runtime.credentialMount, volumeIdentity,
...(existingSiwc ? {
registrationId: marker.registrationId,
ownerHostId: marker.ownerHostId,
} : {}),
running: runtime.running,
});
}
export function reviewLocalN8nLegacyMigration({ plan }, deps = {}) {
return reviewOwnedN8nReplacement({ plan }, deps);
}
export function reviewLocalN8nSiwcReplacement({ plan }, deps = {}) {
return reviewOwnedN8nReplacement({ plan, existingSiwc: true }, deps);
}
async function readOwnedCredentialVolumeIdentity({ runProcess, installRoot, marker, volumeName }) {
validateDockerName(volumeName);
const volume = parseJson((await runOrThrow(runProcess, {
file: "docker", args: ["volume", "inspect", "--format", "{{json .}}", volumeName],
cwd: installRoot, dockerHost: marker.dockerHost,
}, "Owned credential volume identity check")).stdout, "Owned credential volume identity check");
requireOwnershipLabels(volume?.Labels, { installId: marker.installId, projectName: marker.projectName, service: false });
if (volume?.Name !== volumeName || volume.Driver !== "local" ||
typeof volume.CreatedAt !== "string" || !Number.isFinite(Date.parse(volume.CreatedAt)) ||
typeof volume.Mountpoint !== "string" || !volume.Mountpoint.startsWith("/") ||
(volume.Options !== null && Object.keys(volume.Options ?? {}).length !== 0)) {
throw new Error("The owned credential volume identity changed.");
}
return createHash("sha256").update(JSON.stringify({
name: volume.Name, createdAt: volume.CreatedAt, driver: volume.Driver,
mountpoint: volume.Mountpoint, options: volume.Options, labels: volume.Labels,
})).digest("hex");
}
async function attestRetiredN8n({ fileSystem, runProcess, installRoot, binding }) {
if (!/^[a-f0-9]{32}$/u.test(binding?.installId)) throw new Error("The retired sidecar identity is invalid.");
const archive = join(installRoot, "legacy", binding.installId);
const marker = validateMarker(JSON.parse(await fileSystem.readFile(join(archive, MANAGED_MARKER), "utf8")));
const runtime = await inspectOwnedSidecarRuntime({ runProcess, installRoot: archive, marker });
if (!runtime || runtime.running || runtime.paused || runtime.containerId !== binding.containerId ||
runtime.imageId !== binding.imageId ||
JSON.stringify(runtime.credentialMount) !== JSON.stringify(binding.credentialMount) ||
await readOwnedCredentialVolumeIdentity({ runProcess, installRoot: archive, marker,
volumeName: binding.volumeName }) !== binding.volumeIdentity) {
throw new Error("The retained old sidecar writer or credential volume changed.");
}
}
export async function reviewLocalN8nSiwcResume(
{ registration, plan: reviewedPlan } = {},
{ fileSystem = defaultFileSystem, env = process.env, homeDirectory = homedir(),
runProcess = runLocalProcess, platform = process.platform, lockDownPath = lockDownLocalPath } = {},
) {
rejectDockerEnvironmentOverrides(env);
const installRoot = await resolveLocalN8nSidecarInstallRoot({ env, homeDirectory, fileSystem, platform });
const staged = await readLocalSiwcStaging({ fileSystem, installRoot, platform, lockDownPath });
if (!staged || staged.checkpoint.stage === "completed") throw new Error("There is no interrupted SIWC sidecar to resume.");
const { checkpoint, checkpointSha256 } = staged;
let plan = normalizeLocalN8nSidecarPlan(reviewedPlan ?? checkpoint.plan);
if (reviewedPlan && reviewedPlan.authBinding?.registrationId !== registration?.registrationId) {
throw new Error("The selected SIWC account does not match the fresh n8n review.");
}
for (const field of ["dockerHost", "networkName", "dockerNetworkId"]) {
if (plan[field] !== checkpoint.plan[field]) throw new Error("The stable SIWC destination changed.");
}
if (checkpoint.projectName !== `${PROJECT_PREFIX}-${checkpoint.installId}` ||
await resolveLocalDockerHost({ runProcess, cwd: homeDirectory, env, platform }) !== checkpoint.dockerHost) {
throw new Error("The staged SIWC destination changed.");
}
await attestProjectOwnership({ runProcess, cwd: homeDirectory, dockerHost: checkpoint.dockerHost,
installId: checkpoint.installId, projectName: checkpoint.projectName });
const aliasOwner = checkpoint.reviewedLegacy && !checkpoint.oldStopped ? checkpoint.reviewedLegacy : checkpoint;
await attestPlanAndAlias({ plan, runProcess, cwd: homeDirectory,
installId: aliasOwner.installId, projectName: aliasOwner.projectName });
if (checkpoint.reviewedLegacy && checkpoint.oldStopped) {
await attestRetiredN8n({ fileSystem, runProcess, installRoot, binding: checkpoint.reviewedLegacy });
}
const filesSha256 = await fingerprintLocalSiwcFiles({ fileSystem, installRoot, platform, lockDownPath });
const resourcesSha256 = await fingerprintLocalSiwcResources({ runProcess, installRoot, checkpoint });
if (checkpoint.ownerHostId) {
const { marker } = await inspectManagedInstall({ fileSystem, installRoot });
if (marker?.installId !== checkpoint.installId || marker?.ownerHostId !== checkpoint.ownerHostId) {
throw new Error("The staged SIWC owner marker changed.");
}
await assertManagedSiwcCompose(fileSystem, installRoot, marker);
}
const authBinding = await readLocalSiwcResumeAuthBinding({ checkpoint, registration, readRegistration, readPendingAuthHandoff,
readReceipt: pending => runSiwcCli({ runProcess, installRoot, marker: checkpoint, command: "receipt",
input: Buffer.from(JSON.stringify({ handoffId: pending.handoffId, binding: pending.binding, identity: pending.identity })) }) });
plan = normalizeLocalN8nSidecarPlan({ ...plan, authBinding });
return { target: LOCAL_N8N_SIDECAR_TARGET, installId: checkpoint.installId,
registrationId: plan.authBinding.registrationId, stage: checkpoint.stage, plan, checkpointSha256,
...(checkpoint.migration ? { migration: checkpoint.migration } : {}),
filesSha256, resourcesSha256 };
}
export async function reconcileLocalN8nSiwcHandoff(
{ registration, confirmed },
{ fileSystem = defaultFileSystem, env = process.env, homeDirectory = homedir(),
runProcess = runLocalProcess, platform = process.platform, lockDownPath = lockDownLocalPath,
getProcessIdentity, lifecycleLockNow = Date.now } = {},
) {
if (confirmed !== true) throw new Error("Confirm reconciliation of the selected SIWC handoff.");
validateSiwcRegistrationId(registration?.registrationId);
rejectDockerEnvironmentOverrides(env);
const installRoot = await resolveLocalN8nSidecarInstallRoot({ env, homeDirectory, fileSystem, platform });
const releaseLock = await acquireSidecarLock({ fileSystem, getProcessIdentity, installRoot,
lockDownPath, now: lifecycleLockNow, platform });
return settleLocalIntegrationLifecycleOperation({ completionLabel: "Local n8n SIWC handoff reconciliation",
releaseLock, operation: async () => {
await fingerprintLocalSiwcFiles({ fileSystem, installRoot, platform, lockDownPath });
const managed = await inspectManagedInstall({ fileSystem, installRoot });
const marker = validateMarker(managed.marker);
if (marker.registrationId !== registration.registrationId) throw new Error("The selected SIWC destination changed.");
await assertManagedSiwcCompose(fileSystem, installRoot, marker);
if (await resolveLocalDockerHost({ runProcess, cwd: installRoot, env, platform }) !== marker.dockerHost) {
throw new Error("The selected Docker context changed.");
}
await attestPlanAndAlias({ plan: marker, runProcess, cwd: installRoot,
installId: marker.installId, projectName: marker.projectName, attestN8n: false });
await attestProjectOwnership({ runProcess, cwd: installRoot, dockerHost: marker.dockerHost,
installId: marker.installId, projectName: marker.projectName });
const source = await readRegistration(registration);
const pending = await readPendingAuthHandoff(registration);
const request = pending ?? (source?.handoff?.state === "transferred" ? {
handoffId: source.handoff.handoffId, binding: source.handoff.receipt.binding,
target: source.handoff.receipt.binding.target,
identity: { issuer: source.identity.issuer, clientId: source.clientId, subject: source.identity.subject },
} : null);
if (!request || request.target.hostId !== marker.ownerHostId || request.target.runtimeId !== marker.installId) {
throw new Error("The frozen handoff does not match this destination.");
}
let receipt;
try {
({ receipt } = await runSiwcCli({ runProcess, installRoot, marker, command: "receipt",
input: Buffer.from(JSON.stringify({ handoffId: request.handoffId,
binding: request.binding, identity: request.identity })) }));
} catch {
throw Object.assign(new Error("The SIWC receipt outcome is unresolved. The source remains frozen."), { remoteOutcomeUnknown: true });
}
if (receipt === null) {
await assertNoLocalSiwcOneOffContainers({ runProcess, installRoot,
dockerHost: marker.dockerHost, projectName: marker.projectName });
const staged = await readLocalSiwcStaging({ fileSystem, installRoot, platform, lockDownPath });
if (!staged || staged.checkpoint.registrationId !== registration.registrationId) {
throw new Error("The original SIWC checkpoint changed.");
}
staged.checkpoint.notAccepted = true;
const path = localSiwcStagingPath(installRoot);
await writeManagedFile(fileSystem, path, `${JSON.stringify(staged.checkpoint)}\n`, 0o600);
if (platform === "win32") await lockDownPath(path, { platform, kind: "file" });
return { outcome: "not-accepted",
account: (await listRegistrations({ storageRoot: registration.storageRoot })).find(
account => account.registrationId === registration.registrationId) };
}
if (receipt?.handoffId !== request.handoffId ||
JSON.stringify(receipt.binding) !== JSON.stringify(request.binding)) throw new Error("The SIWC receipt binding changed.");
return { outcome: "finished", account: await finishAuthHandoff(registration, {
handoffId: request.handoffId, receipt: receipt.receipt }) };
} });
}
export async function installLocalN8nSidecar(
{ plan, registration, backgroundConsent, confirmed, migrationConsent, legacyBinding, replacementConsent, existingBinding, resume },
{
fileSystem = defaultFileSystem,
env = process.env,
homeDirectory = homedir(),
runProcess = runLocalProcess,
randomBytes = createRandomBytes,
platform = process.platform,
lockDownPath = lockDownLocalPath,
getProcessIdentity,
lifecycleLockNow = Date.now,
collectAssets = collectSiwcRuntimeAssets,
} = {},
) {
if (confirmed !== true) {
throw new Error("Confirm the reviewed private n8n bridge plan before installing.");
}
assertSupportedPlatform(platform);
rejectDockerEnvironmentOverrides(env);
const selected = normalizeLocalN8nSidecarPlan(plan);
const binding = validateSiwcAuthBinding(selected.authBinding);
if (registration?.registrationId !== binding.registrationId || !registration.storageRoot) {
throw new Error("The selected SIWC account does not match the reviewed bridge.");
}
const safeConsent = Object.freeze({
...validateSiwcBackgroundConsent(backgroundConsent, { target: "local-n8n" }),
});
if (!resume) await validateReviewedLocalAccount(registration, binding);
const installRoot = await resolveLocalN8nSidecarInstallRoot({
env, homeDirectory, fileSystem, platform,
});
const releaseLock = await acquireSidecarLock({
fileSystem, getProcessIdentity, installRoot, lockDownPath,
now: lifecycleLockNow, platform,
});
let preservedResult;
return settleLocalIntegrationLifecycleOperation({
completionLabel: "Local n8n SIWC sidecar installation",
releaseLock,
operation: async () => {
try {
const staged = await readLocalSiwcStaging({ fileSystem, installRoot, platform, lockDownPath });
if (staged && staged.checkpoint.stage !== "completed" && !resume) {
throw new Error("Review the interrupted SIWC sidecar before resuming.");
}
let handoffCommitted = false;
if (resume) {
const reviewed = await reviewLocalN8nSiwcResume({ registration, plan: selected },
{ fileSystem, env, homeDirectory, runProcess, platform, lockDownPath });
if (JSON.stringify(reviewed) !== JSON.stringify(resume) ||
JSON.stringify(reviewed.plan) !== JSON.stringify(selected)) {
throw new Error("The reviewed SIWC resume binding changed.");
}
const source = await readRegistration(registration);
if (source?.handoff?.state === "handoff-pending") throw new Error("Reconcile the frozen handoff before resuming.");
handoffCommitted = source?.handoff?.state === "transferred";
if (!handoffCommitted) await validateReviewedLocalAccount(registration, binding);
else if (source.handoff.receipt.binding.target.runtimeId !== staged.checkpoint.installId ||
source.handoff.receipt.binding.target.hostId !== staged.checkpoint.ownerHostId) {
throw new Error("The completed handoff belongs to another destination.");
}
}
const managed = await inspectManagedInstall({ fileSystem, installRoot, staging: staged?.checkpoint });
const migrating = migrationConsent !== undefined || legacyBinding !== undefined || resume?.migration === "legacy";
const replacing = replacementConsent !== undefined || existingBinding !== undefined || resume?.migration === "replacement";
const retiring = migrating || replacing;
let reviewedLegacy = resume ? staged.checkpoint.reviewedLegacy ?? null : null;
if (retiring && !(resume && staged.checkpoint.oldStopped)) {
const expectedSchema = replacing ? MARKER_SCHEMA_VERSION : 1;
if ((migrating && replacing) ||
(!resume && migrating && migrationConsent !== true) ||
(!resume && replacing && replacementConsent !== true) ||
managed.marker?.schemaVersion !== expectedSchema) {
throw new Error("Explicit reviewed old-bridge replacement consent is required.");
}
const inspect = replacing
? reviewLocalN8nSiwcReplacement : reviewLocalN8nLegacyMigration;
reviewedLegacy = await inspect(
{ plan: selected },
{ fileSystem, env, homeDirectory, runProcess, platform, lockDownPath },
);
const expected = resume ? staged.checkpoint.reviewedLegacy : replacing ? existingBinding : legacyBinding;
for (const field of [
"installId", "projectName", "dockerHost", "n8nContainerId",
"dockerNetworkId", "networkName", "containerId", "imageId",
"volumeName", "volumeIdentity", "running",
...(replacing ? ["registrationId", "ownerHostId"] : []),
]) {
if (expected?.[field] !== reviewedLegacy[field]) {
throw new Error("The reviewed old n8n sidecar identity changed.");
}
}
if (JSON.stringify(expected?.credentialMount) !== JSON.stringify(reviewedLegacy.credentialMount)) {
throw new Error("The reviewed credential mount changed.");
}
if (replacing && reviewedLegacy.registrationId === binding.registrationId) {
throw new Error("A replacement requires a fresh independently authorized SIWC registration.");
}
} else if (managed.marker && !resume) {
throw new Error("The existing bridge requires a separately reviewed migration or account replacement.");
}
const identityBytes = randomBytes(32);
const capabilityBytes = randomBytes(32);
if (!Buffer.isBuffer(identityBytes) || identityBytes.length !== 32 ||
!Buffer.isBuffer(capabilityBytes) || capabilityBytes.length !== 32) {
throw new Error("Relmio could not generate a strong installation identity and local capability.");
}
const installId = resume ? staged.checkpoint.installId : identityBytes.subarray(0, 16).toString("hex");
if (retiring && installId === reviewedLegacy.installId) {
throw new Error("The fresh SIWC sidecar identity collided with the old project.");
}
const projectName = `${PROJECT_PREFIX}-${installId}`;
const clientCredential = capabilityBytes.toString("base64url");
const tokenSha256 = createHash("sha256").update(clientCredential).digest("hex");
const runtime = { installId, projectName, dockerHost: selected.dockerHost, registrationId: binding.registrationId };
if (handoffCommitted) {
const account = (await runSiwcCli({ runProcess, installRoot, marker: runtime, command: "account" })).account;
if (account?.registrationId !== binding.registrationId || account?.ownerHostId !== staged.checkpoint.ownerHostId ||
account?.ownerRuntimeId !== installId || account?.ownership !== "owned") {
throw new Error("The resumed destination account changed.");
}
preservedResult = { target: LOCAL_N8N_SIDECAR_TARGET, endpoint: LOCAL_N8N_SIDECAR_ENDPOINT,
protocol: "openai-v1", clientCredential, credentialShownOnce: true, models: [],
networkName: selected.networkName, hostPublication: "unknown", account, runtimeState: "unknown" };
}
const cwd = dirname(resolve(installRoot, "..", ".."));
await attestProjectOwnership({
runProcess, cwd, dockerHost: selected.dockerHost, installId, projectName,
});
if (!retiring) {
await attestPlanAndAlias({
plan: selected, runProcess, cwd, installId, projectName,
});
}
const checkpoint = resume ? staged.checkpoint : {
schemaVersion: 1, target: LOCAL_N8N_SIDECAR_TARGET, installId, projectName,
dockerHost: selected.dockerHost, registrationId: binding.registrationId,
plan: selected, stage: "staged",
...(retiring ? { reviewedLegacy, migration: replacing ? "replacement" : "legacy",
...(replacing ? { previousGeneration: existingBinding.expectedGeneration } : {}) } : {}),
};
if (resume) {
if (checkpoint.registrationId !== binding.registrationId) {
await assertNoLocalSiwcOneOffContainers({ runProcess, installRoot,
dockerHost: selected.dockerHost, projectName });
}
checkpoint.registrationId = binding.registrationId;
checkpoint.plan = selected;
checkpoint.notAccepted = false;
}
const saveStage = async stage => {
checkpoint.stage = stage;
const path = localSiwcStagingPath(installRoot);
await writeManagedFile(fileSystem, path, `${JSON.stringify(checkpoint)}\n`, 0o600);
if (platform === "win32") await lockDownPath(path, { platform, kind: "file" });
};
await saveStage(checkpoint.stage);
await initializeManagedDirectories({
fileSystem, installRoot, baseExists: managed.baseExists, platform, lockDownPath,
});
const write = async (path, contents) => {
const fullPath = join(installRoot, path);
await writeManagedFile(fileSystem, fullPath, contents, 0o600);
if (platform === "win32") await lockDownPath(fullPath, { platform, kind: "file" });
};
let legacyArchive = retiring ? join(installRoot, "legacy", reviewedLegacy.installId) : null;
if (retiring && !(resume && checkpoint.oldStopped)) {
await validateReviewedLocalAccount(registration, binding);
const inspect = replacing
? reviewLocalN8nSiwcReplacement : reviewLocalN8nLegacyMigration;
const freshPrevious = await inspect(
{ plan: selected },
{ fileSystem, env, homeDirectory, runProcess, platform, lockDownPath },
);
for (const field of [
"installId", "projectName", "dockerHost", "n8nContainerId",
"dockerNetworkId", "networkName", "containerId", "imageId",
"volumeName", "volumeIdentity", "running",
...(replacing ? ["registrationId", "ownerHostId"] : []),
]) {
if (freshPrevious[field] !== reviewedLegacy[field]) {
throw new Error("The old n8n sidecar changed before replacement. Review again.");
}
}
if (JSON.stringify(freshPrevious.credentialMount) !== JSON.stringify(reviewedLegacy.credentialMount)) {
throw new Error("The old credential mount changed before replacement.");
}
if (replacing) {
const old = (await runSiwcCli({
runProcess, installRoot, marker: managed.marker, command: "account",
})).account;
if (old?.registrationId !== reviewedLegacy.registrationId ||
old?.ownerHostId !== reviewedLegacy.ownerHostId ||
old?.generation !== (resume ? checkpoint.previousGeneration : existingBinding.expectedGeneration) ||
old?.session !== "signed-out" || old?.planEnabled !== false) {
throw new Error("Sign out of the exact old SIWC account before reviewed replacement.");
}
}
const legacyRoot = join(installRoot, "legacy");
legacyArchive = join(legacyRoot, reviewedLegacy.installId);
if (!resume && await lstatIfExists(fileSystem, legacyArchive)) {
throw new Error("A prior n8n sidecar replacement is incomplete. Inspect it before retrying.");
}
await ensurePrivateDirectory(fileSystem, legacyRoot, platform, lockDownPath);
await ensurePrivateDirectory(fileSystem, legacyArchive, platform, lockDownPath);
const retain = async (filename, contents) => {
const path = join(legacyArchive, filename);
await writeManagedFile(fileSystem, path, contents, 0o600);
if (platform === "win32") await lockDownPath(path, { platform, kind: "file" });
};
for (const filename of [
MANAGED_MARKER, COMPOSE_FILENAME, "Dockerfile", ".dockerignore",
...(replacing ? ["package.json", "package-lock.json"] : []),
]) {
const oldPath = join(installRoot, filename);
const metadata = await lstatIfExists(fileSystem, oldPath);
if (!metadata?.isFile?.() || metadata.isSymbolicLink() ||
metadata.size > 1024 * 1024 ||
(platform !== "win32" && (metadata.mode & 0o077) !== 0)) {
throw new Error("The old bridge generated files are missing or unsafe.");
}
if (platform === "win32") {
await lockDownPath(oldPath, {
platform, kind: "file", verifyOnly: true, verifyEffectiveOwnerOnly: true,
});
}
await retain(filename, await fileSystem.readFile(oldPath));
}
const oldRuntimePath = join(installRoot, "openai-oauth-sidecar.mjs");
const oldRuntime = await lstatIfExists(fileSystem, oldRuntimePath);
if (oldRuntime) {
if (!oldRuntime.isFile() || oldRuntime.isSymbolicLink() ||
oldRuntime.size > 1024 * 1024) {
throw new Error("The old bridge runtime file is unsafe.");
}
await retain("openai-oauth-sidecar.mjs", await fileSystem.readFile(oldRuntimePath));
}
const journal = async state => retain("migration.json", Buffer.from(
`${JSON.stringify({
schemaVersion: 1, state, reviewedLegacy,
replacementInstallId: installId, previousWasSiwc: replacing,
})}\n`,
));
await journal("prepared");
if (reviewedLegacy.running) {
await runOrThrow(runProcess, {
file: "docker",
args: createComposeArgs(reviewedLegacy.projectName, [
"stop", "--timeout", "30", SERVICE_NAME,
]),
cwd: installRoot, dockerHost: reviewedLegacy.dockerHost,
}, "Attested old n8n sidecar drain");
}
const stoppedPrevious = await inspect(
{ plan: selected },
{ fileSystem, env, homeDirectory, runProcess, platform, lockDownPath },
);
for (const field of [
"installId", "projectName", "dockerHost", "n8nContainerId",
"dockerNetworkId", "networkName", "containerId", "imageId", "volumeName", "volumeIdentity",
...(replacing ? ["registrationId", "ownerHostId"] : []),
]) {
if (stoppedPrevious[field] !== reviewedLegacy[field]) {
throw new Error("The old sidecar identity changed while stopping. It will not be resumed automatically.");
}
}
if (stoppedPrevious.running) {
throw new Error("The old credential refresh writer did not stop.");
}
if (JSON.stringify(stoppedPrevious.credentialMount) !== JSON.stringify(reviewedLegacy.credentialMount)) {
throw new Error("The old credential mount changed while stopping.");
}
if (replacing) {
const old = (await runSiwcCli({
runProcess, installRoot, marker: managed.marker, command: "account",
})).account;
if (old?.registrationId !== reviewedLegacy.registrationId ||
old?.generation !== (resume ? checkpoint.previousGeneration : existingBinding.expectedGeneration) ||
old?.session !== "signed-out" || old?.planEnabled !== false) {
throw new Error("The old signed-out SIWC account changed. The retired bridge remains stopped.");
}
}
await journal("stopped");
checkpoint.oldStopped = true;
await saveStage("staged");
}
if (handoffCommitted) {
await runOrThrow(runProcess, { file: "docker",
args: createComposeArgs(projectName, ["stop", "--timeout", "30", SERVICE_NAME]),
cwd: installRoot, dockerHost: selected.dockerHost }, "Resumed owned sidecar stop");
const running = await runOrThrow(runProcess, { file: "docker",
args: createComposeArgs(projectName, ["ps", "--status", "running", "--services", SERVICE_NAME]),
cwd: installRoot, dockerHost: selected.dockerHost }, "Resumed owned sidecar stopped check");
if (running.stdout.trim() !== "") throw new Error("The resumed sidecar did not stop.");
}
const assets = await collectAssets();
for (const folder of ["services", "gateway", "infrastructure"]) {
await ensurePrivateDirectory(fileSystem, join(installRoot, folder), platform, lockDownPath);
}
await write("Dockerfile", createLocalN8nSidecarDockerfile({ installId }));
await write(".dockerignore", createLocalN8nSidecarDockerignore());
await write("package.json", assets.packageJson);
await write("package-lock.json", assets.packageLock);
for (const asset of assets.files) {
if (!SIWC_ASSET_PATHS.has(asset.path) || !Buffer.isBuffer(asset.contents)) {
throw new Error("The packaged SIWC runtime assets are invalid.");
}
await write(asset.path, asset.contents);
}
await write(COMPOSE_FILENAME, createLocalN8nSidecarComposeFile({
installId, networkName: selected.networkName,
registrationId: binding.registrationId, tokenSha256,
}));
await runOrThrow(runProcess, {
file: "docker", args: createComposeArgs(projectName, ["config", "--quiet"]),
cwd: installRoot, dockerHost: selected.dockerHost,
}, "Local sidecar Compose validation");
await attestProjectOwnership({
runProcess, cwd: installRoot, dockerHost: selected.dockerHost,
installId, projectName,
});
await attestPlanAndAlias({
plan: selected, runProcess, cwd: installRoot, installId, projectName,
});
await runOrThrow(runProcess, {
file: "docker", args: createComposeArgs(projectName, ["build", SERVICE_NAME]),
cwd: installRoot, dockerHost: selected.dockerHost,
}, "Local SIWC image build");
await runOrThrow(runProcess, {
file: "docker", args: createComposeArgs(projectName, [
"run", "--rm", "--no-deps", "-T", "credential-seed",
]),
cwd: installRoot, dockerHost: selected.dockerHost,
}, "Local SIWC private volume initialization");
const destination = await runSiwcCli({
runProcess, installRoot, marker: runtime, command: "host",
});
validateSiwcHostId(destination?.hostId);
if (destination.runtimeId !== installId || destination.hostId === binding.ownerHostId ||
(handoffCommitted && destination.hostId !== checkpoint.ownerHostId)) {
throw new Error("The local SIWC destination host identity is invalid.");
}
await write(MANAGED_MARKER, `${JSON.stringify({
schemaVersion: MARKER_SCHEMA_VERSION, kind: "relmio-local-n8n-sidecar",
target: LOCAL_N8N_SIDECAR_TARGET, installId, projectName,
dockerHost: selected.dockerHost,
n8nContainerId: selected.n8nContainerId,
n8nContainerName: selected.n8nContainerName,
dockerNetworkId: selected.dockerNetworkId,
networkName: selected.networkName,
registrationId: binding.registrationId, clientId: binding.clientId,
ownerHostId: destination.hostId, tokenSha256,
...(retiring ? {
legacyInstallId: reviewedLegacy.installId,
...(replacing ? { previousWasSiwc: true } : {}),
} : {}),
})}\n`);
checkpoint.ownerHostId = destination.hostId;
await saveStage("prepared");
await attestPlanAndAlias({
plan: selected, runProcess, cwd: installRoot, installId, projectName,
});
let accepted;
if (handoffCommitted) {
const account = (await runSiwcCli({ runProcess, installRoot, marker: runtime, command: "account" })).account;
if (account?.registrationId !== binding.registrationId || account?.ownerHostId !== destination.hostId ||
account?.ownerRuntimeId !== installId || account?.ownership !== "owned") {
throw new Error("The resumed SIWC destination account changed.");
}
accepted = { account };
} else {
await validateReviewedLocalAccount(registration, binding);
const { handoffId } = await prepareAuthHandoff(registration, {
expectedGeneration: binding.generation, target: destination, backgroundConsent: safeConsent,
});
await saveStage("handoff-pending");
const contents = await readAuthHandoff(registration, {
handoffId, expectedGeneration: binding.generation,
});
// Acceptance is attempted once; recovery reads its durable receipt.
try {
accepted = await runSiwcCli({
runProcess, installRoot, marker: runtime, command: "accept", input: contents,
});
} catch (error) {
throw Object.assign(new Error("The SIWC transfer outcome is unresolved. The source remains frozen; reconcile the destination before retrying."),
{ remoteOutcomeUnknown: true });
}
if (accepted?.handoffId !== handoffId ||
accepted.account?.registrationId !== binding.registrationId ||
accepted.account?.ownerHostId !== destination.hostId ||
accepted.account?.ownerRuntimeId !== installId ||
accepted.account?.ownership !== "owned") {
throw new Error("The SIWC handoff receipt or destination account did not match. The source remains frozen.");
}
preservedResult = { target: LOCAL_N8N_SIDECAR_TARGET, endpoint: LOCAL_N8N_SIDECAR_ENDPOINT,
protocol: "openai-v1", clientCredential, credentialShownOnce: true, models: [],
networkName: selected.networkName, hostPublication: "none",
account: accepted.account, runtimeState: "stopped" };
await finishAuthHandoff(registration, { handoffId, receipt: accepted.receipt });
handoffCommitted = true;
}
try {
await saveStage("transferred");
await runOrThrow(runProcess, {
file: "docker",
args: createComposeArgs(projectName, [
"up", "-d", "--wait", "--wait-timeout", "90", "--no-deps", SERVICE_NAME,
]),
cwd: installRoot, dockerHost: selected.dockerHost,
}, "Local SIWC sidecar start");
await verifyRunningSidecar({
runProcess, installRoot, plan: selected, installId, projectName,
});
} catch (error) {
let stopped = false;
try {
await runOrThrow(runProcess, {
file: "docker",
args: createComposeArgs(projectName, [
"stop", "--timeout", "30", SERVICE_NAME,
]),
cwd: installRoot, dockerHost: selected.dockerHost,
}, "Unverified sidecar stop");
const running = await runOrThrow(runProcess, {
file: "docker",
args: createComposeArgs(projectName, [
"ps", "--status", "running", "--services", SERVICE_NAME,
]),
cwd: installRoot, dockerHost: selected.dockerHost,
}, "Unverified sidecar stopped check");
stopped = running.stdout.trim() === "";
} catch {
// A disconnected Docker operation leaves this owned service uncertain.
}
const unsafePublication = /published.*host port/iu.test(error?.message ?? "");
return {
target: LOCAL_N8N_SIDECAR_TARGET,
endpoint: LOCAL_N8N_SIDECAR_ENDPOINT,
protocol: "openai-v1", clientCredential, credentialShownOnce: true,
models: [], networkName: selected.networkName,
hostPublication: stopped ? "none" : "unknown",
deploymentMode: "partial", account: accepted.account,
readiness: "unverified", runtimeState: stopped ? "stopped" : "unknown",
runtimeFailure: unsafePublication
? { error: "The sidecar may have published a host port. Inspect it before use.",
status: 503, recovery: "resolve-handoff" }
: safeSiwcRuntimeFailure(),
finalizationFailure: {
error: "Ownership transferred, but the runtime could not be verified. Save the one-time key and resolve recovery before use.",
recovery: "resolve-handoff",
},
...(migrating ? { migrationPending: true, legacyRetained: true } : {}),
...(replacing ? { replacementPending: true, oldHistoryRetained: true } : {}),
};
}
let models = [];
let catalogFailure;
try {
models = await verifySidecarCatalog({
runProcess, installRoot, plan: selected, projectName, clientCredential,
});
} catch (error) {
catalogFailure = safeSiwcCatalogFailure(error);
}
let account = accepted.account;
let status;
try {
status = await runSiwcCli({
runProcess, installRoot, marker: runtime, command: "account-live",
});
} catch {
catalogFailure ??= safeSiwcCatalogFailure({
code: "owner_status_unavailable", recovery: "retry-later",
});
}
if (status) {
if (status.account?.registrationId !== binding.registrationId ||
status.account.ownerHostId !== destination.hostId ||
status.account.ownerRuntimeId !== installId ||
status.account.ownership !== "owned") {
return localSiwcFinalizationFailure({
target: LOCAL_N8N_SIDECAR_TARGET, endpoint: LOCAL_N8N_SIDECAR_ENDPOINT,
protocol: "openai-v1", clientCredential, credentialShownOnce: true,
models: [], networkName: selected.networkName, hostPublication: "none",
account, runtimeState: "running",
}, "resolve-handoff");
}
account = status.account;
}
try {
if (retiring) {
const journalPath = join(legacyArchive, "migration.json");
await writeManagedFile(fileSystem, journalPath, Buffer.from(
`${JSON.stringify({
schemaVersion: 1, state: "transferred",
reviewedLegacy, replacementInstallId: installId,
previousWasSiwc: replacing,
})}\n`,
), 0o600);
if (platform === "win32") {
await lockDownPath(journalPath, { platform, kind: "file" });
}
}
await saveStage("completed");
} catch {
return localSiwcFinalizationFailure({
target: LOCAL_N8N_SIDECAR_TARGET, endpoint: LOCAL_N8N_SIDECAR_ENDPOINT,
protocol: "openai-v1", clientCredential, credentialShownOnce: true,
models, networkName: selected.networkName, hostPublication: "none",
account, runtimeState: "running",
...(migrating ? { migrationPending: true, legacyRetained: true } : {}),
...(replacing ? { replacementPending: true, oldHistoryRetained: true } : {}),
});
}
return {
target: LOCAL_N8N_SIDECAR_TARGET,
endpoint: LOCAL_N8N_SIDECAR_ENDPOINT,
protocol: "openai-v1", clientCredential, credentialShownOnce: true,
models, networkName: selected.networkName, hostPublication: "none",
deploymentMode: migrating ? "migrated" : replacing ? "replaced" : "installed", account,
readiness: catalogFailure ? "unverified" : "verified",
runtimeState: "running",
...(catalogFailure ? { catalogFailure } : {}),
...(migrating ? { migratedLegacy: true, legacyRetained: true } : {}),
...(replacing ? { replacedAccount: true, oldHistoryRetained: true } : {}),
};
} catch (error) {
if (preservedResult) return localSiwcFinalizationFailure(preservedResult, "resolve-handoff");
throw error;
}
},
});
}
export async function inspectStoppedLocalN8nSiwcInstallation(
{ registrationId, confirmed },
{
fileSystem = defaultFileSystem, env = process.env,
homeDirectory = homedir(), runProcess = runLocalProcess,
platform = process.platform, lockDownPath = lockDownLocalPath,
getProcessIdentity, lifecycleLockNow = Date.now,
} = {},
) {
if (confirmed !== true) {
throw new Error("Confirm inspection of the stopped local n8n SIWC account.");
}
validateSiwcRegistrationId(registrationId);
assertSupportedPlatform(platform);
rejectDockerEnvironmentOverrides(env);
const installRoot = await resolveLocalN8nSidecarInstallRoot({
env, homeDirectory, fileSystem, platform,
});
const releaseLock = await acquireSidecarLock({
fileSystem, getProcessIdentity, installRoot, lockDownPath,
now: lifecycleLockNow, platform,
});
return settleLocalIntegrationLifecycleOperation({
completionLabel: "Stopped local n8n SIWC inspection",
releaseLock,
operation: async () => {
await verifyWindowsSidecarStatusPathSecurity({
fileSystem, installRoot, platform, lockDownPath,
});
const managed = await inspectManagedInstall({ fileSystem, installRoot });
const marker = validateMarker(managed.marker);
if (marker.registrationId !== registrationId) {
throw new Error("The selected account is not installed in this bridge.");
}
await assertManagedSiwcCompose(fileSystem, installRoot, marker);
const selectedDockerHost = await resolveLocalDockerHost({
runProcess, cwd: installRoot, env, platform,
});
if (selectedDockerHost !== marker.dockerHost) {
throw new Error("The selected Docker context changed.");
}
await attestPlanAndAlias({
plan: marker, runProcess, cwd: installRoot,
installId: marker.installId, projectName: marker.projectName,
});
const ownership = await attestProjectOwnership({
runProcess, cwd: installRoot, dockerHost: marker.dockerHost,
installId: marker.installId, projectName: marker.projectName,
returnDetails: true,
});
if (!ownership.exact) throw new Error("The exact owned bridge is missing.");
const runtime = await inspectOwnedSidecarRuntime({ runProcess, installRoot, marker });
if (!runtime || runtime.running || runtime.paused) {
throw new Error("The owned sidecar is not safely stopped.");
}
const account = (await runSiwcCli({
runProcess, installRoot, marker, command: "account",
})).account;
if (account?.registrationId !== registrationId ||
account?.ownerHostId !== marker.ownerHostId ||
account?.ownerRuntimeId !== marker.installId ||
account?.ownership !== "owned") {
throw new Error("The stopped SIWC account could not be attested.");
}
return { account };
},
});
}
export async function manageLocalN8nSiwcInstallation(
{ registrationId, action, expectedGeneration, backgroundConsent, confirmed },
{
fileSystem = defaultFileSystem, env = process.env,
homeDirectory = homedir(), runProcess = runLocalProcess,
platform = process.platform, lockDownPath = lockDownLocalPath,
getProcessIdentity, lifecycleLockNow = Date.now,
} = {},
) {
if (confirmed !== true ||
!["sign-out", "disable-plan", "enable-plan"].includes(action) ||
(action === "enable-plan" && backgroundConsent !== true)) {
throw new Error("Confirm the selected installed SIWC account and background workflow consent.");
}
validateSiwcRegistrationId(registrationId);
if (typeof expectedGeneration !== "string" ||
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(expectedGeneration)) {
throw new TypeError("The selected SIWC account generation is invalid.");
}
assertSupportedPlatform(platform);
rejectDockerEnvironmentOverrides(env);
const installRoot = await resolveLocalN8nSidecarInstallRoot({
env, homeDirectory, fileSystem, platform,
});
const releaseLock = await acquireSidecarLock({
fileSystem, getProcessIdentity, installRoot, lockDownPath,
now: lifecycleLockNow, platform,
});
return settleLocalIntegrationLifecycleOperation({
completionLabel: "Installed local n8n SIWC account operation",
releaseLock,
operation: async () => {
await verifyWindowsSidecarStatusPathSecurity({
fileSystem, installRoot, platform, lockDownPath,
});
const managed = await inspectManagedInstall({ fileSystem, installRoot });
const marker = validateMarker(managed.marker);
if (marker.registrationId !== registrationId) {
throw new Error("The selected SIWC account is not installed in this bridge.");
}
await assertManagedSiwcCompose(fileSystem, installRoot, marker);
const selectedDockerHost = await resolveLocalDockerHost({
runProcess, cwd: installRoot, env, platform,
});
if (selectedDockerHost !== marker.dockerHost) {
throw new Error("The selected Docker context changed.");
}
await attestPlanAndAlias({
plan: marker, runProcess, cwd: installRoot,
installId: marker.installId, projectName: marker.projectName,
});
const ownership = await attestProjectOwnership({
runProcess, cwd: installRoot, dockerHost: marker.dockerHost,
installId: marker.installId, projectName: marker.projectName,
returnDetails: true,
});
if (!ownership.exact) throw new Error("The exact owned sidecar project is missing.");
const runtime = await inspectOwnedSidecarRuntime({ runProcess, installRoot, marker });
if (!runtime || runtime.paused) {
throw new Error("The owned sidecar state is ambiguous; account mutation was not attempted.");
}
const before = (await runSiwcCli({
runProcess, installRoot, marker, command: "account",
})).account;
if (before?.registrationId !== registrationId ||
before?.generation !== expectedGeneration ||
before?.ownerHostId !== marker.ownerHostId ||
before?.ownerRuntimeId !== marker.installId ||
before?.ownership !== "owned") {
throw new Error("The installed SIWC account changed; review it again.");
}
if (action === "enable-plan" &&
(before.planPermission !== "granted" || before.session !== "connected")) {
throw new Error("This installed account needs a fresh authorized SIWC sign-in before plan use can resume.");
}
// Before the account goes and while the sidecar still runs, as on a VPS: a later sign-in on this
// volume must not inherit the image sign-in.
const imagesRevocation = action === "sign-out"
? await signOutLocalCodexImages({ runProcess, installRoot, marker, running: runtime.running })
: undefined;
await runOrThrow(runProcess, {
file: "docker",
args: createComposeArgs(marker.projectName, ["stop", "--timeout", "30", SERVICE_NAME]),
cwd: installRoot, dockerHost: marker.dockerHost,
}, "Owned local sidecar stop");
const stopped = await inspectOwnedSidecarRuntime({ runProcess, installRoot, marker });
if (!stopped || stopped.containerId !== runtime.containerId ||
stopped.running || stopped.paused) {
throw new Error("The owned sidecar did not stop; account mutation was not attempted.");
}
const changed = await runSiwcCli({
runProcess, installRoot, marker, command: action,
input: Buffer.from(JSON.stringify({
registrationId, expectedGeneration,
...(action === "enable-plan" ? { backgroundConsent: true } : {}),
})),
});
if (changed.account?.registrationId !== registrationId ||
changed.account?.ownerHostId !== marker.ownerHostId ||
changed.account?.ownerRuntimeId !== marker.installId ||
changed.account?.ownership !== "owned") {
throw new Error("The installed SIWC account result could not be attested.");
}
if (action === "enable-plan") {
if (changed.account.planEnabled !== true ||
changed.account.planPermission !== "granted") {
throw new Error("The installed SIWC grant could not be confirmed. The sidecar remains stopped.");
}
try {
await runOrThrow(runProcess, {
file: "docker",
args: createComposeArgs(marker.projectName, [
"up", "-d", "--wait", "--wait-timeout", "90",
"--no-build", "--no-deps", SERVICE_NAME,
]),
cwd: installRoot, dockerHost: marker.dockerHost,
}, "Owned SIWC sidecar start");
await verifyRunningSidecar({
runProcess, installRoot, plan: marker,
installId: marker.installId, projectName: marker.projectName,
});
} catch {
let stoppedAfterFailure = false;
try {
await runOrThrow(runProcess, { file: "docker",
args: createComposeArgs(marker.projectName, ["stop", "--timeout", "30", SERVICE_NAME]),
cwd: installRoot, dockerHost: marker.dockerHost }, "Unverified enabled sidecar stop");
const verified = await inspectOwnedSidecarRuntime({ runProcess, installRoot, marker });
stoppedAfterFailure = !!verified && !verified.running && !verified.paused &&
verified.containerId === runtime.containerId && verified.imageId === runtime.imageId;
} catch { /* Preserve the unknown runtime outcome. */ }
throw Object.assign(new Error(stoppedAfterFailure
? "The enabled sidecar failed verification and its stopped state was confirmed."
: "The enabled sidecar outcome is unknown. Inspect and stop the owned service before use."),
{ runtimeStopped: stoppedAfterFailure, remoteOutcomeUnknown: !stoppedAfterFailure });
}
}
return {
account: changed.account, revocation: changed.revocation,
runtimeStopped: action !== "enable-plan",
...(imagesRevocation === undefined ? {} : { imagesRevocation }),
};
},
});
}
// Best effort: signs out of the image add-on in the running sidecar, or in a one-off container
// when it is stopped, and returns OpenAI's revocation result. "unknown" means the step could not
// run or failed; a sidecar built before the add-on has no image module.
async function signOutLocalCodexImages({ runProcess, installRoot, marker, running }) {
try {
const { revocation } = readCodexImagesCliResult(await runProcess({
file: "docker",
args: createComposeArgs(marker.projectName, [
...(running ? ["exec", "-T", SERVICE_NAME, "node"]
: ["run", "--rm", "--no-deps", "-T", "--entrypoint", "node", SERVICE_NAME]),
"/app/services/codex-images.mjs", "sign-out",
]),
cwd: installRoot, dockerHost: marker.dockerHost,
}));
return ["confirmed", "unconfirmed", "not-applicable"].includes(revocation) ? revocation : "unknown";
} catch {
return "unknown";
}
}
const CODEX_IMAGES_ACTIONS = new Set(["login-start", "login-poll", "login-cancel", "sign-out"]);
const CODEX_IMAGES_NOT_RUNNING = "The installed SIWC sidecar is not running for the reviewed account.";
// The image add-on's CLI runs in the exact running owned container, one fixed command per action.
// Status, sign-in start and sign-out re-attest the whole owned project; polling and cancelling a
// pending sign-in check only that the container from the status check still runs.
async function runLocalCodexImages(
{ registrationId, action, expectedContainerId },
{
fileSystem = defaultFileSystem, env = process.env,
homeDirectory = homedir(), runProcess = runLocalProcess,
platform = process.platform, lockDownPath = lockDownLocalPath,
getProcessIdentity, lifecycleLockNow = Date.now,
} = {},
) {
validateSiwcRegistrationId(registrationId);
assertSupportedPlatform(platform);
rejectDockerEnvironmentOverrides(env);
const installRoot = await resolveLocalN8nSidecarInstallRoot({ env, homeDirectory, fileSystem, platform });
const releaseLock = await acquireSidecarLock({
fileSystem, getProcessIdentity, installRoot, lockDownPath, now: lifecycleLockNow, platform,
});
return settleLocalIntegrationLifecycleOperation({
completionLabel: "Local n8n Codex image sign-in operation",
releaseLock,
operation: async () => {
await verifyWindowsSidecarStatusPathSecurity({ fileSystem, installRoot, platform, lockDownPath });
const marker = validateMarker((await inspectManagedInstall({ fileSystem, installRoot })).marker);
if (marker.registrationId !== registrationId) throw new Error(CODEX_IMAGES_NOT_RUNNING);
await assertManagedSiwcCompose(fileSystem, installRoot, marker);
if (await resolveLocalDockerHost({ runProcess, cwd: installRoot, env, platform }) !== marker.dockerHost) {
throw new Error("The selected Docker context changed.");
}
if (["status", "login-start", "sign-out"].includes(action)) {
await attestPlanAndAlias({
plan: marker, runProcess, cwd: installRoot,
installId: marker.installId, projectName: marker.projectName,
});
const ownership = await attestProjectOwnership({
runProcess, cwd: installRoot, dockerHost: marker.dockerHost,
installId: marker.installId, projectName: marker.projectName, returnDetails: true,
});
if (!ownership.exact) throw new Error("The exact owned sidecar project is missing.");
}
const runtime = await inspectOwnedSidecarRuntime({ runProcess, installRoot, marker });
if (!runtime?.running || runtime.paused) throw new Error(CODEX_IMAGES_NOT_RUNNING);
if (expectedContainerId !== undefined && runtime.containerId !== expectedContainerId) {
throw new Error("The installed sidecar changed. Check image generation again.");
}
const result = await runProcess({
file: "docker",
args: createComposeArgs(marker.projectName, [
"exec", "-T", SERVICE_NAME, "node", "/app/services/codex-images.mjs", action,
]),
cwd: installRoot, dockerHost: marker.dockerHost,
});
return {
...readCodexImagesCliResult(result, { allowUnavailable: action === "status" }),
containerId: runtime.containerId,
};
},
});
}
// "unavailable" means the running sidecar predates the image add-on.
export function getLocalN8nCodexImagesStatus({ registrationId }, deps = {}) {
return runLocalCodexImages({ registrationId, action: "status" }, deps);
}
export async function changeLocalN8nCodexImages(
{ registrationId, action, expectedContainerId, confirmed }, deps = {},
) {
if (!CODEX_IMAGES_ACTIONS.has(action)) throw new TypeError("The image sign-in action is invalid.");
if ((action === "login-start" || action === "sign-out") && confirmed !== true) {
throw new Error("Confirm the Codex image sign-in change for this computer.");
}
validateDockerObjectId(expectedContainerId, "reviewed sidecar container");
return runLocalCodexImages({ registrationId, action, expectedContainerId }, deps);
}
export async function removeLocalN8nSidecar(
{ confirmed },
{
fileSystem = defaultFileSystem,
env = process.env,
homeDirectory = homedir(),
runProcess = runLocalProcess,
platform = process.platform,
lockDownPath = lockDownLocalPath,
getProcessIdentity,
lifecycleLockNow = Date.now,
} = {},
) {
if (confirmed !== true) {
throw new Error("Confirm removal of the managed local n8n bridge.");
}
assertSupportedPlatform(platform);
rejectDockerEnvironmentOverrides(env);
const installRoot = await resolveLocalN8nSidecarInstallRoot({
env,
homeDirectory,
fileSystem,
platform,
});
const releaseLock = await acquireSidecarLock({
fileSystem,
getProcessIdentity,
installRoot,
lockDownPath,
now: lifecycleLockNow,
platform,
});
return settleLocalIntegrationLifecycleOperation({
completionLabel: "Local n8n OAuth sidecar removal",
releaseLock,
operation: async () => {
await verifyWindowsSidecarStatusPathSecurity({
fileSystem,
installRoot,
platform,
lockDownPath,
});
const managed = await inspectManagedInstall({ fileSystem, installRoot });
if (!managed.marker) {
throw new Error("The managed local n8n bridge is not installed.");
}
const marker = validateMarker(managed.marker);
if (marker.legacyInstallId) {
throw new Error("This bridge retains an offline legacy credential volume and migration archive. Removal needs a separate reviewed cleanup; nothing was deleted.");
}
await attestProjectOwnership({
runProcess,
cwd: installRoot,
dockerHost: marker.dockerHost,
installId: marker.installId,
projectName: marker.projectName,
});
let imagesRevocation;
if (marker.schemaVersion === MARKER_SCHEMA_VERSION) {
await assertManagedSiwcCompose(fileSystem, installRoot, marker);
const status = await runSiwcCli({
runProcess, installRoot, marker, command: "account",
});
if (status.account?.registrationId !== marker.registrationId ||
status.account?.ownerHostId !== marker.ownerHostId ||
status.account?.session !== "signed-out" ||
status.account?.planEnabled !== false) {
throw new Error("Sign out of the installed SIWC account before removing its protected storage.");
}
// The volume delete below would drop a live image refresh token without revoking it. An install
// made before the add-on has no image module, so it cannot hold an image sign-in.
if (await lstatIfExists(fileSystem, join(installRoot, "services", "codex-images.mjs"))) {
const runtime = await inspectOwnedSidecarRuntime({ runProcess, installRoot, marker });
imagesRevocation = await signOutLocalCodexImages({
runProcess, installRoot, marker, running: runtime?.running === true && !runtime.paused,
});
if (imagesRevocation === "unknown") {
throw new Error("The image sign-out did not finish, so the bridge and its volume were kept. Try removal again.");
}
} else imagesRevocation = "not-applicable";
}
await cleanupSidecarProject({
runProcess,
installRoot,
dockerHost: marker.dockerHost,
projectName: marker.projectName,
});
const imageName = `${marker.projectName}:local`;
if (
await inspectOwnedImageIfPresent({
runProcess,
cwd: installRoot,
dockerHost: marker.dockerHost,
installId: marker.installId,
projectName: marker.projectName,
})
) {
await runOrThrow(
runProcess,
{
file: "docker",
args: ["image", "rm", imageName],
cwd: installRoot,
dockerHost: marker.dockerHost,
},
"Local sidecar image removal",
);
}
await fileSystem.rm(installRoot, { recursive: true, force: false });
return { removed: true, target: LOCAL_N8N_SIDECAR_TARGET,
...(imagesRevocation === undefined ? {} : { imagesRevocation }) };
},
});
}