@@ -1023,6 +1023,166 @@ The only special thing about it is its ability to use the `OperationContext` par
10231023
10241024The `@AchievesGoal` annotation can be added to an `@Action` method to indicate that the completion of the action achieves a specific goal.
10251025
1026+ [[reference.annotations_secure_agent_tool]]
1027+ ==== The `@SecureAgentTool` annotation
1028+
1029+ `@SecureAgentTool` declares the security contract for an Embabel `@Action` method or `@Agent`
1030+ class exposed as a remote MCP tool.
1031+ It accepts a Spring Security SpEL expression evaluated against the current `Authentication`
1032+ at the point of tool invocation, before Embabel's GOAP planner executes the action body.
1033+
1034+ ===== Placement
1035+
1036+ `@SecureAgentTool` can be placed on the `@Agent` class to protect every `@Action` uniformly,
1037+ or on individual methods for finer-grained control.
1038+ Method-level annotation takes precedence over class-level when both are present.
1039+
1040+ **Class-level** — one annotation secures all actions in the agent, including intermediate steps
1041+ that run before the goal-achieving action:
1042+
1043+ [tabs]
1044+ ====
1045+ Kotlin::
1046+ +
1047+ [source,kotlin]
1048+ ----
1049+ @Agent(description = "Research a topic and return a news digest")
1050+ @SecureAgentTool("hasAuthority('news:read')") // <1>
1051+ class NewsDigestAgent {
1052+
1053+ @Action
1054+ fun extractTopic(userInput: UserInput, context: OperationContext): NewsTopic { ... } // <2>
1055+
1056+ @AchievesGoal(description = "Produce a curated news digest",
1057+ export = Export(remote = true, name = "newsDigest",
1058+ startingInputTypes = [UserInput::class]))
1059+ @Action
1060+ fun produceDigest(topic: NewsTopic, context: OperationContext): NewsDigest { ... } // <2>
1061+ }
1062+ ----
1063+
1064+ Java::
1065+ +
1066+ [source,java]
1067+ ----
1068+ @Agent(description = "Research a topic and return a news digest")
1069+ @SecureAgentTool("hasAuthority('news:read')") // <1>
1070+ public class NewsDigestAgent {
1071+
1072+ @Action
1073+ public NewsTopic extractTopic(UserInput userInput, OperationContext context) { ... } // <2>
1074+
1075+ @AchievesGoal(description = "Produce a curated news digest",
1076+ export = @Export(remote = true, name = "newsDigest",
1077+ startingInputTypes = {UserInput.class}))
1078+ @Action
1079+ public NewsDigest produceDigest(NewsTopic topic, OperationContext context) { ... } // <2>
1080+ }
1081+ ----
1082+ ====
1083+ <1> One annotation on the class protects every `@Action` in the agent.
1084+ <2> Both `extractTopic` and `produceDigest` require `news:read`.
1085+ Without class-level protection, intermediate actions like `extractTopic` would run freely
1086+ before the security check on the goal-achieving action fires.
1087+
1088+ **Method-level override** — a method-level annotation takes precedence over the class-level
1089+ expression, allowing one action to require elevated authority:
1090+
1091+ [tabs]
1092+ ====
1093+ Kotlin::
1094+ +
1095+ [source,kotlin]
1096+ ----
1097+ @Agent(description = "Market intelligence agent")
1098+ @SecureAgentTool("hasAuthority('market:read')") // <1>
1099+ class MarketIntelligenceAgent {
1100+
1101+ @Action
1102+ fun gatherIntelligence(subject: AnalysisSubject, context: OperationContext): String { ... }
1103+
1104+ @SecureAgentTool("hasAuthority('market:admin')") // <2>
1105+ @AchievesGoal(description = "Produce market report")
1106+ @Action
1107+ fun synthesiseReport(
1108+ subject: AnalysisSubject,
1109+ rawIntelligence: String,
1110+ context: OperationContext
1111+ ): MarketIntelligenceReport { ... }
1112+ }
1113+ ----
1114+
1115+ Java::
1116+ +
1117+ [source,java]
1118+ ----
1119+ @Agent(description = "Market intelligence agent")
1120+ @SecureAgentTool("hasAuthority('market:read')") // <1>
1121+ public class MarketIntelligenceAgent {
1122+
1123+ @Action
1124+ public String gatherIntelligence(AnalysisSubject subject, OperationContext context) { ... }
1125+
1126+ @SecureAgentTool("hasAuthority('market:admin')") // <2>
1127+ @AchievesGoal(description = "Produce market report")
1128+ @Action
1129+ public MarketIntelligenceReport synthesiseReport(
1130+ AnalysisSubject subject,
1131+ String rawIntelligence,
1132+ OperationContext context) { ... }
1133+ }
1134+ ----
1135+ ====
1136+ <1> All actions default to requiring `market:read`.
1137+ <2> `synthesiseReport` requires `market:admin` — the method-level annotation overrides the class.
1138+
1139+ ===== Supported expressions
1140+
1141+ Any Spring Security SpEL expression is valid:
1142+
1143+ [cols="2,3",options="header"]
1144+ |===
1145+ |Expression |Meaning
1146+
1147+ |`hasAuthority('finance:read')`
1148+ |Principal must carry this exact authority
1149+
1150+ |`hasAnyAuthority('finance:read', 'finance:admin')`
1151+ |Principal must carry at least one of the listed authorities
1152+
1153+ |`hasRole('ADMIN')`
1154+ |Principal must carry `ROLE_ADMIN` (the `ROLE_` prefix is added automatically)
1155+
1156+ |`isAuthenticated()`
1157+ |Any authenticated principal, regardless of authorities
1158+
1159+ |`hasAuthority('payments:write') and #request.amount < 10000`
1160+ |Combines an authority check with a method parameter expression
1161+ |===
1162+
1163+ ===== Setup
1164+
1165+ Add the MCP security starter to your `pom.xml`:
1166+
1167+ [source,xml]
1168+ ----
1169+ <dependency>
1170+ <groupId>com.embabel.agent</groupId>
1171+ <artifactId>embabel-agent-starter-mcpserver-security</artifactId>
1172+ <version>${embabel-agent.version}</version>
1173+ </dependency>
1174+ ----
1175+
1176+ The starter auto-configures `SecureAgentToolAspect` and the required Spring Security
1177+ `MethodSecurityExpressionHandler`.
1178+ No additional `@EnableMethodSecurity` annotation is required.
1179+
1180+ NOTE: `@SecureAgentTool` is a method-level security control, not an HTTP-level one.
1181+ For production use, combine it with a `SecurityFilterChain` that validates JWT Bearer tokens
1182+ so unauthenticated requests are rejected before reaching the GOAP planner.
1183+ See the https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html[Spring Security JWT Resource Server documentation] for general setup,
1184+ or xref:reference.integrations__mcp_security[MCP Security] for an MCP-specific example.
1185+
10261186==== Implementing the `StuckHandler` interface
10271187
10281188If an annotated agent class implements the `StuckHandler` interface, it can handle situations where an action is stuck itself.
0 commit comments