feat: use OpenClaw onboard for live auth

This commit is contained in:
Shakker 2026-05-01 10:21:41 +01:00
parent e1583a4d1a
commit b35ebb7934
No known key found for this signature in database
5 changed files with 200 additions and 9 deletions

View File

@ -91,8 +91,11 @@ the scenario/state explicitly tests missing or broken auth. `--auth mock` is the
default and uses Kova's deterministic local OpenAI-compatible provider.
`--auth live` requires credentials configured through `kova setup`; live results
are marked environment-dependent and should be compared separately from mock
baselines. Kova's live auth setup patches disposable env config as fixture setup
for runtime validation; it is not proof that OpenClaw onboarding/auth UX passed.
baselines. For supported API-key/env-only providers, Kova configures live auth
through OpenClaw's own non-interactive `onboard` path with env-backed
SecretRefs. Live paths without a stable OpenClaw command path are labeled
fixture setup and must not be cited as proof that OpenClaw onboarding/auth UX
passed.
`plan --json` is coverage-aware: scenarios map to declared OpenClaw surfaces,
surfaces declare process roles and required metrics, and profile coverage gaps

View File

@ -69,8 +69,10 @@ setup passes. `openai + external-cli` uses Codex CLI; `anthropic + external-cli`
uses Claude CLI. External CLI fallback is only valid when setup explicitly
selected `--fallback-policy external-cli`. Use API-key or env-only auth for
`custom-openai`.
Live auth setup patches disposable env config as fixture setup for runtime
validation; do not cite it as proof that OpenClaw onboarding/auth UX passed.
For supported API-key/env-only providers, live auth setup runs OpenClaw's own
non-interactive `onboard` path with env-backed SecretRefs. Live auth paths that
do not expose a stable OpenClaw command path are labeled fixture setup; do not
cite those runs as proof that OpenClaw onboarding/auth UX passed.
4. Execute one scenario explicitly:

View File

@ -137,6 +137,17 @@ Executed phases include:
Successful command stdout/stderr may be present in JSON but should not be pasted
by agents unless it explains a failure.
## Auth Evidence
Record `auth.setupKind` states how Kova configured model auth for the disposable
OpenClaw env:
- `openclaw-onboard`: Kova used OpenClaw's own non-interactive `onboard`
command, normally with env-backed SecretRefs for API-key/env-only providers.
- `fixture-config-patch`: Kova patched disposable env config directly for a
live path that has no stable non-interactive OpenClaw command path. Treat this
as runtime validation only, not proof that OpenClaw onboarding/auth UX passed.
## Metrics
Metrics use explicit collector result contracts. The top-level metrics object

View File

@ -129,7 +129,7 @@ export function scenarioAuthPolicy(context, scenario, state) {
fallbackFrom: live.fallbackFrom ?? null,
fallbackPolicy: live.fallbackPolicy ?? null,
setup: true,
setupKind: "fixture-config-patch",
setupKind: liveAuthSetupKind(live),
commandEnv: env,
redactionValues: [...(context.auth?.redactionValues ?? []), ...secretValues(env)],
summary: authDisplay({
@ -140,7 +140,7 @@ export function scenarioAuthPolicy(context, scenario, state) {
fallbackFrom: live.fallbackFrom ?? null,
fallbackPolicy: live.fallbackPolicy ?? null,
setup: true,
setupKind: "fixture-config-patch",
setupKind: liveAuthSetupKind(live),
envVars: live.envVars
})
};
@ -199,9 +199,9 @@ export function buildAuthSetupPhase(authPolicy, envName, artifactDir) {
return {
id: "auth-setup",
title: "Auth Setup",
intent: "Patch the disposable OpenClaw env with fixture live auth config; this proves runtime behavior, not OpenClaw onboarding/auth UX.",
intent: liveAuthSetupIntent(authPolicy),
commands: [configureLiveAuthCommand(authPolicy, envName)],
evidence: ["fixture auth config applied", "OpenClaw config references live auth env vars or selected external CLI", "live auth is environment-dependent"]
evidence: liveAuthSetupEvidence(authPolicy)
};
}
@ -593,6 +593,9 @@ function configureMockAuthCommand(envName, dir) {
}
function configureLiveAuthCommand(authPolicy, envName) {
if (authPolicy.setupKind === "openclaw-onboard") {
return configureLiveAuthViaOpenClawOnboardCommand(authPolicy, envName);
}
const envVar = authPolicy.summary.envVars?.[0] ?? defaultEnvVarForProvider(authPolicy.providerId);
const externalCliArgs = authPolicy.source === "external-cli" && authPolicy.externalCli
? ` --auth-method external-cli --external-cli ${quoteShell(authPolicy.externalCli)}`
@ -600,6 +603,77 @@ function configureLiveAuthCommand(authPolicy, envName) {
return `ocm env exec ${quoteShell(envName)} -- node ${quoteShell(join(repoRoot, "support/configure-openclaw-live-auth.mjs"))} --provider ${quoteShell(authPolicy.providerId)} --env-var ${quoteShell(envVar)}${externalCliArgs}`;
}
function configureLiveAuthViaOpenClawOnboardCommand(authPolicy, envName) {
const onboard = liveOnboardConfig(authPolicy);
const args = [
"onboard",
"--non-interactive",
"--accept-risk",
"--mode", "local",
"--auth-choice", onboard.authChoice,
"--skip-health",
"--skip-ui",
"--skip-search",
"--skip-skills",
"--skip-channels",
"--skip-bootstrap",
"--no-install-daemon",
"--json"
];
if (onboard.secretInputMode) {
args.push("--secret-input-mode", onboard.secretInputMode);
}
return `ocm @${quoteShell(envName)} -- ${args.map(quoteShell).join(" ")}`;
}
function liveAuthSetupKind(live) {
if (live.method === "api-key" || live.method === "env-only") {
if (live.providerId === "openai" || live.providerId === "anthropic") {
return "openclaw-onboard";
}
}
if (live.method === "external-cli" && live.providerId === "anthropic") {
return "openclaw-onboard";
}
return "fixture-config-patch";
}
function liveAuthSetupIntent(authPolicy) {
if (authPolicy.setupKind === "openclaw-onboard") {
return "Configure the disposable OpenClaw env through OpenClaw's own non-interactive onboarding/auth path using env-backed SecretRefs where applicable.";
}
return "Patch the disposable OpenClaw env with fixture live auth config; this proves runtime behavior, not OpenClaw onboarding/auth UX.";
}
function liveAuthSetupEvidence(authPolicy) {
if (authPolicy.setupKind === "openclaw-onboard") {
return ["OpenClaw onboard command completed", "OpenClaw config references live auth env vars or selected external CLI", "live auth is environment-dependent"];
}
return ["fixture auth config applied", "OpenClaw config references live auth env vars or selected external CLI", "live auth is environment-dependent"];
}
function liveOnboardConfig(authPolicy) {
if (authPolicy.source === "external-cli" && authPolicy.providerId === "anthropic") {
return {
authChoice: "anthropic-cli",
secretInputMode: null
};
}
if (authPolicy.providerId === "openai") {
return {
authChoice: "openai-api-key",
secretInputMode: "ref"
};
}
if (authPolicy.providerId === "anthropic") {
return {
authChoice: "apiKey",
secretInputMode: "ref"
};
}
throw new Error(`provider ${authPolicy.providerId} does not have a supported OpenClaw non-interactive live auth setup path`);
}
function defaultEnvVarForProvider(providerId) {
if (providerId === "anthropic") {
return "ANTHROPIC_API_KEY";

View File

@ -70,6 +70,7 @@ export async function runSelfCheck(flags = {}) {
checks.push(await claudeCliOpenClawConfigCheck(tmp));
checks.push(await liveApiKeyExecutionCheck(tmp));
checks.push(await liveExternalCliDryRunCheck(tmp));
checks.push(await liveAnthropicExternalCliDryRunCheck(tmp));
checks.push(await liveExternalCliFallbackCheck(tmp));
checks.push(await failingCommandCheck(
"setup-custom-provider-rejects-external-cli",
@ -1047,6 +1048,7 @@ async function liveApiKeyExecutionCheck(tmp) {
assertEqual(report.auth?.live?.environmentDependent, true, "top-level live env-dependent flag");
assertEqual(record?.auth?.mode, "live", "record live auth mode");
assertEqual(record?.auth?.source, "api-key", "record live auth source");
assertEqual(record?.auth?.setupKind, "openclaw-onboard", "record live setup kind");
assertEqual(record?.auth?.environmentDependent, true, "record live env-dependent flag");
assertEqual(record?.auth?.secretValues, "redacted", "record secret values redacted");
assertEqual(record?.providerEvidence?.environmentDependent, true, "provider evidence live env-dependent flag");
@ -1127,6 +1129,7 @@ async function liveExternalCliDryRunCheck(tmp) {
assertEqual(record?.auth?.mode, "live", "external cli record live mode");
assertEqual(record?.auth?.source, "external-cli", "external cli record source");
assertEqual(record?.auth?.externalCli, "codex", "external cli record name");
assertEqual(record?.auth?.setupKind, "fixture-config-patch", "codex cli fixture setup kind");
const authSetupCommand = record.phases
?.flatMap((phase) => phase.commands ?? [])
?.find((item) => item.includes("configure-openclaw-live-auth.mjs")) ?? "";
@ -1215,6 +1218,76 @@ async function liveExternalCliFallbackCheck(tmp) {
}
}
async function liveAnthropicExternalCliDryRunCheck(tmp) {
const home = join(tmp, "live-anthropic-cli-home");
const kovaHome = join(tmp, "live-anthropic-cli-kova-home");
const fakeBin = join(tmp, "live-anthropic-cli-bin");
const reportDir = join(tmp, "live-anthropic-cli-report");
await mkdir(join(home, ".claude"), { recursive: true });
await mkdir(join(kovaHome, "credentials"), { recursive: true });
await mkdir(fakeBin, { recursive: true });
await writeFile(join(home, ".claude", ".credentials.json"), "{\"claudeAiOauth\":{\"accessToken\":\"redacted\"}}\n", "utf8");
await writeFile(join(fakeBin, "claude"), "#!/bin/sh\necho claude-selfcheck\n", "utf8");
await chmod(join(fakeBin, "claude"), 0o755);
await writeFile(join(kovaHome, "credentials", "providers.json"), `${JSON.stringify({
schemaVersion: "kova.credentials.providers.v1",
defaultProvider: "anthropic",
providers: {
anthropic: {
id: "anthropic",
method: "external-cli",
envVars: [],
externalCli: "claude",
fallbackPolicy: "mock",
configuredAt: new Date().toISOString()
}
}
}, null, 2)}\n`, "utf8");
await writeFile(join(kovaHome, "credentials", "live.env"), "", { encoding: "utf8", mode: 0o600 });
const command = [
`HOME=${quoteShell(home)}`,
`PATH=${quoteShell(`${fakeBin}:${process.env.PATH}`)}`,
`KOVA_HOME=${quoteShell(kovaHome)}`,
`node bin/kova.mjs run --target runtime:stable --scenario fresh-install --auth live --report-dir ${quoteShell(reportDir)} --json`
].join(" ");
const result = await runCommand(command, { timeoutMs: 30000, maxOutputChars: 1000000 });
try {
if (result.status !== 0) {
throw new Error(result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`);
}
const receipt = JSON.parse(result.stdout);
const report = JSON.parse(await readFile(receipt.jsonPath, "utf8"));
const record = report.records?.[0];
assertEqual(report.auth?.live?.method, "external-cli", "anthropic external cli live method");
assertEqual(report.auth?.live?.externalCli, "claude", "anthropic external cli name");
assertEqual(record?.auth?.mode, "live", "anthropic cli record live mode");
assertEqual(record?.auth?.providerId, "anthropic", "anthropic cli provider");
assertEqual(record?.auth?.setupKind, "openclaw-onboard", "anthropic cli onboard setup");
const authSetupCommand = record.phases
?.flatMap((phase) => phase.commands ?? [])
?.find((item) => item.includes("onboard")) ?? "";
if (!authSetupCommand.includes("--auth-choice") || !authSetupCommand.includes("anthropic-cli")) {
throw new Error(`anthropic external-cli auth setup command missing OpenClaw onboard path: ${authSetupCommand}`);
}
return {
id: "live-anthropic-external-cli-dry-run",
status: "PASS",
command,
durationMs: result.durationMs
};
} catch (error) {
return {
id: "live-anthropic-external-cli-dry-run",
status: "FAIL",
command,
durationMs: result.durationMs,
message: error.message
};
}
}
function fakeOcmScript() {
return `#!/bin/sh
printf '%s\\n' "$*" >> "$KOVA_MOCK_OCM_LOG"
@ -1231,7 +1304,35 @@ esac
case "$1" in
start) echo '{"ok":true}'; exit 0 ;;
logs) exit 0 ;;
@*) echo "live command key=$OPENAI_API_KEY"; exit 0 ;;
@*)
env_name="$1"
shift
if [ "$1" = "--" ]; then shift; fi
if [ "$1" = "onboard" ]; then
mkdir -p "$KOVA_FAKE_OPENCLAW_HOME/.openclaw"
case " $* " in
*" --auth-choice openai-api-key "*)
cat > "$KOVA_FAKE_OPENCLAW_HOME/.openclaw/openclaw.json" <<'JSON'
{"models":{"mode":"merge","providers":{"openai":{"apiKey":{"source":"env","provider":"default","id":"OPENAI_API_KEY"},"models":[{"id":"gpt-5.5","name":"gpt-5.5","api":"openai-responses"}]}}},"agents":{"defaults":{"model":{"primary":"openai/gpt-5.5"}}}}
JSON
;;
*" --auth-choice apiKey "*)
cat > "$KOVA_FAKE_OPENCLAW_HOME/.openclaw/openclaw.json" <<'JSON'
{"models":{"mode":"merge","providers":{"anthropic":{"apiKey":{"source":"env","provider":"default","id":"ANTHROPIC_API_KEY"},"models":[{"id":"claude-sonnet-4-5","name":"claude-sonnet-4-5"}]}}},"agents":{"defaults":{"model":{"primary":"anthropic/claude-sonnet-4-5"}}}}
JSON
;;
*" --auth-choice anthropic-cli "*)
cat > "$KOVA_FAKE_OPENCLAW_HOME/.openclaw/openclaw.json" <<'JSON'
{"agents":{"defaults":{"model":{"primary":"claude-cli/claude-sonnet-4-5"},"agentRuntime":{"id":"claude-cli","fallback":"none"}}}}
JSON
;;
esac
echo '{"ok":true}'
exit 0
fi
echo "live command key=$OPENAI_API_KEY"
exit 0
;;
--version) echo 'mock-ocm'; exit 0 ;;
esac
echo "unhandled mock ocm command: $*" >&2