Remove dead Android bridge implementations

Drop the obsolete direct AIDL Agent bridge and abstract local-socket bridge, simplify the Android manifests to match the working framework session bridge path, and stop labeling codexd as legacy in the UI while it still provides auth and fallback transport.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Iliyan Malchev
2026-03-21 09:56:00 -07:00
parent 446c119f1b
commit e174cc2e77
14 changed files with 6 additions and 514 deletions

View File

@@ -1,8 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<permission
android:name="com.openai.codex.permission.BIND_AGENT_BRIDGE"
android:protectionLevel="signature" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
@@ -24,11 +20,6 @@
</intent-filter>
</service>
<service
android:name=".CodexAgentBridgeService"
android:exported="true"
android:permission="com.openai.codex.permission.BIND_AGENT_BRIDGE" />
<service
android:name=".CodexdForegroundService"
android:exported="true"

View File

@@ -1,281 +0,0 @@
package com.openai.codexd
import android.content.Context
import android.net.LocalServerSocket
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.util.Log
import com.openai.codex.bridge.AgentSocketBridgeContract
import java.io.ByteArrayOutputStream
import java.io.Closeable
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.charset.StandardCharsets
import java.util.Collections
import java.util.concurrent.atomic.AtomicBoolean
import org.json.JSONObject
object AgentSocketBridgeServer {
@Volatile
private var runningServer: RunningServer? = null
fun ensureStarted(context: Context) {
synchronized(this) {
if (runningServer != null) {
return
}
runningServer = RunningServer(context.applicationContext).also(RunningServer::start)
}
}
private class RunningServer(
private val context: Context,
) : Closeable {
companion object {
private const val TAG = "AgentSocketBridge"
}
private val boundSocket = LocalSocket().apply {
bind(
LocalSocketAddress(
AgentSocketBridgeContract.SOCKET_NAME,
LocalSocketAddress.Namespace.ABSTRACT,
),
)
}
private val serverSocket = LocalServerSocket(boundSocket.fileDescriptor)
private val closed = AtomicBoolean(false)
private val clientSockets = Collections.synchronizedSet(mutableSetOf<LocalSocket>())
private val acceptThread = Thread(::acceptLoop, "AgentSocketBridge")
fun start() {
acceptThread.start()
Log.i(TAG, "Listening on ${AgentSocketBridgeContract.SOCKET_PATH}")
}
override fun close() {
if (!closed.compareAndSet(false, true)) {
return
}
runCatching { serverSocket.close() }
runCatching { boundSocket.close() }
synchronized(clientSockets) {
clientSockets.forEach { socket -> runCatching { socket.close() } }
clientSockets.clear()
}
acceptThread.interrupt()
}
private fun acceptLoop() {
while (!closed.get()) {
val socket = try {
serverSocket.accept()
} catch (err: IOException) {
if (!closed.get()) {
Log.w(TAG, "Failed to accept Agent socket bridge connection", err)
}
return
}
clientSockets += socket
Thread(
{ handleClient(socket) },
"AgentSocketBridgeClient",
).start()
}
}
private fun handleClient(socket: LocalSocket) {
socket.use { client ->
try {
val request = readRequest(client.inputStream)
when {
request.method == "GET" && request.path == "/internal/runtime/status" -> {
writeJsonResponse(
output = client.outputStream,
statusCode = 200,
body = buildRuntimeStatusJson().toString(),
)
}
request.method == "POST" && request.path == "/v1/responses" -> {
AgentResponsesProxy.streamResponsesTo(
context = context,
requestBody = request.body.orEmpty(),
output = client.outputStream,
)
}
request.method != "POST" && request.path == "/v1/responses" -> {
writeTextResponse(
output = client.outputStream,
statusCode = 405,
body = "Unsupported socket bridge method: ${request.method}",
)
}
else -> {
writeTextResponse(
output = client.outputStream,
statusCode = 404,
body = "Unsupported socket bridge path: ${request.path}",
)
}
}
} catch (err: Exception) {
if (!closed.get()) {
Log.w(TAG, "Agent socket bridge request failed", err)
runCatching {
writeTextResponse(
output = client.outputStream,
statusCode = 502,
body = err.message ?: err::class.java.simpleName,
)
}
}
} finally {
clientSockets -= client
}
}
}
private fun buildRuntimeStatusJson(): JSONObject {
val status = AgentCodexAppServerClient.readRuntimeStatus(context)
return JSONObject()
.put("authenticated", status.authenticated)
.put("accountEmail", status.accountEmail)
.put("clientCount", status.clientCount)
.put("modelProviderId", status.modelProviderId)
.put("configuredModel", status.configuredModel)
.put("effectiveModel", status.effectiveModel)
.put("upstreamBaseUrl", status.upstreamBaseUrl)
}
}
private data class ParsedRequest(
val method: String,
val path: String,
val body: String?,
)
private fun readRequest(input: InputStream): ParsedRequest {
val headerBuffer = ByteArrayOutputStream()
var matched = 0
while (matched < 4) {
val next = input.read()
if (next == -1) {
throw EOFException("unexpected EOF while reading Agent socket bridge request headers")
}
headerBuffer.write(next)
matched = when {
matched == 0 && next == '\r'.code -> 1
matched == 1 && next == '\n'.code -> 2
matched == 2 && next == '\r'.code -> 3
matched == 3 && next == '\n'.code -> 4
next == '\r'.code -> 1
else -> 0
}
}
val headerBytes = headerBuffer.toByteArray()
val headerText = headerBytes
.copyOfRange(0, headerBytes.size - 4)
.toString(StandardCharsets.US_ASCII)
val lines = headerText.split("\r\n")
val requestLine = lines.firstOrNull()
?: throw IOException("socket bridge request line missing")
val requestParts = requestLine.split(" ", limit = 3)
if (requestParts.size < 2) {
throw IOException("invalid socket bridge request line: $requestLine")
}
val headers = mutableMapOf<String, String>()
lines.drop(1).forEach { line ->
val separatorIndex = line.indexOf(':')
if (separatorIndex <= 0) {
return@forEach
}
val name = line.substring(0, separatorIndex).trim().lowercase()
val value = line.substring(separatorIndex + 1).trim()
headers[name] = value
}
if (headers["transfer-encoding"]?.contains("chunked", ignoreCase = true) == true) {
throw IOException("chunked socket bridge requests are unsupported")
}
val contentLength = headers["content-length"]?.toIntOrNull() ?: 0
val bodyBytes = ByteArray(contentLength)
var offset = 0
while (offset < bodyBytes.size) {
val read = input.read(bodyBytes, offset, bodyBytes.size - offset)
if (read == -1) {
throw EOFException("unexpected EOF while reading Agent socket bridge request body")
}
offset += read
}
return ParsedRequest(
method = requestParts[0],
path = requestParts[1],
body = if (bodyBytes.isEmpty()) null else bodyBytes.toString(StandardCharsets.UTF_8),
)
}
private fun writeJsonResponse(
output: OutputStream,
statusCode: Int,
body: String,
) {
writeResponse(
output = output,
statusCode = statusCode,
body = body,
contentType = "application/json; charset=utf-8",
)
}
private fun writeTextResponse(
output: OutputStream,
statusCode: Int,
body: String,
) {
writeResponse(
output = output,
statusCode = statusCode,
body = body,
contentType = "text/plain; charset=utf-8",
)
}
private fun writeResponse(
output: OutputStream,
statusCode: Int,
body: String,
contentType: String,
) {
val bodyBytes = body.toByteArray(StandardCharsets.UTF_8)
val headers = buildString {
append("HTTP/1.1 $statusCode ${reasonPhrase(statusCode)}\r\n")
append("Content-Type: $contentType\r\n")
append("Content-Length: ${bodyBytes.size}\r\n")
append("Connection: close\r\n")
append("\r\n")
}
output.write(headers.toByteArray(StandardCharsets.US_ASCII))
output.write(bodyBytes)
output.flush()
}
private fun reasonPhrase(statusCode: Int): String {
return when (statusCode) {
200 -> "OK"
400 -> "Bad Request"
401 -> "Unauthorized"
403 -> "Forbidden"
404 -> "Not Found"
500 -> "Internal Server Error"
502 -> "Bad Gateway"
503 -> "Service Unavailable"
else -> "Response"
}
}
}

View File

@@ -1,74 +0,0 @@
package com.openai.codexd
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.util.Log
import com.openai.codex.bridge.BridgeHttpResponse
import com.openai.codex.bridge.BridgeRuntimeStatus
import com.openai.codex.bridge.ICodexAgentBridgeService
class CodexAgentBridgeService : Service() {
companion object {
const val PERMISSION_BIND_AGENT_BRIDGE = "com.openai.codex.permission.BIND_AGENT_BRIDGE"
private const val TAG = "CodexAgentBridgeSvc"
}
private val binder = object : ICodexAgentBridgeService.Stub() {
override fun getRuntimeStatus(): BridgeRuntimeStatus {
val status = runCatching {
AgentCodexAppServerClient.readRuntimeStatus(this@CodexAgentBridgeService)
}.getOrElse { err ->
throw err.asBinderError("getRuntimeStatus")
}
Log.i(TAG, "Served runtime status")
return BridgeRuntimeStatus(
status.authenticated,
status.accountEmail,
status.clientCount,
status.modelProviderId,
status.configuredModel,
status.effectiveModel,
status.upstreamBaseUrl,
)
}
override fun sendResponsesRequest(requestBody: String?): BridgeHttpResponse {
val response = runCatching {
AgentResponsesProxy.sendResponsesRequest(
this@CodexAgentBridgeService,
requestBody.orEmpty(),
)
}.getOrElse { err ->
throw err.asBinderError("sendResponsesRequest")
}
Log.i(TAG, "Proxied /v1/responses")
return BridgeHttpResponse(response.statusCode, response.body)
}
override fun openResponsesStream(requestBody: String?): ParcelFileDescriptor {
val stream = runCatching {
AgentResponsesProxy.openResponsesStream(
this@CodexAgentBridgeService,
requestBody.orEmpty(),
)
}.getOrElse { err ->
throw err.asBinderError("openResponsesStream")
}
Log.i(TAG, "Opened /v1/responses stream")
return stream
}
}
override fun onBind(intent: Intent?): IBinder {
return binder
}
private fun Throwable.asBinderError(operation: String): IllegalStateException {
val detail = message ?: javaClass.simpleName
val message = "$operation failed: $detail"
Log.w(TAG, message, this)
return IllegalStateException(message, this)
}
}

View File

@@ -712,7 +712,7 @@ class MainActivity : Activity() {
val authSummary = if (runtimeStatus.authenticated) {
runtimeStatus.accountEmail?.let { "signed in ($it)" } ?: "signed in"
} else {
"not signed in; use the legacy codexd controls below to start sign-in"
"not signed in; use the codexd controls below to start sign-in"
}
val configuredModelSuffix = runtimeStatus.configuredModel
?.takeIf { it != runtimeStatus.effectiveModel }
@@ -733,7 +733,7 @@ class MainActivity : Activity() {
val statusView = findViewById<TextView>(R.id.auth_status)
statusView.text = message
val serviceButton = findViewById<Button>(R.id.service_toggle)
serviceButton.text = if (isServiceRunning) "Stop legacy codexd" else "Start legacy codexd"
serviceButton.text = if (isServiceRunning) "Stop codexd" else "Start codexd"
val actionButton = findViewById<Button>(R.id.auth_action)
actionButton.text = if (authenticated) "Sign out" else "Start sign-in"
actionButton.isEnabled = isServiceRunning

View File

@@ -265,7 +265,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:onClick="toggleCodexd"
android:text="Start legacy codexd" />
android:text="Start codexd" />
<Button
android:id="@+id/auth_action"

View File

@@ -24,10 +24,6 @@ android {
minSdk = 26
}
buildFeatures {
aidl = true
}
compileOptions {
sourceCompatibility = androidJavaVersion
targetCompatibility = androidJavaVersion

View File

@@ -1,3 +0,0 @@
package com.openai.codex.bridge;
parcelable BridgeHttpResponse;

View File

@@ -1,3 +0,0 @@
package com.openai.codex.bridge;
parcelable BridgeRuntimeStatus;

View File

@@ -1,11 +0,0 @@
package com.openai.codex.bridge;
import android.os.ParcelFileDescriptor;
import com.openai.codex.bridge.BridgeHttpResponse;
import com.openai.codex.bridge.BridgeRuntimeStatus;
interface ICodexAgentBridgeService {
BridgeRuntimeStatus getRuntimeStatus();
BridgeHttpResponse sendResponsesRequest(String requestBody);
ParcelFileDescriptor openResponsesStream(String requestBody);
}

View File

@@ -1,8 +0,0 @@
package com.openai.codex.bridge;
public final class AgentSocketBridgeContract {
public static final String SOCKET_NAME = "com.openai.codex.agentbridge";
public static final String SOCKET_PATH = "@" + SOCKET_NAME;
private AgentSocketBridgeContract() {}
}

View File

@@ -1,42 +0,0 @@
package com.openai.codex.bridge;
import android.os.Parcel;
import android.os.Parcelable;
public final class BridgeHttpResponse implements Parcelable {
public final int statusCode;
public final String body;
public BridgeHttpResponse(int statusCode, String body) {
this.statusCode = statusCode;
this.body = body;
}
private BridgeHttpResponse(Parcel in) {
this.statusCode = in.readInt();
this.body = in.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(statusCode);
dest.writeString(body);
}
public static final Creator<BridgeHttpResponse> CREATOR = new Creator<>() {
@Override
public BridgeHttpResponse createFromParcel(Parcel in) {
return new BridgeHttpResponse(in);
}
@Override
public BridgeHttpResponse[] newArray(int size) {
return new BridgeHttpResponse[size];
}
};
}

View File

@@ -1,70 +0,0 @@
package com.openai.codex.bridge;
import android.os.Parcel;
import android.os.Parcelable;
public final class BridgeRuntimeStatus implements Parcelable {
public final boolean authenticated;
public final String accountEmail;
public final int clientCount;
public final String modelProviderId;
public final String configuredModel;
public final String effectiveModel;
public final String upstreamBaseUrl;
public BridgeRuntimeStatus(
boolean authenticated,
String accountEmail,
int clientCount,
String modelProviderId,
String configuredModel,
String effectiveModel,
String upstreamBaseUrl
) {
this.authenticated = authenticated;
this.accountEmail = accountEmail;
this.clientCount = clientCount;
this.modelProviderId = modelProviderId;
this.configuredModel = configuredModel;
this.effectiveModel = effectiveModel;
this.upstreamBaseUrl = upstreamBaseUrl;
}
private BridgeRuntimeStatus(Parcel in) {
this.authenticated = in.readByte() != 0;
this.accountEmail = in.readString();
this.clientCount = in.readInt();
this.modelProviderId = in.readString();
this.configuredModel = in.readString();
this.effectiveModel = in.readString();
this.upstreamBaseUrl = in.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByte((byte) (authenticated ? 1 : 0));
dest.writeString(accountEmail);
dest.writeInt(clientCount);
dest.writeString(modelProviderId);
dest.writeString(configuredModel);
dest.writeString(effectiveModel);
dest.writeString(upstreamBaseUrl);
}
public static final Creator<BridgeRuntimeStatus> CREATOR = new Creator<>() {
@Override
public BridgeRuntimeStatus createFromParcel(Parcel in) {
return new BridgeRuntimeStatus(in);
}
@Override
public BridgeRuntimeStatus[] newArray(int size) {
return new BridgeRuntimeStatus[size];
}
};
}

View File

@@ -1,10 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<package android:name="com.openai.codexd" />
</queries>
<uses-permission android:name="com.openai.codex.permission.BIND_AGENT_BRIDGE" />
<application
android:allowBackup="false"
android:label="@string/app_name">

View File

@@ -25,6 +25,9 @@ The current repo now contains these implementation slices:
**per-session bridge** returned by `AgentManager.openSessionBridge(...)` on
the Agent side and `GenieService.Callback.openSessionBridge(...)` on the
Genie side, not framework question/answer events.
- The older direct cross-app bind/socket bridge experiments have been removed
from the app code; the framework session bridge is now the only supported
Agent<->Genie control plane.
- The current session bridge exposes small fixed-form calls, and the Genie
runtime already uses it to fetch Agent-owned runtime metadata from the
hosted Agent Codex runtime, including auth status and configured model/provider.