diff --git a/.agents/skills/crabbox/SKILL.md b/.agents/skills/crabbox/SKILL.md index 536d7e2021e..2f011ad57ae 100644 --- a/.agents/skills/crabbox/SKILL.md +++ b/.agents/skills/crabbox/SKILL.md @@ -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 diff --git a/.agents/skills/openclaw-pr-maintainer/SKILL.md b/.agents/skills/openclaw-pr-maintainer/SKILL.md index 41a67f2514f..a9ad35d4cd5 100644 --- a/.agents/skills/openclaw-pr-maintainer/SKILL.md +++ b/.agents/skills/openclaw-pr-maintainer/SKILL.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2576dbbfaba..12dd6c06eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/commands/codex-runtime-plugin-install.ts b/src/commands/codex-runtime-plugin-install.ts index 0c584dcfae2..6663791c354 100644 --- a/src/commands/codex-runtime-plugin-install.ts +++ b/src/commands/codex-runtime-plugin-install.ts @@ -45,6 +45,7 @@ export async function ensureCodexRuntimePluginForModelSelection(params: { defaultChoice: "npm", }, trustedSourceLinkedOfficialInstall: true, + preferRemoteInstall: true, }, prompter: params.prompter, runtime: params.runtime, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts index 86c06cec668..16eebcfafde 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts @@ -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 => ({ + 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); + }); }); diff --git a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts index bdaa4f23601..32a78587d17 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts @@ -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 = (message: string): Promise => + 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; } diff --git a/src/commands/onboarding-plugin-install.ts b/src/commands/onboarding-plugin-install.ts index d82667bb25b..2c1e23437af 100644 --- a/src/commands/onboarding-plugin-install.ts +++ b/src/commands/onboarding-plugin-install.ts @@ -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({ diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index 14baba52c08..5bb0007714f 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -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({ diff --git a/src/plugins/gateway-startup-plugin-ids.ts b/src/plugins/gateway-startup-plugin-ids.ts index 6f4421b3216..f68f9e81ded 100644 --- a/src/plugins/gateway-startup-plugin-ids.ts +++ b/src/plugins/gateway-startup-plugin-ids.ts @@ -331,6 +331,62 @@ function canStartConfiguredGenerationProviderPlugin(params: { ); } +function canStartRequiredAgentHarnessPlugin(params: { + plugin: InstalledPluginIndexRecord; + pluginsConfig: ReturnType; + activationSource: { + plugins: ReturnType; + rootConfig?: OpenClawConfig; + }; + config: OpenClawConfig; + requiredAgentHarnessRuntimes: ReadonlySet; + 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({