From 4ca5fee2c089a1023d915ff132f100dd89f9e847 Mon Sep 17 00:00:00 2001 From: daniel Date: Sat, 16 May 2026 09:49:28 -0500 Subject: [PATCH] refactor: move factcloud from hardcoded SSM to per-user DynamoDB oauth2_m2m connection - Add oauth2_m2m auth type to mcp_loader.py (client_secret in record, not SSM) - Remove _get_factcloud_token(), FACTCLOUD_* config, factcloud_clients from main.py - Seed Daniel's factcloud connection into enrolled_services.mcp_connections - factcloud now loaded dynamically via mcp_loader at session start --- agentclaw/app/agent_claw_main/config.py | 6 --- agentclaw/app/agent_claw_main/main.py | 46 +-------------------- agentclaw/app/agent_claw_main/mcp_loader.py | 28 +++++++++++++ scripts/seed_factcloud.py | 35 ++++++++++++++++ 4 files changed, 65 insertions(+), 50 deletions(-) create mode 100644 scripts/seed_factcloud.py diff --git a/agentclaw/app/agent_claw_main/config.py b/agentclaw/app/agent_claw_main/config.py index cf2b7cf..67bc990 100644 --- a/agentclaw/app/agent_claw_main/config.py +++ b/agentclaw/app/agent_claw_main/config.py @@ -6,9 +6,6 @@ _DEFAULTS = { '/agent-claw/model-id': 'us.anthropic.claude-sonnet-4-6', '/agent-claw/config/compaction_model_id': 'us.anthropic.claude-3-5-haiku-20241022-v1:0', '/agent-claw/aws-mcp-url': 'https://aws-mcp.us-east-1.api.aws/mcp', - '/agent-claw/factcloud/client-id': '', - '/agent-claw/factcloud/client-secret': '', - '/agent-claw/factcloud/mcp-url': '', } @@ -28,6 +25,3 @@ _params = _load() AGENT_MODEL_ID: str = _params['/agent-claw/model-id'] COMPACTION_MODEL_ID: str = _params['/agent-claw/config/compaction_model_id'] AWS_MCP_URL: str = _params['/agent-claw/aws-mcp-url'] -FACTCLOUD_CLIENT_ID: str = _params.get('/agent-claw/factcloud/client-id', '') -FACTCLOUD_CLIENT_SECRET: str = _params.get('/agent-claw/factcloud/client-secret', '') -FACTCLOUD_MCP_URL: str = _params.get('/agent-claw/factcloud/mcp-url', '') diff --git a/agentclaw/app/agent_claw_main/main.py b/agentclaw/app/agent_claw_main/main.py index 9f45b72..bfef579 100644 --- a/agentclaw/app/agent_claw_main/main.py +++ b/agentclaw/app/agent_claw_main/main.py @@ -89,39 +89,8 @@ except Exception as _e: print(traceback.format_exc()) -# ── factcloud token cache ──────────────────────────────────────────────── -_fc_token: str = '' -_fc_token_expiry: float = 0.0 - -def _get_factcloud_token() -> str: - """Fetch/cache a Cognito M2M token for factcloud. Thread-safe for Lambda.""" - global _fc_token, _fc_token_expiry - if _fc_token and time.time() < _fc_token_expiry - 60: - return _fc_token - if not config.FACTCLOUD_CLIENT_ID or not config.FACTCLOUD_CLIENT_SECRET: - raise RuntimeError('factcloud credentials not configured in SSM') - resp = httpx.post( - 'https://factbase-cloud.auth.us-east-1.amazoncognito.com/oauth2/token', - data={ - 'grant_type': 'client_credentials', - 'client_id': config.FACTCLOUD_CLIENT_ID, - 'client_secret': config.FACTCLOUD_CLIENT_SECRET, - 'scope': 'factbase-cloud/read factbase-cloud/write', - }, - headers={'Content-Type': 'application/x-www-form-urlencoded'}, - timeout=10, - ) - resp.raise_for_status() - data = resp.json() - _fc_token = data['access_token'] - _fc_token_expiry = time.time() + data.get('expires_in', 3600) - print(f'[main] factcloud token refreshed, expires in {data.get("expires_in", 3600)}s') - return _fc_token - - # ── Subagent loading ────────────────────────────────────────────────────── -from mcp.client.streamable_http import streamablehttp_client TOOL_PRESETS = { "aws": lambda: [MCPClient(lambda: aws_iam_streamablehttp_client(config.AWS_MCP_URL, aws_service="aws-mcp"))], @@ -686,7 +655,7 @@ async def main(payload: dict, context): system_prompt = system_prompt + '\n\n---\n\n' + ltm_block system_prompt += '\nAWS tools available: call_aws (any AWS API via AWS MCP Server), aws_list_lambda_functions, aws_get_cost_and_usage, aws_describe_service. Use call_aws directly for AWS API calls — do NOT say you lack AWS access.' - system_prompt += '\n\nSubagents available — use them aggressively to save cost and improve quality:\n- aws_agent: all AWS infrastructure, cost, resource, IAM, CloudWatch queries\n- coding_agent: code writing, builds, deployments, CodeBuild/AppRunner/ECR\n- document_agent: summarize URLs, extract data from documents, process long text\nYou also have direct access to factcloud MCP tools (your personal knowledge graph) — use them directly for any factbase, factcloud, or knowledge base queries. Do NOT say you lack access to factcloud.\nDefault to delegating to subagents; only answer directly for simple conversational responses or tasks that don\'t fit a subagent.' + system_prompt += '\n\nSubagents available — use them aggressively to save cost and improve quality:\n- aws_agent: all AWS infrastructure, cost, resource, IAM, CloudWatch queries\n- coding_agent: code writing, builds, deployments, CodeBuild/AppRunner/ECR\n- document_agent: summarize URLs, extract data from documents, process long text\nYou also have direct access to factcloud MCP tools (your personal knowledge graph) loaded from your MCP connections — use them directly for any factbase, factcloud, or knowledge base queries. Do NOT say you lack access to factcloud.\nDefault to delegating to subagents; only answer directly for simple conversational responses or tasks that don\'t fit a subagent.' # Model: claude-sonnet-4-6 via cross-region inference # NOTE: extended thinking disabled — causes retry/duplicate issues with streaming @@ -704,17 +673,6 @@ async def main(payload: dict, context): run_code, send_file, request_iam_permission, apply_iam_permission, aws_list_lambda_functions, aws_get_cost_and_usage, aws_describe_service] - # factcloud: direct MCP connection (M2M Cognito auth) - factcloud_clients = [] - if config.FACTCLOUD_MCP_URL: - try: - factcloud_clients = [MCPClient(lambda: streamablehttp_client( - url=config.FACTCLOUD_MCP_URL, - headers={"Authorization": f"Bearer {_get_factcloud_token()}"}, - ))] - except Exception as e: - print(f'[main] factcloud connection failed: {e}') - # Load user's dynamic MCP connections mcp_connections = services.get('mcp_connections', []) mcp_clients, _mcp_to_close = mcp_loader.load_mcp_tools(mcp_connections, actor_id) @@ -723,7 +681,7 @@ async def main(payload: dict, context): ssm = boto3.client('ssm', region_name='us-east-1') subagent_tools = _load_subagents(ssm) - all_tools = base_tools + factcloud_clients + _aws_mcp_tools + mcp_clients + subagent_tools + all_tools = base_tools + _aws_mcp_tools + mcp_clients + subagent_tools agent = Agent( model=model, diff --git a/agentclaw/app/agent_claw_main/mcp_loader.py b/agentclaw/app/agent_claw_main/mcp_loader.py index 79a6572..bc421b5 100644 --- a/agentclaw/app/agent_claw_main/mcp_loader.py +++ b/agentclaw/app/agent_claw_main/mcp_loader.py @@ -45,12 +45,40 @@ def _get_oauth_token(conn: dict, actor_id: str) -> str: return token +def _get_m2m_token(conn: dict, actor_id: str) -> str: + """Fetch OAuth token for oauth2_m2m (secret stored directly in record).""" + cache_key = f"{actor_id}:{conn['name']}" + cached = _token_cache.get(cache_key) + if cached and cached['expires_at'] > time.time() + 60: + return cached['token'] + + data = urllib.parse.urlencode({ + 'grant_type': 'client_credentials', + 'client_id': conn['client_id'], + 'client_secret': conn['client_secret'], + 'scope': conn.get('scopes', conn.get('scope', '')), + }).encode() + + req = urllib.request.Request(conn['token_url'], data=data, + headers={'Content-Type': 'application/x-www-form-urlencoded'}) + with urllib.request.urlopen(req, timeout=10) as resp: + body = json.loads(resp.read()) + + token = body['access_token'] + expires_in = body.get('expires_in', 3600) + _token_cache[cache_key] = {'token': token, 'expires_at': time.time() + expires_in} + return token + + def _resolve_auth_headers(conn: dict, actor_id: str) -> dict: """Resolve auth headers for a connection.""" auth_type = conn.get('auth_type', 'none') if auth_type == 'oauth_client_credentials': token = _get_oauth_token(conn, actor_id) return {'Authorization': f'Bearer {token}'} + elif auth_type == 'oauth2_m2m': + token = _get_m2m_token(conn, actor_id) + return {'Authorization': f'Bearer {token}'} elif auth_type == 'bearer': token = _get_ssm_value(conn['token_ssm']) return {'Authorization': f'Bearer {token}'} diff --git a/scripts/seed_factcloud.py b/scripts/seed_factcloud.py new file mode 100644 index 0000000..52595b3 --- /dev/null +++ b/scripts/seed_factcloud.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Seed Daniel's factcloud MCP connection into DynamoDB.""" +import boto3 + +ACTOR_ID = 'telegram:8537376738' +TABLE_NAME = 'agent-claw-users' + +conn = { + 'name': 'factcloud', + 'url': 'https://factbase-cloud-gateway-2czetaoh3u.gateway.bedrock-agentcore.us-east-1.amazonaws.com/mcp', + 'auth_type': 'oauth2_m2m', + 'client_id': '5fo2q4fb452j3aekd55g3190i4', + 'client_secret': '1e0bqs8r4jk90sbeivh96mn893mgmv96h2olvcq7m3o5gjpjc56p', + 'token_url': 'https://factbase-cloud.auth.us-east-1.amazoncognito.com/oauth2/token', + 'scopes': 'factbase-cloud/read factbase-cloud/write', + 'enabled': True, +} + +session = boto3.Session(profile_name='ai1', region_name='us-east-1') +ddb = session.resource('dynamodb') +table = ddb.Table(TABLE_NAME) + +# Get existing connections, upsert factcloud +resp = table.get_item(Key={'actor_id': ACTOR_ID}) +services = resp.get('Item', {}).get('enrolled_services', {}) +connections = services.get('mcp_connections', []) +connections = [c for c in connections if c['name'] != 'factcloud'] +connections.append(conn) + +table.update_item( + Key={'actor_id': ACTOR_ID}, + UpdateExpression='SET enrolled_services.mcp_connections = :conns', + ExpressionAttributeValues={':conns': connections}, +) +print(f'Seeded factcloud connection for {ACTOR_ID}')