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:
@@ -22,7 +22,8 @@
|
||||
"WORKSPACE_BUCKET_NAME": "agent-claw-workspace-495395224548",
|
||||
"TELEGRAM_BOT_TOKEN_SSM_PARAM": "/agent-claw/telegram-bot-token",
|
||||
"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": [],
|
||||
"abTests": [],
|
||||
"httpGateways": []
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
_DEFAULTS = {
|
||||
'/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/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']
|
||||
COMPACTION_MODEL_ID: str = _params['/agent-claw/config/compaction_model_id']
|
||||
AWS_MCP_URL: str = _params['/agent-claw/aws-mcp-url']
|
||||
|
||||
@@ -35,6 +35,7 @@ OAUTH_START_URL = (
|
||||
or 'https://sptejrymri.execute-api.us-east-1.amazonaws.com/oauth/start'
|
||||
)
|
||||
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):
|
||||
@@ -62,6 +63,26 @@ class _SigV4HttpxAuth(httpx.Auth):
|
||||
if self._actor_id:
|
||||
request.headers['x-actor-id'] = self._actor_id
|
||||
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.session_manager import AgentCoreMemorySessionManager
|
||||
# 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".'
|
||||
|
||||
|
||||
@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 ────────────────────────────────────────────────────────────
|
||||
|
||||
# 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,
|
||||
manage_service, manage_mcp_connection, schedule_reminder, list_reminders, cancel_reminder,
|
||||
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
|
||||
mcp_connections = services.get('mcp_connections', [])
|
||||
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(
|
||||
model=model,
|
||||
@@ -418,6 +501,11 @@ async def main(payload: dict, context):
|
||||
_typing_active = False
|
||||
session_manager.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
|
||||
memory_manager.check_window_and_flag(actor_id, session_id)
|
||||
|
||||
|
||||
@@ -371,6 +371,51 @@ export class AgentClawStack extends cdk.Stack {
|
||||
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 ────────────────────────────────────────────────────────────
|
||||
|
||||
new cdk.CfnOutput(this, 'WorkspaceMcpFunctionUrl', {
|
||||
|
||||
Reference in New Issue
Block a user