Skip to content

fix: harden credential and runtime trust boundaries (rebase of #916) - #936

Merged
lidge-jun merged 6 commits into
devfrom
codex/916-trust-boundaries
Aug 6, 2026
Merged

fix: harden credential and runtime trust boundaries (rebase of #916)#936
lidge-jun merged 6 commits into
devfrom
codex/916-trust-boundaries

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Rebase of #916 (@Ingwannu) onto current dev, with the three defects a branch review found.

Draft on purpose. This touches authentication, credential handling, GitHub workflows, and the durable launcher — MAINTAINERS.md requires explicit security review for exactly that surface. The review below is an audit, not that review, and an agent does not substitute for it. Do not merge on my say-so.

The defects are real on current dev

Confirmed by direct probe rather than by reading the PR description:

  • Vertex authority injection. src/adapters/google.ts builds the request host from provider.location with no validation, then attaches an ADC bearer. A location of attacker.example:443/capture# yields authority attacker.example — I reproduced this against dev.
  • Durable Bun executable injection. overrideBunPath() rereads OPENCODEX_BUN_PATH after Bun has loaded project dotenv, so every durableBunRuntime() caller — service, shim, WinSW, tray — can persist a repository-chosen executable.
  • Claude destination injection. An ambient ANTHROPIC_BASE_URL survived credential stripping and reached Claude, which can redirect an OAuth-bearing request.
  • Management-token disclosure. src/oauth/health.ts attached the admin token after only a forgeable /healthz identity check.

None of these were superseded by #917. That PR resolved the inbound management principal — who may call the star route. This is the outbound listener: whether the thing answering on the port deserves the token. Different boundary.

What the review changed

src/cli/claude.ts no-context fallback — narrowed. As submitted, a launch with no trusted launcher context deleted all three ambient Anthropic slots. bun src/cli/index.ts is a documented entry point (structure/01_runtime.md:9) and has no launcher context, so a user with a shell-exported ANTHROPIC_API_KEY simply lost it.

The two slot classes are not symmetric. ANTHROPIC_BASE_URL stays fail-closed — a dotenv-only destination plus subscription auth is precisely how the OAuth bearer leaves for a repository-chosen host, and losing a legitimate custom destination costs a flag rather than an account. Credentials are preserved: the destination is already pinned by the time they are read, so stripping them defends against a project file that could equally well have supplied the key being blamed. The test that mandated the old behavior is replaced by two that pin the asymmetry.

structure/06_docs-and-release.md — the ci.yml row still described the pre-#899 hosted-Windows selector. Rewritten against the real job graph: four Linux shards plus gates, full macOS, Windows only at the shipping boundary, and the aggregate ci job asserting Windows actually succeeded there.

structure/01_runtime.md decision log — recorded the rejected fail-closed-for-everything behavior; now describes the split.

Rebase

One conflict, src/server/index.ts, resolved keeping both #917's managementPrincipal() dispatch and this branch's attestation imports.

Evidence

  • bun run test: 7602 pass / 0 fail across 505 files
  • bun x tsc --noEmit exit 0, bun run privacy:scan passed
  • Focused security suites: 124 pass / 0 fail across the seven affected files

Authorship on the original commits is preserved. @Ingwannu — the substance is yours; the three changes above are the review response, and I would rather you disagree with them here than have them land silently.


Security review round — my narrowing was wrong

The adversarial review returned FAIL on the one change I made to @Ingwannu's behavior, and it was right.

I had preserved ambient credentials when no launcher context exists, reasoning that the destination is pinned before they are read, so a dotenv-supplied key could only ever reach the local proxy. That reasoning does not survive the subscription path. CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST is only set when opencodex owns an auth token — asserting it otherwise logs a subscriber out (#253) — so Claude Code's settings.env merge can still replace ANTHROPIC_BASE_URL after buildClaudeEnv returns. A preserved key travels to that host.

This repository already documents the destination residual for subscription mode, with a test asserting it so it cannot drift into an assumed guarantee. Preserving credentials would have quietly widened that documented residual into a credential leak.

So all three slots fail closed again without provenance, exactly as @Ingwannu submitted it. Direct bun src/cli/index.ts loses ambient Anthropic values — a real cost to a documented entry point, and the honest trade. The escape hatch is running through the published ocx bin, where genuine shell exports survive by proof.

The gap that let my bad revision pass: the suite tested no-context credential handling and settings-hijack separately, never combined. It does now — a no-context ambient key must be absent after the merge that hijacks the destination. Reintroducing the narrowing fails 6 tests.

The review also cleared the rest: the Vertex validator is a strict lowercase 1–63 byte DNS label rejecting dots, Unicode, percent-encoding, fragments, paths, uppercase and overlong values while accepting every legitimate location, enforced on both the adapter and the management write path; local attestation binds a fresh 256-bit challenge plus PID and port, fails closed on malformed proofs, and compares timing-safely; the Bun launcher stamps before Bun starts on every documented invocation including the Windows .cmd shim; and the rebase kept managementPrincipal() with the correct handleManagementAPI arity.

Updated: bun run test 7602 pass / 0 fail across 505 files, typecheck exit 0, privacy scan passed.

Still draft, still needs maintainer security review. Two rounds of adversarial audit are not that review.

Summary by CodeRabbit

  • Security & Reliability

    • Added listener verification for local management actions, helping ensure status and diagnostics communicate with the intended running proxy.
    • Improved protection against unintended credential and configuration leakage during Claude launches.
    • Strengthened runtime selection and launch validation for more predictable operation.
  • Bug Fixes

    • Google Vertex locations are now validated before requests are sent, with clearer configuration errors.
  • Documentation

    • Updated runtime, management, and CI documentation to reflect current behavior and operational safeguards.

Ingwannu and others added 3 commits August 3, 2026 16:16
Review of this branch rejected three things. Two are docs; this is the behavior
one.

With no trusted launcher context, the branch deleted all three ambient Anthropic
slots. That breaks a documented entry point: `bun src/cli/index.ts` is supported
(structure/01_runtime.md:9) and has no launcher context, so a user who exported
ANTHROPIC_API_KEY in their shell simply loses it.

The two slot classes are not symmetric. ANTHROPIC_BASE_URL stays fail-closed — a
dotenv-only destination combined with subscription auth is exactly how Claude's
OAuth bearer and prompt leave for a host the repository chose, and losing a
legitimate custom destination costs a flag rather than an account. Credentials
are preserved: the destination is already pinned by the time they are read, so
stripping them defends against a project file that could equally well have
supplied the key it is being blamed for.

The test that mandated the old behavior is replaced by two: an ambient key
survives without context, and an ambient base URL is still replaced. That pair
is the contract.

Also corrected two structure docs the review flagged as stale: the ci.yml row
still described the pre-#899 hosted-Windows selector, and the runtime decision
log recorded the rejected fail-closed-for-everything behavior.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9ceaccee-83c4-482c-968b-2dedbc254171

📥 Commits

Reviewing files that changed from the base of the PR and between b3a1d90 and 4874390.

📒 Files selected for processing (31)
  • .github/workflows/ci.yml
  • bin/ocx.mjs
  • src/adapters/google.ts
  • src/cli/claude.ts
  • src/cli/index.ts
  • src/cli/launcher-context.ts
  • src/config.ts
  • src/lib/bun-runtime.ts
  • src/lib/local-management-attestation.ts
  • src/oauth/health.ts
  • src/providers/google-vertex-location.ts
  • src/server/auth-cors.ts
  • src/server/index.ts
  • structure/00_overview.md
  • structure/01_runtime.md
  • structure/05_gui-and-management-api.md
  • structure/06_docs-and-release.md
  • tests/bun-runtime.test.ts
  • tests/claude-auth-mode.test.ts
  • tests/claude-cli.test.ts
  • tests/claude-dotenv-provenance-transport.test.ts
  • tests/cli-catalog-prewarm.test.ts
  • tests/config.test.ts
  • tests/gcp-adc.test.ts
  • tests/local-management-attestation.test.ts
  • tests/oauth-health.test.ts
  • tests/ocx-launcher-source.test.ts
  • tests/server-management-auth.test.ts
  • tests/service.test.ts
  • tests/update-notify.test.ts
  • tests/update-stop-first.test.ts

📝 Walkthrough

Walkthrough

The PR adds proof-bound launcher provenance, fail-closed Claude environment handling, durable Bun runtime validation, and local listener attestation for management requests. It also validates Google Vertex locations and updates CI operational documentation.

Changes

Trusted runtime and management controls

Layer / File(s) Summary
Proof-bound launcher provenance
bin/ocx.mjs, src/cli/launcher-context.ts, src/cli/claude.ts, tests/claude-*, tests/ocx-launcher-source.test.ts
The Node launcher sends signed context and Anthropic slot provenance to Bun. Claude preserves only trusted credentials and destinations.
Durable Bun provenance
src/lib/bun-runtime.ts, tests/bun-runtime.test.ts, tests/service.test.ts, structure/01_runtime.md
Durable runtime selection uses validated recorded provenance instead of directly resolving later OPENCODEX_BUN_PATH overrides.
Local management listener attestation
src/lib/local-management-attestation.ts, src/cli/index.ts, src/server/index.ts, src/oauth/health.ts, src/config.ts, tests/*management*, tests/oauth-health.test.ts
The CLI stores a per-process secret with runtime metadata. /healthz returns a challenge-bound proof, and OAuth health checks verify it before sending the management bearer.

Google Vertex location validation

Layer / File(s) Summary
Vertex location validation and coverage
src/providers/google-vertex-location.ts, src/adapters/google.ts, src/server/auth-cors.ts, tests/gcp-adc.test.ts
Vertex locations must be lowercase DNS-compatible labels. Validation runs before endpoint construction and ADC token acquisition.

CI and operations documentation

Layer / File(s) Summary
CI runner documentation
.github/workflows/ci.yml, structure/06_docs-and-release.md
CI documentation describes platform-specific execution, Windows runner controls, approval requirements, and the non-security role of runner selection.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NodeLauncher
  participant Bun
  participant LauncherContext
  participant Claude
  NodeLauncher->>Bun: Pass proof-bound launch context
  Bun->>LauncherContext: Validate and consume proof
  LauncherContext->>Claude: Supply trusted Anthropic slots
  Claude->>Claude: Remove untrusted credentials and destinations
Loading
sequenceDiagram
  participant CLI
  participant RuntimePort
  participant Proxy
  participant OAuthHealth
  CLI->>Proxy: Start with local attestation secret
  CLI->>RuntimePort: Persist secret, PID, and port
  OAuthHealth->>RuntimePort: Read runtime identity
  OAuthHealth->>Proxy: Request challenge-bound proof
  Proxy->>OAuthHealth: Return proof
  OAuthHealth->>Proxy: Send bearer after verification
Loading

Possibly related PRs

Suggested reviewers: ingwannu, wibias, invalid-email-address

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary changes that harden credential and runtime trust boundaries.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/916-trust-boundaries

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 added the bug Something isn't working label Aug 3, 2026
The security review rejected my narrowing, correctly.

I had preserved ambient credentials when no launcher context exists, reasoning
that the destination is pinned before they are read so a dotenv key would only
reach the local proxy. That reasoning does not survive the subscription path:
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST is only set when opencodex owns an auth
token — asserting it otherwise logs a subscriber out (#253) — so Claude Code's
settings.env merge can still replace ANTHROPIC_BASE_URL after buildClaudeEnv
returns. A preserved key travels to that host. The repository documents the
destination residual for subscription mode; preserving credentials would have
widened it into a credential leak.

So all three slots fail closed again without provenance. Direct
`bun src/cli/index.ts` loses ambient Anthropic values, which is a real cost to a
documented entry point; the escape hatch is the published `ocx` bin, where
genuine shell exports survive by proof.

The gap that let the bad revision pass: the suite tested no-context credential
handling and settings-hijack separately, never combined. It does now — a
no-context ambient key must be absent after the merge that hijacks the
destination. Reintroducing the narrowing fails 6 tests.
@lidge-jun
lidge-jun requested review from Ingwannu and Wibias August 5, 2026 14:41
@lidge-jun

Copy link
Copy Markdown
Owner Author

@Ingwannu @Wibias — requesting the security review this has been waiting on since 07-27.

Surfaced by a triage sweep over every open issue and PR (devlog/_plan/260805_issue_pr_triage/). This PR has been recorded as needing explicit security review in four separate devlog rounds, and in none of them was the blocker technical — it was simply unassigned. Reviewers are now set so it stops falling off the board.

State against origin/dev = aaa71967a:

  • draft, CONFLICTING, 620 commits behind, label check CANCELLED
  • touches the credential and runtime trust boundary, which MAINTAINERS.md puts under mandatory security review
  • authored by @lidge-jun, so self-approval is not available

Two things worth separating. The rebase is mechanical and mine to do — please don't spend review time on the conflicts. What is not unblocked by a rebase is the boundary decision itself, and that is the part I'm asking for: even a perfectly rebased branch cannot merge without this review.

For context the PR body already makes explicit: this is the outbound listener question — whether the process answering on the port deserves to — and it was not superseded by #917, which resolved the inbound management principal. Reviewing it as a duplicate of #917 would miss the change.

lidge-jun added a commit that referenced this pull request Aug 5, 2026
Closed #1045 on ancestry plus a green suite. Requested review on #936, #557,
and #1018 - and found that #936, recorded as needing security review in four
separate devlog rounds, had no reviewer assigned at all. Every round was right
that it was blocked; none of them assigned anyone, so an administrative gap
read as a technical one for nine days.

The record also states why sixteen issues stayed open, including the four
whose fix is provably on dev but whose reported symptom was never reproduced
here. Closing on a shipped fix that was never shown to address the report is
how an issue gets closed twice.
@lidge-jun

Copy link
Copy Markdown
Owner Author

Brought this branch onto current dev (b3a1d90a8) via merge commit a90981e67. Two conflicts, both resolved compositionally: src/server/auth-cors.ts keeps dev's redactSecretString alongside this branch's Vertex effectiveGoogleMode validation, and src/server/index.ts folds the branch's localAttestationSecret into dev's new StartServerDeps seam (deps.localAttestationSecret ?? createLocalAttestationSecret()), with the CLI caller updated to the object form.

Independent audit of the merged result (regression + security): the four hardening claims still hold on the merged tree — Vertex location validation before ADC acquisition, Bun-path provenance validation in the launcher, fail-closed ambient Anthropic slots without launcher context, and health-attestation-gated management token. No duplicate hardening landed on dev since the branch point, so nothing double-applies. Three stale test call-sites for the old positional secret were aligned in 4874390dd.

Verification: bun x tsc --noEmit clean, full suite 9076 pass / 0 fail (8 skip) across 579 files.

Still draft on purpose: the human security review required by MAINTAINERS.md for this surface has not happened, and this PR does not merge without it.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

WZBbiao pushed a commit to WZBbiao/opencodex that referenced this pull request Aug 6, 2026
@lidge-jun
lidge-jun marked this pull request as ready for review August 6, 2026 04:19

@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: 4874390dd1

ℹ️ 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 thread src/oauth/health.ts
Comment on lines 379 to 381
const res = await fetchImpl(
`http://${probeHostname(live.hostname)}:${live.port}/api/codex-auth/accounts`,
{ headers, signal: AbortSignal.timeout(4000) },

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 Bind attestation to the bearer-bearing request

If the attested proxy exits and another local process binds the same port between the /healthz response and this second fetch, the replacement listener receives the long-lived admin bearer without knowing the attestation secret. Because these are separate HTTP requests—and the health response body is not consumed—the proof authenticates only the earlier connection, not the listener receiving Authorization; authenticate the management request itself with a secret-derived one-time proof or otherwise avoid transmitting the raw bearer after a separate probe. This credential-exposure path is a release-blocking security-boundary issue.

AGENTS.md reference: AGENTS.md:L218-L224

Useful? React with 👍 / 👎.

Comment thread src/lib/bun-runtime.ts
const source = reportedBunRuntimeSource(env);
const path = env[BUN_RUNTIME_PATH_ENV]?.trim();
if (!source || !path || !samePath(path, process.execPath)) return null;
return { path, source, overrideEnv: BUN_OVERRIDE_ENV };

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 Canonicalize the recorded runtime before persisting it

When an untrusted environment supplies OCX_BUN_RUNTIME_PATH as a symlink that currently resolves to process.execPath, samePath accepts it but this return preserves the attacker-controlled symlink name. durableBunRuntime() then embeds that name into a service or Codex shim, so the repository can retarget the symlink after installation and make the durable launcher execute an arbitrary binary. This remains reachable from a direct Bun/legacy launch where project dotenv can supply the otherwise unauthenticated marker pair; return the verified process.execPath (as the previous provenance path did), a fixed canonical path, or authenticate the pair before allowing it into durable artifacts.

AGENTS.md reference: AGENTS.md:L218-L224

Useful? React with 👍 / 👎.

@lidge-jun
lidge-jun merged commit 9795aeb into dev Aug 6, 2026
32 checks passed
lidge-jun added a commit that referenced this pull request Aug 6, 2026
docs(devlog): #936 security-review verdict + v2.10.2 release train ledger
chrisae9 pushed a commit to chrisae9/opencodex that referenced this pull request Aug 6, 2026
@lidge-jun
lidge-jun deleted the codex/916-trust-boundaries 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

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants