fix(onboarding): harden live inference handoff (#107041)

* fix(onboarding): harden live inference handoff

* fix(crestodian): preserve auto runtime probes

* fix(ci): restore current main lint gates
This commit is contained in:
Peter Steinberger
2026-07-13 21:15:36 -07:00
committed by GitHub
parent f72e936f26
commit 27f2a45c54
15 changed files with 309 additions and 18 deletions
@@ -131,12 +131,15 @@ function createStartOptions(
return {
transport: "stdio",
command: "codex",
commandSource: "resolved-managed",
args: ["app-server"],
headers: { authorization: "Bearer dev-token" },
...overrides,
};
}
const EPHEMERAL_AUTH_ARGS = ["app-server", "-c", 'cli_auth_credentials_store="ephemeral"'];
async function expectPathMissing(filePath: string): Promise<void> {
try {
await fs.access(filePath);
@@ -226,6 +229,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
}),
).resolves.toEqual({
...startOptions,
args: EPHEMERAL_AUTH_ARGS,
env: {
CODEX_HOME: codexHome,
},
@@ -254,6 +258,56 @@ describe("bridgeCodexAppServerStartOptions", () => {
});
});
it("places the ephemeral auth-store override after configured root overrides", async () => {
await withTempDir("openclaw-codex-auth-store-", async (agentDir) => {
const startOptions = createStartOptions({
args: ["-c", 'cli_auth_credentials_store="keyring"', "app-server"],
});
const bridged = await bridgeCodexAppServerStartOptions({ startOptions, agentDir });
expect(bridged.args).toEqual([
"-c",
'cli_auth_credentials_store="keyring"',
"app-server",
"-c",
'cli_auth_credentials_store="ephemeral"',
]);
});
});
it("does not mistake an option value for the app-server subcommand", async () => {
await withTempDir("openclaw-codex-profile-name-", async (agentDir) => {
const startOptions = createStartOptions({
args: ["--profile", "app-server", "app-server"],
});
const bridged = await bridgeCodexAppServerStartOptions({ startOptions, agentDir });
expect(bridged.args).toEqual([
"--profile",
"app-server",
"app-server",
"-c",
'cli_auth_credentials_store="ephemeral"',
]);
});
});
it("preserves custom stdio backend arguments", async () => {
await withTempDir("openclaw-codex-custom-backend-", async (agentDir) => {
const startOptions = createStartOptions({
command: "custom-codex-compatible-server",
commandSource: "config",
args: [],
});
const bridged = await bridgeCodexAppServerStartOptions({ startOptions, agentDir });
expect(bridged.args).toEqual([]);
});
});
it("preserves inherited HOME when clearEnv asks to clear app-server isolation vars", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
const startOptions = createStartOptions({
@@ -267,6 +321,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
}),
).resolves.toEqual({
...startOptions,
args: EPHEMERAL_AUTH_ARGS,
env: {
CODEX_HOME: resolveCodexAppServerHomeDir(agentDir),
},
@@ -294,6 +349,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
}),
).resolves.toEqual({
...startOptions,
args: EPHEMERAL_AUTH_ARGS,
env: {
CODEX_HOME: codexHome,
HOME: nativeHome,
@@ -336,6 +392,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
}),
).resolves.toEqual({
...startOptions,
args: EPHEMERAL_AUTH_ARGS,
env: {
EXISTING: "1",
CODEX_HOME: resolveCodexAppServerHomeDir(agentDir),
@@ -374,6 +431,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
}),
).resolves.toEqual({
...startOptions,
args: EPHEMERAL_AUTH_ARGS,
env: {
CODEX_HOME: resolveCodexAppServerHomeDir(agentDir),
},
@@ -406,6 +464,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
}),
).resolves.toEqual({
...startOptions,
args: EPHEMERAL_AUTH_ARGS,
env: {
CODEX_HOME: resolveCodexAppServerHomeDir(agentDir),
},
@@ -438,6 +497,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
});
expect(bridged).toEqual({
...startOptions,
args: EPHEMERAL_AUTH_ARGS,
env: { CODEX_HOME: resolveCodexAppServerHomeDir(agentDir) },
clearEnv: ["FOO", "OPENAI_API_KEY", "CODEX_API_KEY", "CODEX_ACCESS_TOKEN"],
});
@@ -608,6 +668,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
}),
).resolves.toEqual({
...startOptions,
args: EPHEMERAL_AUTH_ARGS,
env: {
CODEX_HOME: resolveCodexAppServerHomeDir(agentDir),
},
@@ -20,6 +20,7 @@ import {
type OAuthCredential,
} from "openclaw/plugin-sdk/agent-runtime";
import { hasUsableOAuthCredential } from "openclaw/plugin-sdk/provider-auth";
import { resolveCodexAppServerHomeDir, withEphemeralCodexAuthStore } from "./auth-start-options.js";
import type { CodexAppServerClient } from "./client.js";
import { ensureCodexComputerUseSharedPluginCache } from "./computer-use-cache.js";
import {
@@ -46,7 +47,6 @@ const OPENAI_PROVIDER = "openai";
const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai:default";
const CODEX_HOME_ENV_VAR = "CODEX_HOME";
const HOME_ENV_VAR = "HOME";
const CODEX_APP_SERVER_HOME_DIRNAME = "codex-home";
const CODEX_API_KEY_ENV_VAR = "CODEX_API_KEY";
const OPENAI_API_KEY_ENV_VAR = "OPENAI_API_KEY";
const CODEX_ACCESS_TOKEN_ENV_VAR = "CODEX_ACCESS_TOKEN";
@@ -59,7 +59,6 @@ const CODEX_APP_SERVER_PREPARED_AUTH_ENV_VARS = [
const CODEX_APP_SERVER_HOME_ENV_VARS = [CODEX_HOME_ENV_VAR, HOME_ENV_VAR];
const CODEX_AUTH_JSON_FILENAME = "auth.json";
const CODEX_HOME_DIRNAME = ".codex";
type AuthProfileOrderConfig = Parameters<typeof resolveAuthProfileOrder>[0]["cfg"];
const scopedOAuthRefreshQueues = new WeakMap<
AuthProfileStore,
@@ -79,7 +78,7 @@ export async function bridgeCodexAppServerStartOptions(params: {
return params.startOptions;
}
const scopedStartOptions = await withCodexHomeEnvironment(
params.startOptions,
withEphemeralCodexAuthStore(params),
params.agentDir,
params.pluginConfig,
);
@@ -419,9 +418,7 @@ function fingerprintCodexCliAuthFileApiKeyCacheKey(apiKey: string): string {
return `CODEX_AUTH_JSON:sha256:${hash.digest("hex")}`;
}
export function resolveCodexAppServerHomeDir(agentDir: string): string {
return path.join(path.resolve(agentDir), CODEX_APP_SERVER_HOME_DIRNAME);
}
export { resolveCodexAppServerHomeDir } from "./auth-start-options.js";
async function withCodexHomeEnvironment(
startOptions: CodexAppServerStartOptions,
@@ -0,0 +1,33 @@
import path from "node:path";
import type { CodexAppServerStartOptions } from "./config.js";
const CODEX_APP_SERVER_HOME_DIRNAME = "codex-home";
const CODEX_EPHEMERAL_AUTH_STORE_OVERRIDE = 'cli_auth_credentials_store="ephemeral"';
export function resolveCodexAppServerHomeDir(agentDir: string): string {
return path.join(path.resolve(agentDir), CODEX_APP_SERVER_HOME_DIRNAME);
}
/** Forces OpenClaw-owned Codex auth to remain process-local. */
export function withEphemeralCodexAuthStore(params: {
startOptions: CodexAppServerStartOptions;
preparedAuth?: unknown;
authProfileId?: string | null;
}): CodexAppServerStartOptions {
const { startOptions } = params;
const managedCodexCli =
startOptions.commandSource === "managed" || startOptions.commandSource === "resolved-managed";
if (!managedCodexCli || (!params.preparedAuth && params.authProfileId === null)) {
return startOptions;
}
if (
startOptions.args.at(-2) === "-c" &&
startOptions.args.at(-1) === CODEX_EPHEMERAL_AUTH_STORE_OVERRIDE
) {
return startOptions;
}
return {
...startOptions,
args: [...startOptions.args, "-c", CODEX_EPHEMERAL_AUTH_STORE_OVERRIDE],
};
}
@@ -1663,6 +1663,90 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
);
});
it("binds a harness-owned SecretRef to its exact runtime when auth stays opaque", async () => {
const { clearAgentHarnesses, registerAgentHarness } = await import("../harness/registry.js");
const runtimeArtifact = {
id: "codex-app-server:test",
fingerprint: "codex-runtime-fingerprint",
};
const pluginRunAttempt = vi.fn<AgentHarness["runAttempt"]>(async () =>
makeAttemptResult({
assistantTexts: ["ok"],
runtimeArtifact,
}),
);
const codexAuthStore = {
version: 1 as const,
profiles: {
"openai:work": {
type: "api_key" as const,
provider: "openai",
keyRef: { source: "env" as const, provider: "default", id: "OPENAI_WORK_KEY" },
},
},
};
const onSuccessfulAuthBinding = vi.fn();
clearAgentHarnesses();
registerAgentHarness({
id: "codex",
label: "Codex",
supports: codexHarnessSupportsKnownProviders,
authBootstrap: "harness",
runtimeArtifact: { validate: vi.fn(async () => true) },
runAttempt: pluginRunAttempt,
});
mockedEnsureAuthProfileStore.mockReturnValueOnce(codexAuthStore);
mockedEnsureAuthProfileStoreWithoutExternalProfiles.mockReturnValueOnce(codexAuthStore);
mockedResolveModelAsync.mockResolvedValueOnce({
model: {
id: "gpt-5.4",
provider: "openai",
contextWindow: 200000,
api: "openai-chatgpt-responses",
},
error: null,
authStorage: { setRuntimeApiKey: vi.fn() },
modelRegistry: {},
});
try {
await runEmbeddedAgent({
...overflowBaseRunParams,
provider: "openai",
model: "gpt-5.4",
config: {
agents: { defaults: { agentRuntime: { id: "codex" } } },
auth: {
profiles: {
"openai:work": { provider: "openai", mode: "api_key" },
},
},
},
authProfileId: "openai:work",
authProfileIdSource: "user",
runId: "harness-secretref-runtime-owner-binding",
onSuccessfulAuthBinding,
} as RunEmbeddedAgentParams & {
onSuccessfulAuthBinding: typeof onSuccessfulAuthBinding;
});
} finally {
clearAgentHarnesses();
}
expect(pluginRunAttempt).toHaveBeenCalledWith(
expect.objectContaining({ captureRuntimeArtifact: true }),
);
expect(onSuccessfulAuthBinding).toHaveBeenCalledWith({
authProfileId: "openai:work",
agentHarnessId: "codex",
runtimeOwnerFingerprint: expect.any(String),
runtimeOwnerKind: "plugin-harness",
runtimeOwnerId: "codex",
runtimeArtifactId: runtimeArtifact.id,
runtimeArtifactFingerprint: runtimeArtifact.fingerprint,
});
});
it("bootstraps OAuth credentials for forced openai/* Codex response runs", async () => {
const { clearAgentHarnesses, registerAgentHarness } = await import("../harness/registry.js");
const pluginRunAttempt = vi.fn<AgentHarness["runAttempt"]>(async () =>
+1 -2
View File
@@ -4181,8 +4181,7 @@ async function runEmbeddedAgentInternal(
? fingerprintResolvedProviderAuth(successfulApiKeyInfo)
: undefined;
const authProfileOwnerFingerprint =
successfulProfileId &&
(!pluginHarnessOwnsTransport || successfulCredential?.type === "oauth")
successfulProfileId && successfulCredential !== undefined
? fingerprintAuthProfileOwnerShape({
profileId: successfulProfileId,
credential: successfulCredential,
+18
View File
@@ -0,0 +1,18 @@
const UNCONFIGURED_CONFIG_IGNORED_KEYS = new Set(["$schema", "meta"]);
function isIncompleteWizardConfig(value: unknown): boolean {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.keys(value).every((key) => key === "securityAcknowledgedAt")
);
}
export function isUnconfiguredConfigSource(sourceConfig: Record<string, unknown>): boolean {
return Object.entries(sourceConfig).every(
([key, value]) =>
UNCONFIGURED_CONFIG_IGNORED_KEYS.has(key) ||
(key === "wizard" && isIncompleteWizardConfig(value)),
);
}
+19
View File
@@ -2713,6 +2713,25 @@ describe("runCli exit behavior", () => {
expect(buildProgramMock).not.toHaveBeenCalled();
});
it("resumes onboarding when an interrupted first run only persisted risk acknowledgement", async () => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
exists: true,
valid: true,
sourceConfig: {
meta: { updatedBy: "fixture" },
wizard: { securityAcknowledgedAt: "2026-07-13T00:00:00.000Z" },
},
});
await withInteractiveTty(async () => {
await runCli(["node", "openclaw"]);
});
expect(setupWizardCommandMock).toHaveBeenCalledWith({});
expect(tryRouteCliMock).not.toHaveBeenCalled();
expect(buildProgramMock).not.toHaveBeenCalled();
});
it("points noninteractive fresh bare root invocations to onboarding automation", async () => {
const previousExitCode = process.exitCode;
const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
+2 -5
View File
@@ -27,6 +27,7 @@ import {
shouldSkipPluginCommandRegistration,
} from "./command-registration-policy.js";
import { maybeRunCliInContainer, parseCliContainerArgs } from "./container-target.js";
import { isUnconfiguredConfigSource } from "./fresh-install-config.js";
import {
consumeGatewayFastPathRootOptionToken,
consumeGatewayRunOptionToken,
@@ -269,8 +270,6 @@ async function disposeCliAgentHarnesses(): Promise<void> {
}
}
const UNCONFIGURED_CONFIG_IGNORED_KEYS = new Set(["$schema", "meta"]);
function isUnconfiguredConfigSnapshot(
snapshot: Pick<ConfigFileSnapshot, "exists" | "valid" | "sourceConfig">,
): boolean {
@@ -280,9 +279,7 @@ function isUnconfiguredConfigSnapshot(
if (!snapshot.valid) {
return false;
}
return Object.keys(snapshot.sourceConfig).every((key) =>
UNCONFIGURED_CONFIG_IGNORED_KEYS.has(key),
);
return isUnconfiguredConfigSource(snapshot.sourceConfig);
}
export async function shouldStartOnboardingForFreshInstall(argv: string[]): Promise<boolean> {
@@ -157,6 +157,29 @@ describe("Codex runtime plugin install repair", () => {
});
});
it("allows source checkouts to use the matching bundled Codex plugin", async () => {
const { ensureCodexRuntimePluginForModelSelection } =
await import("./codex-runtime-plugin-install.js");
await ensureCodexRuntimePluginForModelSelection({
cfg: {},
model: "openai/gpt-5.5",
prompter: {} as never,
runtime: {} as never,
});
expect(mocks.ensureOnboardingPluginInstalled).toHaveBeenCalledWith(
expect.objectContaining({
entry: {
pluginId: "codex",
label: "Codex",
install: { npmSpec: "@openclaw/codex", defaultChoice: "npm" },
trustedSourceLinkedOfficialInstall: true,
},
}),
);
});
it("sees an agent-scoped Codex runtime pin behind a custom OpenAI route", async () => {
mocks.loadInstalledPluginIndexInstallRecords.mockResolvedValue({
codex: { source: "npm", installPath: process.cwd() },
+18
View File
@@ -103,6 +103,7 @@ function setupDeps(params: {
detect?: GuidedOnboardingDeps["detect"];
activate?: GuidedOnboardingDeps["activate"];
runCrestodianChat?: GuidedOnboardingDeps["runCrestodianChat"];
persistRiskAcknowledgement?: GuidedOnboardingDeps["persistRiskAcknowledgement"];
}) {
const runCrestodianChat = vi.fn<NonNullable<GuidedOnboardingDeps["runCrestodianChat"]>>(
params.runCrestodianChat ?? (async () => {}),
@@ -118,6 +119,7 @@ function setupDeps(params: {
latencyMs: 1250,
lines: ["Workspace: /tmp/work", "Gateway: running"],
})),
persistRiskAcknowledgement: params.persistRiskAcknowledgement ?? vi.fn(async () => undefined),
runCrestodianChat,
} satisfies GuidedOnboardingDeps;
}
@@ -178,6 +180,22 @@ describe("runGuidedOnboarding", () => {
);
});
it("persists the one-time risk acknowledgement before inference detection", async () => {
const prompter = createWizardPrompter();
const persistRiskAcknowledgement = vi.fn(async () => undefined);
const detect = vi.fn(async () => detection());
const deps = setupDeps({ prompter, persistRiskAcknowledgement, detect });
await runGuidedOnboarding({ acceptRisk: true }, makeRuntime(), deps);
expect(persistRiskAcknowledgement).toHaveBeenCalledWith({
wizard: { securityAcknowledgedAt: expect.any(String) },
});
expect(persistRiskAcknowledgement.mock.invocationCallOrder[0]).toBeLessThan(
detect.mock.invocationCallOrder[0]!,
);
});
it("uses the configured workspace only as inference and Crestodian context", async () => {
readConfigFileSnapshot.mockResolvedValueOnce({
exists: true,
+26 -2
View File
@@ -34,6 +34,7 @@ export type GuidedOnboardingDeps = {
acceptRisk: boolean,
) => Promise<void>;
createPrompter?: () => WizardPrompter | Promise<WizardPrompter>;
persistRiskAcknowledgement?: (config: OpenClawConfig) => Promise<void>;
};
type GuidedOnboardingHandoff = { workspace: string };
@@ -270,6 +271,22 @@ function activationLines(result: Extract<ActivateSetupInferenceResult, { ok: tru
];
}
async function persistRiskAcknowledgement(config: OpenClawConfig): Promise<void> {
const securityAcknowledgedAt = config.wizard?.securityAcknowledgedAt;
if (!securityAcknowledgedAt) {
return;
}
const { mutateConfigFileWithRetry } = await import("../config/config.js");
await mutateConfigFileWithRetry({
mutate: (draft) => {
if (draft.wizard?.securityAcknowledgedAt) {
return;
}
draft.wizard = { ...draft.wizard, securityAcknowledgedAt };
},
});
}
async function runGuidedOnboardingFlow(
opts: OnboardOptions,
runtime: RuntimeEnv,
@@ -307,14 +324,21 @@ async function runGuidedOnboardingFlow(
}
const existingConfig =
snapshot.exists && snapshot.valid ? (snapshot.sourceConfig ?? snapshot.config) : {};
await requireRiskAcknowledgement({ opts, prompter, config: existingConfig });
const acknowledgedConfig = await requireRiskAcknowledgement({
opts,
prompter,
config: existingConfig,
});
if (!existingConfig.wizard?.securityAcknowledgedAt) {
await (deps.persistRiskAcknowledgement ?? persistRiskAcknowledgement)(acknowledgedConfig);
}
// Inference is the only prerequisite for Crestodian. Use the caller's or
// current default workspace as isolated probe context; Crestodian owns any
// workspace choice and persistence after the live completion succeeds.
const workspace = resolveUserPath(
opts.workspace?.trim() ||
existingConfig.agents?.defaults?.workspace?.trim() ||
acknowledgedConfig.agents?.defaults?.workspace?.trim() ||
onboardHelpers.DEFAULT_WORKSPACE,
);
-1
View File
@@ -139,7 +139,6 @@ async function ensureRuntimePluginForModelSelection(params: {
defaultChoice: "npm",
},
trustedSourceLinkedOfficialInstall: true,
preferRemoteInstall: true,
},
prompter: params.prompter,
runtime: params.runtime,
+10
View File
@@ -0,0 +1,10 @@
const SETUP_INFERENCE_TEST_MAX_TOKENS = 32;
/** Plugin and auto-selected harnesses may not support OpenClaw's request-scoped token cap. */
export function resolveSetupInferenceProbeStreamParams(agentHarnessId?: string): {
streamParams?: { maxTokens: number };
} {
return !agentHarnessId || agentHarnessId === "openclaw"
? { streamParams: { maxTokens: SETUP_INFERENCE_TEST_MAX_TOKENS } }
: {};
}
+9
View File
@@ -39,6 +39,7 @@ import {
} from "./agent-turn.js";
import { resolveCrestodianConfiguredRouteFromConfig } from "./inference-route.js";
import { applyCrestodianModelSelection } from "./setup-apply.js";
import { resolveSetupInferenceProbeStreamParams } from "./setup-inference-probe.js";
import {
SetupInferenceActivationIndeterminateError,
activateSetupInference as activateSetupInferenceImpl,
@@ -572,6 +573,13 @@ async function runCodexSetupWithFinalConfig(params: {
}
describe("activateSetupInference", () => {
it("omits the token cap when harness selection is automatic", () => {
expect(resolveSetupInferenceProbeStreamParams("auto")).toEqual({});
expect(resolveSetupInferenceProbeStreamParams("openclaw")).toEqual({
streamParams: { maxTokens: 32 },
});
});
beforeEach(() => {
mocks.appendAudit.mockReset();
mocks.ensureSelectedAgentHarnessPlugin.mockReset().mockResolvedValue(undefined);
@@ -3390,6 +3398,7 @@ describe("activateSetupInference", () => {
expect(runEmbeddedAgent.mock.calls[0]?.[0]).toMatchObject({
agentHarnessRuntimeOverride: "codex",
});
expect(runEmbeddedAgent.mock.calls[0]?.[0]).not.toHaveProperty("streamParams");
expect(persistedConfig).toMatchObject({
gateway: { port: 19000 },
models: {
+2 -2
View File
@@ -74,6 +74,7 @@ import {
createCrestodianModelSelectionUpdater,
createQuickstartNotePrompter,
} from "./setup-apply.js";
import { resolveSetupInferenceProbeStreamParams } from "./setup-inference-probe.js";
import {
captureCrestodianOwnerPluginArtifacts,
createCrestodianVerifiedInferenceBinding,
@@ -95,7 +96,6 @@ const log = createSubsystemLogger("crestodian/setup-inference");
*/
export const SETUP_INFERENCE_TEST_TIMEOUT_MS = 90_000;
const SETUP_INFERENCE_TEST_PROMPT = "Reply with the single word OK. Do not use tools.";
const SETUP_INFERENCE_TEST_MAX_TOKENS = 32;
export type SetupInferenceCandidate = {
kind: InferenceBackendKind;
@@ -2880,7 +2880,7 @@ async function runSetupInferenceTest(params: {
thinkLevel: "off",
reasoningLevel: "off",
verboseLevel: "off",
streamParams: { maxTokens: SETUP_INFERENCE_TEST_MAX_TOKENS },
...resolveSetupInferenceProbeStreamParams(plan.agentHarnessRuntimeOverride),
disableTools: true,
modelRun: true,
messageChannel: "crestodian",