mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: install Codex for OpenAI onboarding (#79709)
* fix: install codex for openai onboarding * docs: require crabbox e2e bug proof
This commit is contained in:
committed by
GitHub
parent
8ff6707e85
commit
5849c32402
@@ -131,6 +131,55 @@ unclear:
|
||||
blacksmith testbox list
|
||||
```
|
||||
|
||||
## Efficient Bug E2E Verification
|
||||
|
||||
Use the smallest Crabbox lane that proves the reported user path, not just the
|
||||
touched code. Aim for one after-fix E2E proof before commenting, closing, or
|
||||
opening a PR for a user-visible bug.
|
||||
|
||||
Pick the lane by symptom:
|
||||
|
||||
- Docker/setup/install bug: build a package tarball and run the matching
|
||||
`scripts/e2e/*-docker.sh` or package script. This proves npm packaging,
|
||||
install paths, runtime deps, config writes, and container behavior.
|
||||
- Provider/model/auth bug: use a live lane when a `.profile`/Testbox profile key
|
||||
is available; otherwise use the repo's mock provider lane and state clearly
|
||||
that live provider auth was not exercised.
|
||||
- Channel delivery bug: use the channel Docker/live lane when available; include
|
||||
setup, config, gateway start, send/receive or agent-turn proof, and redacted
|
||||
logs.
|
||||
- Gateway/session/tool bug: prefer an end-to-end CLI or Gateway RPC command that
|
||||
creates real state and inspects the resulting files/API output.
|
||||
- Pure parser/config bug: targeted tests may be enough, but still run a
|
||||
Crabbox command when OS, package, Docker, secrets, or service lifecycle could
|
||||
change behavior.
|
||||
|
||||
Efficient flow:
|
||||
|
||||
1. Reproduce or prove the pre-fix symptom when feasible. If the issue cannot be
|
||||
reproduced, capture the exact command and observed behavior instead.
|
||||
2. Patch locally and run narrow local tests for edit speed.
|
||||
3. Run one Crabbox E2E command that starts from the user-facing entrypoint:
|
||||
package install, Docker setup, onboarding, channel add, gateway start, or
|
||||
agent turn as appropriate.
|
||||
4. Record proof as: Testbox id, command, environment shape, redacted secret
|
||||
source, and copied success/failure output.
|
||||
5. If the issue says "cannot reproduce", ask for the missing config/log fields
|
||||
that would distinguish the tested path from the reporter's path.
|
||||
|
||||
Keep it efficient:
|
||||
|
||||
- Reuse existing E2E scripts and helper assertions before writing ad hoc shell.
|
||||
- Use one-shot Crabbox for a single proof; use a reusable Testbox only when
|
||||
several commands must share built images, installed packages, or live state.
|
||||
- Prefer `OPENCLAW_CURRENT_PACKAGE_TGZ` with Docker/package lanes when testing a
|
||||
candidate tarball; prefer the repo's package helper instead of direct source
|
||||
execution when the bug might be packaging/install related.
|
||||
- Keep secrets redacted. It is fine to report key presence, source, and length;
|
||||
never print secret values.
|
||||
- Include `--timing-json` on broad or flaky runs when command duration or sync
|
||||
behavior matters.
|
||||
|
||||
## Reuse And Keepalive
|
||||
|
||||
For most Blacksmith-backed Crabbox calls, one-shot is enough. Use reuse only
|
||||
|
||||
@@ -132,6 +132,10 @@ Output only qualifying candidates, with: ref, surface, proof, cause, fix sketch,
|
||||
## Enforce the bug-fix evidence bar
|
||||
|
||||
- Never merge a bug-fix PR based only on issue text, PR text, or AI rationale.
|
||||
- Whenever feasible, use Crabbox (`$crabbox`) for end-to-end verification before
|
||||
commenting that a bug is unreproducible, closing an issue, or opening/landing
|
||||
a fix PR. Prefer a real packaged/Docker/live lane that exercises the reported
|
||||
user flow over unit-only proof.
|
||||
- Before landing, require:
|
||||
1. symptom evidence such as a repro, logs, or a failing test
|
||||
2. a verified root cause in code with file/line
|
||||
@@ -139,6 +143,9 @@ Output only qualifying candidates, with: ref, surface, proof, cause, fix sketch,
|
||||
4. a regression test when feasible, or explicit manual verification plus a reason no test was added
|
||||
- If the claim is unsubstantiated or likely wrong, request evidence or changes instead of merging.
|
||||
- If the linked issue appears outdated or incorrect, correct triage first. Do not merge a speculative fix.
|
||||
- If Crabbox/E2E proof is blocked, say exactly why and use the closest available
|
||||
local, Docker, mocked, or targeted proof. Do not present unit tests as real
|
||||
behavior proof.
|
||||
|
||||
## Close low-signal manual PRs carefully
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- OpenAI/Codex: install the Codex runtime plugin from npm during OpenAI onboarding and load it automatically for implicit OpenAI model routes, while preserving manual PI runtime overrides. Fixes #79358.
|
||||
- Gateway: avoid false degraded event-loop health during rapid health/readiness/status probes unless sustained load has delay co-evidence, while keeping hard delay detection immediate. (#77028) Thanks @rubencu.
|
||||
- Codex app-server: keep native hook relays alive for long-running turns so shell and file approvals stay reachable until the configured run window finishes. (#77533) Thanks @rubencu.
|
||||
- Gateway/agent: pass the session-key agent id into inline image attachment validation so the first image in a fresh per-agent session uses the agent's vision-capable model override instead of the text-only system default. Fixes #79407. Thanks @pandadev66.
|
||||
|
||||
@@ -45,6 +45,7 @@ export async function ensureCodexRuntimePluginForModelSelection(params: {
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
preferRemoteInstall: true,
|
||||
},
|
||||
prompter: params.prompter,
|
||||
runtime: params.runtime,
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import type { CodexRuntimePluginInstallResult } from "../../codex-runtime-plugin-install.js";
|
||||
import { applyNonInteractivePluginProviderChoice } from "./auth-choice.plugin-providers.js";
|
||||
|
||||
const ensureCodexRuntimePluginForModelSelection = vi.hoisted(() =>
|
||||
vi.fn(
|
||||
async ({ cfg }: { cfg: OpenClawConfig }): Promise<CodexRuntimePluginInstallResult> => ({
|
||||
cfg,
|
||||
required: false,
|
||||
installed: false,
|
||||
}),
|
||||
),
|
||||
);
|
||||
vi.mock("../../codex-runtime-plugin-install.js", () => ({
|
||||
ensureCodexRuntimePluginForModelSelection,
|
||||
}));
|
||||
const resolvePreferredProviderForAuthChoice = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
vi.mock("../../../plugins/provider-auth-choice-preference.js", () => ({
|
||||
resolvePreferredProviderForAuthChoice,
|
||||
@@ -29,6 +42,11 @@ beforeEach(() => {
|
||||
resolveOwningPluginIdsForProvider.mockReturnValue(undefined as never);
|
||||
resolveProviderPluginChoice.mockReturnValue(undefined);
|
||||
resolvePluginProviders.mockReturnValue([] as never);
|
||||
ensureCodexRuntimePluginForModelSelection.mockImplementation(async ({ cfg }) => ({
|
||||
cfg,
|
||||
required: false,
|
||||
installed: false,
|
||||
}));
|
||||
});
|
||||
|
||||
function createRuntime() {
|
||||
@@ -208,4 +226,48 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ensures Codex after a non-interactive OpenAI provider choice sets the default model", async () => {
|
||||
const runtime = createRuntime();
|
||||
const selectedConfig = {
|
||||
agents: { defaults: { model: { primary: "openai/gpt-5.5" } } },
|
||||
} as OpenClawConfig;
|
||||
const installedConfig = {
|
||||
...selectedConfig,
|
||||
plugins: { entries: { codex: { enabled: true } } },
|
||||
} as OpenClawConfig;
|
||||
const runNonInteractive = vi.fn(async () => selectedConfig);
|
||||
ensureCodexRuntimePluginForModelSelection.mockResolvedValue({
|
||||
cfg: installedConfig,
|
||||
required: true,
|
||||
installed: true,
|
||||
status: "installed",
|
||||
});
|
||||
resolvePluginProviders.mockReturnValue([{ id: "openai", pluginId: "openai" }] as never);
|
||||
resolveProviderPluginChoice.mockReturnValue({
|
||||
provider: { id: "openai", pluginId: "openai", label: "OpenAI" },
|
||||
method: { runNonInteractive },
|
||||
});
|
||||
|
||||
const result = await applyNonInteractivePluginProviderChoice({
|
||||
nextConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
authChoice: "openai-api-key",
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
|
||||
expect(runNonInteractive).toHaveBeenCalledOnce();
|
||||
expect(ensureCodexRuntimePluginForModelSelection).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cfg: selectedConfig,
|
||||
model: "openai/gpt-5.5",
|
||||
runtime,
|
||||
workspaceDir: expect.any(String),
|
||||
}),
|
||||
);
|
||||
expect(result).toBe(installedConfig);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from "../../../agents/agent-scope.js";
|
||||
import type { ApiKeyCredential } from "../../../agents/auth-profiles/types.js";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../../../agents/workspace.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../../../config/model-input.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import { enablePluginInConfig } from "../../../plugins/enable.js";
|
||||
import { resolvePreferredProviderForAuthChoice } from "../../../plugins/provider-auth-choice-preference.js";
|
||||
@@ -16,6 +17,8 @@ import type {
|
||||
} from "../../../plugins/types.js";
|
||||
import type { RuntimeEnv } from "../../../runtime.js";
|
||||
import { createLazyRuntimeSurface } from "../../../shared/lazy-runtime.js";
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
import { ensureCodexRuntimePluginForModelSelection } from "../../codex-runtime-plugin-install.js";
|
||||
import type { OnboardOptions } from "../../onboard-types.js";
|
||||
|
||||
const PROVIDER_PLUGIN_CHOICE_PREFIX = "provider-plugin:";
|
||||
@@ -29,6 +32,47 @@ const loadAuthChoicePluginProvidersRuntime = createLazyRuntimeSurface(
|
||||
({ authChoicePluginProvidersRuntime }) => authChoicePluginProvidersRuntime,
|
||||
);
|
||||
|
||||
function createNonInteractivePluginInstallPrompter(runtime: RuntimeEnv): WizardPrompter {
|
||||
const unavailable = <T>(message: string): Promise<T> =>
|
||||
Promise.reject(new Error(`Non-interactive setup cannot prompt for plugin install: ${message}`));
|
||||
return {
|
||||
async intro(title) {
|
||||
runtime.log(title);
|
||||
},
|
||||
async outro(message) {
|
||||
runtime.log(message);
|
||||
},
|
||||
async note(message, title) {
|
||||
runtime.log(title ? `${title}\n${message}` : message);
|
||||
},
|
||||
async select(params) {
|
||||
return unavailable(params.message);
|
||||
},
|
||||
async multiselect(params) {
|
||||
return unavailable(params.message);
|
||||
},
|
||||
async text(params) {
|
||||
return unavailable(params.message);
|
||||
},
|
||||
async confirm(params) {
|
||||
return unavailable(params.message);
|
||||
},
|
||||
progress(label) {
|
||||
runtime.log(label);
|
||||
return {
|
||||
update(message) {
|
||||
runtime.log(message);
|
||||
},
|
||||
stop(message) {
|
||||
if (message) {
|
||||
runtime.log(message);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function applyNonInteractivePluginProviderChoice(params: {
|
||||
nextConfig: OpenClawConfig;
|
||||
authChoice: string;
|
||||
@@ -139,7 +183,7 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
return method.runNonInteractive({
|
||||
const result = await method.runNonInteractive({
|
||||
authChoice: params.authChoice,
|
||||
config: enableResult.config,
|
||||
baseConfig: params.baseConfig,
|
||||
@@ -150,4 +194,20 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
||||
resolveApiKey: params.resolveApiKey,
|
||||
toApiKeyCredential: params.toApiKeyCredential,
|
||||
});
|
||||
if (!result) {
|
||||
return result;
|
||||
}
|
||||
const selectedModel = resolveAgentModelPrimaryValue(result.agents?.defaults?.model);
|
||||
if (!selectedModel) {
|
||||
return result;
|
||||
}
|
||||
return (
|
||||
await ensureCodexRuntimePluginForModelSelection({
|
||||
cfg: result,
|
||||
model: selectedModel,
|
||||
prompter: createNonInteractivePluginInstallPrompter(params.runtime),
|
||||
runtime: params.runtime,
|
||||
workspaceDir,
|
||||
})
|
||||
).cfg;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export type OnboardingPluginInstallEntry = {
|
||||
label: string;
|
||||
install: PluginPackageInstall;
|
||||
trustedSourceLinkedOfficialInstall?: boolean;
|
||||
preferRemoteInstall?: boolean;
|
||||
};
|
||||
|
||||
export type OnboardingPluginInstallStatus = "installed" | "skipped" | "failed" | "timed_out";
|
||||
@@ -730,14 +731,18 @@ export async function ensureOnboardingPluginInstalled(params: {
|
||||
const { entry, prompter, runtime, workspaceDir } = params;
|
||||
let next = params.cfg;
|
||||
const allowLocal = hasGitWorkspace(workspaceDir);
|
||||
const bundledLocalPath = resolveBundledLocalPath({ entry, workspaceDir });
|
||||
const bundledLocalPath = entry.preferRemoteInstall
|
||||
? null
|
||||
: resolveBundledLocalPath({ entry, workspaceDir });
|
||||
const localPath =
|
||||
bundledLocalPath ??
|
||||
resolveLocalPath({
|
||||
entry,
|
||||
workspaceDir,
|
||||
allowLocal,
|
||||
});
|
||||
(entry.preferRemoteInstall
|
||||
? null
|
||||
: resolveLocalPath({
|
||||
entry,
|
||||
workspaceDir,
|
||||
allowLocal,
|
||||
}));
|
||||
const clawhubSpec = resolveClawHubSpecForOnboarding(entry.install);
|
||||
const npmSpec = resolveNpmSpecForOnboarding(entry.install);
|
||||
const updateChannel = resolveRegistryUpdateChannel({
|
||||
|
||||
@@ -1425,12 +1425,27 @@ describe("resolveGatewayStartupPluginIds", () => {
|
||||
expectStartupPluginIdsCase({
|
||||
config: createStartupConfig({
|
||||
modelId: "openai/gpt-5.5",
|
||||
enabledPluginIds: ["codex"],
|
||||
}),
|
||||
expected: ["demo-channel", "browser", "codex", "memory-core"],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not include Codex when an OpenAI model is manually pinned to PI", () => {
|
||||
expectStartupPluginIdsCase({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-5.5" },
|
||||
models: {
|
||||
"openai/gpt-5.5": { agentRuntime: { id: "pi" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
expected: ["demo-channel", "browser", "memory-core"],
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores legacy per-agent runtime during startup planning", () => {
|
||||
expectStartupPluginIdsCase({
|
||||
config: createStartupConfig({
|
||||
|
||||
@@ -331,6 +331,62 @@ function canStartConfiguredGenerationProviderPlugin(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function canStartRequiredAgentHarnessPlugin(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
||||
activationSource: {
|
||||
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
||||
rootConfig?: OpenClawConfig;
|
||||
};
|
||||
config: OpenClawConfig;
|
||||
requiredAgentHarnessRuntimes: ReadonlySet<string>;
|
||||
platform?: NodeJS.Platform;
|
||||
}): boolean {
|
||||
if (
|
||||
!params.plugin.startup.agentHarnesses.some((runtime) =>
|
||||
params.requiredAgentHarnessRuntimes.has(runtime),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!params.pluginsConfig.enabled || !params.activationSource.plugins.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
||||
params.activationSource.plugins.deny.includes(params.plugin.pluginId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
||||
params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.pluginsConfig.allow.length > 0 &&
|
||||
!params.pluginsConfig.allow.includes(params.plugin.pluginId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.activationSource.plugins.allow.length > 0 &&
|
||||
!params.activationSource.plugins.allow.includes(params.plugin.pluginId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const activationState = resolveEffectivePluginActivationState({
|
||||
id: params.plugin.pluginId,
|
||||
origin: params.plugin.origin,
|
||||
config: params.pluginsConfig,
|
||||
rootConfig: params.config,
|
||||
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
||||
activationSource: params.activationSource,
|
||||
});
|
||||
return activationState.enabled || params.plugin.origin === "bundled";
|
||||
}
|
||||
|
||||
function canStartConfiguredSpeechProviderPlugin(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
manifest: PluginManifestRecord | undefined;
|
||||
@@ -672,17 +728,16 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: {
|
||||
});
|
||||
}
|
||||
if (
|
||||
plugin.startup.agentHarnesses.some((runtime) => requiredAgentHarnessRuntimes.has(runtime))
|
||||
) {
|
||||
const activationState = resolveEffectivePluginActivationState({
|
||||
id: plugin.pluginId,
|
||||
origin: plugin.origin,
|
||||
config: pluginsConfig,
|
||||
rootConfig: params.config,
|
||||
enabledByDefault: isPluginEnabledByDefaultForPlatform(plugin, params.platform),
|
||||
canStartRequiredAgentHarnessPlugin({
|
||||
plugin,
|
||||
pluginsConfig,
|
||||
activationSource,
|
||||
});
|
||||
return activationState.enabled;
|
||||
config: params.config,
|
||||
requiredAgentHarnessRuntimes,
|
||||
platform: params.platform,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
canStartConfiguredRootPlugin({
|
||||
|
||||
Reference in New Issue
Block a user