openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
242 lines (241 loc) • 11.8 kB
JavaScript
import { f as withPluginCache, i as createPluginCache } from "./plugin-cache-DGWspMEc.js";
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { w as root } from "./fs-safe-B6pvPGnf.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { r as loadPluginManifest, s as resolvePackageExtensionEntries } from "./manifest-ByRdkf9X.js";
import { r as containsConfigIncludeDirective } from "./io.read-helpers-ZKp-UiGx.js";
import { n as GUARDED_CONFIG_INCLUDE_WRITE_ERROR } from "./mutation-conflict-Be0wSyDG.js";
import { g as resolvePackedRootDir, u as extractArchive } from "./archive-BfjFIxAJ.js";
import { c as withInstallWorkspace } from "./install-source-utils-DPJygB4h.js";
import { n as applyPersistentOperation, u as isPluginBackingDefaultInferenceRoute } from "./operations-execution-helpers-BpievPkg.js";
import path from "node:path";
import fs from "node:fs/promises";
import { createHash } from "node:crypto";
//#region src/system-agent/plugin-artifact.ts
const MAX_ARTIFACT_BYTES = 33554432;
const MAX_REVIEW_CHARS = 3e3;
const ARTIFACT_REVIEW_TTL_MS = 36e5;
const ARTIFACT_IMPORT_DIR = "imports/plugins";
const PENDING_ARTIFACT_DIR = `${ARTIFACT_IMPORT_DIR}/pending`;
async function assertArtifactConfigPublicationSupported() {
const { readConfigFileSnapshot } = await import("./config/config.js");
const { parsed } = await readConfigFileSnapshot();
if (isRecord(parsed) && (Object.hasOwn(parsed, "$include") || containsConfigIncludeDirective(parsed.plugins))) throw new Error(`${GUARDED_CONFIG_INCLUDE_WRITE_ERROR} Install the reviewed archive with openclaw plugins install.`);
}
function verifyArtifactDigest(bytes, expected) {
if (!/^[a-f0-9]{64}$/u.test(expected) || createHash("sha256").update(bytes).digest("hex") !== expected) throw new Error("Plugin artifact SHA256 does not match. Repack the plugin and propose its new digest.");
}
function artifactArchiveName(sha256) {
if (!/^[a-f0-9]{64}$/u.test(sha256)) throw new Error("Plugin artifact requires a lowercase SHA256 digest.");
return `${sha256}.tgz`;
}
async function withArtifactImports(run) {
const { withPluginLifecycleLease } = await import("./plugin-lifecycle-lease-CLKma701.js");
return await withPluginLifecycleLease({}, async (lease) => {
const stateDir = resolveStateDir();
lease.assertOwned();
await fs.mkdir(stateDir, {
recursive: true,
mode: 448
});
const files = await root(stateDir, {
hardlinks: "reject",
symlinks: "reject",
mode: 384,
maxBytes: MAX_ARTIFACT_BYTES
});
lease.assertOwned();
await files.mkdir(PENDING_ARTIFACT_DIR);
return await run(files, () => lease.assertOwned());
});
}
async function prunePendingArtifacts(files, archiveName, assertOwned) {
const pending = (await files.list(PENDING_ARTIFACT_DIR, { withFileTypes: true })).filter((entry) => entry.isFile && /^[a-f0-9]{64}\.tgz$/u.test(entry.name)).toSorted((left, right) => right.mtimeMs - left.mtimeMs || left.name.localeCompare(right.name));
const expiresBefore = Date.now() - ARTIFACT_REVIEW_TTL_MS;
let remaining = 7;
for (const entry of pending) {
if (entry.name === archiveName) continue;
if (entry.mtimeMs <= expiresBefore || entry.size > MAX_ARTIFACT_BYTES || remaining === 0) {
assertOwned();
await files.remove(`${PENDING_ARTIFACT_DIR}/${entry.name}`);
} else remaining -= 1;
}
}
async function readVerifiedArtifact(filePath, sha256) {
if (!path.isAbsolute(filePath) || !/\.(?:tgz|tar\.gz)$/u.test(filePath)) throw new Error("Plugin artifact path must be an absolute .tgz or .tar.gz file from openclaw plugins pack.");
const bytes = await (await root(path.dirname(filePath), {
hardlinks: "reject",
symlinks: "reject",
maxBytes: MAX_ARTIFACT_BYTES
})).readBytes(path.basename(filePath));
verifyArtifactDigest(bytes, sha256);
return bytes;
}
async function inspectArtifact(rootDir) {
const artifact = await root(rootDir, {
hardlinks: "reject",
symlinks: "reject",
maxBytes: 1048576
});
const packageJson = await artifact.readJson("package.json");
if (!isRecord(packageJson)) throw new Error("Plugin artifact package.json must be an object.");
for (const field of [
"dependencies",
"optionalDependencies",
"scripts"
]) {
const value = packageJson[field];
if (value !== void 0 && (!isRecord(value) || Object.keys(value).length > 0)) throw new Error(`Plugin artifacts cannot contain ${field}. Use openclaw plugins pack to bundle the plugin first.`);
}
const peers = packageJson.peerDependencies;
if (peers !== void 0 && (!isRecord(peers) || Object.keys(peers).some((name) => name !== "openclaw")) || await artifact.exists("node_modules")) throw new Error("Plugin artifacts must bundle dependencies and may only reference the host openclaw peer.");
const extensions = resolvePackageExtensionEntries(packageJson);
if (extensions.status !== "ok" || extensions.entries.length !== 1) throw new Error("Plugin artifact must declare exactly one compiled openclaw.extensions entry.");
const entry = (extensions.entries[0] ?? "").replace(/^\.\//u, "");
if (!/^dist\/(?:[\w-][\w.-]*\/)*[\w-][\w.-]*\.(?:[cm]?js)$/u.test(entry)) throw new Error("Plugin artifact entry must be compiled JavaScript under dist/.");
await artifact.readBytes(entry, { maxBytes: MAX_ARTIFACT_BYTES });
const loaded = withPluginCache(createPluginCache(), () => loadPluginManifest(rootDir, true, artifact.rootReal));
if (!loaded.ok) throw new Error(loaded.error);
const manifest = loaded.manifest;
if (manifest.controlUi) for (const asset of [manifest.controlUi.entry, ...manifest.controlUi.styles ?? []]) await artifact.readBytes(asset, { maxBytes: MAX_ARTIFACT_BYTES });
const { resolvePluginArtifactDeclaredSurface } = await import("./capability-artifact-CiRByqot.js");
const { computeDeclaredSurfaceHash } = await import("./capability-summary-Dp2gyoi6.js");
const declared = resolvePluginArtifactDeclaredSurface(rootDir);
const review = {
pluginId: manifest.id,
name: manifest.name ?? manifest.id,
...manifest.version ? { version: manifest.version } : {},
nativeControlUi: Boolean(manifest.controlUi),
declared,
reviewToken: computeDeclaredSurfaceHash(declared)
};
if (JSON.stringify(review).length > MAX_REVIEW_CHARS) throw new Error("Plugin artifact review is too large for an agent proposal. Install it from a trusted shell after reviewing its declared capabilities.");
return review;
}
async function withVerifiedArtifact(bytes, run) {
return await withInstallWorkspace("openclaw-plugin-artifact-", async (workspace) => {
const files = await root(workspace, {
hardlinks: "reject",
symlinks: "reject",
mode: 384
});
await files.create("package.tgz", bytes);
await files.mkdir("extract");
const archivePath = path.join(workspace, "package.tgz");
const extractDir = path.join(workspace, "extract");
await extractArchive({
archivePath,
destDir: extractDir,
timeoutMs: 3e4,
limits: {
maxArchiveBytes: MAX_ARTIFACT_BYTES,
maxEntries: 512,
maxExtractedBytes: 67108864,
maxEntryBytes: MAX_ARTIFACT_BYTES
}
});
return await run({
archivePath,
review: await inspectArtifact(await resolvePackedRootDir(extractDir, { rootMarkers: ["package.json", "openclaw.plugin.json"] }))
});
});
}
/** Completes and inspects the import before the host records any executable proposal. */
async function prepareSystemAgentPluginArtifact(operation) {
const bytes = await readVerifiedArtifact(operation.path, operation.sha256);
const review = await withVerifiedArtifact(bytes, async (artifact) => artifact.review);
await assertArtifactConfigPublicationSupported();
if (await isPluginBackingDefaultInferenceRoute(review.pluginId)) throw new Error("This plugin backs OpenClaw's active inference route. Stop OpenClaw and install the artifact from a trusted shell.");
return await withArtifactImports(async (files, assertOwned) => {
const archiveName = artifactArchiveName(operation.sha256);
const importPath = `${PENDING_ARTIFACT_DIR}/${archiveName}`;
await prunePendingArtifacts(files, archiveName, assertOwned);
assertOwned();
await files.write(importPath, bytes);
return {
...review,
sha256: operation.sha256,
retainedPath: path.join(files.rootDir, importPath)
};
});
}
async function executePluginArtifactActivation(operation, runtime, opts) {
if (!opts.approved) runtime.log(JSON.stringify(await prepareSystemAgentPluginArtifact(operation)));
const result = await applyPersistentOperation({
auditOperation: "plugin.activateArtifact",
operation,
runtime,
opts,
run: async (ctx) => await withArtifactImports(async (files, assertOwned) => {
const archiveName = artifactArchiveName(operation.sha256);
const pendingPath = `${PENDING_ARTIFACT_DIR}/${archiveName}`;
if (!await files.exists(pendingPath) || (await files.stat(pendingPath)).mtimeMs <= Date.now() - ARTIFACT_REVIEW_TTL_MS) throw new Error("Plugin artifact review expired or is no longer retained. Propose the exact artifact again before applying it.");
const bytes = await files.readBytes(pendingPath);
verifyArtifactDigest(bytes, operation.sha256);
return await withVerifiedArtifact(bytes, async ({ archivePath, review }) => {
const { assertConfigWriteAllowedInCurrentMode } = await import("./config/config.js");
const { loadConfigForInstall } = await import("./plugins-install-config-DlairFPF.js");
const { installManagedPluginSource } = await import("./management-install-DsHuE9D_.js");
assertConfigWriteAllowedInCurrentMode();
const assertPersistentApply = () => {
ctx.assertPersistentApply?.();
assertOwned();
};
const guard = async () => {
await assertArtifactConfigPublicationSupported();
if (await isPluginBackingDefaultInferenceRoute(review.pluginId)) throw new Error("Artifact activation stopped: this plugin now backs the active inference route. Stop OpenClaw and install it from a trusted shell.");
assertPersistentApply();
};
await assertArtifactConfigPublicationSupported();
const snapshot = await loadConfigForInstall({
rawSpec: archivePath,
normalizedSpec: archivePath,
resolvedPath: archivePath,
installKind: "plugin"
});
const importPath = `${ARTIFACT_IMPORT_DIR}/${archiveName}`;
const retainedPath = path.join(files.rootDir, importPath);
const retained = await files.exists(importPath);
if (retained) verifyArtifactDigest(await files.readBytes(importPath), operation.sha256);
await ctx.commit(async () => {
assertOwned();
if (!retained) await files.create(importPath, bytes);
assertOwned();
await files.remove(pendingPath);
try {
const installed = await installManagedPluginSource({
request: {
source: "local",
path: archivePath,
recordPath: retainedPath,
recordSource: "archive",
mode: "update"
},
snapshot,
runtime,
acknowledgeCapabilities: { reviewToken: review.reviewToken },
beforePersistentApply: assertPersistentApply,
beforePersistentEffect: guard
});
if (!installed.ok) throw new Error(installed.error);
} catch (error) {
runtime.error("Inspect the plugin's current status, then propose the exact artifact again before retrying.");
throw error;
}
});
return {
summary: `Installed approved plugin artifact ${review.pluginId}`,
details: {
pluginId: review.pluginId,
sha256: operation.sha256,
sourcePath: retainedPath
}
};
});
})
});
if (result.applied) runtime.log("Artifact installed. Restart the Gateway to load backend changes, then inspect the plugin's Control UI activation status.");
return result;
}
//#endregion
export { executePluginArtifactActivation, prepareSystemAgentPluginArtifact };