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
3 changes: 2 additions & 1 deletion embabel-agent-docs/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@
<dir>${project.parent.basedir}/embabel-agent-autoconfigure/embabel-agent-platform-autoconfigure/src/main/java</dir>
<dir>${project.parent.basedir}/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/main/java</dir>
<dir>${project.parent.basedir}/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/main/kotlin</dir>
<dir>${project.parent.basedir}/embabel-agent-mcpserver/src/main/kotlin</dir>
<dir>${project.parent.basedir}/embabel-agent-mcp/embabel-agent-mcpserver/src/main/kotlin</dir>
<dir>${project.parent.basedir}/embabel-agent-mcp/embabel-agent-mcp-security/src/main/kotlin</dir>
<dir>${project.parent.basedir}/embabel-agent-rag/embabel-agent-rag-lucene/src/main/kotlin</dir>
<dir>${project.parent.basedir}/embabel-agent-shell/src/main/kotlin</dir>
</sourceDirectories>
Expand Down
160 changes: 160 additions & 0 deletions embabel-agent-docs/src/main/asciidoc/reference/annotations/page.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,166 @@ The only special thing about it is its ability to use the `OperationContext` par

The `@AchievesGoal` annotation can be added to an `@Action` method to indicate that the completion of the action achieves a specific goal.

[[reference.annotations_secure_agent_tool]]
==== The `@SecureAgentTool` annotation

`@SecureAgentTool` declares the security contract for an Embabel `@Action` method or `@Agent`
class exposed as a remote MCP tool.
It accepts a Spring Security SpEL expression evaluated against the current `Authentication`
at the point of tool invocation, before Embabel's GOAP planner executes the action body.

===== Placement

`@SecureAgentTool` can be placed on the `@Agent` class to protect every `@Action` uniformly,
or on individual methods for finer-grained control.
Method-level annotation takes precedence over class-level when both are present.

**Class-level** — one annotation secures all actions in the agent, including intermediate steps
that run before the goal-achieving action:

[tabs]
====
Kotlin::
+
[source,kotlin]
----
@Agent(description = "Research a topic and return a news digest")
@SecureAgentTool("hasAuthority('news:read')") // <1>
class NewsDigestAgent {
@Action
fun extractTopic(userInput: UserInput, context: OperationContext): NewsTopic { ... } // <2>
@AchievesGoal(description = "Produce a curated news digest",
export = Export(remote = true, name = "newsDigest",
startingInputTypes = [UserInput::class]))
@Action
fun produceDigest(topic: NewsTopic, context: OperationContext): NewsDigest { ... } // <2>
}
----
Java::
+
[source,java]
----
@Agent(description = "Research a topic and return a news digest")
@SecureAgentTool("hasAuthority('news:read')") // <1>
public class NewsDigestAgent {
@Action
public NewsTopic extractTopic(UserInput userInput, OperationContext context) { ... } // <2>
@AchievesGoal(description = "Produce a curated news digest",
export = @Export(remote = true, name = "newsDigest",
startingInputTypes = {UserInput.class}))
@Action
public NewsDigest produceDigest(NewsTopic topic, OperationContext context) { ... } // <2>
}
----
====
<1> One annotation on the class protects every `@Action` in the agent.
<2> Both `extractTopic` and `produceDigest` require `news:read`.
Without class-level protection, intermediate actions like `extractTopic` would run freely
before the security check on the goal-achieving action fires.

**Method-level override** — a method-level annotation takes precedence over the class-level
expression, allowing one action to require elevated authority:

[tabs]
====
Kotlin::
+
[source,kotlin]
----
@Agent(description = "Market intelligence agent")
@SecureAgentTool("hasAuthority('market:read')") // <1>
class MarketIntelligenceAgent {
@Action
fun gatherIntelligence(subject: AnalysisSubject, context: OperationContext): String { ... }
@SecureAgentTool("hasAuthority('market:admin')") // <2>
@AchievesGoal(description = "Produce market report")
@Action
fun synthesiseReport(
subject: AnalysisSubject,
rawIntelligence: String,
context: OperationContext
): MarketIntelligenceReport { ... }
}
----
Java::
+
[source,java]
----
@Agent(description = "Market intelligence agent")
@SecureAgentTool("hasAuthority('market:read')") // <1>
public class MarketIntelligenceAgent {
@Action
public String gatherIntelligence(AnalysisSubject subject, OperationContext context) { ... }
@SecureAgentTool("hasAuthority('market:admin')") // <2>
@AchievesGoal(description = "Produce market report")
@Action
public MarketIntelligenceReport synthesiseReport(
AnalysisSubject subject,
String rawIntelligence,
OperationContext context) { ... }
}
----
====
<1> All actions default to requiring `market:read`.
<2> `synthesiseReport` requires `market:admin` — the method-level annotation overrides the class.

===== Supported expressions

Any Spring Security SpEL expression is valid:

[cols="2,3",options="header"]
|===
|Expression |Meaning

|`hasAuthority('finance:read')`
|Principal must carry this exact authority

|`hasAnyAuthority('finance:read', 'finance:admin')`
|Principal must carry at least one of the listed authorities

|`hasRole('ADMIN')`
|Principal must carry `ROLE_ADMIN` (the `ROLE_` prefix is added automatically)

|`isAuthenticated()`
|Any authenticated principal, regardless of authorities

|`hasAuthority('payments:write') and #request.amount < 10000`
|Combines an authority check with a method parameter expression
|===

===== Setup

Add the MCP security starter to your `pom.xml`:

[source,xml]
----
<dependency>
<groupId>com.embabel.agent</groupId>
<artifactId>embabel-agent-starter-mcpserver-security</artifactId>
<version>${embabel-agent.version}</version>
</dependency>
----

The starter auto-configures `SecureAgentToolAspect` and the required Spring Security
`MethodSecurityExpressionHandler`.
No additional `@EnableMethodSecurity` annotation is required.

NOTE: `@SecureAgentTool` is a method-level security control, not an HTTP-level one.
For production use, combine it with a `SecurityFilterChain` that validates JWT Bearer tokens
so unauthenticated requests are rejected before reaching the GOAP planner.
See the https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html[Spring Security JWT Resource Server documentation] for general setup,
or xref:reference.integrations__mcp_security[MCP Security] for an MCP-specific example.

==== Implementing the `StuckHandler` interface

If an annotated agent class implements the `StuckHandler` interface, it can handle situations where an action is stuck itself.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,4 +472,12 @@ In summary, guardrails and bean validators are complementary but distinct:
- **Bean validation** ensures objects are well-formed and meet business constraints
- **Guardrails** ensure AI interactions are safe and compliant with policies

Both can be enabled independently and serve different aspects of the AI safety stack.
Both can be enabled independently and serve different aspects of the AI safety stack.

`@SecureAgentTool` is a third, orthogonal mechanism: it enforces _access control_ rather than
content safety or data validity.
Where guardrails ask "is this content acceptable?", `@SecureAgentTool` asks "is this principal
allowed to invoke this agent action at all?"
The two work well together — `@SecureAgentTool` prevents unauthorised principals from calling
a tool, while guardrails validate the inputs and outputs of calls that are permitted.
See xref:reference.annotations_secure_agent_tool[`@SecureAgentTool`] for details.
141 changes: 141 additions & 0 deletions embabel-agent-docs/src/main/asciidoc/reference/integrations/page.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,147 @@ Every MCP server includes a built-in `helloBanner` tool that displays server inf
}
----

[[reference.integrations__mcp_security]]
===== Security

Embabel MCP servers support two complementary layers of security that work together.
Think of them like a building with a reception desk and locked office doors: the HTTP filter
chain is the reception desk that turns away anyone without a badge, and `@SecureAgentTool`
is the locked door on each individual office that checks what the badge actually permits.

====== Layer 1 — HTTP transport (filter chain)

All requests to MCP endpoints (`/sse/**`, `/mcp/**`, `/message/**`) must carry a valid JWT
Bearer token or they are rejected with `401 Unauthorized` before the GOAP planner is invoked.

Configure a `SecurityFilterChain` and a JWT resource server in your Spring Security setup:

[tabs]
====
Kotlin::
+
[source,kotlin]
----
@Configuration
@EnableWebSecurity
class McpSecurityConfiguration {

@Bean
fun mcpFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.securityMatcher("/sse/**", "/mcp/**", "/message/**")
.authorizeHttpRequests { it.anyRequest().authenticated() }
.sessionManagement {
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
.oauth2ResourceServer { oauth2 ->
oauth2.jwt { jwt ->
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
}
}
.csrf { it.disable() }
return http.build()
}

@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val authoritiesConverter = JwtGrantedAuthoritiesConverter().apply {
setAuthoritiesClaimName("authorities")
setAuthorityPrefix("") // <1>
}
return JwtAuthenticationConverter().apply {
setJwtGrantedAuthoritiesConverter(authoritiesConverter)
}
}
}
----
====
<1> Empty prefix means JWT claim values like `news:read` map directly to Spring Security
authorities, so `hasAuthority('news:read')` in a `@SecureAgentTool` expression works without
any `SCOPE_` prefix.

Configure JWT validation in `application.yml`:

[source,yaml]
----
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:keys/public.pem # local dev
jws-algorithms: RS256
# For production, use issuer-uri or jwk-set-uri instead
----

====== Layer 2 — Method-level (`@SecureAgentTool`)

Enforces per-action authorization inside the GOAP execution pipeline, after the HTTP layer
has validated the token.
Place `@SecureAgentTool` on the `@Agent` class to protect every `@Action` in that agent:

[tabs]
====
Kotlin::
+
[source,kotlin]
----
@Agent(description = "Curated news digest agent")
@SecureAgentTool("hasAuthority('news:read')") // <1>
class NewsDigestAgent {

@Action
fun extractTopic(userInput: UserInput, context: OperationContext): NewsTopic { ... } // <2>

@AchievesGoal(description = "Produce news digest",
export = Export(remote = true, name = "newsDigest",
startingInputTypes = [UserInput::class]))
@Action
fun produceDigest(topic: NewsTopic, context: OperationContext): NewsDigest { ... } // <2>
}
----

Java::
+
[source,java]
----
@Agent(description = "Curated news digest agent")
@SecureAgentTool("hasAuthority('news:read')") // <1>
public class NewsDigestAgent {

@Action
public NewsTopic extractTopic(UserInput userInput, OperationContext context) { ... } // <2>

@AchievesGoal(description = "Produce news digest",
export = @Export(remote = true, name = "newsDigest",
startingInputTypes = {UserInput.class}))
@Action
public NewsDigest produceDigest(NewsTopic topic, OperationContext context) { ... } // <2>
}
----
====
<1> Class-level annotation applies to every `@Action` in this agent.
<2> Both `extractTopic` (the intermediate step) and `produceDigest` (the goal action) require
`news:read` — without class-level security, intermediate actions run freely before the goal
action's check fires, potentially burning LLM tokens on an unauthorised request.

See xref:reference.annotations_secure_agent_tool[`@SecureAgentTool`] for the full annotation
reference including supported SpEL expressions and method-level override behaviour.

====== Dependency

[source,xml]
----
<dependency>
<groupId>com.embabel.agent</groupId>
<artifactId>embabel-agent-starter-mcpserver-security</artifactId>
<version>${embabel-agent.version}</version>
</dependency>
----

The starter auto-configures `SecureAgentToolAspect` and wires the Spring Security
`MethodSecurityExpressionHandler`. No additional `@EnableMethodSecurity` is required.

[[reference.integrations__mcp_consuming]]
===== Consuming

Expand Down
Loading