Inspect target apps inside Genie sessions

Inspect the paired target package from inside the Genie sandbox, publish that metadata into the framework session trace, and feed the launch-intent and permission context into the Agent-bridged model prompt.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Iliyan Malchev
2026-03-19 01:19:14 -07:00
parent 3410562a58
commit 9de3f3ba6a
5 changed files with 210 additions and 4 deletions

View File

@@ -57,6 +57,19 @@ class CodexGenieService : GenieService() {
sessionId,
"Genie is headless and routes model/backend traffic through the Agent-owned bridge.",
)
val targetAppContext = runCatching { TargetAppInspector.inspect(this, request.targetPackage) }
targetAppContext.onSuccess { targetApp ->
callback.publishTrace(
sessionId,
"Inspected target app inside the paired sandbox: ${targetApp.describeForTrace()}",
)
}
targetAppContext.onFailure { err ->
callback.publishTrace(
sessionId,
"Target app inspection failed for ${request.targetPackage}: ${err.message}",
)
}
val runtimeStatus = runCatching { requestAgentRuntimeStatus(sessionId, callback, control) }
runtimeStatus.onSuccess { status ->
val accountSuffix = status.accountEmail?.let { " (${it})" } ?: ""
@@ -79,7 +92,7 @@ class CodexGenieService : GenieService() {
callback.publishQuestion(
sessionId,
"Codex Genie scaffold is active for ${request.targetPackage}. Continue the placeholder flow?",
"Codex Genie is active for ${targetAppContext.getOrNull()?.displayName() ?: request.targetPackage}. Continue with the Agent-bridged next-step synthesis?",
)
callback.updateState(sessionId, AgentSessionInfo.STATE_WAITING_FOR_USER)
@@ -101,7 +114,15 @@ class CodexGenieService : GenieService() {
"Requesting a non-streaming /v1/responses call through the Agent using ${status.effectiveModel}.",
)
runCatching {
requestModelNextStep(sessionId, request, answer, status, callback, control)
requestModelNextStep(
sessionId = sessionId,
request = request,
answer = answer,
runtimeStatus = status,
targetAppContext = targetAppContext.getOrNull(),
callback = callback,
control = control,
)
}
}
@@ -166,6 +187,7 @@ class CodexGenieService : GenieService() {
request: GenieRequest,
answer: String,
runtimeStatus: CodexAgentBridge.RuntimeStatus,
targetAppContext: TargetAppContext?,
callback: Callback,
control: SessionControl,
): String {
@@ -176,7 +198,11 @@ class CodexGenieService : GenieService() {
CodexAgentBridge.buildResponsesRequest(
requestId = requestId,
model = model,
prompt = buildModelPrompt(request, answer),
prompt = buildModelPrompt(
request = request,
answer = answer,
targetAppContext = targetAppContext,
),
),
)
callback.updateState(sessionId, AgentSessionInfo.STATE_WAITING_FOR_USER)
@@ -210,13 +236,21 @@ class CodexGenieService : GenieService() {
throw IOException("Cancelled while waiting for user response")
}
private fun buildModelPrompt(request: GenieRequest, answer: String): String {
private fun buildModelPrompt(
request: GenieRequest,
answer: String,
targetAppContext: TargetAppContext?,
): String {
val objective = abbreviate(request.prompt, MAX_BRIDGE_PROMPT_CHARS)
val userAnswer = abbreviate(answer, MAX_BRIDGE_ANSWER_CHARS)
val targetSummary = targetAppContext?.renderPromptSection()
?: "Target app inspection:\n- unavailable"
return """
You are Codex acting as an Android Genie for the target package ${request.targetPackage}.
Original objective: $objective
The user answered the current question with: $userAnswer
$targetSummary
Reply with one short sentence describing the next automation step you would take in the target app.
""".trimIndent()

View File

@@ -0,0 +1,60 @@
package com.openai.codex.genie
data class TargetAppContext(
val packageName: String,
val applicationLabel: String?,
val versionName: String?,
val versionCode: Long?,
val launchIntentAction: String?,
val launchIntentComponent: String?,
val requestedPermissions: List<String>,
) {
fun displayName(): String {
return applicationLabel?.takeIf(String::isNotBlank) ?: packageName
}
fun describeForTrace(): String {
val versionSummary = when {
versionName != null && versionCode != null -> "version=$versionName ($versionCode)"
versionName != null -> "version=$versionName"
versionCode != null -> "versionCode=$versionCode"
else -> "version=unknown"
}
val launcherSummary = launchIntentComponent?.let { component ->
val actionSuffix = launchIntentAction?.let { " action=$it" } ?: ""
"launcher=$component$actionSuffix"
} ?: "launcher=unavailable"
val permissionSummary = summarizePermissions(maxVisible = 3)
return "${displayName()} ($packageName), $versionSummary, $launcherSummary, permissions=$permissionSummary"
}
fun renderPromptSection(): String {
val permissions = requestedPermissions.joinToString(separator = "\n") { "- $it" }
.ifBlank { "- none declared or visible" }
return """
Target app inspection:
- package: $packageName
- label: ${displayName()}
- versionName: ${versionName ?: "unknown"}
- versionCode: ${versionCode ?: "unknown"}
- launcherAction: ${launchIntentAction ?: "unavailable"}
- launcherComponent: ${launchIntentComponent ?: "unavailable"}
- requestedPermissions:
$permissions
""".trimIndent()
}
private fun summarizePermissions(maxVisible: Int): String {
if (requestedPermissions.isEmpty()) {
return "none"
}
val visible = requestedPermissions.take(maxVisible)
val summary = visible.joinToString()
val remaining = requestedPermissions.size - visible.size
return if (remaining > 0) {
"$summary (+$remaining more)"
} else {
summary
}
}
}

View File

@@ -0,0 +1,50 @@
package com.openai.codex.genie
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
object TargetAppInspector {
fun inspect(context: Context, packageName: String): TargetAppContext {
val packageManager = context.packageManager
val packageInfo = packageManager.getPackageInfoCompat(packageName)
val applicationInfo = packageInfo.applicationInfo
?: packageManager.getApplicationInfo(packageName, 0)
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
val applicationLabel = runCatching {
applicationInfo.loadLabel(packageManager)?.toString()
}.getOrNull()
return TargetAppContext(
packageName = packageName,
applicationLabel = applicationLabel,
versionName = packageInfo.versionName,
versionCode = packageInfo.longVersionCodeCompat(),
launchIntentAction = launchIntent?.action,
launchIntentComponent = launchIntent?.component?.flattenToShortString(),
requestedPermissions = packageInfo.requestedPermissions
?.filterNotNull()
?.sorted()
?: emptyList(),
)
}
private fun PackageManager.getPackageInfoCompat(packageName: String): PackageInfo {
val flags = PackageManager.GET_PERMISSIONS
return if (Build.VERSION.SDK_INT >= 33) {
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
} else {
@Suppress("DEPRECATION")
getPackageInfo(packageName, flags)
}
}
private fun PackageInfo.longVersionCodeCompat(): Long? {
return if (Build.VERSION.SDK_INT >= 28) {
longVersionCode
} else {
@Suppress("DEPRECATION")
versionCode.toLong()
}
}
}

View File

@@ -0,0 +1,57 @@
package com.openai.codex.genie
import org.junit.Assert.assertEquals
import org.junit.Test
class TargetAppContextTest {
@Test
fun describeForTraceUsesLabelAndTruncatesPermissions() {
val context = TargetAppContext(
packageName = "com.android.deskclock",
applicationLabel = "Clock",
versionName = "14",
versionCode = 42,
launchIntentAction = "android.intent.action.MAIN",
launchIntentComponent = "com.android.deskclock/.DeskClock",
requestedPermissions = listOf(
"android.permission.POST_NOTIFICATIONS",
"android.permission.SCHEDULE_EXACT_ALARM",
"android.permission.SET_ALARM",
"android.permission.WAKE_LOCK",
),
)
assertEquals(
"Clock (com.android.deskclock), version=14 (42), launcher=com.android.deskclock/.DeskClock action=android.intent.action.MAIN, permissions=android.permission.POST_NOTIFICATIONS, android.permission.SCHEDULE_EXACT_ALARM, android.permission.SET_ALARM (+1 more)",
context.describeForTrace(),
)
}
@Test
fun renderPromptSectionFallsBackWhenMetadataIsMissing() {
val context = TargetAppContext(
packageName = "com.example.target",
applicationLabel = null,
versionName = null,
versionCode = null,
launchIntentAction = null,
launchIntentComponent = null,
requestedPermissions = emptyList(),
)
assertEquals(
"""
Target app inspection:
- package: com.example.target
- label: com.example.target
- versionName: unknown
- versionCode: unknown
- launcherAction: unavailable
- launcherComponent: unavailable
- requestedPermissions:
- none declared or visible
""".trimIndent(),
context.renderPromptSection(),
)
}
}

View File

@@ -24,6 +24,9 @@ The current repo now contains the first implementation slice:
- The current bridge shape carries small request/response envelopes, and the
Genie runtime already uses it to fetch Agent-owned runtime metadata from the
embedded `codexd`, including auth status and the effective model/provider.
- The Genie runtime now also inspects the paired target package from inside the
target-app sandbox and feeds package metadata plus launcher intent details
into the bridged model prompt.
- The Genie scaffold now issues one real **non-streaming `/v1/responses`**
request through that bridge after the user answer, proving that model traffic
can stay Agent-owned even while the Genie runs inside the target-app sandbox.
@@ -103,6 +106,8 @@ existing network/auth bridge while this refactor proceeds.
- Generic small HTTP request/response envelopes over the internal bridge, with
the Genie using the real `codexd` HTTP response bodies
- 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 bridged model prompt
- One real non-streaming proxied `/v1/responses` request from Genie through the
Agent-owned bridge after the user answer
- Abstract-unix-socket support in the legacy Rust bridge via `@name` or