mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user