Require framework HTTP for Android Agent runtime

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Iliyan Malchev
2026-03-24 21:57:26 -07:00
parent 702b3cd9eb
commit ff61d54770
5 changed files with 25 additions and 204 deletions

View File

@@ -32,11 +32,6 @@
</intent-filter>
</service>
<service
android:name=".AgentRuntimeForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<activity
android:name=".MainActivity"
android:exported="true"

View File

@@ -107,11 +107,7 @@ object AgentCodexAppServerClient {
executionSettings: SessionExecutionSettings = SessionExecutionSettings.default,
requestTimeoutMs: Long = REQUEST_TIMEOUT_MS,
frameworkSessionId: String? = null,
keepForeground: Boolean = false,
): String = synchronized(lifecycleLock) {
if (keepForeground) {
AgentRuntimeForegroundService.start(context.applicationContext)
}
ensureStarted(context.applicationContext)
val previousFrameworkSessionId = activeFrameworkSessionId
activeFrameworkSessionId = frameworkSessionId?.trim()?.ifEmpty { null }
@@ -142,9 +138,6 @@ object AgentCodexAppServerClient {
activeRequests.decrementAndGet()
updateClientCount()
activeFrameworkSessionId = previousFrameworkSessionId
if (keepForeground) {
AgentRuntimeForegroundService.stop(context.applicationContext)
}
}
}
@@ -296,28 +289,12 @@ object AgentCodexAppServerClient {
}
val agentManager = context.getSystemService(AgentManager::class.java)
?: throw IOException("AgentManager unavailable for framework session transport")
return runCatching {
AgentResponsesProxy.sendResponsesRequestThroughFramework(
agentManager = agentManager,
sessionId = frameworkSessionId,
context = context,
requestBody = requestBody,
)
}.getOrElse { err ->
if (!shouldFallbackToDirectResponsesProxy(err)) {
throw err
}
Log.i(
TAG,
"Falling back to direct Agent /responses proxy for $frameworkSessionId: ${err.message}",
)
AgentResponsesProxy.sendResponsesRequest(context, requestBody)
}
}
private fun shouldFallbackToDirectResponsesProxy(err: Throwable): Boolean {
return err is SecurityException &&
err.message?.contains("Only the active Genie runtime may open framework HTTP exchanges") == true
return AgentResponsesProxy.sendResponsesRequestThroughFramework(
agentManager = agentManager,
sessionId = frameworkSessionId,
context = context,
requestBody = requestBody,
)
}
private fun initialize() {

View File

@@ -39,7 +39,6 @@ object AgentPlannerRuntimeManager {
check(activePlannerSessions.putIfAbsent(plannerSessionId, true) == null) {
"Planner runtime already active for parent session $plannerSessionId"
}
AgentRuntimeForegroundService.acquire(applicationContext)
try {
AgentPlannerRuntime(
context = applicationContext,
@@ -56,7 +55,6 @@ object AgentPlannerRuntimeManager {
}
} finally {
activePlannerSessions.remove(plannerSessionId)
AgentRuntimeForegroundService.release(applicationContext)
}
}
@@ -323,33 +321,17 @@ object AgentPlannerRuntimeManager {
private fun forwardResponsesRequest(requestBody: String): AgentResponsesProxy.HttpResponse {
val activeFrameworkSessionId = frameworkSessionId
if (activeFrameworkSessionId.isNullOrBlank()) {
return AgentResponsesProxy.sendResponsesRequest(context, requestBody)
check(!activeFrameworkSessionId.isNullOrBlank()) {
"Planner runtime requires a framework session id for /responses transport"
}
val agentManager = context.getSystemService(AgentManager::class.java)
?: throw IOException("AgentManager unavailable for framework session transport")
return runCatching {
AgentResponsesProxy.sendResponsesRequestThroughFramework(
agentManager = agentManager,
sessionId = activeFrameworkSessionId,
context = context,
requestBody = requestBody,
)
}.getOrElse { err ->
if (!shouldFallbackToDirectResponsesProxy(err)) {
throw err
}
Log.i(
TAG,
"Falling back to direct Agent /responses proxy for $activeFrameworkSessionId: ${err.message}",
)
AgentResponsesProxy.sendResponsesRequest(context, requestBody)
}
}
private fun shouldFallbackToDirectResponsesProxy(err: Throwable): Boolean {
return err is SecurityException &&
err.message?.contains("Only the active Genie runtime may open framework HTTP exchanges") == true
return AgentResponsesProxy.sendResponsesRequestThroughFramework(
agentManager = agentManager,
sessionId = activeFrameworkSessionId,
context = context,
requestBody = requestBody,
)
}
private fun request(

View File

@@ -1,128 +0,0 @@
package com.openai.codex.agent
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import java.util.concurrent.atomic.AtomicInteger
class AgentRuntimeForegroundService : Service() {
companion object {
private const val CHANNEL_ID = "codex_agent_runtime"
private const val CHANNEL_NAME = "Codex Agent Runtime"
private const val NOTIFICATION_ID = 0xC0D3002
private const val ACTION_START = "com.openai.codex.agent.action.START_RUNTIME_FOREGROUND"
private const val ACTION_STOP = "com.openai.codex.agent.action.STOP_RUNTIME_FOREGROUND"
private val activeLeases = AtomicInteger(0)
fun acquire(context: Context) {
val previous = activeLeases.getAndIncrement()
if (previous > 0) {
return
}
val intent = Intent(context, AgentRuntimeForegroundService::class.java).apply {
action = ACTION_START
}
context.startForegroundService(intent)
}
fun release(context: Context) {
while (true) {
val current = activeLeases.get()
if (current <= 0) {
return
}
if (!activeLeases.compareAndSet(current, current - 1)) {
continue
}
if (current > 1) {
return
}
val intent = Intent(context, AgentRuntimeForegroundService::class.java).apply {
action = ACTION_STOP
}
context.startService(intent)
return
}
}
fun start(context: Context) {
acquire(context)
}
fun stop(context: Context) {
release(context)
}
fun activeLeaseCount(): Int {
return activeLeases.get()
}
fun resetLeases() {
activeLeases.set(0)
}
}
override fun onStartCommand(
intent: Intent?,
flags: Int,
startId: Int,
): Int {
when (intent?.action) {
ACTION_STOP -> {
resetLeases()
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelfResult(startId)
}
else -> {
val manager = getSystemService(NotificationManager::class.java)
ensureChannel(manager)
startForeground(NOTIFICATION_ID, buildNotification())
}
}
return START_NOT_STICKY
}
override fun onBind(intent: Intent?) = null
private fun buildNotification(): Notification {
val openAgentIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
return Notification.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_codex)
.setContentTitle("Codex Agent is working")
.setContentText("Planning or supervising an active Agent session.")
.setContentIntent(openAgentIntent)
.setOngoing(true)
.setOnlyAlertOnce(true)
.build()
}
private fun ensureChannel(manager: NotificationManager) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || manager.getNotificationChannel(CHANNEL_ID) != null) {
return
}
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW,
).apply {
description = "Shows when Codex Agent is actively planning or supervising a session."
setSound(null, null)
enableVibration(false)
}
manager.createNotificationChannel(channel)
}
}

View File

@@ -54,13 +54,11 @@ The current repo now contains these implementation slices:
Agent sessions can plan concurrently.
- Planner runtime ownership is 1:1 with the top-level parent session id. The
app does not allow overlapping planner runtimes for the same parent session.
- A single narrow foreground service now acts as a shared keepalive lease for
the app UID while any Agent-owned planner runtime is active in the
background.
- The framework still only permits `openFrameworkHttpExchange(...)` for active
Genie runtimes. Each direct parent planner runtime therefore attempts
framework transport first and falls back to the Agent-owned proxy only for
that planner path.
- The framework now permits both active Genie runtimes and active top-level
Agent runtimes to open framework HTTP exchanges. Direct parent planner
runtimes therefore use the same framework-owned `/responses` transport as
Genie child runtimes, and the old Agent-owned planner fallback has been
removed.
- The Genie runtime now keeps host dynamic tools limited to framework-only
detached-target controls and frame capture, while standard Android shell and
device commands stay in the normal Codex tool path.
@@ -96,7 +94,8 @@ The current repo now contains these implementation slices:
framework-owned recovery instead of guessed ordinary app launch.
The Android app now owns auth origination, runtime status, and per-session
transport configuration handoff. Active Genie model traffic is framework-owned.
transport configuration handoff. Active Genie and top-level Agent planner model
traffic are framework-owned.
The older standalone
service/client split has been removed from the repo and is no longer part of
the Android Agent/Genie flow.
@@ -167,8 +166,8 @@ the Android Agent/Genie flow.
- Android dynamic tool execution
- Agent escalation via `request_user_input`
- runtime bootstrap from the framework session bridge
- forwarding hosted `codex` `/v1/responses` traffic onto the framework-owned
HTTP bridge
- forwarding hosted `codex` `/v1/responses` traffic from both Agent and Genie
runtimes onto the framework-owned HTTP bridge
## First Milestone Scope
@@ -241,12 +240,8 @@ the Android Agent/Genie flow.
- `android/app/src/main/java/com/openai/codex/agent/AgentSessionBridgeServer.kt`
- Agent-side server for the framework-managed per-session bridge
- `android/app/src/main/java/com/openai/codex/agent/AgentResponsesProxy.kt`
- Agent-owned Responses transport used by the hosted Agent runtime itself,
including the direct-parent planner fallback when the framework rejects
hidden Agent-side HTTP exchange opens outside an active Genie runtime
- `android/app/src/main/java/com/openai/codex/agent/AgentRuntimeForegroundService.kt`
- shared foreground-service keepalive for Agent-owned planning while the UI
is backgrounded
- Agent/Genie bridge helper for framework-owned Responses transport setup and
execution
- `android/genie/src/main/java/com/openai/codex/genie/AgentBridgeClient.kt`
- Genie-side client for the framework-managed control bridge plus the
framework-owned streaming HTTP exchange bridge