Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .changeset/new-apes-make.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

No release note is needed because this dependency maintenance does not change operator- or user-visible behavior.
2 changes: 1 addition & 1 deletion server/.mvn/wrapper/maven-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
33 changes: 18 additions & 15 deletions server/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,23 @@
<skipCoverage>true</skipCoverage>
<postgres.port>5432</postgres.port>
<resilience4j.version>2.4.0</resilience4j.version>
<okhttp.version>5.3.2</okhttp.version>
<okhttp.version>5.5.0</okhttp.version>
<docker-java.version>3.7.1</docker-java.version>
<archunit.version>1.4.2</archunit.version>
<spring-modulith.version>2.0.0</spring-modulith.version>
<liquibase-hibernate7.version>5.0.3</liquibase-hibernate7.version>
<springdoc.version>3.0.3</springdoc.version>
<archunit.version>1.5.0</archunit.version>
<spring-modulith.version>2.1.1</spring-modulith.version>
<liquibase.version>5.0.4</liquibase.version>
<liquibase-hibernate7.version>5.0.4</liquibase-hibernate7.version>
<springdoc.version>3.1.0</springdoc.version>
<springdoc-maven-plugin.version>1.5</springdoc-maven-plugin.version>
<sentry.version>8.41.0</sentry.version>
<slack-bolt.version>1.48.1</slack-bolt.version>
<jnats.version>2.25.2</jnats.version>
<jgit.version>7.6.0.202603022253-r</jgit.version>
<java-jwt.version>4.5.2</java-jwt.version>
<postgresql.version>42.7.12</postgresql.version>
<sentry.version>8.53.0</sentry.version>
<slack-bolt.version>1.50.0</slack-bolt.version>
<jnats.version>2.26.2</jnats.version>
<jgit.version>7.7.1.202607240634-r</jgit.version>
<java-jwt.version>4.6.0</java-jwt.version>
<therapi.version>0.15.0</therapi.version>
<error-prone.version>2.50.0</error-prone.version>
<nullaway.version>0.14.0</nullaway.version>
<shedlock.version>5.16.0</shedlock.version>
<shedlock.version>7.9.0</shedlock.version>
<bucket4j.version>8.19.0</bucket4j.version>
<maven.build.cache.skipCache>true</maven.build.cache.skipCache>
<maven.build.cache.skipSave>true</maven.build.cache.skipSave>
Expand Down Expand Up @@ -190,8 +190,6 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Modulith 2.0: declares module boundaries via @ApplicationModule,
verified by ModulithVerificationTest in the architecture surefire group. -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
Expand Down Expand Up @@ -375,6 +373,11 @@
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>${archunit.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp-bom</artifactId>
Expand Down Expand Up @@ -454,7 +457,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.14</version>
<version>0.8.15</version>
<executions>
<execution>
<id>default-prepare-agent</id>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package de.tum.cit.aet.hephaestus.agent.catalog;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
Expand All @@ -21,10 +21,10 @@ public record CreateLlmModelRequestDTO(
@NonNull @NotBlank @Size(max = 256) @Schema(description = "Upstream provider model id")
String upstreamModelId,

@Nullable @PositiveOrZero @Schema(description = "Context window in tokens")
@Nullable @Min(1) @Schema(description = "Context window in tokens")
Integer contextWindow,

@Nullable @PositiveOrZero @Schema(description = "Maximum output tokens")
@Nullable @Min(1) @Schema(description = "Maximum output tokens")
Integer maxOutputTokens,

@Nullable @Schema(description = "Whether the model supports a reasoning mode")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package de.tum.cit.aet.hephaestus.agent.catalog;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import org.jspecify.annotations.NonNull;
Expand All @@ -22,10 +22,10 @@ public record CreateWorkspaceLlmModelRequestDTO(
@NonNull @NotBlank @Size(max = 256) @Schema(description = "Upstream provider model id")
String upstreamModelId,

@Nullable @PositiveOrZero @Schema(description = "Context window in tokens")
@Nullable @Min(1) @Schema(description = "Context window in tokens")
Integer contextWindow,

@Nullable @PositiveOrZero @Schema(description = "Maximum output tokens")
@Nullable @Min(1) @Schema(description = "Maximum output tokens")
Integer maxOutputTokens,

@Nullable @Schema(description = "Whether the model supports a reasoning mode")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.tum.cit.aet.hephaestus.agent.catalog;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import org.jspecify.annotations.Nullable;

Expand All @@ -14,10 +14,10 @@ public record UpdateLlmModelRequestDTO(
@Nullable @Size(max = 128) @Schema(description = "Human-readable name")
String displayName,

@Nullable @PositiveOrZero @Schema(description = "Context window in tokens")
@Nullable @Min(1) @Schema(description = "Context window in tokens")
Integer contextWindow,

@Nullable @PositiveOrZero @Schema(description = "Maximum output tokens")
@Nullable @Min(1) @Schema(description = "Maximum output tokens")
Integer maxOutputTokens,

@Nullable @Schema(description = "Whether the model supports a reasoning mode")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.tum.cit.aet.hephaestus.agent.catalog;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import org.jspecify.annotations.Nullable;
Expand All @@ -18,10 +18,10 @@ public record UpdateWorkspaceLlmModelRequestDTO(
@Nullable @Size(max = 128) @Schema(description = "Human-readable name")
String displayName,

@Nullable @PositiveOrZero @Schema(description = "Context window in tokens")
@Nullable @Min(1) @Schema(description = "Context window in tokens")
Integer contextWindow,

@Nullable @PositiveOrZero @Schema(description = "Maximum output tokens")
@Nullable @Min(1) @Schema(description = "Maximum output tokens")
Integer maxOutputTokens,

@Nullable @Schema(description = "Whether the model supports a reasoning mode")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
import org.slf4j.LoggerFactory;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.boot.web.server.context.WebServerGracefulShutdownLifecycle;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.SmartLifecycle;

/**
* SIGTERM-driven graceful shutdown for the worker. Runs at
* {@link WebServerGracefulShutdownLifecycle#SMART_LIFECYCLE_PHASE} {@code - 1024} (after HTTP
* Graceful shutdown for the worker. Runs at
* {@link WebServerApplicationContext#GRACEFUL_SHUTDOWN_PHASE} {@code - 1024} (after HTTP
* server stop, before executor teardown). Liveness stays {@code CORRECT} — kubelet must
* not kill the pod early; only readiness flips to {@code REFUSING_TRAFFIC}.
*
Expand All @@ -32,7 +32,7 @@ public class WorkerDrainCoordinator implements SmartLifecycle {

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

static final int PHASE = WebServerGracefulShutdownLifecycle.SMART_LIFECYCLE_PHASE - 1024;
static final int PHASE = WebServerApplicationContext.GRACEFUL_SHUTDOWN_PHASE - 1024;

private final WorkerControlClient client;
private final WorkerCapacityState state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,13 @@
import de.tum.cit.aet.hephaestus.core.security.StaleAuthCookieFilter;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;

/**
* Resolves the resource-server bearer token from the SPA's {@code __Host-HEPHAESTUS_AT} cookie
* (name from {@link AuthProperties#cookieName()}), falling back to the standard
* {@code Authorization: Bearer} header (ADR 0017).
*
* <p>The SPA authenticates by an HttpOnly access-token cookie set on the OAuth success path
* ({@code HephaestusAuthSuccessHandler}) and sent automatically with {@code credentials:"include"};
* it never sends an {@code Authorization} header. The framework default
* ({@link DefaultBearerTokenResolver}) only reads the header, so without this resolver every
* browser request is 401. Worker / API / bearer-token integration clients still authenticate via
* the header fallback.
*
* <p>Order is cookie-first: a browser request carries only the cookie, while a non-browser client
* carries only the header, so they never collide in practice. CSRF (double-submit) covers the
* cookie-authenticated, state-changing browser path — see {@code SecurityConfig#requiresCsrf}.
* Resolves the configured access-token cookie before falling back to a standard bearer header.
* Rejected stale cookies are ignored. See ADR 0017 for the cookie-first security policy.
*/
public class CookieBearerTokenResolver implements BearerTokenResolver {

Expand All @@ -33,10 +22,8 @@ public CookieBearerTokenResolver(AuthProperties properties) {
}

@Override
public String resolve(HttpServletRequest request) {
// A stale cookie already rejected by StaleAuthCookieFilter: ignore it so this request stays
// anonymous (a permitAll endpoint serves instead of 401ing on the dead token). The header
// fallback below still applies for worker/API/bearer clients.
public @Nullable String resolve(HttpServletRequest request) {
// A rejected stale cookie must not authenticate this request.
if (Boolean.TRUE.equals(request.getAttribute(StaleAuthCookieFilter.COOKIE_INVALID_ATTRIBUTE))) {
return headerResolver.resolve(request);
}
Expand All @@ -51,7 +38,6 @@ public String resolve(HttpServletRequest request) {
}
}
}
// No cookie token → fall back to the standard Authorization: Bearer header (worker/API/tests).
return headerResolver.resolve(request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
Expand All @@ -25,8 +26,7 @@
* rather than the HTTP session. Required because every existing Hephaestus filter chain
* is {@code STATELESS} — the Spring default
* {@code HttpSessionOAuth2AuthorizationRequestRepository} would silently create a session
* that the callback request cannot see on the next pod (the failure mode Wave-2 PE flagged
* as the most likely "doesn't work on multi-pod" bug).
* that the callback request cannot see on another pod.
*
* <h2>Cookie</h2>
* Name {@value #COOKIE_NAME}; {@code HttpOnly}, {@code Secure}, {@code SameSite=Lax}
Expand All @@ -45,10 +45,7 @@
* {@code "oauth2-state"} — distinct from any tenant or system AAD so a confused-deputy
* substitution between domains is rejected.
*
* <h2>Multi-flight</h2>
* v1 supports a single in-flight authorization request per browser. Opening a second
* login tab while the first is mid-flight will invalidate the first. Acceptable for
* launch UX; LRU multi-flight cookie can be added if real users hit issues.
* <p>One authorization request is stored per browser; starting another replaces it.
*/
public class CookieOAuth2AuthorizationRequestRepository
implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
Expand Down Expand Up @@ -202,9 +199,9 @@ private static byte[] serialize(OAuth2AuthorizationRequest req) {
StoredAuthorizationRequest stored = new StoredAuthorizationRequest(
req.getAuthorizationUri(),
req.getClientId(),
req.getRedirectUri(),
Objects.requireNonNull(req.getRedirectUri(), "OAuth redirect URI must be configured"),
req.getScopes(),
req.getState(),
Objects.requireNonNull(req.getState(), "OAuth state must be generated before serialization"),
req.getAuthorizationRequestUri(),
stringifyValues(req.getAttributes()),
stringifyValues(req.getAdditionalParameters()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -42,11 +43,13 @@ public class GitHubEmailOAuth2UserService implements OAuth2UserService<OAuth2Use
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User user = delegate.loadUser(userRequest);
String nameAttrKey = userRequest
.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName(); // "id"
String nameAttrKey = Objects.requireNonNull(
userRequest
.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName(),
"GitHub user-name attribute must be configured");

Map<String, Object> attrs = new HashMap<>(user.getAttributes());
fetchPrimaryVerifiedEmail(userRequest).ifPresent(email -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ParameterizedTypeReference;
Expand Down Expand Up @@ -61,11 +62,13 @@ public OutlineAuthInfoUserService() {

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
String authInfoUri = userRequest
.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUri();
String authInfoUri = Objects.requireNonNull(
userRequest
.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUri(),
"Outline auth.info URI must be configured");
Map<String, Object> body;
try {
body = webClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.server.context.WebServerGracefulShutdownLifecycle;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.SmartLifecycle;
import org.springframework.web.socket.CloseStatus;
Expand Down Expand Up @@ -118,8 +118,8 @@ public void stop() {
@Override
public int getPhase() {
// SmartLifecycle stops in DESCENDING phase order — higher phase stops first. A phase
// greater than WebServerGracefulShutdownLifecycle.SMART_LIFECYCLE_PHASE makes the WS
// greater than WebServerApplicationContext.GRACEFUL_SHUTDOWN_PHASE makes the WS
// registry drain BEFORE the embedded server stops accepting traffic.
return WebServerGracefulShutdownLifecycle.SMART_LIFECYCLE_PHASE + 1;
return WebServerApplicationContext.GRACEFUL_SHUTDOWN_PHASE + 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,24 @@
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.server.context.WebServerGracefulShutdownLifecycle;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.context.SmartLifecycle;

/**
* Drains in-flight webhook publishes during graceful shutdown, AFTER the HTTP server has stopped
* accepting new requests but BEFORE the NATS connection closes.
* Drains in-flight webhook publishes after the HTTP server stops accepting requests and before the
* NATS connection closes.
*
* <p>{@link SmartLifecycle} semantics: HIGHER phase stops FIRST. Sitting at a phase lower than
* {@link WebServerGracefulShutdownLifecycle#SMART_LIFECYCLE_PHASE} means we stop after Spring's
* <p>{@link SmartLifecycle} stops higher phases first. Sitting at a phase lower than
* {@link WebServerApplicationContext#GRACEFUL_SHUTDOWN_PHASE} means we stop after Spring's
* web server — no new requests can arrive while we drain.
*
* <p>The drain budget comes from {@code hephaestus.webhook.shutdown.drain-timeout}. Docker's
* {@code stop_grace_period} (40s in compose) must cover Spring's HTTP drain plus this budget.
* <p>The drain budget comes from {@code hephaestus.webhook.shutdown.drain-timeout}.
*/
public class WebhookGracefulShutdown implements SmartLifecycle {

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

private static final int PHASE = WebServerGracefulShutdownLifecycle.SMART_LIFECYCLE_PHASE - 1024;
private static final int PHASE = WebServerApplicationContext.GRACEFUL_SHUTDOWN_PHASE - 1024;

private final JetStreamPublisher publisher;
private final Duration drainTimeout;
Expand Down
Loading
Loading