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,
)
}
}

View File

@@ -6,12 +6,28 @@ import java.io.IOException
object CodexAgentBridge {
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"
fun buildAuthStatusRequest(requestId: String): String {
return buildHttpRequest(requestId, "GET", "/internal/auth/status", null)
}
fun buildHttpRequest(
requestId: String,
httpMethod: String,
path: String,
body: String?,
): String {
val payload = JSONObject()
.put("requestId", requestId)
.put("method", METHOD_GET_AUTH_STATUS)
.put("method", METHOD_HTTP_REQUEST)
.put("httpMethod", httpMethod)
.put("path", path)
if (body == null) {
payload.put("body", JSONObject.NULL)
} else {
payload.put("body", body)
}
return "$BRIDGE_REQUEST_PREFIX$payload"
}
@@ -25,7 +41,25 @@ object CodexAgentBridge {
val clientCount: Int,
)
data class HttpResponse(
val statusCode: Int,
val body: String,
)
fun parseAuthStatusResponse(response: String, requestId: String): AuthStatus {
val httpResponse = parseHttpResponse(response, requestId)
if (httpResponse.statusCode != 200) {
throw IOException("HTTP ${httpResponse.statusCode}: ${httpResponse.body}")
}
val data = JSONObject(httpResponse.body)
return AuthStatus(
authenticated = data.optBoolean("authenticated", false),
accountEmail = if (data.isNull("accountEmail")) null else data.optString("accountEmail"),
clientCount = data.optInt("clientCount", 0),
)
}
private fun parseHttpResponse(response: String, requestId: String): HttpResponse {
if (!response.startsWith(BRIDGE_RESPONSE_PREFIX)) {
throw IOException("Unexpected bridge response format")
}
@@ -36,10 +70,9 @@ object CodexAgentBridge {
if (!data.optBoolean("ok", false)) {
throw IOException(data.optString("error", "Agent bridge request failed"))
}
return AuthStatus(
authenticated = data.optBoolean("authenticated", false),
accountEmail = if (data.isNull("accountEmail")) null else data.optString("accountEmail"),
clientCount = data.optInt("clientCount", 0),
return HttpResponse(
statusCode = data.optInt("statusCode", 200),
body = data.optString("body"),
)
}
}

View File

@@ -20,10 +20,14 @@ The current repo now contains the first implementation slice:
- The Genie app currently validates framework lifecycle, detached-target
requests, question flow, and result publication with a placeholder executor.
- The first Agent<->Genie bridge now uses **framework question/answer events**
for internal machine-to-machine RPC. This is intentional: runtime testing on
the emulator showed that a Genie execution runs inside the paired target
app's sandbox/UID, so ordinary cross-app Android service/provider IPC to the
Agent app is not a reliable transport.
for internal machine-to-machine RPC.
- The current bridge shape carries small request/response envelopes, and the
Genie placeholder already uses it to fetch the Agent-owned
`/internal/auth/status` response from the embedded `codexd`.
- This is intentional: runtime testing on the emulator showed that a Genie
execution runs inside the paired target app's sandbox/UID, so ordinary
cross-app Android service/provider IPC to the Agent app is not a reliable
transport.
The Rust `codexd` service/client split remains in place and is still the
existing network/auth bridge while this refactor proceeds.
@@ -46,7 +50,8 @@ existing network/auth bridge while this refactor proceeds.
the Rust runtime can migrate incrementally.
- Internal Agent<->Genie coordination must use a transport that survives the
target-app sandbox boundary. The current working bootstrap path is
AgentSDK-mediated internal question/answer exchange.
AgentSDK-mediated internal request/response exchange over question/answer
events.
## Runtime Model
@@ -92,6 +97,8 @@ existing network/auth bridge while this refactor proceeds.
- Question answering and detached-target attach controls
- Framework-mediated internal bridge request handling in `CodexAgentService`
- Framework-mediated internal bridge request issuance in `CodexGenieService`
- Generic small HTTP request/response envelopes over the internal bridge, with
the auth-status probe using the real `codexd` HTTP response body
- Abstract-unix-socket support in the legacy Rust bridge via `@name` or
`abstract:name`, so the compatibility transport can move off app-private
filesystem sockets when Agent<->Genie traffic is introduced
@@ -100,8 +107,9 @@ existing network/auth bridge while this refactor proceeds.
- Replacing the placeholder Genie executor with a real Codex runtime
- Moving network/auth mediation from `codexd` into the Agent runtime
- Replacing the temporary internal question/answer bridge with a transport that
supports richer request/response and eventually streaming semantics
- Replacing the temporary internal bridge with a transport that supports richer
request/response and eventually streaming semantics without surfacing as
framework question events
- Wiring Android-native target-driving tools into the Genie runtime
- Making the Agent the default product surface instead of the legacy service app