CLI: skip exec SecretRef dry-run resolution unless explicitly allowed (#49322)

* CLI: gate exec SecretRef dry-run resolution behind opt-in

* Docs: clarify config dry-run exec opt-in behavior

* CLI: preserve static exec dry-run validation
This commit is contained in:
Josh Avant
2026-03-17 20:20:11 -05:00
committed by GitHub
parent 9a455a8c08
commit 2d3bcbfe08
7 changed files with 495 additions and 14 deletions

View File

@@ -386,6 +386,7 @@ describe("config cli", () => {
expect(helpText).toContain("--provider-source");
expect(helpText).toContain("--batch-json");
expect(helpText).toContain("--dry-run");
expect(helpText).toContain("--allow-exec");
expect(helpText).toContain("openclaw config set gateway.port 19001 --strict-json");
expect(helpText).toContain(
"openclaw config set channels.discord.token --ref-provider default --ref-source",
@@ -556,6 +557,169 @@ describe("config cli", () => {
expect(mockResolveSecretRefValue).toHaveBeenCalledTimes(1);
});
it("skips exec SecretRef resolvability checks in dry-run by default", async () => {
const resolved: OpenClawConfig = {
gateway: { port: 18789 },
secrets: {
providers: {
runner: {
source: "exec",
command: "/usr/bin/env",
allowInsecurePath: true,
},
},
},
};
setSnapshot(resolved, resolved);
await runConfigCommand([
"config",
"set",
"channels.discord.token",
"--ref-provider",
"runner",
"--ref-source",
"exec",
"--ref-id",
"openai",
"--dry-run",
]);
expect(mockWriteConfigFile).not.toHaveBeenCalled();
expect(mockResolveSecretRefValue).not.toHaveBeenCalled();
expect(mockLog).toHaveBeenCalledWith(
expect.stringContaining(
"Dry run note: skipped 1 exec SecretRef resolvability check(s). Re-run with --allow-exec",
),
);
});
it("allows exec SecretRef resolvability checks in dry-run when --allow-exec is set", async () => {
const resolved: OpenClawConfig = {
gateway: { port: 18789 },
secrets: {
providers: {
runner: {
source: "exec",
command: "/usr/bin/env",
allowInsecurePath: true,
},
},
},
};
setSnapshot(resolved, resolved);
await runConfigCommand([
"config",
"set",
"channels.discord.token",
"--ref-provider",
"runner",
"--ref-source",
"exec",
"--ref-id",
"openai",
"--dry-run",
"--allow-exec",
]);
expect(mockWriteConfigFile).not.toHaveBeenCalled();
expect(mockResolveSecretRefValue).toHaveBeenCalledTimes(1);
expect(mockResolveSecretRefValue).toHaveBeenCalledWith(
expect.objectContaining({
source: "exec",
provider: "runner",
id: "openai",
}),
expect.any(Object),
);
expect(mockLog).not.toHaveBeenCalledWith(
expect.stringContaining("Dry run note: skipped 1 exec SecretRef resolvability check(s)."),
);
});
it("rejects --allow-exec without --dry-run", async () => {
const nonexistentBatchPath = path.join(
os.tmpdir(),
`openclaw-config-batch-nonexistent-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
);
await expect(
runConfigCommand(["config", "set", "--batch-file", nonexistentBatchPath, "--allow-exec"]),
).rejects.toThrow("__exit__:1");
expect(mockWriteConfigFile).not.toHaveBeenCalled();
expect(mockResolveSecretRefValue).not.toHaveBeenCalled();
expect(mockError).toHaveBeenCalledWith(
expect.stringContaining("config set mode error: --allow-exec requires --dry-run."),
);
});
it("fails dry-run when skipped exec refs use an unconfigured provider", async () => {
const resolved: OpenClawConfig = {
gateway: { port: 18789 },
secrets: {
providers: {},
},
};
setSnapshot(resolved, resolved);
await expect(
runConfigCommand([
"config",
"set",
"channels.discord.token",
"--ref-provider",
"runner",
"--ref-source",
"exec",
"--ref-id",
"openai",
"--dry-run",
]),
).rejects.toThrow("__exit__:1");
expect(mockResolveSecretRefValue).not.toHaveBeenCalled();
expect(mockError).toHaveBeenCalledWith(
expect.stringContaining('Secret provider "runner" is not configured'),
);
});
it("fails dry-run when skipped exec refs use a provider with mismatched source", async () => {
const resolved: OpenClawConfig = {
gateway: { port: 18789 },
secrets: {
providers: {
runner: {
source: "env",
},
},
},
};
setSnapshot(resolved, resolved);
await expect(
runConfigCommand([
"config",
"set",
"channels.discord.token",
"--ref-provider",
"runner",
"--ref-source",
"exec",
"--ref-id",
"openai",
"--dry-run",
]),
).rejects.toThrow("__exit__:1");
expect(mockResolveSecretRefValue).not.toHaveBeenCalled();
expect(mockError).toHaveBeenCalledWith(
expect.stringContaining(
'Secret provider "runner" has source "env" but ref requests "exec".',
),
);
});
it("writes sibling SecretRef paths when target uses sibling-ref shape", async () => {
const resolved: OpenClawConfig = {
gateway: { port: 18789 },
@@ -749,19 +913,66 @@ describe("config cli", () => {
expect(typeof raw).toBe("string");
const payload = JSON.parse(String(raw)) as {
ok: boolean;
checks: { schema: boolean; resolvability: boolean };
checks: { schema: boolean; resolvability: boolean; resolvabilityComplete: boolean };
refsChecked: number;
skippedExecRefs: number;
operations: number;
};
expect(payload.ok).toBe(true);
expect(payload.operations).toBe(1);
expect(payload.refsChecked).toBe(1);
expect(payload.skippedExecRefs).toBe(0);
expect(payload.checks).toEqual({
schema: false,
resolvability: true,
resolvabilityComplete: true,
});
});
it("emits skipped exec metadata for --dry-run --json success", async () => {
const resolved: OpenClawConfig = {
gateway: { port: 18789 },
secrets: {
providers: {
runner: {
source: "exec",
command: "/usr/bin/env",
allowInsecurePath: true,
},
},
},
};
setSnapshot(resolved, resolved);
await runConfigCommand([
"config",
"set",
"channels.discord.token",
"--ref-provider",
"runner",
"--ref-source",
"exec",
"--ref-id",
"openai",
"--dry-run",
"--json",
]);
const raw = mockLog.mock.calls.at(-1)?.[0];
expect(typeof raw).toBe("string");
const payload = JSON.parse(String(raw)) as {
ok: boolean;
checks: { resolvability: boolean; resolvabilityComplete: boolean };
refsChecked: number;
skippedExecRefs: number;
};
expect(payload.ok).toBe(true);
expect(payload.checks.resolvability).toBe(true);
expect(payload.checks.resolvabilityComplete).toBe(false);
expect(payload.refsChecked).toBe(0);
expect(payload.skippedExecRefs).toBe(1);
});
it("emits structured JSON for --dry-run --json failure", async () => {
const resolved: OpenClawConfig = {
gateway: { port: 18789 },