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",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
Reference in New Issue
Block a user