multi-tenant Phase 2: per-user Google OAuth

- workspace-mcp: add proxy.py (port 8080) that reads X-Actor-Id header,
  fetches per-user Google credentials from Secrets Manager, writes creds
  file, sets USER_GOOGLE_EMAIL, proxies to workspace-mcp on port 8081
- workspace-mcp: update bootstrap to start workspace-mcp on 8081 + proxy on 8080
- workspace-mcp: update Dockerfile to include proxy.py
- oauth-handler Lambda: new Lambda with /oauth/start + /oauth/callback
  routes; exchanges Google auth code, stores tokens in Secrets Manager
  at agent-claw/google-credentials/{actor_id_safe}, updates DynamoDB
- CDK: add OAuthHandler Lambda + GET /oauth/start + /oauth/callback routes
- CDK: remove shared google-workspace-credentials secret; add per-user
  secret IAM grants (agent-claw/google-credentials/*) for workspace-mcp
  role, runtime1 role, and oauth-handler role
- CDK: output OAuthStartUrl + OAuthRedirectUri
- agent-runner: pass google_email in user_profile payload
- main.py: pass actor_id as X-Actor-Id header in workspace-mcp MCP calls;
  skip workspace-mcp if user has no google_email; add connect_google_account
  tool that generates OAuth URL for the current user
- main.py: include google_email in user_context for system prompt
- agentcore.json: add OAUTH_START_URL env var for agent runtime
This commit is contained in:
daniel
2026-05-06 21:42:33 -05:00
parent 841e729b18
commit ac5bd78d5a
24 changed files with 1736 additions and 95 deletions

View File

@@ -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"
]
]
}