openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
393 lines (392 loc) • 23.4 kB
JavaScript
import { r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { t as exitCliAfterOutput } from "./one-shot-exit-BkFPnjbM.js";
import { t as createLazyImportLoader } from "./lazy-promise-DGqyc4Y4.js";
import { t as hasErrnoCode } from "./errno-CkbDOfLk.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { n as ENV_SECRET_REF_ID_RE } from "./types.secrets-kC0nOetj.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { t as formatDocsLink } from "./links-ClIwBcy4.js";
import { r as theme } from "./theme-vjDs9tao.js";
import { n as callGatewayFromCli, t as addGatewayClientOptions } from "./gateway-rpc-C9fDQ0vT.js";
import { t as danger } from "./globals-CTaGxEqj.js";
import { t as formatGatewayCommandFailure } from "./error-format-Cae7EwnT.js";
import { c as rethrowExpectedCliError, r as formatCliJsonFailure } from "./failure-output-Dc5ce9_F.js";
import { n as isSensitiveEnvName } from "./secret-env-name-gnYTrC3J.js";
//#region src/cli/secrets-cli-output.ts
/** Keep each secrets command's payload, failure envelope, and exit code in one path. */
async function runSecretsCommand(json, run, renderHumanFailure, failureExit, jsonResult) {
try {
const result = await run();
if (json) defaultRuntime.writeJson(jsonResult ? jsonResult(result) : result);
return result;
} catch (error) {
const failure = typeof failureExit === "function" ? failureExit(error) : {
error,
exitCode: failureExit
};
if (json) defaultRuntime.writeJson(formatCliJsonFailure(failure.error));
else renderHumanFailure(failure.error);
return exitCliAfterOutput(defaultRuntime, failure.exitCode);
}
}
//#endregion
//#region src/cli/secrets-store-cli.ts
var SecretStoreCliFailure = class extends Error {
constructor(exitCode, message) {
super(message);
this.exitCode = exitCode;
this.name = "SecretStoreCliFailure";
}
};
function teamScope(scope) {
if (!scope || scope === "team") return { kind: "team" };
if (scope === "me") throw new SecretStoreCliFailure(2, "Identity scope is not supported yet; use --scope team.");
throw new SecretStoreCliFailure(2, `Invalid scope "${scope}"; only "team" is supported.`);
}
function storeKind(kind, name) {
if (!kind) return isSensitiveEnvName(name) ? "secret" : "env";
if (kind === "secret" || kind === "env") return kind;
throw new SecretStoreCliFailure(2, `Invalid kind "${kind}"; use "secret" or "env".`);
}
function assertStoreName(name) {
if (!ENV_SECRET_REF_ID_RE.test(name)) throw new SecretStoreCliFailure(2, `Name must match ${String(ENV_SECRET_REF_ID_RE)}.`);
}
function assertOutputMode(options) {
if (options.json && options.plain) throw new SecretStoreCliFailure(2, "Choose either --json or --plain, not both.");
}
function mapStoreError(error) {
if (error instanceof SecretStoreCliFailure) return error;
const validation = error;
if (validation?.name === "SecretStoreValidationError" && (validation.code === "SECRET_STORE_INVALID_NAME" || validation.code === "SECRET_STORE_VALUE_TOO_LARGE" || validation.code === "SECRET_STORE_VALUE_EMPTY" || validation.code === "SECRET_STORE_INVALID_ALLOWED_HOST")) return new SecretStoreCliFailure(2, validation.message ?? "Invalid secret store input.");
return new SecretStoreCliFailure(1, formatErrorMessage(error));
}
async function runStoreAction(action, json, renderHumanSuccess) {
const result = await runSecretsCommand(json, action, (error) => defaultRuntime.error(danger(formatErrorMessage(error))), (error) => {
const failure = mapStoreError(error);
return {
error: failure,
exitCode: failure.exitCode
};
});
if (!json) renderHumanSuccess?.(result);
}
function renderList(entries, options) {
if (options.plain) {
for (const entry of entries) defaultRuntime.writeStdout([
entry.name,
entry.kind,
entry.kind === "env" ? entry.valuePreview ?? "" : "",
entry.kind === "secret" ? (entry.allowedHosts ?? []).join(",") : ""
].join(" "));
return;
}
if (entries.length === 0) {
defaultRuntime.log("No team secret store entries.");
return;
}
for (const entry of entries) {
const value = entry.kind === "env" ? ` = ${entry.valuePreview ?? ""}` : " (write-only)";
const hosts = entry.kind === "secret" ? `; allowed hosts: ${(entry.allowedHosts ?? []).join(", ") || "none"}` : "";
defaultRuntime.log(`${entry.name} [${entry.kind}]${value}${hosts}`);
}
}
async function noteGatewayReload() {
try {
const { readActiveGatewayLockIdentity } = await import("./gateway-lock-RSBoYtTh.js");
if (await readActiveGatewayLockIdentity()) defaultRuntime.log("A gateway is running. Run `openclaw secrets reload` for config-referenced values to take effect.");
} catch {}
}
async function confirmMutation(message, yes) {
if (yes) return;
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new SecretStoreCliFailure(2, `${message} Re-run with --yes in non-interactive mode.`);
const { confirm, isCancel } = await import("@clack/prompts");
const approved = await confirm({
message,
initialValue: false
});
if (isCancel(approved) || !approved) throw new SecretStoreCliFailure(2, "Operation cancelled.");
}
function registerSecretStoreCli(secrets) {
const store = secrets.command("store").description("Manage the team-scoped SQLite secret and environment store").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/secrets", "docs.openclaw.ai/cli/secrets")}\n`);
store.command("list").description("List stored names and non-secret metadata").option("--scope <team>", "Store scope", "team").option("--json", "Output JSON", false).option("--plain", "Output tab-separated rows", false).action((options) => runStoreAction(async () => {
assertOutputMode(options);
const scope = teamScope(options.scope);
const { listSecretStoreEntries } = await import("./secret-store-DU4PlAuJ.js");
return listSecretStoreEntries({ scope });
}, options.json, (entries) => renderList(entries, options)));
store.command("set <NAME>").description("Create or update one store entry").option("--value <value>", "Literal value (env kind only)").option("--value-file <path>", "Read value from a file; use - for stdin").option("--kind <secret|env>", "Entry kind (defaults from NAME)").option("--allow-host <host>", "Allow substitution only for this exact host (repeatable)", (host, hosts) => [...hosts, host], []).option("--clear-allowed-hosts", "Remove all allowed hosts", false).option("--scope <team>", "Store scope", "team").option("--dry-run", "Validate without writing", false).action((name, options) => runStoreAction(async () => {
assertStoreName(name);
const scope = teamScope(options.scope);
const storeModule = await import("./secret-store-DU4PlAuJ.js");
const requestedHosts = options.allowHost ?? [];
const existingEntry = requestedHosts.length > 0 || options.clearAllowedHosts === true ? storeModule.listSecretStoreEntries({ scope }).find((entry) => entry.name === name) : void 0;
const kind = options.kind ? storeKind(options.kind, name) : existingEntry?.kind ?? storeKind(void 0, name);
if (requestedHosts.length > 0 && options.clearAllowedHosts) throw new SecretStoreCliFailure(2, "Use either --allow-host or --clear-allowed-hosts, not both.");
if (kind === "env" && (requestedHosts.length > 0 || options.clearAllowedHosts)) throw new SecretStoreCliFailure(2, "Allowed hosts apply only to secret entries.");
const allowedHosts = requestedHosts.length > 0 ? storeModule.normalizeSecretAllowedHosts(requestedHosts) : options.clearAllowedHosts ? [] : void 0;
if (options.value !== void 0 && options.valueFile !== void 0) throw new SecretStoreCliFailure(2, "Use only one of --value or --value-file.");
if (kind === "secret" && options.value !== void 0) throw new SecretStoreCliFailure(2, "--value is refused for secret entries. Use a stdin pipe, --value-file, or the interactive no-echo prompt.");
if (allowedHosts !== void 0 && options.value === void 0 && options.valueFile === void 0 && existingEntry?.kind === "secret") {
if (options.dryRun) {
defaultRuntime.log(`Would update allowed hosts for ${name}.`);
return;
}
storeModule.updateSecretStoreAllowedHosts({
scope,
name,
allowedHosts,
updatedBy: "cli"
});
defaultRuntime.log(allowedHosts.length > 0 ? `Allowed ${name} for ${allowedHosts.join(", ")}.` : `Cleared allowed hosts for ${name}.`);
return;
}
const value = options.value !== void 0 ? options.value : await (await import("./secrets-store-input-CE_8mi2u.js")).readSecretStoreInput({ valueFile: options.valueFile });
storeModule.assertSecretStoreValue(value, kind);
if (options.dryRun) {
defaultRuntime.log(`Would ${kind === "secret" ? "write" : "set"} ${name} (${kind}).`);
return;
}
storeModule.writeSecretStoreEntry({
scope,
name,
value,
kind,
...allowedHosts !== void 0 ? { allowedHosts } : {},
updatedBy: "cli"
});
storeModule.purgeExpiredSecretStoreEntries();
defaultRuntime.log(`Stored ${name} (${kind}).`);
await noteGatewayReload();
}));
store.command("get <NAME>").description("Read an env-kind value; secret-kind values are write-only").option("--scope <team>", "Store scope", "team").option("--json", "Output JSON", false).option("--plain", "Output only the env value", false).action((name, options) => runStoreAction(async () => {
assertOutputMode(options);
assertStoreName(name);
const scope = teamScope(options.scope);
const { listSecretStoreEntries, readSecretStoreValue } = await import("./secret-store-DU4PlAuJ.js");
const metadata = listSecretStoreEntries({ scope }).find((entry) => entry.name === name);
if (!metadata) throw new SecretStoreCliFailure(3, `Secret store entry "${name}" was not found.`);
if (metadata.kind === "secret") throw new SecretStoreCliFailure(2, `Secret store entry "${name}" is write-only by design. Reference it from config with a store SecretRef.`);
const result = readSecretStoreValue({
scope,
name
});
if (!result.ok) throw new SecretStoreCliFailure(result.error.code === "SECRET_STORE_NOT_FOUND" ? 3 : 1, result.error.message);
return {
name,
kind: metadata.kind,
value: result.value
};
}, options.json, (result) => {
if (options.plain) defaultRuntime.writeStdout(result.value);
else defaultRuntime.log(`${name}=${result.value}`);
}));
store.command("rm <NAME...>").description("Soft-delete one or more entries").option("--scope <team>", "Store scope", "team").option("--dry-run", "Show what would be removed", false).option("--yes", "Skip confirmation", false).action((names, options) => runStoreAction(async () => {
const scope = teamScope(options.scope);
for (const name of names) assertStoreName(name);
if (options.dryRun) {
defaultRuntime.log(`Would remove ${names.length} team store entr${names.length === 1 ? "y" : "ies"}.`);
return;
}
await confirmMutation(`Remove ${names.length} team store entr${names.length === 1 ? "y" : "ies"}?`, options.yes);
const { deleteSecretStoreEntry, purgeExpiredSecretStoreEntries } = await import("./secret-store-DU4PlAuJ.js");
for (const name of names) deleteSecretStoreEntry({
scope,
name
});
purgeExpiredSecretStoreEntries();
defaultRuntime.log(`Removed ${names.length} team store entr${names.length === 1 ? "y" : "ies"}.`);
await noteGatewayReload();
}));
store.command("import").description("Import dotenv-formatted entries from a file or stdin").option("--from <file>", "Dotenv file; use - or omit for stdin").option("--kind <secret|env>", "Override the detected kind for all entries").option("--scope <team>", "Store scope", "team").option("--dry-run", "Validate without writing", false).option("--yes", "Skip confirmation", false).action((options) => runStoreAction(async () => {
const scope = teamScope(options.scope);
if (!options.from && process.stdin.isTTY) throw new SecretStoreCliFailure(2, "Import requires --from <file> or piped stdin.");
const values = await (await import("./secrets-store-input-CE_8mi2u.js")).readSecretStoreImport(options.from);
const entries = Object.entries(values);
if (entries.length === 0) throw new SecretStoreCliFailure(2, "Import input contains no dotenv assignments.");
const normalized = entries.map(([name, value]) => {
assertStoreName(name);
return {
name,
value,
kind: storeKind(options.kind, name)
};
});
const storeModule = await import("./secret-store-DU4PlAuJ.js");
for (const entry of normalized) storeModule.assertSecretStoreValue(entry.value, entry.kind);
if (options.dryRun) {
defaultRuntime.log(`Would import ${normalized.length} team store entries.`);
return;
}
await confirmMutation(`Import ${normalized.length} team store entries?`, options.yes);
for (const entry of normalized) storeModule.writeSecretStoreEntry({
scope,
...entry,
updatedBy: "cli"
});
storeModule.purgeExpiredSecretStoreEntries();
defaultRuntime.log(`Imported ${normalized.length} team store entries.`);
await noteGatewayReload();
}));
}
//#endregion
//#region src/cli/secrets-cli.ts
const fsModuleLoader = createLazyImportLoader(() => import("node:fs"));
const clackPromptsLoader = createLazyImportLoader(() => import("@clack/prompts"));
const secretsApplyLoader = createLazyImportLoader(() => import("./apply-B9B05y0S.js"));
var SecretsPlanFileNotFoundError = class extends Error {};
const SECRETS_PLAN_MAX_BYTES = 16777216;
function serializePlanFile(plan, pathname) {
const raw = `${JSON.stringify(plan, null, 2)}\n`;
if (Buffer.byteLength(raw, "utf8") > SECRETS_PLAN_MAX_BYTES) throw new RangeError(`Secrets plan exceeds ${SECRETS_PLAN_MAX_BYTES} bytes and cannot be written: ${pathname}`);
return raw;
}
async function readPlanFile(pathname) {
const [fsModule, { readFileDescriptorBounded }, { isSecretsApplyPlan }] = await Promise.all([
fsModuleLoader.load(),
import("./infra/boundary-file-read.js"),
import("./plan-DNnaVdR9.js")
]);
const fsConstants = fsModule.constants;
const openFlags = fsConstants.O_RDONLY | (fsConstants.O_NONBLOCK ?? 0);
const file = await fsModule.promises.open(pathname, openFlags).catch((err) => {
if (hasErrnoCode(err, "ENOENT")) throw new SecretsPlanFileNotFoundError(`Secrets plan file not found: ${pathname}`, { cause: err });
throw err;
});
let raw;
try {
const stat = await file.stat();
if (!stat.isFile()) throw new Error(`Secrets plan path is not a regular file: ${pathname}`);
if (stat.size > SECRETS_PLAN_MAX_BYTES) throw new RangeError(`Secrets plan file exceeds ${SECRETS_PLAN_MAX_BYTES} bytes: ${pathname}`);
raw = (await readFileDescriptorBounded(file.fd, SECRETS_PLAN_MAX_BYTES)).toString("utf8");
} finally {
await file.close();
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch (err) {
throw new Error(`Malformed JSON in secrets plan file: ${pathname}`, { cause: err });
}
if (!isSecretsApplyPlan(parsed)) throw new Error(`Invalid secrets plan file: ${pathname}. Generate a fresh plan with ${formatCliCommand("openclaw secrets configure --plan-out <path>")}.`);
return parsed;
}
function registerSecretsCli(program) {
const secrets = program.command("secrets").description("Secrets runtime controls").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/gateway/security", "docs.openclaw.ai/gateway/security")}\n`);
registerSecretStoreCli(secrets);
addGatewayClientOptions(secrets.command("reload").description("Re-resolve secret references and atomically swap runtime snapshot").option("--json", "Output JSON", false)).action(async (opts) => {
const result = await runSecretsCommand(opts.json, () => callGatewayFromCli("secrets.reload", opts, void 0, { expectFinal: false }), (err) => {
rethrowExpectedCliError(err);
defaultRuntime.error(danger(formatGatewayCommandFailure({
action: "reload secrets",
error: err,
inspectCommand: "openclaw gateway status --deep"
})));
}, 1);
if (opts.json) return;
const warningCount = Number(result?.warningCount ?? 0);
defaultRuntime.log(Number.isFinite(warningCount) && warningCount > 0 ? `Secrets reloaded with ${warningCount} warning(s).` : "Secrets reloaded.");
});
secrets.command("audit").description("Audit plaintext secrets, unresolved refs, and precedence drift").option("--check", "Exit non-zero when findings are present", false).option("--allow-exec", "Allow exec SecretRef resolution during audit (may execute provider commands)", false).option("--json", "Output JSON", false).action(async (opts) => {
const { report, exitCode } = await runSecretsCommand(opts.json, async () => {
const { resolveSecretsAuditExitCode, runSecretsAudit } = await import("./audit-CIA4Hgxk.js");
const auditReport = await runSecretsAudit({ allowExec: Boolean(opts.allowExec) });
return {
report: auditReport,
exitCode: resolveSecretsAuditExitCode(auditReport, Boolean(opts.check))
};
}, (err) => {
defaultRuntime.error(danger(`Secrets audit failed: ${formatErrorMessage(err)}. Run ${formatCliCommand("openclaw doctor")} to inspect config and credential state.`));
}, 2, (outcome) => outcome.report);
if (!opts.json) {
defaultRuntime.log(`Secrets audit: ${report.status}. plaintext=${report.summary.plaintextCount}, unresolved=${report.summary.unresolvedRefCount}, shadowed=${report.summary.shadowedRefCount}, storeResidue=${report.summary.storeResidueCount}, legacy=${report.summary.legacyResidueCount}.`);
for (const finding of report.findings.slice(0, 20)) defaultRuntime.log(`- [${finding.code}] ${finding.file}:${finding.jsonPath} ${finding.message}`);
if (report.findings.length > 20) defaultRuntime.log(`... ${report.findings.length - 20} more finding(s).`);
if (report.resolution.skippedExecRefs > 0) defaultRuntime.log(`Audit note: skipped ${report.resolution.skippedExecRefs} exec SecretRef resolvability check(s). Re-run with --allow-exec to execute exec providers during audit.`);
}
if (exitCode !== 0) exitCliAfterOutput(defaultRuntime, exitCode);
});
secrets.command("configure").description("Interactive secrets helper (provider setup + SecretRef mapping + preflight)").option("--apply", "Apply changes immediately after preflight", false).option("--yes", "Skip apply confirmation prompt", false).option("--providers-only", "Configure secrets.providers only, skip credential mapping", false).option("--skip-provider-setup", "Skip provider setup and only map credential fields to existing providers", false).option("--agent <id>", "Agent id for auth-profiles targets (default: configured default agent)").option("--allow-exec", "Allow exec SecretRef preflight checks (may execute provider commands)", false).option("--plan-out <path>", "Write generated plan JSON to a file (max 16 MiB)").option("--json", "Output JSON", false).action(async (opts) => {
await runSecretsCommand(opts.json, async () => {
const { runSecretsConfigureInteractive } = await import("./configure-nhsl2GPU.js");
const configured = await runSecretsConfigureInteractive({
providersOnly: Boolean(opts.providersOnly),
skipProviderSetup: Boolean(opts.skipProviderSetup),
agentId: typeof opts.agent === "string" ? opts.agent : void 0,
allowExecInPreflight: Boolean(opts.allowExec)
});
if (opts.planOut) {
const { writeFileSync } = await fsModuleLoader.load();
writeFileSync(opts.planOut, serializePlanFile(configured.plan, opts.planOut), "utf8");
}
let shouldApply = Boolean(opts.apply || opts.yes);
if (!opts.json) {
defaultRuntime.log(`Preflight: changed=${configured.preflight.changed}, files=${configured.preflight.changedFiles.length}, warnings=${configured.preflight.warningCount}.`);
if (configured.preflight.warningCount > 0) for (const warning of configured.preflight.warnings) defaultRuntime.log(`- warning: ${warning}`);
if (!configured.preflight.checks.resolvabilityComplete && configured.preflight.skippedExecRefs > 0) defaultRuntime.log(`Preflight note: skipped ${configured.preflight.skippedExecRefs} exec SecretRef resolvability check(s). Re-run with --allow-exec to execute exec providers during preflight.`);
const providerUpserts = Object.keys(configured.plan.providerUpserts ?? {}).length;
const providerDeletes = configured.plan.providerDeletes?.length ?? 0;
defaultRuntime.log(`Plan: targets=${configured.plan.targets.length}, providerUpserts=${providerUpserts}, providerDeletes=${providerDeletes}.`);
if (opts.planOut) defaultRuntime.log(`Plan written to ${opts.planOut}`);
}
if (!shouldApply && !opts.json) {
const { confirm } = await clackPromptsLoader.load();
const approved = await confirm({
message: "Apply this plan now?",
initialValue: true
});
if (typeof approved === "boolean") shouldApply = approved;
}
if (shouldApply) {
if (!opts.yes && !opts.json) {
const { confirm } = await clackPromptsLoader.load();
if (await confirm({
message: "This migration is one-way for migrated plaintext values. Continue with apply?",
initialValue: true
}) !== true) {
defaultRuntime.log("Apply cancelled.");
return {
plan: configured.plan,
preflight: configured.preflight
};
}
}
const { runSecretsApply } = await secretsApplyLoader.load();
const result = await runSecretsApply({
plan: configured.plan,
write: true,
allowExec: Boolean(opts.allowExec)
});
if (!opts.json) defaultRuntime.log(result.changed ? `Secrets applied. Updated ${result.changedFiles.length} file(s).` : "Secrets apply: no changes.");
return result;
}
return {
plan: configured.plan,
preflight: configured.preflight
};
}, (err) => {
defaultRuntime.error(danger(`Secrets configure failed: ${formatErrorMessage(err)}. Re-run ${formatCliCommand("openclaw secrets audit")} before applying changes.`));
}, 1);
});
secrets.command("apply").description("Apply a previously generated secrets plan").requiredOption("--from <path>", "Path to plan JSON (max 16 MiB)").option("--dry-run", "Validate/preflight only", false).option("--allow-exec", "Allow exec SecretRef checks (may execute provider commands)", false).option("--json", "Output JSON", false).action(async (opts) => {
const result = await runSecretsCommand(opts.json, async () => {
const [{ runSecretsApply }, plan] = await Promise.all([secretsApplyLoader.load(), readPlanFile(opts.from)]);
return runSecretsApply({
plan,
write: !opts.dryRun,
allowExec: Boolean(opts.allowExec)
});
}, (err) => {
const message = err instanceof SecretsPlanFileNotFoundError ? err.message : formatErrorMessage(err);
defaultRuntime.error(danger(`Secrets apply failed: ${message}. Re-run ${formatCliCommand("openclaw secrets apply --from <path> --dry-run")} to inspect the plan without writing.`));
}, 1);
if (opts.json) return;
if (opts.dryRun) {
defaultRuntime.log(result.changed ? `Secrets apply dry run: ${result.changedFiles.length} file(s) would change.` : "Secrets apply dry run: no changes.");
if (!result.checks.resolvabilityComplete && result.skippedExecRefs > 0) defaultRuntime.log(`Secrets apply dry-run note: skipped ${result.skippedExecRefs} exec SecretRef resolvability check(s). Re-run with --allow-exec to execute exec providers during dry-run.`);
return;
}
defaultRuntime.log(result.changed ? `Secrets applied. Updated ${result.changedFiles.length} file(s).` : "Secrets apply: no changes.");
});
}
//#endregion
export { registerSecretsCli };