mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user