refactor: dedupe cli config cron and install flows

This commit is contained in:
Peter Steinberger
2026-03-02 19:48:38 +00:00
parent 9d30159fcd
commit b1c30f0ba9
80 changed files with 1379 additions and 2027 deletions

View File

@@ -210,6 +210,31 @@ function assertNoCancel<T>(value: T | symbol, message: string): T {
return value;
}
function validateEnvNameCsv(value: string): string | undefined {
const entries = parseCsv(value);
for (const entry of entries) {
if (!ENV_NAME_PATTERN.test(entry)) {
return `Invalid env name: ${entry}`;
}
}
return undefined;
}
async function promptEnvNameCsv(params: {
message: string;
initialValue: string;
}): Promise<string[]> {
const raw = assertNoCancel(
await text({
message: params.message,
initialValue: params.initialValue,
validate: (value) => validateEnvNameCsv(String(value ?? "")),
}),
"Secrets configure cancelled.",
);
return parseCsv(String(raw ?? ""));
}
async function promptOptionalPositiveInt(params: {
message: string;
initialValue?: number;
@@ -275,23 +300,10 @@ async function promptProviderSource(initial?: SecretRefSource): Promise<SecretRe
async function promptEnvProvider(
base?: Extract<SecretProviderConfig, { source: "env" }>,
): Promise<Extract<SecretProviderConfig, { source: "env" }>> {
const allowlistRaw = assertNoCancel(
await text({
message: "Env allowlist (comma-separated, blank for unrestricted)",
initialValue: base?.allowlist?.join(",") ?? "",
validate: (value) => {
const entries = parseCsv(String(value ?? ""));
for (const entry of entries) {
if (!ENV_NAME_PATTERN.test(entry)) {
return `Invalid env name: ${entry}`;
}
}
return undefined;
},
}),
"Secrets configure cancelled.",
);
const allowlist = parseCsv(String(allowlistRaw ?? ""));
const allowlist = await promptEnvNameCsv({
message: "Env allowlist (comma-separated, blank for unrestricted)",
initialValue: base?.allowlist?.join(",") ?? "",
});
return {
source: "env",
...(allowlist.length > 0 ? { allowlist } : {}),
@@ -436,22 +448,10 @@ async function promptExecProvider(
"Secrets configure cancelled.",
);
const passEnvRaw = assertNoCancel(
await text({
message: "Pass-through env vars (comma-separated, blank for none)",
initialValue: base?.passEnv?.join(",") ?? "",
validate: (value) => {
const entries = parseCsv(String(value ?? ""));
for (const entry of entries) {
if (!ENV_NAME_PATTERN.test(entry)) {
return `Invalid env name: ${entry}`;
}
}
return undefined;
},
}),
"Secrets configure cancelled.",
);
const passEnv = await promptEnvNameCsv({
message: "Pass-through env vars (comma-separated, blank for none)",
initialValue: base?.passEnv?.join(",") ?? "",
});
const trustedDirsRaw = assertNoCancel(
await text({
@@ -486,7 +486,6 @@ async function promptExecProvider(
);
const args = await parseArgsInput(String(argsRaw ?? ""));
const passEnv = parseCsv(String(passEnvRaw ?? ""));
const trustedDirs = parseCsv(String(trustedDirsRaw ?? ""));
return {

View File

@@ -27,6 +27,46 @@ describe("secret ref resolver", () => {
return dir;
};
type ExecProviderConfig = {
source: "exec";
command: string;
passEnv?: string[];
jsonOnly?: boolean;
allowSymlinkCommand?: boolean;
trustedDirs?: string[];
args?: string[];
};
function createExecProviderConfig(
command: string,
overrides: Partial<ExecProviderConfig> = {},
): ExecProviderConfig {
return {
source: "exec",
command,
passEnv: ["PATH"],
...overrides,
};
}
async function resolveExecSecret(
command: string,
overrides: Partial<ExecProviderConfig> = {},
): Promise<string> {
return resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: createExecProviderConfig(command, overrides),
},
},
},
},
);
}
beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-"));
const sharedExecDir = path.join(fixtureRoot, "shared-exec");
@@ -134,22 +174,7 @@ describe("secret ref resolver", () => {
return;
}
const value = await resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: {
source: "exec",
command: execProtocolV1ScriptPath,
passEnv: ["PATH"],
},
},
},
},
},
);
const value = await resolveExecSecret(execProtocolV1ScriptPath);
expect(value).toBe("value:openai/api-key");
});
@@ -158,23 +183,7 @@ describe("secret ref resolver", () => {
return;
}
const value = await resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: {
source: "exec",
command: execPlainScriptPath,
passEnv: ["PATH"],
jsonOnly: false,
},
},
},
},
},
);
const value = await resolveExecSecret(execPlainScriptPath, { jsonOnly: false });
expect(value).toBe("plain-secret");
});
@@ -210,25 +219,9 @@ describe("secret ref resolver", () => {
const symlinkPath = path.join(root, "resolver-link.mjs");
await fs.symlink(execPlainScriptPath, symlinkPath);
await expect(
resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: {
source: "exec",
command: symlinkPath,
passEnv: ["PATH"],
jsonOnly: false,
},
},
},
},
},
),
).rejects.toThrow("must not be a symlink");
await expect(resolveExecSecret(symlinkPath, { jsonOnly: false })).rejects.toThrow(
"must not be a symlink",
);
});
it("allows symlink command paths when allowSymlinkCommand is enabled", async () => {
@@ -240,25 +233,11 @@ describe("secret ref resolver", () => {
await fs.symlink(execPlainScriptPath, symlinkPath);
const trustedRoot = await fs.realpath(fixtureRoot);
const value = await resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: {
source: "exec",
command: symlinkPath,
passEnv: ["PATH"],
jsonOnly: false,
allowSymlinkCommand: true,
trustedDirs: [trustedRoot],
},
},
},
},
},
);
const value = await resolveExecSecret(symlinkPath, {
jsonOnly: false,
allowSymlinkCommand: true,
trustedDirs: [trustedRoot],
});
expect(value).toBe("plain-secret");
});
@@ -287,44 +266,15 @@ describe("secret ref resolver", () => {
await fs.symlink(targetCommand, symlinkCommand);
const trustedRoot = await fs.realpath(root);
await expect(
resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: {
source: "exec",
command: symlinkCommand,
args: ["brew"],
passEnv: ["PATH"],
},
},
},
},
},
),
).rejects.toThrow("must not be a symlink");
const value = await resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: {
source: "exec",
command: symlinkCommand,
args: ["brew"],
allowSymlinkCommand: true,
trustedDirs: [trustedRoot],
},
},
},
},
},
await expect(resolveExecSecret(symlinkCommand, { args: ["brew"] })).rejects.toThrow(
"must not be a symlink",
);
const value = await resolveExecSecret(symlinkCommand, {
args: ["brew"],
allowSymlinkCommand: true,
trustedDirs: [trustedRoot],
});
expect(value).toBe("brew:openai/api-key");
});
@@ -337,25 +287,11 @@ describe("secret ref resolver", () => {
await fs.symlink(execPlainScriptPath, symlinkPath);
await expect(
resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: {
source: "exec",
command: symlinkPath,
passEnv: ["PATH"],
jsonOnly: false,
allowSymlinkCommand: true,
trustedDirs: [root],
},
},
},
},
},
),
resolveExecSecret(symlinkPath, {
jsonOnly: false,
allowSymlinkCommand: true,
trustedDirs: [root],
}),
).rejects.toThrow("outside trustedDirs");
});
@@ -363,73 +299,27 @@ describe("secret ref resolver", () => {
if (process.platform === "win32") {
return;
}
await expect(
resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: {
source: "exec",
command: execProtocolV2ScriptPath,
passEnv: ["PATH"],
},
},
},
},
},
),
).rejects.toThrow("protocolVersion must be 1");
await expect(resolveExecSecret(execProtocolV2ScriptPath)).rejects.toThrow(
"protocolVersion must be 1",
);
});
it("rejects exec refs when response omits requested id", async () => {
if (process.platform === "win32") {
return;
}
await expect(
resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: {
source: "exec",
command: execMissingIdScriptPath,
passEnv: ["PATH"],
},
},
},
},
},
),
).rejects.toThrow('response missing id "openai/api-key"');
await expect(resolveExecSecret(execMissingIdScriptPath)).rejects.toThrow(
'response missing id "openai/api-key"',
);
});
it("rejects exec refs with invalid JSON when jsonOnly is true", async () => {
if (process.platform === "win32") {
return;
}
await expect(
resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: {
source: "exec",
command: execInvalidJsonScriptPath,
passEnv: ["PATH"],
jsonOnly: true,
},
},
},
},
},
),
).rejects.toThrow("returned invalid JSON");
await expect(resolveExecSecret(execInvalidJsonScriptPath, { jsonOnly: true })).rejects.toThrow(
"returned invalid JSON",
);
});
it("supports file singleValue mode with id=value", async () => {