Skip to content

feat(server): optional unauthenticated loopback listener for direct-spawn Codex (#1102) - #1220

Merged
lidge-jun merged 6 commits into
devfrom
codex/260807-loopback-listener
Aug 7, 2026
Merged

feat(server): optional unauthenticated loopback listener for direct-spawn Codex (#1102)#1220
lidge-jun merged 6 commits into
devfrom
codex/260807-loopback-listener

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

A codex app-server launched by a host that resolves the entrypoint directly never passes through the generated shim, so it never inherits OPENCODEX_API_AUTH_TOKEN. On a wildcard bind every model call then 401s at admission, before any stream frame.

The tempting fix is to exempt callers whose socket peer looks like loopback. That is unsound: requestIP() proves only the last transport hop, and Docker Desktop port forwarding, host-network containers, WSL mirrored networking and tunnel terminators all re-open remote connections locally. It would also have ridden resolveApiAuth into eight endpoints unrelated to this issue.

Instead a second listener binds 127.0.0.1. The kernel refuses remote connections outright, so there is no address to judge, and the public listener's admission policy is unchanged.

Off by default. unauthenticatedLoopbackListener: { enabled: true, port: N }.

What is load-bearing

A per-request policy view. Auth and CORS read a Pick<OcxConfig, "hostname" | "corsAllowOrigins" | "apiKeys"> chosen from the receiving listener. Rewriting the whole config and holding it would go stale on the next management change; adding an allowUnauthenticated parameter to the resolvers would put an admission bypass on the wrong side of the boundary. The narrow type also means a policy view leaking into a routing path fails to typecheck.

The bind is not the whole boundary. An attacker page can make a victim's browser connect to 127.0.0.1, and that connection is local. The listener takes the same Host/Origin branch a plain loopback bind always has. A test asserts the same hostile Origin the loopback policy rejects would be accepted under the public policy.

A fixed port. Never OS-assigned. An ephemeral port would change across restarts while running app-servers kept the old base_url — the exact symptom this issue reported for token rotation, which investigation showed does not actually happen (writeServiceApiTokenFile mirrors an already-present env value, only from install/repair).

GET /v1/models is on the four-route allowlist. When catalog materialization fails, syncCodex injects with catalogPath: null and Codex falls back to an online model manager that refreshes through it. Returning 404 would fix the direct-spawn host while breaking its model list.

One startup transaction. If the loopback bind fails, the public listener rolls back rather than being stranded — otherwise the CLI's port retry reads it as a public-port conflict and picks a different port, accumulating listeners. Composite stop completes cleanup on both listeners and propagates failure; swallowing it would let drainAndShutdown report success while a socket is held.

Verification

  • bun run typecheck clean, bun run privacy:scan passed.
  • bun run test — 9756 pass, 8 skip, 0 fail across 607 files.
  • 20 integration tests speak HTTP to real listeners: same request 401 on public and 200 on loopback, connection refused on the machine's non-loopback IPv4, every non-allowlisted route probed with the method its handler accepts, the rebinding Host/Origin shape rejected, both ports rebindable after stop, a squatted loopback port rolling back a fixed public port.
  • Ablation, all driven red rather than argued: loopback bound to 0.0.0.0 fails 2; allowlist check disabled fails 1; policy view collapsed to the shared config fails 3; /v1/messages added to the allowlist fails 1; the inject substitution removed fails 1; per-step catch removed fails 2; collected failures swallowed fails 3; requestServer.upgrade swapped for server.upgrade fails 1.

Known gaps, stated rather than implied

  • No direct-spawn activation evidence yet. This PR is a draft for that reason. @openai/codex is not a test dependency, so the plan calls for a non-skippable activation run against a real app-server before ready-for-review: isolated CODEX_HOME with no models_cache.json, entrypoint resolved directly, token stripped from the child environment, a runtime-generated unique routed model asserted in model/list, and one turn against that model. Catalog-present and catalog-absent both.
  • No admission-attribution assertion on a completed turn. Admission is proven; the { kind: "loopback" } log row on a real turn is not.
  • Two seams have no runtime oracle on this Bun version and are pinned by source assertions instead, with the reason recorded in the tests.

Status

This does not close #1102. The default is off, so the reporter's repro still 401s until they adopt the option, and the question of whether it suits their deployment is still open on the issue.

Stacked on #1216.

Refs #1102

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4968ce69-aa45-4355-aa0c-a6d25d0344fd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 7, 2026
@lidge-jun
lidge-jun force-pushed the codex/260807-loopback-listener branch from 03bc98d to 9d95125 Compare August 7, 2026 16:47
@lidge-jun
lidge-jun force-pushed the codex/260807-windows-acl-token-sid branch from ea8f5d0 to 89e32ae Compare August 7, 2026 16:47
@lidge-jun

Copy link
Copy Markdown
Owner Author

Direct-spawn activation evidence

The audit named this the ready gate. Here it is, from a real codex app-server on Codex 0.146.0 rather than an in-process stand-in.

PASS  resolved the real Codex entrypoint, not PATH — /Users/jun/.nvm/.../node_modules/@openai/codex/bin/codex.js
PASS  entrypoint runs — codex-cli 0.146.0
PASS  built the catalog with our own serializer — 1 entries
PASS  wrote an isolated CODEX_HOME with no models_cache.json
PASS  stripped OPENCODEX_API_AUTH_TOKEN from the child environment
PASS  app-server initialized — ok
PASS  model/list contains the unique routed model that only our listener can supply — ocx-direct-spawn-da6d9c3f-...
PASS  thread/start accepted — 019fdd24-8ae1-7a61-9f4d-d962a9d6e19e
PASS  the app-server reached the loopback listener without a credential — POST /v1/responses

9/9 checks passed

The last line is the claim of #1102: a real app-server, spawned from the resolved entrypoint with no token in its environment, opened /v1/responses against the injected base_url and was admitted.

On the oracle. Codex caches model lists and falls back to a bundled catalog when a refresh fails, so "did model/list succeed" proves nothing — a broken path is indistinguishable from a working one. The model id is generated per run, so a name no bundled catalog can contain could only have arrived through our listener.

Two things writing it surfaced. model/list reads model_catalog_json; it does not call the provider's /v1/models. The first version of the harness therefore watched Codex return its five bundled ids while never touching the listener — the exact false-negative shape the unique id exists to expose, aimed at the harness instead of the feature. And a hand-written catalog fixture is a liability: Codex rejects the whole file on any schema mismatch and silently falls back, so the catalog is now built with buildCatalogEntries, which also means the script exercises the bytes ocx sync writes.

Ablation. Pointing base_url at a dead port makes the final check red, so the harness watches the hop rather than asserting its own setup.

The script is committed at scripts/verify-loopback-direct-spawn.mjs. It is deliberately not a bun test file: the repository does not depend on @openai/codex, and a test that skips when it is absent would report green on machines that never ran it.

Marking ready for review.

@lidge-jun
lidge-jun marked this pull request as ready for review August 7, 2026 16:54

@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: 0d7e0ca751

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

Comment on lines +3 to +6
> **개정 이력.** 첫 판은 "opt-in 으로 loopback 소켓 피어를 무인증 admit" 을
> 제안했다. 독립 감사가 P1 다섯 건으로 되돌렸고, 그중 둘이 설계를 바꿨다:
> (a) 그 스위치는 `resolveApiAuth` 를 타고 #1102 와 무관한 8개 엔드포인트까지
> 열고, (b) 공용 리스너의 피어 주소는 최종 사용자 신원이 아니다. 아래는

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 Move pre-disclosure security notes out of devlog

This new _plan document records pre-merge security audit findings, P1 severity assessments, admission-bypass reasoning, and threat/deployment paths. Because devlog/ is tracked and public, this material should remain in .tmp/ or another scratch directory until the fix and any advisory are public; only the published outcome should enter the repository.

AGENTS.md reference: AGENTS.md:L61-L68

Useful? React with 👍 / 👎.

Comment thread src/codex/inject.ts
Comment on lines +647 to +648
const loopback = config?.unauthenticatedLoopbackListener;
if (loopback?.enabled) port = loopback.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.

P1 Badge Inject the loopback hostname together with its port

When the public listener uses a specific non-loopback hostname such as 192.168.1.10, this substitutes only the loopback port; the later setRootOpenaiBaseUrl(..., config?.hostname) still emits http://192.168.1.10:<loopback-port>/v1. Unlike wildcard hosts, providerBaseHost preserves that address, while the new socket listens only on 127.0.0.1, so directly spawned Codex processes get connection refusals and the advertised feature fails. Use 127.0.0.1 as the injected host whenever this listener is selected.

AGENTS.md reference: src/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

Comment thread src/codex/inject.ts
// credential (#1102). Emitting the env header anyway would be worse than useless: the
// directly-spawned app-server this exists for has no OPENCODEX_API_AUTH_TOKEN in its
// environment, and Codex would send an empty header value.
if (config?.unauthenticatedLoopbackListener?.enabled) return false;

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 Keep auth-header selection scoped to Codex injection

With a remote public bind and this option enabled, returning false here also changes unrelated callers in src/clients/config-export.ts: opencodeProviderOptions and proxyAdmissionHeaders stop emitting x-opencodex-api-key. Both CLI export (src/cli/export-command.ts:176) and management export (src/server/management/model-routes.ts:183-186) still point those clients at the authenticated public listener, so existing OpenCode, Hermes, and OpenClaw generated configs begin receiving 401 responses. Keep this helper describing the target/public listener, and special-case the unauthenticated listener only in Codex injection or make the target listener explicit.

AGENTS.md reference: src/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

Comment thread src/config.ts
Comment on lines +2016 to +2018
const proxyPort = (value as Record<string, unknown>).port;
if (typeof proxyPort === "number" && proxyPort === listenerPort) {
return "schema_invalid: unauthenticatedLoopbackListener.port: must differ from the proxy 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 Reject collisions with the implicit proxy port

When the config omits port, the schema supplies the proxy default 10100, but this comparison only runs when the raw candidate contains a numeric port. Therefore ocx config set unauthenticatedLoopbackListener '{"enabled":true,"port":10100}' can report success for a config whose two effective ports are identical; the next startup then fails in chooseListenPort despite write-time validation promising to reject this relationship. Compare the listener port against the effective defaulted proxy port as well.

AGENTS.md reference: src/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

Comment on lines +87 to +88
`ocx sync` then writes `base_url = "http://127.0.0.1:10200/v1"` into the managed Codex provider block
and omits the auth header, so a directly spawned app-server works without any credential plumbing.

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 a restart before syncing the loopback endpoint

When this setting is added while the proxy is already running, startServer has already captured the old listener configuration and no socket is created on the new port, while ocx sync reloads the persisted config and immediately writes that port into Codex. Following this documented sequence therefore repoints app-servers to a refused connection until the proxy restarts. Instruct users to restart first—or rely on startup sync after restarting—instead of presenting ocx sync alone as sufficient.

AGENTS.md reference: docs-site/AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

Comment on lines +40 to +47
const probe = spawnSync(process.execPath, [
"-e",
"process.stdout.write(require.resolve('@openai/codex/bin/codex.js'))",
], { encoding: "utf8" });
if (probe.status === 0 && probe.stdout.trim()) return probe.stdout.trim();
const which = spawnSync("readlink", ["-f", spawnSync("which", ["codex"], { encoding: "utf8" }).stdout.trim()], { encoding: "utf8" });
const path = which.stdout.trim();
if (!path) throw new Error("cannot resolve @openai/codex/bin/codex.js");

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 Resolve the real Codex entrypoint portably

The fallback uses Unix-only which and readlink, so it cannot run on supported Windows hosts when the repository-local require.resolve fails, which is expected because this repository does not depend on @openai/codex. On Unix it can also return the generated OpenCodex shell shim when that shim owns PATH, after which invoking the result through node does not exercise the direct JavaScript entrypoint this probe claims to verify. Resolve the installed package through a cross-platform package/global-install or recorded-shim-backup path and explicitly reject managed shims.

AGENTS.md reference: scripts/AGENTS.md:L14-L15

Useful? React with 👍 / 👎.

Comment on lines +224 to +228
const hitResponses = seen.some(entry => entry.includes("/v1/responses"));
record(
"the app-server reached the loopback listener without a credential",
hitModels || hitResponses,
seen.slice(0, 8).join(" | ") || "no requests observed",

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 Require the direct-spawn turn to reach Responses

If Codex refreshes GET /v1/models successfully but turn/start is rejected locally or never dispatches a model request, hitModels makes this final check pass even though no /v1/responses request reached the listener. The script does not otherwise mark a missing or failed turn/start response as a failed step, so it can exit 0 without proving the direct-spawn model-call path that the feature exists to repair. Require hitResponses here; model discovery is already checked separately.

AGENTS.md reference: scripts/AGENTS.md:L15-L15

Useful? React with 👍 / 👎.

The design changed twice under audit. The first proposal was an opt-in
switch that would have admitted loopback socket peers on a remote bind;
it rode resolveApiAuth into eight endpoints unrelated to #1102, and a
public listener's peer address only proves the last transport hop, which
Docker Desktop, host-network containers, WSL mirrored networking and
tunnels all terminate locally.

The shipped design leaves public admission untouched and opens a
separate 127.0.0.1-bound listener, so the kernel refuses remote
connections instead of us judging addresses.

Later rounds closed the implementation contracts: a fixed port (an
ephemeral one would manufacture the restart breakage the issue claimed
and we disproved), a per-request auth/CORS policy view narrow enough
that it cannot masquerade as business config, one startup transaction
across both binds, composite stop that completes cleanup AND propagates
failure, and GET /v1/models on the allowlist because Codex falls back to
it when no catalog is installed.

The last blocker was the sharpest: the /v1/models ablation would not
have gone red, because the models-manager catches refresh failures and
returns its bundled list. The acceptance test now turns on a
runtime-generated unique routed model that no bundled catalog can
synthesize.
…pawn Codex (#1102)

A `codex app-server` launched by a host that resolves the entrypoint
directly never passes through the generated shim, so it never inherits
OPENCODEX_API_AUTH_TOKEN. On a wildcard bind every model call then 401s
at admission, before any SSE frame.

The tempting fix — exempt callers whose socket peer looks like loopback
— is unsound. `requestIP()` proves only the last transport hop, and
Docker Desktop port forwarding, host-network containers, WSL mirrored
networking and tunnel terminators all re-open remote connections
locally. It would also have ridden `resolveApiAuth` into eight
endpoints unrelated to this issue.

Instead a second listener binds 127.0.0.1. The kernel refuses remote
connections outright, so there is no address to judge, and the public
listener's admission policy is byte-for-byte unchanged.

The parts that are load-bearing:

Auth and CORS read a `RequestPolicyView` — a Pick of hostname,
corsAllowOrigins and apiKeys — chosen per request from the receiving
listener. Rewriting the whole config and holding it would go stale on
the next management change; adding an `allowUnauthenticated` parameter
to the resolvers would put an admission bypass on the wrong side of the
boundary. The narrow type also means a policy view that leaks into a
routing path fails to typecheck.

The bind is loopback but the boundary is not the bind alone: an
attacker page can make a victim's browser connect to 127.0.0.1. The
listener therefore takes the same Host/Origin branch a plain loopback
bind always has. A test asserts the same hostile Origin that the
loopback policy rejects would be accepted under the public policy.

The port is required in config, never OS-assigned. An ephemeral port
would change across restarts while running app-servers kept the old
base_url — the exact symptom this issue reported for token rotation,
which does not actually happen.

`GET /v1/models` is on the four-route allowlist because when catalog
materialization fails, `syncCodex` injects with `catalogPath: null` and
Codex falls back to an online model manager that refreshes through it.

Both binds are one startup transaction, and composite stop completes
cleanup on both listeners while still propagating failure — swallowing
it would let drainAndShutdown report success while a socket is held.

Off by default. When on, every local process can spend account quota
and paid provider credentials, which the startup warning and the docs
say plainly.

Ablation: reverting the policy view to the shared config makes 3 tests
red including the hostile-Host case; dropping the auth-header rule
makes 1 red; removing the port validator makes 2 red; rewriting the
public bind to a literal 127.0.0.1 makes the F4 symmetry guard red.

Refs #1102
…eserve its port

The audit found the previous commit shipped an unauthenticated surface
whose tests never started a server. Every assertion ran against pure
helpers, so the listener could fail to open, bind the wrong address,
lose the identity comparison against the public server, or serve routes
outside its allowlist, and the suite would stay green. That is not a
coverage gap; it is a missing oracle for a security boundary.

Ten integration tests now speak HTTP to real listeners: the same
request answered 401 on the public socket and 200 on the loopback one,
a connection to the machine's non-loopback IPv4 being refused,
management and dashboard and unrelated data-plane routes returning 404
while an allowlisted one returns 200, the loopback Host/Origin gate
rejecting the rebinding shape the public policy would accept, both
ports rebindable after stop, and a squatted loopback port rolling back
the public bind.

Ablation, all driven red rather than argued: binding the loopback
listener to 0.0.0.0 fails 2; removing the allowlist check fails 1;
collapsing the policy view to the shared config fails 3.

The second finding was a real gap rather than a test one. `ocx start
--port <loopback port>` would bind the public listener onto the address
the loopback listener is configured for, and the loopback bind would
then fail EADDRINUSE — rolling back a startup whose only problem was a
config collision. `findAvailablePort` takes a `reservedPort`: an
explicit preference for it is refused up front rather than retried, and
ephemeral selection redraws if the OS hands it back.

Two smaller corrections. `drainingResponse` and `serverBusyResponse`
run before the auth checks and were still using the shared config, so a
503 on the loopback listener could echo a hostile origin; both now take
the receiving listener's policy. And a non-boolean `enabled` was
silently deleted by the schema's catch, leaving an operator convinced
they had enabled a listener that was off — write-time now rejects it.

Refs #1102
…ch the real inject wiring

Two of the previous tests were watching nothing, and the audit was right
about both.

The allowlist check probed POST routes with GET. Chat Completions,
Messages, Images, search and Live all 404 on method mismatch inside
their handlers, so widening the allowlist to admit one of them would
have kept the assertion green. Each route is now requested with the
method its handler accepts, plus two cases proving the allowlisted paths
still reject methods they do not serve. Ablation: adding /v1/messages to
the allowlist now fails it.

The injection test handed the loopback port straight to
buildProviderTableBlock, so deleting the substitution inside
injectCodexConfig changed nothing it observed. It now runs the real
injector in a subprocess — CODEX_CONFIG_PATH resolves at module load, so
an in-process CODEX_HOME would write somewhere else — passes the PUBLIC
port the way every caller does, and reads the written config.toml.
Ablation: removing the substitution fails it.

Also added: POST /v1/responses and /v1/responses/compact admitted on the
loopback listener and 401 on the public one, asserted as neither 401 nor
404 because "not 401" alone would survive removing the route; a
Responses WebSocket handshake on both listeners; and a rollback test
that binds a fixed public port and rebinds it after the throw, since
throwing while leaving the public listener up is the failure the
rollback exists to prevent.

Two honest gaps are recorded rather than papered over. The WebSocket
test cannot defend requestServer.upgrade over server.upgrade: that
ablation stayed green because this Bun version accepts an upgrade issued
from a sibling Bun.serve. And composite stop's failure propagation has
no test, because the composite captures the underlying stop at
construction and there is no seam to inject a rejection through; its
sibling property, cleanup across both listeners, is covered.

Port selection fixes from the same review: the ephemeral redraw is now a
bounded loop behind an injectable allocator rather than unbounded async
recursion, and it has tests that actually reach the redraw branch. An
explicit --port collision with the reserved port is rejected before the
60-second reclaim path instead of after it, with a message naming the
real cause. runAdmittedHttpTurn now takes the policy explicitly, so no
CORS-emitting helper falls back to the shared config.

Refs #1102
…two blind seams

The shutdown orchestration held two properties that pull against each
other — keep cleaning up after a failure, yet still report it — and
neither was testable in place, because the composite captures the
underlying stop at construction. Extracted to `runListenerShutdown`,
which now has four cases including both failure shapes. Ablation:
removing the per-step catch fails 2, swallowing the collected failures
fails 3.

Two seams have no runtime oracle on this Bun version, and both would
regress silently. Swapping `requestServer.upgrade` for `server.upgrade`
stays green because this Bun accepts an upgrade issued from a sibling
Bun.serve; another version is not promised to, and the loopback listener
would then fail to upgrade at all. And the connection-refused test
degrades to a warning on a host with no external IPv4, so the 0.0.0.0
ablation would pass there. Source assertions are a weak instrument, but
one aimed at a known blind spot beats a comment nobody runs — and the
upgrade assertion does go red on that swap.

Smaller review fixes: the WebSocket handshake helper now clears its
timer and settles once, so a late timeout cannot fire into the next
test; and the rollback test draws its public port with the loopback port
reserved, since two back-to-back freePort() calls can return the same
port and the test would squat itself.

Refs #1102
The audit named this the ready gate, and it was right to. Every existing
test proves a piece — admission, the route allowlist, CORS, the bind scope,
the injected port — and none of them prove the thing the feature exists
for: that a real `codex app-server`, spawned the way a third-party host
spawns it, reaches the proxy without a credential. That seam is between two
processes.

Not a `bun test` file. The repository does not depend on `@openai/codex`,
so a test that skips when it is absent would report green on machines that
never ran it. This fails loudly and its output is the evidence.

The oracle is a routed model id generated at run time. Codex caches model
lists and falls back to a bundled catalog when a refresh fails, so "did
model/list succeed" proves nothing — a broken path looks identical to a
working one. A name no bundled catalog can contain can only have arrived
through our listener.

Writing it surfaced two things worth recording. `model/list` reads
`model_catalog_json`; it does not call the provider's `/v1/models`, so the
first version watched Codex return its five bundled ids while never
touching the listener — the exact false-negative shape the unique id exists
to expose, pointed at the harness instead of the feature. And a hand-written
catalog fixture is a liability: Codex rejects the whole file on any schema
mismatch and silently falls back, so the catalog is now built with our own
serializer, which also means the script exercises the bytes `ocx sync`
writes.

Result on Codex 0.146.0, isolated CODEX_HOME with no models_cache.json and
OPENCODEX_API_AUTH_TOKEN stripped from the child environment: 9/9, ending
with POST /v1/responses observed on the loopback listener.

Ablation: pointing base_url at a dead port makes the last check red, so the
harness is watching the hop rather than asserting its own setup.

Refs #1102
@lidge-jun
lidge-jun force-pushed the codex/260807-windows-acl-token-sid branch from 89e32ae to fca2cd7 Compare August 7, 2026 17:15
@lidge-jun
lidge-jun force-pushed the codex/260807-loopback-listener branch from 0d7e0ca to 912cb99 Compare August 7, 2026 17:15
@lidge-jun
lidge-jun changed the base branch from codex/260807-windows-acl-token-sid to dev August 7, 2026 17:48
@lidge-jun
lidge-jun merged commit 4c3ffa6 into dev Aug 7, 2026
43 of 63 checks passed
@lidge-jun
lidge-jun deleted the codex/260807-loopback-listener branch August 8, 2026 00:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant