diff --git a/apps/java-spring-ai-agents/backoffice/expense/ExpenseService.java b/apps/java-spring-ai-agents/backoffice/expense/ExpenseService.java index 3b5ffb22..e4516db4 100644 --- a/apps/java-spring-ai-agents/backoffice/expense/ExpenseService.java +++ b/apps/java-spring-ai-agents/backoffice/expense/ExpenseService.java @@ -77,4 +77,13 @@ public Expense getExpense(String expenseReference) { .items().stream().findFirst() .orElseThrow(() -> new ResourceNotFoundException("Expense", expenseReference)); } + + public Expense deleteExpense(String expenseReference) { + Expense expense = getExpense(expenseReference); + if (expense.getStatus() != Expense.ExpenseStatus.DRAFT) { + throw new InvalidOperationException("Only draft expenses can be deleted"); + } + dynamoDbTemplate.delete(expense); + return expense; + } } diff --git a/apps/java-spring-ai-agents/backoffice/tools/ExpenseTools.java b/apps/java-spring-ai-agents/backoffice/tools/ExpenseTools.java index 39a00973..20e848df 100644 --- a/apps/java-spring-ai-agents/backoffice/tools/ExpenseTools.java +++ b/apps/java-spring-ai-agents/backoffice/tools/ExpenseTools.java @@ -61,4 +61,10 @@ public Expense submitExpense( @ToolParam(description = "Expense reference (EXP-XXXXXXXX)") String expenseReference) { return service.submitExpense(expenseReference); } + + @Tool(description = "Delete a draft expense") + public Expense deleteExpense( + @ToolParam(description = "Expense reference (EXP-XXXXXXXX)") String expenseReference) { + return service.deleteExpense(expenseReference); + } } diff --git a/apps/java-spring-ai-agents/scripts/13-mcp-policy.sh b/apps/java-spring-ai-agents/scripts/13-mcp-policy.sh new file mode 100755 index 00000000..553bedc9 --- /dev/null +++ b/apps/java-spring-ai-agents/scripts/13-mcp-policy.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Deploy Cedar policy to MCP Gateway +# Requires AWS CLI 2.32+ +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Load environment +if [ -f "$SCRIPT_DIR/.env" ]; then + source "$SCRIPT_DIR/.env" +fi + +GATEWAY_ARN="arn:aws:bedrock-agentcore:${AWS_REGION}:${ACCOUNT_ID}:gateway/${MCP_GATEWAY_ID}" +POLICY_FILE="${SCRIPT_DIR}/backoffice-policy.cedar" + +echo "Deploying Cedar policy..." +echo " Gateway: ${MCP_GATEWAY_ID}" +echo " Policy Engine: ${MCP_POLICY_ENGINE_ID}" +echo "" + +# Read and substitute policy +POLICY_STATEMENT=$(sed "s|\${GATEWAY_ARN}|${GATEWAY_ARN}|g" "$POLICY_FILE") + +# Delete existing policies and wait for deletion +echo "1. Cleaning up existing policies..." +EXISTING=$(aws bedrock-agentcore-control list-policies \ + --policy-engine-id "${MCP_POLICY_ENGINE_ID}" \ + --query 'policies[].policyId' --output text 2>/dev/null || true) + +for PID in $EXISTING; do + echo " Deleting: $PID" + aws bedrock-agentcore-control delete-policy \ + --policy-engine-id "${MCP_POLICY_ENGINE_ID}" \ + --policy-id "$PID" > /dev/null + + # Wait for deletion to complete + while true; do + STATUS=$(aws bedrock-agentcore-control get-policy \ + --policy-engine-id "${MCP_POLICY_ENGINE_ID}" \ + --policy-id "$PID" \ + --query 'status' --output text 2>/dev/null || echo "DELETED") + + if [ "$STATUS" = "DELETED" ] || [ -z "$STATUS" ]; then + break + fi + sleep 2 + done +done + +# Create new policy +echo "" +echo "2. Creating policy..." +POLICY_ID=$(aws bedrock-agentcore-control create-policy \ + --policy-engine-id "${MCP_POLICY_ENGINE_ID}" \ + --name "ForbidDangerousOperations" \ + --validation-mode "IGNORE_ALL_FINDINGS" \ + --definition "{\"cedar\":{\"statement\":$(echo "$POLICY_STATEMENT" | jq -Rs .)}}" \ + --query 'policyId' --output text) + +echo " Policy ID: $POLICY_ID" + +# Wait for ACTIVE +echo "" +echo "3. Waiting for policy to become ACTIVE..." +for i in {1..30}; do + STATUS=$(aws bedrock-agentcore-control get-policy \ + --policy-engine-id "${MCP_POLICY_ENGINE_ID}" \ + --policy-id "$POLICY_ID" \ + --query 'status' --output text) + + echo " Status: $STATUS" + + if [ "$STATUS" = "ACTIVE" ]; then + echo "" + echo "✅ Policy deployed successfully!" + exit 0 + elif [[ "$STATUS" == *"FAILED"* ]]; then + echo "" + echo "❌ Policy deployment failed" + exit 1 + fi + sleep 2 +done + +echo "❌ Timeout waiting for policy" +exit 1 diff --git a/apps/java-spring-ai-agents/scripts/14-mcp-policy-cleanup.sh b/apps/java-spring-ai-agents/scripts/14-mcp-policy-cleanup.sh new file mode 100755 index 00000000..9aede11a --- /dev/null +++ b/apps/java-spring-ai-agents/scripts/14-mcp-policy-cleanup.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Cleanup Cedar policies from MCP Gateway +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if [ -f "$SCRIPT_DIR/.env" ]; then + source "$SCRIPT_DIR/.env" +fi + +echo "Cleaning up policies from engine: ${MCP_POLICY_ENGINE_ID}" +echo "" + +POLICIES=$(aws bedrock-agentcore-control list-policies \ + --policy-engine-id "${MCP_POLICY_ENGINE_ID}" \ + --query 'policies[].policyId' --output text 2>/dev/null || true) + +if [ -z "$POLICIES" ]; then + echo "No policies found." + exit 0 +fi + +for PID in $POLICIES; do + echo "Deleting: $PID" + aws bedrock-agentcore-control delete-policy \ + --policy-engine-id "${MCP_POLICY_ENGINE_ID}" \ + --policy-id "$PID" > /dev/null + + echo " Waiting for deletion..." + while true; do + STATUS=$(aws bedrock-agentcore-control get-policy \ + --policy-engine-id "${MCP_POLICY_ENGINE_ID}" \ + --policy-id "$PID" \ + --query 'status' --output text 2>/dev/null || echo "DELETED") + + if [ "$STATUS" = "DELETED" ] || [ -z "$STATUS" ]; then + echo " ✅ Deleted" + break + fi + echo " Status: $STATUS" + sleep 2 + done +done + +echo "" +echo "✅ All policies cleaned up!" diff --git a/apps/java-spring-ai-agents/scripts/backoffice-policy.cedar b/apps/java-spring-ai-agents/scripts/backoffice-policy.cedar new file mode 100644 index 00000000..7dd69a58 --- /dev/null +++ b/apps/java-spring-ai-agents/scripts/backoffice-policy.cedar @@ -0,0 +1,11 @@ +// Backoffice MCP Gateway Policy +// Forbid dangerous operations - everything else allowed by default + +forbid( + principal, + action in [ + AgentCore::Action::"backoffice___cancelTrip", + AgentCore::Action::"backoffice___deleteExpense" + ], + resource == AgentCore::Gateway::"${GATEWAY_ARN}" +);