Skip to content

feat(ai): add read-only OpenCodex provider - #3698

Merged
Yeachan-Heo merged 4 commits into
devfrom
feat/opencodex
Aug 1, 2026
Merged

feat(ai): add read-only OpenCodex provider#3698
Yeachan-Heo merged 4 commits into
devfrom
feat/opencodex

Conversation

@Yeachan-Heo

Copy link
Copy Markdown
Owner

Summary

  • discover identity-checked local OpenCodex endpoints from runtime metadata or port 10100
  • expose cached /api/models models with opencodex/<id> selectors and raw wire IDs
  • add keyless Responses streaming and read-only /login opencodex status reprobes

Verification

  • bun test packages/ai/test/openai-opencodex-responses.test.ts packages/ai/test/register-builtins.test.ts packages/ai/test/models-lazy.test.ts packages/ai/test/auth-storage-check-credentials.test.ts packages/coding-agent/test/models-config-tool-choice-support.test.ts (29 passed)
  • Biome checks passed on changed files
  • Full workspace typecheck remains affected by existing sibling-worktree package links

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a3fdf7878

ℹ️ 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".

catalog("Alibaba Token Plan", ["ALIBABA_TOKEN_PLAN_API_KEY"], { oauthProvider: "alibaba-token-plan" }),
),
descriptor("openai", "gpt-5.4", config => openaiModelManagerOptions(config)),
descriptor("opencodex", "gpt-5.4", () => openCodexModelManagerOptions(), { allowUnauthenticated: true }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Register OpenCodex as a keyless runtime provider

allowUnauthenticated only causes the model manager to run; it does not add the provider to ModelRegistry.#keylessProviders. Consequently, even when the local proxy returns a valid catalog, getAvailable() rejects every OpenCodex model because there is neither a stored credential nor a keyless entry, and /login opencodex deliberately persists nothing. The newly discovered provider therefore cannot be selected or used from the coding-agent CLI.

Useful? React with 👍 / 👎.

function normalizeModel(row: CatalogRow, endpoint: OpenCodexEndpoint): Model<"openai-responses"> | undefined {
const rawId = typeof row.id === "string" ? row.id.trim() : typeof row.model === "string" ? row.model.trim() : "";
if (!rawId || rawId.includes("\n")) return undefined;
const publicId = `opencodex/${rawId}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the provider prefix out of the model ID

Model selectors are constructed throughout the coding agent as ${model.provider}/${model.id}, so prefixing id here produces selectors such as opencodex/opencodex/provider/model. A documented or manually supplied selector like opencodex/provider/model is parsed as provider opencodex plus ID provider/model and cannot match the stored ID opencodex/provider/model; the descriptor's default gpt-5.4 also cannot match a discovered opencodex/gpt-5.4 ID. Preserve rawId as id and use wireModelId solely for the transport mapping.

Useful? React with 👍 / 👎.

Comment on lines +84 to +87
return (
typeof health.version === "string" ||
(typeof health.pid === "number" && Number.isFinite(health.pid)) ||
(typeof health.port === "number" && Number.isFinite(health.port))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require an OpenCodex-specific health identity

When a stale runtime endpoint or port 10100 is occupied by another service, any generic response such as { "ok": true, "version": "1.0" } or { "ok": true, "pid": 42 } passes this check. If that service also exposes a compatible /api/models, it is registered as OpenCodex and subsequent prompts can be sent to the foreign listener. Validate an OpenCodex-specific discriminator rather than accepting generic version, PID, or port fields.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc72a69c60

ℹ️ 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".

async function readRuntimeEndpoint(): Promise<string | undefined> {
const home = process.env.OPENCODEX_HOME?.trim() || path.join(os.homedir(), ".opencodex");
try {
const raw = JSON.parse(await fs.readFile(path.join(home, "runtime-port.json"), "utf8")) as RuntimePortFile;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Read runtime metadata with Bun.file

readRuntimeEndpoint uses fs.readFile for file-content access, contrary to the repository contract requiring Bun.file() for reads and limiting node:fs/promises to directory operations. Replace this with await Bun.file(path.join(home, "runtime-port.json")).text() so the new provider follows the required Bun runtime conventions.

AGENTS.md reference: AGENTS.md:L114-L122

Useful? React with 👍 / 👎.

Comment on lines +29 to +31
id: "opencodex",
name: "OpenCodex (local proxy status)",
available: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Special-case OpenCodex in the package CLI

Adding this entry to getOAuthProviders() makes the shipped pi-ai login opencodex command pass through the generic packages/ai/src/cli.ts login path, which unconditionally prints Credentials saved to ~/.gjc/agent/agent.db. The OpenCodex branch only performs a status probe and persists nothing—even when the proxy is unavailable—so this command now gives users a false credential-success confirmation; the package CLI needs status-only handling analogous to the coding-agent controller.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Signed: Yeachan-Heo / gajae-code AI-provider red-team

VERDICT: REQUEST_CHANGES

Exact head reviewed: bc72a69c60336f759be0257132815d1d1eb80f39. The owning implementation lane CI run 30691789283 is terminal-green: 24 checks, 18 success, 6 skipped, 0 failures, 0 non-terminal.

Hostile findings:

  1. MEDIUM — OpenCodex endpoint identity is not actually checked. normalizeEndpoint() accepts arbitrary hostnames from OPENCODEX_HOME/runtime-port.json, while isOpenCodexHealth() accepts any HTTP 200 { ok: true, version: <any string> } or a numeric pid/port. A foreign listener or non-loopback host can therefore be classified as OpenCodex and queried for /api/models. The implementation claims identity-checked local discovery, but neither loopback restriction nor an exact OpenCodex health identity/port binding is enforced. The test suite only rejects { ok: true }; it does not reject a foreign version, mismatched port, or non-loopback endpoint.

  2. MEDIUM — Merge safety is stale against current dev. The PR base is b40bc271502a0802e212e5538c5d0e8436643029, while current dev is 2c99a7f226635e812bc8fe8669787242029ce662. The branch is one commit behind current dev and diverges from it; the green CI proves the old base, not the current merge result. Rebase and rerun exact-head CI before acceptance.

Scope is otherwise bounded to OpenCodex provider discovery/status, Responses routing, and focused tests/changelog. No merge performed.

Formal REQUEST_CHANGES review submission was rejected by GitHub because the authenticated owner cannot request changes on an own-authored PR; this signed comment is the attached terminal disposition.

[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Signed: Yeachan-Heo / gajae-code AI-provider red-team

VERDICT: REQUEST_CHANGES

Exact head remains bc72a69c60336f759be0257132815d1d1eb80f39. Exact CI remains terminal-green: 24 checks, 18 success, 6 skipped, 0 failures, 0 non-terminal.

Latest Codex automated review was attached to this exact commit (bc72a69c60) but contained no substantive suggestions; it was only the generic Codex review envelope. That does not replace hostile review.

Provider/auth/contract review: the OpenCodex addition is scoped to read-only local discovery/status, Responses model routing, raw wire-model preservation, OAuth/status plumbing, focused tests, and changelog updates. It does not persist credentials.

Blocking red-team finding remains: endpoint identity is not actually enforced. runtime-port.json can supply an arbitrary hostname, and the health predicate accepts any { ok: true, version: <string> } or numeric pid/port response. A foreign listener or non-loopback endpoint can therefore be classified as OpenCodex and queried for /api/models. The tests do not reject a foreign version, mismatched port, or non-loopback endpoint. The implementation claims identity-checked local discovery without enforcing that contract.

Merge safety is also invalidated by base drift: PR base is b40bc271502a0802e212e5538c5d0e8436643029; current dev is now ee58e8fe64380007646f87a340a3d59a3ee52ee9. The green CI validates the old base, not the current merge result. Rebase and rerun exact-head CI before acceptance.

#3699 remains deferred and dirty; it was not reopened or reviewed. No merge performed.

[repo owner's gaebal-gajae (clawdbot) 🦞]

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1fd8b0671

ℹ️ 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".

export function openCodexModelManagerOptions(): ModelManagerOptions<"openai-responses"> {
return {
providerId: "opencodex",
cacheTtlMs: OPENCODEX_MODEL_CACHE_TTL_MS,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Revalidate cached OpenCodex endpoints before use

After a successful discovery, this generic model cache persists each model's identity-bound baseUrl; on subsequent starts a fresh authoritative cache skips fetchDynamicModels, while a failed refresh merges cached models back in. Therefore, if OpenCodex exits and another Responses-compatible listener later occupies the cached port, prompts can be routed to it without executing the strict /healthz identity check. Fresh evidence in model-manager.ts is that both the fresh-cache fast path and failed-fetch fallback retain these cached models, so this provider needs revalidation before cached routes are published or used.

Useful? React with 👍 / 👎.

const models = rows
.map(row => normalizeModel(row, endpoint))
.filter((model): model is Model<"openai-responses"> => model !== undefined);
return models.length > 0 ? models : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat an empty model catalog as an authoritative result

When /api/models validly returns an empty array—for example after the proxy loses all configured upstream models—this converts the successful response to null. ModelManager interprets null as a dynamic-fetch failure and merges the previous cached catalog back in, leaving removed models selectable even though the proxy explicitly reported none. Return the empty array so the authoritative cache and visible catalog are cleared.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

VERDICT: REQUEST_CHANGES

Exact head: c1fd8b0671d531f4c8d36c17ebbe00cbbb54147f. Base is current dev ee58e8fe64380007646f87a340a3d59a3ee52ee9. Terminal exact-head CI is green: Dev CI 30696082131 and Public site sync 30696082134; merge state is CLEAN/MERGEABLE.

Blocking security finding — discovery follows redirects outside the loopback boundary. fetchJson() calls fetch(url) without redirect: "error" or validating the final response URL. A process listening on an allowed loopback port can return a 30x from /healthz or /api/models to an arbitrary host; Bun follows it, so this “loopback-only” discovery performs attacker-controlled SSRF and can ingest arbitrary remote catalog data. The health identity and port checks run only after the redirect has already escaped the boundary. Add redirect rejection (and a regression test asserting no request leaves the original loopback origin) before treating the repair as safe.

Additional test hole: the new tests cover foreign hosts and identity/port mismatches, but not redirects, malformed catalog rows beyond newline ids, or the unauthenticated provider’s no-endpoint/static-model behavior.


[repo owner's gaebal-gajae (clawdbot) 🦞]

Yeachan-Heo and others added 4 commits August 1, 2026 12:37
Discover local OpenCodex endpoints without owning proxy credentials or lifecycle, expose cached models with raw wire ids, and make /login opencodex a read-only status probe.

Tested: focused OpenCodex, registration, model, auth, and schema suites (29 tests)

Not-tested: full workspace typecheck remains affected by existing sibling-worktree package links
Bun 1.3.14 models fetch.preconnect as part of the fetch function type. Keep the test spies installed in place and preserve that property on mocked implementations so the provider check compiles without changing runtime behavior.

Lore-id: 3698\nConstraint: repair the affected AI/provider CI lane only\nConstraint: preserve read-only OpenCodex behavior\nRejected: weakening type checks | hides the CI failure\nConfidence: high\nScope-risk: narrow\nReversibility: easy\nTested: OpenCodex and stream focused tests; AI and coding-agent checks; affected-plan replay and evidence validation
Runtime metadata could select a foreign listener, and health responses were accepted without proving the OpenCodex identity or matching port. Restrict endpoint candidates to loopback IPs and require strict health identity and port binding before model discovery.

Lore-id: 3698\nConstraint: keep discovery local and read-only\nConstraint: do not persist credentials\nRejected: accept arbitrary host or loose health payloads | permits foreign listener confusion\nConfidence: high\nScope-risk: narrow\nReversibility: easy\nTested: OpenCodex focused tests and packages/ai check
OpenCodex probes must not follow a loopback redirect into a foreign listener. Force redirect:error for health and catalog requests and cover redirect rejection while retaining loopback identity and strict port binding.

Lore-id: 3698\nConstraint: keep OpenCodex discovery loopback-only\nConstraint: do not persist credentials\nRejected: follow redirects | permits SSRF and foreign catalog ingestion\nConfidence: high\nScope-risk: narrow\nReversibility: easy\nTested: redirect regression tests and packages/ai check
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Signed: Yeachan-Heo / fresh OpenCodex successor red-team review

VERDICT: MERGE_READY

Reviewed exact head 1b023bfdc008819723c17cc165816f12130373cb against current dev c78825874905112b1c98371f2e50eab5f2145581. The exact head is based on current dev, and GitHub reports CLEAN / MERGEABLE.

The redirect escape is repaired at the shared discovery boundary: fetchJson() now passes redirect: "error", so both /healthz and /api/models reject redirects before any foreign destination is contacted. The two added regressions verify the request option for health probing and catalog retrieval, assert the redirect target is never fetched, and preserve the fallback/identity/port tests. This closes the previously reported SSRF and foreign-catalog path without changing the provider’s endpoint construction.

Red-team checks:

  • Runtime metadata is reduced to literal loopback IPs (127.0.0.0/8 or ::1); hostnames, DNS rebinding, IPv4-mapped alternatives, and non-loopback addresses are not accepted.
  • Health acceptance still requires ok === true, version === "opencodex", and the response port to equal the candidate URL port.
  • Catalog retrieval is performed only after that health gate, uses the same no-redirect policy, and returns null on non-2xx, malformed JSON, or empty normalized catalogs.
  • OpenCodex login remains a read-only status probe; no credential is persisted. Streaming uses the explicit local sentinel key only for opencodex, preserving other provider authentication paths.
  • The remaining threat-model boundary is explicit: a hostile process already able to bind the local loopback port can impersonate an unauthenticated local proxy by returning the exact health shape. The current contract advertises a local unauthenticated proxy, so this is residual local-process trust rather than a regression in this repair; stronger hostile-local-user protection would require authenticated IPC or a secret handshake.

Exact CI run 30700119457 is terminal green, including affected-path checks, state gates, native build, and Telegram generation guard; its paired Public site sync is also successful. No merge performed.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo
Yeachan-Heo merged commit 49f9ee4 into dev Aug 1, 2026
24 checks passed
@Yeachan-Heo
Yeachan-Heo deleted the feat/opencodex branch August 1, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant