Generalize Genie bridge to HTTP envelopes

Carry small HTTP request/response envelopes over the framework-mediated Agent bridge, switch the Genie auth probe to the real /internal/auth/status response body, and update the Android refactor doc to reflect the current bridge shape.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Iliyan Malchev
2026-03-19 00:51:24 -07:00
parent e03e28b38d
commit d5679d7c06
4 changed files with 173 additions and 59 deletions

View File

@@ -15,6 +15,7 @@ class CodexAgentService : AgentService() {
private const val BRIDGE_REQUEST_PREFIX = "__codex_bridge__ "
private const val BRIDGE_RESPONSE_PREFIX = "__codex_bridge_result__ "
private const val METHOD_GET_AUTH_STATUS = "get_auth_status"
private const val METHOD_HTTP_REQUEST = "http_request"
}
private val handledBridgeRequests = ConcurrentHashMap.newKeySet<String>()
@@ -70,9 +71,38 @@ class CodexAgentService : AgentService() {
JSONObject()
.put("requestId", requestId)
.put("ok", false)
.put("error", err.message ?: err::class.java.simpleName)
},
)
.put("error", err.message ?: err::class.java.simpleName)
},
)
METHOD_HTTP_REQUEST -> {
val httpMethod = requestJson.optString("httpMethod")
val path = requestJson.optString("path")
val body = if (requestJson.isNull("body")) null else requestJson.optString("body")
if (httpMethod.isBlank() || path.isBlank()) {
JSONObject()
.put("requestId", requestId)
.put("ok", false)
.put("error", "Missing httpMethod or path")
} else {
runCatching {
CodexdLocalClient.waitForResponse(this, httpMethod, path, body)
}.fold(
onSuccess = { httpResponse ->
JSONObject()
.put("requestId", requestId)
.put("ok", true)
.put("statusCode", httpResponse.statusCode)
.put("body", httpResponse.body)
},
onFailure = { err ->
JSONObject()
.put("requestId", requestId)
.put("ok", false)
.put("error", err.message ?: err::class.java.simpleName)
},
)
}
}
else -> JSONObject()
.put("requestId", requestId)
.put("ok", false)

View File

@@ -9,13 +9,23 @@ import java.io.BufferedInputStream
import java.nio.charset.StandardCharsets
object CodexdLocalClient {
data class HttpResponse(
val statusCode: Int,
val body: String,
)
data class AuthStatus(
val authenticated: Boolean,
val accountEmail: String?,
val clientCount: Int,
)
fun waitForAuthStatus(context: Context): AuthStatus {
fun waitForResponse(
context: Context,
method: String,
path: String,
body: String?,
): HttpResponse {
context.startForegroundService(
android.content.Intent(context, CodexdForegroundService::class.java).apply {
action = CodexdForegroundService.ACTION_START
@@ -25,60 +35,93 @@ object CodexdLocalClient {
)
repeat(30) {
fetchAuthStatus(CodexSocketConfig.DEFAULT_SOCKET_PATH)?.let { return it }
runCatching {
executeRequest(CodexSocketConfig.DEFAULT_SOCKET_PATH, method, path, body)
}.getOrNull()?.let { return it }
Thread.sleep(100)
}
throw IOException("codexd unavailable")
}
fun waitForAuthStatus(context: Context): AuthStatus {
val response = waitForResponse(context, "GET", "/internal/auth/status", null)
if (response.statusCode != 200) {
throw IOException("HTTP ${response.statusCode}: ${response.body}")
}
return parseAuthStatus(response.body)
}
fun fetchAuthStatus(socketPath: String): AuthStatus? {
return try {
val socket = LocalSocket()
val address = CodexSocketConfig.toLocalSocketAddress(socketPath)
socket.connect(address)
val request = buildString {
append("GET /internal/auth/status HTTP/1.1\r\n")
append("Host: localhost\r\n")
append("Connection: close\r\n")
append("\r\n")
}
val output = socket.outputStream
output.write(request.toByteArray(StandardCharsets.UTF_8))
output.flush()
val responseBytes = BufferedInputStream(socket.inputStream).use { it.readBytes() }
socket.close()
val responseText = responseBytes.toString(StandardCharsets.UTF_8)
val splitIndex = responseText.indexOf("\r\n\r\n")
if (splitIndex == -1) {
val response = executeRequest(socketPath, "GET", "/internal/auth/status", null)
if (response.statusCode != 200) {
return null
}
val statusLine = responseText.substring(0, splitIndex)
.lineSequence()
.firstOrNull()
.orEmpty()
val statusCode = statusLine.split(" ").getOrNull(1)?.toIntOrNull() ?: return null
if (statusCode != 200) {
return null
}
val body = responseText.substring(splitIndex + 4)
val json = JSONObject(body)
val accountEmail =
if (json.isNull("accountEmail")) null else json.optString("accountEmail")
val clientCount = if (json.has("clientCount")) {
json.optInt("clientCount", 0)
} else {
json.optInt("client_count", 0)
}
AuthStatus(
authenticated = json.optBoolean("authenticated", false),
accountEmail = accountEmail,
clientCount = clientCount,
)
parseAuthStatus(response.body)
} catch (_: Exception) {
null
}
}
fun executeRequest(
socketPath: String,
method: String,
path: String,
body: String?,
): HttpResponse {
val socket = LocalSocket()
val address = CodexSocketConfig.toLocalSocketAddress(socketPath)
socket.connect(address)
val payload = body ?: ""
val request = buildString {
append("$method $path HTTP/1.1\r\n")
append("Host: localhost\r\n")
append("Connection: close\r\n")
if (body != null) {
append("Content-Type: application/json\r\n")
}
append("Content-Length: ${payload.toByteArray(StandardCharsets.UTF_8).size}\r\n")
append("\r\n")
append(payload)
}
val output = socket.outputStream
output.write(request.toByteArray(StandardCharsets.UTF_8))
output.flush()
val responseBytes = BufferedInputStream(socket.inputStream).use { it.readBytes() }
socket.close()
val responseText = responseBytes.toString(StandardCharsets.UTF_8)
val splitIndex = responseText.indexOf("\r\n\r\n")
if (splitIndex == -1) {
throw IOException("Invalid HTTP response")
}
val statusLine = responseText.substring(0, splitIndex)
.lineSequence()
.firstOrNull()
.orEmpty()
val statusCode = statusLine.split(" ").getOrNull(1)?.toIntOrNull()
?: throw IOException("Missing status code")
return HttpResponse(
statusCode = statusCode,
body = responseText.substring(splitIndex + 4),
)
}
private fun parseAuthStatus(body: String): AuthStatus {
val json = JSONObject(body)
val accountEmail =
if (json.isNull("accountEmail")) null else json.optString("accountEmail")
val clientCount = if (json.has("clientCount")) {
json.optInt("clientCount", 0)
} else {
json.optInt("client_count", 0)
}
return AuthStatus(
authenticated = json.optBoolean("authenticated", false),
accountEmail = accountEmail,
clientCount = clientCount,
)
}
}