From 27f2a45c5407ab28a21e2efe3286e0721606b0c5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 13 Jul 2026 21:15:36 -0700 Subject: [PATCH] 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 --- .../codex/src/app-server/auth-bridge.test.ts | 61 ++++++++++++++ .../codex/src/app-server/auth-bridge.ts | 9 +- .../src/app-server/auth-start-options.ts | 33 ++++++++ .../run.overflow-compaction.test.ts | 84 +++++++++++++++++++ src/agents/embedded-agent-runner/run.ts | 3 +- src/cli/fresh-install-config.ts | 18 ++++ src/cli/run-main.exit.test.ts | 19 +++++ src/cli/run-main.ts | 7 +- .../codex-runtime-plugin-install.test.ts | 23 +++++ src/commands/onboard-guided.test.ts | 18 ++++ src/commands/onboard-guided.ts | 28 ++++++- src/commands/runtime-plugin-install.ts | 1 - src/crestodian/setup-inference-probe.ts | 10 +++ src/crestodian/setup-inference.test.ts | 9 ++ src/crestodian/setup-inference.ts | 4 +- 15 files changed, 309 insertions(+), 18 deletions(-) create mode 100644 extensions/codex/src/app-server/auth-start-options.ts create mode 100644 src/cli/fresh-install-config.ts create mode 100644 src/crestodian/setup-inference-probe.ts diff --git a/extensions/codex/src/app-server/auth-bridge.test.ts b/extensions/codex/src/app-server/auth-bridge.test.ts index c813b7864836..fe67fe52cea1 100644 --- a/extensions/codex/src/app-server/auth-bridge.test.ts +++ b/extensions/codex/src/app-server/auth-bridge.test.ts @@ -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 { 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), }, diff --git a/extensions/codex/src/app-server/auth-bridge.ts b/extensions/codex/src/app-server/auth-bridge.ts index 2b7eea73c655..32a5dcc47a51 100644 --- a/extensions/codex/src/app-server/auth-bridge.ts +++ b/extensions/codex/src/app-server/auth-bridge.ts @@ -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[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, diff --git a/extensions/codex/src/app-server/auth-start-options.ts b/extensions/codex/src/app-server/auth-start-options.ts new file mode 100644 index 000000000000..d3f0abaa825b --- /dev/null +++ b/extensions/codex/src/app-server/auth-start-options.ts @@ -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], + }; +} diff --git a/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts b/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts index bff39d31dd8e..8623cd5ff0e7 100644 --- a/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts +++ b/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts @@ -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(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(async () => diff --git a/src/agents/embedded-agent-runner/run.ts b/src/agents/embedded-agent-runner/run.ts index 72e4f00d2bad..45ebcdb62656 100644 --- a/src/agents/embedded-agent-runner/run.ts +++ b/src/agents/embedded-agent-runner/run.ts @@ -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, diff --git a/src/cli/fresh-install-config.ts b/src/cli/fresh-install-config.ts new file mode 100644 index 000000000000..e2ba1394e2cc --- /dev/null +++ b/src/cli/fresh-install-config.ts @@ -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): boolean { + return Object.entries(sourceConfig).every( + ([key, value]) => + UNCONFIGURED_CONFIG_IGNORED_KEYS.has(key) || + (key === "wizard" && isIncompleteWizardConfig(value)), + ); +} diff --git a/src/cli/run-main.exit.test.ts b/src/cli/run-main.exit.test.ts index d5671c03b289..762abbb3238c 100644 --- a/src/cli/run-main.exit.test.ts +++ b/src/cli/run-main.exit.test.ts @@ -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"); diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index 93beffec901a..740abc45f258 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -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 { } } -const UNCONFIGURED_CONFIG_IGNORED_KEYS = new Set(["$schema", "meta"]); - function isUnconfiguredConfigSnapshot( snapshot: Pick, ): 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 { diff --git a/src/commands/codex-runtime-plugin-install.test.ts b/src/commands/codex-runtime-plugin-install.test.ts index a48957b2fac0..9d3293c43a8e 100644 --- a/src/commands/codex-runtime-plugin-install.test.ts +++ b/src/commands/codex-runtime-plugin-install.test.ts @@ -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() }, diff --git a/src/commands/onboard-guided.test.ts b/src/commands/onboard-guided.test.ts index 0000eb2ed9aa..1f2be4ccef48 100644 --- a/src/commands/onboard-guided.test.ts +++ b/src/commands/onboard-guided.test.ts @@ -103,6 +103,7 @@ function setupDeps(params: { detect?: GuidedOnboardingDeps["detect"]; activate?: GuidedOnboardingDeps["activate"]; runCrestodianChat?: GuidedOnboardingDeps["runCrestodianChat"]; + persistRiskAcknowledgement?: GuidedOnboardingDeps["persistRiskAcknowledgement"]; }) { const runCrestodianChat = vi.fn>( 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, diff --git a/src/commands/onboard-guided.ts b/src/commands/onboard-guided.ts index ad33789447ac..9504197ee289 100644 --- a/src/commands/onboard-guided.ts +++ b/src/commands/onboard-guided.ts @@ -34,6 +34,7 @@ export type GuidedOnboardingDeps = { acceptRisk: boolean, ) => Promise; createPrompter?: () => WizardPrompter | Promise; + persistRiskAcknowledgement?: (config: OpenClawConfig) => Promise; }; type GuidedOnboardingHandoff = { workspace: string }; @@ -270,6 +271,22 @@ function activationLines(result: Extract { + 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, ); diff --git a/src/commands/runtime-plugin-install.ts b/src/commands/runtime-plugin-install.ts index 6bc508df8ceb..08ce7e0a9cd7 100644 --- a/src/commands/runtime-plugin-install.ts +++ b/src/commands/runtime-plugin-install.ts @@ -139,7 +139,6 @@ async function ensureRuntimePluginForModelSelection(params: { defaultChoice: "npm", }, trustedSourceLinkedOfficialInstall: true, - preferRemoteInstall: true, }, prompter: params.prompter, runtime: params.runtime, diff --git a/src/crestodian/setup-inference-probe.ts b/src/crestodian/setup-inference-probe.ts new file mode 100644 index 000000000000..b56529dd98eb --- /dev/null +++ b/src/crestodian/setup-inference-probe.ts @@ -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 } } + : {}; +} diff --git a/src/crestodian/setup-inference.test.ts b/src/crestodian/setup-inference.test.ts index baef58003601..b466e06b7b15 100644 --- a/src/crestodian/setup-inference.test.ts +++ b/src/crestodian/setup-inference.test.ts @@ -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: { diff --git a/src/crestodian/setup-inference.ts b/src/crestodian/setup-inference.ts index 783afbb64e00..3f821474904e 100644 --- a/src/crestodian/setup-inference.ts +++ b/src/crestodian/setup-inference.ts @@ -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",