Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion dev/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
ADK development utilities such as Spring REST server for agent.
ADK development utilities such as Spring REST server for agent.

## Serving the dev UI

The UI and its assets are served under `/dev-ui/`, and both `/` and `/dev-ui`
redirect there, keeping the query string. The assets are not served from the
origin root: `/adk_favicon.svg` and the like return 404, and only the `/dev-ui/`
form resolves.

## Behind a reverse proxy

When a gateway publishes this server under a path prefix and strips it, tell the
server the address browsers actually reach it on:

```properties
adk.web.backend-url=https://gateway.example.com/my-app
```

That one value does both halves: the entry redirect carries the prefix, and the
UI's own API calls go back through it. Nothing has to be forwarded by the proxy,
and nothing is read from the request.

It must be an absolute URL. The UI reads a value without a scheme as the host of
its live/websocket connection, so a bare `/my-app` makes that socket dial a host
named `my-app`.

Include any `server.servlet.context-path` in the value: in the redirect it
replaces the context path rather than stacking on it.

Leave it unset and nothing changes: the redirect is unprefixed and the bundled
`backendUrl` is served as it always was. A deployment that already restores the
prefix with Spring's own `server.forward-headers-strategy=framework` keeps
working that way; this property takes precedence over it for the redirect's
path.

Turning that Spring setting on is a decision to trust forwarded headers, and
both the standard `Forwarded` header and the `X-Forwarded-*` family are supplied
by the client unless something overwrites them. The proxy at the edge has to
strip or overwrite both kinds arriving from outside, or a caller can tell the
server it was reached somewhere it was not. Setting `adk.web.backend-url` does
not require that setting at all.
76 changes: 43 additions & 33 deletions dev/src/main/java/com/google/adk/web/AdkWebServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
import com.google.adk.memory.InMemoryMemoryService;
import com.google.adk.sessions.BaseSessionService;
import com.google.adk.sessions.InMemorySessionService;
import com.google.adk.web.config.BackendUrl;
import com.google.adk.web.config.DevUiAssets;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -51,6 +54,20 @@
@Value("${adk.web.ui.dir:#{null}}")
private String webUiDir;

@Value("${adk.web.backend-url:}")
private String backendUrlProperty;

private @Nullable BackendUrl parsedBackendUrl;

/** Parsed here once, and shared, so this and the runtime-config endpoint cannot diverge. */
@Bean
public synchronized BackendUrl backendUrl() {
if (parsedBackendUrl == null) {
parsedBackendUrl = BackendUrl.from(backendUrlProperty);
}
return parsedBackendUrl;
}

@Bean
public BaseSessionService sessionService() {
// TODO: Add logic to select service based on config (e.g., DB URL)
Expand Down Expand Up @@ -103,54 +120,47 @@
* @return A configured MappingJackson2HttpMessageConverter.
*/
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(

Check warning on line 123 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (17)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 123 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (17)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 123 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (17)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 123 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (17)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 123 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (25)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 123 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (25)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 123 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (25)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 123 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (25)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 123 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (21)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 123 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (21)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 123 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (21)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 123 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (21)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal
ObjectMapper objectMapper) {
return new MappingJackson2HttpMessageConverter(objectMapper);

Check warning on line 125 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (17)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 125 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (25)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 125 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (21)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal
}

/**
* Configures resource handlers for serving static content (like the Dev UI). Maps requests
* starting with "/dev-ui/" to the directory specified by the 'adk.web.ui.dir' system property.
* Maps requests under "/dev-ui/" to the directory named by the 'adk.web.ui.dir' property, or to
* the bundled copy on the classpath when that is unset.
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (webUiDir != null && !webUiDir.isEmpty()) {
// Ensure the path uses forward slashes and ends with a slash
String location = webUiDir.replace("\\", "/");
if (!location.startsWith("file:")) {
location = "file:" + location; // Ensure file: prefix
}
if (!location.endsWith("/")) {
location += "/";
}
log.debug("Mapping URL path /** to static resources at location: {}", location);
registry
.addResourceHandler("/**")
.addResourceLocations(location)
.setCachePeriod(0)
.resourceChain(true);

} else {
log.debug(
"System property 'adk.web.ui.dir' or config 'adk.web.ui.dir' is not set. Mapping URL path"
+ " /** to classpath:/browser/");
registry
.addResourceHandler("/**")
.addResourceLocations("classpath:/browser/")
.setCachePeriod(0)
.resourceChain(true);
}
String location = DevUiAssets.assetRoot(webUiDir);
log.debug("Mapping URL path /dev-ui/** to static resources at location: {}", location);
registry
.addResourceHandler("/dev-ui/**")
.addResourceLocations(location)
.setCachePeriod(0)
.resourceChain(true);
}

/**
* Configures simple automated controllers: - Redirects the root path "/" to "/dev-ui". - Forwards
* requests to "/dev-ui" to "/dev-ui/index.html" so the ResourceHandler serves it.
* Configures simple automated controllers: "/" and "/dev-ui" both redirect to the UI, at {@code
* adk.web.backend-url}'s path when that is set, and it forwards to index.html. The trailing slash
* is required: index.html declares a {@code <base href="./">}, so served from "/dev-ui" the app
* resolves its own router path to "dev-ui" and matches none of its routes. The query string is
* carried across because the UI selects its agent from {@code ?app=}.
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addRedirectViewController("/", "/dev-ui");
registry.addViewController("/dev-ui").setViewName("forward:/index.html");
registry.addViewController("/dev-ui/").setViewName("forward:/index.html");
String prefix = backendUrl().pathPrefix();
// The configured value is the public base, so do not stack the context path on it.
boolean contextRelative = prefix.isEmpty();
registry
.addRedirectViewController("/", prefix + "/dev-ui/")
.setKeepQueryParams(true)
.setContextRelative(contextRelative);
registry
.addRedirectViewController("/dev-ui", prefix + "/dev-ui/")
.setKeepQueryParams(true)
.setContextRelative(contextRelative);
registry.addViewController("/dev-ui/").setViewName("forward:/dev-ui/index.html");
}

/**
Expand Down
107 changes: 107 additions & 0 deletions dev/src/main/java/com/google/adk/web/config/BackendUrl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.adk.web.config;

import com.google.common.base.CharMatcher;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The address browsers reach this server on, from {@code adk.web.backend-url}. Parsed in one place,
* so the dev UI's config and its entry redirect read the same setting the same way.
*/
public final class BackendUrl {

private static final Logger log = LoggerFactory.getLogger(BackendUrl.class);

/** The UI strips the scheme case-sensitively, so an upper-case one is not usable. */
private static final Pattern ABSOLUTE_URL = Pattern.compile("^https?://.+");

private static final BackendUrl UNSET = new BackendUrl("", "");

private final String value;
private final String pathPrefix;

private BackendUrl(String value, String pathPrefix) {
this.value = value;
this.pathPrefix = pathPrefix;
}

/** Interprets {@code configured}, warning once if it is not something the UI can use. */
public static BackendUrl from(@Nullable String configured) {
if (configured == null || configured.trim().isEmpty()) {
return UNSET;
}
String trimmed = configured.trim();
// A trailing slash would double up: the UI appends paths that already start with one.
String normalized = CharMatcher.is('/').trimTrailingFrom(trimmed);
String path = pathOf(normalized);
if (ABSOLUTE_URL.matcher(normalized).matches() && path != null) {
return new BackendUrl(normalized, path);
}
log.warn(
"adk.web.backend-url should be an absolute URL, but is \"{}\". The dev UI reads a value"
+ " without a lower-case http:// or https:// scheme as the host of its live/websocket"
+ " connection.",
trimmed);
// Served as configured: an explicit value is never silently discarded.
return new BackendUrl(trimmed, path == null ? "" : path);
}

/** What the dev UI's runtime config reports, or empty when unset. */
public String value() {
return value;
}

/**
* The path a gateway strips, which the entry redirect has to carry, or empty when there is none.
* Percent-encoding is kept, because this goes into a {@code Location} header.
*/
public String pathPrefix() {
return pathPrefix;
}

/**
* The URL's path, or null when it is absent, relative, or carries something the UI cannot use.
*/
private static @Nullable String pathOf(String url) {
URI uri;
try {
uri = new URI(url);
} catch (URISyntaxException e) {
return null;
}
// The UI appends onto the whole value, so a query, fragment or userinfo would end up spliced
// into the middle of every request it builds.
if (uri.getRawQuery() != null || uri.getRawFragment() != null || uri.getRawUserInfo() != null) {
return null;
}
String raw = uri.getRawPath();
if (raw == null || raw.isEmpty()) {
return "";
}
if (!raw.startsWith("/")) {
return null;
}
// "//host" in a Location is protocol-relative, so a browser would read it as a host.
return CharMatcher.is('/').trimTrailingFrom(raw.replaceAll("^/+", "/"));
}
}
55 changes: 55 additions & 0 deletions dev/src/main/java/com/google/adk/web/config/DevUiAssets.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.adk.web.config;

import org.jspecify.annotations.Nullable;
import org.springframework.core.io.ResourceLoader;

/**
* Where the dev UI's static assets live. Shared so the resource handler and the runtime-config
* endpoint resolve the same location; normalizing {@code adk.web.ui.dir} separately in each would
* diverge silently.
*/
public final class DevUiAssets {

/** The runtime config, relative to the asset root. */
public static final String RUNTIME_CONFIG_PATH = "assets/config/runtime-config.json";

private static final String CLASSPATH_ROOT = ResourceLoader.CLASSPATH_URL_PREFIX + "/browser/";

/**
* The asset root: {@code webUiDir} as a {@code file:} URL when set, else the bundled classpath
* copy. Always ends in a slash.
*/
public static String assetRoot(@Nullable String webUiDir) {
if (webUiDir == null || webUiDir.isEmpty()) {
return CLASSPATH_ROOT;
}
String location = webUiDir.replace("\\", "/");
if (!location.startsWith("file:")) {
location = "file:" + location;
}
return location.endsWith("/") ? location : location + "/";
}

/** The location of a single asset, given relative to the asset root. */
public static String assetLocation(@Nullable String webUiDir, String relativePath) {
return assetRoot(webUiDir) + relativePath;
}

private DevUiAssets() {}
}
Loading