Secrets: gate exec dry-run and preflight resolution behind --allow-exec (#49417)

* Secrets: gate exec dry-run resolution behind --allow-exec

* Secrets: fix dry-run completeness and skipped exec audit semantics

* Secrets: require --allow-exec for exec-containing apply writes

* Docs: align secrets exec consent behavior

* Changelog: note secrets exec consent gating
This commit is contained in:
Josh Avant
2026-03-17 23:24:34 -05:00
committed by GitHub
parent bf470b711b
commit 0ffcc308f2
13 changed files with 747 additions and 49 deletions

View File

@@ -15,6 +15,7 @@ type SecretsReloadOptions = GatewayRpcOpts & { json?: boolean };
type SecretsAuditOptions = {
check?: boolean;
json?: boolean;
allowExec?: boolean;
};
type SecretsConfigureOptions = {
apply?: boolean;
@@ -23,11 +24,13 @@ type SecretsConfigureOptions = {
providersOnly?: boolean;
skipProviderSetup?: boolean;
agent?: string;
allowExec?: boolean;
json?: boolean;
};
type SecretsApplyOptions = {
from: string;
dryRun?: boolean;
allowExec?: boolean;
json?: boolean;
};
@@ -82,10 +85,17 @@ export function registerSecretsCli(program: Command) {
.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();
const report = await runSecretsAudit({
allowExec: Boolean(opts.allowExec),
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(report, null, 2));
} else {
@@ -102,6 +112,11 @@ export function registerSecretsCli(program: Command) {
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) {
@@ -128,6 +143,11 @@ export function registerSecretsCli(program: Command) {
"--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) => {
@@ -136,6 +156,7 @@ export function registerSecretsCli(program: Command) {
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");
@@ -160,6 +181,14 @@ export function registerSecretsCli(program: Command) {
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(
@@ -196,6 +225,7 @@ export function registerSecretsCli(program: Command) {
const result = await runSecretsApply({
plan: configured.plan,
write: true,
allowExec: Boolean(opts.allowExec),
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
@@ -218,6 +248,7 @@ export function registerSecretsCli(program: Command) {
.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 {
@@ -225,6 +256,7 @@ export function registerSecretsCli(program: Command) {
const result = await runSecretsApply({
plan,
write: !opts.dryRun,
allowExec: Boolean(opts.allowExec),
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
@@ -236,6 +268,11 @@ export function registerSecretsCli(program: Command) {
? `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(