Copy runtime guidance from Agent into Genie homes

Move the Android runtime guidance out of Java source into a checked-in AGENTS.md asset, install that file into the Agent home at bootstrap, and copy the installed Agent file into each per-session Genie home over the framework session bridge.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Iliyan Malchev
2026-03-21 09:16:04 -07:00
parent 4086f04ddb
commit 1e902a99cf
9 changed files with 99 additions and 84 deletions

View File

@@ -157,7 +157,7 @@ object AgentCodexAppServerClient {
}.also(AgentLocalCodexProxy::start)
val proxyBaseUrl = localProxy?.baseUrl
?: throw IOException("local Agent proxy did not start")
HostedCodexConfig.write(codexHome, proxyBaseUrl)
HostedCodexConfig.write(context, codexHome, proxyBaseUrl)
val startedProcess = ProcessBuilder(
listOf(
CodexCliBinaryLocator.resolve(context).absolutePath,

View File

@@ -4,6 +4,7 @@ import android.app.agent.AgentManager
import android.content.Context
import android.os.ParcelFileDescriptor
import android.util.Log
import com.openai.codex.bridge.HostedCodexConfig
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.Closeable
@@ -13,6 +14,7 @@ import java.io.EOFException
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.File
import java.nio.charset.StandardCharsets
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
@@ -49,6 +51,7 @@ object AgentSessionBridgeServer {
private const val TAG = "AgentSessionBridge"
private const val METHOD_GET_RUNTIME_STATUS = "getRuntimeStatus"
private const val METHOD_SEND_RESPONSES_REQUEST = "sendResponsesRequest"
private const val METHOD_READ_INSTALLED_AGENTS_FILE = "readInstalledAgentsFile"
}
private val closed = AtomicBoolean(false)
@@ -138,6 +141,14 @@ object AgentSessionBridgeServer {
.put("body", httpResponse.body),
)
}
METHOD_READ_INSTALLED_AGENTS_FILE -> {
val codexHome = File(context.filesDir, "codex-home")
HostedCodexConfig.installBundledAgentsFile(context, codexHome)
JSONObject()
.put("requestId", requestId)
.put("ok", true)
.put("agentsMarkdown", HostedCodexConfig.readInstalledAgentsMarkdown(codexHome))
}
else -> {
JSONObject()
.put("requestId", requestId)

View File

@@ -68,7 +68,7 @@ class CodexdForegroundService : Service() {
val socketPath = intent.getStringExtra(EXTRA_SOCKET_PATH) ?: defaultSocketPath()
val codexHome = intent.getStringExtra(EXTRA_CODEX_HOME) ?: defaultCodexHome()
HostedCodexConfig.installAgentsFile(File(codexHome))
HostedCodexConfig.installBundledAgentsFile(this, File(codexHome))
val codexdBinary = resolveCodexdBinary()
val args = mutableListOf(

View File

@@ -0,0 +1,45 @@
# Android Agent/Genie Runtime Notes
This Codex runtime is operating on an Android device through the Agent Platform.
## If you are the Agent
- The user interacts only with the Agent.
- Plan the work, choose the target package or packages, and start one Genie session per target app that needs to be driven.
- Delegate objectives, not tool choices. Tell each Genie what outcome it must achieve in its paired app and let the Genie choose its own tools.
- Answer Genie questions directly when you can. If the answer depends on user intent or missing constraints, ask the user.
- Keep auth, upstream access, and any internet-facing model traffic on the Agent side.
## If you are a Genie
- You are paired with exactly one target app sandbox for this session.
- Solve the delegated objective inside that sandbox by using the normal Codex tool path and the Android tools that are available on-device.
- Ask the Agent a concise free-form question only when you are blocked on missing intent, missing constraints, or a framework-owned action.
- Do not assume you can reach the internet directly. Model and auth traffic are Agent-owned.
- Do not rely on direct cross-app `bindService(...)` or raw local sockets to reach the Agent. Use the framework-managed session bridge.
## Shell and device tooling
- Prefer standard Android shell tools first: `cmd`, `am`, `pm`, `input`, `uiautomator`, `dumpsys`, `wm`, `settings`, `content`, `logcat`.
- Do not assume desktop/Linux extras such as `python3`, GNU `date -d`, or other non-stock userland tools are present.
- When a command affects app launch or user-visible state, prefer an explicit `--user 0` when the tool supports it.
- Keep temporary artifacts in app-private storage such as the current app `files/` or `cache/` directories, or under `$CODEX_HOME`. Do not rely on shared storage.
## UI inspection and files
- In self-target Genie mode, prefer `uiautomator dump /proc/self/fd/1` or `uiautomator dump /dev/stdout` when stdout capture is acceptable.
- Plain `uiautomator dump` writes to the app-private dump directory.
- Explicit shared-storage targets such as `/sdcard/...` are redirected back into app-private storage in self-target mode.
- Do not assume `/sdcard` or `/data/local/tmp` are readable or writable from the paired app sandbox.
## Presentation semantics
- Detached launch, shown-detached, and attached are different states.
- `targetDetached=true` means the target is still detached even if it is visible in a detached or mirrored presentation.
- If the task says the app should be visible to the user, do not claim success until the target is attached unless the task explicitly allows detached presentation.
- Treat framework session state as the source of truth for presentation state.
## Working style
- Prefer solving tasks with normal shell/tool use before reverse-engineering APK contents.
- When you need to ask a question, make it specific and short so the Agent can either answer directly or escalate it to the user.

View File

@@ -1,19 +1,22 @@
package com.openai.codex.bridge;
import android.content.Context;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
public final class HostedCodexConfig {
public static final String ANDROID_HTTP_PROVIDER_ID = "android-openai-http";
private static final String AGENTS_FILENAME = "AGENTS.md";
public static final String AGENTS_FILENAME = "AGENTS.md";
private static final String BUNDLED_AGENTS_ASSET_PATH = AGENTS_FILENAME;
private HostedCodexConfig() {}
public static void write(File codexHome, String baseUrl) throws IOException {
public static void write(Context context, File codexHome, String baseUrl) throws IOException {
ensureCodexHome(codexHome);
installAgentsFile(codexHome);
installBundledAgentsFile(context, codexHome);
String escapedBaseUrl = baseUrl
.replace("\\", "\\\\")
@@ -30,61 +33,27 @@ public final class HostedCodexConfig {
configToml.getBytes(StandardCharsets.UTF_8));
}
public static void installAgentsFile(File codexHome) throws IOException {
public static void installBundledAgentsFile(Context context, File codexHome) throws IOException {
installAgentsFile(codexHome, readBundledAgentsMarkdown(context));
}
public static void installAgentsFile(File codexHome, String agentsMarkdown) throws IOException {
ensureCodexHome(codexHome);
Files.write(
new File(codexHome, AGENTS_FILENAME).toPath(),
buildAgentsMarkdown().getBytes(StandardCharsets.UTF_8));
agentsMarkdown.getBytes(StandardCharsets.UTF_8));
}
static String buildAgentsMarkdown() {
return """
# Android Agent/Genie Runtime Notes
public static String readBundledAgentsMarkdown(Context context) throws IOException {
try (InputStream inputStream = context.getAssets().open(BUNDLED_AGENTS_ASSET_PATH)) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
}
This Codex runtime is operating on an Android device through the Agent Platform.
## If you are the Agent
- The user interacts only with the Agent.
- Plan the work, choose the target package or packages, and start one Genie session per target app that needs to be driven.
- Delegate objectives, not tool choices. Tell each Genie what outcome it must achieve in its paired app and let the Genie choose its own tools.
- Answer Genie questions directly when you can. If the answer depends on user intent or missing constraints, ask the user.
- Keep auth, upstream access, and any internet-facing model traffic on the Agent side.
## If you are a Genie
- You are paired with exactly one target app sandbox for this session.
- Solve the delegated objective inside that sandbox by using the normal Codex tool path and the Android tools that are available on-device.
- Ask the Agent a concise free-form question only when you are blocked on missing intent, missing constraints, or a framework-owned action.
- Do not assume you can reach the internet directly. Model and auth traffic are Agent-owned.
- Do not rely on direct cross-app `bindService(...)` or raw local sockets to reach the Agent. Use the framework-managed session bridge.
## Shell and device tooling
- Prefer standard Android shell tools first: `cmd`, `am`, `pm`, `input`, `uiautomator`, `dumpsys`, `wm`, `settings`, `content`, `logcat`.
- Do not assume desktop/Linux extras such as `python3`, GNU `date -d`, or other non-stock userland tools are present.
- When a command affects app launch or user-visible state, prefer an explicit `--user 0` when the tool supports it.
- Keep temporary artifacts in app-private storage such as the current app `files/` or `cache/` directories, or under `$CODEX_HOME`. Do not rely on shared storage.
## UI inspection and files
- In self-target Genie mode, prefer `uiautomator dump /proc/self/fd/1` or `uiautomator dump /dev/stdout` when stdout capture is acceptable.
- Plain `uiautomator dump` writes to the app-private dump directory.
- Explicit shared-storage targets such as `/sdcard/...` are redirected back into app-private storage in self-target mode.
- Do not assume `/sdcard` or `/data/local/tmp` are readable or writable from the paired app sandbox.
## Presentation semantics
- Detached launch, shown-detached, and attached are different states.
- `targetDetached=true` means the target is still detached even if it is visible in a detached or mirrored presentation.
- If the task says the app should be visible to the user, do not claim success until the target is attached unless the task explicitly allows detached presentation.
- Treat framework session state as the source of truth for presentation state.
## Working style
- Prefer solving tasks with normal shell/tool use before reverse-engineering APK contents.
- When you need to ask a question, make it specific and short so the Agent can either answer directly or escalate it to the user.
""";
public static String readInstalledAgentsMarkdown(File codexHome) throws IOException {
return new String(
Files.readAllBytes(new File(codexHome, AGENTS_FILENAME).toPath()),
StandardCharsets.UTF_8);
}
private static void ensureCodexHome(File codexHome) throws IOException {

View File

@@ -1,7 +1,6 @@
package com.openai.codex.bridge;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.nio.charset.StandardCharsets;
@@ -10,43 +9,27 @@ import org.junit.Test;
public final class HostedCodexConfigTest {
@Test
public void writeInstallsConfigAndAgentsFile() throws Exception {
public void installAgentsFileWritesExpectedGuidance() throws Exception {
File codexHome = Files.createTempDirectory("hosted-codex-home").toFile();
String agentsMarkdown = "# Runtime Notes\n\n- prefer `cmd`\n";
HostedCodexConfig.write(codexHome, "http://127.0.0.1:8080");
HostedCodexConfig.installAgentsFile(codexHome, agentsMarkdown);
String configToml =
new String(
Files.readAllBytes(new File(codexHome, "config.toml").toPath()),
StandardCharsets.UTF_8);
String agentsMarkdown =
String installedMarkdown =
new String(
Files.readAllBytes(new File(codexHome, "AGENTS.md").toPath()),
StandardCharsets.UTF_8);
assertTrue(configToml.contains("model_provider = \"android-openai-http\""));
assertTrue(configToml.contains("base_url = \"http://127.0.0.1:8080\""));
assertEquals(HostedCodexConfig.buildAgentsMarkdown(), agentsMarkdown);
assertEquals(agentsMarkdown, installedMarkdown);
}
@Test
public void installAgentsFileWritesExpectedGuidance() throws Exception {
public void readInstalledAgentsMarkdownReadsExistingFile() throws Exception {
File codexHome = Files.createTempDirectory("hosted-codex-agents").toFile();
HostedCodexConfig.installAgentsFile(codexHome, "# Agent file\n");
HostedCodexConfig.installAgentsFile(codexHome);
String installedMarkdown = HostedCodexConfig.readInstalledAgentsMarkdown(codexHome);
String agentsMarkdown =
new String(
Files.readAllBytes(new File(codexHome, "AGENTS.md").toPath()),
StandardCharsets.UTF_8);
assertEquals(HostedCodexConfig.buildAgentsMarkdown(), agentsMarkdown);
assertTrue(agentsMarkdown.contains("The user interacts only with the Agent."));
assertTrue(
agentsMarkdown.contains(
"Do not rely on direct cross-app `bindService(...)` or raw local sockets"));
assertTrue(
agentsMarkdown.contains(
"If the task says the app should be visible to the user, do not claim success until the target is attached"));
assertEquals("# Agent file\n", installedMarkdown);
}
}

View File

@@ -29,6 +29,7 @@ class AgentBridgeClient(
private const val TAG = "AgentBridgeClient"
private const val OP_GET_RUNTIME_STATUS = "getRuntimeStatus"
private const val OP_SEND_RESPONSES_REQUEST = "sendResponsesRequest"
private const val OP_READ_INSTALLED_AGENTS_FILE = "readInstalledAgentsFile"
}
private val bridgeFd: ParcelFileDescriptor = callback.openSessionBridge(sessionId)
@@ -55,6 +56,12 @@ class AgentBridgeClient(
)
}
fun readInstalledAgentsMarkdown(): String {
return request(
JSONObject().put("method", OP_READ_INSTALLED_AGENTS_FILE),
).getString("agentsMarkdown")
}
override fun openResponsesStream(body: String): InputStream {
val response = request(
JSONObject()

View File

@@ -79,7 +79,7 @@ class CodexAppServerHost(
deleteRecursively()
mkdirs()
}
HostedCodexConfig.installAgentsFile(codexHome)
HostedCodexConfig.installAgentsFile(codexHome, bridgeClient.readInstalledAgentsMarkdown())
val proxy = GenieLocalCodexProxy(
sessionId = request.sessionId,
socketDirectory = context.cacheDir,

View File

@@ -28,9 +28,9 @@ The current repo now contains these implementation slices:
- 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.
- The Android host layer now installs a generated `AGENTS.md` into the Agent
Codex home and every per-session Genie Codex home so the hosted runtimes get
the same device-operating guidance at startup.
- The Android host layer now copies a checked-in `AGENTS.md` asset into the
Agent Codex home at bootstrap, and each Genie session copies that installed
Agent file into its per-session Codex home over the framework session bridge.
- Target-package planning now relies on the hosted Agent Codex runtime using
standard Android shell tools already available on-device (`cmd package`, `pm`,
`am`) instead of Kotlin-side app discovery wrappers.