import fs from "node:fs" ;
import { confirm } from "@clack/prompts" ;
import type { Command } from "commander" ;
import { danger } from "../globals.js" ;
import { defaultRuntime } from "../runtime.js" ;
import { runSecretsApply } from "../secrets/apply.js" ;
import { resolveSecretsAuditExitCode, runSecretsAudit } from "../secrets/audit.js" ;
import { runSecretsConfigureInteractive } from "../secrets/configure.js" ;
import { isSecretsApplyPlan, type SecretsApplyPlan } from "../secrets/plan.js" ;
import { formatDocsLink } from "../terminal/links.js" ;
import { theme } from "../terminal/theme.js" ;
import { addGatewayClientOptions, callGatewayFromCli, type GatewayRpcOpts } from "./gateway-rpc.js" ;
type SecretsReloadOptions = GatewayRpcOpts & { json?: boolean };
type SecretsAuditOptions = {
check?: boolean ;
json?: boolean ;
allowExec?: boolean ;
};
type SecretsConfigureOptions = {
apply?: boolean ;
yes?: boolean ;
planOut?: string;
providersOnly?: boolean ;
skipProviderSetup?: boolean ;
agent?: string;
allowExec?: boolean ;
json?: boolean ;
};
type SecretsApplyOptions = {
from: string;
dryRun?: boolean ;
allowExec?: boolean ;
json?: boolean ;
};
function readPlanFile(pathname: string): SecretsApplyPlan {
const raw = fs.readFileSync(pathname, "utf8" );
const parsed = JSON.parse(raw) as unknown;
if (!isSecretsApplyPlan(parsed)) {
throw new Error(`Invalid secrets plan file: ${pathname}`);
}
return parsed;
}
export function registerSecretsCli(program: Command) {
const secrets = program
.command("secrets" )
.description("Secrets runtime controls" )
.addHelpText(
"after" ,
() =>
`\n${theme.muted("Docs:" )} ${formatDocsLink("/gateway/security" , "docs.openclaw.ai/gateway/security" )}\n`,
);
addGatewayClientOptions(
secrets
.command("reload" )
.description("Re-resolve secret references and atomically swap runtime snapshot" )
.option("--json" , "Output JSON" , false ),
).action(async (opts: SecretsReloadOptions) => {
try {
const result = await callGatewayFromCli("secrets.reload" , opts, undefined, {
expectFinal: false ,
});
if (opts.json) {
defaultRuntime.writeJson(result);
return ;
}
const warningCount = Number(
(result as { warningCount?: unknown } | undefined)?.warningCount ?? 0 ,
);
if (Number.isFinite(warningCount) && warningCount > 0 ) {
defaultRuntime.log(`Secrets reloaded with ${warningCount} warning(s).`);
return ;
}
defaultRuntime.log("Secrets reloaded." );
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1 );
}
});
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: SecretsAuditOptions) => {
try {
const report = await runSecretsAudit({
allowExec: Boolean (opts.allowExec),
});
if (opts.json) {
defaultRuntime.writeJson(report);
} else {
defaultRuntime.log(
`Secrets audit: ${report.status}. plaintext=${report.summary.plaintextCount}, unresolved=${report.summary.unresolvedRefCount}, shadowed=${report.summary.shadowedRefCount}, legacy=${report.summary.legacyResidueCount}.`,
);
if (report.findings.length > 0 ) {
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.`,
);
}
}
const exitCode = resolveSecretsAuditExitCode(report, Boolean (opts.check));
if (exitCode !== 0 ) {
defaultRuntime.exit(exitCode);
}
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(2 );
}
});
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" )
.option("--json" , "Output JSON" , false )
.action(async (opts: SecretsConfigureOptions) => {
try {
const configured = await runSecretsConfigureInteractive({
providersOnly: Boolean (opts.providersOnly),
skipProviderSetup: Boolean (opts.skipProviderSetup),
agentId: typeof opts.agent === "string" ? opts.agent : undefined,
allowExecInPreflight: Boolean (opts.allowExec),
});
if (opts.planOut) {
fs.writeFileSync(opts.planOut, `${JSON.stringify(configured.plan, null , 2 )}\n`, "utf8" );
}
if (opts.json) {
defaultRuntime.writeJson({
plan: configured.plan,
preflight: configured.preflight,
});
} else {
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}`);
}
}
let shouldApply = Boolean (opts.apply);
if (!shouldApply && !opts.json) {
const approved = await confirm({
message: "Apply this plan now?" ,
initialValue: true ,
});
if (typeof approved === "boolean" ) {
shouldApply = approved;
}
}
if (shouldApply) {
const needsIrreversiblePrompt = Boolean (opts.apply);
if (needsIrreversiblePrompt && !opts.yes && !opts.json) {
const confirmed = await confirm({
message:
"This migration is one-way for migrated plaintext values. Continue with apply?" ,
initialValue: true ,
});
if (confirmed !== true ) {
defaultRuntime.log("Apply cancelled." );
return ;
}
}
const result = await runSecretsApply({
plan: configured.plan,
write: true ,
allowExec: Boolean (opts.allowExec),
});
if (opts.json) {
defaultRuntime.writeJson(result);
return ;
}
defaultRuntime.log(
result.changed
? `Secrets applied. Updated ${result.changedFiles.length} file(s).`
: "Secrets apply: no changes." ,
);
}
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1 );
}
});
secrets
.command("apply" )
.description("Apply a previously generated secrets plan" )
.requiredOption("--from <path>" , "Path to plan JSON" )
.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: SecretsApplyOptions) => {
try {
const plan = readPlanFile(opts.from);
const result = await runSecretsApply({
plan,
write: !opts.dryRun,
allowExec: Boolean (opts.allowExec),
});
if (opts.json) {
defaultRuntime.writeJson(result);
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." ,
);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1 );
}
});
}
Messung V0.5 in Prozent C=100 H=97 G=98
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland