mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-20 14:30:57 +00:00
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:
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user