mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
Route Genie app-server traffic through Agent proxy
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -13,9 +13,13 @@ import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface CodexHttpRequestForwarder {
|
||||
fun sendHttpRequest(method: String, path: String, body: String?): CodexAgentBridge.HttpResponse
|
||||
}
|
||||
|
||||
class AgentBridgeClient(
|
||||
private val context: Context,
|
||||
) : Closeable {
|
||||
) : Closeable, CodexHttpRequestForwarder {
|
||||
companion object {
|
||||
private const val AGENT_PACKAGE = "com.openai.codexd"
|
||||
private const val AGENT_BRIDGE_SERVICE = "com.openai.codexd.CodexAgentBridgeService"
|
||||
@@ -69,12 +73,16 @@ class AgentBridgeClient(
|
||||
)
|
||||
}
|
||||
|
||||
fun sendHttpRequest(method: String, path: String, body: String?): CodexAgentBridge.HttpResponse {
|
||||
override fun sendHttpRequest(
|
||||
method: String,
|
||||
path: String,
|
||||
body: String?,
|
||||
): CodexAgentBridge.HttpResponse {
|
||||
val service = requireService()
|
||||
val response = service.sendHttpRequest(BridgeHttpRequest(method, path, body))
|
||||
return CodexAgentBridge.HttpResponse(
|
||||
statusCode = response.statusCode,
|
||||
body = response.body,
|
||||
body = response.body ?: "",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ class CodexAppServerHost(
|
||||
private val request: GenieRequest,
|
||||
private val callback: GenieService.Callback,
|
||||
private val control: GenieSessionControl,
|
||||
private val bridgeClient: AgentBridgeClient,
|
||||
private val runtimeStatus: CodexAgentBridge.RuntimeStatus,
|
||||
private val targetAppContext: TargetAppContext?,
|
||||
) : Closeable {
|
||||
companion object {
|
||||
private const val TAG = "CodexAppServerHost"
|
||||
private const val AGENT_SOCKET_PATH = "@com.openai.codexd.codexd"
|
||||
private const val REQUEST_TIMEOUT_MS = 30_000L
|
||||
private const val POLL_TIMEOUT_MS = 250L
|
||||
}
|
||||
@@ -43,6 +43,7 @@ class CodexAppServerHost(
|
||||
private var stderrThread: Thread? = null
|
||||
private var finalAgentMessage: String? = null
|
||||
private var resultPublished = false
|
||||
private var localProxy: GenieLocalCodexProxy? = null
|
||||
|
||||
fun run() {
|
||||
startProcess()
|
||||
@@ -56,6 +57,7 @@ class CodexAppServerHost(
|
||||
override fun close() {
|
||||
stdoutThread?.interrupt()
|
||||
stderrThread?.interrupt()
|
||||
localProxy?.close()
|
||||
synchronized(writerLock) {
|
||||
runCatching { writer.close() }
|
||||
}
|
||||
@@ -67,9 +69,19 @@ class CodexAppServerHost(
|
||||
|
||||
private fun startProcess() {
|
||||
val codexHome = File(context.filesDir, "codex-home").apply { mkdirs() }
|
||||
localProxy = GenieLocalCodexProxy(
|
||||
sessionId = request.sessionId,
|
||||
requestForwarder = bridgeClient,
|
||||
).also(GenieLocalCodexProxy::start)
|
||||
val proxyBaseUrl = localProxy?.baseUrl
|
||||
?: throw IOException("local Genie proxy did not start")
|
||||
val processBuilder = ProcessBuilder(
|
||||
listOf(
|
||||
CodexBinaryLocator.resolve(context).absolutePath,
|
||||
"-c",
|
||||
"enable_request_compression=false",
|
||||
"-c",
|
||||
"openai_base_url=\"$proxyBaseUrl\"",
|
||||
"app-server",
|
||||
"--listen",
|
||||
"stdio://",
|
||||
@@ -77,8 +89,7 @@ class CodexAppServerHost(
|
||||
)
|
||||
val env = processBuilder.environment()
|
||||
env["CODEX_HOME"] = codexHome.absolutePath
|
||||
env["CODEX_OPENAI_UNIX_SOCKET"] = AGENT_SOCKET_PATH
|
||||
env["OPENAI_BASE_URL"] = "http://localhost/v1"
|
||||
env["CODEX_USE_AGENT_AUTH_PROXY"] = "1"
|
||||
env["RUST_LOG"] = "info"
|
||||
process = processBuilder.start()
|
||||
control.process = process
|
||||
|
||||
@@ -49,7 +49,7 @@ class CodexGenieService : GenieService() {
|
||||
)
|
||||
callback.publishTrace(
|
||||
sessionId,
|
||||
"Genie is headless. It hosts codex app-server locally, routes model traffic through the Agent-owned codexd socket, and exposes Android tooling as dynamic tools.",
|
||||
"Genie is headless. It hosts codex app-server locally, routes model traffic through the Agent Binder bridge, and exposes Android tooling as dynamic tools.",
|
||||
)
|
||||
|
||||
val targetAppContext = runCatching { TargetAppInspector.inspect(this, request.targetPackage) }
|
||||
@@ -92,6 +92,7 @@ class CodexGenieService : GenieService() {
|
||||
request = request,
|
||||
callback = callback,
|
||||
control = control,
|
||||
bridgeClient = bridgeClient,
|
||||
runtimeStatus = runtimeStatus,
|
||||
targetAppContext = targetAppContext.getOrNull(),
|
||||
).use { host ->
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
package com.openai.codex.genie
|
||||
|
||||
import android.util.Log
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Closeable
|
||||
import java.io.EOFException
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Collections
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class GenieLocalCodexProxy(
|
||||
private val sessionId: String,
|
||||
private val requestForwarder: CodexHttpRequestForwarder,
|
||||
) : Closeable {
|
||||
companion object {
|
||||
private const val TAG = "GenieLocalProxy"
|
||||
}
|
||||
|
||||
private val pathSecret = UUID.randomUUID().toString().replace("-", "")
|
||||
private val serverSocket = ServerSocket(0, 50, InetAddress.getLoopbackAddress())
|
||||
private val closed = AtomicBoolean(false)
|
||||
private val clientSockets = Collections.synchronizedSet(mutableSetOf<Socket>())
|
||||
private val acceptThread = Thread(::acceptLoop, "GenieLocalProxy-$sessionId")
|
||||
|
||||
val baseUrl: String = "http://127.0.0.1:${serverSocket.localPort}/$pathSecret/v1"
|
||||
|
||||
fun start() {
|
||||
acceptThread.start()
|
||||
logInfo("Listening on $baseUrl for $sessionId")
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return
|
||||
}
|
||||
runCatching { serverSocket.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()) {
|
||||
logWarn("Failed to accept local proxy connection for $sessionId", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
clientSockets += socket
|
||||
Thread(
|
||||
{ handleClient(socket) },
|
||||
"GenieLocalProxyClient-$sessionId",
|
||||
).start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleClient(socket: Socket) {
|
||||
socket.use { client ->
|
||||
try {
|
||||
val request = readRequest(client)
|
||||
logInfo("Forwarding ${request.method} ${request.forwardPath} for $sessionId")
|
||||
val response = requestForwarder.sendHttpRequest(
|
||||
request.method,
|
||||
request.forwardPath,
|
||||
request.body,
|
||||
)
|
||||
writeResponse(
|
||||
socket = client,
|
||||
statusCode = response.statusCode,
|
||||
body = response.body,
|
||||
path = request.forwardPath,
|
||||
)
|
||||
} catch (err: Exception) {
|
||||
if (!closed.get()) {
|
||||
logWarn("Local proxy request failed for $sessionId", err)
|
||||
runCatching {
|
||||
writeResponse(
|
||||
socket = client,
|
||||
statusCode = 502,
|
||||
body = err.message ?: err::class.java.simpleName,
|
||||
path = "/error",
|
||||
)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
clientSockets -= client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readRequest(socket: Socket): ParsedRequest {
|
||||
val input = socket.getInputStream()
|
||||
val headerBuffer = ByteArrayOutputStream()
|
||||
var matched = 0
|
||||
while (matched < 4) {
|
||||
val next = input.read()
|
||||
if (next == -1) {
|
||||
throw EOFException("unexpected EOF while reading local proxy 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("local proxy request line missing")
|
||||
val requestParts = requestLine.split(" ", limit = 3)
|
||||
if (requestParts.size < 2) {
|
||||
throw IOException("invalid local proxy 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 local proxy 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 local proxy request body")
|
||||
}
|
||||
offset += read
|
||||
}
|
||||
|
||||
val rawPath = requestParts[1]
|
||||
val forwardPath = normalizeForwardPath(rawPath)
|
||||
return ParsedRequest(
|
||||
method = requestParts[0],
|
||||
forwardPath = forwardPath,
|
||||
body = if (bodyBytes.isEmpty()) null else bodyBytes.toString(StandardCharsets.UTF_8),
|
||||
)
|
||||
}
|
||||
|
||||
private fun normalizeForwardPath(rawPath: String): String {
|
||||
val expectedPrefix = "/$pathSecret"
|
||||
if (!rawPath.startsWith(expectedPrefix)) {
|
||||
throw IOException("unexpected local proxy path: $rawPath")
|
||||
}
|
||||
val strippedPath = rawPath.removePrefix(expectedPrefix)
|
||||
return if (strippedPath.isBlank()) "/" else strippedPath
|
||||
}
|
||||
|
||||
private fun writeResponse(
|
||||
socket: Socket,
|
||||
statusCode: Int,
|
||||
body: String,
|
||||
path: String,
|
||||
) {
|
||||
val bodyBytes = body.toByteArray(StandardCharsets.UTF_8)
|
||||
val contentType = when {
|
||||
path.startsWith("/v1/responses") -> "text/event-stream; charset=utf-8"
|
||||
body.trimStart().startsWith("{") || body.trimStart().startsWith("[") -> {
|
||||
"application/json; charset=utf-8"
|
||||
}
|
||||
else -> "text/plain; charset=utf-8"
|
||||
}
|
||||
val responseHeaders = 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")
|
||||
}
|
||||
|
||||
val output = socket.getOutputStream()
|
||||
output.write(responseHeaders.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"
|
||||
}
|
||||
}
|
||||
|
||||
private fun logInfo(message: String) {
|
||||
runCatching { Log.i(TAG, message) }
|
||||
}
|
||||
|
||||
private fun logWarn(
|
||||
message: String,
|
||||
err: Throwable,
|
||||
) {
|
||||
runCatching { Log.w(TAG, message, err) }
|
||||
}
|
||||
|
||||
private data class ParsedRequest(
|
||||
val method: String,
|
||||
val forwardPath: String,
|
||||
val body: String?,
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.openai.codex.genie
|
||||
|
||||
import java.io.IOException
|
||||
import org.json.JSONObject
|
||||
|
||||
sealed interface GenieModelTurn {
|
||||
data class Question(val text: String) : GenieModelTurn
|
||||
|
||||
data class Result(val text: String) : GenieModelTurn
|
||||
|
||||
data class ToolCall(
|
||||
val name: String,
|
||||
val arguments: JSONObject,
|
||||
) : GenieModelTurn
|
||||
}
|
||||
|
||||
object GenieModelTurnParser {
|
||||
fun parse(message: String): GenieModelTurn {
|
||||
val trimmed = message.trim()
|
||||
stripTurnPrefix(trimmed, "TOOL:")?.let(::parseToolCall)?.let { return it }
|
||||
stripTurnPrefix(trimmed, "QUESTION:")?.let { return GenieModelTurn.Question(it) }
|
||||
stripTurnPrefix(trimmed, "RESULT:")?.let { return GenieModelTurn.Result(it) }
|
||||
return if (trimmed.endsWith("?")) {
|
||||
GenieModelTurn.Question(trimmed)
|
||||
} else {
|
||||
GenieModelTurn.Result(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseToolCall(payload: String): GenieModelTurn.ToolCall {
|
||||
val call = try {
|
||||
JSONObject(payload)
|
||||
} catch (err: Exception) {
|
||||
throw IOException("Invalid TOOL payload: ${err.message}", err)
|
||||
}
|
||||
val name = call.optString("name").trim()
|
||||
if (name.isEmpty()) {
|
||||
throw IOException("TOOL payload missing name")
|
||||
}
|
||||
return GenieModelTurn.ToolCall(
|
||||
name = name,
|
||||
arguments = call.optJSONObject("arguments") ?: JSONObject(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun stripTurnPrefix(message: String, prefix: String): String? {
|
||||
if (!message.startsWith(prefix, ignoreCase = true)) {
|
||||
return null
|
||||
}
|
||||
return message.substring(prefix.length).trim().ifEmpty { "continue" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.openai.codex.genie
|
||||
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.Socket
|
||||
import java.net.URI
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class GenieLocalCodexProxyTest {
|
||||
@Test
|
||||
fun forwardsLoopbackHttpRequestsToAgentBridge() {
|
||||
val forwardedRequest = AtomicReference<Triple<String, String, String?>>()
|
||||
val proxy = GenieLocalCodexProxy(
|
||||
sessionId = "session-1",
|
||||
requestForwarder = object : CodexHttpRequestForwarder {
|
||||
override fun sendHttpRequest(
|
||||
method: String,
|
||||
path: String,
|
||||
body: String?,
|
||||
): CodexAgentBridge.HttpResponse {
|
||||
forwardedRequest.set(Triple(method, path, body))
|
||||
return CodexAgentBridge.HttpResponse(
|
||||
statusCode = 200,
|
||||
body = """{"ok":true}""",
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
proxy.use { localProxy ->
|
||||
localProxy.start()
|
||||
val uri = URI(localProxy.baseUrl)
|
||||
Socket(uri.host, uri.port).use { socket ->
|
||||
val body = """{"model":"gpt-5.3-codex"}"""
|
||||
val requestPath = "${uri.rawPath}/responses"
|
||||
val writer = OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)
|
||||
writer.write("POST $requestPath HTTP/1.1\r\n")
|
||||
writer.write("Host: ${uri.host}\r\n")
|
||||
writer.write("Content-Type: application/json\r\n")
|
||||
writer.write("Content-Length: ${body.toByteArray(StandardCharsets.UTF_8).size}\r\n")
|
||||
writer.write("\r\n")
|
||||
writer.write(body)
|
||||
writer.flush()
|
||||
socket.shutdownOutput()
|
||||
val responseText = BufferedReader(
|
||||
InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8),
|
||||
).readText()
|
||||
assertTrue(responseText.startsWith("HTTP/1.1 200 OK"))
|
||||
assertTrue(responseText.contains("""{"ok":true}"""))
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
Triple("POST", "/v1/responses", """{"model":"gpt-5.3-codex"}"""),
|
||||
forwardedRequest.get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.openai.codex.genie
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class GenieModelTurnParserTest {
|
||||
@Test
|
||||
fun parseToolCallTurn() {
|
||||
val turn = GenieModelTurnParser.parse(
|
||||
"""TOOL: {"name":"android.intent.launch","arguments":{"packageName":"com.android.deskclock"}}""",
|
||||
)
|
||||
|
||||
val toolCall = turn as GenieModelTurn.ToolCall
|
||||
assertEquals("android.intent.launch", toolCall.name)
|
||||
assertEquals("com.android.deskclock", toolCall.arguments.getString("packageName"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQuestionTurn() {
|
||||
assertEquals(
|
||||
GenieModelTurn.Question("Should I continue?"),
|
||||
GenieModelTurnParser.parse("QUESTION: Should I continue?"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseResultTurn() {
|
||||
assertEquals(
|
||||
GenieModelTurn.Result("Launched the target app."),
|
||||
GenieModelTurnParser.parse("RESULT: Launched the target app."),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::ffi::OsString;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const CODEX_OPENAI_UNIX_SOCKET_ENV_VAR: &str = "CODEX_OPENAI_UNIX_SOCKET";
|
||||
pub const CODEX_USE_AGENT_AUTH_PROXY_ENV_VAR: &str = "CODEX_USE_AGENT_AUTH_PROXY";
|
||||
|
||||
pub fn openai_unix_socket_path() -> Option<PathBuf> {
|
||||
std::env::var(CODEX_OPENAI_UNIX_SOCKET_ENV_VAR)
|
||||
@@ -8,4 +11,73 @@ pub fn openai_unix_socket_path() -> Option<PathBuf> {
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.or_else(default_android_socket_path)
|
||||
}
|
||||
|
||||
pub fn should_route_via_codexd() -> bool {
|
||||
(openai_unix_socket_path().is_some()
|
||||
|| should_use_agent_auth_proxy_env(std::env::var_os(CODEX_USE_AGENT_AUTH_PROXY_ENV_VAR)))
|
||||
&& !is_codexd_process()
|
||||
}
|
||||
|
||||
fn is_codexd_process() -> bool {
|
||||
std::env::args_os()
|
||||
.next()
|
||||
.and_then(|arg0| {
|
||||
Path::new(&arg0)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(str::to_string)
|
||||
})
|
||||
.is_some_and(|arg0| arg0.contains("codexd"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn default_android_socket_path() -> Option<PathBuf> {
|
||||
const CANDIDATES: [&str; 2] = [
|
||||
"/data/data/com.openai.codexd/files/codexd.sock",
|
||||
"/data/user/0/com.openai.codexd/files/codexd.sock",
|
||||
];
|
||||
|
||||
CANDIDATES
|
||||
.iter()
|
||||
.map(PathBuf::from)
|
||||
.find(|path| path.exists())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn default_android_socket_path() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
fn should_use_agent_auth_proxy_env(value: Option<OsString>) -> bool {
|
||||
value
|
||||
.and_then(|raw| raw.into_string().ok())
|
||||
.map(|raw| raw.trim().to_ascii_lowercase())
|
||||
.is_some_and(|value| value == "1" || value == "true" || value == "yes")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::should_use_agent_auth_proxy_env;
|
||||
use std::ffi::OsString;
|
||||
|
||||
#[test]
|
||||
fn agent_auth_proxy_env_accepts_truthy_values() {
|
||||
assert!(should_use_agent_auth_proxy_env(Some(OsString::from("1"))));
|
||||
assert!(should_use_agent_auth_proxy_env(Some(OsString::from(
|
||||
"true"
|
||||
))));
|
||||
assert!(should_use_agent_auth_proxy_env(Some(OsString::from("YES"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_auth_proxy_env_rejects_missing_or_falsey_values() {
|
||||
assert!(!should_use_agent_auth_proxy_env(None));
|
||||
assert!(!should_use_agent_auth_proxy_env(Some(OsString::from(""))));
|
||||
assert!(!should_use_agent_auth_proxy_env(Some(OsString::from("0"))));
|
||||
assert!(!should_use_agent_auth_proxy_env(Some(OsString::from(
|
||||
"false"
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,10 @@ The current repo now contains these implementation slices:
|
||||
- The Genie runtime inspects the paired target package from inside the
|
||||
target-app sandbox and feeds package metadata plus launcher intent details
|
||||
into the delegated Codex prompt.
|
||||
- The hosted `codex app-server` process routes model traffic through the
|
||||
Agent-owned `codexd` abstract Unix socket, keeping network/auth Agent-owned
|
||||
even while the Genie runs inside the target-app sandbox.
|
||||
- The hosted `codex app-server` process now talks to a **Genie-local loopback
|
||||
HTTP proxy** inside the Genie app. That proxy forwards HTTP traffic to the
|
||||
Agent over Binder/AIDL, keeping network/auth Agent-owned without assuming the
|
||||
Genie child process can reach the Agent's abstract socket directly.
|
||||
- The Genie runtime exposes reusable Android capabilities to Codex as
|
||||
**dynamic tools**, not via a custom `TOOL:` text protocol.
|
||||
- Non-bridge Genie questions surface through AgentSDK question flow by mapping
|
||||
@@ -39,7 +40,9 @@ The current repo now contains these implementation slices:
|
||||
- The Agent also attempts to answer Genie questions through the embedded
|
||||
`codexd` runtime before falling back to notification/UI escalation.
|
||||
- Runtime testing on the emulator shows that the exported Agent Binder service
|
||||
is reachable from Genie execution for the current bootstrap calls.
|
||||
is reachable from Genie execution for the current bootstrap calls, while
|
||||
direct cross-app access to the Agent-owned abstract socket is not a valid
|
||||
assumption.
|
||||
|
||||
The Rust `codexd` service/client split remains in place and is still the
|
||||
existing network/auth bridge while this refactor proceeds.
|
||||
@@ -70,7 +73,9 @@ existing network/auth bridge while this refactor proceeds.
|
||||
- Internal Agent<->Genie coordination now splits into:
|
||||
- Binder/AIDL for fixed-form control/data RPC
|
||||
- AgentSDK session events for free-form product dialogue
|
||||
- hosted `codex app-server` inside Genie for the actual Codex execution loop
|
||||
- hosted `codex app-server` inside Genie for the actual Codex execution loop
|
||||
- Genie-local transport termination between the hosted `codex` child process
|
||||
and the Binder control plane
|
||||
|
||||
## Runtime Model
|
||||
|
||||
@@ -104,6 +109,7 @@ existing network/auth bridge while this refactor proceeds.
|
||||
- Android dynamic tool execution
|
||||
- Agent escalation via `request_user_input`
|
||||
- runtime bootstrap from the Agent-owned Binder bridge
|
||||
- local proxying of hosted `codex` HTTP traffic onto Binder
|
||||
|
||||
## First Milestone Scope
|
||||
|
||||
@@ -123,8 +129,8 @@ existing network/auth bridge while this refactor proceeds.
|
||||
- Agent-owned `/internal/runtime/status` metadata for Genie bootstrap
|
||||
- Target-app package metadata and launcher-intent inspection from the Genie
|
||||
sandbox, with that context included in the delegated Codex prompt
|
||||
- Hosted `codex app-server` inside Genie, with model traffic routed through the
|
||||
Agent-owned `codexd` abstract socket
|
||||
- Hosted `codex app-server` inside Genie, with model traffic routed through a
|
||||
Genie-local proxy backed by the Agent Binder bridge
|
||||
- Android dynamic tools registered on the Genie Codex thread with:
|
||||
- `android.package.inspect`
|
||||
- `android.intent.launch`
|
||||
@@ -169,6 +175,9 @@ existing network/auth bridge while this refactor proceeds.
|
||||
- `android/genie/src/main/java/com/openai/codex/genie/CodexAppServerHost.kt`
|
||||
- stdio JSON-RPC host for `codex app-server`, dynamic tools, and
|
||||
`request_user_input` bridging
|
||||
- `android/genie/src/main/java/com/openai/codex/genie/GenieLocalCodexProxy.kt`
|
||||
- Genie-local loopback HTTP proxy that forwards hosted `codex` HTTP traffic to
|
||||
the Agent Binder bridge
|
||||
- `android/app/src/main/java/com/openai/codexd/CodexAgentBridgeService.kt`
|
||||
- exported Binder/AIDL bridge for Genie control-plane calls
|
||||
- `android/genie/src/main/java/com/openai/codex/genie/AgentBridgeClient.kt`
|
||||
|
||||
Reference in New Issue
Block a user