Skip to content

Commit 8dc90aa

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 3a824fc commit 8dc90aa

11 files changed

Lines changed: 362 additions & 11 deletions

File tree

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ import com.google.adk.kt.serialization.adkJson
2323
import com.google.adk.kt.sessions.SessionService
2424
import com.google.adk.kt.telemetry.TelemetryConfig
2525
import com.google.adk.kt.webserver.AdkWebServer.StatusAwareLogger
26+
import com.google.adk.kt.webserver.dev.routes.debugRoutes
27+
import com.google.adk.kt.webserver.dev.routes.evalRoutes
28+
import com.google.adk.kt.webserver.dev.routes.graphRoutes
2629
import com.google.adk.kt.webserver.loaders.AgentLoader
2730
import com.google.adk.kt.webserver.models.VersionInfo
2831
import com.google.adk.kt.webserver.routes.appRoutes
2932
import com.google.adk.kt.webserver.routes.artifactRoutes
30-
import com.google.adk.kt.webserver.routes.debugRoutes
31-
import com.google.adk.kt.webserver.routes.evalRoutes
32-
import com.google.adk.kt.webserver.routes.graphRoutes
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
@@ -57,9 +58,12 @@ import org.slf4j.event.Level
5758
* Embedded Ktor server exposing the ADK dev/web API.
5859
*
5960
* [start] and [stop] are safe to call from different threads; a [stop] arriving while [start] is
60-
* still binding aborts it. A failed [start] leaves the engine recorded, so call [stop] before
61+
* still binding aborts it, and a failed [start] leaves the engine recorded, so call [stop] before
6162
* retrying.
6263
*
64+
* Set `adk.web.ui.enabled` to false, as a system property or in the application config, to leave
65+
* the Development UI unmounted.
66+
*
6367
* @property captureMessageContent When true, the server records prompt/response content into
6468
* telemetry spans so the Dev UI trace view can display it. This may capture PII and increase span
6569
* size, so it defaults to false; enable it only for local development.
@@ -165,6 +169,9 @@ fun Application.adkModule(
165169
)
166170
}
167171

172+
// Hoisted because the Application receiver is not implicitly available inside `routing`.
173+
val webUiEnabled = isWebUiEnabled(default = true)
174+
168175
routing {
169176
get("/health") { call.respond(mapOf("status" to "ok")) }
170177
get("/version") {
@@ -183,6 +190,8 @@ fun Application.adkModule(
183190
graphRoutes(agentLoader, sessionService)
184191
runRoutes(agentLoader, sessionService, artifactService, plugins)
185192
sessionRoutes(sessionService)
186-
staticRoutes(this@adkModule)
193+
if (webUiEnabled) {
194+
staticRoutes(this@adkModule)
195+
}
187196
}
188197
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
/** Kept so callers of the previous package keep compiling; use the `dev` package instead. */
20+
@Deprecated(
21+
"Moved to the development-only package.",
22+
ReplaceWith("AgentGraphGenerator", "com.google.adk.kt.webserver.dev.AgentGraphGenerator"),
23+
)
24+
typealias AgentGraphGenerator = com.google.adk.kt.webserver.dev.AgentGraphGenerator

webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/AgentGraphGenerator.kt renamed to webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/dev/AgentGraphGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.google.adk.kt.webserver
17+
package com.google.adk.kt.webserver.dev
1818

1919
import com.google.adk.kt.agents.BaseAgent
2020
import com.google.adk.kt.agents.LlmAgent

webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/routes/DebugRoutes.kt renamed to webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/dev/routes/DebugRoutes.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.google.adk.kt.webserver.routes
17+
package com.google.adk.kt.webserver.dev.routes
1818

1919
import com.google.adk.kt.webserver.telemetry.ApiServerSpanExporter
2020
import io.ktor.http.HttpStatusCode

webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/routes/EvalRoutes.kt renamed to webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/dev/routes/EvalRoutes.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.google.adk.kt.webserver.routes
17+
package com.google.adk.kt.webserver.dev.routes
1818

1919
import io.ktor.http.HttpStatusCode
2020
import io.ktor.server.application.call

webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/routes/GraphRoutes.kt renamed to webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/dev/routes/GraphRoutes.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.google.adk.kt.webserver.routes
17+
package com.google.adk.kt.webserver.dev.routes
1818

1919
import com.google.adk.kt.sessions.SessionKey
2020
import com.google.adk.kt.sessions.SessionService
21-
import com.google.adk.kt.webserver.AgentGraphGenerator
21+
import com.google.adk.kt.webserver.dev.AgentGraphGenerator
2222
import com.google.adk.kt.webserver.loaders.AgentLoader
2323
import io.ktor.http.HttpStatusCode
2424
import io.ktor.http.Parameters
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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.routes
18+
19+
import com.google.adk.kt.sessions.SessionService
20+
import com.google.adk.kt.webserver.dev.routes.debugRoutes as devDebugRoutes
21+
import com.google.adk.kt.webserver.dev.routes.evalRoutes as devEvalRoutes
22+
import com.google.adk.kt.webserver.dev.routes.graphRoutes as devGraphRoutes
23+
import com.google.adk.kt.webserver.loaders.AgentLoader
24+
import com.google.adk.kt.webserver.telemetry.ApiServerSpanExporter
25+
import io.ktor.server.routing.Route
26+
27+
/** Kept so callers of the previous package keep compiling; use the `dev` package instead. */
28+
@Deprecated(
29+
"Moved to the development-only package.",
30+
ReplaceWith("debugRoutes(exporter)", "com.google.adk.kt.webserver.dev.routes.debugRoutes"),
31+
)
32+
fun Route.debugRoutes(exporter: ApiServerSpanExporter) = devDebugRoutes(exporter)
33+
34+
/** Kept so callers of the previous package keep compiling; use the `dev` package instead. */
35+
@Deprecated(
36+
"Moved to the development-only package.",
37+
ReplaceWith("evalRoutes()", "com.google.adk.kt.webserver.dev.routes.evalRoutes"),
38+
)
39+
fun Route.evalRoutes() = devEvalRoutes()
40+
41+
/** Kept so callers of the previous package keep compiling; use the `dev` package instead. */
42+
@Deprecated(
43+
"Moved to the development-only package.",
44+
ReplaceWith(
45+
"graphRoutes(agentLoader, sessionService)",
46+
"com.google.adk.kt.webserver.dev.routes.graphRoutes",
47+
),
48+
)
49+
fun Route.graphRoutes(agentLoader: AgentLoader, sessionService: SessionService) =
50+
devGraphRoutes(agentLoader, sessionService)

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,29 @@ 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 blank or not a boolean counts as unset, so a
41+
* mistyped system property cannot mask a setting in the config.
42+
*/
43+
internal fun Application.isWebUiEnabled(default: Boolean): Boolean =
44+
webUiSetting(System.getProperty(WEB_UI_ENABLED_PROPERTY))
45+
?: webUiSetting(environment.config.propertyOrNull(WEB_UI_ENABLED_PROPERTY)?.getString())
46+
?: default
47+
48+
/** Parses one configured value, returning null when it is absent, blank or not a boolean. */
49+
private fun webUiSetting(raw: String?): Boolean? {
50+
val value = raw?.trim()?.takeIf { it.isNotEmpty() } ?: return null
51+
return value.lowercase().toBooleanStrictOrNull()
52+
?: run {
53+
logger.warn("Ignoring a non-boolean value of {}: {}", WEB_UI_ENABLED_PROPERTY, value)
54+
null
55+
}
56+
}
57+
3558
fun Route.staticRoutes(application: Application) {
3659
var webUiDir =
3760
System.getProperty("adk.web.ui.dir")
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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.staticRoutes
20+
import com.google.common.truth.Truth.assertThat
21+
import io.ktor.client.request.get
22+
import io.ktor.client.statement.bodyAsText
23+
import io.ktor.http.HttpStatusCode
24+
import io.ktor.server.routing.routing
25+
import io.ktor.server.testing.testApplication
26+
import org.junit.Test
27+
import org.junit.runner.RunWith
28+
import org.junit.runners.JUnit4
29+
30+
/** The server resolves the Development UI from `browser/` on the classpath. */
31+
@RunWith(JUnit4::class)
32+
class DevUiAssetsTest {
33+
34+
@Test
35+
fun devUi_index_isServedFromClasspath() = withoutWebUiDir {
36+
testApplication {
37+
application { routing { staticRoutes(this@application) } }
38+
39+
val response = client.get("/dev-ui/index.html")
40+
41+
assertThat(response.status).isEqualTo(HttpStatusCode.OK)
42+
assertThat(response.bodyAsText()).contains("<html")
43+
}
44+
}
45+
46+
@Test
47+
fun devUi_nestedAsset_isServedFromClasspath() = withoutWebUiDir {
48+
testApplication {
49+
application { routing { staticRoutes(this@application) } }
50+
51+
// A nested path proves the whole asset tree is packaged, not just the entry point.
52+
assertThat(client.get("/dev-ui/assets/audio-processor.js").status)
53+
.isEqualTo(HttpStatusCode.OK)
54+
}
55+
}
56+
57+
/** Runs [body] with the `adk.web.ui.dir` system property cleared. */
58+
private fun withoutWebUiDir(body: () -> Unit) {
59+
val previous: String? = System.getProperty(WEB_UI_DIR_PROPERTY)
60+
System.clearProperty(WEB_UI_DIR_PROPERTY)
61+
try {
62+
body()
63+
} finally {
64+
if (previous != null) System.setProperty(WEB_UI_DIR_PROPERTY, previous)
65+
}
66+
}
67+
68+
private companion object {
69+
const val WEB_UI_DIR_PROPERTY = "adk.web.ui.dir"
70+
}
71+
}

0 commit comments

Comments
 (0)