mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 21:24:31 +00:00
feat(secrets): expand SecretRef coverage across user-supplied credentials (#29580)
* feat(secrets): expand secret target coverage and gateway tooling * docs(secrets): align gateway and CLI secret docs * chore(protocol): regenerate swift gateway models for secrets methods * fix(config): restore talk apiKey fallback and stabilize runner test * ci(windows): reduce test worker count for shard stability * ci(windows): raise node heap for test shard stability * test(feishu): make proxy env precedence assertion windows-safe * fix(gateway): resolve auth password SecretInput refs for clients * fix(gateway): resolve remote SecretInput credentials for clients * fix(secrets): skip inactive refs in command snapshot assignments * fix(secrets): scope gateway.remote refs to effective auth surfaces * fix(secrets): ignore memory defaults when enabled agents disable search * fix(secrets): honor Google Chat serviceAccountRef inheritance * fix(secrets): address tsgo errors in command and gateway collectors * fix(secrets): avoid auth-store load in providers-only configure * fix(gateway): defer local password ref resolution by precedence * fix(secrets): gate telegram webhook secret refs by webhook mode * fix(secrets): gate slack signing secret refs to http mode * fix(secrets): skip telegram botToken refs when tokenFile is set * fix(secrets): gate discord pluralkit refs by enabled flag * fix(secrets): gate discord voice tts refs by voice enabled * test(secrets): make runtime fixture modes explicit * fix(cli): resolve local qr password secret refs * fix(cli): fail when gateway leaves command refs unresolved * fix(gateway): fail when local password SecretRef is unresolved * fix(gateway): fail when required remote SecretRefs are unresolved * fix(gateway): resolve local password refs only when password can win * fix(cli): skip local password SecretRef resolution on qr token override * test(gateway): cast SecretRef fixtures to OpenClawConfig * test(secrets): activate mode-gated targets in runtime coverage fixture * fix(cron): support SecretInput webhook tokens safely * fix(bluebubbles): support SecretInput passwords across config paths * fix(msteams): make appPassword SecretInput-safe in onboarding/token paths * fix(bluebubbles): align SecretInput schema helper typing * fix(cli): clarify secrets.resolve version-skew errors * refactor(secrets): return structured inactive paths from secrets.resolve * refactor(gateway): type onboarding secret writes as SecretInput * chore(protocol): regenerate swift models for secrets.resolve * feat(secrets): expand extension credential secretref support * fix(secrets): gate web-search refs by active provider * fix(onboarding): detect SecretRef credentials in extension status * fix(onboarding): allow keeping existing ref in secret prompt * fix(onboarding): resolve gateway password SecretRefs for probe and tui * fix(onboarding): honor secret-input-mode for local gateway auth * fix(acp): resolve gateway SecretInput credentials * fix(secrets): gate gateway.remote refs to remote surfaces * test(secrets): cover pattern matching and inactive array refs * docs(secrets): clarify secrets.resolve and remote active surfaces * fix(bluebubbles): keep existing SecretRef during onboarding * fix(tests): resolve CI type errors in new SecretRef coverage * fix(extensions): replace raw fetch with SSRF-guarded fetch * test(secrets): mark gateway remote targets active in runtime coverage * test(infra): normalize home-prefix expectation across platforms * fix(cli): only resolve local qr password refs in password mode * test(cli): cover local qr token mode with unresolved password ref * docs(cli): clarify local qr password ref resolution behavior * refactor(extensions): reuse sdk SecretInput helpers * fix(wizard): resolve onboarding env-template secrets before plaintext * fix(cli): surface secrets.resolve diagnostics in memory and qr * test(secrets): repair post-rebase runtime and fixtures * fix(gateway): skip remote password ref resolution when token wins * fix(secrets): treat tailscale remote gateway refs as active * fix(gateway): allow remote password fallback when token ref is unresolved * fix(gateway): ignore stale local password refs for none and trusted-proxy * fix(gateway): skip remote secret ref resolution on local call paths * test(cli): cover qr remote tailscale secret ref resolution * fix(secrets): align gateway password active-surface with auth inference * fix(cli): resolve inferred local gateway password refs in qr * fix(gateway): prefer resolvable remote password over token ref pre-resolution * test(gateway): cover none and trusted-proxy stale password refs * docs(secrets): sync qr and gateway active-surface behavior * fix: restore stability blockers from pre-release audit * Secrets: fix collector/runtime precedence contradictions * docs: align secrets and web credential docs * fix(rebase): resolve integration regressions after main rebase * fix(node-host): resolve gateway secret refs for auth * fix(secrets): harden secretinput runtime readers * gateway: skip inactive auth secretref resolution * cli: avoid gateway preflight for inactive secret refs * extensions: allow unresolved refs in onboarding status * tests: fix qr-cli module mock hoist ordering * Security: align audit checks with SecretInput resolution * Gateway: resolve local-mode remote fallback secret refs * Node host: avoid resolving inactive password secret refs * Secrets runtime: mark Slack appToken inactive for HTTP mode * secrets: keep inactive gateway remote refs non-blocking * cli: include agent memory secret targets in runtime resolution * docs(secrets): sync docs with active-surface and web search behavior * fix(secrets): keep telegram top-level token refs active for blank account tokens * fix(daemon): resolve gateway password secret refs for probe auth * fix(secrets): skip IRC NickServ ref resolution when NickServ is disabled * fix(secrets): align token inheritance and exec timeout defaults * docs(secrets): clarify active-surface notes in cli docs * cli: require secrets.resolve gateway capability * gateway: log auth secret surface diagnostics * secrets: remove dead provider resolver module * fix(secrets): restore gateway auth precedence and fallback resolution * fix(tests): align plugin runtime mock typings --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -47,6 +47,8 @@ import {
|
||||
type VerboseLevel,
|
||||
} from "../auto-reply/thinking.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
|
||||
import { getAgentRuntimeCommandSecretTargetIds } from "../cli/command-secret-targets.js";
|
||||
import { type CliDeps, createDefaultDeps } from "../cli/deps.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import {
|
||||
@@ -332,7 +334,15 @@ async function agentCommandInternal(
|
||||
throw new Error("Pass --to <E.164>, --session-id, or --agent to choose a session");
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
const loadedRaw = loadConfig();
|
||||
const { resolvedConfig: cfg, diagnostics } = await resolveCommandSecretRefsViaGateway({
|
||||
config: loadedRaw,
|
||||
commandName: "agent",
|
||||
targetIds: getAgentRuntimeCommandSecretTargetIds(),
|
||||
});
|
||||
for (const entry of diagnostics) {
|
||||
runtime.log(`[secrets] ${entry}`);
|
||||
}
|
||||
const agentIdOverrideRaw = opts.agentId?.trim();
|
||||
const agentIdOverride = agentIdOverrideRaw ? normalizeAgentId(agentIdOverrideRaw) : undefined;
|
||||
if (agentIdOverride) {
|
||||
|
||||
@@ -19,6 +19,25 @@ const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
|
||||
|
||||
type SecretRefChoice = "env" | "provider";
|
||||
|
||||
export type SecretInputModePromptCopy = {
|
||||
modeMessage?: string;
|
||||
plaintextLabel?: string;
|
||||
plaintextHint?: string;
|
||||
refLabel?: string;
|
||||
refHint?: string;
|
||||
};
|
||||
|
||||
export type SecretRefOnboardingPromptCopy = {
|
||||
sourceMessage?: string;
|
||||
envVarMessage?: string;
|
||||
envVarPlaceholder?: string;
|
||||
envVarFormatError?: string;
|
||||
envVarMissingError?: (envVar: string) => string;
|
||||
noProvidersMessage?: string;
|
||||
envValidatedMessage?: (envVar: string) => string;
|
||||
providerValidatedMessage?: (provider: string, id: string, source: "file" | "exec") => string;
|
||||
};
|
||||
|
||||
function formatErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error && typeof error.message === "string" && error.message.trim()) {
|
||||
return error.message;
|
||||
@@ -69,11 +88,12 @@ function resolveRefFallbackInput(params: {
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveApiKeyRefForOnboarding(params: {
|
||||
export async function promptSecretRefForOnboarding(params: {
|
||||
provider: string;
|
||||
config: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
preferredEnvVar?: string;
|
||||
copy?: SecretRefOnboardingPromptCopy;
|
||||
}): Promise<{ ref: SecretRef; resolvedValue: string }> {
|
||||
const defaultEnvVar =
|
||||
params.preferredEnvVar ?? resolveDefaultProviderEnvVar(params.provider) ?? "";
|
||||
@@ -82,7 +102,7 @@ async function resolveApiKeyRefForOnboarding(params: {
|
||||
|
||||
while (true) {
|
||||
const sourceRaw: SecretRefChoice = await params.prompter.select<SecretRefChoice>({
|
||||
message: "Where is this API key stored?",
|
||||
message: params.copy?.sourceMessage ?? "Where is this API key stored?",
|
||||
initialValue: sourceChoice,
|
||||
options: [
|
||||
{
|
||||
@@ -102,16 +122,22 @@ async function resolveApiKeyRefForOnboarding(params: {
|
||||
|
||||
if (source === "env") {
|
||||
const envVarRaw = await params.prompter.text({
|
||||
message: "Environment variable name",
|
||||
message: params.copy?.envVarMessage ?? "Environment variable name",
|
||||
initialValue: defaultEnvVar || undefined,
|
||||
placeholder: "OPENAI_API_KEY",
|
||||
placeholder: params.copy?.envVarPlaceholder ?? "OPENAI_API_KEY",
|
||||
validate: (value) => {
|
||||
const candidate = value.trim();
|
||||
if (!ENV_SECRET_REF_ID_RE.test(candidate)) {
|
||||
return 'Use an env var name like "OPENAI_API_KEY" (uppercase letters, numbers, underscores).';
|
||||
return (
|
||||
params.copy?.envVarFormatError ??
|
||||
'Use an env var name like "OPENAI_API_KEY" (uppercase letters, numbers, underscores).'
|
||||
);
|
||||
}
|
||||
if (!process.env[candidate]?.trim()) {
|
||||
return `Environment variable "${candidate}" is missing or empty in this session.`;
|
||||
return (
|
||||
params.copy?.envVarMissingError?.(candidate) ??
|
||||
`Environment variable "${candidate}" is missing or empty in this session.`
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
@@ -136,7 +162,8 @@ async function resolveApiKeyRefForOnboarding(params: {
|
||||
env: process.env,
|
||||
});
|
||||
await params.prompter.note(
|
||||
`Validated environment variable ${envVar}. OpenClaw will store a reference, not the key value.`,
|
||||
params.copy?.envValidatedMessage?.(envVar) ??
|
||||
`Validated environment variable ${envVar}. OpenClaw will store a reference, not the key value.`,
|
||||
"Reference validated",
|
||||
);
|
||||
return { ref, resolvedValue };
|
||||
@@ -147,7 +174,8 @@ async function resolveApiKeyRefForOnboarding(params: {
|
||||
);
|
||||
if (externalProviders.length === 0) {
|
||||
await params.prompter.note(
|
||||
"No file/exec secret providers are configured yet. Add one under secrets.providers, or select Environment variable.",
|
||||
params.copy?.noProvidersMessage ??
|
||||
"No file/exec secret providers are configured yet. Add one under secrets.providers, or select Environment variable.",
|
||||
"No providers configured",
|
||||
);
|
||||
continue;
|
||||
@@ -222,7 +250,8 @@ async function resolveApiKeyRefForOnboarding(params: {
|
||||
env: process.env,
|
||||
});
|
||||
await params.prompter.note(
|
||||
`Validated ${providerEntry.source} reference ${selectedProvider}:${id}. OpenClaw will store a reference, not the key value.`,
|
||||
params.copy?.providerValidatedMessage?.(selectedProvider, id, providerEntry.source) ??
|
||||
`Validated ${providerEntry.source} reference ${selectedProvider}:${id}. OpenClaw will store a reference, not the key value.`,
|
||||
"Reference validated",
|
||||
);
|
||||
return { ref, resolvedValue };
|
||||
@@ -346,6 +375,7 @@ export function normalizeSecretInputModeInput(
|
||||
export async function resolveSecretInputModeForEnvSelection(params: {
|
||||
prompter: WizardPrompter;
|
||||
explicitMode?: SecretInputMode;
|
||||
copy?: SecretInputModePromptCopy;
|
||||
}): Promise<SecretInputMode> {
|
||||
if (params.explicitMode) {
|
||||
return params.explicitMode;
|
||||
@@ -356,18 +386,20 @@ export async function resolveSecretInputModeForEnvSelection(params: {
|
||||
return "plaintext";
|
||||
}
|
||||
const selected = await params.prompter.select<SecretInputMode>({
|
||||
message: "How do you want to provide this API key?",
|
||||
message: params.copy?.modeMessage ?? "How do you want to provide this API key?",
|
||||
initialValue: "plaintext",
|
||||
options: [
|
||||
{
|
||||
value: "plaintext",
|
||||
label: "Paste API key now",
|
||||
hint: "Stores the key directly in OpenClaw config",
|
||||
label: params.copy?.plaintextLabel ?? "Paste API key now",
|
||||
hint: params.copy?.plaintextHint ?? "Stores the key directly in OpenClaw config",
|
||||
},
|
||||
{
|
||||
value: "ref",
|
||||
label: "Use secret reference",
|
||||
hint: "Stores a reference to env or configured external secret providers",
|
||||
label: params.copy?.refLabel ?? "Use external secret provider",
|
||||
hint:
|
||||
params.copy?.refHint ??
|
||||
"Stores a reference to env or configured external secret providers",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -466,7 +498,7 @@ export async function ensureApiKeyFromEnvOrPrompt(params: {
|
||||
await params.setCredential(fallback.ref, selectedMode);
|
||||
return fallback.resolvedValue;
|
||||
}
|
||||
const resolved = await resolveApiKeyRefForOnboarding({
|
||||
const resolved = await promptSecretRefForOnboarding({
|
||||
provider: params.provider,
|
||||
config: params.config,
|
||||
prompter: params.prompter,
|
||||
|
||||
61
src/commands/auth-choice.apply.anthropic.test.ts
Normal file
61
src/commands/auth-choice.apply.anthropic.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { applyAuthChoiceAnthropic } from "./auth-choice.apply.anthropic.js";
|
||||
import { ANTHROPIC_SETUP_TOKEN_PREFIX } from "./auth-token.js";
|
||||
import {
|
||||
createAuthTestLifecycle,
|
||||
createExitThrowingRuntime,
|
||||
createWizardPrompter,
|
||||
readAuthProfilesForAgent,
|
||||
setupAuthTestEnv,
|
||||
} from "./test-wizard-helpers.js";
|
||||
|
||||
describe("applyAuthChoiceAnthropic", () => {
|
||||
const lifecycle = createAuthTestLifecycle([
|
||||
"OPENCLAW_STATE_DIR",
|
||||
"OPENCLAW_AGENT_DIR",
|
||||
"PI_CODING_AGENT_DIR",
|
||||
"ANTHROPIC_SETUP_TOKEN",
|
||||
]);
|
||||
|
||||
async function setupTempState() {
|
||||
const env = await setupAuthTestEnv("openclaw-anthropic-");
|
||||
lifecycle.setStateDir(env.stateDir);
|
||||
return env.agentDir;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await lifecycle.cleanup();
|
||||
});
|
||||
|
||||
it("persists setup-token ref without plaintext token in auth-profiles store", async () => {
|
||||
const agentDir = await setupTempState();
|
||||
process.env.ANTHROPIC_SETUP_TOKEN = `${ANTHROPIC_SETUP_TOKEN_PREFIX}${"x".repeat(100)}`;
|
||||
|
||||
const prompter = createWizardPrompter({}, { defaultSelect: "ref" });
|
||||
const runtime = createExitThrowingRuntime();
|
||||
|
||||
const result = await applyAuthChoiceAnthropic({
|
||||
authChoice: "setup-token",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.config.auth?.profiles?.["anthropic:default"]).toMatchObject({
|
||||
provider: "anthropic",
|
||||
mode: "token",
|
||||
});
|
||||
|
||||
const parsed = await readAuthProfilesForAgent<{
|
||||
profiles?: Record<string, { token?: string; tokenRef?: unknown }>;
|
||||
}>(agentDir);
|
||||
expect(parsed.profiles?.["anthropic:default"]?.token).toBeUndefined();
|
||||
expect(parsed.profiles?.["anthropic:default"]?.tokenRef).toMatchObject({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "ANTHROPIC_SETUP_TOKEN",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,8 @@ import { normalizeApiKeyInput, validateApiKeyInput } from "./auth-choice.api-key
|
||||
import {
|
||||
normalizeSecretInputModeInput,
|
||||
ensureApiKeyFromOptionEnvOrPrompt,
|
||||
promptSecretRefForOnboarding,
|
||||
resolveSecretInputModeForEnvSelection,
|
||||
} from "./auth-choice.apply-helpers.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import { buildTokenProfileId, validateAnthropicSetupToken } from "./auth-token.js";
|
||||
@@ -28,11 +30,41 @@ export async function applyAuthChoiceAnthropic(
|
||||
"Anthropic setup-token",
|
||||
);
|
||||
|
||||
const tokenRaw = await params.prompter.text({
|
||||
message: "Paste Anthropic setup-token",
|
||||
validate: (value) => validateAnthropicSetupToken(String(value ?? "")),
|
||||
const selectedMode = await resolveSecretInputModeForEnvSelection({
|
||||
prompter: params.prompter,
|
||||
explicitMode: requestedSecretInputMode,
|
||||
copy: {
|
||||
modeMessage: "How do you want to provide this setup token?",
|
||||
plaintextLabel: "Paste setup token now",
|
||||
plaintextHint: "Stores the token directly in the auth profile",
|
||||
},
|
||||
});
|
||||
const token = String(tokenRaw ?? "").trim();
|
||||
let token = "";
|
||||
let tokenRef: { source: "env" | "file" | "exec"; provider: string; id: string } | undefined;
|
||||
if (selectedMode === "ref") {
|
||||
const resolved = await promptSecretRefForOnboarding({
|
||||
provider: "anthropic-setup-token",
|
||||
config: params.config,
|
||||
prompter: params.prompter,
|
||||
preferredEnvVar: "ANTHROPIC_SETUP_TOKEN",
|
||||
copy: {
|
||||
sourceMessage: "Where is this Anthropic setup token stored?",
|
||||
envVarPlaceholder: "ANTHROPIC_SETUP_TOKEN",
|
||||
},
|
||||
});
|
||||
token = resolved.resolvedValue.trim();
|
||||
tokenRef = resolved.ref;
|
||||
} else {
|
||||
const tokenRaw = await params.prompter.text({
|
||||
message: "Paste Anthropic setup-token",
|
||||
validate: (value) => validateAnthropicSetupToken(String(value ?? "")),
|
||||
});
|
||||
token = String(tokenRaw ?? "").trim();
|
||||
}
|
||||
const tokenValidationError = validateAnthropicSetupToken(token);
|
||||
if (tokenValidationError) {
|
||||
throw new Error(tokenValidationError);
|
||||
}
|
||||
|
||||
const profileNameRaw = await params.prompter.text({
|
||||
message: "Token name (blank = default)",
|
||||
@@ -51,6 +83,7 @@ export async function applyAuthChoiceAnthropic(
|
||||
type: "token",
|
||||
provider,
|
||||
token,
|
||||
...(tokenRef ? { tokenRef } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import type { ChannelResolveKind, ChannelResolveResult } from "../../channels/plugins/types.js";
|
||||
import { resolveCommandSecretRefsViaGateway } from "../../cli/command-secret-gateway.js";
|
||||
import { getChannelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { danger } from "../../globals.js";
|
||||
import { resolveMessageChannelSelection } from "../../infra/outbound/channel-selection.js";
|
||||
@@ -68,7 +70,15 @@ function formatResolveResult(result: ResolveResult): string {
|
||||
}
|
||||
|
||||
export async function channelsResolveCommand(opts: ChannelsResolveOptions, runtime: RuntimeEnv) {
|
||||
const cfg = loadConfig();
|
||||
const loadedRaw = loadConfig();
|
||||
const { resolvedConfig: cfg, diagnostics } = await resolveCommandSecretRefsViaGateway({
|
||||
config: loadedRaw,
|
||||
commandName: "channels resolve",
|
||||
targetIds: getChannelsCommandSecretTargetIds(),
|
||||
});
|
||||
for (const entry of diagnostics) {
|
||||
runtime.log(`[secrets] ${entry}`);
|
||||
}
|
||||
const entries = (opts.entries ?? []).map((entry) => entry.trim()).filter(Boolean);
|
||||
if (entries.length === 0) {
|
||||
throw new Error("At least one entry is required.");
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { type ChannelId, getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import { resolveCommandSecretRefsViaGateway } from "../../cli/command-secret-gateway.js";
|
||||
import { getChannelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
@@ -9,7 +11,19 @@ export type ChatChannel = ChannelId;
|
||||
export async function requireValidConfig(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
): Promise<OpenClawConfig | null> {
|
||||
return await requireValidConfigSnapshot(runtime);
|
||||
const cfg = await requireValidConfigSnapshot(runtime);
|
||||
if (!cfg) {
|
||||
return null;
|
||||
}
|
||||
const { resolvedConfig, diagnostics } = await resolveCommandSecretRefsViaGateway({
|
||||
config: cfg,
|
||||
commandName: "channels",
|
||||
targetIds: getChannelsCommandSecretTargetIds(),
|
||||
});
|
||||
for (const entry of diagnostics) {
|
||||
runtime.log(`[secrets] ${entry}`);
|
||||
}
|
||||
return resolvedConfig;
|
||||
}
|
||||
|
||||
export function formatAccountLabel(params: { accountId: string; name?: string }) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { readConfigFileSnapshot, resolveGatewayPort, writeConfigFile } from "../config/config.js";
|
||||
import { logConfigUpdated } from "../config/logging.js";
|
||||
import { normalizeSecretInputString } from "../config/types.secrets.js";
|
||||
import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
@@ -61,7 +62,9 @@ async function runGatewayHealthCheck(params: {
|
||||
const remoteUrl = params.cfg.gateway?.remote?.url?.trim();
|
||||
const wsUrl = params.cfg.gateway?.mode === "remote" && remoteUrl ? remoteUrl : localLinks.wsUrl;
|
||||
const token = params.cfg.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
const password = params.cfg.gateway?.auth?.password ?? process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
const password =
|
||||
normalizeSecretInputString(params.cfg.gateway?.auth?.password) ??
|
||||
process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
|
||||
await waitForGatewayReachable({
|
||||
url: wsUrl,
|
||||
@@ -247,13 +250,15 @@ export async function runConfigureWizard(
|
||||
const localProbe = await probeGatewayReachable({
|
||||
url: localUrl,
|
||||
token: baseConfig.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN,
|
||||
password: baseConfig.gateway?.auth?.password ?? process.env.OPENCLAW_GATEWAY_PASSWORD,
|
||||
password:
|
||||
normalizeSecretInputString(baseConfig.gateway?.auth?.password) ??
|
||||
process.env.OPENCLAW_GATEWAY_PASSWORD,
|
||||
});
|
||||
const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? "";
|
||||
const remoteProbe = remoteUrl
|
||||
? await probeGatewayReachable({
|
||||
url: remoteUrl,
|
||||
token: baseConfig.gateway?.remote?.token,
|
||||
token: normalizeSecretInputString(baseConfig.gateway?.remote?.token),
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -312,8 +317,8 @@ export async function runConfigureWizard(
|
||||
DEFAULT_WORKSPACE;
|
||||
let gatewayPort = resolveGatewayPort(baseConfig);
|
||||
let gatewayToken: string | undefined =
|
||||
nextConfig.gateway?.auth?.token ??
|
||||
baseConfig.gateway?.auth?.token ??
|
||||
normalizeSecretInputString(nextConfig.gateway?.auth?.token) ??
|
||||
normalizeSecretInputString(baseConfig.gateway?.auth?.token) ??
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
|
||||
const persistConfig = async () => {
|
||||
@@ -534,8 +539,12 @@ export async function runConfigureWizard(
|
||||
basePath: nextConfig.gateway?.controlUi?.basePath,
|
||||
});
|
||||
// Try both new and old passwords since gateway may still have old config.
|
||||
const newPassword = nextConfig.gateway?.auth?.password ?? process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
const oldPassword = baseConfig.gateway?.auth?.password ?? process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
const newPassword =
|
||||
normalizeSecretInputString(nextConfig.gateway?.auth?.password) ??
|
||||
process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
const oldPassword =
|
||||
normalizeSecretInputString(baseConfig.gateway?.auth?.password) ??
|
||||
process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
const token = nextConfig.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
|
||||
let gatewayProbe = await probeGatewayReachable({
|
||||
|
||||
@@ -40,6 +40,31 @@ describe("noteMacLaunchctlGatewayEnvOverrides", () => {
|
||||
expect(noteFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats SecretRef-backed credentials as configured", async () => {
|
||||
const noteFn = vi.fn();
|
||||
const getenv = vi.fn(async (name: string) =>
|
||||
name === "OPENCLAW_GATEWAY_PASSWORD" ? "launchctl-password" : undefined,
|
||||
);
|
||||
const cfg = {
|
||||
gateway: {
|
||||
auth: {
|
||||
password: { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_PASSWORD" },
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
providers: {
|
||||
default: { source: "env" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
await noteMacLaunchctlGatewayEnvOverrides(cfg, { platform: "darwin", getenv, noteFn });
|
||||
|
||||
expect(noteFn).toHaveBeenCalledTimes(1);
|
||||
const [message] = noteFn.mock.calls[0] ?? [];
|
||||
expect(message).toContain("OPENCLAW_GATEWAY_PASSWORD");
|
||||
});
|
||||
|
||||
it("does nothing on non-darwin platforms", async () => {
|
||||
const noteFn = vi.fn();
|
||||
const getenv = vi.fn(async () => "launchctl-token");
|
||||
|
||||
@@ -4,6 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
|
||||
@@ -45,14 +46,16 @@ async function launchctlGetenv(name: string): Promise<string | undefined> {
|
||||
|
||||
function hasConfigGatewayCreds(cfg: OpenClawConfig): boolean {
|
||||
const localToken =
|
||||
typeof cfg.gateway?.auth?.token === "string" ? cfg.gateway?.auth?.token.trim() : "";
|
||||
const localPassword =
|
||||
typeof cfg.gateway?.auth?.password === "string" ? cfg.gateway?.auth?.password.trim() : "";
|
||||
const remoteToken =
|
||||
typeof cfg.gateway?.remote?.token === "string" ? cfg.gateway?.remote?.token.trim() : "";
|
||||
const remotePassword =
|
||||
typeof cfg.gateway?.remote?.password === "string" ? cfg.gateway?.remote?.password.trim() : "";
|
||||
return Boolean(localToken || localPassword || remoteToken || remotePassword);
|
||||
typeof cfg.gateway?.auth?.token === "string" ? cfg.gateway.auth.token : undefined;
|
||||
const localPassword = cfg.gateway?.auth?.password;
|
||||
const remoteToken = cfg.gateway?.remote?.token;
|
||||
const remotePassword = cfg.gateway?.remote?.password;
|
||||
return Boolean(
|
||||
hasConfiguredSecretInput(localToken) ||
|
||||
hasConfiguredSecretInput(localPassword, cfg.secrets?.defaults) ||
|
||||
hasConfiguredSecretInput(remoteToken, cfg.secrets?.defaults) ||
|
||||
hasConfiguredSecretInput(remotePassword, cfg.secrets?.defaults),
|
||||
);
|
||||
}
|
||||
|
||||
export async function noteMacLaunchctlGatewayEnvOverrides(
|
||||
|
||||
@@ -18,6 +18,14 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const resolveCommandSecretRefsViaGateway = vi.fn(async ({ config }: { config: unknown }) => ({
|
||||
resolvedConfig: config,
|
||||
diagnostics: [] as string[],
|
||||
}));
|
||||
vi.mock("../cli/command-secret-gateway.js", () => ({
|
||||
resolveCommandSecretRefsViaGateway,
|
||||
}));
|
||||
|
||||
const callGatewayMock = vi.fn();
|
||||
vi.mock("../gateway/call.js", () => ({
|
||||
callGateway: callGatewayMock,
|
||||
@@ -69,6 +77,7 @@ beforeEach(async () => {
|
||||
handleSlackAction.mockClear();
|
||||
handleTelegramAction.mockClear();
|
||||
handleWhatsAppAction.mockClear();
|
||||
resolveCommandSecretRefsViaGateway.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -2,6 +2,8 @@ import {
|
||||
CHANNEL_MESSAGE_ACTION_NAMES,
|
||||
type ChannelMessageActionName,
|
||||
} from "../channels/plugins/types.js";
|
||||
import { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
|
||||
import { getChannelsCommandSecretTargetIds } from "../cli/command-secret-targets.js";
|
||||
import { createOutboundSendDeps, type CliDeps } from "../cli/outbound-send-deps.js";
|
||||
import { withProgress } from "../cli/progress.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
@@ -16,7 +18,15 @@ export async function messageCommand(
|
||||
deps: CliDeps,
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const cfg = loadConfig();
|
||||
const loadedRaw = loadConfig();
|
||||
const { resolvedConfig: cfg, diagnostics } = await resolveCommandSecretRefsViaGateway({
|
||||
config: loadedRaw,
|
||||
commandName: "message",
|
||||
targetIds: getChannelsCommandSecretTargetIds(),
|
||||
});
|
||||
for (const entry of diagnostics) {
|
||||
runtime.log(`[secrets] ${entry}`);
|
||||
}
|
||||
const rawAction = typeof opts.action === "string" ? opts.action.trim() : "";
|
||||
const actionInput = rawAction || "send";
|
||||
const actionMatch = (CHANNEL_MESSAGE_ACTION_NAMES as readonly string[]).find(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { logConfigUpdated } from "../../config/logging.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { loadModelsConfig } from "./load-config.js";
|
||||
import {
|
||||
ensureFlagCompatibility,
|
||||
normalizeAlias,
|
||||
@@ -13,7 +13,7 @@ export async function modelsAliasesListCommand(
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
ensureFlagCompatibility(opts);
|
||||
const cfg = loadConfig();
|
||||
const cfg = await loadModelsConfig({ commandName: "models aliases list", runtime });
|
||||
const models = cfg.agents?.defaults?.models ?? {};
|
||||
const aliases = Object.entries(models).reduce<Record<string, string>>(
|
||||
(acc, [modelKey, entry]) => {
|
||||
@@ -53,7 +53,8 @@ export async function modelsAliasesAddCommand(
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const alias = normalizeAlias(aliasRaw);
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg: loadConfig() });
|
||||
const cfg = await loadModelsConfig({ commandName: "models aliases add", runtime });
|
||||
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
|
||||
const _updated = await updateConfig((cfg) => {
|
||||
const modelKey = `${resolved.provider}/${resolved.model}`;
|
||||
const nextModels = { ...cfg.agents?.defaults?.models };
|
||||
|
||||
@@ -5,13 +5,13 @@ import {
|
||||
setAuthProfileOrder,
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import { normalizeProviderId } from "../../agents/model-selection.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { shortenHomePath } from "../../utils.js";
|
||||
import { loadModelsConfig } from "./load-config.js";
|
||||
import { resolveKnownAgentId } from "./shared.js";
|
||||
|
||||
function resolveTargetAgent(
|
||||
cfg: ReturnType<typeof loadConfig>,
|
||||
cfg: Awaited<ReturnType<typeof loadModelsConfig>>,
|
||||
raw?: string,
|
||||
): {
|
||||
agentId: string;
|
||||
@@ -28,13 +28,16 @@ function describeOrder(store: AuthProfileStore, provider: string): string[] {
|
||||
return Array.isArray(order) ? order : [];
|
||||
}
|
||||
|
||||
function resolveAuthOrderContext(opts: { provider: string; agent?: string }) {
|
||||
async function resolveAuthOrderContext(
|
||||
opts: { provider: string; agent?: string },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const rawProvider = opts.provider?.trim();
|
||||
if (!rawProvider) {
|
||||
throw new Error("Missing --provider.");
|
||||
}
|
||||
const provider = normalizeProviderId(rawProvider);
|
||||
const cfg = loadConfig();
|
||||
const cfg = await loadModelsConfig({ commandName: "models auth-order", runtime });
|
||||
const { agentId, agentDir } = resolveTargetAgent(cfg, opts.agent);
|
||||
return { cfg, agentId, agentDir, provider };
|
||||
}
|
||||
@@ -43,7 +46,7 @@ export async function modelsAuthOrderGetCommand(
|
||||
opts: { provider: string; agent?: string; json?: boolean },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const { agentId, agentDir, provider } = resolveAuthOrderContext(opts);
|
||||
const { agentId, agentDir, provider } = await resolveAuthOrderContext(opts, runtime);
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
@@ -76,7 +79,7 @@ export async function modelsAuthOrderClearCommand(
|
||||
opts: { provider: string; agent?: string },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const { agentId, agentDir, provider } = resolveAuthOrderContext(opts);
|
||||
const { agentId, agentDir, provider } = await resolveAuthOrderContext(opts, runtime);
|
||||
const updated = await setAuthProfileOrder({
|
||||
agentDir,
|
||||
provider,
|
||||
@@ -95,7 +98,7 @@ export async function modelsAuthOrderSetCommand(
|
||||
opts: { provider: string; agent?: string; order: string[] },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const { agentId, agentDir, provider } = resolveAuthOrderContext(opts);
|
||||
const { agentId, agentDir, provider } = await resolveAuthOrderContext(opts, runtime);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { buildModelAliasIndex, resolveModelRefFromString } from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { logConfigUpdated } from "../../config/logging.js";
|
||||
import { resolveAgentModelFallbackValues, toAgentModelListLike } from "../../config/model-input.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { loadModelsConfig } from "./load-config.js";
|
||||
import {
|
||||
DEFAULT_PROVIDER,
|
||||
ensureFlagCompatibility,
|
||||
@@ -44,7 +44,7 @@ export async function listFallbacksCommand(
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
ensureFlagCompatibility(opts);
|
||||
const cfg = loadConfig();
|
||||
const cfg = await loadModelsConfig({ commandName: `models ${params.key} list`, runtime });
|
||||
const fallbacks = getFallbacks(cfg, params.key);
|
||||
|
||||
if (opts.json) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { formatErrorWithStack } from "./list.errors.js";
|
||||
import { loadModelRegistry, toModelRow } from "./list.registry.js";
|
||||
import { printModelTable } from "./list.table.js";
|
||||
import type { ModelRow } from "./list.types.js";
|
||||
import { loadModelsConfig } from "./load-config.js";
|
||||
import { DEFAULT_PROVIDER, ensureFlagCompatibility, isLocalBaseUrl, modelKey } from "./shared.js";
|
||||
|
||||
export async function modelsListCommand(
|
||||
@@ -21,9 +22,8 @@ export async function modelsListCommand(
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
ensureFlagCompatibility(opts);
|
||||
const { loadConfig } = await import("../../config/config.js");
|
||||
const { ensureAuthProfileStore } = await import("../../agents/auth-profiles.js");
|
||||
const cfg = loadConfig();
|
||||
const cfg = await loadModelsConfig({ commandName: "models list", runtime });
|
||||
const authStore = ensureAuthProfileStore();
|
||||
const providerFilter = (() => {
|
||||
const raw = opts.provider?.trim();
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from "../../agents/model-selection.js";
|
||||
import { formatCliCommand } from "../../cli/command-format.js";
|
||||
import { withProgressTotals } from "../../cli/progress.js";
|
||||
import { CONFIG_PATH, loadConfig } from "../../config/config.js";
|
||||
import { CONFIG_PATH } from "../../config/config.js";
|
||||
import {
|
||||
resolveAgentModelFallbackValues,
|
||||
resolveAgentModelPrimaryValue,
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
sortProbeResults,
|
||||
type AuthProbeSummary,
|
||||
} from "./list.probe.js";
|
||||
import { loadModelsConfig } from "./load-config.js";
|
||||
import {
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_PROVIDER,
|
||||
@@ -76,7 +77,7 @@ export async function modelsStatusCommand(
|
||||
if (opts.plain && opts.probe) {
|
||||
throw new Error("--probe cannot be used with --plain output.");
|
||||
}
|
||||
const cfg = loadConfig();
|
||||
const cfg = await loadModelsConfig({ commandName: "models status", runtime });
|
||||
const agentId = resolveKnownAgentId({ cfg, rawAgentId: opts.agent });
|
||||
const agentDir = agentId ? resolveAgentDir(cfg, agentId) : resolveOpenClawAgentDir();
|
||||
const agentModelPrimary = agentId ? resolveAgentExplicitModelPrimary(cfg, agentId) : undefined;
|
||||
|
||||
22
src/commands/models/load-config.ts
Normal file
22
src/commands/models/load-config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { resolveCommandSecretRefsViaGateway } from "../../cli/command-secret-gateway.js";
|
||||
import { getModelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
|
||||
import { loadConfig, type OpenClawConfig } from "../../config/config.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
|
||||
export async function loadModelsConfig(params: {
|
||||
commandName: string;
|
||||
runtime?: RuntimeEnv;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const loadedRaw = loadConfig();
|
||||
const { resolvedConfig, diagnostics } = await resolveCommandSecretRefsViaGateway({
|
||||
config: loadedRaw,
|
||||
commandName: params.commandName,
|
||||
targetIds: getModelsCommandSecretTargetIds(),
|
||||
});
|
||||
if (params.runtime) {
|
||||
for (const entry of diagnostics) {
|
||||
params.runtime.log(`[secrets] ${entry}`);
|
||||
}
|
||||
}
|
||||
return resolvedConfig;
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { cancel, multiselect as clackMultiselect, isCancel } from "@clack/prompt
|
||||
import { resolveApiKeyForProvider } from "../../agents/model-auth.js";
|
||||
import { type ModelScanResult, scanOpenRouterModels } from "../../agents/model-scan.js";
|
||||
import { withProgressTotals } from "../../cli/progress.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { logConfigUpdated } from "../../config/logging.js";
|
||||
import { toAgentModelListLike } from "../../config/model-input.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
stylePromptTitle,
|
||||
} from "../../terminal/prompt-style.js";
|
||||
import { pad, truncate } from "./list.format.js";
|
||||
import { loadModelsConfig } from "./load-config.js";
|
||||
import { formatMs, formatTokenK, updateConfig } from "./shared.js";
|
||||
|
||||
const MODEL_PAD = 42;
|
||||
@@ -167,7 +167,7 @@ export async function modelsScanCommand(
|
||||
throw new Error("--concurrency must be > 0");
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
const cfg = await loadModelsConfig({ commandName: "models scan", runtime });
|
||||
const probe = opts.probe ?? true;
|
||||
let storedKey: string | undefined;
|
||||
if (probe) {
|
||||
|
||||
@@ -132,23 +132,46 @@ describe("promptRemoteGatewayConfig", () => {
|
||||
expect(next.gateway?.remote?.token).toBeUndefined();
|
||||
});
|
||||
|
||||
it("allows private ws:// only when OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1", async () => {
|
||||
process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS = "1";
|
||||
|
||||
it("supports storing remote auth as an external env secret ref", async () => {
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN = "remote-token-value";
|
||||
const text: WizardPrompter["text"] = vi.fn(async (params) => {
|
||||
if (params.message === "Gateway WebSocket URL") {
|
||||
expect(params.validate?.("ws://10.0.0.8:18789")).toBeUndefined();
|
||||
return "ws://10.0.0.8:18789";
|
||||
return "wss://remote.example.com:18789";
|
||||
}
|
||||
if (params.message === "Environment variable name") {
|
||||
return "OPENCLAW_GATEWAY_TOKEN";
|
||||
}
|
||||
return "";
|
||||
}) as WizardPrompter["text"];
|
||||
|
||||
const { next } = await runRemotePrompt({
|
||||
text,
|
||||
confirm: false,
|
||||
selectResponses: { "Gateway auth": "off" },
|
||||
const select: WizardPrompter["select"] = vi.fn(async (params) => {
|
||||
if (params.message === "Gateway auth") {
|
||||
return "token" as never;
|
||||
}
|
||||
if (params.message === "How do you want to provide this gateway token?") {
|
||||
return "ref" as never;
|
||||
}
|
||||
if (params.message === "Where is this gateway token stored?") {
|
||||
return "env" as never;
|
||||
}
|
||||
return (params.options[0]?.value ?? "") as never;
|
||||
});
|
||||
|
||||
expect(next.gateway?.remote?.url).toBe("ws://10.0.0.8:18789");
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const prompter = createPrompter({
|
||||
confirm: vi.fn(async () => false),
|
||||
select,
|
||||
text,
|
||||
});
|
||||
|
||||
const next = await promptRemoteGatewayConfig(cfg, prompter);
|
||||
|
||||
expect(next.gateway?.mode).toBe("remote");
|
||||
expect(next.gateway?.remote?.url).toBe("wss://remote.example.com:18789");
|
||||
expect(next.gateway?.remote?.token).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "OPENCLAW_GATEWAY_TOKEN",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { SecretInput } from "../config/types.secrets.js";
|
||||
import { isSecureWebSocketUrl } from "../gateway/net.js";
|
||||
import type { GatewayBonjourBeacon } from "../infra/bonjour-discovery.js";
|
||||
import { discoverGatewayBeacons } from "../infra/bonjour-discovery.js";
|
||||
import { resolveWideAreaDiscoveryDomain } from "../infra/widearea-dns.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import {
|
||||
promptSecretRefForOnboarding,
|
||||
resolveSecretInputModeForEnvSelection,
|
||||
} from "./auth-choice.apply-helpers.js";
|
||||
import { detectBinary } from "./onboard-helpers.js";
|
||||
import type { SecretInputMode } from "./onboard-types.js";
|
||||
|
||||
const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";
|
||||
|
||||
@@ -51,6 +57,7 @@ function validateGatewayWebSocketUrl(value: string): string | undefined {
|
||||
export async function promptRemoteGatewayConfig(
|
||||
cfg: OpenClawConfig,
|
||||
prompter: WizardPrompter,
|
||||
options?: { secretInputMode?: SecretInputMode },
|
||||
): Promise<OpenClawConfig> {
|
||||
let selectedBeacon: GatewayBonjourBeacon | null = null;
|
||||
let suggestedUrl = cfg.gateway?.remote?.url ?? DEFAULT_GATEWAY_URL;
|
||||
@@ -150,21 +157,80 @@ export async function promptRemoteGatewayConfig(
|
||||
message: "Gateway auth",
|
||||
options: [
|
||||
{ value: "token", label: "Token (recommended)" },
|
||||
{ value: "password", label: "Password" },
|
||||
{ value: "off", label: "No auth" },
|
||||
],
|
||||
});
|
||||
|
||||
let token = cfg.gateway?.remote?.token ?? "";
|
||||
let token: SecretInput | undefined = cfg.gateway?.remote?.token;
|
||||
let password: SecretInput | undefined = cfg.gateway?.remote?.password;
|
||||
if (authChoice === "token") {
|
||||
token = String(
|
||||
await prompter.text({
|
||||
message: "Gateway token",
|
||||
initialValue: token,
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
const selectedMode = await resolveSecretInputModeForEnvSelection({
|
||||
prompter,
|
||||
explicitMode: options?.secretInputMode,
|
||||
copy: {
|
||||
modeMessage: "How do you want to provide this gateway token?",
|
||||
plaintextLabel: "Enter token now",
|
||||
plaintextHint: "Stores the token directly in OpenClaw config",
|
||||
},
|
||||
});
|
||||
if (selectedMode === "ref") {
|
||||
const resolved = await promptSecretRefForOnboarding({
|
||||
provider: "gateway-remote-token",
|
||||
config: cfg,
|
||||
prompter,
|
||||
preferredEnvVar: "OPENCLAW_GATEWAY_TOKEN",
|
||||
copy: {
|
||||
sourceMessage: "Where is this gateway token stored?",
|
||||
envVarPlaceholder: "OPENCLAW_GATEWAY_TOKEN",
|
||||
},
|
||||
});
|
||||
token = resolved.ref;
|
||||
} else {
|
||||
token = String(
|
||||
await prompter.text({
|
||||
message: "Gateway token",
|
||||
initialValue: typeof token === "string" ? token : undefined,
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
}
|
||||
password = undefined;
|
||||
} else if (authChoice === "password") {
|
||||
const selectedMode = await resolveSecretInputModeForEnvSelection({
|
||||
prompter,
|
||||
explicitMode: options?.secretInputMode,
|
||||
copy: {
|
||||
modeMessage: "How do you want to provide this gateway password?",
|
||||
plaintextLabel: "Enter password now",
|
||||
plaintextHint: "Stores the password directly in OpenClaw config",
|
||||
},
|
||||
});
|
||||
if (selectedMode === "ref") {
|
||||
const resolved = await promptSecretRefForOnboarding({
|
||||
provider: "gateway-remote-password",
|
||||
config: cfg,
|
||||
prompter,
|
||||
preferredEnvVar: "OPENCLAW_GATEWAY_PASSWORD",
|
||||
copy: {
|
||||
sourceMessage: "Where is this gateway password stored?",
|
||||
envVarPlaceholder: "OPENCLAW_GATEWAY_PASSWORD",
|
||||
},
|
||||
});
|
||||
password = resolved.ref;
|
||||
} else {
|
||||
password = String(
|
||||
await prompter.text({
|
||||
message: "Gateway password",
|
||||
initialValue: typeof password === "string" ? password : undefined,
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
}
|
||||
token = undefined;
|
||||
} else {
|
||||
token = "";
|
||||
token = undefined;
|
||||
password = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -174,7 +240,8 @@ export async function promptRemoteGatewayConfig(
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url,
|
||||
token: token || undefined,
|
||||
...(token !== undefined ? { token } : {}),
|
||||
...(password !== undefined ? { password } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
|
||||
import { getStatusCommandSecretTargetIds } from "../cli/command-secret-targets.js";
|
||||
import { withProgress } from "../cli/progress.js";
|
||||
import { loadConfig, readConfigFileSnapshot, resolveGatewayPort } from "../config/config.js";
|
||||
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
|
||||
@@ -36,7 +38,12 @@ export async function statusAllCommand(
|
||||
): Promise<void> {
|
||||
await withProgress({ label: "Scanning status --all…", total: 11 }, async (progress) => {
|
||||
progress.setLabel("Loading config…");
|
||||
const cfg = loadConfig();
|
||||
const loadedRaw = loadConfig();
|
||||
const { resolvedConfig: cfg } = await resolveCommandSecretRefsViaGateway({
|
||||
config: loadedRaw,
|
||||
commandName: "status --all",
|
||||
targetIds: getStatusCommandSecretTargetIds(),
|
||||
});
|
||||
const osSummary = resolveOsSummary();
|
||||
const snap = await readConfigFileSnapshot().catch(() => null);
|
||||
progress.tick();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
|
||||
import { getStatusCommandSecretTargetIds } from "../cli/command-secret-targets.js";
|
||||
import { withProgress } from "../cli/progress.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||
@@ -148,7 +150,12 @@ async function scanStatusJsonFast(opts: {
|
||||
timeoutMs?: number;
|
||||
all?: boolean;
|
||||
}): Promise<StatusScanResult> {
|
||||
const cfg = loadConfig();
|
||||
const loadedRaw = loadConfig();
|
||||
const { resolvedConfig: cfg } = await resolveCommandSecretRefsViaGateway({
|
||||
config: loadedRaw,
|
||||
commandName: "status --json",
|
||||
targetIds: getStatusCommandSecretTargetIds(),
|
||||
});
|
||||
const osSummary = resolveOsSummary();
|
||||
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
||||
const updateTimeoutMs = opts.all ? 6500 : 2500;
|
||||
@@ -158,7 +165,7 @@ async function scanStatusJsonFast(opts: {
|
||||
includeRegistry: true,
|
||||
});
|
||||
const agentStatusPromise = getAgentLocalStatuses();
|
||||
const summaryPromise = getStatusSummary();
|
||||
const summaryPromise = getStatusSummary({ config: cfg });
|
||||
|
||||
const tailscaleDnsPromise =
|
||||
tailscaleMode === "off"
|
||||
@@ -233,7 +240,12 @@ export async function scanStatus(
|
||||
},
|
||||
async (progress) => {
|
||||
progress.setLabel("Loading config…");
|
||||
const cfg = loadConfig();
|
||||
const loadedRaw = loadConfig();
|
||||
const { resolvedConfig: cfg } = await resolveCommandSecretRefsViaGateway({
|
||||
config: loadedRaw,
|
||||
commandName: "status",
|
||||
targetIds: getStatusCommandSecretTargetIds(),
|
||||
});
|
||||
const osSummary = resolveOsSummary();
|
||||
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
||||
const tailscaleDnsPromise =
|
||||
@@ -251,7 +263,7 @@ export async function scanStatus(
|
||||
}),
|
||||
);
|
||||
const agentStatusPromise = deferResult(getAgentLocalStatuses());
|
||||
const summaryPromise = deferResult(getStatusSummary());
|
||||
const summaryPromise = deferResult(getStatusSummary({ config: cfg }));
|
||||
progress.tick();
|
||||
|
||||
progress.setLabel("Checking Tailscale…");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolveContextTokensForModel } from "../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
@@ -76,10 +77,10 @@ export function redactSensitiveStatusSummary(summary: StatusSummary): StatusSumm
|
||||
}
|
||||
|
||||
export async function getStatusSummary(
|
||||
options: { includeSensitive?: boolean } = {},
|
||||
options: { includeSensitive?: boolean; config?: OpenClawConfig } = {},
|
||||
): Promise<StatusSummary> {
|
||||
const { includeSensitive = true } = options;
|
||||
const cfg = loadConfig();
|
||||
const cfg = options.config ?? loadConfig();
|
||||
const linkContext = await resolveLinkChannelContext(cfg);
|
||||
const agentList = listAgentsForGateway(cfg);
|
||||
const heartbeatAgents: HeartbeatStatus[] = agentList.agents.map((agent) => {
|
||||
|
||||
Reference in New Issue
Block a user