Skip to content

fix(models): separate Codex harness from model choices#71193

Merged
steipete merged 2 commits into
mainfrom
fix/codex-harness-model-separation
Apr 24, 2026
Merged

fix(models): separate Codex harness from model choices#71193
steipete merged 2 commits into
mainfrom
fix/codex-harness-model-separation

Conversation

@steipete

Copy link
Copy Markdown
Contributor

Summary

  • Hide the virtual codex harness provider from normal model picker and /models provider surfaces.
  • Migrate legacy primary codex/* agent model refs to canonical openai/* plus explicit embeddedHarness.runtime = "codex".
  • Preserve fallback-only codex/* refs instead of pinning the whole agent/defaults container to the Codex harness.
  • Document the OpenAI route vs OpenAI Codex OAuth route vs native Codex harness split.

Replaces #71176 with a maintainer-owned rewrite because that branch is not maintainer-editable and the original migration was unsafe for fallback-only codex/* refs.

Tests

  • pnpm test src/commands/model-picker.test.ts src/auto-reply/reply/commands-models.test.ts src/commands/doctor-legacy-config.migrations.test.ts
  • OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test src/agents/model-fallback.test.ts src/agents/tools/image-tool.test.ts src/agents/pi-embedded-runner/run.overflow-compaction.loop.test.ts src/agents/pi-embedded-runner/run.attempt-param-forwarding.test.ts src/agents/pi-embedded-runner/usage-reporting.test.ts
  • pnpm lint:core
  • pnpm check:test-types
  • pnpm check:docs
  • git diff --check

pnpm check:changed passed type/lint/guards but the broad agents test shard timed out under concurrent local load; the failed files passed when rerun serially with one worker.

@aisle-research-bot

aisle-research-bot Bot commented Apr 24, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Prototype pollution via legacy Codex models allowlist key migration
1. 🟠 Prototype pollution via legacy Codex models allowlist key migration
Property Value
Severity High
CWE CWE-1321
Location src/commands/doctor/shared/legacy-config-core-normalizers.ts:266-280

Description

normalizeLegacyCodexAllowlistModels() builds a plain object (next = {}) from user-controlled config keys and assigns with bracket notation. If a legacy config contains a key like "__proto__", "constructor", or "prototype", the assignment can invoke special setters (e.g., __proto__) and mutate the prototype of next (and potentially affect subsequent object operations), i.e. prototype pollution.

This migrator runs as part of the doctor legacy config normalization path and processes untrusted workspace configuration.

Vulnerable code:

const next: Record<string, unknown> = {};
...
next[rawKey] = mergeModelEntry(entry, next[rawKey]);
...
next[migratedKey] = mergeModelEntry(entry, next[migratedKey]);

Recommendation

Prevent dangerous keys from being used as object properties during migration.

Options:

  1. Use a null-prototype dictionary and still block dangerous keys:
const DANGEROUS_RECORD_KEYS = new Set(["__proto__", "prototype", "constructor"]);
const next: Record<string, unknown> = Object.create(null);

for (const [rawKey, entry] of Object.entries(rawModels)) {
  if (DANGEROUS_RECORD_KEYS.has(rawKey)) continue; // or record a warning
  const migratedKey = migrateLegacyCodexModelRef(rawKey);
  const key = migratedKey ?? rawKey;
  if (DANGEROUS_RECORD_KEYS.has(key)) continue;
  next[key] = mergeModelEntry(entry, next[key]);
}
  1. Alternatively, store entries in a Map<string, unknown> during processing and only materialize into a safe object at the end, filtering dangerous keys.

Also consider adding shared helpers (similar to other migration code that defines DANGEROUS_RECORD_KEYS) and unit tests covering __proto__ keys.


Analyzed PR: #71193 at commit f2f5185

Last updated on: 2026-04-24T18:31:08Z

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation gateway Gateway runtime commands Command implementations agents Agent runtime and tooling size: M labels Apr 24, 2026
@cursor

cursor Bot commented Apr 24, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Medium risk because it changes model picker filtering and performs automatic config migrations that may affect which model/harness runs for legacy Codex-based setups.

Overview
Codex harness is now treated as a runtime choice, not a normal model/provider. Model pickers and /models provider menus filter out the virtual codex provider (while still allowing manual entry), via a new isModelPickerVisible* helper used across interactive setup flows and command surfaces.

Doctor compatibility migration now rewrites legacy primary codex/* refs. When an agent’s primary model is codex/*, doctor migrates it (and related allowlist keys and fallbacks) to openai/* and sets embeddedHarness.runtime: "codex"; fallback-only codex/* refs are intentionally left untouched.

Docs and changelog are updated to clarify the split between openai/*, openai-codex/* (OAuth via PI), and the native Codex harness, and tests cover the new visibility + migration behavior.

Reviewed by Cursor Bugbot for commit f2f5185. Bugbot is set up for automated code reviews on this repo. Configure here.

@openclaw-barnacle openclaw-barnacle Bot added the maintainer Maintainer-authored PR label Apr 24, 2026
@greptile-apps

greptile-apps Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hides the virtual codex harness provider from all model picker surfaces and introduces a doctor migration that rewrites legacy codex/* primary model refs to canonical openai/* while explicitly recording embeddedHarness.runtime: "codex". Fallback-only codex/* refs are intentionally left in place. Test coverage for the new visibility filter and the migration logic is thorough.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 style/observability concerns with no correctness impact.

The migration logic is correct (merge precedence, fallback preservation, harness injection all verified against tests). The two P2 findings (unnecessary list reference replacement and a missing change-log entry for the harness mutation) are non-blocking and do not affect runtime behavior.

legacy-config-core-normalizers.ts — shared changed flag and silent harness mutation in the change log.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/commands/doctor/shared/legacy-config-core-normalizers.ts
Line: 357-372

Comment:
**Stale `nextAgents.list` replacement when only defaults changed**

`changed` is shared between the `defaults` block and the `list` block. If `defaults` changes but no list entry changes, `nextAgents.list = nextList` is still executed because `changed` is already `true`. `nextList` is a new array but contains the exact same object references as the original list — functionally correct, but any downstream caller that relies on reference equality to detect list mutations will see a false change. A separate `listChanged` flag would isolate the two sections cleanly.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/commands/doctor/shared/legacy-config-core-normalizers.ts
Line: 323-329

Comment:
**Silent `embeddedHarness` mutation omitted from change log**

When `ensureCodexEmbeddedHarness` returns `changed: true`, `changed = true` is set on the container but no message is pushed to `changes`. Users running the doctor command will see "Moved … legacy codex/* primary refs to openai/* with Codex harness." but will not see an explicit entry for the `embeddedHarness.runtime` field being overwritten. If the field previously held a custom harness id (e.g. `"auto"` or a third-party harness), the silent mutation could be surprising. Adding a `changes.push(...)` call here, analogous to the model and models entries, would make the log complete.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix: separate Codex harness from model c..." | Re-trigger Greptile

Comment on lines +357 to +372
if (Array.isArray(rawAgents.list)) {
const nextList = rawAgents.list.map((entry, index) => {
if (!isRecord(entry)) {
return entry;
}
const agentId = normalizeOptionalString(entry.id) ?? String(index);
const agent = normalizeLegacyCodexAgentContainer(entry, `agents.list.${agentId}`, changes);
if (!agent.changed) {
return entry;
}
changed = true;
return agent.value;
});
if (changed) {
nextAgents.list = nextList;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale nextAgents.list replacement when only defaults changed

changed is shared between the defaults block and the list block. If defaults changes but no list entry changes, nextAgents.list = nextList is still executed because changed is already true. nextList is a new array but contains the exact same object references as the original list — functionally correct, but any downstream caller that relies on reference equality to detect list mutations will see a false change. A separate listChanged flag would isolate the two sections cleanly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/doctor/shared/legacy-config-core-normalizers.ts
Line: 357-372

Comment:
**Stale `nextAgents.list` replacement when only defaults changed**

`changed` is shared between the `defaults` block and the `list` block. If `defaults` changes but no list entry changes, `nextAgents.list = nextList` is still executed because `changed` is already `true`. `nextList` is a new array but contains the exact same object references as the original list — functionally correct, but any downstream caller that relies on reference equality to detect list mutations will see a false change. A separate `listChanged` flag would isolate the two sections cleanly.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +323 to +329
if (model.codexPrimarySelected) {
const harness = ensureCodexEmbeddedHarness(raw.embeddedHarness);
if (harness.changed) {
next.embeddedHarness = harness.value;
changed = true;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent embeddedHarness mutation omitted from change log

When ensureCodexEmbeddedHarness returns changed: true, changed = true is set on the container but no message is pushed to changes. Users running the doctor command will see "Moved … legacy codex/* primary refs to openai/* with Codex harness." but will not see an explicit entry for the embeddedHarness.runtime field being overwritten. If the field previously held a custom harness id (e.g. "auto" or a third-party harness), the silent mutation could be surprising. Adding a changes.push(...) call here, analogous to the model and models entries, would make the log complete.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/doctor/shared/legacy-config-core-normalizers.ts
Line: 323-329

Comment:
**Silent `embeddedHarness` mutation omitted from change log**

When `ensureCodexEmbeddedHarness` returns `changed: true`, `changed = true` is set on the container but no message is pushed to `changes`. Users running the doctor command will see "Moved … legacy codex/* primary refs to openai/* with Codex harness." but will not see an explicit entry for the `embeddedHarness.runtime` field being overwritten. If the field previously held a custom harness id (e.g. `"auto"` or a third-party harness), the silent mutation could be surprising. Adding a `changes.push(...)` call here, analogous to the model and models entries, would make the log complete.

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9bffce2868

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

changes.push(`Moved ${path}.model legacy codex/* primary refs to openai/* with Codex harness.`);
}

const models = normalizeLegacyCodexAllowlistModels(raw.models, model.codexPrimarySelected);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid migrating shared codex model keys when codex fallbacks remain

This rewrites raw.models keys to openai/* whenever the current container has a codex primary, which is problematic for agents.defaults: other agents can still intentionally keep fallback-only codex/* refs (their primary is non-codex, so those refs are preserved), but per-model overrides are read by exact key at runtime (for example in resolveExtraParams and fast-mode resolution). After this migration, those fallback codex/* runs no longer match their agents.defaults.models entry, so alias/params behavior silently changes even though the fallback ref itself was preserved.

Useful? React with 👍 / 👎.

@steipete
steipete force-pushed the fix/codex-harness-model-separation branch from 9bffce2 to f2f5185 Compare April 24, 2026 18:28

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f2f5185. Configure here.

Comment thread src/flows/model-picker.ts
const initialKeys = allowedKeySet
? initialSeeds.filter((key) => allowedKeySet.has(key))
: initialSeeds;
: initialSeeds.filter(isModelPickerVisibleModelRef);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowlist initial values not filtered by visibility

Low Severity

When allowedKeySet is present, initialKeys is filtered only by allowlist membership but not by isModelPickerVisibleModelRef. Meanwhile, the allowedCatalog options and supplementalKeys both apply the visibility filter. This creates an inconsistency where initialValues passed to the multiselect may include codex/* keys that have no matching option, since those entries were removed from the options list by the new visibility filtering.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f2f5185. Configure here.

@steipete
steipete merged commit bc0f54b into main Apr 24, 2026
67 checks passed
@steipete
steipete deleted the fix/codex-harness-model-separation branch April 24, 2026 18:40
@steipete

Copy link
Copy Markdown
Contributor Author

Landed via squash merge onto main.

  • Local gate: pnpm check:docs
  • CI: full PR check suite green on f2f51858f8e41dc411fe32da153f51a02b709c6d
  • Source head: f2f5185
  • Squash commit: bc0f54b

Thanks @vincentkoc for the original direction in #71176.

Angfr95 pushed a commit to Angfr95/openclaw that referenced this pull request Apr 25, 2026
* fix: separate Codex harness from model choices

* docs: note Codex harness model choice fix
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
* fix: separate Codex harness from model choices

* docs: note Codex harness model choice fix
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
* fix: separate Codex harness from model choices

* docs: note Codex harness model choice fix
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
* fix: separate Codex harness from model choices

* docs: note Codex harness model choice fix
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
* fix: separate Codex harness from model choices

* docs: note Codex harness model choice fix
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
* fix: separate Codex harness from model choices

* docs: note Codex harness model choice fix
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
* fix: separate Codex harness from model choices

* docs: note Codex harness model choice fix
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling commands Command implementations docs Improvements or additions to documentation gateway Gateway runtime maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant