fix: pin OpenAI runtime before gateway launch (#1076)
This commit is contained in:
@@ -1469,6 +1469,31 @@ function applyPinnedAgentRuntime(
|
||||
nextProvider.agentRuntime = { id: pinnedRuntimeId };
|
||||
}
|
||||
|
||||
function applyOpenClawProviderAgentRuntimePinsToConfig(config: Record<string, unknown>): string[] {
|
||||
const models = (config.models || {}) as Record<string, unknown>;
|
||||
const providers = (models.providers || {}) as Record<string, unknown>;
|
||||
const pinned: string[] = [];
|
||||
|
||||
for (const provider of Object.keys(OPENCLAW_PROVIDER_PINNED_AGENT_RUNTIME)) {
|
||||
const entry = providers[provider];
|
||||
if (!isPlainRecord(entry)) continue;
|
||||
const before = entry.agentRuntime;
|
||||
applyPinnedAgentRuntime(provider, entry);
|
||||
const after = entry.agentRuntime;
|
||||
if (before !== after) {
|
||||
providers[provider] = entry;
|
||||
pinned.push(provider);
|
||||
}
|
||||
}
|
||||
|
||||
if (pinned.length > 0) {
|
||||
models.providers = providers;
|
||||
config.models = models;
|
||||
}
|
||||
|
||||
return pinned;
|
||||
}
|
||||
|
||||
function upsertOpenClawProviderEntry(
|
||||
config: Record<string, unknown>,
|
||||
provider: string,
|
||||
@@ -1552,29 +1577,12 @@ function upsertOpenClawProviderEntry(
|
||||
* Returns the list of provider keys that received a runtime pin, for logging.
|
||||
*/
|
||||
export async function ensureOpenClawProviderAgentRuntimePins(): Promise<string[]> {
|
||||
const pinned: string[] = [];
|
||||
let pinned: string[] = [];
|
||||
await withConfigLock(async () => {
|
||||
const config = await readOpenClawJson();
|
||||
const models = (config.models || {}) as Record<string, unknown>;
|
||||
const providers = (models.providers || {}) as Record<string, unknown>;
|
||||
let modified = false;
|
||||
pinned = applyOpenClawProviderAgentRuntimePinsToConfig(config);
|
||||
|
||||
for (const [provider, runtimeId] of Object.entries(OPENCLAW_PROVIDER_PINNED_AGENT_RUNTIME)) {
|
||||
const entry = providers[provider];
|
||||
if (!isPlainRecord(entry)) continue;
|
||||
const existing = (entry as Record<string, unknown>).agentRuntime;
|
||||
if (isPlainRecord(existing) && typeof existing.id === 'string' && existing.id.trim()) {
|
||||
continue;
|
||||
}
|
||||
(entry as Record<string, unknown>).agentRuntime = { id: runtimeId };
|
||||
providers[provider] = entry;
|
||||
pinned.push(provider);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
models.providers = providers;
|
||||
config.models = models;
|
||||
if (pinned.length > 0) {
|
||||
await writeOpenClawJson(config);
|
||||
}
|
||||
});
|
||||
@@ -2257,6 +2265,12 @@ export async function batchSyncConfigFields(token: string): Promise<void> {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
const pinnedProviderRuntimes = applyOpenClawProviderAgentRuntimePinsToConfig(config);
|
||||
if (pinnedProviderRuntimes.length > 0) {
|
||||
modified = true;
|
||||
console.log(`[batch-sync] Pinned embedded agent runtime for models.providers entries: ${pinnedProviderRuntimes.join(', ')}`);
|
||||
}
|
||||
|
||||
// ── Session idle minutes ──
|
||||
const session = (
|
||||
config.session && typeof config.session === 'object'
|
||||
@@ -3062,6 +3076,12 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const pinnedProviderRuntimes = applyOpenClawProviderAgentRuntimePinsToConfig(config);
|
||||
if (pinnedProviderRuntimes.length > 0) {
|
||||
modified = true;
|
||||
console.log(`[sanitize] Pinned embedded agent runtime for models.providers entries: ${pinnedProviderRuntimes.join(', ')}`);
|
||||
}
|
||||
|
||||
if (healAnthropicMessagesMaxTokensInConfig(config)) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
---
|
||||
id: openai-api-key-pin-pi-agent-runtime
|
||||
title: Pin the embedded "pi" agent runtime on OpenAI API-key provider entries to avoid the unbundled "codex" harness
|
||||
title: Pin the embedded "pi" agent runtime on OpenAI provider entries to avoid the unbundled "codex" harness
|
||||
scenario: gateway-backend-communication
|
||||
taskType: runtime-bridge
|
||||
intent: Stop OpenClaw from auto-routing API-key OpenAI provider entries (api.openai.com baseUrl) to the externally-bundled "codex" agent harness, which is not registered in the shipped OpenClaw distribution and causes every chat to fail with `Requested agent harness "codex" is not registered.`. Pin `agentRuntime.id = "pi"` on every `models.providers.openai` entry ClawX writes, and self-heal existing on-disk entries before the next default-provider switch so upgrading users do not have to re-save their provider manually.
|
||||
intent: Stop OpenClaw from auto-routing OpenAI provider entries (api.openai.com baseUrl) to the externally-bundled "codex" agent harness, which is not registered in the shipped ClawX/OpenClaw distribution and causes chats/heartbeats to fail with `Requested agent harness "codex" is not registered.`. Pin `agentRuntime.id = "pi"` on every `models.providers.openai` and `models.providers.openai-codex` entry ClawX writes, and self-heal existing on-disk entries before provider switches and before Gateway launch so upgrading users do not have to re-save their provider manually.
|
||||
touchedAreas:
|
||||
- harness/specs/tasks/openai-api-key-pin-pi-agent-runtime.md
|
||||
- electron/utils/openclaw-auth.ts
|
||||
- electron/services/providers/provider-runtime-sync.ts
|
||||
- electron/gateway/config-sync.ts
|
||||
- tests/unit/openclaw-auth.test.ts
|
||||
expectedUserBehavior:
|
||||
- Configuring OpenAI with an API key (default `https://api.openai.com/v1` baseUrl) and starting a chat succeeds without `Requested agent harness "codex" is not registered.` from the Gateway.
|
||||
- Upgrading from an earlier ClawX build that wrote an `openai` provider entry without `agentRuntime` and then switching default provider (or back to OpenAI) self-heals the entry so the next Gateway reload boots cleanly.
|
||||
- OAuth-based OpenAI Codex accounts (which target the separate `openai-codex` runtime key) keep their existing routing — their `models.providers.openai-codex` entry is never auto-pinned.
|
||||
- OpenAI browser-OAuth/Codex accounts that use the runtime provider key `openai-codex` also run through the embedded `pi` runtime unless the user has explicitly configured a different installed harness.
|
||||
- Upgrading from an earlier ClawX build that wrote `openai` or `openai-codex` provider entries without `agentRuntime` self-heals those entries on the next provider switch and during pre-launch config sanitization before Gateway reads the config.
|
||||
- A pre-existing user-supplied non-empty `agentRuntime.id` is preserved; the ClawX pin only fills missing runtime policy.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- comms
|
||||
@@ -24,12 +26,13 @@ requiredRules:
|
||||
requiredTests:
|
||||
- tests/unit/openclaw-auth.test.ts
|
||||
acceptance:
|
||||
- electron/utils/openclaw-auth.ts exports `ensureOpenClawProviderAgentRuntimePins` and pins `agentRuntime: { id: 'pi' }` on every `models.providers.openai` write via `upsertOpenClawProviderEntry`.
|
||||
- electron/services/providers/provider-runtime-sync.ts invokes `ensureOpenClawProviderAgentRuntimePins` inside `syncDefaultProviderToRuntime` right after `pruneInvalidApiProviderEntries`, before either the OAuth or non-OAuth branch runs, so a switch to any healthy provider repairs legacy openai entries.
|
||||
- The pin policy only targets the `openai` provider key (the API-key path). `openai-codex` (the OAuth runtime key) is never auto-pinned.
|
||||
- A pre-existing user-supplied `agentRuntime.id` on the `openai` entry is preserved on both the write path and the self-heal path — the pin only fills in a missing value.
|
||||
- `electron/utils/openclaw-auth.ts` applies a shared OpenAI runtime-pin helper from all ClawX write/self-heal paths and pins `agentRuntime: { id: 'pi' }` on `models.providers.openai` and `models.providers.openai-codex` entries that lack a non-empty runtime id.
|
||||
- `syncProviderConfigToOpenClaw(...)`, `setOpenClawDefaultModel(...)`, and `setOpenClawDefaultModelWithOverride(...)` continue to pin OpenAI provider entries through `upsertOpenClawProviderEntry`.
|
||||
- `ensureOpenClawProviderAgentRuntimePins()` still repairs legacy on-disk entries and is invoked inside `syncDefaultProviderToRuntime` right after `pruneInvalidApiProviderEntries`, before either the OAuth or non-OAuth branch runs.
|
||||
- `sanitizeOpenClawConfig()` repairs missing OpenAI runtime pins during Gateway pre-launch sanitization so a stale config cannot boot into the unregistered `codex` harness path.
|
||||
- `batchSyncConfigFields()` also applies the same shared helper during pre-launch config writes, covering the fallback path if sanitization skipped or failed.
|
||||
- Renderer does not add new direct ipcRenderer or Gateway HTTP calls.
|
||||
- Unit tests cover (a) the write-path pin via `syncProviderConfigToOpenClaw('openai', ...)`, (b) the OAuth-path non-pin via `syncProviderConfigToOpenClaw('openai-codex', ...)`, (c) preservation of a user-supplied override on both paths, and (d) the self-heal helper for legacy on-disk openai entries.
|
||||
- Unit tests cover (a) the write-path pin via `syncProviderConfigToOpenClaw('openai', ...)`, (b) the OAuth-path pin via `syncProviderConfigToOpenClaw('openai-codex', ...)`, (c) preservation of a user-supplied override, (d) the self-heal helper for legacy on-disk entries, and (e) `sanitizeOpenClawConfig()` pre-launch repair.
|
||||
docs:
|
||||
required: false
|
||||
---
|
||||
@@ -37,61 +40,43 @@ docs:
|
||||
## Background
|
||||
|
||||
OpenClaw 2026.5+ ships a provider-routing policy
|
||||
([node_modules/openclaw/dist/policy-AKMwD9k5.js](node_modules/openclaw/dist/policy-AKMwD9k5.js),
|
||||
[node_modules/openclaw/dist/openai-codex-routing-kS7Ub1vB.js](node_modules/openclaw/dist/openai-codex-routing-kS7Ub1vB.js))
|
||||
that auto-routes every `models.providers.openai` entry whose `baseUrl` is the
|
||||
official `https://api.openai.com/v1` endpoint through the `codex` agent
|
||||
harness. The intent is to give OpenAI Codex OAuth accounts a richer
|
||||
trajectory, but the heuristic does not distinguish API-key vs OAuth setups —
|
||||
it always picks `codex` when the baseUrl matches.
|
||||
([node_modules/openclaw/dist/policy-B5E74dCu.js](node_modules/openclaw/dist/policy-B5E74dCu.js),
|
||||
[node_modules/openclaw/dist/openai-codex-routing-qYpDQzyG.js](node_modules/openclaw/dist/openai-codex-routing-qYpDQzyG.js))
|
||||
that can route OpenAI-compatible official endpoints through a separate
|
||||
`codex` agent harness. The intent is to give OpenAI/Codex accounts a richer
|
||||
trajectory, but the shipped ClawX distribution does not register an agent
|
||||
harness with id `"codex"`.
|
||||
|
||||
The bundled OpenClaw distribution we ship does not register any agent harness
|
||||
with id `"codex"` (only `cliBackends: ["codex-cli"]` is declared by the OpenAI
|
||||
plugin manifest at
|
||||
`node_modules/openclaw/dist/extensions/openai/openclaw.plugin.json`). As a
|
||||
result, every API-key OpenAI chat fails inside
|
||||
[node_modules/openclaw/dist/selection-61FIEezO.js](node_modules/openclaw/dist/selection-61FIEezO.js)
|
||||
with:
|
||||
When an OpenAI provider entry lacks an explicit runtime pin, affected chat and
|
||||
heartbeat runs can fail inside OpenClaw harness selection with:
|
||||
|
||||
```
|
||||
Requested agent harness "codex" is not registered.
|
||||
```
|
||||
|
||||
Provider-side validation passes (the API key is valid; the protocol is in the
|
||||
allow-list); the failure is purely about agent harness selection.
|
||||
|
||||
The fix is to make ClawX write an explicit `agentRuntime: { id: "pi" }` on
|
||||
every `models.providers.openai` entry. OpenClaw's policy resolver honours an
|
||||
explicit `agentRuntime.id` before falling into the codex auto-routing
|
||||
heuristic, so the API-key path is rescued from the unbundled harness without
|
||||
disturbing OAuth users (whose entry lives under the separate
|
||||
`models.providers.openai-codex` key).
|
||||
Provider-side validation can still pass (credentials and protocol are valid);
|
||||
the failure is about agent harness selection. ClawX therefore writes an
|
||||
explicit `agentRuntime: { id: "pi" }` for OpenAI provider entries that it owns.
|
||||
OpenClaw's policy resolver honours explicit provider/model runtime policy
|
||||
before falling into the codex auto-routing heuristic.
|
||||
|
||||
## Scope
|
||||
|
||||
- Add an `OPENCLAW_PROVIDER_PINNED_AGENT_RUNTIME` map in
|
||||
`electron/utils/openclaw-auth.ts` (currently `{ openai: 'pi' }`) and apply
|
||||
it inside `upsertOpenClawProviderEntry` so every write of a pinned provider
|
||||
carries `agentRuntime: { id: <runtime> }` when the entry does not already
|
||||
specify one.
|
||||
- Export `ensureOpenClawProviderAgentRuntimePins()` as a self-heal helper
|
||||
that walks existing `models.providers.*` entries on disk and writes the pin
|
||||
in place when missing — mirroring `pruneInvalidApiProviderEntries`.
|
||||
- Call the helper in `syncDefaultProviderToRuntime` immediately after
|
||||
`pruneInvalidApiProviderEntries`, before any OAuth/non-OAuth branching, so
|
||||
the repair happens once per default-provider switch.
|
||||
- Cover both the write-path pin and the self-heal path in
|
||||
`tests/unit/openclaw-auth.test.ts`. Verify that the OAuth `openai-codex`
|
||||
entry is untouched and that any user-supplied `agentRuntime` override is
|
||||
preserved on both paths.
|
||||
- Maintain the `OPENCLAW_PROVIDER_PINNED_AGENT_RUNTIME` map in
|
||||
`electron/utils/openclaw-auth.ts` (`openai` and `openai-codex` currently map
|
||||
to `pi`). Future providers can be added to the map without changing the
|
||||
plumbing.
|
||||
- Use one shared helper to apply the map to `models.providers.*` entries. The
|
||||
helper must preserve non-empty user-provided `agentRuntime.id` values.
|
||||
- Call the helper from provider write paths, explicit self-heal, and Gateway
|
||||
pre-launch config sanitation (`sanitizeOpenClawConfig` plus the batched
|
||||
pre-launch config write).
|
||||
- Cover the write path, self-heal path, override-preservation path, and
|
||||
pre-launch sanitize path in unit tests.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Upstream changes to OpenClaw's policy resolver so it would only auto-route
|
||||
to `codex` when an `openai-codex` harness is actually registered.
|
||||
- Pin policy for any other provider key besides `openai`. Future providers
|
||||
that need the same defense can be added to the map without further
|
||||
plumbing.
|
||||
- Renderer-side UI surface for picking a different agent runtime per
|
||||
provider — there is no user-visible UI change in this task.
|
||||
- README updates (no user-visible UI change).
|
||||
- Upstream changes to OpenClaw's policy resolver so it would only auto-route to
|
||||
`codex` when a `codex` harness is actually registered.
|
||||
- UI for choosing an agent runtime per provider.
|
||||
- README updates (no user-visible UI flow change).
|
||||
|
||||
@@ -1850,6 +1850,33 @@ describe('ensureOpenClawProviderAgentRuntimePins', () => {
|
||||
expect(codex.agentRuntime).toEqual({ id: 'pi' });
|
||||
});
|
||||
|
||||
it('pins legacy OpenAI entries during sanitizeOpenClawConfig before Gateway launch', async () => {
|
||||
await writeOpenClawJson({
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
api: 'openai-responses',
|
||||
models: [{ id: 'gpt-5.5', name: 'gpt-5.5' }],
|
||||
},
|
||||
'openai-codex': {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
api: 'openai-codex-responses',
|
||||
models: [{ id: 'gpt-5.5', name: 'gpt-5.5' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { sanitizeOpenClawConfig } = await import('@electron/utils/openclaw-auth');
|
||||
await sanitizeOpenClawConfig();
|
||||
|
||||
const result = await readOpenClawJson();
|
||||
const providers = (result.models as Record<string, unknown>).providers as Record<string, unknown>;
|
||||
expect((providers.openai as Record<string, unknown>).agentRuntime).toEqual({ id: 'pi' });
|
||||
expect((providers['openai-codex'] as Record<string, unknown>).agentRuntime).toEqual({ id: 'pi' });
|
||||
});
|
||||
|
||||
it('leaves entries untouched when the openai entry already has any agentRuntime.id', async () => {
|
||||
const initial = {
|
||||
models: {
|
||||
|
||||
Reference in New Issue
Block a user