Add AWS MCP Server integration + IAM self-modify with approval gate

- CDK: add compute/build, broad read-only, IAM self-modify (scoped to own role),
  IAM policy management, and SSM read permissions to runtime1Role
- config.py: load /agent-claw/aws-mcp-url from SSM at cold start
- main.py: connect to AWS MCP Server with SigV4 auth (_AwsMcpSigV4Auth);
  add request_iam_permission and apply_iam_permission tools
- agentcore.json: add EXECUTION_ROLE_ARN env var
This commit is contained in:
daniel
2026-05-15 08:56:06 -05:00
parent 68aad4fb71
commit 88ed337938
4 changed files with 141 additions and 5 deletions

View File

@@ -22,7 +22,8 @@
"WORKSPACE_BUCKET_NAME": "agent-claw-workspace-495395224548", "WORKSPACE_BUCKET_NAME": "agent-claw-workspace-495395224548",
"TELEGRAM_BOT_TOKEN_SSM_PARAM": "/agent-claw/telegram-bot-token", "TELEGRAM_BOT_TOKEN_SSM_PARAM": "/agent-claw/telegram-bot-token",
"BRAVE_API_KEY_SSM_PARAM": "/agent-claw/brave-api-key", "BRAVE_API_KEY_SSM_PARAM": "/agent-claw/brave-api-key",
"SCHEDULER_LAMBDA_ARN": "arn:aws:lambda:us-east-1:495395224548:function:agent-claw-scheduler" "SCHEDULER_LAMBDA_ARN": "arn:aws:lambda:us-east-1:495395224548:function:agent-claw-scheduler",
"EXECUTION_ROLE_ARN": "arn:aws:iam::495395224548:role/AgentClawStack-Runtime1RoleA7A82078-VjUcGi0qjATm"
} }
} }
], ],
@@ -60,4 +61,4 @@
"configBundles": [], "configBundles": [],
"abTests": [], "abTests": [],
"httpGateways": [] "httpGateways": []
} }

View File

@@ -1,10 +1,11 @@
"""Config loader — fetches model IDs from SSM Parameter Store at cold start.""" """Config loader — fetches model IDs and service URLs from SSM Parameter Store at cold start."""
import boto3 import boto3
_DEFAULTS = { _DEFAULTS = {
'/agent-claw/model-id': 'us.anthropic.claude-sonnet-4-6', '/agent-claw/model-id': 'us.anthropic.claude-sonnet-4-6',
'/agent-claw/config/compaction_model_id': 'us.anthropic.claude-3-5-haiku-20241022-v1:0', '/agent-claw/config/compaction_model_id': 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
'/agent-claw/aws-mcp-url': 'https://aws-mcp.us-east-1.api.aws/mcp',
} }
@@ -23,3 +24,4 @@ _params = _load()
AGENT_MODEL_ID: str = _params['/agent-claw/model-id'] AGENT_MODEL_ID: str = _params['/agent-claw/model-id']
COMPACTION_MODEL_ID: str = _params['/agent-claw/config/compaction_model_id'] COMPACTION_MODEL_ID: str = _params['/agent-claw/config/compaction_model_id']
AWS_MCP_URL: str = _params['/agent-claw/aws-mcp-url']

View File

@@ -35,6 +35,7 @@ OAUTH_START_URL = (
or 'https://sptejrymri.execute-api.us-east-1.amazonaws.com/oauth/start' or 'https://sptejrymri.execute-api.us-east-1.amazonaws.com/oauth/start'
) )
USERS_TABLE_NAME = os.environ.get('USERS_TABLE_NAME', 'agent-claw-users') USERS_TABLE_NAME = os.environ.get('USERS_TABLE_NAME', 'agent-claw-users')
EXECUTION_ROLE_ARN = os.environ.get('EXECUTION_ROLE_ARN', '')
class _SigV4HttpxAuth(httpx.Auth): class _SigV4HttpxAuth(httpx.Auth):
@@ -62,6 +63,26 @@ class _SigV4HttpxAuth(httpx.Auth):
if self._actor_id: if self._actor_id:
request.headers['x-actor-id'] = self._actor_id request.headers['x-actor-id'] = self._actor_id
yield request yield request
class _AwsMcpSigV4Auth(httpx.Auth):
"""SigV4 auth for AWS MCP Server (service: aws-mcp)."""
def auth_flow(self, request):
creds = boto3.Session().get_credentials().get_frozen_credentials()
parsed = _urlparse(str(request.url))
aws_req = botocore.awsrequest.AWSRequest(
method=request.method,
url=str(request.url),
data=request.content or b'',
headers={
'Host': parsed.hostname,
'Content-Type': request.headers.get('content-type', 'application/json'),
}
)
botocore.auth.SigV4Auth(creds, 'aws-mcp', 'us-east-1').add_auth(aws_req)
for k, v in aws_req.headers.items():
request.headers[k] = v
yield request
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager
# code_interpreter removed — causes [Errno 98] port 8080 conflict on warm container re-init # code_interpreter removed — causes [Errno 98] port 8080 conflict on warm container re-init
@@ -264,6 +285,55 @@ def manage_service(action: str, service: str, config: dict | None = None) -> str
return f'Unknown action: {action}. Use "enroll", "remove", or "list".' return f'Unknown action: {action}. Use "enroll", "remove", or "list".'
@tool
def request_iam_permission(action: str, resource: str, reason: str) -> str:
"""Request IAM permission from the user. ALWAYS call this before apply_iam_permission.
Sends the user a Telegram message explaining what permission is needed and why.
After calling this, wait for the user to explicitly say 'yes' before proceeding.
Args:
action: IAM action to request (e.g. 's3:PutObject')
resource: Resource ARN the action applies to (e.g. 'arn:aws:s3:::my-bucket/*')
reason: Why this permission is needed
"""
msg = (
f"⚠️ *IAM Permission Request*\n\n"
f"I need to add the following permission to my execution role:\n\n"
f"• Action: `{action}`\n"
f"• Resource: `{resource}`\n"
f"• Reason: {reason}\n\n"
f"Reply *yes* to approve or *no* to deny."
)
messaging.send(msg)
return "Permission request sent. Wait for the user to reply 'yes' before calling apply_iam_permission."
@tool
def apply_iam_permission(action: str, resource: str, policy_name: str) -> str:
"""Apply an IAM permission to the agent execution role.
Only call this after the user has explicitly approved via request_iam_permission.
Args:
action: IAM action to allow (e.g. 's3:PutObject')
resource: Resource ARN the action applies to
policy_name: Unique name for the inline policy (e.g. 'AllowS3PutObject')
"""
if not EXECUTION_ROLE_ARN:
return 'EXECUTION_ROLE_ARN not configured.'
import json as _json
role_name = EXECUTION_ROLE_ARN.split('/')[-1]
policy_doc = _json.dumps({
'Version': '2012-10-17',
'Statement': [{'Effect': 'Allow', 'Action': action, 'Resource': resource}],
})
boto3.client('iam', region_name='us-east-1').put_role_policy(
RoleName=role_name,
PolicyName=policy_name,
PolicyDocument=policy_doc,
)
return f"Applied policy '{policy_name}': Allow {action} on {resource}."
# ── Entrypoint ──────────────────────────────────────────────────────────── # ── Entrypoint ────────────────────────────────────────────────────────────
# Module-level actor_id for tool closures (set per-invocation) # Module-level actor_id for tool closures (set per-invocation)
@@ -388,12 +458,25 @@ async def main(payload: dict, context):
home_assistant, connect_google_account, list_google_accounts, remove_google_account, home_assistant, connect_google_account, list_google_accounts, remove_google_account,
manage_service, manage_mcp_connection, schedule_reminder, list_reminders, cancel_reminder, manage_service, manage_mcp_connection, schedule_reminder, list_reminders, cancel_reminder,
list_calendars, get_calendar_events, list_gmail_messages, get_gmail_message, list_calendars, get_calendar_events, list_gmail_messages, get_gmail_message,
run_code, send_file] run_code, send_file, request_iam_permission, apply_iam_permission]
# Load user's dynamic MCP connections # Load user's dynamic MCP connections
mcp_connections = services.get('mcp_connections', []) mcp_connections = services.get('mcp_connections', [])
mcp_clients, _mcp_to_close = mcp_loader.load_mcp_tools(mcp_connections, actor_id) mcp_clients, _mcp_to_close = mcp_loader.load_mcp_tools(mcp_connections, actor_id)
all_tools = base_tools + mcp_clients
# AWS MCP Server connection (system-level, SigV4 auth)
_aws_mcp_client = None
_aws_mcp_tools = []
try:
_aws_mcp_client = MCPClient(
lambda: streamablehttp_client(config.AWS_MCP_URL, auth=_AwsMcpSigV4Auth())
)
_aws_mcp_client.start()
_aws_mcp_tools = [_aws_mcp_client]
except Exception as _e:
print(f'[main] AWS MCP client failed to start: {_e}')
all_tools = base_tools + mcp_clients + _aws_mcp_tools
agent = Agent( agent = Agent(
model=model, model=model,
@@ -418,6 +501,11 @@ async def main(payload: dict, context):
_typing_active = False _typing_active = False
session_manager.close() session_manager.close()
mcp_loader.close_mcp_clients(_mcp_to_close) mcp_loader.close_mcp_clients(_mcp_to_close)
if _aws_mcp_client:
try:
_aws_mcp_client.stop()
except Exception:
pass
# Check if session exceeds window — flag for compaction on next invocation # Check if session exceeds window — flag for compaction on next invocation
memory_manager.check_window_and_flag(actor_id, session_id) memory_manager.check_window_and_flag(actor_id, session_id)

View File

@@ -371,6 +371,51 @@ export class AgentClawStack extends cdk.Stack {
resources: [schedulerFn.functionArn], resources: [schedulerFn.functionArn],
})); }));
// ── AgentCore Runtime 1 — extended permissions ───────────────────────
// Compute/build
runtime1Role.addToPolicy(new iam.PolicyStatement({
sid: 'ComputeBuild',
actions: ['codebuild:*', 'ecr:*', 'ecs:*', 'logs:*'],
resources: ['*'],
}));
// Broad read-only across account
runtime1Role.addToPolicy(new iam.PolicyStatement({
sid: 'BroadReadOnly',
actions: [
's3:List*', 's3:GetObject',
'lambda:List*', 'lambda:Get*',
'cloudformation:Describe*', 'cloudformation:List*',
'sqs:List*', 'sqs:GetQueueAttributes',
'ec2:Describe*',
'ssm:Describe*', 'ssm:List*',
'ce:GetCostAndUsage', 'ce:GetCostForecast',
],
resources: ['*'],
}));
// IAM self-modify — scoped to own role only
runtime1Role.addToPolicy(new iam.PolicyStatement({
sid: 'IamSelfModify',
actions: ['iam:PutRolePolicy', 'iam:AttachRolePolicy', 'iam:DetachRolePolicy', 'iam:DeleteRolePolicy'],
resources: [runtime1Role.roleArn],
}));
// IAM policy management
runtime1Role.addToPolicy(new iam.PolicyStatement({
sid: 'IamPolicyManagement',
actions: ['iam:CreatePolicy', 'iam:GetPolicy', 'iam:ListPolicies'],
resources: ['*'],
}));
// SSM read for AWS MCP URL
runtime1Role.addToPolicy(new iam.PolicyStatement({
sid: 'AwsMcpUrlSsmRead',
actions: ['ssm:GetParameter', 'ssm:GetParameters'],
resources: [`arn:aws:ssm:${this.region}:${this.account}:parameter/agent-claw/aws-mcp-url`],
}));
// ── Outputs ──────────────────────────────────────────────────────────── // ── Outputs ────────────────────────────────────────────────────────────
new cdk.CfnOutput(this, 'WorkspaceMcpFunctionUrl', { new cdk.CfnOutput(this, 'WorkspaceMcpFunctionUrl', {