Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ let package = Package(
.package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", exact: "0.12.1"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.97.1"),
.package(url: "https://github.com/lynnswap/ObservationBridge.git", .upToNextMinor(from: "0.12.0")),
// CodexKit has no tagged releases yet; pin the final review response
// contract fix (lynnswap/CodexKit#17).
.package(url: "https://github.com/lynnswap/CodexKit.git", revision: "23276c81cf160b773b3f9fb5ae710d257838cad0"),
// CodexKit has no tagged releases yet; pin the reviewed native-login
// and final review response contract revision.
.package(url: "https://github.com/lynnswap/CodexKit.git", revision: "d2a694d7f633c1f01d4d260585863c0a3eae21af"),
],
targets: [
.target(
Expand Down
33 changes: 27 additions & 6 deletions Sources/CodexReviewAppServer/AppServerCodexReviewBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,20 +86,31 @@ package actor AppServerCodexReviewBackend: CodexReviewBackend, CodexModelActor {
try await appServer.rateLimits()
}

package func startLogin(_: CodexReviewBackendModel.Login.Request) async throws
package func startLogin(_ request: CodexReviewBackendModel.Login.Request) async throws
-> CodexReviewBackendModel.Login.Challenge
{
// CodexAppServerKit's loginChatGPT cannot yet forward the native
// web-authentication callback scheme to account/login/start, so the
// server never shapes the auth URL for a native session; the host
// deliberately falls back to the external browser until that API
// exists. Do not mark the challenge native here without it.
if let callbackScheme = request.nativeWebAuthenticationCallbackScheme {
let login = try await appServer.loginChatGPT(
nativeWebAuthentication: .init(callbackURLScheme: callbackScheme)
)
return login.backendChallenge()
}
let handle = try await appServer.loginChatGPT()
return try handle.backendChallenge(
nativeWebAuthenticationCallbackScheme: nil
)
}

package func completeLogin(
_ challenge: CodexReviewBackendModel.Login.Challenge,
callbackURL: URL
) async throws {
try await appServer.completeLogin(
id: .init(rawValue: challenge.id),
callbackURL: callbackURL
)
}

package func cancelLogin(_ challenge: CodexReviewBackendModel.Login.Challenge) async throws {
try await appServer.cancelLogin(id: .init(rawValue: challenge.id))
}
Expand Down Expand Up @@ -828,6 +839,16 @@ private extension CodexModel {
}
}

private extension CodexChatGPTLogin {
func backendChallenge() -> CodexReviewBackendModel.Login.Challenge {
.init(
id: id.rawValue,
verificationURL: authenticationURL,
nativeWebAuthenticationCallbackScheme: nativeWebAuthentication?.callbackURLScheme
)
}
}

private extension CodexLoginHandle {
func backendChallenge(
nativeWebAuthenticationCallbackScheme: String?
Expand Down
21 changes: 15 additions & 6 deletions Sources/CodexReviewHost/LiveCodexReviewStoreBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -865,18 +865,14 @@ private final class LiveCodexReviewStoreBackend: CodexReviewStoreBackend {
logger.info("Received ChatGPT login challenge")
let nativeCallbackScheme = challenge.nativeWebAuthenticationCallbackScheme
let usesNativeAuthentication = nativeAuthenticationConfiguration != nil
&& nativeCallbackScheme != nil
&& challenge.verificationURL != nil
auth.updatePhase(.signingIn(.init(
title: "Sign in to Codex",
detail: challenge.signInDetail(nativeAuthentication: usesNativeAuthentication),
browserURL: challenge.verificationURL?.absoluteString,
userCode: challenge.userCode
)))
guard usesNativeAuthentication,
let nativeAuthenticationConfiguration,
challenge.verificationURL != nil
else {
guard let nativeAuthenticationConfiguration, challenge.verificationURL != nil else {
if let verificationURL = challenge.verificationURL {
externalURLOpener(verificationURL)
}
Expand Down Expand Up @@ -912,6 +908,7 @@ private final class LiveCodexReviewStoreBackend: CodexReviewStoreBackend {
await self.monitorAuthenticationSession(
challenge: challenge,
session: session,
completesLoginThroughCallback: nativeCallbackScheme != nil,
auth: auth
)
}
Expand Down Expand Up @@ -946,10 +943,22 @@ private final class LiveCodexReviewStoreBackend: CodexReviewStoreBackend {
private func monitorAuthenticationSession(
challenge: CodexReviewBackendModel.Login.Challenge,
session: any CodexReviewNativeAuthentication.WebSession,
completesLoginThroughCallback: Bool,
auth: CodexReviewAuthModel
) async {
do {
_ = try await session.waitForCallbackURL()
let callbackURL = try await session.waitForCallbackURL()
guard loginChallenge?.id == challenge.id else {
return
}
guard completesLoginThroughCallback else {
logger.info("Authentication session completed; waiting for app-server login completion notification")
return
}
guard let loginBackend else {
throw CodexReviewAPI.Error.io("Authentication runtime is not available.")
}
try await loginBackend.completeLogin(challenge, callbackURL: callbackURL)
guard loginChallenge?.id == challenge.id else {
return
}
Expand Down
Loading