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:
@@ -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', {
|
||||
|
||||
Reference in New Issue
Block a user