Edit User

-
+
+ + + + + + + @@ -25,8 +32,7 @@

Edit User

- + @@ -37,12 +43,12 @@

Edit User

- + @@ -66,5 +72,61 @@

Edit User

+ diff --git a/api/src/test/java/io/sentrius/sso/controllers/api/ATPLPolicyControllerTest.java b/api/src/test/java/io/sentrius/sso/controllers/api/ATPLPolicyControllerTest.java index 32174f75..0c9bdd2a 100644 --- a/api/src/test/java/io/sentrius/sso/controllers/api/ATPLPolicyControllerTest.java +++ b/api/src/test/java/io/sentrius/sso/controllers/api/ATPLPolicyControllerTest.java @@ -1,7 +1,12 @@ package io.sentrius.sso.controllers.api; import com.fasterxml.jackson.databind.ObjectMapper; +import io.sentrius.sso.config.AppConfig; +import io.sentrius.sso.core.config.SystemOptions; +import io.sentrius.sso.core.model.ATPLPolicyEntity; import io.sentrius.sso.core.services.ATPLPolicyService; +import io.sentrius.sso.core.services.ErrorOutputService; +import io.sentrius.sso.core.services.UserService; import io.sentrius.sso.core.trust.ATPLPolicy; import io.sentrius.sso.core.trust.CapabilitySet; import org.junit.jupiter.api.BeforeEach; @@ -15,6 +20,7 @@ import java.util.Map; import java.util.HashMap; import java.util.List; +import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; @@ -26,13 +32,26 @@ class ATPLPolicyControllerTest { @Mock private ATPLPolicyService policyService; + @Mock + UserService userService; + + + @Mock + AppConfig appConfig; + + @Mock + SystemOptions systemOptions; + + @Mock + ErrorOutputService errorOutputService; + private ATPLPolicyController controller; @BeforeEach void setUp() { - controller = new ATPLPolicyController(policyService); + controller = + new ATPLPolicyController(userService, systemOptions, errorOutputService, policyService, appConfig); } - @Test void uploadValidPolicyReturnsSuccess() { String validPolicy = """ @@ -43,12 +62,13 @@ void uploadValidPolicyReturnsSuccess() { } """; - when(policyService.savePolicy(any(ATPLPolicy.class))).thenReturn(null); + var id = UUID.randomUUID().toString(); + when(policyService.savePolicy(any(ATPLPolicy.class))).thenReturn(ATPLPolicyEntity.builder().id(UUID.randomUUID()).policyId(id).build()); - ResponseEntity result = controller.uploadPolicy(validPolicy); + ResponseEntity result = controller.uploadPolicy(false, validPolicy); assertEquals(HttpStatus.CREATED, result.getStatusCode()); - assertEquals("Policy uploaded successfully.", result.getBody()); + assertEquals(id, result.getBody()); verify(policyService).savePolicy(any(ATPLPolicy.class)); } @@ -60,7 +80,7 @@ void uploadInvalidPolicyReturnsBadRequest() { } """; - ResponseEntity result = controller.uploadPolicy(invalidPolicy); + ResponseEntity result = controller.uploadPolicy(false, invalidPolicy); assertEquals(HttpStatus.BAD_REQUEST, result.getStatusCode()); assertTrue(result.getBody().toString().contains("Missing required fields")); @@ -127,9 +147,4 @@ void measureAgentComplianceWithMissingPolicyReturnsNotFound() { assertEquals("Policy not found", result.getBody()); } - @Test - void controllerCanBeInstantiated() { - ATPLPolicyController testController = new ATPLPolicyController(policyService); - assertNotNull(testController); - } } \ No newline at end of file diff --git a/api/src/test/java/io/sentrius/sso/controllers/api/CapabilitiesApiControllerJiraIntegrationTest.java b/api/src/test/java/io/sentrius/sso/controllers/api/CapabilitiesApiControllerJiraIntegrationTest.java new file mode 100644 index 00000000..0089b584 --- /dev/null +++ b/api/src/test/java/io/sentrius/sso/controllers/api/CapabilitiesApiControllerJiraIntegrationTest.java @@ -0,0 +1,94 @@ +package io.sentrius.sso.controllers.api; + +import io.sentrius.sso.core.dto.capabilities.EndpointDescriptor; +import io.sentrius.sso.core.services.capabilities.EndpointScanningService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration test to verify that JIRA verbs are properly discovered by the capabilities endpoint. + */ + + +public class CapabilitiesApiControllerJiraIntegrationTest { + + @Autowired + private EndpointScanningService endpointScanningService; + + + public void testJiraVerbsAreDiscovered() { + // Force refresh to ensure we get latest endpoints + endpointScanningService.refreshEndpoints(); + + List allEndpoints = endpointScanningService.getAllEndpoints(); + + // Verify we found some endpoints + assertTrue(allEndpoints.size() > 0, "Should have found some endpoints"); + + // Look for JIRA-related verbs + List jiraVerbs = allEndpoints.stream() + .filter(endpoint -> "VERB".equals(endpoint.getType())) + .filter(endpoint -> endpoint.getClassName().contains("JiraVerbService")) + .toList(); + + // Verify we found JIRA verbs + assertTrue(jiraVerbs.size() > 0, "Should have found JiraVerbService endpoints"); + + // Check for specific JIRA verbs + boolean foundSearchForTickets = jiraVerbs.stream() + .anyMatch(verb -> "searchForTickets".equals(verb.getName())); + assertTrue(foundSearchForTickets, "Should have found searchForTickets verb"); + + boolean foundAssignTicket = jiraVerbs.stream() + .anyMatch(verb -> "assignTicket".equals(verb.getName())); + assertTrue(foundAssignTicket, "Should have found assignTicket verb"); + + boolean foundIsJiraAvailable = jiraVerbs.stream() + .anyMatch(verb -> "isJiraAvailable".equals(verb.getName())); + assertTrue(foundIsJiraAvailable, "Should have found isJiraAvailable verb"); + + boolean foundUpdateTicket = jiraVerbs.stream() + .anyMatch(verb -> "updateTicket".equals(verb.getName())); + assertTrue(foundUpdateTicket, "Should have found updateTicket verb"); + + // Verify the verbs are marked as AI callable + for (EndpointDescriptor jiraVerb : jiraVerbs) { + Object isAiCallable = jiraVerb.getMetadata().get("isAiCallable"); + assertTrue(isAiCallable instanceof Boolean && (Boolean) isAiCallable, + "JIRA verb " + jiraVerb.getName() + " should be AI callable"); + } + + // Log found verbs for debugging + System.out.println("Found JIRA verbs:"); + jiraVerbs.forEach(verb -> { + System.out.println(" - " + verb.getName() + ": " + verb.getDescription()); + }); + } + + + public void testVerbEndpointFilterReturnsJiraVerbs() { + // Force refresh to ensure we get latest endpoints + endpointScanningService.refreshEndpoints(); + + List verbEndpoints = endpointScanningService.getAllEndpoints() + .stream() + .filter(endpoint -> "VERB".equals(endpoint.getType())) + .toList(); + + // Verify we have some verb endpoints + assertTrue(verbEndpoints.size() > 0, "Should have found verb endpoints"); + + // Look for JIRA verbs specifically + List jiraVerbs = verbEndpoints.stream() + .filter(endpoint -> endpoint.getClassName().contains("JiraVerbService")) + .toList(); + + assertTrue(jiraVerbs.size() >= 4, "Should have found at least 4 JIRA verbs (searchForTickets, assignTicket, updateTicket, isJiraAvailable)"); + } +} \ No newline at end of file diff --git a/core/src/main/java/io/sentrius/sso/core/dto/AgentRegistrationDTO.java b/core/src/main/java/io/sentrius/sso/core/dto/AgentRegistrationDTO.java index c6f2cdb2..aa646d3e 100644 --- a/core/src/main/java/io/sentrius/sso/core/dto/AgentRegistrationDTO.java +++ b/core/src/main/java/io/sentrius/sso/core/dto/AgentRegistrationDTO.java @@ -22,4 +22,6 @@ public class AgentRegistrationDTO { private final String agentCallbackUrl; @Builder.Default private final String agentContextId = ""; + @Builder.Default + private final String agentPolicyId = ""; } diff --git a/core/src/main/java/io/sentrius/sso/core/dto/agents/AgentContextDTO.java b/core/src/main/java/io/sentrius/sso/core/dto/agents/AgentContextDTO.java index 19e3b8de..c16bae06 100644 --- a/core/src/main/java/io/sentrius/sso/core/dto/agents/AgentContextDTO.java +++ b/core/src/main/java/io/sentrius/sso/core/dto/agents/AgentContextDTO.java @@ -16,7 +16,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class AgentContextDTO { @Builder.Default - private UUID id = UUID.randomUUID(); + private UUID contextId = UUID.randomUUID(); private String name; @Builder.Default private String description = ""; diff --git a/core/src/main/java/io/sentrius/sso/core/dto/ztat/AgentExecution.java b/core/src/main/java/io/sentrius/sso/core/dto/agents/AgentExecution.java similarity index 54% rename from core/src/main/java/io/sentrius/sso/core/dto/ztat/AgentExecution.java rename to core/src/main/java/io/sentrius/sso/core/dto/agents/AgentExecution.java index 81789c1b..424900cb 100644 --- a/core/src/main/java/io/sentrius/sso/core/dto/ztat/AgentExecution.java +++ b/core/src/main/java/io/sentrius/sso/core/dto/agents/AgentExecution.java @@ -1,8 +1,9 @@ -package io.sentrius.sso.core.dto.ztat; +package io.sentrius.sso.core.dto.agents; import java.util.ArrayList; import java.util.List; import io.sentrius.sso.core.dto.UserDTO; +import io.sentrius.sso.core.dto.ztat.TokenDTO; import io.sentrius.sso.genai.Message; import lombok.Builder; import lombok.Data; @@ -19,20 +20,6 @@ public class AgentExecution extends TokenDTO { UserDTO user; String executionId; - @Builder.Default - List messages = new ArrayList<>(); - public void addMessages(List messages) { - if (messages == null){ - messages = new ArrayList<>(); - } - this.messages.addAll(messages); - - } - - public void addMessages(Message message) { - this.messages.add(message); - - } } diff --git a/core/src/main/java/io/sentrius/sso/core/dto/agents/AgentExecutionContextDTO.java b/core/src/main/java/io/sentrius/sso/core/dto/agents/AgentExecutionContextDTO.java new file mode 100644 index 00000000..b1fc0259 --- /dev/null +++ b/core/src/main/java/io/sentrius/sso/core/dto/agents/AgentExecutionContextDTO.java @@ -0,0 +1,193 @@ +package io.sentrius.sso.core.dto.agents; + +import java.io.IOException; +import java.util.*; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.sentrius.sso.core.utils.JsonUtil; +import io.sentrius.sso.genai.Message; +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Slf4j +@ToString +@JsonIgnoreProperties(ignoreUnknown = true) +public class AgentExecutionContextDTO { + + @Builder.Default + private List messages = new ArrayList<>(); + + private AgentContextDTO agentContext; + + @Builder.Default + private List agentDataList = new ArrayList<>(); + + @Builder.Default + private Map agentShortTermMemory = new HashMap<>(); + + @Builder.Default + private ObjectNode executionArgs = JsonUtil.MAPPER.createObjectNode(); + + @Builder.Default + private ObjectNode callParams = JsonUtil.MAPPER.createObjectNode(); + + // === Memory Management === + + public void addToMemory(JsonNode node) { + agentDataList.add(node); + flatten("", node, agentShortTermMemory); + } + + public void addToMemory(String key, JsonNode value) { + putStructuredToMemory(key, value); + } + + public void putStructuredToMemory(String key, JsonNode value) { + agentShortTermMemory.put(key, value); + // Optional: Add to agentDataList if you want to preserve all data too + ObjectNode wrapper = JsonUtil.MAPPER.createObjectNode(); + wrapper.set(key, value); + agentDataList.add(wrapper); + } + + public static void flatten(String prefix, JsonNode node, Map map) { + if (node.isObject()) { + node.fields().forEachRemaining(entry -> { + String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey(); + log.info("Flattening key: {}", key); + flatten(key, entry.getValue(), map); + }); + } else if (node.isArray()) { + for (int i = 0; i < node.size(); i++) { + flatten(prefix + "[" + i + "]", node.get(i), map); + } + map.put(prefix + "_length", JsonUtil.MAPPER.convertValue(node.size(), JsonNode.class)); + } else { + map.put(prefix, node); + } + } + + // === Execution Argument Access === + + public Optional getExecutionArgument(String name) { + if (executionArgs != null && executionArgs.has(name)) { + return Optional.of(executionArgs.get(name)); + } + + if (agentShortTermMemory != null && agentShortTermMemory.containsKey(name)) { + log.info("Getting from shortTermMemory for name: {}", name); + return Optional.ofNullable(agentShortTermMemory.get(name)); + } + log.info("Short term memory is {}", agentShortTermMemory); + log.info("Execution argument '{}' not found in executionArgs or shortTermMemory", name); + return Optional.empty(); + } + + public Optional getExecutionArgument(String methodArgumentName, String name) { + log.info("Getting execution argument for methodArgumentName: {}, name: {}", methodArgumentName, name); + if (executionArgs != null && executionArgs.has(methodArgumentName)) { + log.info("Found execution argument for methodArgumentName: {}", executionArgs.get(methodArgumentName)); + if (executionArgs.get(methodArgumentName).has(methodArgumentName)) { + return Optional.of(executionArgs.get(methodArgumentName).get(methodArgumentName).get(name)); + } + return Optional.of(executionArgs.get(methodArgumentName).get(name)); + } + + if (agentShortTermMemory != null && agentShortTermMemory.containsKey(methodArgumentName)) { + + log.info("Found execution argument for methodArgumentName: {}", agentShortTermMemory.get(methodArgumentName)); + return Optional.of(agentShortTermMemory.get(methodArgumentName).get(name)); + } else { + log.info("Execution argument '{}' not found in executionArgs or shortTermMemory {}", methodArgumentName, + agentShortTermMemory); + } + + return Optional.empty(); + } + + public Optional getExecutionArgumentScoped(String name, Class clazz) { + try { + return getExecutionArgument(name) + .map(node -> JsonUtil.MAPPER.convertValue(node, clazz)); + } catch (Exception e) { + log.error("Error while handling scoped argument for '{}'", name, e); + return Optional.empty(); + } + } + + public Optional getExecutionArgumentScoped(String name, TypeReference typeRef) { + return getExecutionArgument(name).flatMap(node -> { + try { + return Optional.of(JsonUtil.MAPPER.readValue( + JsonUtil.MAPPER.treeAsTokens(node), typeRef + )); + } catch (IOException e) { + log.warn("Failed to deserialize '{}' from shortTermMemory: {}", name, e.getMessage()); + return Optional.empty(); + } + }); + } + + // === Messages === + + public void addMessages(List messages) { + if (messages != null) { + this.messages.addAll(messages); + } + } + + public void addMessages(Message message) { + this.messages.add(message); + } + + // === Label Sanitization === + + public String getSafeLabel(String name) { + return getExecutionArgument(name) + .map(JsonNode::asText) + .map(this::sanitizeLabelValue) + .orElse("unknown"); + } + + public String getLabel(String methodArgumentName, String name) { + var safeGet = getExecutionArgument(methodArgumentName, name); + if (safeGet.isEmpty()) { + return getExecutionArgument(name) + .map(JsonNode::asText) + .orElse("unknown"); + } else { + return safeGet + .map(JsonNode::asText) + .orElse("unknown"); + } + } + + public String getSafeLabel(String methodArgumentName, String name) { + var safeGet = getExecutionArgument(methodArgumentName, name); + if (safeGet.isEmpty()) { + return getExecutionArgument(name) + .map(JsonNode::asText) + .map(this::sanitizeLabelValue) + .orElse("unknown"); + } else { + return safeGet + .map(JsonNode::asText) + .map(this::sanitizeLabelValue) + .orElse("unknown"); + } + } + + private String sanitizeLabelValue(String value) { + return value + .replaceAll("^\"|\"$", "") // strip surrounding quotes + .replaceAll("[^A-Za-z0-9_.-]", "") // remove invalid characters + .replaceAll("^[^A-Za-z0-9]+|[^A-Za-z0-9]+$", ""); // trim invalid start/end + } +} diff --git a/core/src/main/java/io/sentrius/sso/core/dto/capabilities/EndpointDescriptor.java b/core/src/main/java/io/sentrius/sso/core/dto/capabilities/EndpointDescriptor.java index cf6fec30..abb56a22 100644 --- a/core/src/main/java/io/sentrius/sso/core/dto/capabilities/EndpointDescriptor.java +++ b/core/src/main/java/io/sentrius/sso/core/dto/capabilities/EndpointDescriptor.java @@ -22,6 +22,8 @@ @NoArgsConstructor @AllArgsConstructor public class EndpointDescriptor { + @Builder.Default + private String serviceUrl = ""; // Base URL of the service providing this endpoint private String name; private String description; private String type; // "REST" or "VERB" diff --git a/core/src/main/java/io/sentrius/sso/core/model/verbs/DefaultInterpreter.java b/core/src/main/java/io/sentrius/sso/core/model/verbs/DefaultInterpreter.java deleted file mode 100644 index 32b2cbec..00000000 --- a/core/src/main/java/io/sentrius/sso/core/model/verbs/DefaultInterpreter.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.sentrius.sso.core.model.verbs; - -import java.util.HashMap; -import java.util.Map; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class DefaultInterpreter implements OutputInterpreterIfc, InputInterpreterIfc> { - - @Override - public Map interpret(VerbResponse input) throws Exception { - // Default implementation: return the response as is - Map responseMap = new HashMap<>(); - responseMap.put("verb.response.type", input.getReturnType().getCanonicalName()); - responseMap.put("verb.response", input.getResponse()); - return responseMap; - } - - @Override - public Map interpret(Map input) throws Exception { - return input; - } -} diff --git a/core/src/main/java/io/sentrius/sso/core/model/verbs/InputInterpreterIfc.java b/core/src/main/java/io/sentrius/sso/core/model/verbs/InputInterpreterIfc.java deleted file mode 100644 index 1d650d8e..00000000 --- a/core/src/main/java/io/sentrius/sso/core/model/verbs/InputInterpreterIfc.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.sentrius.sso.core.model.verbs; - -import java.util.Map; - -public interface InputInterpreterIfc { - - T interpret(Map input) throws Exception; - -} diff --git a/core/src/main/java/io/sentrius/sso/core/model/verbs/ListInterpreter.java b/core/src/main/java/io/sentrius/sso/core/model/verbs/ListInterpreter.java deleted file mode 100644 index 61ff0263..00000000 --- a/core/src/main/java/io/sentrius/sso/core/model/verbs/ListInterpreter.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.sentrius.sso.core.model.verbs; - -import java.util.List; -import java.util.Map; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ArrayNode; -import io.sentrius.sso.core.utils.JsonUtil; - -public class ListInterpreter implements InputInterpreterIfc>{ - @Override - public List interpret(Map input) throws Exception { - if (input.containsKey("verb.response.type") && input.get("verb.response.type").equals("list")) { - return interpretList(input); - } else if (input.containsKey("verb.response.type") && input.get("verb.response.type").equals(ArrayNode.class.getCanonicalName())) { - var str = input.get("verb.response").toString(); - ArrayNode node = (ArrayNode) JsonUtil.MAPPER.readTree(str); - if (node == null) { - throw new IllegalArgumentException("Input response is not a valid JSON array"); - } - TypeReference> typeRef = new TypeReference<>() {}; - return JsonUtil.convertArrayNodeToList(node,typeRef); - } else { - return null; - } - - } - - - private List interpretList(Map input) { - - var field = input.get("verb.response.map.key"); - if (field == null) { - return null; - } - - var object = input.get(field); - if (object instanceof List){ - return (List) object; - } - return null; - } -} diff --git a/core/src/main/java/io/sentrius/sso/core/model/verbs/OutputInterpreterIfc.java b/core/src/main/java/io/sentrius/sso/core/model/verbs/OutputInterpreterIfc.java deleted file mode 100644 index c587f8ea..00000000 --- a/core/src/main/java/io/sentrius/sso/core/model/verbs/OutputInterpreterIfc.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.sentrius.sso.core.model.verbs; - -import java.util.Map; - -public interface OutputInterpreterIfc { - - Map interpret(VerbResponse input) throws Exception; - -} diff --git a/core/src/main/java/io/sentrius/sso/core/model/verbs/Verb.java b/core/src/main/java/io/sentrius/sso/core/model/verbs/Verb.java index 18004615..be16602f 100644 --- a/core/src/main/java/io/sentrius/sso/core/model/verbs/Verb.java +++ b/core/src/main/java/io/sentrius/sso/core/model/verbs/Verb.java @@ -9,10 +9,11 @@ @Retention(RetentionPolicy.RUNTIME) public @interface Verb { String name(); + String returnName() default ""; + String argName() default "arg1"; String description() default ""; Class returnType() default String.class; - Class outputInterpreter() default DefaultInterpreter.class; - Class inputInterpreter() default DefaultInterpreter.class; + String[] pathVariables() default {}; String[] paramDescriptions() default {}; // if set to true, this verb will be callable by AI agents boolean isAiCallable() default true; diff --git a/core/src/main/java/io/sentrius/sso/core/model/verbs/VerbResponse.java b/core/src/main/java/io/sentrius/sso/core/model/verbs/VerbResponse.java index c82fc77a..68ad35f8 100644 --- a/core/src/main/java/io/sentrius/sso/core/model/verbs/VerbResponse.java +++ b/core/src/main/java/io/sentrius/sso/core/model/verbs/VerbResponse.java @@ -11,8 +11,5 @@ @Getter public class VerbResponse { private List messages; - private Object response; private Class returnType; - @Builder.Default - private Class outputInterpreter = DefaultInterpreter.class; } diff --git a/core/src/main/java/io/sentrius/sso/core/services/agents/AgentClientService.java b/core/src/main/java/io/sentrius/sso/core/services/agents/AgentClientService.java index 9d036a8d..5f17cb81 100644 --- a/core/src/main/java/io/sentrius/sso/core/services/agents/AgentClientService.java +++ b/core/src/main/java/io/sentrius/sso/core/services/agents/AgentClientService.java @@ -7,7 +7,7 @@ import java.util.concurrent.TimeoutException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Maps; import io.sentrius.sso.core.dto.AgentCommunicationDTO; import io.sentrius.sso.core.dto.AgentHeartbeatDTO; @@ -15,7 +15,7 @@ import io.sentrius.sso.core.dto.agents.AgentContextDTO; import io.sentrius.sso.core.dto.agents.AgentContextRequestDTO; import io.sentrius.sso.core.dto.capabilities.EndpointDescriptor; -import io.sentrius.sso.core.dto.ztat.AgentExecution; +import io.sentrius.sso.core.dto.agents.AgentExecution; import io.sentrius.sso.core.dto.ztat.AtatRequest; import io.sentrius.sso.core.dto.ztat.TokenDTO; import io.sentrius.sso.core.dto.ztat.ZtatRequestDTO; @@ -80,7 +80,7 @@ public Set getCommunicationIds(AgentExecution execution, ZtatRequestDTO var response = zeroTrustClientService.callGetOnApi(execution, responseUrl, Maps.immutableEntry("requestId", List.of(atatRequest.getRequestId()))); if (response != null) { - log.info("response is {}", response); + log.info("responseis {}", response); return JsonUtil.MAPPER.readValue( response, new TypeReference<>() { @@ -192,12 +192,13 @@ public AgentCommunicationDTO sendResponse(AgentExecution execution, AgentCommuni return JsonUtil.MAPPER.readValue(acommResponse, AgentCommunicationDTO.class); } - public AgentRegistrationDTO bootstrap(String name, String publicKey, String keyType) + public AgentRegistrationDTO bootstrap(String clientId, String name, String publicKey, String keyType) throws ZtatException, JsonProcessingException { String ask = "/agent/bootstrap/register"; AgentRegistrationDTO registration = AgentRegistrationDTO.builder() .agentName(name) + .clientId(clientId) .agentCallbackUrl(getCallbackUrl()) .agentPublicKey(publicKey) .agentPublicKeyAlgo(keyType) @@ -243,26 +244,35 @@ public String getAgentPodStatus(String launcherService, String agentId) throws Z var podResponse = zeroTrustClientService.callAuthenticatedGetOnApi(launcherService, "agent/launcher" + "/status", Maps.immutableEntry("agentId", List.of(agentId)) ); - String apiResponse = "Running"; - switch(podResponse){ - case "Running": - apiResponse = "Running"; - break; - case "Pending": - apiResponse = "Pending"; - break; - case "Succeeded": - apiResponse = "Succeeded"; - break; - case "Failed": - apiResponse = "Failed"; - break; - case "NotFound": - apiResponse = "NotFound"; - break; - default: - log.error("Unknown pod status response: {}", podResponse); - apiResponse = "Unknown"; + + String apiResponse = "Unknown"; + log.info("Pod response: {}", podResponse); + try { + var responseNode = JsonUtil.MAPPER.readTree(podResponse); + if (responseNode.has("status")) { + switch (responseNode.get("status").asText().toLowerCase()) { + case "running": + apiResponse = "Running"; + break; + case "pending": + apiResponse = "Pending"; + break; + case "succeeded": + apiResponse = "Succeeded"; + break; + case "failed": + apiResponse = "Failed"; + break; + case "notfound": + apiResponse = "NotFound"; + break; + default: + log.error("Unknown pod status response: {}", podResponse); + apiResponse = "Unknown"; + } + } + }catch (Exception e) { + log.error("Error parsing pod status response: {}", e.getMessage()); } return apiResponse; } diff --git a/core/src/main/java/io/sentrius/sso/core/services/agents/AgentExecutionService.java b/core/src/main/java/io/sentrius/sso/core/services/agents/AgentExecutionService.java index 426890c5..b3d7f5b8 100644 --- a/core/src/main/java/io/sentrius/sso/core/services/agents/AgentExecutionService.java +++ b/core/src/main/java/io/sentrius/sso/core/services/agents/AgentExecutionService.java @@ -5,7 +5,7 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import io.sentrius.sso.core.dto.UserDTO; -import io.sentrius.sso.core.dto.ztat.AgentExecution; +import io.sentrius.sso.core.dto.agents.AgentExecution; import org.springframework.stereotype.Service; @Service diff --git a/core/src/main/java/io/sentrius/sso/core/services/agents/ZeroTrustClientService.java b/core/src/main/java/io/sentrius/sso/core/services/agents/ZeroTrustClientService.java index 43eeb73a..8eecbc61 100644 --- a/core/src/main/java/io/sentrius/sso/core/services/agents/ZeroTrustClientService.java +++ b/core/src/main/java/io/sentrius/sso/core/services/agents/ZeroTrustClientService.java @@ -1,5 +1,6 @@ package io.sentrius.sso.core.services.agents; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -8,7 +9,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.sentrius.sso.core.dto.UserDTO; -import io.sentrius.sso.core.dto.ztat.AgentExecution; +import io.sentrius.sso.core.dto.agents.AgentExecution; import io.sentrius.sso.core.dto.ztat.EndpointRequest; import io.sentrius.sso.core.dto.ztat.TokenDTO; import io.sentrius.sso.core.dto.ztat.ZtatRequestDTO; @@ -24,6 +25,7 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; @Slf4j @Service @@ -72,7 +74,7 @@ public String registerAgent(@NonNull TokenDTO token) throws ZtatException { try{ ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - if (response.getStatusCode() == HttpStatus.OK) { + if (response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) { return response.getBody(); // This is the ZTAT (JWT or opaque token) } else { throw new RuntimeException("Failed to obtain ZTAT: " + response.getStatusCode()); @@ -97,7 +99,8 @@ public String callPostOnApi(@NonNull TokenDTO token,@NonNull String apiEndpo return callPostOnApi(token, agentApiUrl, apiEndpoint, body, params); } - String callPostOnApi(@NonNull TokenDTO token, String endpoint, @NonNull String apiEndpoint, T body,Map.Entry>... params) throws ZtatException { + public String callPostOnApi(@NonNull TokenDTO token, String endpoint, @NonNull String apiEndpoint, T body, + Map.Entry>... params) throws ZtatException { String keycloakJwt = getKeycloakToken(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -117,13 +120,15 @@ String callPostOnApi(@NonNull TokenDTO token, String endpoint, @NonNull Stri .path(apiEndpoint); if (null != params) { for (Map.Entry> entry : params) { - builder.queryParam(entry.getKey(), entry.getValue()); + for (String value : entry.getValue()) { + builder.queryParam(entry.getKey(), UriUtils.encodeQueryParam(value, StandardCharsets.UTF_8)); + } } } try{ ResponseEntity response = restTemplate.exchange(builder.build(true).toUriString(), HttpMethod.POST, requestEntity, String.class); - if (response.getStatusCode() == HttpStatus.OK) { + if (response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) { return response.getBody(); // This is the ZTAT (JWT or opaque token) } else if (response.getStatusCode() == HttpStatus.PRECONDITION_REQUIRED) { // we need to get @@ -192,7 +197,7 @@ String callPostOnApi(String endpoint, @NonNull String apiEndpoint, T body,Ma try{ ResponseEntity response = restTemplate.exchange(builder.build(true).toUriString(), HttpMethod.POST, requestEntity, String.class); - if (response.getStatusCode() == HttpStatus.OK) { + if (response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) { return response.getBody(); // This is the ZTAT (JWT or opaque token) } else if (response.getStatusCode() == HttpStatus.PRECONDITION_REQUIRED) { // we need to get @@ -253,7 +258,6 @@ String callAuthenticatedPostOnApi(String endpoint, @NonNull String apiEndpoi headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(keycloakJwt); - log.info("**** EXPOSING JWT {}", keycloakJwt); log.info("Sending {}", body.toString()); HttpEntity requestEntity = new HttpEntity<>(body, headers); if (!apiEndpoint.startsWith("/")) { @@ -272,7 +276,7 @@ String callAuthenticatedPostOnApi(String endpoint, @NonNull String apiEndpoi try{ ResponseEntity response = restTemplate.exchange(builder.build(true).toUriString(), HttpMethod.POST, requestEntity, String.class); - if (response.getStatusCode() == HttpStatus.OK) { + if (response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) { return response.getBody(); // This is the ZTAT (JWT or opaque token) } else if (response.getStatusCode() == HttpStatus.PRECONDITION_REQUIRED) { // we need to get @@ -322,7 +326,7 @@ public String callAuthenticatedGetOnApi( ResponseEntity response = restTemplate.exchange(builder.build(true).toUriString(), HttpMethod.GET, requestEntity, String.class); - if (response.getStatusCode() == HttpStatus.OK) { + if (response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) { return response.getBody(); // This is the ZTAT (JWT or opaque token) } else if (response.getStatusCode() == HttpStatus.PRECONDITION_REQUIRED) { // we need to get @@ -388,7 +392,7 @@ final String callPutOnApi( requestEntity, String.class); - if (response.getStatusCode() == HttpStatus.OK) { + if (response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) { return response.getBody(); // This is the ZTAT (JWT or opaque token) } else if (response.getStatusCode() == HttpStatus.PRECONDITION_REQUIRED) { // we need to get @@ -439,7 +443,7 @@ String callPostOnApi(@NonNull TokenDTO token, String endpoint, @NonNull Stri try { ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - if (response.getStatusCode() == HttpStatus.OK) { + if (response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) { return response.getBody(); // This is the ZTAT (JWT or opaque token) } else if (response.getStatusCode() == HttpStatus.PRECONDITION_REQUIRED) { // we need to get @@ -476,7 +480,7 @@ public final String callGetOnApi(@NonNull TokenDTO token, @SafeVarargs - final String callGetOnApi( + public final String callGetOnApi( @NonNull TokenDTO token, String endpoint, @NonNull String apiEndpoint, Map.Entry> param, Map.Entry>... params @@ -499,12 +503,16 @@ final String callGetOnApi( var builder = UriComponentsBuilder.fromHttpUrl(endpoint) .path(apiEndpoint); - for (String value : param.getValue()){ - builder.queryParam(param.getKey(), value); + if (null != param) { + for (String value : param.getValue()) { + builder.queryParam(param.getKey(), UriUtils.encodeQueryParam(value, StandardCharsets.UTF_8)); + } } - for (Map.Entry> entry : params) { - for(String value : entry.getValue()) { - builder.queryParam(entry.getKey(), value); + if (null != params) { + for (Map.Entry> entry : params) { + for (String value : entry.getValue()) { + builder.queryParam(entry.getKey(), UriUtils.encodeQueryParam(value, StandardCharsets.UTF_8)); + } } } try{ @@ -512,7 +520,7 @@ final String callGetOnApi( requestEntity, String.class); - if (response.getStatusCode() == HttpStatus.OK) { + if (response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) { return response.getBody(); // This is the ZTAT (JWT or opaque token) } else if (response.getStatusCode() == HttpStatus.PRECONDITION_REQUIRED) { // we need to get @@ -563,7 +571,7 @@ T callGetOnApi(@NonNull TokenDTO token, String endpoint, @NonNull String api try{ ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, requestEntity, (Class) Object.class); - if (response.getStatusCode() == HttpStatus.OK) { + if (response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) { return response.getBody(); // This is the ZTAT (JWT or opaque token) } else if (response.getStatusCode() == HttpStatus.PRECONDITION_REQUIRED) { // we need to get @@ -605,7 +613,7 @@ public String requestZtatToken(TokenDTO token, UserDTO user, String command) { try{ ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - if (response.getStatusCode() == HttpStatus.OK) { + if (response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) { JsonNode node = JsonUtil.MAPPER.readTree(response.getBody()); return node.get("ztat_request").asText(); } else { @@ -637,7 +645,7 @@ public String requestZtatToken(TokenDTO token, UserDTO user, ZtatRequestDTO requ try{ ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - if (response.getStatusCode() == HttpStatus.OK) { + if (response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) { JsonNode node = JsonUtil.MAPPER.readTree(response.getBody()); return node.get("ztat_request").asText(); } else { diff --git a/core/src/main/java/io/sentrius/sso/core/services/capabilities/EndpointScanningService.java b/core/src/main/java/io/sentrius/sso/core/services/capabilities/EndpointScanningService.java index 00394ac6..266689e6 100644 --- a/core/src/main/java/io/sentrius/sso/core/services/capabilities/EndpointScanningService.java +++ b/core/src/main/java/io/sentrius/sso/core/services/capabilities/EndpointScanningService.java @@ -13,11 +13,9 @@ import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.*; -import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*; -import java.util.stream.Stream; /** * Service that scans for both REST API endpoints and Verb methods across the application. @@ -30,9 +28,16 @@ public class EndpointScanningService { private final ApplicationContext applicationContext; private final Map cachedEndpoints = new HashMap<>(); private boolean cacheInitialized = false; + private volatile boolean selectVerbs = false; public EndpointScanningService(ApplicationContext applicationContext) { this.applicationContext = applicationContext; + this.selectVerbs = true; + } + + + public void disableVerbScanning() { + this.selectVerbs = false; } /** @@ -67,9 +72,11 @@ private void scanAndCacheEndpoints() { // Scan for REST endpoints scanRestEndpoints(); - - // Scan for Verb methods - scanVerbEndpoints(); + + if (selectVerbs) { + // Scan for Verb methods + scanVerbEndpoints(); + } log.info("Endpoint scanning completed. Found {} endpoints", cachedEndpoints.size()); } @@ -105,7 +112,16 @@ private void scanRestControllerClass(Class clazz) { String basePath = classMapping != null && classMapping.value().length > 0 ? classMapping.value()[0] : ""; for (Method method : clazz.getDeclaredMethods()) { + // ⛔ Skip methods that are also annotated with @Verb + if (method.isAnnotationPresent(Verb.class)) { + log.info("Skipping method {} in class {} because it is annotated with @Verb", method.getName(), clazz.getName()); + continue; + } else { + log.info("Scanning method {} in class {}", method.getName(), clazz.getName() ); + } + EndpointDescriptor descriptor = scanRestMethod(clazz, method, basePath); + log.info("Scanned method {} in class {}: {}", method.getName(), clazz.getName(), descriptor); if (descriptor != null) { String key = descriptor.getType() + ":" + descriptor.getName(); cachedEndpoints.put(key, descriptor); @@ -164,7 +180,7 @@ private EndpointDescriptor scanRestMethod(Class clazz, Method method, String return EndpointDescriptor.builder() .name(method.getName()) - .description("REST endpoint: " + httpMethod + " " + path) // TODO: Extract from JavaDoc or custom annotation + .description("REST endpoint: " + httpMethod + " " + path) .type("REST") .httpMethod(httpMethod) .path(path) @@ -223,9 +239,7 @@ private EndpointDescriptor scanVerbMethod(Class clazz, Method method) { .requiresTokenManagement(verbAnnotation.requiresTokenManagement()) .accessLimitations(AccessLimitations.builder().hasLimitAccess(false).build()) .metadata(Map.of( - "isAiCallable", verbAnnotation.isAiCallable(), - "outputInterpreter", verbAnnotation.outputInterpreter().getName(), - "inputInterpreter", verbAnnotation.inputInterpreter().getName() + "isAiCallable", verbAnnotation.isAiCallable() )) .build(); } @@ -280,6 +294,7 @@ private List extractRestParameters(Method method) { name = !requestHeader.value().isEmpty() ? requestHeader.value() : (!requestHeader.name().isEmpty() ? requestHeader.name() : name); required = requestHeader.required(); + continue; } parameters.add(ParameterDescriptor.builder() diff --git a/core/src/main/java/io/sentrius/sso/core/services/security/JwtUtil.java b/core/src/main/java/io/sentrius/sso/core/services/security/JwtUtil.java index b62b10f2..e9e45b05 100644 --- a/core/src/main/java/io/sentrius/sso/core/services/security/JwtUtil.java +++ b/core/src/main/java/io/sentrius/sso/core/services/security/JwtUtil.java @@ -31,7 +31,7 @@ public static ObjectNode getJWT() { .writeValueAsString(authentication.getPrincipal()); return (ObjectNode) JsonUtil.MAPPER.readTree(jwt); } catch (Exception e) { - e.printStackTrace(); + // ignorable error, just return an empty node } } } diff --git a/core/src/main/java/io/sentrius/sso/core/trust/ATPLPolicy.java b/core/src/main/java/io/sentrius/sso/core/trust/ATPLPolicy.java index bbad29cb..80dbebdf 100644 --- a/core/src/main/java/io/sentrius/sso/core/trust/ATPLPolicy.java +++ b/core/src/main/java/io/sentrius/sso/core/trust/ATPLPolicy.java @@ -34,13 +34,16 @@ public class ATPLPolicy { private Provenance provenance; @JsonProperty("runtime") - private AgentRuntimePolicies runtimePolicies; + private AgentRuntimePolicies runtimePolicies = new AgentRuntimePolicies(); private Behavior behavior; @JsonProperty("trust_score") private TrustScore trustScore; - private Actions actions; + + @Builder.Default + private Actions actions = + Actions.builder().onSuccess("allow").onFailure("ztat").onMarginal(OnMarginal.builder().action("ztat").build()).build(); private CapabilitySet capabilities; diff --git a/dataplane/src/main/java/io/sentrius/sso/core/config/SystemOptions.java b/dataplane/src/main/java/io/sentrius/sso/core/config/SystemOptions.java index 56a2d3a2..be0dad54 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/config/SystemOptions.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/config/SystemOptions.java @@ -190,13 +190,13 @@ public boolean setValue(String fieldName, Object fieldValue, boolean save){ List fields = getAllInstanceFields(); for (var field : fields) { if (field.getName().equalsIgnoreCase(fieldName)) { - log.debug("Setting field {} to {}", fieldName, fieldValue); + log.trace("Setting field {} to {}", fieldName, fieldValue); try { field.set(this, fieldValue); // Update the AppConfig with the new field value dynamicPropertiesService.updateProperty(fieldName, fieldValue.toString()); - log.debug("Set field {} to {}", fieldName, fieldValue); + log.trace("Set field {} to {}", fieldName, fieldValue); return true; } catch (IllegalAccessException e) { log.error("Failed to update field {}", fieldName); @@ -240,7 +240,7 @@ public Map getOptions() throws IllegalAccessException { String fieldName = field.getName(); Object fieldValue = field.get(this); - log.debug("Field: {} Value: {}", fieldName, fieldValue); + log.trace("Field: {} Value: {}", fieldName, fieldValue); // Create a SystemOption object with the field details var sysOpt = SystemOption.builder() diff --git a/dataplane/src/main/java/io/sentrius/sso/core/controllers/BaseController.java b/dataplane/src/main/java/io/sentrius/sso/core/controllers/BaseController.java index ac4ea59a..f4dd5b96 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/controllers/BaseController.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/controllers/BaseController.java @@ -1,5 +1,10 @@ package io.sentrius.sso.core.controllers; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -139,4 +144,15 @@ public HostGroup getSelectedHostGroup(HttpServletRequest request) { */ + protected InputStream getStream(String requestedPath) throws IOException { + Path path = Paths.get(requestedPath); // 🔁 Replace with your actual path + + if (!Files.exists(path)) { + throw new RuntimeException("File not found at path: " + path.toAbsolutePath()); + } + + return Files.newInputStream(path); + + } + } diff --git a/dataplane/src/main/java/io/sentrius/sso/core/integrations/ticketing/JiraService.java b/dataplane/src/main/java/io/sentrius/sso/core/integrations/ticketing/JiraService.java index 23423f89..0a047682 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/integrations/ticketing/JiraService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/integrations/ticketing/JiraService.java @@ -1,5 +1,7 @@ package io.sentrius.sso.core.integrations.ticketing; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -7,7 +9,9 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import io.sentrius.sso.core.dto.TicketDTO; import io.sentrius.sso.core.integrations.external.ExternalIntegrationDTO; @@ -45,13 +49,17 @@ public JiraService(RestTemplate builder, IntegrationSecurityToken integration) t ExternalIntegrationDTO externalIntegrationDTO = JsonUtil.MAPPER.readValue(integration.getConnectionInfo(), ExternalIntegrationDTO.class); this.jiraBaseUrl = externalIntegrationDTO.getBaseUrl(); + if (null != jiraBaseUrl && !jiraBaseUrl.startsWith("https://")) { + jiraBaseUrl = "https://" + jiraBaseUrl; + } this.apiToken = externalIntegrationDTO.getApiToken(); this.username = externalIntegrationDTO.getUsername(); } public boolean isTicketActive(String ticketKey) { - String url = String.format("%s/rest/api/3/issue/%s", jiraBaseUrl, ticketKey); + String url = String.format("%s/rest/api/3/issue/%s", jiraBaseUrl, ticketKey); + log.info(url); HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(apiToken); //headers.setBasicAuth(username, apiToken); @@ -95,18 +103,21 @@ public Optional getUser(User user) throws JsonProcessingException { return Optional.empty(); } - public List searchForIncidents(String query) throws ExecutionException, InterruptedException { + public List searchForIncidents(String query) + throws ExecutionException, InterruptedException, JsonProcessingException { List ticketsFound = new ArrayList<>(); // Jira Search API endpoint String url = String.format("%s/rest/api/3/search", jiraBaseUrl); + // ✅ Decode query (in case it was encoded when passed via endpoint) + String decodedQuery = URLDecoder.decode(query, StandardCharsets.UTF_8); // JQL query to search by summary, description, or issue key boolean isIssueKey = query.matches("[A-Z]+-\\d+"); // Regex to match issue keys like "PROJECT-123" String jql = isIssueKey - ? String.format("(key = \"%s\" OR summary ~ \"%s\" OR description ~ \"%s\") ", query, query, query) - : String.format("(summary ~ \"%s\" OR description ~ \"%s\") ", query, query); + ? String.format("(key = \"%s\" OR summary ~ \"%s\" OR description ~ \"%s\") ", decodedQuery, decodedQuery, decodedQuery) + : String.format("%s", decodedQuery); log.info("Searching Jira with JQL: {}", jql); // Request body for Jira API @@ -133,7 +144,16 @@ public List searchForIncidents(String query) throws ExecutionExceptio String key = (String) issue.get("key"); Map fields = (Map) issue.get("fields"); String summary = (String) fields.get("summary"); - String description = (String) fields.get("description"); + Object descriptionRaw = fields.get("description"); + String description; + + if (descriptionRaw instanceof String) { + description = (String) descriptionRaw; + } else if (descriptionRaw != null) { + description = JsonUtil.MAPPER.writeValueAsString(descriptionRaw); // JSON blob fallback + } else { + description = ""; + } String status = (String) ((Map) fields.get("status")).get("name"); // Add to the result list @@ -215,4 +235,16 @@ public boolean updateTicket(String ticketKey, String message) { return false; } } + + public String extractTextFromADF(Object adf) { + try { + JsonNode root = JsonUtil.MAPPER.convertValue(adf, JsonNode.class); + return root.path("content") + .findValuesAsText("text") + .stream().collect(Collectors.joining(" ")); + } catch (Exception e) { + log.warn("Failed to parse ADF description", e); + return ""; + } + } } diff --git a/dataplane/src/main/java/io/sentrius/sso/core/integrations/ticketing/JiraVerbService.java b/dataplane/src/main/java/io/sentrius/sso/core/integrations/ticketing/JiraVerbService.java new file mode 100644 index 00000000..0e1c3ffa --- /dev/null +++ b/dataplane/src/main/java/io/sentrius/sso/core/integrations/ticketing/JiraVerbService.java @@ -0,0 +1,147 @@ +package io.sentrius.sso.core.integrations.ticketing; + +import java.util.List; +import java.util.Optional; + +import io.sentrius.sso.core.dto.TicketDTO; +import io.sentrius.sso.core.model.security.IntegrationSecurityToken; +import io.sentrius.sso.core.model.users.User; +import io.sentrius.sso.core.model.verbs.Verb; +import io.sentrius.sso.core.services.security.IntegrationSecurityTokenService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Service; + +/** + * Service that exposes JIRA operations as AI-callable verbs. + * This allows AI agents to discover and call JIRA functionality through the capabilities API. + */ +@Slf4j +@Service +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +public class JiraVerbService { + + private final TicketService ticketService; + private final IntegrationSecurityTokenService integrationService; + + public JiraVerbService(TicketService ticketService, IntegrationSecurityTokenService integrationService) { + this.ticketService = ticketService; + this.integrationService = integrationService; + } + + /** + * Searches for JIRA tickets based on a query string. + * This method is exposed as a Verb so AI agents can discover and call it. + * + * @param query The search query (can be JQL or simple text) + * @return List of tickets matching the query + */ + @Verb( + name = "searchForTickets", + description = "Search for JIRA tickets using a query string. Can use JQL or simple text search.", + returnType = List.class, + isAiCallable = true, + paramDescriptions = {"Search query string (JQL or simple text)"} + ) + public List searchForTickets(String query) { + log.info("Searching for tickets with query: {}", query); + + // Check if JIRA integration is available + if (!isJiraIntegrationAvailable()) { + log.warn("JIRA integration not available, returning empty results"); + return List.of(); + } + + return ticketService.searchForIncidents(query); + } + + /** + * Assigns a JIRA ticket to a user. + * This method is exposed as a Verb so AI agents can discover and call it. + * + * @param ticketKey The JIRA ticket key (e.g., "PROJ-123") + * @param user The user to assign the ticket to + * @return true if assignment was successful, false otherwise + */ + @Verb( + name = "assignTicket", + description = "Assign a JIRA ticket to a user", + returnType = Boolean.class, + isAiCallable = true, + paramDescriptions = {"JIRA ticket key (e.g., PROJ-123)", "User to assign the ticket to"} + ) + public Boolean assignTicket(String ticketKey, User user) { + log.info("Assigning ticket {} to user {}", ticketKey, user.getEmailAddress()); + + // Check if JIRA integration is available + if (!isJiraIntegrationAvailable()) { + log.warn("JIRA integration not available, cannot assign ticket"); + return false; + } + + return ticketService.assignJira(ticketKey, user); + } + + /** + * Updates a JIRA ticket with a comment. + * This method is exposed as a Verb so AI agents can discover and call it. + * + * @param ticketKey The JIRA ticket key (e.g., "PROJ-123") + * @param user The user adding the comment + * @param message The comment message + * @return true if update was successful, false otherwise + */ + @Verb( + name = "updateTicket", + description = "Add a comment to a JIRA ticket", + returnType = Boolean.class, + isAiCallable = true, + paramDescriptions = {"JIRA ticket key (e.g., PROJ-123)", "User adding the comment", "Comment message"} + ) + public Boolean updateTicket(String ticketKey, User user, String message) { + log.info("Updating ticket {} with comment from user {}", ticketKey, user.getEmailAddress()); + + // Check if JIRA integration is available + if (!isJiraIntegrationAvailable()) { + log.warn("JIRA integration not available, cannot update ticket"); + return false; + } + + return ticketService.updateJira(ticketKey, user, message); + } + + /** + * Checks if at least one JIRA integration is configured and available. + * This method is exposed as a Verb so AI agents can check JIRA availability. + * + * @return true if JIRA integration is available, false otherwise + */ + @Verb( + name = "isJiraAvailable", + description = "Check if JIRA integration is configured and available", + returnType = Boolean.class, + isAiCallable = true, + paramDescriptions = {} + ) + public Boolean isJiraAvailable() { + boolean available = isJiraIntegrationAvailable(); + log.info("JIRA integration availability check: {}", available); + return available; + } + + /** + * Helper method to check if JIRA integration is available. + * + * @return true if at least one JIRA integration is configured + */ + private boolean isJiraIntegrationAvailable() { + try { + List jiraIntegrations = integrationService.findByConnectionType("jira"); + return !jiraIntegrations.isEmpty(); + } catch (Exception e) { + log.error("Error checking JIRA integration availability", e); + return false; + } + } +} \ No newline at end of file diff --git a/dataplane/src/main/java/io/sentrius/sso/core/model/security/AccessControlAspect.java b/dataplane/src/main/java/io/sentrius/sso/core/model/security/AccessControlAspect.java index a9a4dd6f..5d333d58 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/model/security/AccessControlAspect.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/model/security/AccessControlAspect.java @@ -260,14 +260,14 @@ else if (atplPolicyService.allowsEndpoint(policy.get(), endpoint)) { // Get the required roles from the annotation for (var userAccess : accessAnnotation.userAccess()) { if (!canAccess(operatingUser, userAccess)) { - log.debug("Access Denied to {} at {}", operatingUser, userAccess); + log.debug("Access Denied to {} at {}, {}", operatingUser, userAccess, operatingUser.getAuthorizationType()); canAccess = false; break; } } for (var appAccess : accessAnnotation.applicationAccess()) { if (!canAccess(operatingUser, appAccess)) { - log.debug("Access Denied to {} at {}", operatingUser, appAccess); + log.debug("Access Denied to {} at {}, {}", operatingUser, appAccess, operatingUser.getAuthorizationType()); canAccess = false; break; } diff --git a/dataplane/src/main/java/io/sentrius/sso/core/repository/ATPLPolicyRepository.java b/dataplane/src/main/java/io/sentrius/sso/core/repository/ATPLPolicyRepository.java index fb1dae29..a131d74c 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/repository/ATPLPolicyRepository.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/repository/ATPLPolicyRepository.java @@ -3,13 +3,14 @@ import java.nio.channels.FileChannel; import java.util.Collection; import java.util.List; +import java.util.UUID; import io.sentrius.sso.core.model.ATPLPolicyEntity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository -public interface ATPLPolicyRepository extends JpaRepository { +public interface ATPLPolicyRepository extends JpaRepository { List findAllByPolicyId(String policyID); @Query(value = """ diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/ATPLPolicyService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/ATPLPolicyService.java index 539f5aea..80d1f973 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/ATPLPolicyService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/ATPLPolicyService.java @@ -6,9 +6,15 @@ import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.TimeUnit; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.github.benmanes.caffeine.cache.AsyncCache; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; import io.sentrius.sso.core.annotations.LimitAccess; import io.sentrius.sso.core.model.ATPLPolicyEntity; import io.sentrius.sso.core.model.AgentPolicyAssignment; @@ -31,7 +37,10 @@ public class ATPLPolicyService { private final ATPLPolicyRepository repository; private final AgentPolicyAssignmentRepository agentPolicyAssignmentRepository; private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); - + Cache policyCacheLoader = Caffeine.newBuilder() + .expireAfterWrite(30, TimeUnit.MINUTES) + .maximumSize(100) + .build(); @Transactional public ATPLPolicyEntity savePolicy(ATPLPolicy policy) { try { @@ -248,4 +257,25 @@ public List getAllPolicies() { .toList(); } + @Transactional + public boolean deletePolicyById(String id) { + log.info("Deleting policy with ID: {}", id); + List policyEntity = repository.findAllByPolicyId(id); + boolean deleted = false; + for(ATPLPolicyEntity entity : policyEntity) { + repository.delete(entity); + deleted = true; + log.info("Deleted policy with ID: {}", entity.getId()); + + } + return deleted; + } + + public void cachePolicy(String key, String policyId) { + policyCacheLoader.put(key,policyId); + } + + public String getCachedPolicy(String key) { + return policyCacheLoader.getIfPresent(key); + } } \ No newline at end of file diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/UserService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/UserService.java index 40a9261d..431bb889 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/UserService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/UserService.java @@ -391,6 +391,40 @@ public Optional getUserType(UserType baseUser) { return ret; } + + /** + * Retrieves a user type by its base user. + * + * @param userTypeName userTypeId + * @return An optional containing the user type if found, or empty if not found. + */ + public Optional getUserType(String userTypeName) { + if (userTypeName == null) { + log.warn("Attempted to get UserType with null baseUser or null ID"); + return Optional.empty(); + } + log.info("Getting user type for baseUser: {}", userTypeName); + var ret = userTypeRepository.findByUserTypeName(userTypeName); + log.info("Got user type for baseUser: {}", ret); + return ret; + } + /** + * Retrieves a user type by its base user. + * + * @param userTypeId userTypeId + * @return An optional containing the user type if found, or empty if not found. + */ + public Optional getUserType(Long userTypeId) { + if (userTypeId == null) { + log.warn("Attempted to get UserType with null baseUser or null ID"); + return Optional.empty(); + } + log.info("Getting user type for baseUser: {}", userTypeId); + var ret = userTypeRepository.findById(userTypeId); + log.info("Got user type for baseUser: {}", ret); + return ret; + } + /** * Validates a JWT token. * diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/agents/AgentContextService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/agents/AgentContextService.java index c4534f68..ca567c57 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/agents/AgentContextService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/agents/AgentContextService.java @@ -8,6 +8,7 @@ import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Slf4j @Service @@ -19,6 +20,7 @@ public AgentContextService(AgentContextRepository contextRepo) { this.contextRepo = contextRepo; } + @Transactional public AgentContext create(@NonNull AgentContextRequestDTO dto) { log.info("Creating AgentContext from {}", dto); AgentContext context = new AgentContext(); diff --git a/dataplane/src/test/java/io/sentrius/sso/core/integrations/ticketing/JiraVerbServiceTest.java b/dataplane/src/test/java/io/sentrius/sso/core/integrations/ticketing/JiraVerbServiceTest.java new file mode 100644 index 00000000..cba93655 --- /dev/null +++ b/dataplane/src/test/java/io/sentrius/sso/core/integrations/ticketing/JiraVerbServiceTest.java @@ -0,0 +1,202 @@ +package io.sentrius.sso.core.integrations.ticketing; + +import io.sentrius.sso.core.dto.TicketDTO; +import io.sentrius.sso.core.model.security.IntegrationSecurityToken; +import io.sentrius.sso.core.model.users.User; +import io.sentrius.sso.core.services.security.IntegrationSecurityTokenService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class JiraVerbServiceTest { + + @Mock + private TicketService ticketService; + + @Mock + private IntegrationSecurityTokenService integrationService; + + @InjectMocks + private JiraVerbService jiraVerbService; + + private User testUser; + private IntegrationSecurityToken mockIntegration; + + @BeforeEach + void setUp() { + testUser = new User(); + testUser.setEmailAddress("test@example.com"); + + mockIntegration = IntegrationSecurityToken.builder() + .connectionType("jira") + .name("Test JIRA Integration") + .connectionInfo("{}") + .build(); + } + + @Test + void testSearchForTickets_WithJiraAvailable() { + // Given + String query = "project = TEST"; + List expectedTickets = List.of( + TicketDTO.builder() + .id("TEST-1") + .summary("Test ticket") + .status("Open") + .type("jira") + .build() + ); + + when(integrationService.findByConnectionType("jira")).thenReturn(List.of(mockIntegration)); + when(ticketService.searchForIncidents(query)).thenReturn(expectedTickets); + + // When + List result = jiraVerbService.searchForTickets(query); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("TEST-1", result.get(0).getId()); + assertEquals("Test ticket", result.get(0).getSummary()); + + verify(integrationService).findByConnectionType("jira"); + verify(ticketService).searchForIncidents(query); + } + + @Test + void testSearchForTickets_WithoutJiraAvailable() { + // Given + String query = "project = TEST"; + when(integrationService.findByConnectionType("jira")).thenReturn(Collections.emptyList()); + + // When + List result = jiraVerbService.searchForTickets(query); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + + verify(integrationService).findByConnectionType("jira"); + verify(ticketService, never()).searchForIncidents(any()); + } + + @Test + void testAssignTicket_WithJiraAvailable() { + // Given + String ticketKey = "TEST-1"; + when(integrationService.findByConnectionType("jira")).thenReturn(List.of(mockIntegration)); + when(ticketService.assignJira(ticketKey, testUser)).thenReturn(true); + + // When + Boolean result = jiraVerbService.assignTicket(ticketKey, testUser); + + // Then + assertTrue(result); + + verify(integrationService).findByConnectionType("jira"); + verify(ticketService).assignJira(ticketKey, testUser); + } + + @Test + void testAssignTicket_WithoutJiraAvailable() { + // Given + String ticketKey = "TEST-1"; + when(integrationService.findByConnectionType("jira")).thenReturn(Collections.emptyList()); + + // When + Boolean result = jiraVerbService.assignTicket(ticketKey, testUser); + + // Then + assertFalse(result); + + verify(integrationService).findByConnectionType("jira"); + verify(ticketService, never()).assignJira(any(), any()); + } + + @Test + void testUpdateTicket_WithJiraAvailable() { + // Given + String ticketKey = "TEST-1"; + String message = "Test comment"; + when(integrationService.findByConnectionType("jira")).thenReturn(List.of(mockIntegration)); + when(ticketService.updateJira(ticketKey, testUser, message)).thenReturn(true); + + // When + Boolean result = jiraVerbService.updateTicket(ticketKey, testUser, message); + + // Then + assertTrue(result); + + verify(integrationService).findByConnectionType("jira"); + verify(ticketService).updateJira(ticketKey, testUser, message); + } + + @Test + void testUpdateTicket_WithoutJiraAvailable() { + // Given + String ticketKey = "TEST-1"; + String message = "Test comment"; + when(integrationService.findByConnectionType("jira")).thenReturn(Collections.emptyList()); + + // When + Boolean result = jiraVerbService.updateTicket(ticketKey, testUser, message); + + // Then + assertFalse(result); + + verify(integrationService).findByConnectionType("jira"); + verify(ticketService, never()).updateJira(any(), any(), any()); + } + + @Test + void testIsJiraAvailable_WithJiraConfigured() { + // Given + when(integrationService.findByConnectionType("jira")).thenReturn(List.of(mockIntegration)); + + // When + Boolean result = jiraVerbService.isJiraAvailable(); + + // Then + assertTrue(result); + + verify(integrationService).findByConnectionType("jira"); + } + + @Test + void testIsJiraAvailable_WithoutJiraConfigured() { + // Given + when(integrationService.findByConnectionType("jira")).thenReturn(Collections.emptyList()); + + // When + Boolean result = jiraVerbService.isJiraAvailable(); + + // Then + assertFalse(result); + + verify(integrationService).findByConnectionType("jira"); + } + + @Test + void testIsJiraAvailable_WithException() { + // Given + when(integrationService.findByConnectionType("jira")).thenThrow(new RuntimeException("Database error")); + + // When + Boolean result = jiraVerbService.isJiraAvailable(); + + // Then + assertFalse(result); + + verify(integrationService).findByConnectionType("jira"); + } +} \ No newline at end of file diff --git a/demo/ai-agent-jira-integration-demo.sh b/demo/ai-agent-jira-integration-demo.sh new file mode 100755 index 00000000..c5ab0db6 --- /dev/null +++ b/demo/ai-agent-jira-integration-demo.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# AI Agent JIRA Integration Demo Script +# This script demonstrates how AI agents can discover and call JIRA capabilities + +echo "=== AI Agent JIRA Integration Demo ===" +echo +echo "This demo shows how AI agents can discover and interact with JIRA capabilities" +echo "through the ai-agent module integration." +echo + +# Build the project +echo "1. Building ai-agent module with JIRA integration..." +mvn clean compile -pl ai-agent -am -q + +if [ $? -eq 0 ]; then + echo "✓ Build successful" +else + echo "✗ Build failed" + exit 1 +fi + +# Run tests +echo +echo "2. Running AI Agent JIRA integration tests..." +mvn test -pl ai-agent -am -q + +if [ $? -eq 0 ]; then + echo "✓ All tests passed" +else + echo "✗ Some tests failed" + exit 1 +fi + +echo +echo "3. AI Agent JIRA Capabilities Summary:" +echo " The ai-agent module now includes:" +echo " • AIAgentJiraIntegrationService - bridges ai-agent with JIRA capabilities" +echo " • AIAgentJiraVerbService - provides @Verb methods for AI agents" +echo " • VerbRegistry extended to scan JIRA verbs in dataplane module" +echo " • Comprehensive tests for all integration scenarios" +echo + +echo "4. Available JIRA Verbs for AI Agents:" +echo " • searchJiraTickets - Search for tickets using JQL or simple text" +echo " • assignJiraTicket - Assign tickets to users" +echo " • updateJiraTicket - Add comments to tickets" +echo " • checkJiraAvailability - Check if JIRA integration is configured" +echo + +echo "5. AI Agent Discovery Flow:" +echo " 1. AI Agent starts and calls VerbRegistry.scanClasspath()" +echo " 2. VerbRegistry discovers JIRA verbs from dataplane module" +echo " 3. AI Agent can check JIRA availability: checkJiraAvailability()" +echo " 4. AI Agent can search tickets: searchJiraTickets(query)" +echo " 5. AI Agent can assign tickets: assignJiraTicket(ticketKey, user)" +echo " 6. AI Agent can update tickets: updateJiraTicket(ticketKey, user, message)" +echo + +echo "6. Integration Features:" +echo " • Conditional loading - only available when JIRA is configured" +echo " • Graceful degradation - returns empty results when JIRA unavailable" +echo " • Zero breaking changes - builds on existing verb system" +echo " • AI-callable verbs - all marked with isAiCallable = true" +echo + +echo "✓ AI Agent JIRA Integration Demo Complete" +echo +echo "The ai-agent module now has flexible access to JIRA capabilities!" \ No newline at end of file diff --git a/demo/jira-capabilities-demo.sh b/demo/jira-capabilities-demo.sh new file mode 100755 index 00000000..007297e2 --- /dev/null +++ b/demo/jira-capabilities-demo.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +# JIRA Capabilities Integration Demonstration Script +# This script demonstrates the AI agent workflow for discovering and using JIRA capabilities + +echo "===========================================" +echo "JIRA Capabilities Integration Demonstration" +echo "===========================================" +echo + +echo "This demonstrates how an AI agent would discover and use JIRA capabilities:" +echo + +echo "1. AI Agent discovers available capabilities:" +echo " GET /api/v1/capabilities/verbs" +echo " -> Returns list of all AI-callable verb methods" +echo + +echo "2. AI Agent finds JIRA-related verbs in the response:" +echo " - searchForTickets: Search for JIRA tickets using query" +echo " - assignTicket: Assign a ticket to a user" +echo " - updateTicket: Add a comment to a ticket" +echo " - isJiraAvailable: Check if JIRA integration is configured" +echo + +echo "3. AI Agent checks JIRA availability:" +echo " CALL isJiraAvailable()" +echo " -> Returns true if JIRA integration is configured" +echo + +echo "4. AI Agent searches for tickets:" +echo " CALL searchForTickets('project = SUPPORT AND status = Open')" +echo " -> Returns list of matching tickets:" +echo " [{" +echo " \"id\": \"SUPPORT-123\"," +echo " \"summary\": \"Login issue reported\"," +echo " \"status\": \"Open\"," +echo " \"type\": \"jira\"" +echo " }]" +echo + +echo "5. AI Agent assigns a ticket:" +echo " CALL assignTicket('SUPPORT-123', currentUser)" +echo " -> Returns true if assignment successful" +echo + +echo "6. AI Agent adds a comment:" +echo " CALL updateTicket('SUPPORT-123', currentUser, 'Investigating this issue')" +echo " -> Returns true if comment added successfully" +echo + +echo "===========================================" +echo "Implementation Benefits:" +echo "===========================================" +echo "✓ AI agents can discover JIRA capabilities automatically" +echo "✓ Works whether JIRA is configured or not" +echo "✓ Uses existing security and authentication" +echo "✓ Minimal changes to existing codebase" +echo "✓ Consistent with other Sentrius capabilities" +echo + +echo "===========================================" +echo "Key Components Implemented:" +echo "===========================================" +echo "✓ JiraVerbService - Exposes JIRA operations as @Verb methods" +echo "✓ Conditional availability checking" +echo "✓ AI-callable annotations (isAiCallable = true)" +echo "✓ Integration with existing CapabilitiesApiController" +echo "✓ Comprehensive unit tests" +echo "✓ Documentation and examples" +echo + +echo "Ready for AI agent integration!" \ No newline at end of file diff --git a/docs/AI_AGENT_JIRA_INTEGRATION.md b/docs/AI_AGENT_JIRA_INTEGRATION.md new file mode 100644 index 00000000..35e5dfab --- /dev/null +++ b/docs/AI_AGENT_JIRA_INTEGRATION.md @@ -0,0 +1,268 @@ +# AI Agent JIRA Integration Guide + +## Overview + +The AI Agent JIRA integration enables AI agents to flexibly discover and call JIRA capabilities through the existing verb system. This integration extends the `ai-agent` module to work with JIRA operations from the `dataplane` module. + +## Problem Solved + +Previously, the JIRA capabilities were only available in the `dataplane` module through the `JiraVerbService`, but AI agents in the `ai-agent` module couldn't access them. This integration bridges that gap by: + +1. **Adding dataplane dependency** to the ai-agent module +2. **Extending VerbRegistry** to scan JIRA verbs from the dataplane +3. **Creating integration services** that bridge ai-agent with JIRA capabilities +4. **Providing AI-callable verbs** that agents can discover and use + +## Architecture + +``` +ai-agent module +├── AIAgentJiraIntegrationService (bridges ai-agent with JIRA) +├── AIAgentJiraVerbService (provides @Verb methods) +├── VerbRegistry (extended to scan dataplane JIRA verbs) +└── Tests (comprehensive integration tests) + +dataplane module +└── JiraVerbService (core JIRA operations) +``` + +## Integration Components + +### 1. AIAgentJiraIntegrationService + +**Purpose**: Bridges the ai-agent module with JIRA capabilities from the dataplane module. + +**Features**: +- Delegates to `JiraVerbService` for actual JIRA operations +- Provides logging for AI agent actions +- Conditionally loaded only when JIRA is available + +**Methods**: +- `searchForTickets(String query)` - Search for tickets +- `assignTicket(String ticketKey, User user)` - Assign tickets +- `updateTicket(String ticketKey, User user, String message)` - Update tickets +- `isJiraAvailable()` - Check JIRA availability + +### 2. AIAgentJiraVerbService + +**Purpose**: Provides @Verb methods that AI agents can discover and call. + +**Features**: +- All methods marked with `@Verb` annotation +- `isAiCallable = true` for AI agent discovery +- Parameter validation and error handling +- Uses `AgentExecution` context for security + +**Available Verbs**: +- `searchJiraTickets` - Search for JIRA tickets +- `assignJiraTicket` - Assign tickets to users +- `updateJiraTicket` - Add comments to tickets +- `checkJiraAvailability` - Check JIRA integration status + +### 3. Extended VerbRegistry + +**Purpose**: Enhanced to discover JIRA verbs from the dataplane module. + +**Changes**: +- Added `"io.sentrius.sso.core.integrations.ticketing"` to scan packages +- Now discovers both ai-agent and dataplane verbs +- Maintains backward compatibility + +## Usage Examples + +### 1. Check JIRA Availability + +```java +// AI Agent can check if JIRA is configured +Map args = new HashMap<>(); +Boolean isAvailable = aiAgentJiraVerbService.checkJiraAvailability(execution, args); + +if (isAvailable) { + // Proceed with JIRA operations + log.info("JIRA integration is available"); +} else { + // Handle gracefully + log.warn("JIRA integration not configured"); +} +``` + +### 2. Search for Tickets + +```java +// Search using JQL +Map args = new HashMap<>(); +args.put("query", "project = SUPPORT AND status = Open"); + +List tickets = aiAgentJiraVerbService.searchJiraTickets(execution, args); +log.info("Found {} tickets", tickets.size()); +``` + +### 3. Assign Tickets + +```java +// Assign ticket to user +Map args = new HashMap<>(); +args.put("ticketKey", "SUPPORT-123"); +args.put("user", currentUser); + +Boolean success = aiAgentJiraVerbService.assignJiraTicket(execution, args); +if (success) { + log.info("Ticket assigned successfully"); +} +``` + +### 4. Update Tickets + +```java +// Add comment to ticket +Map args = new HashMap<>(); +args.put("ticketKey", "SUPPORT-123"); +args.put("user", currentUser); +args.put("message", "Working on this issue"); + +Boolean success = aiAgentJiraVerbService.updateJiraTicket(execution, args); +if (success) { + log.info("Ticket updated successfully"); +} +``` + +## AI Agent Discovery Flow + +### 1. Startup Discovery + +```java +// During AI agent startup +VerbRegistry verbRegistry = new VerbRegistry(...); +verbRegistry.scanClasspath(); // Discovers both ai-agent and JIRA verbs + +// Check what verbs are available +Map verbs = verbRegistry.getVerbs(); +log.info("Available verbs: {}", verbs.keySet()); +``` + +### 2. Runtime Capability Check + +```java +// AI agent can check capabilities at runtime +if (verbRegistry.isVerbRegistered("checkJiraAvailability")) { + // JIRA integration is available + Boolean jiraAvailable = verbRegistry.execute(execution, null, "checkJiraAvailability", null); + + if (jiraAvailable) { + // Proceed with JIRA operations + verbRegistry.execute(execution, null, "searchJiraTickets", searchArgs); + } +} +``` + +## Configuration + +### Dependencies + +The ai-agent module now includes: + +```xml + + io.sentrius + sentrius-dataplane + 1.0.0-SNAPSHOT + +``` + +### Conditional Loading + +Services are conditionally loaded: + +```java +@ConditionalOnBean(JiraVerbService.class) +public class AIAgentJiraIntegrationService { + // Only loaded when JIRA is configured +} +``` + +## Testing + +### Unit Tests + +- **AIAgentJiraIntegrationServiceTest**: Tests the integration service +- **AIAgentJiraVerbServiceTest**: Tests the verb service with various scenarios +- **VerbRegistryJiraIntegrationTest**: Tests verb discovery + +### Test Coverage + +- ✓ Successful JIRA operations +- ✓ Error handling and validation +- ✓ Graceful degradation when JIRA unavailable +- ✓ Verb discovery and registration +- ✓ Parameter validation and edge cases + +### Running Tests + +```bash +# Run all ai-agent tests +mvn test -pl ai-agent -am + +# Run specific test class +mvn test -pl ai-agent -Dtest=AIAgentJiraVerbServiceTest +``` + +## Benefits + +### 1. Flexible Integration + +- AI agents can now discover and call JIRA capabilities +- No hardcoded dependencies on JIRA +- Graceful handling when JIRA is not configured + +### 2. Zero Breaking Changes + +- Uses existing verb system +- Maintains backward compatibility +- Builds on established patterns + +### 3. Comprehensive Testing + +- Full test coverage for all scenarios +- Mock-based testing for reliable results +- Integration tests for end-to-end validation + +### 4. Enterprise-Ready + +- Proper error handling and logging +- Security through AgentExecution context +- Conditional loading based on configuration + +## Troubleshooting + +### Common Issues + +1. **JIRA verbs not discovered** + - Ensure dataplane dependency is added + - Check VerbRegistry scan packages include dataplane + +2. **JIRA operations return false** + - Check if JIRA integration is configured + - Use `checkJiraAvailability()` to verify + +3. **Parameter validation failures** + - Ensure all required parameters are provided + - Check parameter types match expected values + +### Debug Logging + +Enable debug logging to see verb discovery and execution: + +```properties +logging.level.io.sentrius.agent.analysis.agents=DEBUG +``` + +## Future Enhancements + +1. **Dynamic Verb Registration**: Allow runtime registration of new JIRA verbs +2. **Caching**: Cache JIRA availability checks for performance +3. **Bulk Operations**: Support for bulk ticket operations +4. **Advanced Queries**: Support for complex JQL queries with parameters + +## Conclusion + +The AI Agent JIRA integration provides a flexible, enterprise-ready solution for AI agents to interact with JIRA capabilities. By building on the existing verb system and maintaining backward compatibility, it enables powerful new use cases while preserving system stability. \ No newline at end of file diff --git a/docs/JIRA_CAPABILITIES_INTEGRATION.md b/docs/JIRA_CAPABILITIES_INTEGRATION.md new file mode 100644 index 00000000..4c07fd65 --- /dev/null +++ b/docs/JIRA_CAPABILITIES_INTEGRATION.md @@ -0,0 +1,142 @@ +# JIRA Capabilities Integration for AI Agents + +This implementation enables AI agents to discover and call JIRA integration capabilities through the Sentrius capability discovery system. + +## Architecture + +The solution consists of several components working together: + +1. **JiraVerbService** - Exposes JIRA operations as `@Verb` methods that AI agents can discover and call +2. **CapabilitiesApiController** - Provides REST endpoints for capability discovery +3. **EndpointScanningService** - Scans the application for available capabilities including JIRA verbs +4. **Existing JIRA Infrastructure** - TicketService and JiraService provide the underlying JIRA integration + +## JIRA Verb Methods + +The `JiraVerbService` exposes the following AI-callable methods: + +### `searchForTickets(String query)` +- **Description**: Search for JIRA tickets using a query string. Can use JQL or simple text search. +- **Parameters**: Search query string (JQL or simple text) +- **Returns**: List of TicketDTO objects +- **AI Callable**: Yes + +### `assignTicket(String ticketKey, User user)` +- **Description**: Assign a JIRA ticket to a user +- **Parameters**: JIRA ticket key (e.g., PROJ-123), User to assign the ticket to +- **Returns**: Boolean (true if successful) +- **AI Callable**: Yes + +### `updateTicket(String ticketKey, User user, String message)` +- **Description**: Add a comment to a JIRA ticket +- **Parameters**: JIRA ticket key, User adding the comment, Comment message +- **Returns**: Boolean (true if successful) +- **AI Callable**: Yes + +### `isJiraAvailable()` +- **Description**: Check if JIRA integration is configured and available +- **Parameters**: None +- **Returns**: Boolean (true if JIRA is available) +- **AI Callable**: Yes + +## Conditional Availability + +All JIRA verbs include logic to check if JIRA integration is available before attempting operations: + +```java +private boolean isJiraIntegrationAvailable() { + try { + List jiraIntegrations = integrationService.findByConnectionType("jira"); + return !jiraIntegrations.isEmpty(); + } catch (Exception e) { + log.error("Error checking JIRA integration availability", e); + return false; + } +} +``` + +This ensures that: +- JIRA verbs are always discoverable (listed in capabilities) +- JIRA verbs gracefully handle the case when no JIRA integration is configured +- AI agents can call `isJiraAvailable()` to check availability before attempting operations + +## AI Agent Discovery + +AI agents can discover JIRA capabilities through the existing capabilities API: + +### 1. Query All Capabilities +```http +GET /api/v1/capabilities/endpoints +``` + +This returns all available endpoints including JIRA verbs. + +### 2. Query Only Verb Methods (AI-focused) +```http +GET /api/v1/capabilities/verbs +``` + +This returns only `@Verb` methods that are marked as AI-callable. + +### 3. Filter for JIRA Capabilities +The response will include JIRA verbs with metadata like: + +```json +{ + "name": "searchForTickets", + "description": "Search for JIRA tickets using a query string. Can use JQL or simple text search.", + "type": "VERB", + "className": "io.sentrius.sso.core.integrations.ticketing.JiraVerbService", + "methodName": "searchForTickets", + "parameters": [ + { + "name": "query", + "description": "Search query string (JQL or simple text)", + "type": "class java.lang.String", + "required": true, + "source": "METHOD_PARAM" + } + ], + "returnType": "interface java.util.List", + "metadata": { + "isAiCallable": true, + "outputInterpreter": "io.sentrius.sso.core.model.verbs.DefaultInterpreter", + "inputInterpreter": "io.sentrius.sso.core.model.verbs.DefaultInterpreter" + } +} +``` + +## Usage Example + +An AI agent workflow might look like: + +1. **Discovery**: Call `/api/v1/capabilities/verbs` to discover available capabilities +2. **Check Availability**: Call `isJiraAvailable()` to verify JIRA is configured +3. **Search Tickets**: Call `searchForTickets("project = SUPPORT AND status = Open")` to find tickets +4. **Assign Ticket**: Call `assignTicket("SUPPORT-123", currentUser)` to assign a ticket +5. **Add Comment**: Call `updateTicket("SUPPORT-123", currentUser, "Working on this issue")` to update + +## Integration with Existing Systems + +This implementation leverages existing Sentrius infrastructure: + +- **Security**: Uses existing `@LimitAccess` annotations and authentication +- **JIRA Service**: Delegates to existing `TicketService` and `JiraService` classes +- **Discovery**: Integrates with existing `EndpointScanningService` and `CapabilitiesApiController` +- **Configuration**: Works with existing JIRA integration configuration + +## Testing + +The implementation includes comprehensive unit tests: + +- **JiraVerbServiceTest**: Tests all verb methods with mocked dependencies +- **Conditional Logic**: Verifies behavior when JIRA is/isn't available +- **Error Handling**: Tests exception scenarios + +## Benefits + +1. **AI Discoverability**: JIRA capabilities are automatically discoverable by AI agents +2. **Graceful Degradation**: Works whether JIRA is configured or not +3. **Consistent Interface**: Uses the same patterns as other Sentrius capabilities +4. **Minimal Changes**: Builds on existing infrastructure without breaking changes +5. **Security**: Maintains existing security and access control patterns \ No newline at end of file diff --git a/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/IntegrationCapabilitiesApiController.java b/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/IntegrationCapabilitiesApiController.java new file mode 100644 index 00000000..fd90511d --- /dev/null +++ b/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/IntegrationCapabilitiesApiController.java @@ -0,0 +1,136 @@ +package io.sentrius.sso.controllers.api; + +import java.util.List; +import java.util.stream.Collectors; +import io.sentrius.sso.core.annotations.LimitAccess; +import io.sentrius.sso.core.config.SystemOptions; +import io.sentrius.sso.core.controllers.BaseController; +import io.sentrius.sso.core.dto.capabilities.EndpointDescriptor; +import io.sentrius.sso.core.model.security.enums.ApplicationAccessEnum; +import io.sentrius.sso.core.services.ErrorOutputService; +import io.sentrius.sso.core.services.UserService; +import io.sentrius.sso.core.services.capabilities.EndpointScanningService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * API controller for exposing endpoint capabilities across the system. + * This provides a unified view of all REST endpoints and Verb methods available. + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/capabilities") +public class IntegrationCapabilitiesApiController extends BaseController { + + private final EndpointScanningService endpointScanningService; + + public IntegrationCapabilitiesApiController( + UserService userService, + SystemOptions systemOptions, + ErrorOutputService errorOutputService, + EndpointScanningService endpointScanningService) { + super(userService, systemOptions, errorOutputService); + this.endpointScanningService = endpointScanningService; + } + + /** + * Returns all available endpoints (REST and Verb) in the system. + * This can be used by AI agents and other systems to understand what capabilities are available. + */ + @GetMapping("/endpoints") + @LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN}) + public ResponseEntity> getAllEndpoints( + @RequestParam(required = false) String type, + @RequestParam(required = false) Boolean requiresAuth, + HttpServletRequest request, + HttpServletResponse response) { + + log.info("Retrieving all endpoints with filters - type: {}, requiresAuth: {}", type, requiresAuth); + + endpointScanningService.disableVerbScanning(); + + List endpoints = endpointScanningService.getAllEndpoints(); + + // Apply filters if provided + if (type != null) { + endpoints = endpoints.stream() + .filter(endpoint -> type.equalsIgnoreCase(endpoint.getType())) + .collect(Collectors.toList()); + } + + if (requiresAuth != null) { + endpoints = endpoints.stream() + .filter(endpoint -> endpoint.isRequiresAuthentication() == requiresAuth) + .collect(Collectors.toList()); + } + + log.info("Returning {} endpoints", endpoints.size()); + return ResponseEntity.ok(endpoints); + } + + /** + * Returns only REST API endpoints. + */ + @GetMapping("/rest") + @LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN}) + public ResponseEntity> getRestEndpoints( + HttpServletRequest request, + HttpServletResponse response) { + + log.info("Retrieving REST endpoints"); + + List endpoints = endpointScanningService.getAllEndpoints() + .stream() + .filter(endpoint -> "REST".equals(endpoint.getType())) + .collect(Collectors.toList()); + + log.info("Returning {} REST endpoints", endpoints.size()); + return ResponseEntity.ok(endpoints); + } + + /** + * Returns only Verb methods (for AI agents). + */ + @GetMapping("/verbs") + @LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN}) + public ResponseEntity> getVerbEndpoints( + HttpServletRequest request, + HttpServletResponse response) { + + log.info("Retrieving Verb endpoints"); + + List endpoints = endpointScanningService.getAllEndpoints() + .stream() + .filter(endpoint -> "VERB".equals(endpoint.getType())) + .collect(Collectors.toList()); + + log.info("Returning {} Verb endpoints", endpoints.size()); + return ResponseEntity.ok(endpoints); + } + + /** + * Forces a refresh of the endpoint cache. + * This can be useful during development or after deploying new capabilities. + */ + @GetMapping("/refresh") + @LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_MANAGE_APPLICATION}) + public ResponseEntity refreshEndpoints( + HttpServletRequest request, + HttpServletResponse response) { + + log.info("Refreshing endpoint cache"); + endpointScanningService.refreshEndpoints(); + + int count = endpointScanningService.getAllEndpoints().size(); + String message = String.format("Endpoint cache refreshed. Found %d endpoints.", count); + + log.info(message); + return ResponseEntity.ok(message); + } +} \ No newline at end of file diff --git a/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/JiraProxyController.java b/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/JiraProxyController.java index 78393b1a..cfa53b63 100644 --- a/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/JiraProxyController.java +++ b/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/JiraProxyController.java @@ -60,7 +60,7 @@ protected JiraProxyController( @GetMapping("/rest/api/3/search") @LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN}) - public ResponseEntity search( + public ResponseEntity searchForJiraIssue( @RequestHeader("Authorization") String token, @RequestParam(value = "jql", required = false) String jql, @RequestParam(value = "query", required = false) String query, @@ -115,11 +115,11 @@ public ResponseEntity search( } } - @GetMapping("/rest/api/3/issue/{issueKey}") + @GetMapping("/rest/api/3/issue") @LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN}) - public ResponseEntity getIssue( + public ResponseEntity fetchJiraIssue( @RequestHeader("Authorization") String token, - @PathVariable String issueKey, + @RequestParam(name = "issueKey") String issueKey, HttpServletRequest request, HttpServletResponse response ) throws JsonProcessingException, HttpException { @@ -160,11 +160,11 @@ public ResponseEntity getIssue( } } - @PostMapping("/rest/api/3/issue/{issueKey}/comment") + @PostMapping("/rest/api/3/issue/comment") @LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN}) public ResponseEntity addComment( @RequestHeader("Authorization") String token, - @PathVariable String issueKey, + @RequestParam(name="issueKey") String issueKey, @RequestBody CommentRequest commentRequest, HttpServletRequest request, HttpServletResponse response @@ -217,11 +217,11 @@ public ResponseEntity addComment( } } - @PutMapping("/rest/api/3/issue/{issueKey}/assignee") + @PutMapping("/rest/api/3/issue/assignee") @LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN}) - public ResponseEntity assignIssue( + public ResponseEntity assignJiraIssue( @RequestHeader("Authorization") String token, - @PathVariable String issueKey, + @RequestParam(name="issueKey") String issueKey, @RequestBody AssigneeRequest assigneeRequest, HttpServletRequest request, HttpServletResponse response diff --git a/integration-proxy/src/main/java/io/sentrius/sso/mcp/service/MCPProxyService.java b/integration-proxy/src/main/java/io/sentrius/sso/mcp/service/MCPProxyService.java index f973fbea..de15d4bc 100644 --- a/integration-proxy/src/main/java/io/sentrius/sso/mcp/service/MCPProxyService.java +++ b/integration-proxy/src/main/java/io/sentrius/sso/mcp/service/MCPProxyService.java @@ -8,7 +8,7 @@ import io.sentrius.sso.core.services.agents.ZeroTrustClientService; import io.sentrius.sso.core.dto.UserDTO; import io.sentrius.sso.core.dto.ztat.TokenDTO; -import io.sentrius.sso.core.dto.ztat.AgentExecution; +import io.sentrius.sso.core.dto.agents.AgentExecution; import io.sentrius.sso.core.exceptions.ZtatException; import io.sentrius.sso.mcp.model.MCPRequest; import io.sentrius.sso.mcp.model.MCPResponse; @@ -20,11 +20,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.HttpServerErrorException; import java.time.Instant; import java.util.Map; diff --git a/integration-proxy/src/test/java/io/sentrius/sso/controllers/api/JiraProxyControllerTest.java b/integration-proxy/src/test/java/io/sentrius/sso/controllers/api/JiraProxyControllerTest.java index f2f3fae7..1abb65ef 100644 --- a/integration-proxy/src/test/java/io/sentrius/sso/controllers/api/JiraProxyControllerTest.java +++ b/integration-proxy/src/test/java/io/sentrius/sso/controllers/api/JiraProxyControllerTest.java @@ -75,7 +75,7 @@ void searchReturnsUnauthorizedWhenTokenIsInvalid() throws Exception { when(keycloakService.validateJwt("invalid-token")).thenReturn(false); // When - ResponseEntity result = jiraProxyController.search( + ResponseEntity result = jiraProxyController.searchForJiraIssue( invalidToken, "test query", null, request, response ); @@ -94,7 +94,7 @@ void searchReturnsNotFoundWhenNoJiraIntegrationConfigured() throws Exception { .thenReturn(Collections.emptyList()); // When - ResponseEntity result = jiraProxyController.search( + ResponseEntity result = jiraProxyController.searchForJiraIssue( validToken, "test query", null, request, response ); @@ -118,7 +118,7 @@ void searchReturnsBadRequestWhenNoQueryProvided() throws Exception { .thenReturn(Arrays.asList(mockToken)); // When - ResponseEntity result = jiraProxyController.search( + ResponseEntity result = jiraProxyController.searchForJiraIssue( validToken, null, null, request, response ); @@ -134,7 +134,7 @@ void getIssueReturnsUnauthorizedWhenTokenIsInvalid() throws Exception { when(keycloakService.validateJwt("invalid-token")).thenReturn(false); // When - ResponseEntity result = jiraProxyController.getIssue( + ResponseEntity result = jiraProxyController.fetchJiraIssue( invalidToken, "TEST-123", request, response ); @@ -153,7 +153,7 @@ void getIssueReturnsNotFoundWhenNoJiraIntegrationConfigured() throws Exception { .thenReturn(Collections.emptyList()); // When - ResponseEntity result = jiraProxyController.getIssue( + ResponseEntity result = jiraProxyController.fetchJiraIssue( validToken, "TEST-123", request, response ); @@ -191,7 +191,7 @@ void assignIssueReturnsUnauthorizedWhenTokenIsInvalid() throws Exception { assigneeRequest.setAccountId("test-account-id"); // When - ResponseEntity result = jiraProxyController.assignIssue( + ResponseEntity result = jiraProxyController.assignJiraIssue( invalidToken, "TEST-123", assigneeRequest, request, response ); diff --git a/python-agent/tests/test_config_manager.py b/python-agent/tests/test_config_manager.py index 74a6d756..8acc85b6 100644 --- a/python-agent/tests/test_config_manager.py +++ b/python-agent/tests/test_config_manager.py @@ -1,3 +1,4 @@ + """ Test configuration manager functionality. """ diff --git a/sentrius-chart-launcher/templates/configmap.yaml b/sentrius-chart-launcher/templates/configmap.yaml index ae9c230f..7464b54b 100644 --- a/sentrius-chart-launcher/templates/configmap.yaml +++ b/sentrius-chart-launcher/templates/configmap.yaml @@ -30,16 +30,12 @@ data: chat-atpl-helper.yaml: | description: "Agent that handles logs and OpenAI access." context: | - You help launch agents. User will define agent responsibilities that will fill in the agent context. Create the - trust policy for the agent, and - name its policy id after the agent name. If the user has a trust policy ID, use it, but don't ask for it. - Query for schema of the trust policy and ask the user to fill it - required parts if they ask to create one, but don't ask them about the policy unless they bring it up. Save it, - then launch the agent with the defined - responsibilities and - using the trust policy ID. If you need to define the trust policy system endpoints are available for query. - You can query status of the agent launch and return it to the user. Validate the agent launch by checking if the agent is running. - regex for agent names: [a-z]([-a-z0-9]*[a-z0-9])? + You help launch agents. User will define agent responsibilities that will fill in the agent context. + You can query status of the agent launch and return it to the user after you create them. Validate the agent + launch + by checking + if the agent is running. + regex for agent names: [a-z]([-a-z0-9]*[a-z0-9])? . use hyphens instead of underscores in agent name. launcher.properties: | spring.main.web-application-type=servlet spring.thymeleaf.enabled=false diff --git a/sentrius-chart/templates/configmap.yaml b/sentrius-chart/templates/configmap.yaml index 4f7fe741..7bbefdc3 100644 --- a/sentrius-chart/templates/configmap.yaml +++ b/sentrius-chart/templates/configmap.yaml @@ -371,8 +371,10 @@ data: spring.kafka.bootstrap-servers=sentrius-kafka:9092 sentrius.agent.launcher.service=http://sentrius-agents-launcherservice:8080/ sentrius.agent.register.bootstrap.allow=true - sentrius.agent.bootstrap.policy=default-policy.yaml + sentrius.agent.bootstrap.policy=/config/default-policy.yaml agentproxy.externalUrl={{ .Values.agentproxyDomain }} + integrationproxy.externalUrl=http://sentrius-integrationproxy:8080/ + sentrius.integration.proxyUrl=http://sentrius-integrationproxy:8080/ default-policy.yaml: | --- version: "v0" @@ -397,8 +399,27 @@ data: primitives: - id: "accessLLM" description: "access llm" - endpoint: + endpoints: - "/api/v1/chat/completions" + - id: "endpoints" + description: "endpoints " + endpoints: + - "/api/v1/capabilities/endpoints" + - id: "registration" + description: "registration " + endpoints: + - "/api/v1/agent/bootstrap/register" + - id: "verbs" + description: "verb endpoint " + endpoints: + - "/api/v1/capabilities/verbs" + - id: "createAgent" + description: "Create agent " + endpoints: + - "/api/v1/agent/context" + - "/api/v1/agent/bootstrap/launcher/create" + - "/api/v1/agent/bootstrap/launcher/status" + - "/api/v1/capabilities/endpoints" composed: ztat: provider: "ztat-service.internal"
-