openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
416 lines (415 loc) • 14.5 kB
JavaScript
import { t as sanitizeForLog } from "./ansi-DrXAcdMD.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js";
import "./fs-safe-defaults-DGE52WTl.js";
import { d as pathExists } from "./fs-safe-B6pvPGnf.js";
import "./utils-P__uGsPB.js";
import { o as redactSensitiveUrlLikeString } from "./redact-sensitive-url-BN1NZvXG.js";
import { o as sha256HexPrefixCore } from "./crypto-digest-C4hqTb_e.js";
import { o as resolveDefaultPluginGitDir } from "./install-paths-ChnxikBv.js";
import { t as PLUGIN_INSTALL_ERROR_CODE } from "./install-types-DY_kphq4.js";
import { t as hasHttpUrlPrefix } from "./url-protocol-OU3K-ySz.js";
import { r as runCommandWithTimeout } from "./exec-BIE-3oLG.js";
import { c as withInstallWorkspace } from "./install-source-utils-DPJygB4h.js";
import { a as resolvePackageDirInstallTransaction, i as requestDeferredPackageDirInstall, o as createSafeNpmInstallArgs, r as installPackageDir, s as createSafeNpmInstallEnv } from "./install-package-dir-kJK8m3kQ.js";
import { n as preflightPluginGitInstallPolicy } from "./install-security-scan-DYLR970X.js";
import { E as loadPluginInstallRuntime, F as emitPluginInstallSecurityEvent, I as pluginAuditOutcomeForReason, P as emitPluginAuditSecurityEvent, R as attachPluginInstallTransaction, S as ensureInstallTargetAvailableForMode, U as resolvePluginInstallTransactionRequest } from "./install-managed-npm-state-BlNS_UI7.js";
import { a as installPluginFromInstalledPackageDir } from "./install-BezM8CtS.js";
import path from "node:path";
import fs from "node:fs/promises";
//#region src/plugins/git-install.ts
const GIT_SPEC_PREFIX = "git:";
const DEFAULT_GIT_TIMEOUT_MS = 12e4;
const FULL_GIT_COMMIT_PATTERN = /^[0-9a-f]{40}$/i;
/** Returns true for full commit SHAs that do not require branch/tag drift checks. */
function isImmutableGitCommitRef(ref) {
return FULL_GIT_COMMIT_PATTERN.test(ref ?? "");
}
function splitGitSpecRef(input) {
const hashIndex = input.lastIndexOf("#");
if (hashIndex > 0) return {
base: input.slice(0, hashIndex),
ref: normalizeOptionalString(input.slice(hashIndex + 1))
};
for (let atIndex = input.lastIndexOf("@"); atIndex > 0; atIndex = input.lastIndexOf("@", atIndex - 1)) {
const base = input.slice(0, atIndex);
const ref = normalizeOptionalString(input.slice(atIndex + 1));
if (ref && isGitSpecBase(base)) return {
base,
ref
};
}
return { base: input };
}
function isGitSpecBase(value) {
return looksLikeGitHubRepoShorthand(value) || looksLikeGitHubHostPath(value) || looksLikeUrlGitSpecBase(value) || looksLikeScpGitUrl(value) || value.endsWith(".git") || value.startsWith("./") || value.startsWith("../") || value.startsWith("~/");
}
function looksLikeGitHubRepoShorthand(value) {
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(value);
}
function looksLikeGitHubHostPath(value) {
return /^github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/i.test(value);
}
function isGitUrl(value) {
if (value.startsWith("-")) return false;
return /^(?:ssh|git|file):\/\//i.test(value) || looksLikeScpGitUrl(value) || value.endsWith(".git");
}
function looksLikeScpGitUrl(value) {
return /^[^@\s]+@[^:\s]+:.+/.test(value);
}
function looksLikeUrlGitSpecBase(value) {
try {
const url = new URL(value);
if (![
"http:",
"https:",
"ssh:",
"git:",
"file:"
].includes(url.protocol)) return false;
if (url.protocol === "file:") return url.pathname.length > 1;
return Boolean(url.hostname) && url.pathname.length > 1;
} catch {
return false;
}
}
function stripGitSuffix(value) {
return value.replace(/\.git$/i, "");
}
function normalizeGitHubRepo(value) {
const repo = stripGitSuffix(value.replace(/^github\.com\//i, ""));
return {
url: `https://github.com/${repo}.git`,
label: repo
};
}
function normalizeGitLabel(value) {
if (hasHttpUrlPrefix(value) || /^(?:ssh|git|file):\/\//i.test(value)) try {
const url = new URL(value);
return stripGitSuffix(`${url.hostname}${url.pathname}`).replace(/^\/+/, "");
} catch {
return stripGitSuffix(value);
}
return stripGitSuffix(value);
}
function parseGitPluginSpec(raw) {
const trimmed = raw.trim();
if (!trimmed.toLowerCase().startsWith(GIT_SPEC_PREFIX)) return null;
const body = trimmed.slice(4).trim();
if (!body) return null;
const split = splitGitSpecRef(body);
const base = split.base.trim();
if (!base) return null;
if (looksLikeGitHubRepoShorthand(base) || looksLikeGitHubHostPath(base)) {
const normalized = normalizeGitHubRepo(base);
return {
input: trimmed,
url: normalized.url,
ref: split.ref,
label: normalized.label,
normalizedSpec: `${GIT_SPEC_PREFIX}${normalized.url}${split.ref ? `@${split.ref}` : ""}`
};
}
if (hasHttpUrlPrefix(base) || isGitUrl(base) || base.startsWith("./") || base.startsWith("../") || base.startsWith("~/")) {
const url = base.startsWith("./") || base.startsWith("../") || base.startsWith("~/") ? resolveUserPath(base) : base;
return {
input: trimmed,
url,
ref: split.ref,
label: normalizeGitLabel(url),
normalizedSpec: `${GIT_SPEC_PREFIX}${url}${split.ref ? `@${split.ref}` : ""}`
};
}
return null;
}
function createGitCommandEnv() {
return {
GIT_TERMINAL_PROMPT: "0",
GIT_CONFIG_NOSYSTEM: "1",
GIT_TEMPLATE_DIR: "",
GIT_EDITOR: "",
GIT_SEQUENCE_EDITOR: "",
GIT_EXTERNAL_DIFF: "",
GIT_DIR: void 0,
GIT_WORK_TREE: void 0,
GIT_COMMON_DIR: void 0,
GIT_INDEX_FILE: void 0,
GIT_OBJECT_DIRECTORY: void 0,
GIT_ALTERNATE_OBJECT_DIRECTORIES: void 0,
GIT_NAMESPACE: void 0,
GIT_EXEC_PATH: void 0,
GIT_SSL_NO_VERIFY: void 0
};
}
function resolveGitInstallRepoDir(params) {
const gitRoot = params.gitDir ? resolveUserPath(params.gitDir) : resolveDefaultPluginGitDir();
const redactedSpec = redactSensitiveUrlLikeString(params.source.normalizedSpec);
return path.join(gitRoot, `git-${sha256HexPrefixCore(redactedSpec, 16)}`, "repo");
}
async function withGitStagingDir(persistentRepoDir, fn) {
if (!persistentRepoDir) return await withInstallWorkspace("openclaw-git-plugin-", fn);
const targetParent = path.dirname(persistentRepoDir);
try {
await fs.mkdir(targetParent, { recursive: true });
} catch {
return await withInstallWorkspace("openclaw-git-plugin-", fn);
}
let callbackStarted = false;
try {
return await withInstallWorkspace("openclaw-git-plugin-", async (tmpDir) => {
callbackStarted = true;
return await fn(tmpDir);
}, { rootDir: targetParent });
} catch (err) {
if (callbackStarted) throw err;
return await withInstallWorkspace("openclaw-git-plugin-", fn);
}
}
async function replaceManagedGitRepo(params) {
let artifactConsentFailure;
const reviewFinalArtifact = async (stagedRepoDir) => {
try {
await params.onBeforePublish?.(stagedRepoDir);
return { ok: true };
} catch (error) {
artifactConsentFailure = { error };
throw error;
}
};
try {
const installParams = {
sourceDir: params.stagedRepoDir,
targetDir: params.persistentRepoDir,
mode: await pathExists(params.persistentRepoDir) ? "update" : "install",
timeoutMs: DEFAULT_GIT_TIMEOUT_MS,
copyErrorPrefix: "failed to replace managed git plugin repository",
hasDeps: false,
depsLogMessage: "",
afterInstall: reviewFinalArtifact,
beforePersistentApply: params.beforePersistentApply
};
const result = await installPackageDir(params.deferCommit ? requestDeferredPackageDirInstall(installParams, params.assertOwned) : installParams);
if (artifactConsentFailure) throw artifactConsentFailure.error;
const transaction = result.ok ? resolvePackageDirInstallTransaction(result) : void 0;
return result.ok ? {
ok: true,
...transaction ? { transaction } : {}
} : result;
} catch (err) {
if (artifactConsentFailure) throw artifactConsentFailure.error;
return {
ok: false,
error: `failed to replace managed git plugin repository: ${String(err)}`
};
}
}
function formatGitCommandFailure(params) {
const detail = sanitizeForLog(redactSensitiveUrlLikeString(params.stderr.trim() || params.stdout.trim() || "git failed"));
return `failed to ${params.action} ${sanitizeForLog(redactSensitiveUrlLikeString(params.source.label))}: ${detail}`;
}
function buildBlockedGitInstallResult(params) {
return {
ok: false,
error: params.blocked.reason,
...params.blocked.code === "security_scan_failed" ? { code: PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_FAILED } : params.blocked.code === "security_scan_blocked" ? { code: PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED } : {}
};
}
async function runGitCommand(params) {
const result = await runCommandWithTimeout(params.argv, {
cwd: params.cwd,
timeoutMs: params.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS,
env: createGitCommandEnv()
});
if (result.code !== 0) return {
ok: false,
error: formatGitCommandFailure({
action: params.action,
source: params.source,
stdout: result.stdout,
stderr: result.stderr
})
};
return {
ok: true,
stdout: result.stdout
};
}
async function installPluginFromGitSpec(params) {
const parsed = parseGitPluginSpec(params.spec);
if (!parsed) return {
ok: false,
error: `unsupported git: plugin spec: ${params.spec}`
};
const persistentRepoDir = resolveGitInstallRepoDir({
gitDir: params.gitDir,
source: parsed
});
const effectiveMode = params.mode === "update" && await pathExists(persistentRepoDir) ? "update" : "install";
const availability = await ensureInstallTargetAvailableForMode({
runtime: await loadPluginInstallRuntime(),
targetPath: persistentRepoDir,
mode: effectiveMode
});
if (!availability.ok) return availability;
return await withGitStagingDir(params.dryRun ? void 0 : persistentRepoDir, async (tmpDir) => {
const repoDir = path.join(tmpDir, "repo");
params.logger?.info?.(`Cloning ${sanitizeForLog(redactSensitiveUrlLikeString(parsed.label))}...`);
const clone = await runGitCommand({
argv: parsed.ref ? [
"git",
"clone",
"--",
parsed.url,
repoDir
] : [
"git",
"clone",
"--depth",
"1",
"--",
parsed.url,
repoDir
],
action: "clone",
source: parsed,
timeoutMs: params.timeoutMs
});
if (!clone.ok) return clone;
if (parsed.ref) {
const checkout = await runGitCommand({
argv: [
"git",
"switch",
"--detach",
"--",
parsed.ref
],
action: `checkout ${parsed.ref}`,
source: parsed,
cwd: repoDir,
timeoutMs: params.timeoutMs
});
if (!checkout.ok) return checkout;
}
const rev = await runGitCommand({
argv: [
"git",
"rev-parse",
"HEAD"
],
action: "resolve commit for",
source: parsed,
cwd: repoDir,
timeoutMs: params.timeoutMs
});
if (!rev.ok) return rev;
const installPolicyRequest = {
kind: "plugin-git",
requestedSpecifier: parsed.input,
source: {
kind: "git",
authority: "third-party",
mutable: !isImmutableGitCommitRef(parsed.ref),
network: true
}
};
const preflight = await preflightPluginGitInstallPolicy({
config: params.config,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
onInstallPolicyWarning: params.onInstallPolicyWarning,
logger: params.logger ?? {},
mode: effectiveMode,
pluginId: params.expectedPluginId ?? parsed.label,
requestedSpecifier: parsed.input,
source: installPolicyRequest.source,
sourcePath: repoDir
});
if (preflight?.blocked) {
const reason = preflight.blocked.code === "security_scan_failed" ? "security_scan_failed" : "security_scan_blocked";
emitPluginAuditSecurityEvent({
outcome: pluginAuditOutcomeForReason(reason),
reason,
pluginId: params.expectedPluginId,
mode: effectiveMode,
sourceFamily: "git"
});
return buildBlockedGitInstallResult({ blocked: preflight.blocked });
}
if (!params.dryRun) {
params.logger?.info?.("Installing plugin dependencies with npm…");
const install = await runCommandWithTimeout(["npm", ...createSafeNpmInstallArgs({
omitDev: true,
loglevel: "error",
noAudit: true,
noFund: true
})], {
cwd: repoDir,
timeoutMs: Math.max(params.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS, 3e5),
env: createSafeNpmInstallEnv(process.env, {
npmConfigCwd: repoDir,
packageLock: true,
quiet: true
})
});
if (install.code !== 0) return {
ok: false,
error: `npm install failed: ${install.stderr.trim() || install.stdout.trim()}`
};
}
const result = await installPluginFromInstalledPackageDir({
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
onInstallPolicyWarning: params.onInstallPolicyWarning,
config: params.config,
packageDir: repoDir,
dryRun: params.dryRun,
expectedPluginId: params.expectedPluginId,
logger: params.logger,
mode: effectiveMode,
emitSuccessSecurityEvent: false,
installPolicyRequest
});
if (!result.ok) return result;
let transaction;
if (!params.dryRun) {
const transactionRequest = resolvePluginInstallTransactionRequest(params);
const replaceResult = await replaceManagedGitRepo({
stagedRepoDir: repoDir,
persistentRepoDir,
deferCommit: transactionRequest?.deferCommit,
assertOwned: transactionRequest?.assertOwned,
onBeforePublish: async (stagedArtifactDir) => {
await params.onBeforePluginArtifactCommit?.({
pluginId: result.pluginId,
...effectiveMode === "update" ? { currentArtifactDir: persistentRepoDir } : {},
stagedArtifactDir,
mode: effectiveMode
});
},
beforePersistentApply: params.beforePersistentApply
});
if (!replaceResult.ok) return replaceResult;
transaction = replaceResult.transaction;
emitPluginInstallSecurityEvent({
pluginId: result.pluginId,
mode: effectiveMode,
sourceFamily: "git",
extensionCount: result.extensions.length,
hasVersion: Boolean(result.version),
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall
});
}
const installed = {
...result,
targetDir: params.dryRun ? result.targetDir : persistentRepoDir,
git: {
url: parsed.url,
ref: parsed.ref,
commit: normalizeOptionalString(rev.stdout),
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
}
};
return transaction ? attachPluginInstallTransaction(installed, transaction) : installed;
});
}
//#endregion
export { isImmutableGitCommitRef as n, parseGitPluginSpec as r, installPluginFromGitSpec as t };