src/domain/assistant.js
Rama main · réplica de lectura del repositorio original.
import {
INSTALL_ROOT,
MANAGED_MARKER_PATH,
SHARED_ROOT_COMMAND,
SHARED_ROOT_MARKER_CONTENT,
SHARED_ROOT_MARKER_PATH,
SIDECAR_MARKER_CONTENT,
} from "./safety.js";
import { validateDockerName } from "./validation.js";
export const ASSISTANT_ROOT = INSTALL_ROOT + "/assistant-sandbox";
export const ASSISTANT_MARKER_PATH =
ASSISTANT_ROOT + "/.managed-by-relmio-ai-assistant";
export { SHARED_ROOT_MARKER_PATH as ASSISTANT_ROOT_MARKER_PATH };
const ASSISTANT_MARKER_VERSION = 2;
const LEGACY_ASSISTANT_MARKER_VERSION = 1;
const INSTALL_ID_PATTERN = /^[a-f0-9]{32}$/u;
const PROJECT_NAME_PATTERN = /^relmio-ai-[a-f0-9]{32}$/u;
const SANDBOX_ALIAS_PATTERN = /^relmio-ai-sandbox-[a-f0-9]{32}$/u;
const SEARXNG_ALIAS_PATTERN = /^relmio-ai-searxng-[a-f0-9]{32}$/u;
const MAX_MARKER_BYTES = 512;
const MANAGED_LABEL = "io.relmio.ai-assistant.managed";
const INSTALL_ID_LABEL = "io.relmio.ai-assistant.install-id";
const EXPECTED_MANAGED_LABEL_VALUE = "true";
const SANDBOX_SERVICE_NAMES = Object.freeze([
"relmio-sandbox-certs",
"relmio-sandbox-api",
"relmio-sandbox-runner-1",
]);
const SEARXNG_SERVICE_NAME = "relmio-searxng";
const ROOT_PRECHECK =
"if [ -e " + INSTALL_ROOT + " ]; then " +
"if [ -L " + INSTALL_ROOT + " ] || [ ! -d " + INSTALL_ROOT + " ] || " +
"[ -L " + SHARED_ROOT_MARKER_PATH + " ] || [ -L " + MANAGED_MARKER_PATH + " ]; then exit 43; " +
"elif [ -e " + SHARED_ROOT_MARKER_PATH + " ]; then " +
"if [ ! -f " + SHARED_ROOT_MARKER_PATH + " ] || [ \"$(cat " + SHARED_ROOT_MARKER_PATH + ")\" != \"" +
SHARED_ROOT_MARKER_CONTENT.trim() + "\" ]; then exit 42; fi; " +
"elif [ -f " + MANAGED_MARKER_PATH + " ] && [ \"$(cat " +
MANAGED_MARKER_PATH + ")\" = \"" + SIDECAR_MARKER_CONTENT.trim() +
"\" ]; then :; else exit 42; fi; fi";
export const ASSISTANT_PRECHECK_COMMAND =
ROOT_PRECHECK + "; if [ -L " + ASSISTANT_ROOT + " ]; then exit 43; " +
"elif [ -e " + ASSISTANT_ROOT + " ]; then " +
"if [ ! -d " + ASSISTANT_ROOT + " ]; then exit 42; " +
"elif [ -L " + ASSISTANT_MARKER_PATH + " ] || [ -L " +
ASSISTANT_ROOT + "/docker-compose.yml ] || [ -L " +
ASSISTANT_ROOT + "/searxng-settings.yml ] || [ -L " +
ASSISTANT_ROOT + "/.env ]; then exit 43; " +
"elif [ ! -f " + ASSISTANT_MARKER_PATH + " ]; then exit 42; " +
"else printf '%s\\n' managed; cat " + ASSISTANT_MARKER_PATH +
"; fi; else printf '%s\\n' new; fi";
function createInstallHex(randomBytes) {
const value = randomBytes(16);
if (!Buffer.isBuffer(value) || value.length !== 16) {
throw new TypeError("A cryptographic AI Assistant install generator is required.");
}
return value.toString("hex");
}
function objectHasExactKeys(value, keys) {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
return actual.length === expected.length &&
actual.every((key, index) => key === expected[index]);
}
export function validateAssistantInstallation(value) {
const legacyKeys = [
"version",
"installId",
"projectName",
"sandboxAlias",
"searxngAlias",
];
if (
value?.version === LEGACY_ASSISTANT_MARKER_VERSION &&
objectHasExactKeys(value, legacyKeys)
) {
return validateAssistantInstallation({
...value,
version: ASSISTANT_MARKER_VERSION,
includeSearxng: true,
});
}
const requiredKeys = [...legacyKeys, "includeSearxng"];
if (
!objectHasExactKeys(value, requiredKeys) ||
value.version !== ASSISTANT_MARKER_VERSION ||
typeof value.installId !== "string" ||
!INSTALL_ID_PATTERN.test(value.installId) ||
typeof value.projectName !== "string" ||
!PROJECT_NAME_PATTERN.test(value.projectName) ||
typeof value.sandboxAlias !== "string" ||
!SANDBOX_ALIAS_PATTERN.test(value.sandboxAlias) ||
typeof value.searxngAlias !== "string" ||
!SEARXNG_ALIAS_PATTERN.test(value.searxngAlias) ||
typeof value.includeSearxng !== "boolean" ||
value.sandboxAlias === value.searxngAlias ||
value.sandboxAlias.endsWith(value.installId) ||
value.searxngAlias.endsWith(value.installId) ||
value.sandboxAlias.slice(-32) === value.searxngAlias.slice(-32)
) {
throw new TypeError("The AI Assistant installation marker is invalid.");
}
return {
version: ASSISTANT_MARKER_VERSION,
installId: value.installId,
projectName: value.projectName,
sandboxAlias: value.sandboxAlias,
searxngAlias: value.searxngAlias,
includeSearxng: value.includeSearxng,
};
}
export function validateAssistantSearxngSelection(includeSearxng) {
if (typeof includeSearxng !== "boolean") {
throw new TypeError("Choose whether to include optional SearXNG web search.");
}
return includeSearxng;
}
export function createAssistantInstallation({ randomBytes, includeSearxng }) {
if (typeof randomBytes !== "function") {
throw new TypeError("A cryptographic AI Assistant install generator is required.");
}
const selectedSearxng = validateAssistantSearxngSelection(includeSearxng);
const installId = createInstallHex(randomBytes);
const projectSuffix = createInstallHex(randomBytes);
const sandboxSuffix = createInstallHex(randomBytes);
const searxngSuffix = createInstallHex(randomBytes);
return validateAssistantInstallation({
version: ASSISTANT_MARKER_VERSION,
installId,
projectName: "relmio-ai-" + projectSuffix,
sandboxAlias: "relmio-ai-sandbox-" + sandboxSuffix,
searxngAlias: "relmio-ai-searxng-" + searxngSuffix,
includeSearxng: selectedSearxng,
});
}
export function serializeAssistantMarker(installation) {
return JSON.stringify(validateAssistantInstallation(installation)) + "\n";
}
export function parseAssistantMarker(contents) {
if (
typeof contents !== "string" ||
contents.length === 0 ||
Buffer.byteLength(contents, "utf8") > MAX_MARKER_BYTES
) {
throw new TypeError("The AI Assistant installation marker is invalid.");
}
try {
return validateAssistantInstallation(JSON.parse(contents));
} catch {
throw new TypeError("The AI Assistant installation marker is invalid.");
}
}
export function parseAssistantPrecheck(output) {
if (typeof output !== "string" || output.length === 0) {
throw new TypeError("The VPS AI Assistant install-directory check returned an invalid state.");
}
if (output === "new\n") return { state: "new", installation: null };
if (!output.startsWith("managed\n")) {
throw new TypeError("The VPS AI Assistant install-directory check returned an invalid state.");
}
return {
state: "managed",
installation: parseAssistantMarker(output.slice("managed\n".length)),
};
}
function composePrefix(installation) {
const safeInstallation = validateAssistantInstallation(installation);
return "docker compose --project-name " + safeInstallation.projectName +
" --file " + ASSISTANT_ROOT + "/docker-compose.yml";
}
export function getAssistantServiceNames(installation) {
const safeInstallation = validateAssistantInstallation(installation);
return safeInstallation.includeSearxng
? [...SANDBOX_SERVICE_NAMES, SEARXNG_SERVICE_NAME]
: [...SANDBOX_SERVICE_NAMES];
}
export function getAssistantContainerNames(installation) {
const safeInstallation = validateAssistantInstallation(installation);
const prefix = "relmio-ai-" + safeInstallation.installId.slice(0, 16) + "-";
return {
certs: prefix + "certs",
api: prefix + "api",
runner: prefix + "runner",
...(safeInstallation.includeSearxng ? { searxng: prefix + "search" } : {}),
};
}
function validateAssistantStartupScope(installation, startupScope) {
if (startupScope === "all") return getAssistantServiceNames(installation);
if (startupScope === "searxng" && installation.includeSearxng) {
return [SEARXNG_SERVICE_NAME];
}
throw new TypeError("The AI Assistant startup scope is invalid.");
}
export function createAssistantDeploymentCommands({ installation, startupScope = "all" }) {
const safeInstallation = validateAssistantInstallation(installation);
const prefix = composePrefix(safeInstallation);
const publicPaths = [ASSISTANT_ROOT + "/docker-compose.yml"];
if (safeInstallation.includeSearxng) {
publicPaths.push(ASSISTANT_ROOT + "/searxng-settings.yml");
}
const startupServices = validateAssistantStartupScope(safeInstallation, startupScope);
return [
// The guard every installer runs first: /docker and the shared folder are root's own, and the
// shared folder is created or set to mode 0700.
SHARED_ROOT_COMMAND,
// Creates the Assistant folder for root only, then proves it is root's 0700 folder and not a link.
"[ ! -L " + ASSISTANT_ROOT + " ] && if [ ! -e " + ASSISTANT_ROOT + " ]; then mkdir -m 0700 " +
ASSISTANT_ROOT + "; fi && [ -d " + ASSISTANT_ROOT + " ] && [ \"$(stat -c %u:%a " +
ASSISTANT_ROOT + ")\" = 0:700 ]",
"chmod 600 " + SHARED_ROOT_MARKER_PATH + " " + ASSISTANT_MARKER_PATH +
" " + ASSISTANT_ROOT + "/.env",
"chmod 644 " + publicPaths.join(" "),
"test \"$(stat -c '%a' " + SHARED_ROOT_MARKER_PATH + ")\" = 600 && " +
"test \"$(stat -c '%a' " + ASSISTANT_MARKER_PATH + ")\" = 600 && " +
"test \"$(stat -c '%a' " + ASSISTANT_ROOT + "/.env)\" = 600",
publicPaths.map(
(path) => "test \"$(stat -c '%a' " + path + ")\" = 644",
).join(" && "),
prefix + " config --quiet",
prefix + " up -d --wait --wait-timeout 90 " + startupServices.join(" "),
];
}
export function createAssistantVerificationCommands({ installation, cleanupScope = "project" }) {
const safeInstallation = validateAssistantInstallation(installation);
const services = getAssistantServiceNames(safeInstallation).filter(
(service) => service !== "relmio-sandbox-certs",
);
if (cleanupScope !== "project" && cleanupScope !== "searxng") {
throw new TypeError("The AI Assistant cleanup scope is invalid.");
}
if (cleanupScope === "searxng" && !safeInstallation.includeSearxng) {
throw new TypeError("The AI Assistant cleanup scope is invalid.");
}
const prefix = composePrefix(safeInstallation);
return {
health: prefix +
" exec -T relmio-sandbox-api wget -qO- http://127.0.0.1:8080/healthz",
runningServices: prefix + " ps --status running --services",
publicationState: prefix + " ps --format json " + services.join(" "),
cleanup: cleanupScope === "searxng"
? prefix + " rm -sf " + SEARXNG_SERVICE_NAME
: prefix + " down --volumes",
};
}
export function getAssistantManagedResourceNames(installation) {
const safeInstallation = validateAssistantInstallation(installation);
return {
containers: Object.values(getAssistantContainerNames(safeInstallation)),
network: safeInstallation.projectName + "-internal",
volume: safeInstallation.projectName + "-sandbox-tls",
};
}
function createAttestationCommand(resource, installation) {
const safeInstallation = validateAssistantInstallation(installation);
const filter = "--filter \"label=com.docker.compose.project=" +
safeInstallation.projectName + "\"";
if (resource === "containers") {
return "docker container ls -a " + filter +
" --format '{{.Label \"" + INSTALL_ID_LABEL + "\"}}|{{.Label \"" +
MANAGED_LABEL + "\"}}'";
}
if (resource === "networks") {
return "docker network ls " + filter +
" --format '{{.Label \"" + INSTALL_ID_LABEL + "\"}}|{{.Label \"" +
MANAGED_LABEL + "\"}}'";
}
return "docker volume ls " + filter +
" --format '{{.Label \"" + INSTALL_ID_LABEL + "\"}}|{{.Label \"" +
MANAGED_LABEL + "\"}}'";
}
export function createAssistantOwnershipAttestationCommands({ installation }) {
return {
containers: createAttestationCommand("containers", installation),
networks: createAttestationCommand("networks", installation),
volumes: createAttestationCommand("volumes", installation),
};
}
function createExactResourceCommand(kind, names) {
const nameField = kind === "containers" ? ".Names" : ".Name";
const format = " --format '{{" + nameField + "}}|{{.Label \"" + INSTALL_ID_LABEL +
"\"}}|{{.Label \"" + MANAGED_LABEL + "\"}}'";
if (kind === "containers") {
return names.containers.map(
(name) => "docker container ls -a --filter \"name=" + name + "\"" + format,
).join(" && ");
}
if (kind === "network") {
return "docker network ls --filter \"name=" + names.network + "\"" + format;
}
return "docker volume ls --filter \"name=" + names.volume + "\"" + format;
}
export function createAssistantExactResourceAttestationCommands({ installation }) {
const names = getAssistantManagedResourceNames(installation);
return {
containers: createExactResourceCommand("containers", names),
network: createExactResourceCommand("network", names),
volume: createExactResourceCommand("volume", names),
};
}
export function createAssistantNetworkCollisionCommand({ networkName, installation }) {
const safeNetworkName = validateDockerName(networkName);
validateAssistantInstallation(installation);
return "ids=$(docker network inspect " + safeNetworkName +
" --format '{{range $id, $_ := .Containers}}{{$id}}{{\"\\n\"}}{{end}}') && " +
"if [ -n \"$ids\" ]; then printf '%s\\n' \"$ids\" | xargs -r docker inspect --format '{{.Id}}|{{index .Config.Labels \"" +
INSTALL_ID_LABEL + "\"}}|{{with index .NetworkSettings.Networks \"" +
safeNetworkName + "\"}}{{join .Aliases \",\"}}{{end}}'; fi";
}
export function getAssistantLabels(installation) {
const safeInstallation = validateAssistantInstallation(installation);
return {
[MANAGED_LABEL]: EXPECTED_MANAGED_LABEL_VALUE,
[INSTALL_ID_LABEL]: safeInstallation.installId,
};
}
export function assertAssistantOnlyCommands({
commands,
installation,
networkName,
startupScope = "all",
cleanupScope = "project",
}) {
if (!Array.isArray(commands) || commands.length === 0) {
throw new TypeError("An AI Assistant command list is required.");
}
const allowed = new Set([
ASSISTANT_PRECHECK_COMMAND,
createAssistantNetworkCollisionCommand({ networkName, installation }),
...Object.values(createAssistantOwnershipAttestationCommands({ installation })),
...Object.values(createAssistantExactResourceAttestationCommands({ installation })),
...createAssistantDeploymentCommands({ installation, startupScope }),
...Object.values(createAssistantVerificationCommands({ installation, cleanupScope })),
]);
for (const command of commands) {
if (typeof command !== "string" || !allowed.has(command)) {
throw new Error(
"Command rejected: only the managed AI Assistant companion may be changed; n8n must remain untouched.",
);
}
}
return commands;
}