fix: pin OpenAI runtime before gateway launch (#1076)

This commit is contained in:
paisley
2026-05-28 11:34:03 +08:00
committed by GitHub
parent 1830bbe787
commit a87a86df50
3 changed files with 107 additions and 75 deletions

View File

@@ -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;
}

View File

@@ -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).

View File

@@ -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: {