Skip to content

Commit 1dcb019

Browse files
sherryfoxcopybara-github
authored andcommitted
feat: allow disabling the ADK Development UI with adk.web.ui.enabled
Set adk.web.ui.enabled to false, as a system property or in the application config, to leave the Development UI routes unmounted. It stays mounted by default. PiperOrigin-RevId: 968449879
1 parent e856bce commit 1dcb019

3 files changed

Lines changed: 249 additions & 1 deletion

File tree

webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/AdkWebServer.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.google.adk.kt.webserver.loaders.AgentLoader
3030
import com.google.adk.kt.webserver.models.VersionInfo
3131
import com.google.adk.kt.webserver.routes.appRoutes
3232
import com.google.adk.kt.webserver.routes.artifactRoutes
33+
import com.google.adk.kt.webserver.routes.isWebUiEnabled
3334
import com.google.adk.kt.webserver.routes.runRoutes
3435
import com.google.adk.kt.webserver.routes.sessionRoutes
3536
import com.google.adk.kt.webserver.routes.staticRoutes
@@ -123,6 +124,13 @@ class AdkWebServer(
123124
}
124125
}
125126

127+
/**
128+
* Installs the ADK routes, including the Development UI.
129+
*
130+
* Set the `adk.web.ui.enabled` system property to `false` to leave the Development UI unmounted, or
131+
* set it in this application's Ktor config; only `true` and `false` count, and any other value is
132+
* ignored with a warning so the next source decides.
133+
*/
126134
@OptIn(FrameworkInternalApi::class)
127135
fun Application.adkModule(
128136
sessionService: SessionService,
@@ -183,6 +191,8 @@ fun Application.adkModule(
183191
graphRoutes(agentLoader, sessionService)
184192
runRoutes(agentLoader, sessionService, artifactService, plugins)
185193
sessionRoutes(sessionService)
186-
staticRoutes(this@adkModule)
194+
if (this@adkModule.isWebUiEnabled(default = true)) {
195+
staticRoutes(this@adkModule)
196+
}
187197
}
188198
}

webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/routes/StaticRoutes.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,39 @@ import org.slf4j.LoggerFactory
3232

3333
private val logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass())
3434

35+
/** Property that decides whether the Development UI is served at all. */
36+
internal const val WEB_UI_ENABLED_PROPERTY = "adk.web.ui.enabled"
37+
38+
/**
39+
* Whether to mount the Development UI, from the `adk.web.ui.enabled` system property, else the
40+
* application config, else [default]. A value that is not a boolean counts as unset, so a mistyped
41+
* system property cannot mask a setting in the config. Only a host-supplied Ktor config is read;
42+
* `embeddedServer` supplies none.
43+
*/
44+
internal fun Application.isWebUiEnabled(default: Boolean): Boolean =
45+
webUiSettingOrNull(System.getProperty(WEB_UI_ENABLED_PROPERTY), "system property")
46+
?: webUiSettingOrNull(
47+
environment.config.propertyOrNull(WEB_UI_ENABLED_PROPERTY)?.getString(),
48+
"application config",
49+
)
50+
?: default
51+
52+
/** Parses one configured value; null when absent or not a boolean, warning in the latter case. */
53+
private fun webUiSettingOrNull(raw: String?, source: String): Boolean? {
54+
if (raw == null) return null
55+
val value = raw.trim()
56+
return value.lowercase().toBooleanStrictOrNull().also {
57+
if (it == null) {
58+
logger.warn(
59+
"Ignoring a non-boolean {} from the {}: \"{}\"",
60+
WEB_UI_ENABLED_PROPERTY,
61+
source,
62+
value,
63+
)
64+
}
65+
}
66+
}
67+
3568
fun Route.staticRoutes(application: Application) {
3669
var webUiDir =
3770
System.getProperty("adk.web.ui.dir")
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.kt.webserver
18+
19+
import com.google.adk.kt.webserver.routes.WEB_UI_ENABLED_PROPERTY
20+
import com.google.adk.kt.webserver.routes.isWebUiEnabled
21+
import com.google.adk.kt.webserver.telemetry.ApiServerSpanExporter
22+
import com.google.common.truth.Truth.assertThat
23+
import io.ktor.client.request.get
24+
import io.ktor.http.HttpStatusCode
25+
import io.ktor.server.config.MapApplicationConfig
26+
import io.ktor.server.testing.ApplicationTestBuilder
27+
import io.ktor.server.testing.testApplication
28+
import org.junit.Test
29+
import org.junit.runner.RunWith
30+
import org.junit.runners.JUnit4
31+
32+
/**
33+
* The `adk.web.ui.enabled` property decides whether the Development UI routes are mounted.
34+
*
35+
* `/` is the discriminator: `staticRoutes` registers its redirect only when the UI is mounted.
36+
*/
37+
@RunWith(JUnit4::class)
38+
class WebUiToggleTest {
39+
private val sessionService = FakeSessionService()
40+
private val artifactService = FakeArtifactService()
41+
private val agentLoader = FakeAgentLoader()
42+
43+
@Test
44+
fun webUi_unset_isMounted() =
45+
withWebUiProperty(null) {
46+
testApplication {
47+
installAdk()
48+
49+
assertThat(rootStatus()).isEqualTo(HttpStatusCode.Found)
50+
}
51+
}
52+
53+
@Test
54+
fun webUi_disabled_isNotMounted() =
55+
withWebUiProperty("false") {
56+
testApplication {
57+
installAdk()
58+
59+
assertThat(rootStatus()).isEqualTo(HttpStatusCode.NotFound)
60+
assertThat(client.get(DEV_UI_INDEX).status).isEqualTo(HttpStatusCode.NotFound)
61+
// The contract endpoints are untouched by the toggle.
62+
assertThat(client.get("/health").status).isEqualTo(HttpStatusCode.OK)
63+
}
64+
}
65+
66+
@Test
67+
fun webUi_paddedValue_isTrimmedAndHonoured() =
68+
withWebUiProperty(" false ") {
69+
testApplication {
70+
installAdk()
71+
72+
assertThat(rootStatus()).isEqualTo(HttpStatusCode.NotFound)
73+
}
74+
}
75+
76+
@Test
77+
fun webUi_disabledInMixedCase_isNotMounted() =
78+
withWebUiProperty("False") {
79+
testApplication {
80+
installAdk()
81+
82+
assertThat(rootStatus()).isEqualTo(HttpStatusCode.NotFound)
83+
}
84+
}
85+
86+
@Test
87+
fun webUi_nonBooleanValue_fallsBackToDefault() =
88+
withWebUiProperty("perhaps") {
89+
testApplication {
90+
installAdk()
91+
92+
assertThat(rootStatus()).isEqualTo(HttpStatusCode.Found)
93+
}
94+
}
95+
96+
@Test
97+
fun webUi_disabledInConfig_isNotMounted() =
98+
withWebUiProperty(null) {
99+
testApplication {
100+
environment { config = MapApplicationConfig(WEB_UI_ENABLED_PROPERTY to "false") }
101+
installAdk()
102+
103+
assertThat(rootStatus()).isEqualTo(HttpStatusCode.NotFound)
104+
}
105+
}
106+
107+
@Test
108+
fun webUi_nonBooleanConfigValue_fallsBackToDefault() =
109+
withWebUiProperty(null) {
110+
testApplication {
111+
environment { config = MapApplicationConfig(WEB_UI_ENABLED_PROPERTY to "perhaps") }
112+
installAdk()
113+
114+
assertThat(rootStatus()).isEqualTo(HttpStatusCode.Found)
115+
}
116+
}
117+
118+
@Test
119+
fun webUi_enabledInConfig_overridesCallerDefault() =
120+
withWebUiProperty(null) {
121+
testApplication {
122+
environment { config = MapApplicationConfig(WEB_UI_ENABLED_PROPERTY to "true") }
123+
application { assertThat(isWebUiEnabled(default = false)).isTrue() }
124+
125+
client.get("/")
126+
}
127+
}
128+
129+
@Test
130+
fun webUi_blankProperty_fallsThroughToConfig() =
131+
withWebUiProperty(" ") {
132+
testApplication {
133+
environment { config = MapApplicationConfig(WEB_UI_ENABLED_PROPERTY to "false") }
134+
installAdk()
135+
136+
assertThat(rootStatus()).isEqualTo(HttpStatusCode.NotFound)
137+
}
138+
}
139+
140+
@Test
141+
fun webUi_nonBooleanProperty_fallsThroughToConfig() =
142+
withWebUiProperty("no") {
143+
testApplication {
144+
environment { config = MapApplicationConfig(WEB_UI_ENABLED_PROPERTY to "false") }
145+
installAdk()
146+
147+
assertThat(rootStatus()).isEqualTo(HttpStatusCode.NotFound)
148+
}
149+
}
150+
151+
@Test
152+
fun webUi_property_winsOverConfig() =
153+
withWebUiProperty("true") {
154+
testApplication {
155+
environment { config = MapApplicationConfig(WEB_UI_ENABLED_PROPERTY to "false") }
156+
installAdk()
157+
158+
assertThat(rootStatus()).isEqualTo(HttpStatusCode.Found)
159+
}
160+
}
161+
162+
@Test
163+
fun webUi_unsetEverywhere_usesCallerDefault() =
164+
withWebUiProperty(null) {
165+
testApplication {
166+
application {
167+
assertThat(isWebUiEnabled(default = false)).isFalse()
168+
assertThat(isWebUiEnabled(default = true)).isTrue()
169+
}
170+
// Start the app here so a failed assertion is reported at this line, not at teardown.
171+
client.get("/")
172+
}
173+
}
174+
175+
private fun ApplicationTestBuilder.installAdk() {
176+
application { adkModule(sessionService, artifactService, agentLoader, ApiServerSpanExporter()) }
177+
}
178+
179+
/** Status of `/` without following the redirect, so the redirect itself is what is asserted. */
180+
private suspend fun ApplicationTestBuilder.rootStatus(): HttpStatusCode =
181+
createClient { followRedirects = false }.get("/").status
182+
183+
/** Runs [body] with `adk.web.ui.enabled` set to [value], or unset when it is null. */
184+
private fun withWebUiProperty(value: String?, body: () -> Unit) {
185+
val previous: String? = System.getProperty(WEB_UI_ENABLED_PROPERTY)
186+
if (value == null) {
187+
System.clearProperty(WEB_UI_ENABLED_PROPERTY)
188+
} else {
189+
System.setProperty(WEB_UI_ENABLED_PROPERTY, value)
190+
}
191+
try {
192+
body()
193+
} finally {
194+
if (previous == null) {
195+
System.clearProperty(WEB_UI_ENABLED_PROPERTY)
196+
} else {
197+
System.setProperty(WEB_UI_ENABLED_PROPERTY, previous)
198+
}
199+
}
200+
}
201+
202+
private companion object {
203+
const val DEV_UI_INDEX = "/dev-ui/index.html"
204+
}
205+
}

0 commit comments

Comments
 (0)