feat: use OpenClaw onboard for live auth
This commit is contained in:
parent
e1583a4d1a
commit
b35ebb7934
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
82
src/auth.mjs
82
src/auth.mjs
@ -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";
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user