Skip to content

feat(messages): OAuth, beta allowlist and opaque-state guard on the native lane (PF-10) - #5819

Closed
lidge-jun wants to merge 15 commits into
feat/pf11-protocol-evidence-guifrom
feat/pf10-auth-opaque-state
Closed

lidge-jun wants to merge 15 commits into
feat/pf11-protocol-evidence-guifrom
feat/pf10-auth-opaque-state

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

PF-10 of the protocol-first-class unit (devlog/_plan/260924_protocol_first_class/020_engine_and_codecs.md#pf-10-auth-and-opaque-state). Stacked on #5817 (PF-11).

Extends the managed native Messages lane (PF-08) to Anthropic OAuth and makes what it forwards explicit. Everything stays behind switches that default off.

  • anthropic-beta allowlist (src/adapters/anthropic/beta-allowlist.ts): the native builder takes the caller's anthropic-beta header only as an explicit argument and re-emits allowlisted values from the allowlist's own spelling, so no caller bytes reach the wire. The allowlist starts with interleaved-thinking-2025-05-14, first-party Anthropic only; Anthropic-compatible hosts get no caller betas. Dropped values are traced with the fixed reason anthropic-beta-dropped (never the header text).
  • OAuth native Messages behind managedMessagesNativeOAuth (effective only with managedMessagesNative): the unpooled anthropic provider on https://api.anthropic.com. The account is resolved at dispatch the way the bridge resolves an unpooled account (capture, resolve, commit against the capture) and re-checked before every send; planning never selects or refreshes. The OAuth header placement moves unchanged into an exported helper the adapter and the native builder share. An OAuth request never carries x-api-key, and an OAuth token is refused for any other host. Pooled OAuth (anthropicAccountPool.enabled or a failover quorum) declines with oauth-account-pool and stays on the bridge. Declared tool names get the OAuth prefix and are mapped back in streamed and JSON answers; a prefix collision is refused.
  • Opaque state (src/protocols/opaque-state.ts): on the native lane, thinking signatures and redacted_thinking blocks are kept only for first-party Anthropic. For any other destination they are removed from a copy and traced as opaque-state-stripped; under unrepresentable: "reject" the request is refused before any send.
  • New reason codes oauth-account-pool, anthropic-beta-dropped, opaque-state-stripped; PROTOCOL_CONTRACT_VERSION → 2026-09-25.2.

Verification

  • bun x tsc --noEmit: exit 0 on this head.

  • bun run structure:check, bun run privacy:scan: passed.

  • Tests (tests/adapters/anthropic/anthropic-beta-allowlist.test.ts, anthropic-messages-passthrough-oauth.test.ts, tests/responses/protocol-opaque-state.test.ts, messages-native-oauth-eligibility.test.ts, tests/claude-integration/messages-native-oauth.test.ts, messages-native-opaque-state.test.ts) are registered and run in the full local suite below.

  • Full local run on the stack head (feat(protocols): protocol paths as a first-class concern — PF-01..PF-12 #5820, which contains this change): bun run test — the only failures are Lab CL-03/CL-07/CL-08/SEC-02 and release helper timeouts, which fail identically on a checkout without this stack (local environment), plus service/toggle cases that pass when run alone; cd gui && bun test --isolate tests — 2398 pass, 0 fail.

  • CI on this head: all required checks pass.

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.

…state

The native Messages lane needs fixed codes for a pooled OAuth decline, a caller beta it
refused to forward and opaque thinking state it removed. New codes: contract 2026-09-25.2.
A caller's anthropic-beta can change what the provider accepts and bills. Only listed
values survive, re-spelled from the list, per first-party or compatible destination.
…main

Signatures and redacted_thinking can only be verified where they were minted. A native
send keeps them for api.anthropic.com and removes them, copy-on-write, everywhere else.
…plies

The native Messages lane must authenticate an OAuth account exactly as the adapter does.
Moving the block into applyAnthropicOAuthAuth changes no header and no order.
…nd opaque state

The builder takes the caller's beta header explicitly, keeps signatures for first-party
Anthropic only, and shapes an OAuth request as the adapter does, to api.anthropic.com only.
Behind managedMessagesNativeOAuth, the anthropic OAuth provider to api.anthropic.com is
eligible. A pooled account set declines: its rotation and affinity stay in the bridge.
Mirrors the unpooled transport steps: capture, resolve, commit against the capture, and
re-check before each send. A pooled set fails closed; renamed tool names map back.
…d opaque guard

The lane resolves an OAuth account at dispatch only, re-checks it before each send, maps
renamed tools back, and records dropped betas and stripped opaque state by fixed code.
… state at ingress

The ingress supplies the OAuth quorum fact, passes only anthropic-beta to the lane, and
under reject refuses a non-Anthropic native route that would lose thinking signatures.
The preview reads config only; the stored-account quorum is a dispatch-time fact, so a
preview never touches the OAuth store.
…shape

Unknown betas never reach the wire, a compatible host gets none, and an OAuth build matches
the adapter's headers, prefixes client tools only, and refuses any host but Anthropic's.
Signatures stay with first-party Anthropic and rebuild from the envelope per domain; OAuth
needs both switches, is judged from config in planning, and a pool declines.
…te and betas

The selected account's token is sent and never the caller's, tool names map back, reject
refuses before a send, and dropped betas appear in the trace by code only.
…state and OAuth

protocol-paths owns the three PF-10 rules and their tests; the 040 inventory records OAuth
native, the pooled-account decline and the opaque-state rule on the native lane.
…rivacy scan

A literal bearer header and a userinfo URL read as secrets to privacy:scan; constructing
them from parts keeps the same assertions without tripping the scanner.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 25, 2026 04:32
@coderabbitai

coderabbitai Bot commented Sep 25, 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: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 0165412a-769a-4f2b-9ce6-c6bd93203599

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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 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-09-25T04:38:46.422656Z 6af9cce 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.

@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: 6af9cce397

ℹ️ 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 +65 to +67
function pooled(config: OcxConfig): boolean {
return isAnthropicAccountPoolEnabled(config) || hasAnthropicFailoverQuorum();
}

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 Re-read the OAuth quorum at dispatch

hasAnthropicFailoverQuorum() caches results for two seconds, but adding an account through saveCredential does not invalidate that cache. If a request caches false while one account exists and the operator then logs in a second usable account, both ingress eligibility and this dispatch guard reuse false, so a request is sent through the native lane even though the bridge is now required to provide account rotation and affinity. Invalidate the quorum cache on account-store changes or use an uncached roster check for this dispatch-time guard.

Useful? React with 👍 / 👎.

Comment on lines +143 to +147
const renames = (name: unknown): name is string => typeof name === "string" && declared.has(name);
if (isRec(body.tool_choice) && body.tool_choice.type === "tool" && renames(body.tool_choice.name)) {
out.tool_choice = { ...body.tool_choice, name: wireName(body.tool_choice.name) };
}
const isRenamedUse = (block: unknown): block is Rec & { name: string } => isRec(block) && block.type === "tool_use" && renames(block.name);

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 Prefix historical custom tools even when no longer declared

The history rewrite is restricted to names present in the current tools array. A valid continuation can retain an earlier assistant tool_use/user tool_result pair while omitting that old tool from the current declarations; in that case an ordinary name such as lookup is sent unchanged even though Anthropic OAuth rejects arbitrary unprefixed tool names, whereas the existing adapter transforms every historical tool call. Rewrite historical custom names independently of current declarations, or decline the native lane when their classification is ambiguous.

AGENTS.md reference: src/AGENTS.md:L19-L20

Useful? React with 👍 / 👎.

@github-actions github-actions Bot added the enhancement New feature or request label Sep 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 58 / 80

이 변경은 Messages 요청을 프록시가 Anthropic에게 직접 보내는 길에, 로그인 계정(OAuth)도 올라탈 수 있게 한다. 스위치는 두 개고, 둘 다 기본이 꺼져 있다. managedMessagesNative가 켜져야 하고, 그 위에 managedMessagesNativeOAuth가 켜져야 로그인 계정이 이 길을 탄다.

타는 조건은 좁다. 프로바이더 이름이 anthropic이고, 주소가 https://api.anthropic.com이며, 쓸 수 있는 계정이 하나일 때다. 계정 풀이 켜져 있거나 쓸 수 있는 계정이 둘 이상이면 oauth-account-pool로 거절하고 예전 다리에 남긴다. 계정 바꾸기와 대화 붙이기는 다리가 하기 때문이다.

호출자가 보낸 anthropic-beta는 그대로 전달하지 않는다. 목록에 있는 값만 목록의 철자로 다시 적는다. 지금 목록은 interleaved-thinking-2025-05-14 하나다. Anthropic이 아닌 호스트에는 호출자 베타를 하나도 안 붙인다. 빠진 값은 내용 없이 anthropic-beta-dropped로만 기록한다.

생각 블록의 서명과 redacted_thinking은 Anthropic 본사로만 간다. 다른 주소면 원본은 두고 복사본에서 지운 뒤 opaque-state-stripped를 남긴다. 설정이 unrepresentable: "reject"이면 보내기 전에 400으로 거절한다.

로그인으로 보낼 때는 기존 어댑터와 같은 헤더를 쓴다. x-api-key는 안 붙이고, 본사 주소가 아니면 토큰 자체를 거절한다. 호출자가 선언한 도구 이름에는 custom_을 붙이고, 답의 tool_use 이름은 다시 원래 이름으로 돌린다.

src/adapters/anthropic/passthrough.ts anthropicOAuthWireBody - custom_은 이번 요청의 tools에 적힌 클라이언트 도구에만 붙는다. 이전 턴의 tool_use가 lookup처럼 일반 이름인데 이번 tools에는 그 이름이 없으면 그대로 나간다. OAuth는 접두사 없는 임의 이름을 거절한다. bash처럼 타입이 이름을 고정하는 도구는 함부로 접두사를 붙이면 안 되므로, 이번 목록에 없는 예전 이름은 이 길을 포기하고 다리로 보내는 편이 맞다.

src/server/messages-native-oauth.ts pooled - 계정이 둘 이상인지는 hasAnthropicFailoverQuorum()이 2초간 저장한 참/거짓으로 판단한다. saveCredential은 이 저장을 지우지 않는다. 계정이 하나일 때 거짓을 읽어 둔 뒤 2초 안에 두 번째 계정이 생기면, 입장 검사와 보내기 직전 검사가 둘 다 거짓을 본다. 그러면 회전이 필요한 요청이 네이티브로 나간다. 모듈 주석은 이 지연이 429 한 번을 놓치는 정도라고 적어 두었는데, 이번 길은 그 참/거짓으로 길 자체를 고른다. 보내기 직전에는 저장소를 다시 읽어야 한다.

src/protocols/opaque-state.ts opaqueStateForDestination - redacted_thinking만 있는 assistant 턴은 메시지를 통째로 뺀다. 그 턴이 user와 user 사이에 혼자 있으면 남는 순서가 user, user가 된다. Anthropic은 같은 역할이 연속되면 요청을 거절한다. 테스트 픽스처는 그 앞에 내용이 남은 assistant 턴이 있어서, 이 경우는 통과만 하고 구멍은 안 잡힌다.

메인테이너의 판단이 필요한 지점

베이스가 dev가 아니라 feat/pf11-protocol-evidence-gui다. #5817 위에 쌓인 변경이라, 아래 PR이 바뀌거나 닫히면 이 커밋을 다시 맞춰야 한다.

계획 미리보기는 계정 저장소를 안 읽는다. 풀 스위치만 본다. 계정은 둘인데 풀이 꺼져 있으면 미리보기는 네이티브라고 하고, 실제 요청은 oauth-account-pool로 다리에 남는다. 미리보기가 계정을 만지지 않게 한 선택이니, 그 차이를 안내 문구에 둘지 그대로 둘지만 정하면 된다.

본문은 이 헤드에서 tsc, structure:check, privacy:scan은 통과했다고 적고, 등록된 테스트의 전체 결과는 나중에 붙인다고 한다. 그 결과가 오기 전에는 테스트가 이 헤드에서 통과했는지 본문에 없다.

너의 추천

토큰을 본사 주소로만 보내고, 풀은 다리에 남기며, 스위치 기본값이 꺼진 줄기는 유지하면 된다. 머지 전에 위 세 곳만 손보는 게 좋다. 2초 기억 때문에 계정이 둘인 요청이 네이티브로 새는 것과, 예전 도구 이름이 접두사 없이 나가는 것이 먼저다. redacted_thinking만 있는 턴을 지운 뒤 역할이 겹치면 그 요청은 거절하거나 다리로 돌리면 된다.

이 댓글은 grok-bot이 작성했습니다

@devin-ai-integration devin-ai-integration Bot added the priority: P3 Low: new provider/client integration, large or experimental feature (>2000 LOC or >50 files), RFC/ro label Sep 25, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Maintainer triage: priority: P3 — protocol-first-class series PF-10 (default off; touches OAuth, needs security review).

Criteria (P3): Low: new provider/client integration, large or experimental feature (>2000 LOC or >50 files), RFC/roadmap, or long-stale branch.

Related / overlapping PRs:

lidge-jun added a commit that referenced this pull request Sep 25, 2026
…12 (#5820)

Squash of the protocol-first-class stack #5808, #5809, #5810, #5811, #5812, #5813, #5814, #5815, #5816, #5817, #5819 and #5820. Every new lane sits behind a protocols.rollout switch that defaults off.
@lidge-jun

Copy link
Copy Markdown
Owner Author

Landed in dev as part of the single squash of the protocol-first-class stack: #5820 (0f4c8d4).

@lidge-jun lidge-jun closed this Sep 25, 2026
@lidge-jun
lidge-jun deleted the feat/pf10-auth-opaque-state branch September 26, 2026 01:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request priority: P3 Low: new provider/client integration, large or experimental feature (>2000 LOC or >50 files), RFC/ro

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant