diff --git a/README.ko.md b/README.ko.md
index abf8079..9ba324c 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -29,7 +29,7 @@
전체 인덱스: [github.com/devslab-kr/devslab-examples](https://github.com/devslab-kr/devslab-examples).
-## 모듈 매트릭스 (v3.0.0)
+## 모듈 매트릭스
쓰는 HTTP 클라이언트에 맞는 모듈만 고르세요. `ssrf-guard-core`는 transitive로 따라옴.
@@ -38,9 +38,11 @@
| **`ssrf-guard`** | 메타 — RestClient + HttpClient5 (v2.0.0 호환) | ✅ |
| `ssrf-guard-restclient` | Spring 6.1+ `RestClient` | ✅ |
| `ssrf-guard-resttemplate` | Spring `RestTemplate` | ✅ |
-| `ssrf-guard-webclient` | Spring WebFlux `WebClient` | ✅ |
+| `ssrf-guard-webclient` | Spring WebFlux `WebClient` — URL 단계 필터 + reactor-netty DNS 단계 IP 필터 (v3.1+) | ✅ |
| `ssrf-guard-feign` | Spring Cloud OpenFeign | ✅ |
-| **`ssrf-guard-springai`** ⭐ | Spring AI `ToolCallback` URL 검증 — LLM 에이전트 SSRF 차단 | ✅ |
+| `ssrf-guard-llm` 🧩 | 프레임워크-중립 JSON 툴 입력 검증 (v3.1+) — LLM 어댑터들이 재사용 | — |
+| **`ssrf-guard-springai`** ⭐ | Spring AI `ToolCallback` URL 검증 — `-llm` 위의 thin adapter | ✅ |
+| **`ssrf-guard-langchain4j`** ⭐ | LangChain4j `ToolExecutor` URL 검증 — Java LLM 양대 프레임워크 다른 한쪽 (v3.1+) | ✅ |
| `ssrf-guard-httpclient5` | Apache HttpClient 5 직접 | — |
| `ssrf-guard-jdkhttp` | `java.net.http.HttpClient` | — |
| `ssrf-guard-okhttp` | OkHttp | — |
diff --git a/README.md b/README.md
index d706149..0c99e90 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ Standalone Spring Boot projects that exercise every module documented below —
Full index at [github.com/devslab-kr/devslab-examples](https://github.com/devslab-kr/devslab-examples).
-## Module matrix (v3.0.0)
+## Module matrix
Pick the module matching your HTTP client. The core (`ssrf-guard-core`) follows transitively.
@@ -38,9 +38,11 @@ Pick the module matching your HTTP client. The core (`ssrf-guard-core`) follows
| **`ssrf-guard`** | Meta artifact — RestClient + HttpClient5 (v2.0.0 back-compat) | ✅ |
| `ssrf-guard-restclient` | Spring 6.1+ `RestClient` | ✅ |
| `ssrf-guard-resttemplate` | Spring `RestTemplate` | ✅ |
-| `ssrf-guard-webclient` | Spring WebFlux `WebClient` | ✅ |
+| `ssrf-guard-webclient` | Spring WebFlux `WebClient` — URL-time filter + reactor-netty DNS-time IP filter (v3.1+) | ✅ |
| `ssrf-guard-feign` | Spring Cloud OpenFeign | ✅ |
-| **`ssrf-guard-springai`** ⭐ | Spring AI `ToolCallback` URL validation — closes the LLM-agent SSRF surface | ✅ |
+| `ssrf-guard-llm` 🧩 | Framework-agnostic JSON tool-input validator (v3.1+) — reused by the LLM adapters | — |
+| **`ssrf-guard-springai`** ⭐ | Spring AI `ToolCallback` URL validation — thin adapter over `-llm` | ✅ |
+| **`ssrf-guard-langchain4j`** ⭐ | LangChain4j `ToolExecutor` URL validation — same defense for the other Java LLM framework (v3.1+) | ✅ |
| `ssrf-guard-httpclient5` | Apache HttpClient 5 directly | — |
| `ssrf-guard-jdkhttp` | `java.net.http.HttpClient` | — |
| `ssrf-guard-okhttp` | OkHttp | — |
diff --git a/docs/getting-started/installation.ko.md b/docs/getting-started/installation.ko.md
index 2c7d473..7c98050 100644
--- a/docs/getting-started/installation.ko.md
+++ b/docs/getting-started/installation.ko.md
@@ -69,7 +69,38 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
```
- 모든 `ToolCallback` 빈을 URL 인자 검증으로 감쌈. **LLM 에이전트가 URL을 받아 fetch하는 시나리오의 결정적인 새 SSRF 표면.**
+ 모든 `ToolCallback` 빈을 URL 인자 검증으로 감쌈. **LLM 에이전트가 URL을 받아 fetch하는 시나리오의 결정적인 새 SSRF 표면.** v3.1+는 내부적으로 `ssrf-guard-llm` (프레임워크-중립 코어)에 위임 — public API 동일.
+
+=== "LangChain4j 툴 실행"
+
+ ```xml
+
+ kr.devslab
+ ssrf-guard-langchain4j
+ 3.1.0
+
+ ```
+
+ Spring AI 탭과 동일한 위협 모델, 다른 프레임워크. 모든 `ToolExecutor` 빈을 wrap. LangChain4j 툴이 Spring 빈일 때 `BeanPostProcessor`가 자동 처리; 프로그래매틱/비-Spring 사용에는 `SsrfGuardedToolExecutors.wrap(...)` 헬퍼가 `AiServices.builder(...).tools(Map)` 형태 지원.
+
+=== "커스텀 툴 dispatcher (프레임워크 없음)"
+
+ ```xml
+
+ kr.devslab
+ ssrf-guard-llm
+ 3.1.0
+
+ ```
+
+ 프레임워크-중립 코어. 자체 툴 라우터 (MCP 서버, 내부 RPC dispatcher, 커스텀 에이전트 루프) 만든 경우 직접 사용:
+
+ ```java
+ JsonToolInputGuard guard = new JsonToolInputGuard(urlPolicy);
+ String violation = guard.checkOrFormatError(rawJsonInput);
+ if (violation != null) return violation; // LLM에게 줄 구조화된 JSON
+ // ... 진짜 툴 실행
+ ```
=== "JDK HttpClient (Spring 없음)"
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
index de63dfc..2df4953 100644
--- a/docs/getting-started/installation.md
+++ b/docs/getting-started/installation.md
@@ -69,7 +69,38 @@ ssrf-guard v3.0.0 is split along HTTP-client boundaries — pick the module(s) m
```
- Wraps every `ToolCallback` bean with URL-argument validation. The defining new SSRF surface — LLM agents that take a URL as a parameter and fetch it.
+ Wraps every `ToolCallback` bean with URL-argument validation. The defining new SSRF surface — LLM agents that take a URL as a parameter and fetch it. v3.1+ delegates to `ssrf-guard-llm` (the framework-agnostic core); same public API.
+
+=== "LangChain4j tool execution"
+
+ ```xml
+
+ kr.devslab
+ ssrf-guard-langchain4j
+ 3.1.0
+
+ ```
+
+ Same threat model as the Spring AI tab — different framework. Wraps every `ToolExecutor` bean. Useful when your LangChain4j tools are Spring beans (auto-wrapped by `BeanPostProcessor`); for programmatic / non-Spring use, the `SsrfGuardedToolExecutors.wrap(...)` helpers cover the `AiServices.builder(...).tools(Map)` shape.
+
+=== "Custom tool dispatcher (no framework)"
+
+ ```xml
+
+ kr.devslab
+ ssrf-guard-llm
+ 3.1.0
+
+ ```
+
+ The framework-agnostic core. Use directly when you've built your own tool router (MCP server, internal RPC dispatcher, custom agent loop):
+
+ ```java
+ JsonToolInputGuard guard = new JsonToolInputGuard(urlPolicy);
+ String violation = guard.checkOrFormatError(rawJsonInput);
+ if (violation != null) return violation; // structured JSON for the LLM
+ // ... run the real tool
+ ```
=== "Plain JDK HttpClient (no Spring)"
diff --git a/docs/guides/configuration.ko.md b/docs/guides/configuration.ko.md
index 7db4fe5..45e7d5c 100644
--- a/docs/guides/configuration.ko.md
+++ b/docs/guides/configuration.ko.md
@@ -92,3 +92,24 @@ ssrf:
```
이 정도 화이트리스트면 partner-integration이 많은 거의 모든 서비스를 SSRF에서 보호.
+
+## LLM 어댑터 properties (v3.1+)
+
+LLM 툴 빈 자동 wrap을 제어하는 두 토글. 둘 다 기본 `true` — 자체 코드에서 (`SsrfGuardedToolCallbacks.wrap(...)` / `SsrfGuardedToolExecutors.wrap(...)`) 선별적으로 wrap하고 싶으면 `false`로 끔.
+
+| 키 | 기본값 | 효과 |
+|---|---|---|
+| `ssrf.guard.springai.wrap-tool-callbacks` | `true` | Spring AI 모든 `ToolCallback` 빈 자동 wrap. `ssrf-guard-springai` classpath 필요. |
+| `ssrf.guard.langchain4j.wrap-tool-executors` | `true` | LangChain4j 모든 `ToolExecutor` 빈 자동 wrap. `ssrf-guard-langchain4j` classpath 필요. |
+
+```yaml
+ssrf:
+ guard:
+ enabled: true
+ exact-hosts:
+ - api.partner.com
+ springai:
+ wrap-tool-callbacks: true # 기본
+ langchain4j:
+ wrap-tool-executors: true # 기본
+```
diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md
index c218027..5dda7f2 100644
--- a/docs/guides/configuration.md
+++ b/docs/guides/configuration.md
@@ -92,3 +92,24 @@ ssrf:
```
A whitelist this size will keep almost any partner-integration-heavy service safe from SSRF.
+
+## LLM-adapter properties (v3.1+)
+
+Two additional toggles control automatic wrapping of LLM tool beans. Both default to `true` — set to `false` to wrap selectively from your own code (via `SsrfGuardedToolCallbacks.wrap(...)` or `SsrfGuardedToolExecutors.wrap(...)`) without the `BeanPostProcessor` running.
+
+| Key | Default | Effect |
+|---|---|---|
+| `ssrf.guard.springai.wrap-tool-callbacks` | `true` | Wraps every Spring AI `ToolCallback` bean. Requires `ssrf-guard-springai` on the classpath. |
+| `ssrf.guard.langchain4j.wrap-tool-executors` | `true` | Wraps every LangChain4j `ToolExecutor` bean. Requires `ssrf-guard-langchain4j` on the classpath. |
+
+```yaml
+ssrf:
+ guard:
+ enabled: true
+ exact-hosts:
+ - api.partner.com
+ springai:
+ wrap-tool-callbacks: true # default
+ langchain4j:
+ wrap-tool-executors: true # default
+```
diff --git a/docs/guides/security-model.ko.md b/docs/guides/security-model.ko.md
index febd4bc..1d3207c 100644
--- a/docs/guides/security-model.ko.md
+++ b/docs/guides/security-model.ko.md
@@ -51,7 +51,8 @@ Java 내장 `InetAddress.isSiteLocalAddress()`는 CGNAT, benchmark range, IPv6
솔직한 한계 목록. 경계 인식이 threat model의 일부.
- **HTTP 클라이언트마다 다른 URL 파싱을 검증하지 않음.** JDK `URI` 생성자, Spring `UriComponentsBuilder`, Apache HttpClient의 request line — `://user:pass@a.com\@b.com/` 같은 문자열이 어떤 호스트인지 항상 동의하지 않음. 신뢰할 수 없는 입력에서 URL을 받으면 `RestClient`에 넘기기 전 정규화해야. OWASP cheat sheet의 [URL parser confusion section](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html#network-layer) 권장.
-- **`RestClient` 외 HTTP 클라이언트는 보호 안 함.** `HttpURLConnection`, `OkHttpClient`, 직접 Apache `HttpClient` (래핑 안 된), `WebClient` — 이 중 어떤 것도 SSRF 정책을 거치지 않음. 자동 구성은 Spring Boot 자동 `RestClient.Builder`만 customize.
+- **wrap되지 않은 HTTP 클라이언트는 보호 안 함.** 자동 구성된 `RestClient.Builder` / `RestTemplateBuilder` / `WebClient.Builder` / `OkHttpClient.Builder` 등의 경로를 거치지 않고 직접 만든 `HttpURLConnection` 같은 코드는 SSRF 정책 우회. v3.1+는 주요 Java HTTP 스택 (RestClient · RestTemplate · WebClient · Feign · OkHttp · JDK `HttpClient` · Apache HttpClient 5)과 LLM 툴 dispatch (Spring AI · LangChain4j)를 모두 커버 — 사용 중인 각각에 맞는 모듈을 골라 추가.
+- **WebClient는 v3.1부터 URL 단계 *+* DNS 단계 방어 모두.** v3.0.x WebClient 모듈은 URL 단계 필터만 실행 — 화이트리스트를 통과한 호스트가 사설 IP로 resolve되어도 reactor-netty가 그대로 연결. v3.1의 `SsrfGuardReactorAddressResolverGroup`이 reactor-netty의 address resolver에 후킹해서 다른 모듈과 동일한 사설/loopback 범위로 필터링. 비-Netty WebFlux 백엔드 (Jetty Reactive, Helidon)도 URL 단계 방어는 받음; connector 교체는 reactor-netty classpath 의존.
- **JVM 캐시가 작용할 때 DNS rebinding을 막지 않음.** Java가 DNS 해석을 캐시; JVM이 영구 캐싱 (Java 8u192 이전 보안 정책 default)이면 hostname의 record 변경 후에도 캐시된 IP를 계속 hit. 모던 JVM은 default가 30초이긴 함 — `networkaddress.cache.ttl` 합리적 값 유지.
- **`exact-hosts`에 사설 IP literal을 직접 넣는 걸 막지 않음.** `10.0.0.5`를 화이트리스트하면 인터셉터가 호스트를 통과시키고, DNS resolver가 그 IP로 단락. (단, `block-private-networks=false`로 안 두면) 사설 IP 필터가 여전히 적용되어 요청은 거부 — 하지만 레이어링이 "인터셉터 통과, resolver 거부"이지 "인터셉터 즉시 거부" 아님.
- **응답 본문 검증 안 함.** 화이트리스트 호스트라도 downstream 이슈를 트리거하는 콘텐츠 반환 가능. SSRF 방어는 신뢰하는 호스트로 소켓이 연결될 때 끝남; 그 호스트가 반환하는 건 애플리케이션 로직 책임.
diff --git a/docs/guides/security-model.md b/docs/guides/security-model.md
index 87b73b7..8ce6521 100644
--- a/docs/guides/security-model.md
+++ b/docs/guides/security-model.md
@@ -51,7 +51,8 @@ Java's built-in `InetAddress.isSiteLocalAddress()` misses CGNAT, the benchmark r
Honest list. Knowing the boundary is part of the threat model.
- **It does not validate URL parsing the way every HTTP client interprets it.** The JDK `URI` constructor, Spring's `UriComponentsBuilder`, Apache HttpClient's request line — they don't always agree on what host a `://user:pass@a.com\@b.com/` string represents. If you accept URLs from untrusted input, normalize them before handing them to `RestClient`. The OWASP cheat sheet has a [URL parser confusion section](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html#network-layer) worth reading.
-- **It does not protect non-`RestClient` HTTP clients.** Code using `HttpURLConnection`, `OkHttpClient`, raw Apache `HttpClient` (not the wrapped one), `WebClient` — none of those go through the SSRF policy. The auto-configuration only customizes Spring Boot's auto-configured `RestClient.Builder`.
+- **It does not protect non-wrapped HTTP clients.** Code using raw `HttpURLConnection`, or any client built outside the auto-configured `RestClient.Builder` / `RestTemplateBuilder` / `WebClient.Builder` / `OkHttpClient.Builder` etc. — that custom code bypasses the SSRF policy. v3.1+ covers every major Java HTTP stack (RestClient · RestTemplate · WebClient · Feign · OkHttp · JDK `HttpClient` · Apache HttpClient 5) plus LLM tool dispatch (Spring AI · LangChain4j) — pick the matching module for each one you use.
+- **WebClient gets URL-time *and* DNS-time defense from v3.1.** v3.0.x's WebClient module only ran the URL-time filter — a host that passed the whitelist could still resolve to a private IP, and reactor-netty would connect to it. v3.1's `SsrfGuardReactorAddressResolverGroup` hooks into reactor-netty's address resolver and filters resolved IPs against the same private/loopback ranges. Non-Netty WebFlux backends (Jetty Reactive, Helidon) still get URL-time defense; the connector swap is gated on reactor-netty being on the classpath.
- **It does not protect against DNS rebinding when the JVM cache is in play.** Java caches DNS resolutions; if your JVM caches forever (the default for security policies that pre-date Java 8u192) and a hostname's records change after the cache was populated, the cached IP is what you keep hitting. Set `networkaddress.cache.ttl` to something sane (default in modern JVMs is already 30 seconds).
- **It does not stop you from putting a private-IP literal directly in `exact-hosts`.** If you whitelist `10.0.0.5`, the interceptor accepts that host, and the DNS resolver short-circuits to that IP. The private-IP filter still applies (unless you set `block-private-networks=false`), so the request is rejected — but the layering is "interceptor accepts, resolver rejects," not "interceptor rejects up front."
- **It does not validate response bodies.** A whitelisted host can still return content that triggers downstream issues. SSRF defense ends when the socket connects to a host you trust; what that host returns is application logic.
diff --git a/docs/index.ko.md b/docs/index.ko.md
index 09bfde4..dda186b 100644
--- a/docs/index.ko.md
+++ b/docs/index.ko.md
@@ -17,9 +17,11 @@ ssrf-guard는 작은 core(정책 + IP 분류) + HTTP 클라이언트별 모듈
| **`ssrf-guard`** | 메타 아티팩트 — RestClient + HttpClient5 (v2.0.0 호환) | Yes |
| `ssrf-guard-restclient` | Spring 6.1+ `RestClient` | Yes |
| `ssrf-guard-resttemplate` | Spring `RestTemplate` (엔터프라이즈/레거시) | Yes |
-| `ssrf-guard-webclient` | Spring WebFlux `WebClient` | Yes (WebFlux) |
+| `ssrf-guard-webclient` | Spring WebFlux `WebClient` — URL 단계 필터 **+** reactor-netty DNS 단계 IP 필터 (v3.1+) | Yes (WebFlux) |
| `ssrf-guard-feign` | Spring Cloud OpenFeign | Yes (Cloud) |
-| **`ssrf-guard-springai`** ⭐ | Spring AI `ToolCallback` — LLM 에이전트 SSRF 차단 | Yes (AI) |
+| `ssrf-guard-llm` 🧩 | 프레임워크-중립 JSON 툴 입력 검증 (v3.1+). springai / langchain4j 어댑터가 사용; 커스텀 dispatcher에서도 직접 사용 가능. | No |
+| **`ssrf-guard-springai`** ⭐ | Spring AI `ToolCallback` — LLM 에이전트 SSRF 차단 (`-llm` 위의 thin adapter) | Yes (AI) |
+| **`ssrf-guard-langchain4j`** ⭐ | LangChain4j `ToolExecutor` — Java LLM 양대 프레임워크 다른 한쪽 (v3.1+, `-llm` 위의 thin adapter) | Yes |
| `ssrf-guard-httpclient5` | Apache HttpClient 5 직접 사용 | No |
| `ssrf-guard-jdkhttp` | `java.net.http.HttpClient` (JDK 11+) | No |
| `ssrf-guard-okhttp` | OkHttp | No |
diff --git a/docs/index.md b/docs/index.md
index c1cc565..0898914 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -17,9 +17,11 @@ Pick the module(s) matching your HTTP client. The core is pulled in transitively
| **`ssrf-guard`** | Meta artifact — RestClient + HttpClient5 (v2.0.0 back-compat) | Yes |
| `ssrf-guard-restclient` | Spring 6.1+ `RestClient` | Yes |
| `ssrf-guard-resttemplate` | Spring `RestTemplate` (enterprise / legacy) | Yes |
-| `ssrf-guard-webclient` | Spring WebFlux `WebClient` | Yes (WebFlux) |
+| `ssrf-guard-webclient` | Spring WebFlux `WebClient` — URL-time filter **and** reactor-netty DNS-time IP filter (v3.1+) | Yes (WebFlux) |
| `ssrf-guard-feign` | Spring Cloud OpenFeign | Yes (Cloud) |
-| **`ssrf-guard-springai`** ⭐ | Spring AI `ToolCallback` — closes the LLM-agent SSRF surface | Yes (AI) |
+| `ssrf-guard-llm` 🧩 | Framework-agnostic JSON tool-input validator (v3.1+). Used by the springai / langchain4j adapters; usable directly from a custom dispatcher. | No |
+| **`ssrf-guard-springai`** ⭐ | Spring AI `ToolCallback` — closes the LLM-agent SSRF surface (thin adapter over `-llm`) | Yes (AI) |
+| **`ssrf-guard-langchain4j`** ⭐ | LangChain4j `ToolExecutor` — same defense for the other Java LLM framework (v3.1+, thin adapter over `-llm`) | Yes |
| `ssrf-guard-httpclient5` | Apache HttpClient 5 directly | No |
| `ssrf-guard-jdkhttp` | `java.net.http.HttpClient` (JDK 11+) | No |
| `ssrf-guard-okhttp` | OkHttp | No |
diff --git a/ssrf-guard-langchain4j/src/test/java/kr/devslab/ssrfguard/langchain4j/SsrfGuardLangchain4jAutoConfigurationTest.java b/ssrf-guard-langchain4j/src/test/java/kr/devslab/ssrfguard/langchain4j/SsrfGuardLangchain4jAutoConfigurationTest.java
new file mode 100644
index 0000000..2fa43e3
--- /dev/null
+++ b/ssrf-guard-langchain4j/src/test/java/kr/devslab/ssrfguard/langchain4j/SsrfGuardLangchain4jAutoConfigurationTest.java
@@ -0,0 +1,114 @@
+package kr.devslab.ssrfguard.langchain4j;
+
+import dev.langchain4j.agent.tool.ToolExecutionRequest;
+import dev.langchain4j.service.tool.ToolExecutor;
+import kr.devslab.ssrfguard.core.UrlPolicy;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Nested;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.test.context.TestPropertySource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Boots a Spring context with the langchain4j autoconfig active and asserts:
+ *
+ *
+ * - The autoconfig publishes the expected beans ({@code UrlPolicy},
+ * {@code SsrfGuardMetrics}, and the BeanPostProcessor).
+ * - The BeanPostProcessor actually wraps consumer {@code @Bean ToolExecutor}
+ * declarations — every executor in the context becomes a
+ * {@code SsrfGuardedToolExecutor} without consumer code.
+ * - A request through the wrapped executor blocks attacker URLs
+ * end-to-end. This is the "secure by default" claim we make in
+ * the README — the test holds us to it.
+ * - {@code wrap-tool-executors=false} disables the BeanPostProcessor
+ * (consumers who want to wrap manually keep the off switch).
+ *
+ */
+class SsrfGuardLangchain4jAutoConfigurationTest {
+
+ @SpringBootApplication
+ static class TestApp {
+
+ /** A consumer-side tool that records every invocation. */
+ @Bean
+ ToolExecutor fakeFetchUrlTool() {
+ return (request, memoryId) -> "PRETEND-FETCHED " + request.arguments();
+ }
+ }
+
+ @Nested
+ @SpringBootTest(classes = TestApp.class)
+ @TestPropertySource(properties = {
+ "ssrf.guard.enabled=true",
+ "ssrf.guard.exact-hosts=api.example.com"
+ // wrap-tool-executors defaults to true
+ })
+ class WhenAutoWrapEnabled {
+
+ @Autowired ApplicationContext ctx;
+ @Autowired UrlPolicy policy;
+ @Autowired ToolExecutor injectedExecutor;
+
+ @Test
+ void context_publishes_url_policy_and_postprocessor() {
+ assertThat(policy).isNotNull();
+ // The consumer's ToolExecutor came back wrapped.
+ assertThat(injectedExecutor)
+ .as("autoconfig BeanPostProcessor should wrap @Bean ToolExecutor")
+ .isInstanceOf(SsrfGuardedToolExecutor.class);
+ }
+
+ @Test
+ void wrapped_executor_blocks_aws_metadata_url() {
+ String result = injectedExecutor.execute(
+ ToolExecutionRequest.builder()
+ .name("fetch_url")
+ .arguments("{\"url\":\"http://169.254.169.254/latest/meta-data/\"}")
+ .build(),
+ null);
+ assertThat(result)
+ .as("LLM-facing output on a blocked URL")
+ .contains("\"error\":\"ssrf_blocked\"")
+ .contains("\"reason\":\"blocked_ip_literal\"");
+ }
+
+ @Test
+ void wrapped_executor_allows_whitelisted_url() {
+ String result = injectedExecutor.execute(
+ ToolExecutionRequest.builder()
+ .name("fetch_url")
+ .arguments("{\"url\":\"https://api.example.com/v1\"}")
+ .build(),
+ null);
+ // The fake delegate echoes "PRETEND-FETCHED ..." on success —
+ // if we see that string the wrap let the call through.
+ assertThat(result).startsWith("PRETEND-FETCHED");
+ }
+ }
+
+ @Nested
+ @SpringBootTest(classes = TestApp.class)
+ @TestPropertySource(properties = {
+ "ssrf.guard.enabled=true",
+ "ssrf.guard.exact-hosts=api.example.com",
+ "ssrf.guard.langchain4j.wrap-tool-executors=false"
+ })
+ class WhenAutoWrapDisabled {
+
+ @Autowired ToolExecutor injectedExecutor;
+
+ @Test
+ void executor_is_NOT_wrapped() {
+ // The off switch should leave the consumer's bean untouched —
+ // they presumably plan to wrap manually via
+ // SsrfGuardedToolExecutors.wrap(...).
+ assertThat(injectedExecutor).isNotInstanceOf(SsrfGuardedToolExecutor.class);
+ }
+ }
+}
diff --git a/ssrf-guard-webclient/src/test/java/kr/devslab/ssrfguard/webclient/SsrfGuardWebClientAutoConfigurationTest.java b/ssrf-guard-webclient/src/test/java/kr/devslab/ssrfguard/webclient/SsrfGuardWebClientAutoConfigurationTest.java
index 304e149..3687549 100644
--- a/ssrf-guard-webclient/src/test/java/kr/devslab/ssrfguard/webclient/SsrfGuardWebClientAutoConfigurationTest.java
+++ b/ssrf-guard-webclient/src/test/java/kr/devslab/ssrfguard/webclient/SsrfGuardWebClientAutoConfigurationTest.java
@@ -4,6 +4,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.test.context.TestPropertySource;
import org.springframework.web.reactive.function.client.WebClient;
@@ -19,6 +20,11 @@ class SsrfGuardWebClientAutoConfigurationTest {
@Autowired ApplicationContext ctx;
@Autowired WebClient.Builder webClientBuilder;
@Autowired SsrfGuardExchangeFilterFunction filter;
+ // v3.1 — new connector + resolver beans wired by
+ // ReactorNettyConnectorConfiguration. Should always resolve when
+ // reactor-netty is on the classpath (which it is via spring-webflux).
+ @Autowired ReactorClientHttpConnector connector;
+ @Autowired SsrfGuardReactorAddressResolverGroup resolverGroup;
@Test
void filter_is_registered() {
@@ -34,4 +40,21 @@ void web_client_customizer_is_registered() {
void builder_is_available() {
assertThat(webClientBuilder.build()).isNotNull();
}
+
+ @Test
+ void reactor_client_http_connector_is_registered() {
+ // v3.1 closes the DNS-time gap. The autoconfig should publish a
+ // ReactorClientHttpConnector backed by our filtering resolver.
+ assertThat(connector).isNotNull();
+ assertThat(ctx.containsBean("ssrfReactorClientHttpConnector")).isTrue();
+ }
+
+ @Test
+ void address_resolver_group_is_registered() {
+ // The resolver group bean itself is also exposed so consumers with
+ // their own ClientHttpConnector (custom pool / proxy / mTLS) can
+ // still attach our resolver by calling httpClient.resolver(group).
+ assertThat(resolverGroup).isNotNull();
+ assertThat(ctx.containsBean("ssrfReactorAddressResolverGroup")).isTrue();
+ }
}