openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
737 lines (718 loc) • 34.8 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { I as sameFileIdentity } from "./fs-safe-B6pvPGnf.js";
import { t as isValidAgentId } from "./agent-id-CeT3w4ap.js";
import { i as parseConcreteConfigPathTokens, n as formatConcreteConfigPath } from "./dot-path-BOSboevO.js";
import "./types.secrets-kC0nOetj.js";
import { t as resolveSystemBin } from "./resolve-system-bin-IIzLwiri.js";
import { d as buildEncodedPowerShellArgs, f as buildPowerShellFailureCause, u as WINDOWS_POWERSHELL_COLD_SPAWN_TIMEOUT_MS } from "./sqlite-readonly-location-BC9PgENz.js";
import "./runtime-shared-CqbaocQu.js";
import { u as resolveSecretPlanTargetByPathCore } from "./target-registry-query-qVhyD-Hq.js";
import "./resolve-224YoYfx.js";
import { n as runExec } from "./exec-BIE-3oLG.js";
import { a as inspectPathPermissions, o as safeStat } from "./permissions-BhjKuixU.js";
import "./audit-fs-BZ701GLw.js";
import fs from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { createInterface } from "node:readline/promises";
//#region src/secrets/plugin-setup-plan.ts
/** Shared plan construction for plugin-owned SecretRef setup commands. */
const SECRET_PROVIDER_ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
const MODEL_PROVIDER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
function assertValidPluginSecretProviderAlias(value) {
if (!SECRET_PROVIDER_ALIAS_PATTERN.test(value)) throw new Error(`Invalid provider alias "${value}". Use lowercase letters, numbers, underscores, or hyphens.`);
}
function assertValidPluginModelProviderId(label, value) {
if (!MODEL_PROVIDER_ID_PATTERN.test(value)) throw new Error(`Invalid ${label} model provider id: ${value}`);
}
function parsePluginSecretTargetSpecifier(productName, value) {
if (!value.startsWith("auth-profiles:")) return { path: value.startsWith("openclaw:") ? value.slice(9) : value };
const remainder = value.slice(14);
const separatorIndex = remainder.indexOf(":");
const agentId = separatorIndex >= 0 ? remainder.slice(0, separatorIndex) : "";
const targetPath = separatorIndex >= 0 ? remainder.slice(separatorIndex + 1) : "";
if (!isValidAgentId(agentId) || !targetPath) throw new Error(`Invalid --target auth-profiles target for ${productName}: ${value}`);
return {
agentId,
path: targetPath
};
}
function createPluginModelApiKeyTarget(params) {
assertValidPluginModelProviderId("target", params.providerId);
return {
type: "models.providers.apiKey",
path: `models.providers.${params.providerId}.apiKey`,
pathSegments: [
"models",
"providers",
params.providerId,
"apiKey"
],
providerId: params.providerId,
ref: {
source: "exec",
provider: params.providerAlias,
id: params.secretId
}
};
}
function createPluginConfigSecretTarget(params) {
if (params.agentId && !isValidAgentId(params.agentId)) throw new Error(`Invalid ${params.productName} setup agent id: ${params.agentId}`);
let parsedPath;
try {
parsedPath = parseConcreteConfigPathTokens(params.path);
} catch {
throw new Error(`Invalid --target config path: ${params.path}`);
}
const pathSegments = parsedPath.map(String);
const normalizedPath = formatConcreteConfigPath(parsedPath);
if (normalizedPath !== params.path) throw new Error(`Invalid --target config path: ${params.path}`);
const resolved = resolveSecretPlanTargetByPathCore({
configFile: params.agentId ? "auth-profile-store" : "openclaw.json",
pathSegments,
pathTokens: parsedPath
});
if (!resolved) throw new Error(`Unknown or unsupported ${params.productName} setup target path: ${params.path}`);
const ref = {
source: "exec",
provider: params.providerAlias,
id: params.secretId
};
return {
type: resolved.entry.targetType,
path: normalizedPath,
pathSegments,
...params.agentId ? { agentId: params.agentId } : {},
...resolved.providerId ? { providerId: resolved.providerId } : {},
...resolved.accountId ? { accountId: resolved.accountId } : {},
ref
};
}
function buildPluginSecretRefSetupPlan(params) {
assertValidPluginSecretProviderAlias(params.providerAlias);
const targets = [...params.providerSecrets.map((entry) => createPluginModelApiKeyTarget({
providerAlias: params.providerAlias,
providerId: entry.providerId,
secretId: entry.secretId
})), ...(params.configTargetSecrets ?? []).map((entry) => createPluginConfigSecretTarget({
productName: params.productName,
providerAlias: params.providerAlias,
path: entry.path,
...entry.agentId ? { agentId: entry.agentId } : {},
secretId: entry.secretId
}))];
const seen = /* @__PURE__ */ new Set();
for (const target of targets) {
const key = target.agentId ? `auth-profiles:${target.agentId}:${target.path}` : `openclaw:${target.path}`;
if (seen.has(key)) throw new Error(`Duplicate secret target path in ${params.productName} setup: ${target.path}`);
seen.add(key);
}
return {
version: 1,
protocolVersion: 1,
generatedAt: params.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
generatedBy: "manual",
providerUpserts: { [params.providerAlias]: params.providerConfig },
targets
};
}
//#endregion
//#region src/secrets/trusted-plan-path-policy.ts
const WINDOWS_TRUSTED_INSTALLER_SID = "s-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464";
const WINDOWS_SAFE_DIRECTORY_ACL_TOKENS = new Set("AD CI GE GR I IO NP OI R RA RC RD REA RX S WD X".split(" "));
const WINDOWS_SAFE_EXECUTABLE_PARENT_ACL_TOKENS = new Set([...WINDOWS_SAFE_DIRECTORY_ACL_TOKENS].filter((token) => token !== "AD" && token !== "WD"));
function isTrustedOwner$1(stat, permissions, platform = process.platform, allowWindowsTrustedInstaller = false) {
if (platform === "win32") return permissions.ownerTrusted === true || allowWindowsTrustedInstaller && permissions.ownerSid?.toLowerCase() === WINDOWS_TRUSTED_INSTALLER_SID;
if (typeof process.getuid !== "function" || stat.uid == null) return false;
const uid = process.getuid();
return stat.uid === uid || stat.uid === 0;
}
function isSafeWindowsDirectoryAclEntry(entry, allowChildCreation = true) {
if (!entry || typeof entry.rawRights !== "string") return false;
const tokens = [...entry.rawRights.matchAll(/\(([^)]+)\)/gu)].flatMap((match) => (match[1] ?? "").split(",").map((token) => token.trim().toUpperCase()).filter(Boolean));
const safeTokens = allowChildCreation ? WINDOWS_SAFE_DIRECTORY_ACL_TOKENS : WINDOWS_SAFE_EXECUTABLE_PARENT_ACL_TOKENS;
return tokens.includes("IO") || tokens.length > 0 && tokens.every((token) => safeTokens.has(token));
}
function isSafeWindowsDirectoryAclEntries(entries, allowChildCreation = true) {
return entries.every((entry) => isSafeWindowsDirectoryAclEntry(entry, allowChildCreation));
}
function isSafeWindowsDirectoryAclSummary$1(summary, allowChildCreation = true) {
if (summary === "trusted-only") return true;
if (!summary) return false;
const entries = summary.split(", ").map((value) => {
const separatorIndex = value.lastIndexOf(":");
const rawRights = separatorIndex > 0 ? value.slice(separatorIndex + 1) : "";
return /^(?:\([^)]+\))+$/u.test(rawRights) ? { rawRights } : null;
});
return entries.length > 0 && entries.every((entry) => entry !== null) && isSafeWindowsDirectoryAclEntries(entries, allowChildCreation);
}
//#endregion
//#region src/secrets/trusted-plan-path.ts
const { isSafeWindowsDirectoryAclSummary, isTrustedOwner } = {
isSafeWindowsDirectoryAclEntries,
isSafeWindowsDirectoryAclSummary: isSafeWindowsDirectoryAclSummary$1,
isTrustedOwner: isTrustedOwner$1
};
async function readShebangInterpreter(targetPath) {
const handle = await fs$1.open(targetPath, "r");
try {
const buffer = Buffer.alloc(4096);
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
if (bytesRead < 2 || buffer[0] !== 35 || buffer[1] !== 33) return;
const newline = buffer.indexOf(10, 2);
if (newline < 0) throw new Error(`script interpreter line is too long: ${targetPath}`);
const interpreter = buffer.subarray(2, newline).toString("utf8").trim().split(/\s+/u, 1)[0];
if (!interpreter || !path.isAbsolute(interpreter)) throw new Error(`script interpreter must be an absolute path: ${targetPath}`);
return interpreter;
} finally {
await handle.close();
}
}
async function assertTrustedPathChain(resolvedPath, targetType, options = {}) {
const validatedEntries = [];
let currentPath = resolvedPath;
let first = true;
for (;;) {
const before = await fs$1.lstat(currentPath);
const [stat, permissions] = await Promise.all([safeStat(currentPath), inspectPathPermissions(currentPath)]);
const after = await fs$1.lstat(currentPath);
if (!stat.ok || !permissions.ok || permissions.source === "unknown") throw new Error(`permissions could not be verified: ${currentPath}`);
if (before.isSymbolicLink() || after.isSymbolicLink() || stat.isSymlink || permissions.isSymlink || before.dev !== after.dev || before.ino !== after.ino) throw new Error(`path changed during permission verification: ${currentPath}`);
const expectedDirectory = !first || targetType === "directory";
if (stat.isDir !== expectedDirectory) throw new Error(`unexpected path type: ${currentPath}`);
const allowWindowsTrustedInstaller = !first || first && options.allowWindowsTargetTrustedInstaller === true;
if (!isTrustedOwner(stat, permissions, process.platform, allowWindowsTrustedInstaller)) throw new Error(`path is not owned by the current user or root: ${currentPath}`);
const stickyDirectory = stat.isDir && permissions.mode != null && (permissions.mode & 512) !== 0;
if ((permissions.groupWritable || permissions.worldWritable) && !stickyDirectory) {
if (!(process.platform === "win32" && stat.isDir && isSafeWindowsDirectoryAclSummary(permissions.aclSummary, targetType === "directory" || currentPath !== path.dirname(resolvedPath)))) throw new Error(`path is writable by another user: ${currentPath}`);
}
validatedEntries.push({
path: currentPath,
dev: after.dev,
ino: after.ino
});
const parentPath = path.dirname(currentPath);
if (parentPath === currentPath) break;
currentPath = parentPath;
first = false;
}
for (const entry of validatedEntries.toReversed()) {
const current = await fs$1.lstat(entry.path);
if (current.isSymbolicLink() || current.dev !== entry.dev || current.ino !== entry.ino) throw new Error(`path changed after permission verification: ${entry.path}`);
}
}
async function assertTrustedPath(targetPath, validatedScripts = /* @__PURE__ */ new Set(), options = {}) {
const resolvedPath = await fs$1.realpath(targetPath);
if (!(await fs$1.stat(resolvedPath)).isFile()) throw new Error(`path is not a regular file: ${resolvedPath}`);
await fs$1.access(resolvedPath, fs.constants.X_OK);
await assertTrustedPathChain(resolvedPath, "file", options);
if (process.platform === "win32" && path.extname(resolvedPath).toLowerCase() !== ".exe") throw new Error(`Windows executable must be an .exe file: ${resolvedPath}`);
const interpreter = await readShebangInterpreter(resolvedPath);
if (interpreter) {
if (validatedScripts.has(resolvedPath)) throw new Error(`script interpreter cycle detected: ${resolvedPath}`);
validatedScripts.add(resolvedPath);
if (path.basename(interpreter).toLowerCase() === "env") throw new Error(`script interpreter may not use env indirection: ${resolvedPath}`);
if (await assertTrustedPath(interpreter, validatedScripts) !== interpreter) throw new Error(`script interpreter path must be canonical: ${interpreter}`);
}
return resolvedPath;
}
async function resolveTrustedExecutablePath(targetPath) {
if (!path.isAbsolute(targetPath)) throw new Error(`Executable path must be absolute: ${targetPath}`);
return await assertTrustedPath(targetPath);
}
async function resolveTrustedWindowsSystemExecutablePath(targetPath) {
if (!path.isAbsolute(targetPath)) throw new Error(`Executable path must be absolute: ${targetPath}`);
return await assertTrustedPath(targetPath, /* @__PURE__ */ new Set(), { allowWindowsTargetTrustedInstaller: true });
}
async function resolveTrustedPlanDirectoryPath(targetPath) {
if (!path.isAbsolute(targetPath)) throw new Error(`Directory path must be absolute: ${targetPath}`);
const resolvedPath = await fs$1.realpath(targetPath);
await assertTrustedPathChain(resolvedPath, "directory");
return resolvedPath;
}
//#endregion
//#region src/secrets/private-plan-file.ts
const WINDOWS_PLAN_FILE_EXISTS_MARKER = "PRIVATE_PLAN_FILE_EXISTS";
const WINDOWS_PRIVATE_PLAN_FILE_NATIVE_SOURCE = `
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
public sealed class OpenClawPrivatePlanFile : IDisposable
{
[StructLayout(LayoutKind.Sequential)]
private struct SecurityAttributes
{
public int Length;
public IntPtr SecurityDescriptor;
public int InheritHandle;
}
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool ConvertStringSecurityDescriptorToSecurityDescriptorW(
string securityDescriptor,
uint revision,
out IntPtr convertedSecurityDescriptor,
out uint convertedSecurityDescriptorSize);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern SafeFileHandle CreateFileW(
string fileName,
uint desiredAccess,
uint shareMode,
ref SecurityAttributes securityAttributes,
uint creationDisposition,
uint flagsAndAttributes,
IntPtr templateFile);
[DllImport("kernel32.dll")]
private static extern IntPtr LocalFree(IntPtr memory);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool WriteFile(
SafeFileHandle file,
byte[] buffer,
uint bytesToWrite,
out uint bytesWritten,
IntPtr overlapped);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FlushFileBuffers(SafeFileHandle file);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool MoveFileExW(
string existingFileName,
string newFileName,
uint flags);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool DeleteFileW(string fileName);
[StructLayout(LayoutKind.Sequential)]
private struct FileDispositionInfo
{
[MarshalAs(UnmanagedType.Bool)]
public bool DeleteFile;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetFileInformationByHandle(
SafeFileHandle file,
int fileInformationClass,
ref FileDispositionInfo fileInformation,
uint bufferSize);
private readonly string stagingPath;
private readonly string finalPath;
private SafeFileHandle handle;
private OpenClawPrivatePlanFile(
string stagingPath,
string finalPath,
SafeFileHandle handle)
{
this.stagingPath = stagingPath;
this.finalPath = finalPath;
this.handle = handle;
}
private static bool SetDeleteOnClose(SafeFileHandle handle, bool enabled)
{
var disposition = new FileDispositionInfo { DeleteFile = enabled };
return SetFileInformationByHandle(
handle,
4,
ref disposition,
(uint)Marshal.SizeOf(typeof(FileDispositionInfo)));
}
public static OpenClawPrivatePlanFile Open(
string stagingPath,
string finalPath,
string securityDescriptor,
out int errorCode)
{
errorCode = 0;
IntPtr descriptor;
uint descriptorSize;
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
securityDescriptor,
1,
out descriptor,
out descriptorSize))
{
errorCode = Marshal.GetLastWin32Error();
return null;
}
try
{
var attributes = new SecurityAttributes
{
Length = Marshal.SizeOf(typeof(SecurityAttributes)),
SecurityDescriptor = descriptor,
InheritHandle = 0,
};
var handle = CreateFileW(stagingPath, 0x40010000, 0, ref attributes, 1, 0x80, IntPtr.Zero);
if (handle.IsInvalid)
{
errorCode = Marshal.GetLastWin32Error();
handle.Dispose();
return null;
}
return new OpenClawPrivatePlanFile(stagingPath, finalPath, handle);
}
finally
{
LocalFree(descriptor);
}
}
public int ArmDeleteOnClose()
{
if (handle == null || handle.IsInvalid || handle.IsClosed)
{
return 6;
}
if (SetDeleteOnClose(handle, true))
{
return 0;
}
var dispositionError = Marshal.GetLastWin32Error();
handle.Dispose();
handle = null;
DeleteFileW(stagingPath);
return dispositionError == 0 ? 29 : dispositionError;
}
public int WriteAndPublish(byte[] content)
{
if (handle == null || handle.IsInvalid || handle.IsClosed)
{
return 6;
}
uint written;
if (content.Length > 0 &&
(!WriteFile(handle, content, (uint)content.Length, out written, IntPtr.Zero) ||
written != (uint)content.Length))
{
var writeError = Marshal.GetLastWin32Error();
return writeError == 0 ? 29 : writeError;
}
if (!FlushFileBuffers(handle))
{
var flushError = Marshal.GetLastWin32Error();
return flushError == 0 ? 29 : flushError;
}
if (!SetDeleteOnClose(handle, false))
{
var dispositionError = Marshal.GetLastWin32Error();
return dispositionError == 0 ? 29 : dispositionError;
}
handle.Dispose();
handle = null;
if (MoveFileExW(stagingPath, finalPath, 0x8))
{
return 0;
}
var moveError = Marshal.GetLastWin32Error();
DeleteFileW(stagingPath);
return moveError == 0 ? 29 : moveError;
}
public void Dispose()
{
if (handle != null)
{
var openHandle = handle;
handle = null;
var deletePending = SetDeleteOnClose(openHandle, true);
openHandle.Dispose();
if (!deletePending)
{
DeleteFileW(stagingPath);
}
}
}
}
`;
function readWindowsEnv(env, name) {
const lower = name.toLowerCase();
return Object.entries(env).find(([key]) => key.toLowerCase() === lower)?.[1];
}
async function resolvePrivateWindowsCompilerTempDir(env) {
const candidate = readWindowsEnv(env, "TEMP") ?? readWindowsEnv(env, "TMP");
if (!candidate || !path.win32.isAbsolute(candidate)) throw new Error("Unable to resolve an absolute Windows temp directory for private plan creation.");
return await resolveTrustedPlanDirectoryPath(candidate);
}
async function resolveTrustedPowerShell(targetPath) {
const powershell = resolveSystemBin("powershell");
if (!powershell || powershell.toLowerCase() !== targetPath.toLowerCase()) throw new Error("Unable to resolve trusted Windows PowerShell for private plan creation.");
return await resolveTrustedWindowsSystemExecutablePath(targetPath);
}
async function createPrivateWindowsPlanFile(filePath, content, env = process.env, dependencies = {}) {
const resolveTrustedExecutable = dependencies.resolveTrustedExecutable ?? resolveTrustedPowerShell;
const resolveCompilerTempDir = dependencies.resolveCompilerTempDir ?? resolvePrivateWindowsCompilerTempDir;
const run = dependencies.run ?? runExec;
const systemRoot = readWindowsEnv(env, "SYSTEMROOT") ?? readWindowsEnv(env, "WINDIR") ?? "C:\\Windows";
if (!path.win32.isAbsolute(systemRoot)) throw new Error("Unable to resolve the Windows system directory for private plan creation.");
const resolvedPath = path.resolve(filePath);
const stagingPath = path.join(path.dirname(resolvedPath), `.openclaw-plan-${randomUUID()}.tmp`);
const command = [
"$ErrorActionPreference = 'Stop'",
"$payloadJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String([Console]::In.ReadToEnd()))",
"$payload = $payloadJson | ConvertFrom-Json",
"Add-Type -TypeDefinition $payload.nativeSource -Language CSharp",
"$finalPath = $payload.finalPath",
"$stagingPath = $payload.stagingPath",
"$current = [System.Security.Principal.WindowsIdentity]::GetCurrent().User",
"$security = New-Object System.Security.AccessControl.FileSecurity",
"$security.SetAccessRuleProtection($true, $false)",
"$security.SetOwner($current)",
"$expected = @($current.Value, 'S-1-5-18') | Sort-Object -Unique",
"foreach ($sidValue in $expected) { $sid = New-Object System.Security.Principal.SecurityIdentifier($sidValue); $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($sid, [System.Security.AccessControl.FileSystemRights]::FullControl, [System.Security.AccessControl.AccessControlType]::Allow); [void]$security.AddAccessRule($rule) }",
"$sections = [System.Security.AccessControl.AccessControlSections]::Owner -bor [System.Security.AccessControl.AccessControlSections]::Access",
"$sddl = $security.GetSecurityDescriptorSddlForm($sections)",
"$content = [Convert]::FromBase64String($payload.content)",
"$openError = 0",
"$native = [OpenClawPrivatePlanFile]::Open($stagingPath, $finalPath, $sddl, [ref]$openError)",
"$errorCode = $openError",
"if ($null -ne $native) { try { $actual = Get-Acl -LiteralPath $stagingPath; $rules = @($actual.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])); if (!$actual.AreAccessRulesProtected -or $rules.Count -ne $expected.Count) { throw 'private plan ACL verification failed' }; foreach ($rule in $rules) { if ($rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or $expected -notcontains $rule.IdentityReference.Value -or ($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl) { throw 'private plan ACL verification failed' } }; $errorCode = $native.ArmDeleteOnClose(); if ($errorCode -eq 0) { $errorCode = $native.WriteAndPublish($content) } } finally { $native.Dispose() } }",
`if ($errorCode -eq 80 -or $errorCode -eq 183) { throw '${WINDOWS_PLAN_FILE_EXISTS_MARKER}' }`,
"if ($errorCode -ne 0) { $exception = New-Object System.ComponentModel.Win32Exception($errorCode); throw $exception }"
].join("; ");
const powershell = await resolveTrustedExecutable(path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"));
const compilerTempDir = await resolveCompilerTempDir(env);
const input = Buffer.from(JSON.stringify({
content: Buffer.from(content, "utf8").toString("base64"),
finalPath: path.toNamespacedPath(resolvedPath),
nativeSource: WINDOWS_PRIVATE_PLAN_FILE_NATIVE_SOURCE,
stagingPath: path.toNamespacedPath(stagingPath)
}), "utf8").toString("base64");
try {
await run(powershell, buildEncodedPowerShellArgs(command), {
baseEnv: {},
env: {
SYSTEMROOT: systemRoot,
TEMP: compilerTempDir,
TMP: compilerTempDir,
WINDIR: systemRoot
},
input,
logOutput: false,
maxBuffer: 65536,
timeoutMs: WINDOWS_POWERSHELL_COLD_SPAWN_TIMEOUT_MS
});
} catch (error) {
if (String(error).includes(WINDOWS_PLAN_FILE_EXISTS_MARKER)) {
const existsError = /* @__PURE__ */ new Error(`Private plan file already exists: ${filePath}`);
existsError.code = "EEXIST";
throw existsError;
}
throw new Error(`Unable to create private Windows plan file: ${filePath}`, { cause: buildPowerShellFailureCause(error) });
}
}
//#endregion
//#region src/plugin-sdk/secret-ref-runtime.ts
function throwPlanFileError(error, planPath) {
if (error?.code === "EEXIST") throw new Error(`Plan path already exists; choose a new --plan-out path: ${planPath}`, { cause: error });
throw error;
}
async function writeSecretPlanFile(params) {
if ((params.platform ?? process.platform) === "win32") {
await (params.createPrivateWindowsFile ?? createPrivateWindowsPlanFile)(params.planPath, params.content).catch((error) => throwPlanFileError(error, params.planPath));
return;
}
let handle;
let identity;
try {
handle = await fs$1.open(params.planPath, "wx", 384);
identity = await handle.stat({ bigint: true });
await handle.chmod(384);
if (((await handle.stat()).mode & 511) !== 384) throw new Error("Unable to verify owner-only permissions for the generated plan file.");
const pathStat = await fs$1.lstat(params.planPath, { bigint: true });
const handleStat = await handle.stat({ bigint: true });
if (pathStat.isSymbolicLink() || !sameFileIdentity(identity, handleStat) || !sameFileIdentity(identity, pathStat)) throw new Error("Generated plan path changed during permission setup.");
await handle.writeFile(params.content, "utf8");
await handle.sync();
} catch (error) {
await handle?.close().catch(() => void 0);
if (error?.code === "EEXIST") throwPlanFileError(error, params.planPath);
if (identity) try {
const current = await fs$1.lstat(params.planPath, { bigint: true });
if (!current.isSymbolicLink() && sameFileIdentity(current, identity)) await fs$1.rm(params.planPath, { force: true });
} catch {}
throw error;
} finally {
await handle?.close().catch(() => void 0);
}
}
function quoteSecretRefCliArg(value, shell) {
if (/\r|\n/u.test(value)) throw new Error("Command argument cannot contain CR or LF");
if (shell === "cmd") {
if (/[%!]/u.test(value)) throw new Error("Interactive Command Prompt cannot safely quote paths containing % or !");
const escaped = value.replaceAll("\"", "\\\"");
return /[ \t"&|<>^()]/u.test(value) ? `"${escaped}"` : escaped || "\"\"";
}
if (shell === "powershell") return `'${value.replaceAll("'", "''")}'`;
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) return value;
return `'${value.replaceAll("'", "'\\''")}'`;
}
function renderSecretRefApplyCommands(planPath, platform = process.platform) {
const render = (shell, indent = "") => {
const quotedPlanPath = quoteSecretRefCliArg(planPath, shell);
return [`${indent}openclaw secrets apply --from ${quotedPlanPath} --dry-run --allow-exec`, `${indent}openclaw secrets apply --from ${quotedPlanPath} --allow-exec`];
};
if (platform !== "win32") return render("posix");
const powershellCommands = ["PowerShell:", ...render("powershell", " ")];
if (/[%!]/u.test(planPath)) return [...powershellCommands, "Command Prompt: unavailable for paths containing % or !; use PowerShell."];
return [
...powershellCommands,
"Command Prompt:",
...render("cmd", " ")
];
}
function readSecretRefProviderStatus(config, providerAlias) {
const provider = config.secrets?.providers?.[providerAlias];
if (!isRecord(provider)) return { configured: false };
const base = {
configured: true,
source: normalizeOptionalString(provider.source)
};
if (provider.source !== "exec") return base;
if ("pluginIntegration" in provider) return {
...base,
pluginIntegration: provider.pluginIntegration
};
return {
...base,
command: normalizeOptionalString(provider.command)
};
}
function writeSecretRefCliLine(message = "") {
process.stdout.write(`${message}\n`);
}
/** Build the canonical setup/status adapter shared by plugin-owned SecretRef CLIs. */
function createPluginSecretRefSetupCli(params) {
const isIntegrationProvider = (value) => isRecord(value) && value.source === "exec" && isRecord(value.pluginIntegration) && value.pluginIntegration.pluginId === params.pluginIntegration.pluginId && value.pluginIntegration.integrationId === params.pluginIntegration.integrationId;
const inspectProvider = (config, requestedAlias) => {
const explicitAlias = normalizeOptionalString(requestedAlias);
let providerAlias;
if (explicitAlias) {
assertValidPluginSecretProviderAlias(explicitAlias);
providerAlias = explicitAlias;
} else {
const configuredAliases = Object.entries(config.secrets?.providers ?? {}).filter(([, provider]) => isIntegrationProvider(provider)).map(([alias]) => alias).toSorted();
if (configuredAliases.length > 1) throw new Error(`Multiple ${params.productName} provider aliases are configured (${configuredAliases.join(", ")}). Use --provider-alias <alias>.`);
providerAlias = configuredAliases[0] ?? params.defaultProviderAlias;
}
return {
providerAlias,
provider: readSecretRefProviderStatus(config, providerAlias),
providerReady: isIntegrationProvider(config.secrets?.providers?.[providerAlias])
};
};
const parseProviderKeyMappings = (values) => (values ?? []).map((value) => {
const separator = value.indexOf("=");
if (separator <= 0 || separator === value.length - 1) throw new Error(`Invalid --provider-key value "${value}". Use <model-provider-id>=<${params.secretIdPlaceholder}>.`);
const providerId = value.slice(0, separator).trim();
assertValidPluginModelProviderId("--provider-key", providerId);
return {
providerId,
secretId: params.normalizeSecretId(`--provider-key ${providerId}`, value.slice(separator + 1).trim())
};
});
const parseConfigTargetMappings = (values) => (values ?? []).map((value) => {
const separator = value.indexOf("=");
if (separator <= 0 || separator === value.length - 1) throw new Error(`Invalid --target value "${value}". Use <openclaw-config-path>=<${params.secretIdPlaceholder}>.`);
const target = parsePluginSecretTargetSpecifier(params.productName, value.slice(0, separator).trim());
const secretId = params.normalizeSecretId(`--target ${target.path}`, value.slice(separator + 1).trim());
return Object.assign({
path: target.path,
secretId
}, target.agentId ? { agentId: target.agentId } : {});
});
const promptOptionalSecretId = async (label) => {
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
const readline = createInterface({
input: process.stdin,
output: process.stdout
});
try {
return normalizeOptionalString(await readline.question(`${label} ${params.secretIdLabel} (blank to skip): `));
} finally {
readline.close();
}
};
const collectProviderSecrets = async (options) => {
const commonProviders = [
{
providerId: "openai",
label: "OpenAI",
value: options.openaiId
},
{
providerId: "anthropic",
label: "Anthropic",
value: options.anthropicId
},
{
providerId: "openrouter",
label: "OpenRouter",
value: options.openrouterId
}
];
const providerSecrets = [];
for (const provider of commonProviders) {
const value = normalizeOptionalString(provider.value) ?? await promptOptionalSecretId(provider.label);
if (value) providerSecrets.push({
providerId: provider.providerId,
secretId: params.normalizeSecretId(provider.label, value)
});
}
providerSecrets.push(...parseProviderKeyMappings(options.providerKey));
const seen = /* @__PURE__ */ new Set();
for (const entry of providerSecrets) {
const normalized = entry.providerId.toLowerCase();
if (seen.has(normalized)) throw new Error(`Duplicate model provider id in ${params.productName} setup: ${entry.providerId}`);
seen.add(normalized);
}
return providerSecrets;
};
const runSetup = async (options) => {
const providerAlias = normalizeOptionalString(options.providerAlias) ?? params.defaultProviderAlias;
assertValidPluginSecretProviderAlias(providerAlias);
const providerConfig = {
source: "exec",
pluginIntegration: params.pluginIntegration
};
const plan = buildPluginSecretRefSetupPlan({
productName: params.productName,
providerAlias,
providerConfig,
providerSecrets: await collectProviderSecrets(options),
configTargetSecrets: parseConfigTargetMappings(options.target)
});
if (plan.targets.length === 0) throw new Error("No SecretRef targets selected. Pass --openai-id, --anthropic-id, --openrouter-id, --provider-key, or --target.");
const requestedPlanPath = normalizeOptionalString(options.planOut) ?? params.defaultPlanPath();
const absolutePlanPath = path.resolve(requestedPlanPath);
const planDirectory = await resolveTrustedPlanDirectoryPath(path.dirname(absolutePlanPath));
const planPath = path.join(planDirectory, path.basename(absolutePlanPath));
const applyCommands = renderSecretRefApplyCommands(planPath);
await writeSecretPlanFile({
planPath,
content: `${JSON.stringify(plan, null, 2)}\n`
});
writeSecretRefCliLine(`Plan written to ${planPath}`);
writeSecretRefCliLine(`Targets: ${plan.targets.length}`);
writeSecretRefCliLine();
writeSecretRefCliLine("Next steps:");
for (const command of params.beforeApplyCommands ?? []) writeSecretRefCliLine(` ${command}`);
for (const command of applyCommands) writeSecretRefCliLine(` ${command}`);
writeSecretRefCliLine(" openclaw secrets audit --check --allow-exec");
writeSecretRefCliLine(" openclaw secrets reload");
};
const registerSetupCommand = (command) => {
command.command("setup").description(`Create a ${params.productName} SecretRef setup plan`).option("--plan-out <path>", "Write the generated secrets apply plan to a path").option("--provider-alias <alias>", "Secret provider alias to configure", params.defaultProviderAlias).option("--openai-id <id>", `${params.secretIdLabel} for models.providers.openai.apiKey`).option("--anthropic-id <id>", `${params.secretIdLabel} for models.providers.anthropic.apiKey`).option("--openrouter-id <id>", `${params.secretIdLabel} for models.providers.openrouter.apiKey`).option("--provider-key <provider=id>", `${params.secretIdLabel} for any models.providers.<provider>.apiKey target`, (value, previous = []) => [...previous, value], []).option("--target <path=id>", `${params.secretIdLabel} for any known SecretRef target path`, (value, previous = []) => [...previous, value], []).action((options) => runSetup(options));
};
return {
inspectProvider,
registerSetupCommand
};
}
/** Shared validation and apply-plan construction for plugin-owned SecretRef setup CLIs. */
const pluginSecretRefSetup = {
assertValidModelProviderId: assertValidPluginModelProviderId,
assertValidProviderAlias: assertValidPluginSecretProviderAlias,
buildPlan: buildPluginSecretRefSetupPlan,
parseTargetSpecifier: parsePluginSecretTargetSpecifier,
resolveTrustedDirectoryPath: resolveTrustedPlanDirectoryPath,
resolveTrustedExecutablePath,
writePlanFile: writeSecretPlanFile
};
//#endregion
export { pluginSecretRefSetup as n, createPluginSecretRefSetupCli as t };