diff --git a/agentclaw/agentcore/agentcore.json b/agentclaw/agentcore/agentcore.json index 608cb89..d0b59d3 100644 --- a/agentclaw/agentcore/agentcore.json +++ b/agentclaw/agentcore/agentcore.json @@ -15,7 +15,10 @@ "codeLocation": "app/agent_claw_main/", "runtimeVersion": "PYTHON_3_14", "networkMode": "PUBLIC", - "protocol": "HTTP" + "protocol": "HTTP", + "environmentVariables": { + "OAUTH_START_URL": "https://sptejrymri.execute-api.us-east-1.amazonaws.com/oauth/start" + } } ], "memories": [ diff --git a/agentclaw/app/agent_claw_main/main.py b/agentclaw/app/agent_claw_main/main.py index 3b4f679..7307cd4 100644 --- a/agentclaw/app/agent_claw_main/main.py +++ b/agentclaw/app/agent_claw_main/main.py @@ -23,12 +23,14 @@ import boto3 from urllib.parse import urlparse as _urlparse WORKSPACE_MCP_URL = 'https://25hugrzw4uwtueeg77jsmft6lq0wunmd.lambda-url.us-east-1.on.aws/mcp' +OAUTH_START_URL = os.environ.get('OAUTH_START_URL', '') class _SigV4HttpxAuth(httpx.Auth): - """SigV4 auth for Lambda Function URL with AWS_IAM.""" - def __init__(self, region: str = 'us-east-1'): + """SigV4 auth for Lambda Function URL with AWS_IAM, plus X-Actor-Id header.""" + def __init__(self, region: str = 'us-east-1', actor_id: str = ''): self._region = region + self._actor_id = actor_id def auth_flow(self, request): creds = boto3.Session().get_credentials().get_frozen_credentials() @@ -46,6 +48,8 @@ class _SigV4HttpxAuth(httpx.Auth): botocore.auth.SigV4Auth(creds, 'lambda', self._region).add_auth(aws_req) for k, v in aws_req.headers.items(): request.headers[k] = v + if self._actor_id: + request.headers['x-actor-id'] = self._actor_id yield request from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager @@ -91,11 +95,31 @@ def write_workspace_file(path: str, content: str) -> str: return result +@tool +def connect_google_account() -> str: + """Generate a Google OAuth authorization URL for the current user to connect their Google account. + Use this when the user wants to connect Google Workspace (Gmail, Calendar, Drive, etc.) + or when Google tools fail due to missing credentials.""" + if not OAUTH_START_URL: + return 'Google OAuth is not configured. Set OAUTH_START_URL environment variable.' + # actor_id is injected into the tool's closure via _current_actor_id module-level var + actor_id = _current_actor_id + if not actor_id: + return 'Cannot determine actor_id for OAuth flow.' + url = f'{OAUTH_START_URL}?actor_id={actor_id}' + return f'Please open this URL to connect your Google account:\n{url}\n\nAfter authorizing, Google Workspace tools (Gmail, Calendar, Drive) will be available.' + + # ── Entrypoint ──────────────────────────────────────────────────────────── +# Module-level actor_id for tool closures (set per-invocation) +_current_actor_id: str = '' + + @app.entrypoint def main(payload: dict, context) -> dict: """Handle an invocation from agent-runner Lambda.""" + global _current_actor_id # Set up channel adapter adapter_config = payload.get('channel_adapter', {}) @@ -129,6 +153,7 @@ def main(payload: dict, context) -> dict: MEMORY_ID = 'agentclaw_AgentClawMemory-i7Csf776AH' actor_id = payload.get('actor_id', adapter_config.get('target_id', 'default')) session_id = payload.get('session_id', f'session-{actor_id}') + _current_actor_id = actor_id memory_config = AgentCoreMemoryConfig( memory_id=MEMORY_ID, @@ -146,9 +171,14 @@ def main(payload: dict, context) -> dict: if user_profile: name = user_profile.get('display_name', '') username = user_profile.get('telegram_username', '') + google_email = user_profile.get('google_email', '') user_context = f'Name: {name}' if username: user_context += f'\nTelegram username: @{username}' + if google_email: + user_context += f'\nGoogle account: {google_email}' + else: + user_context += '\nGoogle account: not connected (use connect_google_account tool to connect)' system_prompt = build_system_prompt(user_context=user_context, actor_id=actor_id) # Model: claude-sonnet-4-6 via cross-region inference @@ -158,7 +188,7 @@ def main(payload: dict, context) -> dict: ) base_tools = [send_message, web_search, web_fetch, read_workspace_file, write_workspace_file, - _code_interpreter.code_interpreter, home_assistant] + _code_interpreter.code_interpreter, home_assistant, connect_google_account] def _run_agent(tools): agent = Agent( @@ -170,14 +200,19 @@ def main(payload: dict, context) -> dict: return agent(payload.get('prompt', '')) workspace_mcp_client = MCPClient( - lambda: streamablehttp_client(WORKSPACE_MCP_URL, timeout=20, auth=_SigV4HttpxAuth()) + lambda: streamablehttp_client(WORKSPACE_MCP_URL, timeout=20, auth=_SigV4HttpxAuth(actor_id=actor_id)) ) workspace_tools = [] - try: - with workspace_mcp_client: - workspace_tools = workspace_mcp_client.list_tools_sync() - except Exception as e: - print(f'[main] workspace_mcp unavailable ({type(e).__name__}) — continuing without it') + google_email = user_profile.get('google_email', '') + if google_email: + # Only attempt workspace-mcp if user has connected Google + try: + with workspace_mcp_client: + workspace_tools = workspace_mcp_client.list_tools_sync() + except Exception as e: + print(f'[main] workspace_mcp unavailable ({type(e).__name__}) — continuing without it') + else: + print(f'[main] actor={actor_id} has no google_email — skipping workspace_mcp') try: result = _run_agent(base_tools + list(workspace_tools)) diff --git a/cdk/cdk.out/.cache/292d56e81261416a7ead3d98a94c25e6db0ec754c2655f20ee0717736b5a8bd5.zip b/cdk/cdk.out/.cache/292d56e81261416a7ead3d98a94c25e6db0ec754c2655f20ee0717736b5a8bd5.zip new file mode 100644 index 0000000..54d0c00 Binary files /dev/null and b/cdk/cdk.out/.cache/292d56e81261416a7ead3d98a94c25e6db0ec754c2655f20ee0717736b5a8bd5.zip differ diff --git a/cdk/cdk.out/.cache/5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e.zip b/cdk/cdk.out/.cache/5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e.zip new file mode 100644 index 0000000..71ae677 Binary files /dev/null and b/cdk/cdk.out/.cache/5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e.zip differ diff --git a/cdk/cdk.out/.cache/6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370.zip b/cdk/cdk.out/.cache/6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370.zip new file mode 100644 index 0000000..fbb7a74 Binary files /dev/null and b/cdk/cdk.out/.cache/6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370.zip differ diff --git a/cdk/cdk.out/AgentClawStack.assets.json b/cdk/cdk.out/AgentClawStack.assets.json index 361ae9b..54c7672 100644 --- a/cdk/cdk.out/AgentClawStack.assets.json +++ b/cdk/cdk.out/AgentClawStack.assets.json @@ -16,31 +16,46 @@ } } }, - "7053cd1618f5f520a7aac409588128f920d8fe76791c1dbcc65610454d1a5387": { + "6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370": { "displayName": "AgentRunner/Code", "source": { - "path": "asset.7053cd1618f5f520a7aac409588128f920d8fe76791c1dbcc65610454d1a5387", + "path": "asset.6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370", "packaging": "zip" }, "destinations": { - "495395224548-us-east-1-63ace858": { + "495395224548-us-east-1-ab491e35": { "bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1", - "objectKey": "7053cd1618f5f520a7aac409588128f920d8fe76791c1dbcc65610454d1a5387.zip", + "objectKey": "6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370.zip", "region": "us-east-1", "assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1" } } }, - "2765094d543818b111d837ea62bad41260a47615c5b99bc608a58e99f24d5b85": { + "5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e": { + "displayName": "OAuthHandler/Code", + "source": { + "path": "asset.5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e", + "packaging": "zip" + }, + "destinations": { + "495395224548-us-east-1-23c3d77a": { + "bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1", + "objectKey": "5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e.zip", + "region": "us-east-1", + "assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1" + } + } + }, + "fdf1ff81e9e0ded898f1c1d03a2bb8bbe0bbf63689426c24072f179b49b527c6": { "displayName": "AgentClawStack Template", "source": { "path": "AgentClawStack.template.json", "packaging": "file" }, "destinations": { - "495395224548-us-east-1-b10aaf8d": { + "495395224548-us-east-1-9bba4277": { "bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1", - "objectKey": "2765094d543818b111d837ea62bad41260a47615c5b99bc608a58e99f24d5b85.json", + "objectKey": "fdf1ff81e9e0ded898f1c1d03a2bb8bbe0bbf63689426c24072f179b49b527c6.json", "region": "us-east-1", "assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1" } diff --git a/cdk/cdk.out/AgentClawStack.metadata.json b/cdk/cdk.out/AgentClawStack.metadata.json index ebe94b5..827c746 100644 --- a/cdk/cdk.out/AgentClawStack.metadata.json +++ b/cdk/cdk.out/AgentClawStack.metadata.json @@ -32,21 +32,35 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:234:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:289:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] } ], - "/AgentClawStack/GoogleCredentialsSecretArn": [ + "/AgentClawStack/OAuthStartUrl": [ { "type": "aws:cdk:logicalId", - "data": "GoogleCredentialsSecretArn" + "data": "OAuthStartUrl" }, { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:238:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:293:5)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], + "/AgentClawStack/OAuthRedirectUri": [ + { + "type": "aws:cdk:logicalId", + "data": "OAuthRedirectUri" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:297:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -60,7 +74,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:243:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:302:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -74,7 +88,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:248:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:307:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -88,7 +102,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:253:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:312:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -102,7 +116,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:258:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:317:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -116,7 +130,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:263:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:322:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -130,7 +144,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:268:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:327:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -267,6 +281,21 @@ ] } ], + "/AgentClawStack/OAuthHandler/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "OAuthHandlerC97C2476" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + "...new Function2 in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:235:28)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], "/AgentClawStack/CDKMetadata/Default": [ { "type": "aws:cdk:logicalId", @@ -374,6 +403,74 @@ ] } ], + "/AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration-Permission": [ + { + "type": "aws:cdk:logicalId", + "data": "WebhookApiGEToauthstartOAuthStartIntegrationPermission38BAEF6D" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...", + "Array.map (:)", + "...WrappedClass. in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:263:13)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], + "/AgentClawStack/WebhookApi/GET--oauth--start/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "WebhookApiGEToauthstart6DCA713A" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...", + "Array.map (:)", + "...WrappedClass. in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:263:13)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], + "/AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration-Permission": [ + { + "type": "aws:cdk:logicalId", + "data": "WebhookApiGEToauthcallbackOAuthCallbackIntegrationPermission6BA3A5AD" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...", + "Array.map (:)", + "...WrappedClass. in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:270:13)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], + "/AgentClawStack/WebhookApi/GET--oauth--callback/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "WebhookApiGEToauthcallbackFC1F6BCD" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...", + "Array.map (:)", + "...WrappedClass. in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:270:13)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], "/AgentClawStack/Runtime1Role/DefaultPolicy/Resource": [ { "type": "aws:cdk:logicalId", @@ -398,7 +495,22 @@ "type": "aws:cdk:creationStack", "data": [ "...SecretBase.grantRead in aws-cdk-lib...", - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:207:29)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:198:29)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], + "/AgentClawStack/OAuthHandler/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "OAuthHandlerServiceRole9CDCCF9E" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + "...new Function2 in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:235:28)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -450,5 +562,54 @@ "...node internals, ts-node, ts-node, ts-node..." ] } + ], + "/AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "WebhookApiGEToauthstartOAuthStartIntegrationA546443F" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...", + "Array.map (:)", + "...WrappedClass. in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:263:13)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], + "/AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "WebhookApiGEToauthcallbackOAuthCallbackIntegrationCFBBEB09" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...", + "Array.map (:)", + "...WrappedClass. in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:270:13)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], + "/AgentClawStack/OAuthHandler/ServiceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "OAuthHandlerServiceRoleDefaultPolicy69D90416" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + "...SecretBase.grantRead in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:249:29)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } ] } \ No newline at end of file diff --git a/cdk/cdk.out/AgentClawStack.template.json b/cdk/cdk.out/AgentClawStack.template.json index 091ecfe..a529779 100644 --- a/cdk/cdk.out/AgentClawStack.template.json +++ b/cdk/cdk.out/AgentClawStack.template.json @@ -387,7 +387,7 @@ "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1", - "S3Key": "7053cd1618f5f520a7aac409588128f920d8fe76791c1dbcc65610454d1a5387.zip" + "S3Key": "6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370.zip" }, "Environment": { "Variables": { @@ -423,7 +423,7 @@ ], "Metadata": { "aws:cdk:path": "AgentClawStack/AgentRunner/Resource", - "aws:asset:path": "asset.7053cd1618f5f520a7aac409588128f920d8fe76791c1dbcc65610454d1a5387", + "aws:asset:path": "asset.6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370", "aws:asset:is-bundled": false, "aws:asset:property": "Code" } @@ -545,6 +545,156 @@ "aws:cdk:path": "AgentClawStack/WebhookApi/POST--telegram/Resource" } }, + "WebhookApiGEToauthstartOAuthStartIntegrationA546443F": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "WebhookApi28122C53" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "OAuthHandlerC97C2476", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + }, + "Metadata": { + "aws:cdk:path": "AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration/Resource" + } + }, + "WebhookApiGEToauthstartOAuthStartIntegrationPermission38BAEF6D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "OAuthHandlerC97C2476", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-east-1:495395224548:", + { + "Ref": "WebhookApi28122C53" + }, + "/*/*/oauth/start" + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration-Permission" + } + }, + "WebhookApiGEToauthstart6DCA713A": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "WebhookApi28122C53" + }, + "AuthorizationType": "NONE", + "RouteKey": "GET /oauth/start", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "WebhookApiGEToauthstartOAuthStartIntegrationA546443F" + } + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "AgentClawStack/WebhookApi/GET--oauth--start/Resource" + } + }, + "WebhookApiGEToauthcallbackOAuthCallbackIntegrationCFBBEB09": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "WebhookApi28122C53" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "OAuthHandlerC97C2476", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + }, + "Metadata": { + "aws:cdk:path": "AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration/Resource" + } + }, + "WebhookApiGEToauthcallbackOAuthCallbackIntegrationPermission6BA3A5AD": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "OAuthHandlerC97C2476", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-east-1:495395224548:", + { + "Ref": "WebhookApi28122C53" + }, + "/*/*/oauth/callback" + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration-Permission" + } + }, + "WebhookApiGEToauthcallbackFC1F6BCD": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "WebhookApi28122C53" + }, + "AuthorizationType": "NONE", + "RouteKey": "GET /oauth/callback", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "WebhookApiGEToauthcallbackOAuthCallbackIntegrationCFBBEB09" + } + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "AgentClawStack/WebhookApi/GET--oauth--callback/Resource" + } + }, "Runtime1RoleA7A82078": { "Type": "AWS::IAM::Role", "Properties": { @@ -674,29 +824,16 @@ { "Ref": "AWS::Partition" }, - ":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-workspace-credentials-??????" + ":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????" ] ] } }, { - "Action": [ - "secretsmanager:GetSecretValue", - "secretsmanager:DescribeSecret" - ], + "Action": "secretsmanager:GetSecretValue", "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????" - ] - ] - } + "Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*", + "Sid": "PerUserGoogleCredentialsReadRuntime" } ], "Version": "2012-10-17" @@ -731,11 +868,68 @@ { "Ref": "AWS::Partition" }, - ":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-workspace-credentials-??????" + ":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????" ] ] } }, + { + "Action": "secretsmanager:GetSecretValue", + "Effect": "Allow", + "Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*", + "Sid": "PerUserGoogleCredentialsRead" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "WorkspaceMcpRolePolicy5B8B0072", + "Roles": [ + "agent-claw-workspace-mcp-role" + ] + }, + "Metadata": { + "aws:cdk:path": "AgentClawStack/WorkspaceMcpRole/Policy/Resource" + } + }, + "OAuthHandlerServiceRole9CDCCF9E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "AgentClawStack/OAuthHandler/ServiceRole/Resource" + } + }, + "OAuthHandlerServiceRoleDefaultPolicy69D90416": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ { "Action": [ "secretsmanager:GetSecretValue", @@ -754,17 +948,132 @@ ] ] } + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "UsersTable9725E9C8", + "Arn" + ] + } + ] + }, + { + "Action": [ + "dynamodb:GetRecords", + "dynamodb:GetShardIterator" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "UsersTable9725E9C8", + "Arn" + ] + } + ] + }, + { + "Action": [ + "secretsmanager:CreateSecret", + "secretsmanager:PutSecretValue", + "secretsmanager:GetSecretValue" + ], + "Effect": "Allow", + "Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*", + "Sid": "PerUserGoogleCredentialsWrite" } ], "Version": "2012-10-17" }, - "PolicyName": "WorkspaceMcpRolePolicy5B8B0072", + "PolicyName": "OAuthHandlerServiceRoleDefaultPolicy69D90416", "Roles": [ - "agent-claw-workspace-mcp-role" + { + "Ref": "OAuthHandlerServiceRole9CDCCF9E" + } ] }, "Metadata": { - "aws:cdk:path": "AgentClawStack/WorkspaceMcpRole/Policy/Resource" + "aws:cdk:path": "AgentClawStack/OAuthHandler/ServiceRole/DefaultPolicy/Resource" + } + }, + "OAuthHandlerC97C2476": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1", + "S3Key": "5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e.zip" + }, + "Environment": { + "Variables": { + "GOOGLE_OAUTH_CLIENT_SECRET_ARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client" + ] + ] + }, + "USERS_TABLE_NAME": { + "Ref": "UsersTable9725E9C8" + }, + "OAUTH_REDIRECT_URI": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "WebhookApi28122C53" + }, + ".execute-api.us-east-1.", + { + "Ref": "AWS::URLSuffix" + }, + "/oauth/callback" + ] + ] + } + } + }, + "FunctionName": "agent-claw-oauth-handler", + "Handler": "handler.handler", + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "OAuthHandlerServiceRole9CDCCF9E", + "Arn" + ] + }, + "Runtime": "python3.12", + "Timeout": 30 + }, + "DependsOn": [ + "OAuthHandlerServiceRoleDefaultPolicy69D90416", + "OAuthHandlerServiceRole9CDCCF9E" + ], + "Metadata": { + "aws:cdk:path": "AgentClawStack/OAuthHandler/Resource", + "aws:asset:path": "asset.5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code" } }, "CDKMetadata": { @@ -782,17 +1091,40 @@ "Description": "workspace-mcp Lambda Function URL (MCP endpoint for Gmail/Calendar)", "Value": "https://25hugrzw4uwtueeg77jsmft6lq0wunmd.lambda-url.us-east-1.on.aws" }, - "GoogleCredentialsSecretArn": { - "Description": "Google OAuth user credentials secret ARN", + "OAuthStartUrl": { + "Description": "Google OAuth start URL — set as OAUTH_START_URL in agentcore.json", "Value": { "Fn::Join": [ "", [ - "arn:", + "https://", { - "Ref": "AWS::Partition" + "Ref": "WebhookApi28122C53" }, - ":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-workspace-credentials" + ".execute-api.us-east-1.", + { + "Ref": "AWS::URLSuffix" + }, + "/oauth/start" + ] + ] + } + }, + "OAuthRedirectUri": { + "Description": "Google OAuth redirect URI — register in Google Cloud Console", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "WebhookApi28122C53" + }, + ".execute-api.us-east-1.", + { + "Ref": "AWS::URLSuffix" + }, + "/oauth/callback" ] ] } diff --git a/cdk/cdk.out/asset.292d56e81261416a7ead3d98a94c25e6db0ec754c2655f20ee0717736b5a8bd5/handler.py b/cdk/cdk.out/asset.292d56e81261416a7ead3d98a94c25e6db0ec754c2655f20ee0717736b5a8bd5/handler.py new file mode 100644 index 0000000..051c651 --- /dev/null +++ b/cdk/cdk.out/asset.292d56e81261416a7ead3d98a94c25e6db0ec754c2655f20ee0717736b5a8bd5/handler.py @@ -0,0 +1,191 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + _agentcore = boto3.client('bedrock-agentcore', region_name='us-east-1') + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps({'chat_id': chat_id, 'text': text}).encode() + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + urllib.request.urlopen(req, timeout=10) + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'allowed': user_profile.get('allowed', True), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Consume streaming response (agent delivers to Telegram via send_message tool) + for chunk in response.get('response', []): + pass # intentional no-op — agent handles delivery internally + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.292d56e81261416a7ead3d98a94c25e6db0ec754c2655f20ee0717736b5a8bd5/requirements.txt b/cdk/cdk.out/asset.292d56e81261416a7ead3d98a94c25e6db0ec754c2655f20ee0717736b5a8bd5/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.292d56e81261416a7ead3d98a94c25e6db0ec754c2655f20ee0717736b5a8bd5/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/asset.5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e/handler.py b/cdk/cdk.out/asset.5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e/handler.py new file mode 100644 index 0000000..a3bb646 --- /dev/null +++ b/cdk/cdk.out/asset.5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e/handler.py @@ -0,0 +1,212 @@ +""" +Google OAuth handler Lambda. + +Routes: + GET /oauth/start?actor_id=telegram:123 → redirect to Google OAuth consent + GET /oauth/callback?code=...&state=... → exchange code, store tokens, update DynamoDB +""" +import base64 +import hashlib +import hmac +import json +import os +import time +import urllib.parse +import urllib.request + +import boto3 + +_sm = None +_ddb = None + +SCOPES = ' '.join([ + 'https://www.googleapis.com/auth/gmail.modify', + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/spreadsheets', + 'https://www.googleapis.com/auth/documents', + 'openid', + 'email', + 'profile', +]) + + +def get_sm(): + global _sm + if _sm is None: + _sm = boto3.client('secretsmanager', region_name=os.environ.get('AWS_REGION', 'us-east-1')) + return _sm + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_oauth_client() -> tuple[str, str]: + """Return (client_id, client_secret) from Secrets Manager.""" + arn = os.environ['GOOGLE_OAUTH_CLIENT_SECRET_ARN'] + secret = json.loads(get_sm().get_secret_value(SecretId=arn)['SecretString']) + return secret['client_id'], secret['client_secret'] + + +def actor_id_to_secret_name(actor_id: str) -> str: + safe = actor_id.replace(':', '-').replace('/', '-') + return f'agent-claw/google-credentials/{safe}' + + +def _redirect(url: str) -> dict: + return {'statusCode': 302, 'headers': {'Location': url}, 'body': ''} + + +def _html(body: str, status: int = 200) -> dict: + return {'statusCode': status, 'headers': {'Content-Type': 'text/html'}, 'body': body} + + +def handler(event, context): + path = event.get('rawPath') or event.get('path', '') + params = event.get('queryStringParameters') or {} + + if path.endswith('/oauth/start'): + return handle_start(params) + elif path.endswith('/oauth/callback'): + return handle_callback(params) + else: + return {'statusCode': 404, 'body': 'Not found'} + + +def handle_start(params: dict) -> dict: + actor_id = params.get('actor_id', '') + if not actor_id: + return _html('

Missing actor_id

', 400) + + client_id, _ = get_oauth_client() + redirect_uri = os.environ['OAUTH_REDIRECT_URI'] + + # Encode actor_id in state (base64 to keep URL-safe) + state = base64.urlsafe_b64encode(actor_id.encode()).decode().rstrip('=') + + auth_url = ( + 'https://accounts.google.com/o/oauth2/v2/auth?' + + urllib.parse.urlencode({ + 'client_id': client_id, + 'redirect_uri': redirect_uri, + 'response_type': 'code', + 'scope': SCOPES, + 'access_type': 'offline', + 'prompt': 'consent', + 'state': state, + }) + ) + return _redirect(auth_url) + + +def handle_callback(params: dict) -> dict: + code = params.get('code', '') + state = params.get('state', '') + error = params.get('error', '') + + if error: + return _html(f'

OAuth error: {error}

', 400) + if not code or not state: + return _html('

Missing code or state

', 400) + + # Decode actor_id from state + try: + padding = 4 - len(state) % 4 + actor_id = base64.urlsafe_b64decode(state + '=' * padding).decode() + except Exception: + return _html('

Invalid state

', 400) + + client_id, client_secret = get_oauth_client() + redirect_uri = os.environ['OAUTH_REDIRECT_URI'] + + # Exchange code for tokens + token_data = urllib.parse.urlencode({ + 'code': code, + 'client_id': client_id, + 'client_secret': client_secret, + 'redirect_uri': redirect_uri, + 'grant_type': 'authorization_code', + }).encode() + + req = urllib.request.Request( + 'https://oauth2.googleapis.com/token', + data=token_data, + headers={'Content-Type': 'application/x-www-form-urlencoded'}, + ) + try: + with urllib.request.urlopen(req, timeout=15) as resp: + tokens = json.loads(resp.read()) + except Exception as e: + print(f'[oauth] Token exchange failed: {e}') + return _html(f'

Token exchange failed: {e}

', 500) + + # Fetch user email from Google + user_email = '' + try: + id_token_payload = tokens.get('id_token', '').split('.')[1] + padding = 4 - len(id_token_payload) % 4 + claims = json.loads(base64.urlsafe_b64decode(id_token_payload + '=' * padding)) + user_email = claims.get('email', '') + except Exception: + pass + + if not user_email: + # Fallback: call userinfo endpoint + try: + access_token = tokens.get('access_token', '') + req2 = urllib.request.Request( + 'https://www.googleapis.com/oauth2/v3/userinfo', + headers={'Authorization': f'Bearer {access_token}'}, + ) + with urllib.request.urlopen(req2, timeout=10) as resp2: + user_email = json.loads(resp2.read()).get('email', '') + except Exception as e: + print(f'[oauth] userinfo fetch failed: {e}') + + # Build credentials dict (google-auth format) + creds = { + 'token': tokens.get('access_token'), + 'refresh_token': tokens.get('refresh_token'), + 'token_uri': 'https://oauth2.googleapis.com/token', + 'client_id': client_id, + 'client_secret': client_secret, + 'scopes': SCOPES.split(), + 'email': user_email, + 'user_email': user_email, + } + if tokens.get('expires_in'): + creds['expiry'] = time.strftime( + '%Y-%m-%dT%H:%M:%SZ', + time.gmtime(time.time() + int(tokens['expires_in'])) + ) + + # Store in Secrets Manager + secret_name = actor_id_to_secret_name(actor_id) + sm = get_sm() + try: + sm.create_secret(Name=secret_name, SecretString=json.dumps(creds)) + except sm.exceptions.ResourceExistsException: + sm.put_secret_value(SecretId=secret_name, SecretString=json.dumps(creds)) + print(f'[oauth] Stored credentials for actor={actor_id} email={user_email}') + + # Update DynamoDB users table with google_email + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and actor_id: + try: + get_ddb().Table(table_name).update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET google_email = :e', + ExpressionAttributeValues={':e': user_email}, + ) + except Exception as e: + print(f'[oauth] DynamoDB update failed: {e}') + + return _html( + f'

✅ Google account connected!

' + f'

Connected {user_email} to your agent account.

' + f'

You can close this window and return to Telegram.

' + ) diff --git a/cdk/cdk.out/asset.5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e/requirements.txt b/cdk/cdk.out/asset.5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/asset.6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370/handler.py b/cdk/cdk.out/asset.6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370/handler.py new file mode 100644 index 0000000..12b797f --- /dev/null +++ b/cdk/cdk.out/asset.6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370/handler.py @@ -0,0 +1,192 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + _agentcore = boto3.client('bedrock-agentcore', region_name='us-east-1') + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps({'chat_id': chat_id, 'text': text}).encode() + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + urllib.request.urlopen(req, timeout=10) + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), + 'allowed': user_profile.get('allowed', True), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Consume streaming response (agent delivers to Telegram via send_message tool) + for chunk in response.get('response', []): + pass # intentional no-op — agent handles delivery internally + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370/requirements.txt b/cdk/cdk.out/asset.6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/manifest.json b/cdk/cdk.out/manifest.json index 3a205be..277a81c 100644 --- a/cdk/cdk.out/manifest.json +++ b/cdk/cdk.out/manifest.json @@ -18,7 +18,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-deploy-role-495395224548-us-east-1", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-cfn-exec-role-495395224548-us-east-1", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-495395224548-us-east-1/2765094d543818b111d837ea62bad41260a47615c5b99bc608a58e99f24d5b85.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-495395224548-us-east-1/fdf1ff81e9e0ded898f1c1d03a2bb8bbe0bbf63689426c24072f179b49b527c6.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ diff --git a/cdk/cdk.out/tree.json b/cdk/cdk.out/tree.json index 9d429ab..db3a6b2 100644 --- a/cdk/cdk.out/tree.json +++ b/cdk/cdk.out/tree.json @@ -1 +1 @@ -{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"aws-cdk-lib.App","version":"2.252.0"},"children":{"AgentClawStack":{"id":"AgentClawStack","path":"AgentClawStack","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"2.252.0"},"children":{"TelegramBotToken":{"id":"TelegramBotToken","path":"AgentClawStack/TelegramBotToken","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"BraveApiKey":{"id":"BraveApiKey","path":"AgentClawStack/BraveApiKey","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"WorkspaceBucket":{"id":"WorkspaceBucket","path":"AgentClawStack/WorkspaceBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}},"SessionStore":{"id":"SessionStore","path":"AgentClawStack/SessionStore","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.Table","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/SessionStore/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.CfnTable","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::DynamoDB::Table","aws:cdk:cloudformation:logicalId":"SessionStore8C86EEFE","aws:cdk:cloudformation:props":{"attributeDefinitions":[{"attributeName":"actor_id","attributeType":"S"}],"billingMode":"PAY_PER_REQUEST","keySchema":[{"attributeName":"actor_id","keyType":"HASH"}],"tableName":"agent-claw-sessions","timeToLiveSpecification":{"attributeName":"ttl","enabled":true}}}},"ScalingRole":{"id":"ScalingRole","path":"AgentClawStack/SessionStore/ScalingRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}}}},"UsersTable":{"id":"UsersTable","path":"AgentClawStack/UsersTable","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.Table","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/UsersTable/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.CfnTable","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::DynamoDB::Table","aws:cdk:cloudformation:logicalId":"UsersTable9725E9C8","aws:cdk:cloudformation:props":{"attributeDefinitions":[{"attributeName":"actor_id","attributeType":"S"}],"billingMode":"PAY_PER_REQUEST","keySchema":[{"attributeName":"actor_id","keyType":"HASH"}],"tableName":"agent-claw-users"}}},"ScalingRole":{"id":"ScalingRole","path":"AgentClawStack/UsersTable/ScalingRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}}}},"MessageQueue":{"id":"MessageQueue","path":"AgentClawStack/MessageQueue","constructInfo":{"fqn":"aws-cdk-lib.aws_sqs.Queue","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/MessageQueue/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_sqs.CfnQueue","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::SQS::Queue","aws:cdk:cloudformation:logicalId":"MessageQueue7A3BF959","aws:cdk:cloudformation:props":{"contentBasedDeduplication":false,"fifoQueue":true,"queueName":"agent-claw-messages.fifo","receiveMessageWaitTimeSeconds":20,"visibilityTimeout":900}}}}},"TgIngest":{"id":"TgIngest","path":"AgentClawStack/TgIngest","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"2.252.0"},"children":{"ServiceRole":{"id":"ServiceRole","path":"AgentClawStack/TgIngest/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/TgIngest/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"TgIngestServiceRoleB96980B6","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/TgIngest/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/TgIngest/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"TgIngestServiceRoleDefaultPolicyCC51E135","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["sqs:SendMessage","sqs:GetQueueAttributes","sqs:GetQueueUrl"],"Effect":"Allow","Resource":{"Fn::GetAtt":["MessageQueue7A3BF959","Arn"]}},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"}],"Version":"2012-10-17"},"policyName":"TgIngestServiceRoleDefaultPolicyCC51E135","roles":[{"Ref":"TgIngestServiceRoleB96980B6"}]}}}}}}},"Code":{"id":"Code","path":"AgentClawStack/TgIngest/Code","constructInfo":{"fqn":"aws-cdk-lib.aws_s3_assets.Asset","version":"2.252.0"},"children":{"Stage":{"id":"Stage","path":"AgentClawStack/TgIngest/Code/Stage","constructInfo":{"fqn":"aws-cdk-lib.AssetStaging","version":"2.252.0"}},"AssetBucket":{"id":"AssetBucket","path":"AgentClawStack/TgIngest/Code/AssetBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}}}},"Resource":{"id":"Resource","path":"AgentClawStack/TgIngest/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:logicalId":"TgIngest4CB35C2F","aws:cdk:cloudformation:props":{"code":{"s3Bucket":"cdk-hnb659fds-assets-495395224548-us-east-1","s3Key":"8da48fd743d1e2cb70d8d1935cee795b6f8cf02609db05e2b8f28449be9ef875.zip"},"environment":{"variables":{"MESSAGE_QUEUE_URL":{"Ref":"MessageQueue7A3BF959"},"TELEGRAM_BOT_TOKEN_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3","TELEGRAM_WEBHOOK_SECRET":""}},"functionName":"agent-claw-tg-ingest","handler":"handler.handler","memorySize":128,"role":{"Fn::GetAtt":["TgIngestServiceRoleB96980B6","Arn"]},"runtime":"python3.12","timeout":10}}}}},"AgentRunner":{"id":"AgentRunner","path":"AgentClawStack/AgentRunner","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"2.252.0"},"children":{"ServiceRole":{"id":"ServiceRole","path":"AgentClawStack/AgentRunner/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"AgentRunnerServiceRole40CA0A00","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/AgentRunner/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"AgentRunnerServiceRoleDefaultPolicyA584A5CF","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["SessionStore8C86EEFE","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["SessionStore8C86EEFE","Arn"]}]},{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["s3:GetObject*","s3:GetBucket*","s3:List*"],"Effect":"Allow","Resource":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548"]]},{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548/*"]]}]},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi"},{"Action":["sqs:ReceiveMessage","sqs:ChangeMessageVisibility","sqs:GetQueueUrl","sqs:DeleteMessage","sqs:GetQueueAttributes"],"Effect":"Allow","Resource":{"Fn::GetAtt":["MessageQueue7A3BF959","Arn"]}},{"Action":"bedrock-agentcore:InvokeAgentRuntime","Effect":"Allow","Resource":"*"}],"Version":"2012-10-17"},"policyName":"AgentRunnerServiceRoleDefaultPolicyA584A5CF","roles":[{"Ref":"AgentRunnerServiceRole40CA0A00"}]}}}}}}},"Code":{"id":"Code","path":"AgentClawStack/AgentRunner/Code","constructInfo":{"fqn":"aws-cdk-lib.aws_s3_assets.Asset","version":"2.252.0"},"children":{"Stage":{"id":"Stage","path":"AgentClawStack/AgentRunner/Code/Stage","constructInfo":{"fqn":"aws-cdk-lib.AssetStaging","version":"2.252.0"}},"AssetBucket":{"id":"AssetBucket","path":"AgentClawStack/AgentRunner/Code/AssetBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}}}},"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:logicalId":"AgentRunnerBDE3FA56","aws:cdk:cloudformation:props":{"code":{"s3Bucket":"cdk-hnb659fds-assets-495395224548-us-east-1","s3Key":"7053cd1618f5f520a7aac409588128f920d8fe76791c1dbcc65610454d1a5387.zip"},"environment":{"variables":{"SESSION_TABLE_NAME":{"Ref":"SessionStore8C86EEFE"},"WORKSPACE_BUCKET_NAME":"agent-claw-workspace-495395224548","TELEGRAM_BOT_TOKEN_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3","BRAVE_API_KEY_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi","RUNTIME_1_ARN":"arn:aws:bedrock-agentcore:us-east-1:495395224548:runtime/agentclaw_agent_claw_main-vTRGIEG6ON","AWS_REGION_NAME":"us-east-1","USERS_TABLE_NAME":{"Ref":"UsersTable9725E9C8"},"WORKSPACE_MCP_URL":"https://25hugrzw4uwtueeg77jsmft6lq0wunmd.lambda-url.us-east-1.on.aws/mcp"}},"functionName":"agent-claw-agent-runner","handler":"handler.handler","memorySize":256,"role":{"Fn::GetAtt":["AgentRunnerServiceRole40CA0A00","Arn"]},"runtime":"python3.12","timeout":900}}},"SqsEventSource:AgentClawStackMessageQueue9AF4DF23":{"id":"SqsEventSource:AgentClawStackMessageQueue9AF4DF23","path":"AgentClawStack/AgentRunner/SqsEventSource:AgentClawStackMessageQueue9AF4DF23","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.EventSourceMapping","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/SqsEventSource:AgentClawStackMessageQueue9AF4DF23/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnEventSourceMapping","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::EventSourceMapping","aws:cdk:cloudformation:logicalId":"AgentRunnerSqsEventSourceAgentClawStackMessageQueue9AF4DF234671B32B","aws:cdk:cloudformation:props":{"batchSize":10,"enabled":true,"eventSourceArn":{"Fn::GetAtt":["MessageQueue7A3BF959","Arn"]},"functionName":{"Ref":"AgentRunnerBDE3FA56"}}}}}}}},"WebhookApi":{"id":"WebhookApi","path":"AgentClawStack/WebhookApi","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpApi","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnApi","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Api","aws:cdk:cloudformation:logicalId":"WebhookApi28122C53","aws:cdk:cloudformation:props":{"name":"agent-claw-webhook","protocolType":"HTTP"}}},"DefaultStage":{"id":"DefaultStage","path":"AgentClawStack/WebhookApi/DefaultStage","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpStage","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/DefaultStage/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnStage","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Stage","aws:cdk:cloudformation:logicalId":"WebhookApiDefaultStageC0BC9CA5","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"autoDeploy":true,"stageName":"$default"}}}}},"POST--telegram":{"id":"POST--telegram","path":"AgentClawStack/WebhookApi/POST--telegram","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpRoute","version":"2.252.0"},"children":{"TgIngestIntegration":{"id":"TgIngestIntegration","path":"AgentClawStack/WebhookApi/POST--telegram/TgIngestIntegration","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpIntegration","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/POST--telegram/TgIngestIntegration/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnIntegration","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Integration","aws:cdk:cloudformation:logicalId":"WebhookApiPOSTtelegramTgIngestIntegration9EE5BB85","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"integrationType":"AWS_PROXY","integrationUri":{"Fn::GetAtt":["TgIngest4CB35C2F","Arn"]},"payloadFormatVersion":"2.0"}}}}},"TgIngestIntegration-Permission":{"id":"TgIngestIntegration-Permission","path":"AgentClawStack/WebhookApi/POST--telegram/TgIngestIntegration-Permission","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnPermission","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Permission","aws:cdk:cloudformation:logicalId":"WebhookApiPOSTtelegramTgIngestIntegrationPermissionFEBC2E3B","aws:cdk:cloudformation:props":{"action":"lambda:InvokeFunction","functionName":{"Fn::GetAtt":["TgIngest4CB35C2F","Arn"]},"principal":"apigateway.amazonaws.com","sourceArn":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":execute-api:us-east-1:495395224548:",{"Ref":"WebhookApi28122C53"},"/*/*/telegram"]]}}}},"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/POST--telegram/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnRoute","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Route","aws:cdk:cloudformation:logicalId":"WebhookApiPOSTtelegramF7127CFF","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"authorizationType":"NONE","routeKey":"POST /telegram","target":{"Fn::Join":["",["integrations/",{"Ref":"WebhookApiPOSTtelegramTgIngestIntegration9EE5BB85"}]]}}}}}}}},"Runtime1Role":{"id":"Runtime1Role","path":"AgentClawStack/Runtime1Role","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/Runtime1Role/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"Runtime1RoleA7A82078","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"bedrock-agentcore.amazonaws.com"}}],"Version":"2012-10-17"},"description":"Execution role for agent-claw Runtime 1 (main assistant)"}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/Runtime1Role/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/Runtime1Role/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"Runtime1RoleDefaultPolicy1A3D5ACF","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["bedrock:InvokeModel","bedrock:InvokeModelWithResponseStream"],"Effect":"Allow","Resource":"*"},{"Action":["s3:GetObject*","s3:GetBucket*","s3:List*"],"Effect":"Allow","Resource":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548"]]},{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548/*"]]}]},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi"},{"Action":["bedrock-agentcore:CreateEvent","bedrock-agentcore:ListEvents","bedrock-agentcore:RetrieveMemoryRecords"],"Effect":"Allow","Resource":"*"},{"Action":"lambda:InvokeFunctionUrl","Condition":{"StringEquals":{"lambda:FunctionUrlAuthType":"AWS_IAM"}},"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":lambda:us-east-1:495395224548:function:agent-claw-workspace-mcp"]]},"Sid":"WorkspaceMcpInvoke"},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-workspace-credentials-??????"]]}},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"]]}}],"Version":"2012-10-17"},"policyName":"Runtime1RoleDefaultPolicy1A3D5ACF","roles":[{"Ref":"Runtime1RoleA7A82078"}]}}}}}}},"GoogleWorkspaceCredentials":{"id":"GoogleWorkspaceCredentials","path":"AgentClawStack/GoogleWorkspaceCredentials","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"GoogleOAuthClient":{"id":"GoogleOAuthClient","path":"AgentClawStack/GoogleOAuthClient","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"WorkspaceMcpRole":{"id":"WorkspaceMcpRole","path":"AgentClawStack/WorkspaceMcpRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"},"children":{"Policy":{"id":"Policy","path":"AgentClawStack/WorkspaceMcpRole/Policy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WorkspaceMcpRole/Policy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"WorkspaceMcpRolePolicy5B8B0072","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-workspace-credentials-??????"]]}},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"]]}}],"Version":"2012-10-17"},"policyName":"WorkspaceMcpRolePolicy5B8B0072","roles":["agent-claw-workspace-mcp-role"]}}}}}}},"WorkspaceMcp":{"id":"WorkspaceMcp","path":"AgentClawStack/WorkspaceMcp","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.FunctionBase","version":"2.252.0"}},"WorkspaceMcpFunctionUrl":{"id":"WorkspaceMcpFunctionUrl","path":"AgentClawStack/WorkspaceMcpFunctionUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"GoogleCredentialsSecretArn":{"id":"GoogleCredentialsSecretArn","path":"AgentClawStack/GoogleCredentialsSecretArn","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"WebhookUrl":{"id":"WebhookUrl","path":"AgentClawStack/WebhookUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"WorkspaceBucketName":{"id":"WorkspaceBucketName","path":"AgentClawStack/WorkspaceBucketName","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"SessionTableName":{"id":"SessionTableName","path":"AgentClawStack/SessionTableName","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"UsersTableName":{"id":"UsersTableName","path":"AgentClawStack/UsersTableName","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"MessageQueueUrl":{"id":"MessageQueueUrl","path":"AgentClawStack/MessageQueueUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"Runtime1RoleArn":{"id":"Runtime1RoleArn","path":"AgentClawStack/Runtime1RoleArn","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"CDKMetadata":{"id":"CDKMetadata","path":"AgentClawStack/CDKMetadata","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"},"children":{"Default":{"id":"Default","path":"AgentClawStack/CDKMetadata/Default","constructInfo":{"fqn":"aws-cdk-lib.CfnResource","version":"2.252.0"}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"AgentClawStack/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"2.252.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"AgentClawStack/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"2.252.0"}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"}}}}} \ No newline at end of file +{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"aws-cdk-lib.App","version":"2.252.0"},"children":{"AgentClawStack":{"id":"AgentClawStack","path":"AgentClawStack","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"2.252.0"},"children":{"TelegramBotToken":{"id":"TelegramBotToken","path":"AgentClawStack/TelegramBotToken","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"BraveApiKey":{"id":"BraveApiKey","path":"AgentClawStack/BraveApiKey","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"WorkspaceBucket":{"id":"WorkspaceBucket","path":"AgentClawStack/WorkspaceBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}},"SessionStore":{"id":"SessionStore","path":"AgentClawStack/SessionStore","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.Table","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/SessionStore/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.CfnTable","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::DynamoDB::Table","aws:cdk:cloudformation:logicalId":"SessionStore8C86EEFE","aws:cdk:cloudformation:props":{"attributeDefinitions":[{"attributeName":"actor_id","attributeType":"S"}],"billingMode":"PAY_PER_REQUEST","keySchema":[{"attributeName":"actor_id","keyType":"HASH"}],"tableName":"agent-claw-sessions","timeToLiveSpecification":{"attributeName":"ttl","enabled":true}}}},"ScalingRole":{"id":"ScalingRole","path":"AgentClawStack/SessionStore/ScalingRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}}}},"UsersTable":{"id":"UsersTable","path":"AgentClawStack/UsersTable","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.Table","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/UsersTable/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.CfnTable","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::DynamoDB::Table","aws:cdk:cloudformation:logicalId":"UsersTable9725E9C8","aws:cdk:cloudformation:props":{"attributeDefinitions":[{"attributeName":"actor_id","attributeType":"S"}],"billingMode":"PAY_PER_REQUEST","keySchema":[{"attributeName":"actor_id","keyType":"HASH"}],"tableName":"agent-claw-users"}}},"ScalingRole":{"id":"ScalingRole","path":"AgentClawStack/UsersTable/ScalingRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}}}},"MessageQueue":{"id":"MessageQueue","path":"AgentClawStack/MessageQueue","constructInfo":{"fqn":"aws-cdk-lib.aws_sqs.Queue","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/MessageQueue/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_sqs.CfnQueue","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::SQS::Queue","aws:cdk:cloudformation:logicalId":"MessageQueue7A3BF959","aws:cdk:cloudformation:props":{"contentBasedDeduplication":false,"fifoQueue":true,"queueName":"agent-claw-messages.fifo","receiveMessageWaitTimeSeconds":20,"visibilityTimeout":900}}}}},"TgIngest":{"id":"TgIngest","path":"AgentClawStack/TgIngest","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"2.252.0"},"children":{"ServiceRole":{"id":"ServiceRole","path":"AgentClawStack/TgIngest/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/TgIngest/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"TgIngestServiceRoleB96980B6","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/TgIngest/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/TgIngest/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"TgIngestServiceRoleDefaultPolicyCC51E135","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["sqs:SendMessage","sqs:GetQueueAttributes","sqs:GetQueueUrl"],"Effect":"Allow","Resource":{"Fn::GetAtt":["MessageQueue7A3BF959","Arn"]}},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"}],"Version":"2012-10-17"},"policyName":"TgIngestServiceRoleDefaultPolicyCC51E135","roles":[{"Ref":"TgIngestServiceRoleB96980B6"}]}}}}}}},"Code":{"id":"Code","path":"AgentClawStack/TgIngest/Code","constructInfo":{"fqn":"aws-cdk-lib.aws_s3_assets.Asset","version":"2.252.0"},"children":{"Stage":{"id":"Stage","path":"AgentClawStack/TgIngest/Code/Stage","constructInfo":{"fqn":"aws-cdk-lib.AssetStaging","version":"2.252.0"}},"AssetBucket":{"id":"AssetBucket","path":"AgentClawStack/TgIngest/Code/AssetBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}}}},"Resource":{"id":"Resource","path":"AgentClawStack/TgIngest/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:logicalId":"TgIngest4CB35C2F","aws:cdk:cloudformation:props":{"code":{"s3Bucket":"cdk-hnb659fds-assets-495395224548-us-east-1","s3Key":"8da48fd743d1e2cb70d8d1935cee795b6f8cf02609db05e2b8f28449be9ef875.zip"},"environment":{"variables":{"MESSAGE_QUEUE_URL":{"Ref":"MessageQueue7A3BF959"},"TELEGRAM_BOT_TOKEN_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3","TELEGRAM_WEBHOOK_SECRET":""}},"functionName":"agent-claw-tg-ingest","handler":"handler.handler","memorySize":128,"role":{"Fn::GetAtt":["TgIngestServiceRoleB96980B6","Arn"]},"runtime":"python3.12","timeout":10}}}}},"AgentRunner":{"id":"AgentRunner","path":"AgentClawStack/AgentRunner","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"2.252.0"},"children":{"ServiceRole":{"id":"ServiceRole","path":"AgentClawStack/AgentRunner/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"AgentRunnerServiceRole40CA0A00","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/AgentRunner/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"AgentRunnerServiceRoleDefaultPolicyA584A5CF","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["SessionStore8C86EEFE","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["SessionStore8C86EEFE","Arn"]}]},{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["s3:GetObject*","s3:GetBucket*","s3:List*"],"Effect":"Allow","Resource":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548"]]},{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548/*"]]}]},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi"},{"Action":["sqs:ReceiveMessage","sqs:ChangeMessageVisibility","sqs:GetQueueUrl","sqs:DeleteMessage","sqs:GetQueueAttributes"],"Effect":"Allow","Resource":{"Fn::GetAtt":["MessageQueue7A3BF959","Arn"]}},{"Action":"bedrock-agentcore:InvokeAgentRuntime","Effect":"Allow","Resource":"*"}],"Version":"2012-10-17"},"policyName":"AgentRunnerServiceRoleDefaultPolicyA584A5CF","roles":[{"Ref":"AgentRunnerServiceRole40CA0A00"}]}}}}}}},"Code":{"id":"Code","path":"AgentClawStack/AgentRunner/Code","constructInfo":{"fqn":"aws-cdk-lib.aws_s3_assets.Asset","version":"2.252.0"},"children":{"Stage":{"id":"Stage","path":"AgentClawStack/AgentRunner/Code/Stage","constructInfo":{"fqn":"aws-cdk-lib.AssetStaging","version":"2.252.0"}},"AssetBucket":{"id":"AssetBucket","path":"AgentClawStack/AgentRunner/Code/AssetBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}}}},"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:logicalId":"AgentRunnerBDE3FA56","aws:cdk:cloudformation:props":{"code":{"s3Bucket":"cdk-hnb659fds-assets-495395224548-us-east-1","s3Key":"6f6fdf79f33a947f3e50ffd783a72d04ab5f29ba299a5d51b3ecd2c2eb311370.zip"},"environment":{"variables":{"SESSION_TABLE_NAME":{"Ref":"SessionStore8C86EEFE"},"WORKSPACE_BUCKET_NAME":"agent-claw-workspace-495395224548","TELEGRAM_BOT_TOKEN_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3","BRAVE_API_KEY_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi","RUNTIME_1_ARN":"arn:aws:bedrock-agentcore:us-east-1:495395224548:runtime/agentclaw_agent_claw_main-vTRGIEG6ON","AWS_REGION_NAME":"us-east-1","USERS_TABLE_NAME":{"Ref":"UsersTable9725E9C8"},"WORKSPACE_MCP_URL":"https://25hugrzw4uwtueeg77jsmft6lq0wunmd.lambda-url.us-east-1.on.aws/mcp"}},"functionName":"agent-claw-agent-runner","handler":"handler.handler","memorySize":256,"role":{"Fn::GetAtt":["AgentRunnerServiceRole40CA0A00","Arn"]},"runtime":"python3.12","timeout":900}}},"SqsEventSource:AgentClawStackMessageQueue9AF4DF23":{"id":"SqsEventSource:AgentClawStackMessageQueue9AF4DF23","path":"AgentClawStack/AgentRunner/SqsEventSource:AgentClawStackMessageQueue9AF4DF23","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.EventSourceMapping","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/SqsEventSource:AgentClawStackMessageQueue9AF4DF23/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnEventSourceMapping","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::EventSourceMapping","aws:cdk:cloudformation:logicalId":"AgentRunnerSqsEventSourceAgentClawStackMessageQueue9AF4DF234671B32B","aws:cdk:cloudformation:props":{"batchSize":10,"enabled":true,"eventSourceArn":{"Fn::GetAtt":["MessageQueue7A3BF959","Arn"]},"functionName":{"Ref":"AgentRunnerBDE3FA56"}}}}}}}},"WebhookApi":{"id":"WebhookApi","path":"AgentClawStack/WebhookApi","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpApi","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnApi","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Api","aws:cdk:cloudformation:logicalId":"WebhookApi28122C53","aws:cdk:cloudformation:props":{"name":"agent-claw-webhook","protocolType":"HTTP"}}},"DefaultStage":{"id":"DefaultStage","path":"AgentClawStack/WebhookApi/DefaultStage","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpStage","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/DefaultStage/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnStage","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Stage","aws:cdk:cloudformation:logicalId":"WebhookApiDefaultStageC0BC9CA5","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"autoDeploy":true,"stageName":"$default"}}}}},"POST--telegram":{"id":"POST--telegram","path":"AgentClawStack/WebhookApi/POST--telegram","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpRoute","version":"2.252.0"},"children":{"TgIngestIntegration":{"id":"TgIngestIntegration","path":"AgentClawStack/WebhookApi/POST--telegram/TgIngestIntegration","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpIntegration","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/POST--telegram/TgIngestIntegration/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnIntegration","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Integration","aws:cdk:cloudformation:logicalId":"WebhookApiPOSTtelegramTgIngestIntegration9EE5BB85","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"integrationType":"AWS_PROXY","integrationUri":{"Fn::GetAtt":["TgIngest4CB35C2F","Arn"]},"payloadFormatVersion":"2.0"}}}}},"TgIngestIntegration-Permission":{"id":"TgIngestIntegration-Permission","path":"AgentClawStack/WebhookApi/POST--telegram/TgIngestIntegration-Permission","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnPermission","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Permission","aws:cdk:cloudformation:logicalId":"WebhookApiPOSTtelegramTgIngestIntegrationPermissionFEBC2E3B","aws:cdk:cloudformation:props":{"action":"lambda:InvokeFunction","functionName":{"Fn::GetAtt":["TgIngest4CB35C2F","Arn"]},"principal":"apigateway.amazonaws.com","sourceArn":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":execute-api:us-east-1:495395224548:",{"Ref":"WebhookApi28122C53"},"/*/*/telegram"]]}}}},"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/POST--telegram/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnRoute","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Route","aws:cdk:cloudformation:logicalId":"WebhookApiPOSTtelegramF7127CFF","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"authorizationType":"NONE","routeKey":"POST /telegram","target":{"Fn::Join":["",["integrations/",{"Ref":"WebhookApiPOSTtelegramTgIngestIntegration9EE5BB85"}]]}}}}}},"GET--oauth--start":{"id":"GET--oauth--start","path":"AgentClawStack/WebhookApi/GET--oauth--start","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpRoute","version":"2.252.0"},"children":{"OAuthStartIntegration":{"id":"OAuthStartIntegration","path":"AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpIntegration","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnIntegration","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Integration","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthstartOAuthStartIntegrationA546443F","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"integrationType":"AWS_PROXY","integrationUri":{"Fn::GetAtt":["OAuthHandlerC97C2476","Arn"]},"payloadFormatVersion":"2.0"}}}}},"OAuthStartIntegration-Permission":{"id":"OAuthStartIntegration-Permission","path":"AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration-Permission","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnPermission","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Permission","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthstartOAuthStartIntegrationPermission38BAEF6D","aws:cdk:cloudformation:props":{"action":"lambda:InvokeFunction","functionName":{"Fn::GetAtt":["OAuthHandlerC97C2476","Arn"]},"principal":"apigateway.amazonaws.com","sourceArn":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":execute-api:us-east-1:495395224548:",{"Ref":"WebhookApi28122C53"},"/*/*/oauth/start"]]}}}},"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/GET--oauth--start/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnRoute","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Route","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthstart6DCA713A","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"authorizationType":"NONE","routeKey":"GET /oauth/start","target":{"Fn::Join":["",["integrations/",{"Ref":"WebhookApiGEToauthstartOAuthStartIntegrationA546443F"}]]}}}}}},"GET--oauth--callback":{"id":"GET--oauth--callback","path":"AgentClawStack/WebhookApi/GET--oauth--callback","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpRoute","version":"2.252.0"},"children":{"OAuthCallbackIntegration":{"id":"OAuthCallbackIntegration","path":"AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpIntegration","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnIntegration","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Integration","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthcallbackOAuthCallbackIntegrationCFBBEB09","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"integrationType":"AWS_PROXY","integrationUri":{"Fn::GetAtt":["OAuthHandlerC97C2476","Arn"]},"payloadFormatVersion":"2.0"}}}}},"OAuthCallbackIntegration-Permission":{"id":"OAuthCallbackIntegration-Permission","path":"AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration-Permission","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnPermission","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Permission","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthcallbackOAuthCallbackIntegrationPermission6BA3A5AD","aws:cdk:cloudformation:props":{"action":"lambda:InvokeFunction","functionName":{"Fn::GetAtt":["OAuthHandlerC97C2476","Arn"]},"principal":"apigateway.amazonaws.com","sourceArn":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":execute-api:us-east-1:495395224548:",{"Ref":"WebhookApi28122C53"},"/*/*/oauth/callback"]]}}}},"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/GET--oauth--callback/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnRoute","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Route","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthcallbackFC1F6BCD","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"authorizationType":"NONE","routeKey":"GET /oauth/callback","target":{"Fn::Join":["",["integrations/",{"Ref":"WebhookApiGEToauthcallbackOAuthCallbackIntegrationCFBBEB09"}]]}}}}}}}},"Runtime1Role":{"id":"Runtime1Role","path":"AgentClawStack/Runtime1Role","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/Runtime1Role/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"Runtime1RoleA7A82078","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"bedrock-agentcore.amazonaws.com"}}],"Version":"2012-10-17"},"description":"Execution role for agent-claw Runtime 1 (main assistant)"}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/Runtime1Role/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/Runtime1Role/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"Runtime1RoleDefaultPolicy1A3D5ACF","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["bedrock:InvokeModel","bedrock:InvokeModelWithResponseStream"],"Effect":"Allow","Resource":"*"},{"Action":["s3:GetObject*","s3:GetBucket*","s3:List*"],"Effect":"Allow","Resource":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548"]]},{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548/*"]]}]},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi"},{"Action":["bedrock-agentcore:CreateEvent","bedrock-agentcore:ListEvents","bedrock-agentcore:RetrieveMemoryRecords"],"Effect":"Allow","Resource":"*"},{"Action":"lambda:InvokeFunctionUrl","Condition":{"StringEquals":{"lambda:FunctionUrlAuthType":"AWS_IAM"}},"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":lambda:us-east-1:495395224548:function:agent-claw-workspace-mcp"]]},"Sid":"WorkspaceMcpInvoke"},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"]]}},{"Action":"secretsmanager:GetSecretValue","Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*","Sid":"PerUserGoogleCredentialsReadRuntime"}],"Version":"2012-10-17"},"policyName":"Runtime1RoleDefaultPolicy1A3D5ACF","roles":[{"Ref":"Runtime1RoleA7A82078"}]}}}}}}},"GoogleOAuthClient":{"id":"GoogleOAuthClient","path":"AgentClawStack/GoogleOAuthClient","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"WorkspaceMcpRole":{"id":"WorkspaceMcpRole","path":"AgentClawStack/WorkspaceMcpRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"},"children":{"Policy":{"id":"Policy","path":"AgentClawStack/WorkspaceMcpRole/Policy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WorkspaceMcpRole/Policy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"WorkspaceMcpRolePolicy5B8B0072","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"]]}},{"Action":"secretsmanager:GetSecretValue","Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*","Sid":"PerUserGoogleCredentialsRead"}],"Version":"2012-10-17"},"policyName":"WorkspaceMcpRolePolicy5B8B0072","roles":["agent-claw-workspace-mcp-role"]}}}}}}},"WorkspaceMcp":{"id":"WorkspaceMcp","path":"AgentClawStack/WorkspaceMcp","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.FunctionBase","version":"2.252.0"}},"OAuthHandler":{"id":"OAuthHandler","path":"AgentClawStack/OAuthHandler","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"2.252.0"},"children":{"ServiceRole":{"id":"ServiceRole","path":"AgentClawStack/OAuthHandler/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/OAuthHandler/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"OAuthHandlerServiceRole9CDCCF9E","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/OAuthHandler/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/OAuthHandler/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"OAuthHandlerServiceRoleDefaultPolicy69D90416","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"]]}},{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["secretsmanager:CreateSecret","secretsmanager:PutSecretValue","secretsmanager:GetSecretValue"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*","Sid":"PerUserGoogleCredentialsWrite"}],"Version":"2012-10-17"},"policyName":"OAuthHandlerServiceRoleDefaultPolicy69D90416","roles":[{"Ref":"OAuthHandlerServiceRole9CDCCF9E"}]}}}}}}},"Code":{"id":"Code","path":"AgentClawStack/OAuthHandler/Code","constructInfo":{"fqn":"aws-cdk-lib.aws_s3_assets.Asset","version":"2.252.0"},"children":{"Stage":{"id":"Stage","path":"AgentClawStack/OAuthHandler/Code/Stage","constructInfo":{"fqn":"aws-cdk-lib.AssetStaging","version":"2.252.0"}},"AssetBucket":{"id":"AssetBucket","path":"AgentClawStack/OAuthHandler/Code/AssetBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}}}},"Resource":{"id":"Resource","path":"AgentClawStack/OAuthHandler/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:logicalId":"OAuthHandlerC97C2476","aws:cdk:cloudformation:props":{"code":{"s3Bucket":"cdk-hnb659fds-assets-495395224548-us-east-1","s3Key":"5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e.zip"},"environment":{"variables":{"GOOGLE_OAUTH_CLIENT_SECRET_ARN":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client"]]},"USERS_TABLE_NAME":{"Ref":"UsersTable9725E9C8"},"OAUTH_REDIRECT_URI":{"Fn::Join":["",["https://",{"Ref":"WebhookApi28122C53"},".execute-api.us-east-1.",{"Ref":"AWS::URLSuffix"},"/oauth/callback"]]}}},"functionName":"agent-claw-oauth-handler","handler":"handler.handler","memorySize":128,"role":{"Fn::GetAtt":["OAuthHandlerServiceRole9CDCCF9E","Arn"]},"runtime":"python3.12","timeout":30}}}}},"WorkspaceMcpFunctionUrl":{"id":"WorkspaceMcpFunctionUrl","path":"AgentClawStack/WorkspaceMcpFunctionUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"OAuthStartUrl":{"id":"OAuthStartUrl","path":"AgentClawStack/OAuthStartUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"OAuthRedirectUri":{"id":"OAuthRedirectUri","path":"AgentClawStack/OAuthRedirectUri","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"WebhookUrl":{"id":"WebhookUrl","path":"AgentClawStack/WebhookUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"WorkspaceBucketName":{"id":"WorkspaceBucketName","path":"AgentClawStack/WorkspaceBucketName","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"SessionTableName":{"id":"SessionTableName","path":"AgentClawStack/SessionTableName","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"UsersTableName":{"id":"UsersTableName","path":"AgentClawStack/UsersTableName","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"MessageQueueUrl":{"id":"MessageQueueUrl","path":"AgentClawStack/MessageQueueUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"Runtime1RoleArn":{"id":"Runtime1RoleArn","path":"AgentClawStack/Runtime1RoleArn","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"CDKMetadata":{"id":"CDKMetadata","path":"AgentClawStack/CDKMetadata","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"},"children":{"Default":{"id":"Default","path":"AgentClawStack/CDKMetadata/Default","constructInfo":{"fqn":"aws-cdk-lib.CfnResource","version":"2.252.0"}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"AgentClawStack/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"2.252.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"AgentClawStack/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"2.252.0"}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"}}}}} \ No newline at end of file diff --git a/cdk/lib/agent-claw-stack.js b/cdk/lib/agent-claw-stack.js index 4f80e09..e8c3172 100644 --- a/cdk/lib/agent-claw-stack.js +++ b/cdk/lib/agent-claw-stack.js @@ -87,6 +87,13 @@ class AgentClawStack extends cdk.Stack { timeToLiveAttribute: 'ttl', removalPolicy: cdk.RemovalPolicy.RETAIN, }); + // ── DynamoDB user registry ───────────────────────────────────────────── + const usersTable = new dynamodb.Table(this, 'UsersTable', { + tableName: 'agent-claw-users', + partitionKey: { name: 'actor_id', type: dynamodb.AttributeType.STRING }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); // ── SQS FIFO message queue ───────────────────────────────────────────── const messageQueue = new sqs.Queue(this, 'MessageQueue', { queueName: 'agent-claw-messages.fifo', @@ -129,6 +136,8 @@ class AgentClawStack extends cdk.Stack { }, }); sessionTable.grantReadWriteData(agentRunnerFn); + usersTable.grantReadWriteData(agentRunnerFn); + agentRunnerFn.addEnvironment('USERS_TABLE_NAME', usersTable.tableName); workspaceBucket.grantRead(agentRunnerFn); botTokenSecret.grantRead(agentRunnerFn); braveApiKeySecret.grantRead(agentRunnerFn); @@ -185,18 +194,17 @@ class AgentClawStack extends cdk.Stack { // TODO: Replace with L2 construct when aws-cdk-lib includes it stable // For now, the runtime ARN must be created manually or via CLI after first synth // and fed back as context param runtime1Arn. - // ── Outputs ──────────────────────────────────────────────────────────── // ── Google Workspace MCP ────────────────────────────────────────────── - // Secrets pre-populated after OAuth flow - const googleCredentialsSecret = secretsmanager.Secret.fromSecretNameV2(this, 'GoogleWorkspaceCredentials', 'agent-claw/google-workspace-credentials'); const googleOAuthClientSecret = secretsmanager.Secret.fromSecretNameV2(this, 'GoogleOAuthClient', 'agent-claw/google-oauth-client'); // workspace-mcp Lambda execution role (import existing — created during initial setup) - // NOTE (tech debt #3): workspaceMcpRole imported but not attached to workspaceMcpFn because - // fromFunctionName() returns an IFunction (no role config). Role was set at Lambda creation. - // To fully codify: delete the manual Lambda, let CDK create it with Code.fromBucket + role. const _workspaceMcpRole = iam.Role.fromRoleName(this, 'WorkspaceMcpRole', 'agent-claw-workspace-mcp-role'); - googleCredentialsSecret.grantRead(_workspaceMcpRole); googleOAuthClientSecret.grantRead(_workspaceMcpRole); + // Grant workspace-mcp role read access to all per-user Google credential secrets + _workspaceMcpRole.addToPrincipalPolicy?.(new iam.PolicyStatement({ + sid: 'PerUserGoogleCredentialsRead', + actions: ['secretsmanager:GetSecretValue'], + resources: [`arn:aws:secretsmanager:${this.region}:${this.account}:secret:agent-claw/google-credentials/*`], + })); // workspace-mcp Lambda — import existing (created with zip + layer, no Docker) const workspaceMcpFn = lambda.Function.fromFunctionName(this, 'WorkspaceMcp', 'agent-claw-workspace-mcp'); // Function URL — AWS_IAM auth (already created, reference for policy attachment) @@ -209,18 +217,72 @@ class AgentClawStack extends cdk.Stack { resources: [workspaceMcpFn.functionArn], conditions: { StringEquals: { 'lambda:FunctionUrlAuthType': 'AWS_IAM' } }, })); + // Grant AgentCore execution role read access to Google OAuth client + per-user credentials + googleOAuthClientSecret.grantRead(runtime1Role); + runtime1Role.addToPolicy(new iam.PolicyStatement({ + sid: 'PerUserGoogleCredentialsReadRuntime', + actions: ['secretsmanager:GetSecretValue'], + resources: [`arn:aws:secretsmanager:${this.region}:${this.account}:secret:agent-claw/google-credentials/*`], + })); // Pass workspace_mcp MCP URL to agent-runner (informational) agentRunnerFn.addEnvironment('WORKSPACE_MCP_URL', workspaceMcpMcpUrl); - // Grant AgentCore execution role read access to Google secrets - googleCredentialsSecret.grantRead(runtime1Role); - googleOAuthClientSecret.grantRead(runtime1Role); + // ── OAuth Handler Lambda ─────────────────────────────────────────────── + const oauthHandlerFn = new lambda.Function(this, 'OAuthHandler', { + functionName: 'agent-claw-oauth-handler', + runtime: lambda.Runtime.PYTHON_3_12, + handler: 'handler.handler', + code: lambda.Code.fromAsset(path.join(__dirname, '../../src/lambdas/oauth-handler')), + timeout: cdk.Duration.seconds(30), + memorySize: 128, + environment: { + GOOGLE_OAUTH_CLIENT_SECRET_ARN: googleOAuthClientSecret.secretArn, + USERS_TABLE_NAME: usersTable.tableName, + // OAUTH_REDIRECT_URI set after API GW URL is known — injected via addEnvironment below + OAUTH_REDIRECT_URI: 'PLACEHOLDER', + }, + }); + googleOAuthClientSecret.grantRead(oauthHandlerFn); + usersTable.grantReadWriteData(oauthHandlerFn); + // Grant OAuth handler write access to per-user credential secrets + oauthHandlerFn.addToRolePolicy(new iam.PolicyStatement({ + sid: 'PerUserGoogleCredentialsWrite', + actions: [ + 'secretsmanager:CreateSecret', + 'secretsmanager:PutSecretValue', + 'secretsmanager:GetSecretValue', + ], + resources: [`arn:aws:secretsmanager:${this.region}:${this.account}:secret:agent-claw/google-credentials/*`], + })); + // Add OAuth routes to existing HTTP API + httpApi.addRoutes({ + path: '/oauth/start', + methods: [apigatewayv2.HttpMethod.GET], + integration: new apigatewayv2integrations.HttpLambdaIntegration('OAuthStartIntegration', oauthHandlerFn), + }); + httpApi.addRoutes({ + path: '/oauth/callback', + methods: [apigatewayv2.HttpMethod.GET], + integration: new apigatewayv2integrations.HttpLambdaIntegration('OAuthCallbackIntegration', oauthHandlerFn), + }); + // Set OAUTH_REDIRECT_URI now that we have the API URL + const oauthRedirectUri = `${httpApi.url}oauth/callback`; + oauthHandlerFn.addEnvironment('OAUTH_REDIRECT_URI', oauthRedirectUri); + // Pass OAuth start URL to AgentCore runtime so agent can generate connect links + const oauthStartUrl = `${httpApi.url}oauth/start`; + // NOTE: AgentCore runtime env vars are set in agentcore.json / agentcore deploy, not CDK. + // Output the URL so it can be manually set in agentcore.json OAUTH_START_URL. + // ── Outputs ──────────────────────────────────────────────────────────── new cdk.CfnOutput(this, 'WorkspaceMcpFunctionUrl', { value: workspaceMcpFunctionUrl, description: 'workspace-mcp Lambda Function URL (MCP endpoint for Gmail/Calendar)', }); - new cdk.CfnOutput(this, 'GoogleCredentialsSecretArn', { - value: googleCredentialsSecret.secretArn, - description: 'Google OAuth user credentials secret ARN', + new cdk.CfnOutput(this, 'OAuthStartUrl', { + value: oauthStartUrl, + description: 'Google OAuth start URL — set as OAUTH_START_URL in agentcore.json', + }); + new cdk.CfnOutput(this, 'OAuthRedirectUri', { + value: oauthRedirectUri, + description: 'Google OAuth redirect URI — register in Google Cloud Console', }); new cdk.CfnOutput(this, 'WebhookUrl', { value: `${httpApi.url}telegram`, @@ -234,6 +296,10 @@ class AgentClawStack extends cdk.Stack { value: sessionTable.tableName, description: 'DynamoDB table for session mapping', }); + new cdk.CfnOutput(this, 'UsersTableName', { + value: usersTable.tableName, + description: 'DynamoDB user registry table', + }); new cdk.CfnOutput(this, 'MessageQueueUrl', { value: messageQueue.queueUrl, description: 'SQS FIFO queue for incoming messages', diff --git a/cdk/lib/agent-claw-stack.ts b/cdk/lib/agent-claw-stack.ts index d7ddb36..1973bdc 100644 --- a/cdk/lib/agent-claw-stack.ts +++ b/cdk/lib/agent-claw-stack.ts @@ -186,26 +186,22 @@ export class AgentClawStack extends cdk.Stack { // For now, the runtime ARN must be created manually or via CLI after first synth // and fed back as context param runtime1Arn. - // ── Outputs ──────────────────────────────────────────────────────────── - // ── Google Workspace MCP ────────────────────────────────────────────── - // Secrets pre-populated after OAuth flow - const googleCredentialsSecret = secretsmanager.Secret.fromSecretNameV2( - this, 'GoogleWorkspaceCredentials', 'agent-claw/google-workspace-credentials' - ); const googleOAuthClientSecret = secretsmanager.Secret.fromSecretNameV2( this, 'GoogleOAuthClient', 'agent-claw/google-oauth-client' ); // workspace-mcp Lambda execution role (import existing — created during initial setup) - // NOTE (tech debt #3): workspaceMcpRole imported but not attached to workspaceMcpFn because - // fromFunctionName() returns an IFunction (no role config). Role was set at Lambda creation. - // To fully codify: delete the manual Lambda, let CDK create it with Code.fromBucket + role. const _workspaceMcpRole = iam.Role.fromRoleName( this, 'WorkspaceMcpRole', 'agent-claw-workspace-mcp-role' ); - googleCredentialsSecret.grantRead(_workspaceMcpRole); googleOAuthClientSecret.grantRead(_workspaceMcpRole); + // Grant workspace-mcp role read access to all per-user Google credential secrets + (_workspaceMcpRole as iam.Role).addToPrincipalPolicy?.(new iam.PolicyStatement({ + sid: 'PerUserGoogleCredentialsRead', + actions: ['secretsmanager:GetSecretValue'], + resources: [`arn:aws:secretsmanager:${this.region}:${this.account}:secret:agent-claw/google-credentials/*`], + })); // workspace-mcp Lambda — import existing (created with zip + layer, no Docker) const workspaceMcpFn = lambda.Function.fromFunctionName( @@ -224,20 +220,83 @@ export class AgentClawStack extends cdk.Stack { conditions: { StringEquals: { 'lambda:FunctionUrlAuthType': 'AWS_IAM' } }, })); + // Grant AgentCore execution role read access to Google OAuth client + per-user credentials + googleOAuthClientSecret.grantRead(runtime1Role); + runtime1Role.addToPolicy(new iam.PolicyStatement({ + sid: 'PerUserGoogleCredentialsReadRuntime', + actions: ['secretsmanager:GetSecretValue'], + resources: [`arn:aws:secretsmanager:${this.region}:${this.account}:secret:agent-claw/google-credentials/*`], + })); + // Pass workspace_mcp MCP URL to agent-runner (informational) agentRunnerFn.addEnvironment('WORKSPACE_MCP_URL', workspaceMcpMcpUrl); - // Grant AgentCore execution role read access to Google secrets - googleCredentialsSecret.grantRead(runtime1Role); - googleOAuthClientSecret.grantRead(runtime1Role); + // ── OAuth Handler Lambda ─────────────────────────────────────────────── + const oauthHandlerFn = new lambda.Function(this, 'OAuthHandler', { + functionName: 'agent-claw-oauth-handler', + runtime: lambda.Runtime.PYTHON_3_12, + handler: 'handler.handler', + code: lambda.Code.fromAsset(path.join(__dirname, '../../src/lambdas/oauth-handler')), + timeout: cdk.Duration.seconds(30), + memorySize: 128, + environment: { + GOOGLE_OAUTH_CLIENT_SECRET_ARN: googleOAuthClientSecret.secretArn, + USERS_TABLE_NAME: usersTable.tableName, + // OAUTH_REDIRECT_URI set after API GW URL is known — injected via addEnvironment below + OAUTH_REDIRECT_URI: 'PLACEHOLDER', + }, + }); + googleOAuthClientSecret.grantRead(oauthHandlerFn); + usersTable.grantReadWriteData(oauthHandlerFn); + // Grant OAuth handler write access to per-user credential secrets + oauthHandlerFn.addToRolePolicy(new iam.PolicyStatement({ + sid: 'PerUserGoogleCredentialsWrite', + actions: [ + 'secretsmanager:CreateSecret', + 'secretsmanager:PutSecretValue', + 'secretsmanager:GetSecretValue', + ], + resources: [`arn:aws:secretsmanager:${this.region}:${this.account}:secret:agent-claw/google-credentials/*`], + })); + + // Add OAuth routes to existing HTTP API + httpApi.addRoutes({ + path: '/oauth/start', + methods: [apigatewayv2.HttpMethod.GET], + integration: new apigatewayv2integrations.HttpLambdaIntegration( + 'OAuthStartIntegration', oauthHandlerFn + ), + }); + httpApi.addRoutes({ + path: '/oauth/callback', + methods: [apigatewayv2.HttpMethod.GET], + integration: new apigatewayv2integrations.HttpLambdaIntegration( + 'OAuthCallbackIntegration', oauthHandlerFn + ), + }); + + // Set OAUTH_REDIRECT_URI now that we have the API URL + const oauthRedirectUri = `${httpApi.url}oauth/callback`; + oauthHandlerFn.addEnvironment('OAUTH_REDIRECT_URI', oauthRedirectUri); + + // Pass OAuth start URL to AgentCore runtime so agent can generate connect links + const oauthStartUrl = `${httpApi.url}oauth/start`; + // NOTE: AgentCore runtime env vars are set in agentcore.json / agentcore deploy, not CDK. + // Output the URL so it can be manually set in agentcore.json OAUTH_START_URL. + + // ── Outputs ──────────────────────────────────────────────────────────── new cdk.CfnOutput(this, 'WorkspaceMcpFunctionUrl', { value: workspaceMcpFunctionUrl, description: 'workspace-mcp Lambda Function URL (MCP endpoint for Gmail/Calendar)', }); - new cdk.CfnOutput(this, 'GoogleCredentialsSecretArn', { - value: googleCredentialsSecret.secretArn, - description: 'Google OAuth user credentials secret ARN', + new cdk.CfnOutput(this, 'OAuthStartUrl', { + value: oauthStartUrl, + description: 'Google OAuth start URL — set as OAUTH_START_URL in agentcore.json', + }); + new cdk.CfnOutput(this, 'OAuthRedirectUri', { + value: oauthRedirectUri, + description: 'Google OAuth redirect URI — register in Google Cloud Console', }); new cdk.CfnOutput(this, 'WebhookUrl', { diff --git a/src/lambdas/agent-runner/handler.py b/src/lambdas/agent-runner/handler.py index 051c651..12b797f 100644 --- a/src/lambdas/agent-runner/handler.py +++ b/src/lambdas/agent-runner/handler.py @@ -161,6 +161,7 @@ def handler(event, context): 'user_profile': { 'display_name': user_profile.get('display_name', actor_id), 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), 'allowed': user_profile.get('allowed', True), }, 'channel_adapter': { diff --git a/src/lambdas/oauth-handler/handler.py b/src/lambdas/oauth-handler/handler.py new file mode 100644 index 0000000..a3bb646 --- /dev/null +++ b/src/lambdas/oauth-handler/handler.py @@ -0,0 +1,212 @@ +""" +Google OAuth handler Lambda. + +Routes: + GET /oauth/start?actor_id=telegram:123 → redirect to Google OAuth consent + GET /oauth/callback?code=...&state=... → exchange code, store tokens, update DynamoDB +""" +import base64 +import hashlib +import hmac +import json +import os +import time +import urllib.parse +import urllib.request + +import boto3 + +_sm = None +_ddb = None + +SCOPES = ' '.join([ + 'https://www.googleapis.com/auth/gmail.modify', + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/spreadsheets', + 'https://www.googleapis.com/auth/documents', + 'openid', + 'email', + 'profile', +]) + + +def get_sm(): + global _sm + if _sm is None: + _sm = boto3.client('secretsmanager', region_name=os.environ.get('AWS_REGION', 'us-east-1')) + return _sm + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_oauth_client() -> tuple[str, str]: + """Return (client_id, client_secret) from Secrets Manager.""" + arn = os.environ['GOOGLE_OAUTH_CLIENT_SECRET_ARN'] + secret = json.loads(get_sm().get_secret_value(SecretId=arn)['SecretString']) + return secret['client_id'], secret['client_secret'] + + +def actor_id_to_secret_name(actor_id: str) -> str: + safe = actor_id.replace(':', '-').replace('/', '-') + return f'agent-claw/google-credentials/{safe}' + + +def _redirect(url: str) -> dict: + return {'statusCode': 302, 'headers': {'Location': url}, 'body': ''} + + +def _html(body: str, status: int = 200) -> dict: + return {'statusCode': status, 'headers': {'Content-Type': 'text/html'}, 'body': body} + + +def handler(event, context): + path = event.get('rawPath') or event.get('path', '') + params = event.get('queryStringParameters') or {} + + if path.endswith('/oauth/start'): + return handle_start(params) + elif path.endswith('/oauth/callback'): + return handle_callback(params) + else: + return {'statusCode': 404, 'body': 'Not found'} + + +def handle_start(params: dict) -> dict: + actor_id = params.get('actor_id', '') + if not actor_id: + return _html('

Missing actor_id

', 400) + + client_id, _ = get_oauth_client() + redirect_uri = os.environ['OAUTH_REDIRECT_URI'] + + # Encode actor_id in state (base64 to keep URL-safe) + state = base64.urlsafe_b64encode(actor_id.encode()).decode().rstrip('=') + + auth_url = ( + 'https://accounts.google.com/o/oauth2/v2/auth?' + + urllib.parse.urlencode({ + 'client_id': client_id, + 'redirect_uri': redirect_uri, + 'response_type': 'code', + 'scope': SCOPES, + 'access_type': 'offline', + 'prompt': 'consent', + 'state': state, + }) + ) + return _redirect(auth_url) + + +def handle_callback(params: dict) -> dict: + code = params.get('code', '') + state = params.get('state', '') + error = params.get('error', '') + + if error: + return _html(f'

OAuth error: {error}

', 400) + if not code or not state: + return _html('

Missing code or state

', 400) + + # Decode actor_id from state + try: + padding = 4 - len(state) % 4 + actor_id = base64.urlsafe_b64decode(state + '=' * padding).decode() + except Exception: + return _html('

Invalid state

', 400) + + client_id, client_secret = get_oauth_client() + redirect_uri = os.environ['OAUTH_REDIRECT_URI'] + + # Exchange code for tokens + token_data = urllib.parse.urlencode({ + 'code': code, + 'client_id': client_id, + 'client_secret': client_secret, + 'redirect_uri': redirect_uri, + 'grant_type': 'authorization_code', + }).encode() + + req = urllib.request.Request( + 'https://oauth2.googleapis.com/token', + data=token_data, + headers={'Content-Type': 'application/x-www-form-urlencoded'}, + ) + try: + with urllib.request.urlopen(req, timeout=15) as resp: + tokens = json.loads(resp.read()) + except Exception as e: + print(f'[oauth] Token exchange failed: {e}') + return _html(f'

Token exchange failed: {e}

', 500) + + # Fetch user email from Google + user_email = '' + try: + id_token_payload = tokens.get('id_token', '').split('.')[1] + padding = 4 - len(id_token_payload) % 4 + claims = json.loads(base64.urlsafe_b64decode(id_token_payload + '=' * padding)) + user_email = claims.get('email', '') + except Exception: + pass + + if not user_email: + # Fallback: call userinfo endpoint + try: + access_token = tokens.get('access_token', '') + req2 = urllib.request.Request( + 'https://www.googleapis.com/oauth2/v3/userinfo', + headers={'Authorization': f'Bearer {access_token}'}, + ) + with urllib.request.urlopen(req2, timeout=10) as resp2: + user_email = json.loads(resp2.read()).get('email', '') + except Exception as e: + print(f'[oauth] userinfo fetch failed: {e}') + + # Build credentials dict (google-auth format) + creds = { + 'token': tokens.get('access_token'), + 'refresh_token': tokens.get('refresh_token'), + 'token_uri': 'https://oauth2.googleapis.com/token', + 'client_id': client_id, + 'client_secret': client_secret, + 'scopes': SCOPES.split(), + 'email': user_email, + 'user_email': user_email, + } + if tokens.get('expires_in'): + creds['expiry'] = time.strftime( + '%Y-%m-%dT%H:%M:%SZ', + time.gmtime(time.time() + int(tokens['expires_in'])) + ) + + # Store in Secrets Manager + secret_name = actor_id_to_secret_name(actor_id) + sm = get_sm() + try: + sm.create_secret(Name=secret_name, SecretString=json.dumps(creds)) + except sm.exceptions.ResourceExistsException: + sm.put_secret_value(SecretId=secret_name, SecretString=json.dumps(creds)) + print(f'[oauth] Stored credentials for actor={actor_id} email={user_email}') + + # Update DynamoDB users table with google_email + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and actor_id: + try: + get_ddb().Table(table_name).update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET google_email = :e', + ExpressionAttributeValues={':e': user_email}, + ) + except Exception as e: + print(f'[oauth] DynamoDB update failed: {e}') + + return _html( + f'

✅ Google account connected!

' + f'

Connected {user_email} to your agent account.

' + f'

You can close this window and return to Telegram.

' + ) diff --git a/src/lambdas/oauth-handler/requirements.txt b/src/lambdas/oauth-handler/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/src/lambdas/oauth-handler/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/src/lambdas/workspace-mcp/Dockerfile b/src/lambdas/workspace-mcp/Dockerfile index fcdadb6..be6cb9b 100644 --- a/src/lambdas/workspace-mcp/Dockerfile +++ b/src/lambdas/workspace-mcp/Dockerfile @@ -9,11 +9,13 @@ RUN pip install workspace-mcp==1.20.3 boto3 --quiet # Copy bootstrap and helper scripts COPY bootstrap /var/task/bootstrap COPY fetch_credentials.py /var/task/fetch_credentials.py +COPY proxy.py /var/task/proxy.py RUN chmod +x /var/task/bootstrap -# Lambda Web Adapter config +# Lambda Web Adapter config — proxy listens on 8080, workspace-mcp on 8081 ENV AWS_LAMBDA_EXEC_WRAPPER=/opt/bootstrap ENV PORT=8080 +ENV PROXY_PORT=8080 ENV READINESS_CHECK_PATH=/health CMD ["/var/task/bootstrap"] diff --git a/src/lambdas/workspace-mcp/bootstrap b/src/lambdas/workspace-mcp/bootstrap index 0994d11..56f6903 100755 --- a/src/lambdas/workspace-mcp/bootstrap +++ b/src/lambdas/workspace-mcp/bootstrap @@ -1,6 +1,6 @@ #!/bin/bash -# Lambda Web Adapter bootstrap for workspace-mcp -# Dependencies are in /opt/python (Lambda layer) +# Lambda Web Adapter bootstrap for workspace-mcp (multi-tenant) +# Proxy on port 8080 (Lambda Web Adapter entry), workspace-mcp on port 8081 set -e @@ -14,14 +14,27 @@ export HOME=/tmp export WORKSPACE_MCP_LOG_DIR=/tmp export GOOGLE_WORKSPACE_MCP_CREDENTIALS_DIR=/tmp/workspace_mcp_credentials -echo "[workspace-mcp] Fetching Google credentials..." >&2 -$PYTHON /var/task/fetch_credentials.py +echo "[workspace-mcp] Fetching default Google credentials..." >&2 +$PYTHON /var/task/fetch_credentials.py || true # non-fatal: per-user creds loaded by proxy if [ -f /tmp/workspace-mcp-env ]; then source /tmp/workspace-mcp-env fi -echo "[workspace-mcp] Starting on port $PORT..." >&2 -exec $PYTHON /opt/python/bin/workspace-mcp \ +echo "[workspace-mcp] Starting workspace-mcp on port 8081..." >&2 +PORT=8081 $PYTHON /opt/python/bin/workspace-mcp \ --transport streamable-http \ - --tool-tier extended + --tool-tier extended & +WMCP_PID=$! + +# Wait for workspace-mcp to be ready +for i in $(seq 1 30); do + if curl -sf http://127.0.0.1:8081/health > /dev/null 2>&1; then + echo "[workspace-mcp] Ready on port 8081" >&2 + break + fi + sleep 0.5 +done + +echo "[workspace-mcp] Starting credential proxy on port $PORT..." >&2 +exec $PYTHON /var/task/proxy.py diff --git a/src/lambdas/workspace-mcp/proxy.py b/src/lambdas/workspace-mcp/proxy.py new file mode 100644 index 0000000..220d5bd --- /dev/null +++ b/src/lambdas/workspace-mcp/proxy.py @@ -0,0 +1,143 @@ +""" +Multi-tenant credential proxy for workspace-mcp. + +Sits on port 8080 (Lambda Web Adapter entry point). +Reads X-Actor-Id header, fetches per-user Google credentials from Secrets Manager, +writes them to /tmp/workspace_mcp_credentials/{email}.json, sets USER_GOOGLE_EMAIL, +then proxies the request to workspace-mcp on port 8081. +""" +import json +import os +import threading +import time +import urllib.request +import urllib.error +from http.server import BaseHTTPRequestHandler, HTTPServer + +import boto3 + +_sm = None +_sm_lock = threading.Lock() + +# Cache: actor_id -> (email, creds_json, fetched_at) +_creds_cache: dict = {} +_cache_ttl = 300 # 5 minutes + + +def _get_sm(): + global _sm + if _sm is None: + with _sm_lock: + if _sm is None: + _sm = boto3.client('secretsmanager', region_name=os.environ.get('AWS_REGION', 'us-east-1')) + return _sm + + +def _actor_id_to_secret_name(actor_id: str) -> str: + """Convert actor_id to a valid Secrets Manager secret name.""" + # telegram:123456789 -> agent-claw/google-credentials/telegram-123456789 + safe = actor_id.replace(':', '-').replace('/', '-') + return f'agent-claw/google-credentials/{safe}' + + +def _fetch_credentials(actor_id: str) -> tuple[str, dict] | None: + """Fetch Google credentials for actor_id from Secrets Manager. Returns (email, creds_dict) or None.""" + now = time.time() + cached = _creds_cache.get(actor_id) + if cached and now - cached[2] < _cache_ttl: + return cached[0], cached[1] + + secret_name = _actor_id_to_secret_name(actor_id) + try: + secret = _get_sm().get_secret_value(SecretId=secret_name)['SecretString'] + creds = json.loads(secret) + email = creds.get('client_email') or creds.get('email') or creds.get('user_email', '') + _creds_cache[actor_id] = (email, creds, now) + print(f'[proxy] Loaded credentials for actor={actor_id} email={email}', flush=True) + return email, creds + except Exception as e: + print(f'[proxy] No credentials for actor={actor_id}: {e}', flush=True) + return None + + +def _write_credentials_file(email: str, creds: dict) -> str: + """Write credentials to /tmp/workspace_mcp_credentials/{email}.json. Returns path.""" + creds_dir = '/tmp/workspace_mcp_credentials' + os.makedirs(creds_dir, exist_ok=True) + path = f'{creds_dir}/{email}.json' + with open(path, 'w') as f: + json.dump(creds, f) + return path + + +class ProxyHandler(BaseHTTPRequestHandler): + def log_message(self, format, *args): + pass # suppress default access log + + def _proxy(self): + actor_id = self.headers.get('x-actor-id', '') + + if actor_id: + result = _fetch_credentials(actor_id) + if result: + email, creds = result + _write_credentials_file(email, creds) + os.environ['USER_GOOGLE_EMAIL'] = email + os.environ['GOOGLE_WORKSPACE_MCP_CREDENTIALS_DIR'] = '/tmp/workspace_mcp_credentials' + else: + # No credentials found — proceed without setting email (workspace-mcp will use default or fail) + print(f'[proxy] No Google credentials for actor={actor_id}, proceeding without', flush=True) + + # Read request body + content_length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(content_length) if content_length > 0 else b'' + + # Build upstream request + upstream_url = f'http://127.0.0.1:8081{self.path}' + upstream_headers = {k: v for k, v in self.headers.items() + if k.lower() not in ('host', 'content-length')} + if body: + upstream_headers['Content-Length'] = str(len(body)) + + req = urllib.request.Request( + upstream_url, + data=body or None, + headers=upstream_headers, + method=self.command, + ) + + try: + with urllib.request.urlopen(req, timeout=60) as resp: + self.send_response(resp.status) + for k, v in resp.headers.items(): + if k.lower() not in ('transfer-encoding',): + self.send_header(k, v) + self.end_headers() + self.wfile.write(resp.read()) + except urllib.error.HTTPError as e: + self.send_response(e.code) + for k, v in e.headers.items(): + if k.lower() not in ('transfer-encoding',): + self.send_header(k, v) + self.end_headers() + self.wfile.write(e.read()) + except Exception as e: + print(f'[proxy] Upstream error: {e}', flush=True) + self.send_response(502) + self.end_headers() + self.wfile.write(b'Bad Gateway') + + do_GET = _proxy + do_POST = _proxy + do_PUT = _proxy + do_DELETE = _proxy + do_OPTIONS = _proxy + do_HEAD = _proxy + do_PATCH = _proxy + + +if __name__ == '__main__': + port = int(os.environ.get('PROXY_PORT', 8080)) + server = HTTPServer(('0.0.0.0', port), ProxyHandler) + print(f'[proxy] Listening on port {port}', flush=True) + server.serve_forever()