Skip to content

fix(devin): bind tenant hosts to the transmitted key and sanitize Connect trailers - #5574

Merged
lidge-jun merged 10 commits into
devfrom
codex/260922-next-devin-boundary
Sep 22, 2026
Merged

lidge-jun merged 10 commits into
devfrom
codex/260922-next-devin-boundary

Conversation

@lidge-jun

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

Copy link
Copy Markdown
Owner

Summary

Devin requests could reach the wrong tenant, and Connect error trailers could echo credential-bearing upstream text. This carries the consolidated contributor fix from #5541 and adds one maintainer repair for the part it left open.

Carried from #5541 (eight commits, original authorship kept; it already consolidates #5143 and #5455, which are not re-applied):

  • Connect trailer diagnostics expose only an allowlisted error code, the hexadecimal trace id, and a typed retry delay rendered in our own wording. Raw trailer text stays inside the parser.
  • The stated reset delay survives sanitization as retryAfterSeconds, so the shared retry helper still schedules bounded replays for callers that opt in.
  • The adapter passes maxWaitMs: 0, so a provider 429 surfaces immediately and the admitted turn releases its shared capacity instead of sleeping while holding it.
  • Alias-slot tenant hosts are used only when that credential owns the transmitted key.

Maintainer repair (e24ef95): the literal credential slot still lent its active account's EU or FedStart host to any key. Devin keeps several accounts per provider id, and the request path injects the admitted account's token, which need not be the active one; a provider-configured key or forwarded bearer is not stored at all. resolveDevinApiServer now takes a stored tenant host only from the account whose credential access equals the transmitted key, searching the configured provider id and then its deprecated alias, active account first. Otherwise it uses the validated configured base URL or the US default.

Before: a configured key with a FedStart login in the devin slot dispatched to the FedStart host. After: it dispatches to the configured base URL. A pooled non-active EU account still reaches its own EU host.

The invalid alias-host test now passes the owning key, so it reaches the host validator instead of stopping at the ownership check.

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

Verification

  • Local tests, typecheck and build: NOT RUN (lane policy); hosted CI on the exact head is the verifier.
  • New regression cases: mismatched literal slot, alias owner beside an occupied literal slot, a key neither slot owns, no key (default and configured host), a non-active owning account, and a configured key at the adapter boundary. Existing tenant-selection cases now pass the owning key.
  • Statically inspected, no defect found and no regression added: admitted-capacity release (the runTurn attempt closes its queue when the adapter returns) and the inference/support transport split (inference uses the provider executor; catalog and JWT stay support traffic).
  • Independent security review of the repaired diff: PASS, posted as a PR comment.
  • Residual: a credential replaced between token resolution and host lookup matches nothing and uses the configured host for that turn.

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.

luvs01 and others added 10 commits September 23, 2026 02:45
Connect-RPC EOS trailers propagated upstream-controlled error.message text into adapter error events, API responses, and diagnostic logs, and that text can reflect the credential carried by the request. Parse trailers with an allowlist for known Connect codes, keep the raw message inside the parser for quota/opaque-denial detection, and throw content-free errors carrying only the allowlisted code and hex trace id.

(cherry picked from commit 50eab5a)
(cherry picked from commit 6efc759)
The trailer suppression left the thrown CloudChatError without the upstream "limit will reset in N" sentence that streamChatEventsWithResetRetry scraped for its cooldown, so a stated-reset 429 failed immediately instead of waiting out the quota window. Parse the delay inside the trailer parser while the raw text still exists and carry it as a typed retryAfterSeconds field; the retry prefers that field and keeps the message scrape for errors whose text still carries a hint. Adds a regression test that drives the real Connect EOS trailer path.

(cherry picked from commit 7b7a8b8)
(cherry picked from commit b841abd)
(cherry picked from commit 4b723ef)
(cherry picked from commit 82d452c)
(cherry picked from commit df07bc6)
(cherry picked from commit 98c9298)
… comment

(cherry picked from commit 0e73736)
(cherry picked from commit a2936ca)
The module-mock spy leaked into sibling test files in the same bun test process, so devin-stated-reset-retry and devin-stated-reset-hardening saw a stubbed helper. Drive the real runTurn with a stubbed fetch returning a Connect EOS-trailer refusal instead: the asserted signals are the surfaced error text and the physical GetChatMessage count, which fail fast under the 5s abort signal if the adapter's maxWaitMs: 0 pin is removed or weakened. Register the file in the test layout map and fixture.

(cherry picked from commit 1abc035)
(cherry picked from commit 2c4bf00)
(cherry picked from commit d225fc2)
(cherry picked from commit 9b54aeb)
Reconcile #5455 zero-wait coverage with #5143 content-free Connect diagnostics: retain 429 and generated retry timing while proving reflected credentials do not escape and only one inference send occurs. Exercise a matching deprecated alias credential through tenant dispatch and the same refusal path. Preserve the zero wait pin, host allowlist, credential binding, and typed reset field. Document the safe typed retry delay and opt-in helper replay contract.

(cherry picked from commit 6f2901b)
…tted key

The consolidated fix bound alias-slot tenant hosts to the transmitted key but
still returned the active literal slot's host for any key. Devin keeps several
accounts per provider id and the request path injects the admitted account's
token, which need not be the active one, while a configured key, a forwarded
bearer, or a test token is not stored at all. Each of those could inherit
another account's EU or FedStart tenant.

resolveDevinApiServer now looks up the stored credential whose access token is
exactly the transmitted key, in the configured provider id first and then its
deprecated alias, active account first. Only that owner's validated host is
used; an owner with an unusable host, an unmatched key, or no key falls back to
the validated configured base URL and then the US default. The lookup uses
getCredential and listAccounts, which store mocks elsewhere in the suite leave
real.

Tests pass the owning key where a stored tenant is expected, so the invalid
alias-host case now reaches the host validator. New cases cover a mismatched
literal slot, an alias owner beside an occupied literal slot, a key neither
slot owns, a missing key, a non-active owning account, and a configured key at
the adapter boundary.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 22, 2026 17:54
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 22, 2026
@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 1a621898-31c7-449f-a85d-54eec1bda63f

📥 Commits

Reviewing files that changed from the base of the PR and between d30d120 and 22ac683.

📒 Files selected for processing (15)
  • scripts/test-layout/layout.json
  • src/adapters/devin.ts
  • src/adapters/devin/cloud-direct/chat.ts
  • src/adapters/devin/cloud-direct/stated-reset-retry.ts
  • src/oauth/devin.ts
  • structure/adapters/registry.md
  • structure/providers-and-adapters.md
  • structure/runtime.md
  • structure/transports/responses.md
  • tests/fixtures/test-layout-expected.json
  • tests/providers/devin-adapter-reset-wait.test.ts
  • tests/providers/devin-adapter.test.ts
  • tests/providers/devin-hardening.test.ts
  • tests/providers/devin-login.test.ts
  • tests/providers/devin-stated-reset-retry.test.ts
 ________________________
< Code review is my jam. >
 ------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 74 / 80

이 PR은 Devin(코그니션)으로 나가는 요청이 잘못된 테넌트(EU/FedStart 같은 다른 집 주소)로 가거나, 에러 글에 키 조각이 섞여 밖으로 나가던 구멍을 막습니다. 기여자 PR #5541의 여덟 커밋을 그대로 가져오고, 메인테이너가 한 곳을 더 고쳤습니다.

예전에 Connect 스트림 끝 트레일러의 error.message를 그대로 에러로 올렸습니다. 그 문장 안에 요청에 실었던 키가 반사될 수 있었습니다. 이제는 허용된 코드 이름, 16진 추적 ID, 우리가 다시 쓴 retry after ~Ns만 밖으로 나갑니다. 원래 문장은 파서 안에만 두고, 거기서 초 단위 대기 시간만 retryAfterSeconds로 뽑습니다. 그래서 재시도 도우미는 여전히 대기 시간을 알 수 있습니다.

어댑터는 maxWaitMs: 0을 넣습니다. 이미 자리를 잡은 HTTP 턴이 429를 받으면, 그 자리에서 잠들지 않고 바로 거절을 돌려줍니다. 공유 자리를 붙잡은 채 기다려서 다른 요청을 막지 않습니다. 대기 시간은 에러 글에만 남깁니다.

호스트 고르기도 바뀌었습니다. 저장소에 적힌 테넌트 주소는, 지금 보내는 키와 access가 같은 계정에서만 씁니다. 설정에만 있는 키, 밖에서 넘긴 베어러, 테스트 키는 활성 슬롯의 EU/FedStart를 빌리지 않고, 검증된 설정 base URL 또는 미국 기본 주소로 갑니다. 같은 제공자 id 아래 비활성 계정의 키도 자기 테넌트로 갑니다. 베이스는 dev입니다. 합치면 #5541을 닫는다고 본문에 적혀 있습니다. 회귀 테스트가 테넌트 소유권, 트레일러 내용 제거, zero-wait를 여러 층에서 잡습니다. 체크리스트의 보안 항목은 아직 비어 있고, 본문은 별도 보안 리뷰를 댓글로 올리겠다고 합니다. CI gates·hygiene·structure는 통과했고, test shard 일부는 이 글을 쓸 때 아직 돌고 있었습니다.

라인 - src/oauth/devin.ts resolveDevinApiServer / findDevinCredentialOwner: 토큰을 고른 뒤 호스트를 찾기 전에 같은 슬롯의 키가 바뀌면, 소유자를 못 찾고 설정/미국 주소로 떨어집니다. 본문 residual과 같고, Devin 키가 오래 가는 키라 흔한 경주는 아니라고 적혀 있습니다. 그 턴만 주소가 어긋날 수 있다는 점은 알아 두면 좋습니다.
라인 - 열린 #5541: 이 PR이 그 수정을 흡수하고 리터럴 슬롯 빌려쓰기까지 고칩니다. 둘 다 열어 두면 같은 경계가 두 갈래로 남습니다.
라인 - PR 체크리스트 「Security-sensitive changes were reviewed…」: 아직 비어 있습니다. 본문이 약속한 독립 보안 리뷰가 올라오기 전에는 그 칸을 채우지 않는 편이 맞습니다.

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

#5541을 이 PR 머지와 함께 닫을지, 아니면 #5541에 “superseded by #5574”만 남기고 수동으로 닫을지. 보안 체크리스트는 독립 리뷰 댓글이 달린 뒤에 체크할지, 머지 전에 누가 서명할지.

너의 추천

CI test shard가 초록이면 머지해도 됩니다. 합친 뒤 #5541은 닫으세요. types.ts/config.ts 쪼개기나 미리보기 배포 이야기는 이 PR과 무관합니다.

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

@lidge-jun

Copy link
Copy Markdown
Owner Author

Independent security review of the repaired diff (origin/dev..e24ef95, merged with dev at 22ac683): PASS, no findings.

Reviewed scope and conclusions:

  • Tenant routing: a stored tenant host is selected only for the stored account whose credential exactly owns the transmitted key, the owner's host is validated against the Cognition allowlist, and every other case falls back to the validated configured base URL or the US default. The chat transport revalidates the host before sending.
  • Connect trailers: raw upstream text is kept only for local classification; outward errors carry an allowlisted code, the hexadecimal trace id, and generated retry wording.
  • Capacity and replay: the adapter's zero reset allowance refuses any positive stated wait before sleeping or replaying; the shared helper keeps bounded waits and send accounting for opted-in callers.
  • Cancellation: abort is checked before entry, after sends, in the error path, and after sleeping.
  • Tests: ownership, sanitization, zero-wait, send accounting, and cancellation assertions would fail if the corresponding guard were removed. The invalid alias-host case now reaches the validator.
  • Single-account EU/FedStart logins: the admitted OAuth token becomes the transmitted key, so exact matching finds that account's own tenant.

Local validation was not run for this lane; hosted CI on the exact head is the verifier.

@lidge-jun
lidge-jun merged commit 7841946 into dev Sep 22, 2026
37 of 40 checks passed
@lidge-jun
lidge-jun deleted the codex/260922-next-devin-boundary branch September 22, 2026 18:02
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