diff --git a/agentclaw/agentcore/agentcore.json b/agentclaw/agentcore/agentcore.json index 807b31a..b9416e1 100644 --- a/agentclaw/agentcore/agentcore.json +++ b/agentclaw/agentcore/agentcore.json @@ -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": [] -} +} \ No newline at end of file diff --git a/agentclaw/app/agent_claw_main/config.py b/agentclaw/app/agent_claw_main/config.py index f667e99..b143d5f 100644 --- a/agentclaw/app/agent_claw_main/config.py +++ b/agentclaw/app/agent_claw_main/config.py @@ -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'] diff --git a/agentclaw/app/agent_claw_main/main.py b/agentclaw/app/agent_claw_main/main.py index c093cca..55f9348 100644 --- a/agentclaw/app/agent_claw_main/main.py +++ b/agentclaw/app/agent_claw_main/main.py @@ -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) diff --git a/cdk/lib/agent-claw-stack.ts b/cdk/lib/agent-claw-stack.ts index 159c176..8633251 100644 --- a/cdk/lib/agent-claw-stack.ts +++ b/cdk/lib/agent-claw-stack.ts @@ -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', {