Skip to content

fix(router): preserve policy authorization across fallback redirects (carry #6380) - #6396

Merged
lidge-jun merged 11 commits into
devfrom
codex/rel-l3-6380-policy-scope
Oct 1, 2026
Merged

lidge-jun merged 11 commits into
devfrom
codex/rel-l3-6380-policy-scope

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

Carries #6380 by @luvs01 onto a maintainer branch and adds one maintainer fix found in its security review. The original commits are kept as they are, with their authors.

What #6380 fixes. Policy fallback could authorize a redirect from the bounded diagnostic trace or reselect a public alias, so a retry could leave the provider/model set the original policy evaluation allowed. The fix keeps the first evaluation's full membership as request-owned state (src/server/responses/policy-request-scope.ts), runs every retry as a concrete candidate, and checks the final destination before dispatch. Subagent selection, encrypted-task recovery, alias collisions, and virtual wire models stay inside that membership. A candidate outside it is refused locally with 409 policy_candidate_unavailable.

Maintainer fix (behavior change, not a mechanical carry): 5cb070123c "fix(responses): capture policy scope only for an accepted shadow target". The shadow-call intercept resolved its target through the same capture that fixes the request's policy scope, before deciding whether to intercept. When the target was a policy profile and interception was declined (source and target intersect), the request kept its own model but carried the profile's eligibility. A model outside the profile was refused with 409, and a model inside it was relabeled as a policy route, which made it eligible for policy fallback to providers it never selected. The shadow probe now skips the capture, and the accepted target is captured where it becomes the request's route. A regression case in tests/routing/routing-policy-dispatch-scope.test.ts covers both selectors, and structure/transports/policy-fallback.md records the rule.

Carried from #6380. Original author: @luvs01.

Co-authored-by: luvs01 27862058+luvs01@users.noreply.github.com

Verification

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. A MAINTAINERS.md security review is requested for the routing authorization change. An independent verdict bound to the exact head will be posted before merge.

Summary by CodeRabbit

  • Bug Fixes

    • Policy-based fallback and recovery remain within candidates allowed by the original routing decision, including when redirects or subagents are involved.
    • Retries avoid destinations already attempted through another candidate. When a candidate is skipped locally, the original upstream failure is preserved.
    • Virtual models retain their normal wire-model mapping without requiring the wire identifier to be separately allowlisted.
  • Documentation

    • Clarified candidate eligibility, redirects, retries, and failure handling during policy-based fallback.

luvs01 and others added 10 commits September 29, 2026 00:29
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… traces truncate

An eligibility probe that throws (e.g. a blocked-model redirect cycle) now skips that fallback candidate instead of selecting it and dead-ending the retry. Redirect membership uses the full policy evaluation result carried on the request context, so an eligible destination truncated out of the bounded trace can still receive a hop. Eligibility probes run through previewRouteModel so they never write combo selection state.
…d scope

The dev merge made policy fallback authorization request-owned: the retry's
resolved route must sit inside the eligibility captured from the real first
route, and the candidate probe resolves each trace row concretely. Synthetic
traces can no longer authorize candidates the real evaluation never admitted,
so the fixtures now declare them for real: the second candidate joins the
native-hop profile, and opencode-go-2 joins the provider map.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The shadow-intercept probe resolved its target through the request's policy capture, so a declined policy-profile target left its eligibility and policy route kind on the request's own route. Probe without the capture and capture only when interception is accepted.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner October 1, 2026 13:03
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T13:08:02.924253Z 5cb0701 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (2)
src/AGENTS.md — auto-discovered
structure/AGENTS.md — auto-discovered

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 188a2927-e59d-4f79-b2c0-ceb03b53134a

📥 Commits

Reviewing files that changed from the base of the PR and between 5cb0701 and 10644c2.

📒 Files selected for processing (6)
  • scripts/test-layout/layout.json
  • src/server/responses/compact.ts
  • src/server/responses/core-normalize.ts
  • src/server/responses/request-prepare.ts
  • structure/transports/responses.md
  • tests/fixtures/test-layout-expected.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Policy routes retain their original eligible provider/model set through request preparation and fallback. Retries resolve concrete candidates, reject out-of-scope redirects, and skip previously attempted physical destinations. Tests and documentation cover subagent recovery, virtual-model mapping, and attempt-state preservation.

Changes

Policy-Scoped Fallback Dispatch

Layer / File(s) Summary
Carry policy eligibility through routing
src/router.ts, src/server/request-log.ts, src/server/responses/core-options.ts, src/server/responses/policy-request-scope.ts, src/server/responses/compact.ts, src/server/responses/core-normalize.ts
Route results expose full policy eligibility, including candidates outside the bounded trace. Request-scope helpers retain the policy decision and eligibility, validate normalized routes, and track prepared destinations.
Enforce policy scope during request preparation
src/server/responses/request-prepare.ts, tests/routing/routing-policy-dispatch-scope.test.ts, tests/helpers/responses-core-source.ts, scripts/test-layout/layout.json, tests/fixtures/test-layout-expected.json, tests/responses/responses-shadow-intercept.test.ts, tests/routing/router-blocked-cross-provider.test.ts, structure/subagents.md, structure/transports/responses.md
Concrete fallback selections use concrete routing and skip selector rewrites. Preparation checks policy scope across shadow interception, subagent reroutes, recovery, and final normalization. Tests cover scoped routes, unscoped routes, selector behavior, and request-log handling.
Constrain retries and preserve attempt state
src/server/responses/policy-fallback.ts, tests/routing/routing-policy-dispatch-scope.test.ts, tests/routing/routing-policy-fallback.test.ts, tests/claude-integration/claude-native-affinity.test.ts, tests/providers/opencode-go-session-header.test.ts, docs-site/src/content/docs/reference/configuration/routing.md, structure/INDEX.md, structure/manifest.json, structure/runtime.md, structure/transports/policy-fallback.md, structure/transports/responses-failover.md
Fallback checks redirected candidates against full policy eligibility and skips repeated wire destinations. Local candidate refusals restore failed-attempt log state. Tests cover redirects, virtual models, retries, and spend settlement. Documentation describes these constraints and retained upstream failure context.

Priority: ➖ Normal

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

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant handleResponsesWithPolicyFallback
  participant prepareResponsesRequest
  participant routeConcreteModel
  participant PolicyRequestScope
  participant Upstream
  handleResponsesWithPolicyFallback->>prepareResponsesRequest: Concrete fallback candidate and request scope
  prepareResponsesRequest->>routeConcreteModel: Resolve candidate route
  routeConcreteModel-->>prepareResponsesRequest: Provider and model
  prepareResponsesRequest->>PolicyRequestScope: Validate route and record prepared destination
  prepareResponsesRequest->>Upstream: Dispatch prepared request
Loading

Merge Risk: ⚪ Minimal · up to 10644

The change retains original policy authorization across routing and retries. No actionable merge-blocking issue is established; merge after normal exact-head checks pass.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 10644

The change strengthens authorization during redirects and retries. No new authorization bypass was established in the reviewed paths, but incomplete recovery and concurrency coverage leaves some residual uncertainty.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The relevant exposure is a policy-routed request reaching an unintended configured provider/model, with possible disclosure of request content or unintended credential-domain use. The reviewed head controls constrain that request's destination changes; they do not establish tenant-wide, datastore, or deployment-wide exposure.

Trust Boundaries and Controls

  • observed — A client selector cannot directly supply the internal policy scope. Concrete fallback candidates are checked against retained membership after redirects, and retry construction removes authorization and account headers before crossing credential domains while retaining typed admission context.
  • observed — Compact handling independently checks its resolved route against admission scope before direct native dispatch and now records full policy eligibility. This guard is separate from ordinary Responses preparation.

Resilience and Maintainability Implications

  • observed — Fallback prefiltering recognizes virtual-model wire mappings but not the later account-gated canonical wire rewrite. A candidate can therefore pass prefiltering and be rejected as a duplicate during preparation. The final guard precedes dispatch, and local refusal preserves the previous upstream failure; no duplicate send or authorization bypass was established from this mismatch.

Hardening Proposals

  • proposed — Use one explicit physical-destination identity contract for fallback prefiltering and final preparation, including account-gated wire mappings, while retaining the final duplicate guard. This would reduce control drift without treating the current fail-closed mismatch as a verified vulnerability.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 15 files. (3 skipped:… 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 and concisely describes the main change: preserving policy authorization across fallback redirects. It matches the implementation and PR objectives.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 15 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

@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: 5cb070123c

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

}
}

const localRefusals = new WeakSet<Response>();

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 Preserve the refusal marker across response wrapping

On the normal /v1/responses path, handleResponsesCore owns a translator budget, and finalizeOwnedTranslatorBudget replaces every body-bearing response with a new Response. Because this marker is keyed only by the original object, isPolicyCandidateRefusal returns false after the core call. If policy scope locally rejects a subagent/recovery destination or a duplicate candidate after an upstream failure, fallback therefore returns the synthetic 409 or overwrites the last real failure instead of skipping to another eligible candidate. Propagate the marker through response wrappers or use internal metadata that survives wrapping, and cover the default runCore path rather than only the test stub.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Oct 1, 2026
…cy-scope

# Conflicts:
#	src/server/responses/request-prepare.ts
#	structure/transports/responses.md
@lidge-jun

Copy link
Copy Markdown
Owner Author

Maintainer integration record (MAINTAINERS.md, dev-only) — DRAFT from lane L3

  • PR fix(router): preserve policy authorization across fallback redirects (carry #6380) #6396, maintainer carry of fix(router): preserve policy authorization across fallback redirects #6380 by @luvs01: fix(router): preserve policy authorization across fallback redirects.

  • Exact head: 10644c22a439d33e7ee14aa5417bbdfcffa9fe5c. That is 05bc47d728 from fix(router): preserve policy authorization across fallback redirects #6380, plus the maintainer fix 5cb070123c, plus a merge of origin/dev 7f6b5b7389 that resolves a conflict with fix(xai): authorize the Fast wire model across API endpoints #6373 in src/server/responses/request-prepare.ts and structure/transports/responses.md. The resolution keeps fix(xai): authorize the Fast wire model across API endpoints #6373's Fast-wire admission assert at the end of captureInboundRoutePolicy, keeps this PR's conditional policy capture at the start, and joins the two docs rows.

  • Hosted CI at this head: Cross-platform CI run 36880870084 passed (pull_request). The pre-merge head 5cb070123c also passed run 36866025794 (pull_request). Jobs: test 1/4 through test 4/4, gates, structure gate, storage policy, docker smoke, api usage, docs site build, keyring and npm-global legs, ci. React Doctor and hygiene passed. enforce-target passed at 5cb070123c after a re-run. At 10644c22a4, both of its runs (36880863903, 36881237997) were still queued for a runner when this was written. The dev ruleset has no required status checks, so this does not block the merge. The PR description is unchanged since the last pass.

  • Local suites: not run, on explicit owner instruction.

  • Security review (routing authorization): the independent review of 05bc47d728 found a P2. The shadow-intercept probe captured the request's policy scope before deciding to intercept, so a declined policy-profile target scoped the request's own route: a 409, or a policy relabel that enabled fallback to unselected providers. 5cb070123c fixes it and adds a regression case. The re-review returned PASS bound to 5cb070123c54ed2c6e068dd6ac64d21afe188b45. A further re-attestation of the merge resolution returned PASS bound to 10644c22a439d33e7ee14aa5417bbdfcffa9fe5c. Admission scope still runs on every route; the policy capture comes first and can only narrow or refuse a route. The reviewers also confirmed there are no src/lab imports on the core request path, the scope is per request, and the final destination is checked before dispatch.

  • Review threads on fix(router): preserve policy authorization across fallback redirects #6380: none open. CodeRabbit hit its rate limit and did not review.

  • Attribution: commits by @luvs01 (two co-written with Devin AI). The squash carries Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>. After merge, close fix(router): preserve policy authorization across fallback redirects #6380 with a link to this PR.

  • Enforce PR target branch: every run for this head has sat in the Actions queue for hours, after CodeRabbit status wake-ups flooded it, and resolve-pr never started. The gate never executes head code. I checked its conditions by hand at this head: base is dev, and the Summary, Verification and Checklist sections are present (gui files: 0, screenshots: 0). PR hygiene passed at this head.

@lidge-jun
lidge-jun merged commit 58a26f0 into dev Oct 1, 2026
33 of 34 checks passed
@lidge-jun
lidge-jun deleted the codex/rel-l3-6380-policy-scope branch October 1, 2026 16:12
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