diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentEnv.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentEnv.kt index 0842f6b5a..05d5d4e77 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentEnv.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentEnv.kt @@ -18,7 +18,6 @@ object AgentEnv { private const val TAG = "AgentEnv" private const val FILE = "agent-env.json" private const val GUEST_BIN = "/opt/droiddeck-agent/bin" - private const val BASE_PATH = "/usr/local/bin:/usr/bin:/bin" private val NAME = Regex("[A-Za-z0-9._-]{1,64}") private val KEY = Regex("[A-Za-z_][A-Za-z0-9_]*") private val lock = Any() @@ -160,7 +159,7 @@ object AgentEnv { } private fun pathOf(lines: List) = - lines.lastOrNull { it.startsWith("PATH=") }?.substringAfter('=')?.takeIf { it.isNotBlank() } ?: BASE_PATH + lines.lastOrNull { it.startsWith("PATH=") }?.substringAfter('=')?.takeIf { it.isNotBlank() } ?: LinuxRuntime.GUEST_PATH private fun bin(context: Context): File { val root = LinuxRuntime.rootDir(context).canonicalFile.toPath() diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java index 81f005ab7..3d4045ae3 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java @@ -26,6 +26,7 @@ public final class LinuxRuntime { public static final String EXTRA_LINUX_MODE = "linux_mode"; private static final String KGSL_DEVICE = "/dev/kgsl-3d0"; public static final String GUEST_RUNTIME_DIR = "/run/droiddeck"; + public static final String GUEST_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games"; private static final String SYSTEM_FONTS = "/system/fonts"; private static final String GUEST_SYSTEM_FONTS = "/usr/local/share/fonts/android"; public static final String DEBUG_LOG_DIR = "DroidDeck"; diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index 3bfdd2d53..c87ea5e6e 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -753,7 +753,7 @@ class SessionService : Service() { guest.add("DROIDDECK_BWRAP_SOCKET=" + com.droiddeck.launcher.runtime.BwrapSpawner.socket(this)) guest.add("HOME=/root") guest.add("USER=root") - guest.add("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games") + guest.add("PATH=" + LinuxRuntime.GUEST_PATH) guest.add("TERM=xterm-256color") guest.add("LANG=C.UTF-8") guest.add("BL_STEAM_LANGUAGE=" + SteamLanguage.forLocale( diff --git a/app/src/test/java/com/droiddeck/launcher/agent/AgentSecurityTest.kt b/app/src/test/java/com/droiddeck/launcher/agent/AgentSecurityTest.kt index f625d6b41..7fe76acc9 100644 --- a/app/src/test/java/com/droiddeck/launcher/agent/AgentSecurityTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/agent/AgentSecurityTest.kt @@ -181,6 +181,32 @@ class AgentSecurityTest { assertEquals(listOf("TEST=old", "STICKY=1"), AgentEnv.beginSession(context)) } + @Test fun binaryOverridesPreserveTheRuntimeSearchPathForOneSession() { + AgentAccess.setCommandsEnabled(context, true) + File(AgentEnv.inbox(context).apply { mkdirs() }, "gamescope").writeText("test binary") + AgentEnv.override(context, "gamescope") + + val path = AgentEnv.beginSession(context).single { it.startsWith("PATH=") }.substringAfter('=') + assertEquals("/opt/droiddeck-agent/bin:" + LinuxRuntime.GUEST_PATH, path) + assertTrue(path.split(':').contains("/usr/games")) + assertTrue(path.split(':').contains("/usr/sbin")) + + AgentEnv.endSession(context) + assertTrue(AgentEnv.beginSession(context).none { it.startsWith("PATH=") }) + } + + @Test fun binaryOverridesKeepExplicitPathsAndPendingPathPrecedence() { + AgentAccess.setCommandsEnabled(context, true) + AgentEnv.set(context, listOf("PATH=/custom/persistent"), true) + AgentEnv.set(context, listOf("PATH=/custom/next:/usr/games"), false) + File(AgentEnv.inbox(context).apply { mkdirs() }, "gamescope").writeText("test binary") + AgentEnv.override(context, "gamescope") + + assertEquals("PATH=/opt/droiddeck-agent/bin:/custom/next:/usr/games", AgentEnv.beginSession(context).last()) + AgentEnv.endSession(context) + assertEquals(listOf("PATH=/custom/persistent"), AgentEnv.beginSession(context)) + } + @Test fun fastPathLibraryIsValidatedAndSelectedForOneSession() { AgentAccess.setCommandsEnabled(context, true) val source = File(AgentEnv.inbox(context).apply { mkdirs() }, "libblfastpath.so") diff --git a/docs/agent-control.md b/docs/agent-control.md index 5dc16253e..b131651f3 100644 --- a/docs/agent-control.md +++ b/docs/agent-control.md @@ -39,6 +39,11 @@ Disabling the toggle clears queued and persistent agent environment settings and overrides. Disabled settings cannot apply at the next session start. Already running commands and effects already applied to a session are not undone; stop and restart that session to discard them. +Program overrides prepend their directory to the normal session PATH, including `/usr/games` +where Ubuntu installs `gamescopereaper`. An explicit `PATH=...` environment entry replaces that +default. Keep the runtime directories when setting a custom PATH; if an old persistent override +hides runtime tools, inspect `env list`, clear or correct that entry, and restart the session. + Authorized command execution runs inside the runtime containing Steam's saved login. CDP evaluates JavaScript in the authenticated Steam client, and preferences can contain private values. These capabilities remain unrestricted when enabled: the toggle is an explicit opt-in, not credential