mirror of
https://github.com/openai/codex.git
synced 2026-04-23 22:24:57 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -24,10 +24,6 @@ android {
|
||||
minSdk = 26
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = androidJavaVersion
|
||||
targetCompatibility = androidJavaVersion
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package com.openai.codex.bridge;
|
||||
|
||||
parcelable BridgeHttpResponse;
|
||||
@@ -1,3 +0,0 @@
|
||||
package com.openai.codex.bridge;
|
||||
|
||||
parcelable BridgeRuntimeStatus;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user