Compare commits

..

21 Commits

Author SHA1 Message Date
daniel
4ca5fee2c0 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
2026-05-16 09:49:28 -05:00
daniel
e77417b6cd feat: wire factcloud as direct MCP connection, drop knowledge_agent subagent
- Rename FACTBASE_CLOUD_* -> FACTCLOUD_* in config.py + SSM paths
- factcloud MCPClient added directly to main agent tool set
- knowledge_agent subagent removed (SSM + TOOL_PRESETS)
- System prompt updated: factcloud tools are direct, not via subagent
2026-05-16 09:25:55 -05:00
daniel
ef5734101e fix: add knowledge_agent to system prompt subagent list 2026-05-16 07:11:39 -05:00
daniel
8c28797bca feat: add /goal command for durable multi-turn objectives
- /goal set|status|checkpoint|pause|resume|clear intercept in main.py
- GOAL.md injected into system prompt when active (prompt_builder.py)
- Goal context added to heartbeat for autonomous progress
2026-05-16 07:07:46 -05:00
daniel
42dbdcde9e feat: factbase-cloud integration — knowledge_agent subagent with M2M auth 2026-05-15 23:32:23 -05:00
daniel
ed6577ccf9 feat: billing tags on CDK stack + inference profile creation script 2026-05-15 20:35:02 -05:00
daniel
4f17bbd2c3 fix: intercept [HEARTBEAT] prompt, suppress chatty non-urgent responses 2026-05-15 18:34:14 -05:00
daniel
e00702164d refactor: slim system prompt — SOUL.md+STATUS.md only, fix duplicate time injection 2026-05-15 16:42:27 -05:00
daniel
05fee423f2 feat: dynamic subagent loading from SSM 2026-05-15 15:19:08 -05:00
daniel
85efb082f7 fix: unconditional system prompt for call_aws availability 2026-05-15 11:49:03 -05:00
daniel
40f9712c54 fix: remove explicit MCPClient.start() - Strands calls it internally 2026-05-15 11:26:01 -05:00
daniel
ebd5a57ece fix: pass aws_service=aws-mcp to aws_iam_streamablehttp_client 2026-05-15 10:32:32 -05:00
daniel
9c09dce519 deps: add mcp-proxy-for-aws to runtime dependencies 2026-05-15 10:28:51 -05:00
daniel
0eff46126f Wire AWS MCP Server via mcp-proxy-for-aws 2026-05-15 10:19:44 -05:00
daniel
266231d070 Add native boto3 AWS tools, remove broken AWS MCP client 2026-05-15 10:03:56 -05:00
daniel
17b1536dae fix: move MCPClient imports inside try block, add TOOLS.md placeholder 2026-05-15 09:29:07 -05:00
daniel
add8c6c988 fix: add missing MCPClient/streamablehttp_client imports; fix EXECUTION_ROLE_ARN to actual AgentCore role 2026-05-15 09:14:33 -05:00
daniel
88ed337938 Add AWS MCP Server integration + IAM self-modify with approval gate
- CDK: add compute/build, broad read-only, IAM self-modify (scoped to own role),
  IAM policy management, and SSM read permissions to runtime1Role
- config.py: load /agent-claw/aws-mcp-url from SSM at cold start
- main.py: connect to AWS MCP Server with SigV4 auth (_AwsMcpSigV4Auth);
  add request_iam_permission and apply_iam_permission tools
- agentcore.json: add EXECUTION_ROLE_ARN env var
2026-05-15 08:56:06 -05:00
daniel
68aad4fb71 Read model-id from /agent-claw/model-id SSM param and pass to BedrockModel 2026-05-15 07:00:23 -05:00
daniel
f31d732cb9 Read model-id from SSM and pass to BedrockModel in main.py 2026-05-15 06:58:54 -05:00
daniel
62862f00f0 Make agent and compaction model IDs configurable via SSM 2026-05-14 18:27:35 -05:00
36 changed files with 2258 additions and 577 deletions

View File

@@ -22,7 +22,8 @@
"WORKSPACE_BUCKET_NAME": "agent-claw-workspace-495395224548",
"TELEGRAM_BOT_TOKEN_SSM_PARAM": "/agent-claw/telegram-bot-token",
"BRAVE_API_KEY_SSM_PARAM": "/agent-claw/brave-api-key",
"SCHEDULER_LAMBDA_ARN": "arn:aws:lambda:us-east-1:495395224548:function:agent-claw-scheduler"
"SCHEDULER_LAMBDA_ARN": "arn:aws:lambda:us-east-1:495395224548:function:agent-claw-scheduler",
"EXECUTION_ROLE_ARN": "arn:aws:iam::495395224548:role/AgentCore-agentclaw-defau-ApplicationAgentAgentClaw-Ttg8kEtQ3cJj"
}
}
],
@@ -60,4 +61,4 @@
"configBundles": [],
"abTests": [],
"httpGateways": []
}
}

View File

@@ -0,0 +1,27 @@
"""Config loader — fetches model IDs and service URLs from SSM Parameter Store at cold start."""
import boto3
_DEFAULTS = {
'/agent-claw/model-id': 'us.anthropic.claude-sonnet-4-6',
'/agent-claw/config/compaction_model_id': 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
'/agent-claw/aws-mcp-url': 'https://aws-mcp.us-east-1.api.aws/mcp',
}
def _load():
ssm = boto3.client('ssm', region_name='us-east-1')
names = list(_DEFAULTS.keys())
try:
resp = ssm.get_parameters(Names=names, WithDecryption=True)
found = {p['Name']: p['Value'] for p in resp['Parameters']}
except Exception:
found = {}
return {name: found.get(name, default) for name, default in _DEFAULTS.items()}
_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']

View File

@@ -4,6 +4,7 @@ agent-claw Runtime 1 — main assistant agent.
Entrypoint for AgentCore CodeZip deployment.
"""
import os
import time
from strands import Agent, tool
from strands.models import BedrockModel
from bedrock_agentcore.runtime import BedrockAgentCoreApp
@@ -11,6 +12,7 @@ from bedrock_agentcore.runtime import BedrockAgentCoreApp
from channels.telegram import TelegramAdapter
from prompt_builder import build_system_prompt, invalidate_prompt
import memory_manager
import config
from tools import web as web_tools
from tools import workspace as ws_tools
from tools import messaging
@@ -34,6 +36,7 @@ OAUTH_START_URL = (
or 'https://sptejrymri.execute-api.us-east-1.amazonaws.com/oauth/start'
)
USERS_TABLE_NAME = os.environ.get('USERS_TABLE_NAME', 'agent-claw-users')
EXECUTION_ROLE_ARN = os.environ.get('EXECUTION_ROLE_ARN', '')
class _SigV4HttpxAuth(httpx.Auth):
@@ -61,13 +64,68 @@ class _SigV4HttpxAuth(httpx.Auth):
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
# code_interpreter removed — causes [Errno 98] port 8080 conflict on warm container re-init
from tools.code_interpreter import run_code
from strands_tools import http_request, file_read
app = BedrockAgentCoreApp()
_aws_mcp_client = None
_aws_mcp_tools = []
try:
from strands.tools.mcp import MCPClient
from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client
_aws_mcp_client = MCPClient(
lambda: aws_iam_streamablehttp_client(config.AWS_MCP_URL, aws_service="aws-mcp")
)
_aws_mcp_tools = [_aws_mcp_client]
print('[main] AWS MCP client created')
except Exception as _e:
import traceback
print(f'[main] AWS MCP client failed: {type(_e).__name__}: {_e}')
print(traceback.format_exc())
# ── Subagent loading ──────────────────────────────────────────────────────
TOOL_PRESETS = {
"aws": lambda: [MCPClient(lambda: aws_iam_streamablehttp_client(config.AWS_MCP_URL, aws_service="aws-mcp"))],
"coding": lambda: [MCPClient(lambda: aws_iam_streamablehttp_client(config.AWS_MCP_URL, aws_service="aws-mcp")), run_code],
"documents": lambda: [http_request, file_read],
}
def _load_subagents(ssm_client) -> list:
"""Load subagent definitions from SSM and return as tools."""
import json
try:
resp = ssm_client.get_parameter(Name='/agent-claw/subagents')
defs = json.loads(resp['Parameter']['Value'])
except Exception as e:
print(f'[main] Failed to load subagents from SSM: {type(e).__name__}: {e}')
return []
tools = []
for cfg in defs:
preset = cfg.get('tools', '')
if preset not in TOOL_PRESETS:
print(f'[main] Unknown tool preset "{preset}" for subagent "{cfg.get("name")}", skipping')
continue
try:
sub = Agent(
model=BedrockModel(model_id=cfg['model_id'], region_name='us-east-1'),
system_prompt=cfg['system_prompt'],
tools=TOOL_PRESETS[preset](),
)
tools.append(sub.as_tool(name=cfg['name'], description=cfg['description']))
except Exception as e:
print(f'[main] Failed to build subagent "{cfg.get("name")}": {type(e).__name__}: {e}')
print(f'[main] Loaded {len(tools)} subagent(s)')
return tools
# ── Tool definitions ──────────────────────────────────────────────────────
@@ -263,6 +321,237 @@ def manage_service(action: str, service: str, config: dict | None = None) -> str
return f'Unknown action: {action}. Use "enroll", "remove", or "list".'
@tool
def request_iam_permission(action: str, resource: str, reason: str) -> str:
"""Request IAM permission from the user. ALWAYS call this before apply_iam_permission.
Sends the user a Telegram message explaining what permission is needed and why.
After calling this, wait for the user to explicitly say 'yes' before proceeding.
Args:
action: IAM action to request (e.g. 's3:PutObject')
resource: Resource ARN the action applies to (e.g. 'arn:aws:s3:::my-bucket/*')
reason: Why this permission is needed
"""
msg = (
f"⚠️ *IAM Permission Request*\n\n"
f"I need to add the following permission to my execution role:\n\n"
f"• Action: `{action}`\n"
f"• Resource: `{resource}`\n"
f"• Reason: {reason}\n\n"
f"Reply *yes* to approve or *no* to deny."
)
messaging.send(msg)
return "Permission request sent. Wait for the user to reply 'yes' before calling apply_iam_permission."
@tool
def apply_iam_permission(action: str, resource: str, policy_name: str) -> str:
"""Apply an IAM permission to the agent execution role.
Only call this after the user has explicitly approved via request_iam_permission.
Args:
action: IAM action to allow (e.g. 's3:PutObject')
resource: Resource ARN the action applies to
policy_name: Unique name for the inline policy (e.g. 'AllowS3PutObject')
"""
if not EXECUTION_ROLE_ARN:
return 'EXECUTION_ROLE_ARN not configured.'
import json as _json
role_name = EXECUTION_ROLE_ARN.split('/')[-1]
policy_doc = _json.dumps({
'Version': '2012-10-17',
'Statement': [{'Effect': 'Allow', 'Action': action, 'Resource': resource}],
})
boto3.client('iam', region_name='us-east-1').put_role_policy(
RoleName=role_name,
PolicyName=policy_name,
PolicyDocument=policy_doc,
)
return f"Applied policy '{policy_name}': Allow {action} on {resource}."
@tool
def aws_list_lambda_functions(region: str = "us-east-1") -> str:
"""List AWS Lambda functions in the specified region. Uses execution role credentials directly via boto3."""
import boto3
client = boto3.client("lambda", region_name=region)
paginator = client.get_paginator("list_functions")
functions = []
for page in paginator.paginate():
for fn in page["Functions"]:
functions.append(f"{fn['FunctionName']} ({fn['Runtime']})")
return f"{len(functions)} Lambda functions in {region}:\n" + "\n".join(functions)
@tool
def aws_get_cost_and_usage(start_date: str, end_date: str, granularity: str = "MONTHLY") -> str:
"""Get AWS Cost and Usage report. start_date and end_date in YYYY-MM-DD format. Uses execution role credentials."""
import boto3
client = boto3.client("ce", region_name="us-east-1")
response = client.get_cost_and_usage(
TimePeriod={"Start": start_date, "End": end_date},
Granularity=granularity,
Metrics=["UnblendedCost"]
)
lines = []
for result in response["ResultsByTime"]:
period = f"{result['TimePeriod']['Start']} to {result['TimePeriod']['End']}"
cost = result["Total"]["UnblendedCost"]["Amount"]
unit = result["Total"]["UnblendedCost"]["Unit"]
lines.append(f"{period}: {cost} {unit}")
return "\n".join(lines)
@tool
def aws_describe_service(service: str, region: str = "us-east-1") -> str:
"""Describe an AWS service. service can be: lambda, s3, cloudformation, dynamodb, sqs. Returns summary of key resources."""
import boto3
session = boto3.Session(region_name=region)
if service == "s3":
client = session.client("s3")
buckets = client.list_buckets()["Buckets"]
return f"{len(buckets)} S3 buckets: " + ", ".join(b["Name"] for b in buckets[:20])
elif service == "cloudformation":
client = session.client("cloudformation")
stacks = client.list_stacks(StackStatusFilter=["CREATE_COMPLETE", "UPDATE_COMPLETE", "ROLLBACK_COMPLETE"])["StackSummaries"]
return f"{len(stacks)} stacks: " + ", ".join(s["StackName"] for s in stacks[:20])
elif service == "dynamodb":
client = session.client("dynamodb")
tables = client.list_tables()["TableNames"]
return f"{len(tables)} DynamoDB tables: " + ", ".join(tables[:20])
elif service == "sqs":
client = session.client("sqs")
queues = client.list_queues().get("QueueUrls", [])
return f"{len(queues)} SQS queues: " + ", ".join(q.split("/")[-1] for q in queues[:20])
else:
return f"Service {service} not yet implemented. Try: lambda, s3, cloudformation, dynamodb, sqs"
# ── Goal helpers ──────────────────────────────────────────────────────────
from datetime import datetime as _dt
from zoneinfo import ZoneInfo as _ZoneInfo
def _now_iso() -> str:
return _dt.now(_ZoneInfo('America/Chicago')).strftime('%Y-%m-%dT%H:%M:%S%z')
def _read_goal() -> str | None:
"""Read GOAL.md from S3, return content or None."""
try:
return ws_tools.read_file('GOAL.md')
except Exception:
return None
def _write_goal(content: str):
ws_tools.write_file('GOAL.md', content)
invalidate_prompt()
def _delete_goal():
try:
_s3 = boto3.client('s3')
_s3.delete_object(Bucket=ws_tools.get_bucket(), Key='GOAL.md')
ws_tools._cache.pop('GOAL.md', None)
invalidate_prompt()
except Exception:
pass
def _parse_goal_status(content: str) -> str:
"""Extract Status field from GOAL.md content."""
for line in content.splitlines():
if line.startswith('**Status:**'):
return line.split('**Status:**')[1].strip()
return 'unknown'
def _get_active_goal_context() -> dict | None:
"""Return goal context dict if active, else None."""
content = _read_goal()
if not content or _parse_goal_status(content) != 'active':
return None
objective = stopping = last_cp = ''
for line in content.splitlines():
if line.startswith('**Objective:**'):
objective = line.split('**Objective:**')[1].strip()
elif line.startswith('**Stopping condition:**'):
stopping = line.split('**Stopping condition:**')[1].strip()
elif line.startswith('- ['):
last_cp = line # last checkpoint line wins
return {'objective': objective, 'stopping_condition': stopping, 'last_checkpoint': last_cp}
def _handle_goal_command(prompt: str) -> str | None:
"""Handle /goal commands. Returns reply string or None if not a goal command."""
parts = prompt.split(None, 2) # ['/goal', subcommand?, rest?]
cmd = parts[1] if len(parts) > 1 else 'status'
rest = parts[2] if len(parts) > 2 else ''
if cmd == 'set':
if not rest:
return '❌ Usage: `/goal set <objective>` or `/goal set <objective> | <stopping condition>`'
if '|' in rest:
objective, stopping = [s.strip() for s in rest.split('|', 1)]
else:
objective, stopping = rest.strip(), 'not specified'
content = (
f'# Goal\n\n'
f'**Objective:** {objective}\n'
f'**Stopping condition:** {stopping}\n'
f'**Status:** active\n'
f'**Set at:** {_now_iso()}\n\n'
f'## Checkpoint log\n'
)
_write_goal(content)
return f'✅ Goal set: {objective}\nStopping condition: {stopping}'
elif cmd in ('status', '/goal'):
content = _read_goal()
if not content:
return '📋 No active goal. Use `/goal set <objective>` to set one.'
return content
elif cmd == 'checkpoint':
if not rest:
return '❌ Usage: `/goal checkpoint <note>`'
content = _read_goal()
if not content:
return '❌ No active goal to checkpoint.'
entry = f'- [{_now_iso()}] {rest}\n'
content = content.rstrip() + '\n' + entry
_write_goal(content)
return f'✅ Checkpoint added: {rest}'
elif cmd == 'pause':
content = _read_goal()
if not content:
return '❌ No active goal to pause.'
content = content.replace('**Status:** active', '**Status:** paused')
_write_goal(content)
return '⏸️ Goal paused.'
elif cmd == 'resume':
content = _read_goal()
if not content:
return '❌ No goal to resume.'
content = content.replace('**Status:** paused', '**Status:** active')
_write_goal(content)
return '▶️ Goal resumed.'
elif cmd == 'clear':
_delete_goal()
return '🗑️ Goal cleared.'
else:
# Not a recognized subcommand — treat the whole thing as status check
content = _read_goal()
if not content:
return '📋 No active goal. Use `/goal set <objective>` to set one.'
return content
# ── Entrypoint ────────────────────────────────────────────────────────────
# Module-level actor_id for tool closures (set per-invocation)
@@ -365,20 +654,14 @@ async def main(payload: dict, context):
if ltm_block:
system_prompt = system_prompt + '\n\n---\n\n' + ltm_block
# Inject current datetime so the model always has accurate time context
from datetime import datetime
from zoneinfo import ZoneInfo
_tz = ZoneInfo('America/Chicago')
_now = datetime.now(_tz)
_time_str = _now.strftime('%A, %B %d, %Y %I:%M %p %Z')
system_prompt = system_prompt + f'\n\nCurrent date/time: {_time_str}'
print(f'[main] System prompt time injection: {_time_str}')
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) 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
from botocore.config import Config as BotoConfig
model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-6",
model_id=config.AGENT_MODEL_ID,
region_name="us-east-1",
boto_client_config=BotoConfig(read_timeout=600, connect_timeout=10),
)
@@ -387,12 +670,18 @@ async def main(payload: dict, context):
home_assistant, connect_google_account, list_google_accounts, remove_google_account,
manage_service, manage_mcp_connection, schedule_reminder, list_reminders, cancel_reminder,
list_calendars, get_calendar_events, list_gmail_messages, get_gmail_message,
run_code, send_file]
run_code, send_file, request_iam_permission, apply_iam_permission,
aws_list_lambda_functions, aws_get_cost_and_usage, aws_describe_service]
# Load user's dynamic MCP connections
mcp_connections = services.get('mcp_connections', [])
mcp_clients, _mcp_to_close = mcp_loader.load_mcp_tools(mcp_connections, actor_id)
all_tools = base_tools + mcp_clients
# Load subagents from SSM
ssm = boto3.client('ssm', region_name='us-east-1')
subagent_tools = _load_subagents(ssm)
all_tools = base_tools + _aws_mcp_tools + mcp_clients + subagent_tools
agent = Agent(
model=model,
@@ -401,9 +690,42 @@ async def main(payload: dict, context):
tools=all_tools,
)
# Intercept /goal commands — handle directly without LLM
prompt = payload.get('prompt', '')
if prompt.strip().startswith('/goal'):
goal_reply = _handle_goal_command(prompt.strip())
if goal_reply is not None:
yield {'data': goal_reply}
_typing_active = False
session_manager.close()
mcp_loader.close_mcp_clients(_mcp_to_close)
return
# Intercept heartbeat: replace bare [HEARTBEAT] with a strict-format instruction.
# Agent-runner suppresses replies that start with HEARTBEAT_OK, so only real alerts reach Telegram.
if prompt.strip() == '[HEARTBEAT]':
# Inject goal context into heartbeat if active
goal_ctx = _get_active_goal_context()
goal_heartbeat = ''
if goal_ctx:
goal_heartbeat = (
f' You have an active goal: "{goal_ctx["objective"]}". '
f'Stopping condition: "{goal_ctx["stopping_condition"]}". '
f'Last checkpoint: "{goal_ctx["last_checkpoint"]}". '
f'Make progress toward this goal or report blockers.'
)
prompt = (
'HEARTBEAT CHECK: Silently check for anything urgent Daniel should know about '
'(calendar events starting within 2 hours, unread urgent emails, overdue reminders). '
'Do NOT narrate your checking process. '
'If nothing is urgent: reply with the single word HEARTBEAT_OK and nothing else. '
'If something IS urgent: reply with 2-3 lines max summarising only the urgent items.'
+ goal_heartbeat
)
final_message = None
try:
async for event in agent.stream_async(payload.get('prompt', '')):
async for event in agent.stream_async(prompt):
if 'result' in event:
final_message = event['result'].message
yield event

View File

@@ -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}'}

View File

@@ -10,13 +10,14 @@ import boto3
from bedrock_agentcore.memory.client import MemoryClient
import config
logger = logging.getLogger(__name__)
MEMORY_ID = 'agentclaw_AgentClawMemory-i7Csf776AH'
SESSION_WINDOW_SIZE = 100
USERS_TABLE_NAME = os.environ.get('USERS_TABLE_NAME', 'agent-claw-users')
LTM_SESSION_ID = 'ltm-extractions'
HAIKU_MODEL_ID = 'us.anthropic.claude-3-5-haiku-20241022-v1:0'
_memory_client: MemoryClient | None = None
@@ -110,7 +111,7 @@ def _call_claude_extraction(conversation_text: str) -> dict:
'Conversation:\n' + conversation_text
)
resp = bedrock.converse(
modelId=HAIKU_MODEL_ID,
modelId=config.COMPACTION_MODEL_ID,
messages=[{'role': 'user', 'content': [{'text': prompt}]}],
inferenceConfig={'maxTokens': 1024},
)

View File

@@ -46,14 +46,24 @@ def _get_base_prompt(actor_id: str = '') -> str:
s3 = boto3.client('s3')
parts = []
# MEMORY.md removed — AgentCore Memory handles persistent facts via conversation history.
# Long-term memory extraction (retrieval_config + memory strategy) is the right layer for this.
# Inject active goal at the top of context
try:
obj = s3.get_object(Bucket=bucket, Key='GOAL.md')
goal_content = obj['Body'].read().decode('utf-8')
if '**Status:** active' in goal_content:
parts.append(f'## Active Goal\n{goal_content}')
print(f'[prompt_builder] Injected GOAL.md ({len(goal_content)} bytes)')
except Exception:
pass
for fname in ['SOUL.md', 'AGENTS.md', 'IDENTITY.md', 'TOOLS.md', 'HEARTBEAT.md']:
for fname in ['SOUL.md', 'STATUS.md']:
try:
obj = s3.get_object(Bucket=bucket, Key=fname)
content = obj['Body'].read().decode('utf-8')
parts.append(f'## {fname}\n{content}')
if fname == 'STATUS.md':
parts.append(f'## Status — In Progress\n{content}')
else:
parts.append(content)
print(f'[prompt_builder] Loaded {fname} ({len(content)} bytes)')
except Exception as e:
print(f'[prompt_builder] Failed to load {fname}: {e}')
@@ -74,7 +84,11 @@ def _get_base_prompt(actor_id: str = '') -> str:
'want you to remember it.\n'
'- If you notice a fact that seems important but may not be in LTM yet (e.g. a '
'deadline, a preference, a name), you may say "I\'ll keep that in mind" — but do '
'not ask permission or make a production of it.'
'not ask permission or make a production of it.\n'
'- **In-progress tracking (STATUS.md):** When you start async work (CodeBuild job, '
'reminder, deployment, anything you need to check back on), update STATUS.md using '
"write_workspace_file('STATUS.md', content). Clear entries when complete. Check "
"STATUS.md at the start of sessions where Daniel asks 'what's happening' or 'any updates'."
)
parts.append('## Runtime\nRuntime: agent-claw | host=AgentCore | model=bedrock-claude-sonnet | channel=telegram | timezone=America/Chicago')

View File

@@ -15,6 +15,7 @@ dependencies = [
"strands-agents-tools >= 0.5.0",
"strands-agents >= 1.13.0",
"workspace-mcp >= 1.20.0",
"mcp-proxy-for-aws >= 1.0.0",
]
[tool.hatch.build.targets.wheel]

View File

@@ -15,6 +15,7 @@ dependencies = [
{ name = "aws-opentelemetry-distro" },
{ name = "bedrock-agentcore" },
{ name = "botocore", extra = ["crt"] },
{ name = "mcp-proxy-for-aws" },
{ name = "strands-agents" },
{ name = "strands-agents-tools" },
{ name = "workspace-mcp" },
@@ -25,6 +26,7 @@ requires-dist = [
{ name = "aws-opentelemetry-distro" },
{ name = "bedrock-agentcore", specifier = ">=1.0.3" },
{ name = "botocore", extras = ["crt"], specifier = ">=1.35.0" },
{ name = "mcp-proxy-for-aws", specifier = ">=1.0.0" },
{ name = "strands-agents", specifier = ">=1.13.0" },
{ name = "strands-agents-tools", specifier = ">=0.5.0" },
{ name = "workspace-mcp", specifier = ">=1.20.0" },
@@ -1456,6 +1458,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" },
]
[[package]]
name = "mcp-proxy-for-aws"
version = "1.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "boto3" },
{ name = "botocore", extra = ["crt"] },
{ name = "fastmcp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/de/ba/7de2c1d132206ac5aae6ae4125ff97c62f19add9ea4593d26a4c5dec437e/mcp_proxy_for_aws-1.4.2.tar.gz", hash = "sha256:939c0cad338051776fab263b5f7d1e97c6507e31502e3c3ce895c3166b06b002", size = 417185, upload-time = "2026-04-30T11:19:09.072Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/47/382c2c603c89aa40feb798536564bb285635b4af6689a9d6d5018898057e/mcp_proxy_for_aws-1.4.2-py3-none-any.whl", hash = "sha256:b9cefc389bc618e28cad8b0558dd2eca9112e599738e17a8f695ad759d1b7de0", size = 34665, upload-time = "2026-04-30T11:19:10.18Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"

View File

@@ -5,6 +5,11 @@ import { AgentClawStack } from '../lib/agent-claw-stack';
const app = new cdk.App();
// Billing tags applied to all resources in the stack
cdk.Tags.of(app).add('project', 'agent-claw');
cdk.Tags.of(app).add('env', 'prod');
cdk.Tags.of(app).add('owner', 'daniel');
new AgentClawStack(app, 'AgentClawStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,

View File

@@ -1,91 +1,46 @@
{
"version": "53.0.0",
"files": {
"e2659170a0721541efa761a8d5d04d5e36cbbf691c4b15a9053002b7c825055d": {
"displayName": "WorkspaceFiles/AwsCliLayer/Code",
"source": {
"path": "asset.e2659170a0721541efa761a8d5d04d5e36cbbf691c4b15a9053002b7c825055d.zip",
"packaging": "file"
},
"destinations": {
"495395224548-us-east-1-b19c5879": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "e2659170a0721541efa761a8d5d04d5e36cbbf691c4b15a9053002b7c825055d.zip",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}
}
},
"3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9": {
"displayName": "Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code",
"source": {
"path": "asset.3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9",
"packaging": "zip"
},
"destinations": {
"495395224548-us-east-1-12f29a1a": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9.zip",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}
}
},
"0feea8d997b96e31a1bd7dd049faf8ee17babeb6d2f5b663ba7e3a70387302e0": {
"displayName": "WorkspaceFiles/Asset1",
"source": {
"path": "asset.0feea8d997b96e31a1bd7dd049faf8ee17babeb6d2f5b663ba7e3a70387302e0",
"packaging": "zip"
},
"destinations": {
"495395224548-us-east-1-2e3561b9": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "0feea8d997b96e31a1bd7dd049faf8ee17babeb6d2f5b663ba7e3a70387302e0.zip",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}
}
},
"f4461651bfa7d2822e3f36525ace7882e1610dcdaf85e052e1907241e25491d6": {
"e0a834c0c682fe0c528540082d0b587c43ed791927a064104e5eb6c507c0cdb2": {
"displayName": "TgIngest/Code",
"source": {
"path": "asset.f4461651bfa7d2822e3f36525ace7882e1610dcdaf85e052e1907241e25491d6",
"path": "asset.e0a834c0c682fe0c528540082d0b587c43ed791927a064104e5eb6c507c0cdb2",
"packaging": "zip"
},
"destinations": {
"495395224548-us-east-1-2abe2e26": {
"495395224548-us-east-1-351c433c": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "f4461651bfa7d2822e3f36525ace7882e1610dcdaf85e052e1907241e25491d6.zip",
"objectKey": "e0a834c0c682fe0c528540082d0b587c43ed791927a064104e5eb6c507c0cdb2.zip",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}
}
},
"e8d92532d2cb081ba122764c803acc80aaa41350d3497665468ca165dd5ff799": {
"59b4a808a6125020261c01ae23500d036cdde29fdb255aa6a82d61555fde4848": {
"displayName": "AgentRunner/Code",
"source": {
"path": "asset.e8d92532d2cb081ba122764c803acc80aaa41350d3497665468ca165dd5ff799",
"path": "asset.59b4a808a6125020261c01ae23500d036cdde29fdb255aa6a82d61555fde4848",
"packaging": "zip"
},
"destinations": {
"495395224548-us-east-1-7c682050": {
"495395224548-us-east-1-16e7a6a4": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "e8d92532d2cb081ba122764c803acc80aaa41350d3497665468ca165dd5ff799.zip",
"objectKey": "59b4a808a6125020261c01ae23500d036cdde29fdb255aa6a82d61555fde4848.zip",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}
}
},
"99aabce70089266e2352cb313d55ee18b849e39c418e8e9cd25dea8c4bf85fc4": {
"6a3663dc0651e59d7ac6f549864cf1c084e3fbfc98ebd0479fed6a84075a378e": {
"displayName": "OAuthHandler/Code",
"source": {
"path": "asset.99aabce70089266e2352cb313d55ee18b849e39c418e8e9cd25dea8c4bf85fc4",
"path": "asset.6a3663dc0651e59d7ac6f549864cf1c084e3fbfc98ebd0479fed6a84075a378e",
"packaging": "zip"
},
"destinations": {
"495395224548-us-east-1-793899ae": {
"495395224548-us-east-1-fffb41e6": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "99aabce70089266e2352cb313d55ee18b849e39c418e8e9cd25dea8c4bf85fc4.zip",
"objectKey": "6a3663dc0651e59d7ac6f549864cf1c084e3fbfc98ebd0479fed6a84075a378e.zip",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}
@@ -106,31 +61,31 @@
}
}
},
"8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f": {
"1aaec5c60ed6a294ff9b918a707477e0a0e299a850447e7456a8c8091604131b": {
"displayName": "Scheduler/Code",
"source": {
"path": "asset.8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f",
"path": "asset.1aaec5c60ed6a294ff9b918a707477e0a0e299a850447e7456a8c8091604131b",
"packaging": "zip"
},
"destinations": {
"495395224548-us-east-1-89bca2fb": {
"495395224548-us-east-1-e6bab83a": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f.zip",
"objectKey": "1aaec5c60ed6a294ff9b918a707477e0a0e299a850447e7456a8c8091604131b.zip",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}
}
},
"78c322849179468ec994f5c3e550a0db4961592ea962b9cf484463e5f04b5a70": {
"9c45c012ea9c045aa771b1c3049eadcb15fe66ca16d02e617d50ee9745fa967a": {
"displayName": "AgentClawStack Template",
"source": {
"path": "AgentClawStack.template.json",
"packaging": "file"
},
"destinations": {
"495395224548-us-east-1-02b0125a": {
"495395224548-us-east-1-0ef056b9": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "78c322849179468ec994f5c3e550a0db4961592ea962b9cf484463e5f04b5a70.json",
"objectKey": "9c45c012ea9c045aa771b1c3049eadcb15fe66ca16d02e617d50ee9745fa967a.json",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}

View File

@@ -8,16 +8,6 @@
]
}
],
"/AgentClawStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C": [
{
"type": "aws:cdk:is-custom-resource-handler-singleton",
"data": true
},
{
"type": "aws:cdk:is-custom-resource-handler-runtime-family",
"data": 2
}
],
"/AgentClawStack/SessionStore": [
{
"type": "aws:cdk:hasPhysicalName",
@@ -42,7 +32,7 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:378:5)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:421:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -56,7 +46,7 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:382:5)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:425:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -70,7 +60,7 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:386:5)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:429:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -84,7 +74,7 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:391:5)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:434:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -98,7 +88,7 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:396:5)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:439:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -112,7 +102,7 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:401:5)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:444:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -126,7 +116,7 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:406:5)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:449:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -140,7 +130,7 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:411:5)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:454:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -154,7 +144,7 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:416:5)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:459:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -168,7 +158,7 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:421:5)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:464:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -200,36 +190,6 @@
]
}
],
"/AgentClawStack/WorkspaceBucket/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "WorkspaceBucket53E30B92"
},
{
"type": "aws:cdk:creationStack",
"data": [
"...new Bucket2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:46:9)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
"/AgentClawStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536"
},
{
"type": "aws:cdk:creationStack",
"data": [
"...new BucketDeployment2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:55:7)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
"/AgentClawStack/SessionStore/Resource": [
{
"type": "aws:cdk:logicalId",
@@ -239,7 +199,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Table2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:62:26)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:71:26)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -254,7 +214,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Table2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:71:24)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:80:24)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -269,7 +229,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Queue2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:79:26)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:88:26)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -284,7 +244,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:88:24)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:97:24)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -299,7 +259,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:105:27)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:116:27)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -314,7 +274,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new HttpApi2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:143:21)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:153:21)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -329,7 +289,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Role2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:161:26)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:171:26)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -344,7 +304,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:243:28)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:248:28)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -359,7 +319,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:314:31)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:312:31)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -374,7 +334,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Rule2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:330:27)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:328:27)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -389,7 +349,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...WrappedClass.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:334:19)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:332:19)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -404,7 +364,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:337:25)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:335:25)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -419,7 +379,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...WrappedClass.addPermission in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:350:17)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:348:17)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -438,51 +398,6 @@
]
}
],
"/AgentClawStack/WorkspaceFiles/AwsCliLayer/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "WorkspaceFilesAwsCliLayer50B6E9D8"
},
{
"type": "aws:cdk:creationStack",
"data": [
"...new BucketDeployment2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:55:7)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
"/AgentClawStack/WorkspaceFiles/CustomResource/Default": [
{
"type": "aws:cdk:logicalId",
"data": "WorkspaceFilesCustomResourceA7FC771F"
},
{
"type": "aws:cdk:creationStack",
"data": [
"...new BucketDeployment2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:55:7)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
"/AgentClawStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265"
},
{
"type": "aws:cdk:creationStack",
"data": [
"...new BucketDeployment2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:55:7)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
"/AgentClawStack/TgIngest/ServiceRole/Resource": [
{
"type": "aws:cdk:logicalId",
@@ -492,7 +407,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:88:24)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:97:24)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -507,7 +422,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:105:27)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:116:27)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -522,7 +437,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...WrappedClass.addEventSource in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:137:19)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:147:19)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -537,7 +452,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new HttpApi2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:143:21)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:153:21)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -554,7 +469,7 @@
".../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.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:147:13)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:157:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -571,7 +486,7 @@
".../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.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:147:13)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:157:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -588,7 +503,7 @@
".../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.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:279:13)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:277:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -605,7 +520,7 @@
".../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.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:279:13)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:277:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -622,7 +537,7 @@
".../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.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:286:13)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:284:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -639,7 +554,7 @@
".../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.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:286:13)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:284:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -656,7 +571,7 @@
".../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.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:295:13)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:293:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -673,7 +588,7 @@
".../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.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:295:13)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:293:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -688,7 +603,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...WrappedClass.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:165:18)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:175:18)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -702,8 +617,8 @@
{
"type": "aws:cdk:creationStack",
"data": [
"...environmentFromArn.grantRead in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:201:29)",
"...WrappedClass.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:207:45)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -718,7 +633,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:243:28)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:248:28)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -733,7 +648,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:314:31)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:312:31)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -748,24 +663,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:337:25)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
"/AgentClawStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF"
},
{
"type": "aws:cdk:creationStack",
"data": [
".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.js:1:71 in aws-cdk-lib...",
"Array.map (:)",
"...new BucketDeployment2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:55:7)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:335:25)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -780,7 +678,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...WrappedClass.grantSendMessages in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:101:18)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:111:18)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -795,7 +693,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...WrappedClass.grantReadWriteData in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:122:18)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:133:18)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -812,7 +710,7 @@
".../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.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:147:13)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:157:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -829,7 +727,7 @@
".../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.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:279:13)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:277:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -846,7 +744,7 @@
".../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.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:286:13)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:284:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -863,7 +761,7 @@
".../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.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:295:13)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:293:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -877,8 +775,8 @@
{
"type": "aws:cdk:creationStack",
"data": [
"...environmentFromArn.grantRead in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:258:29)",
"...WrappedClass.addToRolePolicy in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:263:20)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -893,7 +791,7 @@
"type": "aws:cdk:creationStack",
"data": [
"...WrappedClass.grantSendMessages in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:326:18)",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:324:18)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
@@ -907,8 +805,8 @@
{
"type": "aws:cdk:creationStack",
"data": [
"...environmentFromArn.grantRead in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:348:20)",
"...WrappedClass.addToRolePolicy in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:346:17)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]

View File

@@ -1,238 +1,6 @@
{
"Description": "agent-claw: serverless personal assistant on AgentCore",
"Resources": {
"WorkspaceBucket53E30B92": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
},
"BucketName": "agent-claw-workspace-495395224548",
"Tags": [
{
"Key": "aws-cdk:cr-owned:254e75d0",
"Value": "true"
}
]
},
"UpdateReplacePolicy": "Retain",
"DeletionPolicy": "Retain",
"Metadata": {
"aws:cdk:path": "AgentClawStack/WorkspaceBucket/Resource"
}
},
"WorkspaceFilesAwsCliLayer50B6E9D8": {
"Type": "AWS::Lambda::LayerVersion",
"Properties": {
"Content": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "e2659170a0721541efa761a8d5d04d5e36cbbf691c4b15a9053002b7c825055d.zip"
},
"Description": "/opt/awscli/aws"
},
"Metadata": {
"aws:cdk:path": "AgentClawStack/WorkspaceFiles/AwsCliLayer/Resource",
"aws:asset:path": "asset.e2659170a0721541efa761a8d5d04d5e36cbbf691c4b15a9053002b7c825055d.zip",
"aws:asset:is-bundled": false,
"aws:asset:property": "Content"
}
},
"WorkspaceFilesCustomResourceA7FC771F": {
"Type": "Custom::CDKBucketDeployment",
"Properties": {
"ServiceToken": {
"Fn::GetAtt": [
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536",
"Arn"
]
},
"SourceBucketNames": [
"cdk-hnb659fds-assets-495395224548-us-east-1"
],
"SourceObjectKeys": [
"0feea8d997b96e31a1bd7dd049faf8ee17babeb6d2f5b663ba7e3a70387302e0.zip"
],
"DestinationBucketName": {
"Ref": "WorkspaceBucket53E30B92"
},
"WaitForDistributionInvalidation": true,
"Prune": true,
"OutputObjectKeys": true
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete",
"Metadata": {
"aws:cdk:path": "AgentClawStack/WorkspaceFiles/CustomResource/Default"
}
},
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": {
"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/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource"
}
},
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*"
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::cdk-hnb659fds-assets-495395224548-us-east-1"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::cdk-hnb659fds-assets-495395224548-us-east-1/*"
]
]
}
]
},
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*",
"s3:DeleteObject*",
"s3:PutObject",
"s3:PutObjectLegalHold",
"s3:PutObjectRetention",
"s3:PutObjectTagging",
"s3:PutObjectVersionTagging",
"s3:Abort*"
],
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"WorkspaceBucket53E30B92",
"Arn"
]
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"WorkspaceBucket53E30B92",
"Arn"
]
},
"/*"
]
]
}
]
}
],
"Version": "2012-10-17"
},
"PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF",
"Roles": [
{
"Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265"
}
]
},
"Metadata": {
"aws:cdk:path": "AgentClawStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource"
}
},
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9.zip"
},
"Environment": {
"Variables": {
"AWS_CA_BUNDLE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
}
},
"Handler": "index.handler",
"Layers": [
{
"Ref": "WorkspaceFilesAwsCliLayer50B6E9D8"
}
],
"Role": {
"Fn::GetAtt": [
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265",
"Arn"
]
},
"Runtime": "python3.13",
"Timeout": 900
},
"DependsOn": [
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF",
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265"
],
"Metadata": {
"aws:cdk:path": "AgentClawStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource",
"aws:asset:path": "asset.3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9",
"aws:asset:is-bundled": false,
"aws:asset:property": "Code"
}
},
"SessionStore8C86EEFE": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
@@ -353,13 +121,52 @@
]
}
},
{
"Action": "ssm:GetParameter",
"Effect": "Allow",
"Resource": [
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/telegram-bot-token",
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/brave-api-key",
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/google-oauth-client"
]
},
{
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
"s3:DeleteObject*",
"s3:PutObject",
"s3:PutObjectLegalHold",
"s3:PutObjectRetention",
"s3:PutObjectTagging",
"s3:PutObjectVersionTagging",
"s3:Abort*"
],
"Effect": "Allow",
"Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::agent-claw-workspace-495395224548"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::agent-claw-workspace-495395224548/*"
]
]
}
]
}
],
"Version": "2012-10-17"
@@ -380,15 +187,16 @@
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "f4461651bfa7d2822e3f36525ace7882e1610dcdaf85e052e1907241e25491d6.zip"
"S3Key": "e0a834c0c682fe0c528540082d0b587c43ed791927a064104e5eb6c507c0cdb2.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": ""
"TELEGRAM_BOT_TOKEN_SSM_PARAM": "/agent-claw/telegram-bot-token",
"TELEGRAM_WEBHOOK_SECRET": "",
"ATTACHMENTS_BUCKET_NAME": "agent-claw-workspace-495395224548"
}
},
"FunctionName": "agent-claw-tg-ingest",
@@ -409,7 +217,7 @@
],
"Metadata": {
"aws:cdk:path": "AgentClawStack/TgIngest/Resource",
"aws:asset:path": "asset.f4461651bfa7d2822e3f36525ace7882e1610dcdaf85e052e1907241e25491d6",
"aws:asset:path": "asset.e0a834c0c682fe0c528540082d0b587c43ed791927a064104e5eb6c507c0cdb2",
"aws:asset:is-bundled": false,
"aws:asset:property": "Code"
}
@@ -538,42 +346,39 @@
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"WorkspaceBucket53E30B92",
"Arn"
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::agent-claw-workspace-495395224548"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Fn::GetAtt": [
"WorkspaceBucket53E30B92",
"Arn"
]
"Ref": "AWS::Partition"
},
"/*"
":s3:::agent-claw-workspace-495395224548/*"
]
]
}
]
},
{
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Action": "ssm:GetParameter",
"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"
"Resource": [
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/telegram-bot-token",
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/brave-api-key",
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/google-oauth-client"
]
},
{
"Action": [
@@ -615,18 +420,16 @@
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "e8d92532d2cb081ba122764c803acc80aaa41350d3497665468ca165dd5ff799.zip"
"S3Key": "59b4a808a6125020261c01ae23500d036cdde29fdb255aa6a82d61555fde4848.zip"
},
"Environment": {
"Variables": {
"SESSION_TABLE_NAME": {
"Ref": "SessionStore8C86EEFE"
},
"WORKSPACE_BUCKET_NAME": {
"Ref": "WorkspaceBucket53E30B92"
},
"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",
"WORKSPACE_BUCKET_NAME": "agent-claw-workspace-495395224548",
"TELEGRAM_BOT_TOKEN_SSM_PARAM": "/agent-claw/telegram-bot-token",
"BRAVE_API_KEY_SSM_PARAM": "/agent-claw/brave-api-key",
"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": {
@@ -653,7 +456,7 @@
],
"Metadata": {
"aws:cdk:path": "AgentClawStack/AgentRunner/Resource",
"aws:asset:path": "asset.e8d92532d2cb081ba122764c803acc80aaa41350d3497665468ca165dd5ff799",
"aws:asset:path": "asset.59b4a808a6125020261c01ae23500d036cdde29fdb255aa6a82d61555fde4848",
"aws:asset:is-bundled": false,
"aws:asset:property": "Code"
}
@@ -1055,42 +858,39 @@
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"WorkspaceBucket53E30B92",
"Arn"
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::agent-claw-workspace-495395224548"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Fn::GetAtt": [
"WorkspaceBucket53E30B92",
"Arn"
]
"Ref": "AWS::Partition"
},
"/*"
":s3:::agent-claw-workspace-495395224548/*"
]
]
}
]
},
{
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Action": "ssm:GetParameter",
"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"
"Resource": [
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/telegram-bot-token",
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/brave-api-key",
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/google-oauth-client"
]
},
{
"Action": [
@@ -1161,14 +961,6 @@
},
"Sid": "WorkspaceMcpInvoke"
},
{
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Effect": "Allow",
"Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-subXHl"
},
{
"Action": "secretsmanager:GetSecretValue",
"Effect": "Allow",
@@ -1207,6 +999,72 @@
]
},
"Sid": "SchedulerLambdaPermission"
},
{
"Action": [
"codebuild:*",
"ecr:*",
"ecs:*",
"logs:*"
],
"Effect": "Allow",
"Resource": "*",
"Sid": "ComputeBuild"
},
{
"Action": [
"s3:List*",
"s3:GetObject",
"lambda:List*",
"lambda:Get*",
"cloudformation:Describe*",
"cloudformation:List*",
"sqs:List*",
"sqs:GetQueueAttributes",
"ec2:Describe*",
"ssm:Describe*",
"ssm:List*",
"ce:GetCostAndUsage",
"ce:GetCostForecast"
],
"Effect": "Allow",
"Resource": "*",
"Sid": "BroadReadOnly"
},
{
"Action": [
"iam:PutRolePolicy",
"iam:AttachRolePolicy",
"iam:DetachRolePolicy",
"iam:DeleteRolePolicy"
],
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"Runtime1RoleA7A82078",
"Arn"
]
},
"Sid": "IamSelfModify"
},
{
"Action": [
"iam:CreatePolicy",
"iam:GetPolicy",
"iam:ListPolicies"
],
"Effect": "Allow",
"Resource": "*",
"Sid": "IamPolicyManagement"
},
{
"Action": [
"ssm:GetParameter",
"ssm:GetParameters"
],
"Effect": "Allow",
"Resource": "arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/aws-mcp-url",
"Sid": "AwsMcpUrlSsmRead"
}
],
"Version": "2012-10-17"
@@ -1228,12 +1086,13 @@
"PolicyDocument": {
"Statement": [
{
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Action": "ssm:GetParameter",
"Effect": "Allow",
"Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-subXHl"
"Resource": [
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/telegram-bot-token",
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/brave-api-key",
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/google-oauth-client"
]
},
{
"Action": "secretsmanager:GetSecretValue",
@@ -1293,20 +1152,13 @@
"PolicyDocument": {
"Statement": [
{
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Action": "ssm:GetParameter",
"Effect": "Allow",
"Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-subXHl"
},
{
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Effect": "Allow",
"Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"
"Resource": [
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/telegram-bot-token",
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/brave-api-key",
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/google-oauth-client"
]
},
{
"Action": [
@@ -1346,12 +1198,6 @@
}
]
},
{
"Action": "secretsmanager:GetSecretValue",
"Effect": "Allow",
"Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-subXHl",
"Sid": "GoogleOAuthClientSecretExact"
},
{
"Action": [
"secretsmanager:CreateSecret",
@@ -1381,15 +1227,15 @@
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "99aabce70089266e2352cb313d55ee18b849e39c418e8e9cd25dea8c4bf85fc4.zip"
"S3Key": "6a3663dc0651e59d7ac6f549864cf1c084e3fbfc98ebd0479fed6a84075a378e.zip"
},
"Environment": {
"Variables": {
"GOOGLE_OAUTH_CLIENT_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-subXHl",
"GOOGLE_OAUTH_CLIENT_SSM_PARAM": "/agent-claw/google-oauth-client",
"USERS_TABLE_NAME": {
"Ref": "UsersTable9725E9C8"
},
"TELEGRAM_BOT_TOKEN_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3",
"TELEGRAM_BOT_TOKEN_SSM_PARAM": "/agent-claw/telegram-bot-token",
"OAUTH_REDIRECT_URI": {
"Fn::Join": [
"",
@@ -1426,7 +1272,7 @@
],
"Metadata": {
"aws:cdk:path": "AgentClawStack/OAuthHandler/Resource",
"aws:asset:path": "asset.99aabce70089266e2352cb313d55ee18b849e39c418e8e9cd25dea8c4bf85fc4",
"aws:asset:path": "asset.6a3663dc0651e59d7ac6f549864cf1c084e3fbfc98ebd0479fed6a84075a378e",
"aws:asset:is-bundled": false,
"aws:asset:property": "Code"
}
@@ -1656,12 +1502,13 @@
"PolicyDocument": {
"Statement": [
{
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Action": "ssm:GetParameter",
"Effect": "Allow",
"Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"
"Resource": [
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/telegram-bot-token",
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/brave-api-key",
"arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/google-oauth-client"
]
},
{
"Action": [
@@ -1690,11 +1537,11 @@
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f.zip"
"S3Key": "1aaec5c60ed6a294ff9b918a707477e0a0e299a850447e7456a8c8091604131b.zip"
},
"Environment": {
"Variables": {
"TELEGRAM_BOT_TOKEN_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"
"TELEGRAM_BOT_TOKEN_SSM_PARAM": "/agent-claw/telegram-bot-token"
}
},
"FunctionName": "agent-claw-scheduler",
@@ -1715,7 +1562,7 @@
],
"Metadata": {
"aws:cdk:path": "AgentClawStack/Scheduler/Resource",
"aws:asset:path": "asset.8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f",
"aws:asset:path": "asset.1aaec5c60ed6a294ff9b918a707477e0a0e299a850447e7456a8c8091604131b",
"aws:asset:is-bundled": false,
"aws:asset:property": "Code"
}
@@ -1740,7 +1587,7 @@
"CDKMetadata": {
"Type": "AWS::CDK::Metadata",
"Properties": {
"Analytics": "v2:deflate64:H4sIAAAAAAAA/21R207DMAz9Ft4zA934gG2AQAIxOsTr5LVelS1NSu2sqqL+O0rKxoR4OsfHl9gnGWR3GdxcYceTojxMjN5CWAsWB5UTO98WpLDjTeAphIUvDiRqubM/bIQFMg2Kp5tQUmNcX5MVGFP3Z0EhMwnDPMKgDNbbEiEsd/YFe2o/qWXtrFprWxkSZx+9LSQqZ7Lc/YoPR7KyTuu9YtNoW8X0/+qK2lozX84aN9ZYQ8idoViVcOWMLvrUlNigyt5i7cothA/cjpWJDIq/GMK7J5/ERAaFja5QqMP+mEF4EmnmjY75CDFcC1apYSRRyp2XkT1boarF07V/wlQ3KIpHMoTc/yzuDQ0nQzcmmrnBjgujYd7x0ujkr0q+x0eTK57F1ecPjlMu+JuXxsugrCsJ9nx9zGZwO4Obqz1rPWm9FV0T5CN+AxjAathBAgAA"
"Analytics": "v2:deflate64:H4sIAAAAAAAA/21P0U7DMAz8lr2nZnTjAzYEggfEaHmf3NZU2dqk1M6qKsq/o6QbD4inO5991l0O+UMO6xVOnNXNOet0Bb4UrM8KJz563sDe1WeSPTKpZjbY26YC/4lVR+rxyyQSFH8z+A9HLomJBNVhXzUI/tmZWrQ1cfXLny5kpLRurOkNh0GbNq7/Vw809po52m7+mCcojT34wi5REh5sp+s5mRILijdHZCZh2EVQOOgWhSacLzn4F5FhN+hoiBDHUrBNDxcSpcI6WdirEWpHvNX5M6a7oCi2YPCFuyZzHYWgCuLUTKUg8fu137uTwUlQxjYEJ7675Fu438J6dWKts9EZ0T1BseAPQK+curMBAAA="
},
"Metadata": {
"aws:cdk:path": "AgentClawStack/CDKMetadata/Default"
@@ -1811,9 +1658,7 @@
},
"WorkspaceBucketName": {
"Description": "S3 bucket containing agent workspace files",
"Value": {
"Ref": "WorkspaceBucket53E30B92"
}
"Value": "agent-claw-workspace-495395224548"
},
"SessionTableName": {
"Description": "DynamoDB table for session mapping",

View File

@@ -0,0 +1,29 @@
"""EventBridge-triggered Lambda: sends a Telegram reminder then deletes the rule."""
import json
import os
import boto3
import urllib.request
def handler(event, context):
chat_id = event['chat_id']
message = event['message']
rule_name = event['rule_name']
# Fetch bot token
ssm = boto3.client('ssm', region_name='us-east-1')
token = ssm.get_parameter(Name=os.environ['TELEGRAM_BOT_TOKEN_SSM_PARAM'], WithDecryption=True)['Parameter']['Value']
# Send Telegram message
payload = json.dumps({'chat_id': chat_id, 'text': message}).encode()
req = urllib.request.Request(
f'https://api.telegram.org/bot{token}/sendMessage',
data=payload,
headers={'Content-Type': 'application/json'},
)
urllib.request.urlopen(req)
# Delete the one-time rule
eb = boto3.client('events', region_name='us-east-1')
eb.remove_targets(Rule=rule_name, Ids=['scheduler'])
eb.delete_rule(Name=rule_name)

View File

@@ -0,0 +1,292 @@
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:
from botocore.config import Config
_agentcore = boto3.client(
'bedrock-agentcore',
region_name='us-east-1',
config=Config(read_timeout=600, connect_timeout=10)
)
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',
'services': {},
}
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},
)
# Per-invocation dedup: track sent message hashes to prevent AgentCore retry duplicates
_sent_hashes: set = set()
def send_telegram_direct(chat_id: str, token: str, text: str, thread_id: int | None = None) -> None:
import hashlib
h = hashlib.md5(f'{chat_id}:{text}'.encode()).hexdigest()[:12]
if h in _sent_hashes:
print(f'[agent-runner] dedup: skipping duplicate message (hash={h})')
return
_sent_hashes.add(h)
url = f'https://api.telegram.org/bot{token}/sendMessage'
payload: dict = {'chat_id': chat_id, 'text': text}
if thread_id is not None:
payload['message_thread_id'] = thread_id
data = json.dumps(payload).encode()
req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'})
try:
resp = urllib.request.urlopen(req, timeout=10)
resp_body = resp.read()
import re
msg_id = re.search(r'"message_id":(\d+)', resp_body.decode('utf-8', errors='replace'))
print(f'[agent-runner] Telegram sendMessage -> msg_id={msg_id.group(1) if msg_id else "?"} hash={h}')
except Exception as e:
print(f'[agent-runner] Telegram sendMessage FAILED: {type(e).__name__}: {e} hash={h}')
raise
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', '')
message_thread_id = first.get('message_thread_id') # int or None
# Use sender's user ID for identity (not chat_id, which is the group ID in group chats)
from_info_early = first.get('messages', [{}])[0]
sender_id = from_info_early.get('from_id') or chat_id
actor_id = f"{channel}:{sender_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_SSM_PARAM', '')
bot_token = ''
if bot_token_secret_arn:
ssm = boto3.client('ssm', region_name='us-east-1')
bot_token = ssm.get_parameter(Name=bot_token_secret_arn, WithDecryption=True)['Parameter']['Value']
send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?", thread_id=message_thread_id)
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)
# ── Attach file context if present ────────────────────────────────────
attachment = first.get('attachment')
if attachment:
file_name = attachment.get('file_name', 'unknown')
if 'inline_content' in attachment:
prompt += f"\n\n[Attached file: {file_name}]\n```\n{attachment['inline_content']}\n```"
elif 's3_key' in attachment:
s3_ref = f"s3://{attachment['s3_bucket']}/{attachment['s3_key']}"
prompt += f"\n\n[Attached file: {file_name} ({attachment.get('mime_type', '')}) — stored at {s3_ref}]"
elif 'error' in attachment:
prompt += f"\n\n[Attachment {file_name} could not be processed: {attachment['error']}]"
# ── 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_accounts': user_profile.get('google_accounts', {'primary': user_profile['google_email']} if user_profile.get('google_email') else {}),
'allowed': user_profile.get('allowed', True),
'services': user_profile.get('enrolled_services', user_profile.get('services', {})),
},
'channel_adapter': {
'type': channel,
'target_id': str(chat_id),
'message_thread_id': message_thread_id,
'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SSM_PARAM', ''),
},
}
# ── 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(),
)
# Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive
bot_token = ''
bot_token_param = os.environ.get('TELEGRAM_BOT_TOKEN_SSM_PARAM', '')
if bot_token_param:
ssm = boto3.client('ssm', region_name='us-east-1')
try:
bot_token = ssm.get_parameter(Name=bot_token_param, WithDecryption=True)['Parameter']['Value']
except Exception as e:
print(f'[agent-runner] Failed to get bot token: {e}')
body = response.get('response')
text_buffer = ''
leftover = ''
if body is not None:
for raw_chunk in body.iter_chunks():
if not raw_chunk:
continue
# AgentCore streams SSE format: "data: {...}\n\n"
text = leftover + raw_chunk.decode('utf-8', errors='replace')
parts = text.split('\n\n')
leftover = parts[-1]
for part in parts[:-1]:
for line in part.splitlines():
if not line.startswith('data: '):
continue
data = line[6:].strip()
if not data or data == '[DONE]':
continue
try:
event = json.loads(data)
except (json.JSONDecodeError, ValueError):
continue
if not isinstance(event, dict):
continue
# Extract text delta from contentBlockDelta ONLY
# Do NOT use event.get('data') — that's the full formatted summary,
# causing duplicate delivery alongside the token stream.
delta = event.get('event', {}).get('contentBlockDelta', {}).get('delta', {})
if not isinstance(delta, dict):
continue
token = delta.get('text', '')
if token:
text_buffer += token
# Only flush if buffer is very large — prevents splitting multi-turn responses
if len(text_buffer) > 1200:
print(f'[agent-runner] send chunk {len(text_buffer)}c to {chat_id}')
send_telegram_direct(str(chat_id), bot_token, text_buffer.strip(), thread_id=message_thread_id)
text_buffer = ''
# Flush any remaining text
print(f'[agent-runner] stream done buffer={len(text_buffer)} bot_token_set={bool(bot_token)}')
if text_buffer.strip() and bot_token:
# Suppress heartbeat OK responses
if text_buffer.strip().upper().startswith('HEARTBEAT_OK'):
print(f'[agent-runner] heartbeat suppressed for {actor_id}')
return
print(f'[agent-runner] flushing {len(text_buffer)}c to {chat_id}')
send_telegram_direct(str(chat_id), bot_token, text_buffer.strip(), thread_id=message_thread_id)
print(f"[agent-runner] Completed session={session_id} actor={actor_id}")

View File

@@ -0,0 +1,246 @@
"""
Google OAuth handler Lambda.
Routes:
GET /oauth/start?actor_id=telegram:123&label=work → redirect to Google OAuth consent
GET /oauth/callback?code=...&state=... → exchange code, store tokens, update DynamoDB
"""
import base64
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 SSM Parameter Store."""
param_name = os.environ['GOOGLE_OAUTH_CLIENT_SSM_PARAM']
ssm = boto3.client('ssm', region_name=os.environ.get('AWS_REGION', 'us-east-1'))
secret = json.loads(ssm.get_parameter(Name=param_name, WithDecryption=True)['Parameter']['Value'])
return secret['client_id'], secret['client_secret']
def actor_id_to_secret_name(actor_id: str, label: str = 'primary') -> str:
safe = actor_id.replace(':', '-').replace('/', '-')
return f'agent-claw/google-credentials/{safe}/{label}'
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('<h1>Missing actor_id</h1>', 400)
label = params.get('label', 'primary')
client_id, _ = get_oauth_client()
redirect_uri = os.environ['OAUTH_REDIRECT_URI']
# Encode actor_id + label in state (JSON → base64)
state_data = json.dumps({'a': actor_id, 'l': label})
state = base64.urlsafe_b64encode(state_data.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'<h1>OAuth error: {error}</h1>', 400)
if not code or not state:
return _html('<h1>Missing code or state</h1>', 400)
# Decode actor_id + label from state
try:
padding = 4 - len(state) % 4
state_data = json.loads(base64.urlsafe_b64decode(state + '=' * padding).decode())
actor_id = state_data['a']
label = state_data.get('l', 'primary')
except Exception:
# Backward compat: old state was just base64(actor_id)
try:
padding = 4 - len(state) % 4
actor_id = base64.urlsafe_b64decode(state + '=' * padding).decode()
label = 'primary'
except Exception:
return _html('<h1>Invalid state</h1>', 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'<h1>Token exchange failed: {e}</h1>', 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:
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': [s for s in SCOPES.split() if s.startswith('https://')],
'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 at labeled path
secret_name = actor_id_to_secret_name(actor_id, label)
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} label={label} email={user_email}')
# Update DynamoDB: merge into google_accounts map
table_name = os.environ.get('USERS_TABLE_NAME', '')
if table_name and actor_id:
try:
table = get_ddb().Table(table_name)
table.update_item(
Key={'actor_id': actor_id},
UpdateExpression='SET google_accounts = if_not_exists(google_accounts, :empty)',
ExpressionAttributeValues={':empty': {}},
)
table.update_item(
Key={'actor_id': actor_id},
UpdateExpression='SET google_accounts.#label = :email',
ExpressionAttributeNames={'#label': label},
ExpressionAttributeValues={':email': user_email},
)
except Exception as e:
print(f'[oauth] DynamoDB update failed: {e}')
# Best-effort Telegram confirmation
try:
bot_token_param = os.environ.get('TELEGRAM_BOT_TOKEN_SSM_PARAM', '')
if bot_token_param and actor_id.startswith('telegram:'):
chat_id = actor_id.split(':', 1)[1]
ssm = boto3.client('ssm', region_name=os.environ.get('AWS_REGION', 'us-east-1'))
bot_token = ssm.get_parameter(Name=bot_token_param, WithDecryption=True)['Parameter']['Value']
tg_text = f'✅ Connected {user_email} as "{label}"'
tg_payload = json.dumps({'chat_id': chat_id, 'text': tg_text}).encode()
tg_req = urllib.request.Request(
f'https://api.telegram.org/bot{bot_token}/sendMessage',
data=tg_payload,
headers={'Content-Type': 'application/json'},
)
urllib.request.urlopen(tg_req, timeout=5)
except Exception as e:
print(f'[oauth] Telegram notification failed: {e}')
return _html(
f'<h1>✅ Google account connected!</h1>'
f'<p>Connected <b>{user_email}</b> as "<b>{label}</b>".</p>'
f'<p>You can close this window and return to Telegram.</p>'
)

View File

@@ -0,0 +1,229 @@
import json
import os
import threading
import urllib.request
import urllib.parse
import boto3
# Cache bot token (fetched once at Lambda init)
_bot_token: str | None = None
_token_lock = threading.Lock()
TEXT_EXTENSIONS = {'.txt', '.py', '.js', '.ts', '.json', '.md', '.csv', '.xml', '.html',
'.css', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.sh', '.bash',
'.sql', '.log', '.env', '.rs', '.go', '.java', '.c', '.h', '.cpp'}
MAX_INLINE_SIZE = 50 * 1024 # 50KB
def get_bot_token() -> str:
global _bot_token
if _bot_token is None:
with _token_lock:
if _bot_token is None:
sm = boto3.client('secretsmanager')
_bot_token = sm.get_secret_value(
SecretId=os.environ['TELEGRAM_BOT_TOKEN_SECRET_ARN']
)['SecretString']
return _bot_token
def send_typing(chat_id: str, thread_id: int | None = None) -> None:
"""Fire-and-forget typing action (does not raise on failure)."""
try:
token = get_bot_token()
payload: dict = {'chat_id': chat_id, 'action': 'typing'}
if thread_id is not None:
payload['message_thread_id'] = thread_id
data = json.dumps(payload).encode()
req = urllib.request.Request(
f'https://api.telegram.org/bot{token}/sendChatAction',
data=data,
headers={'Content-Type': 'application/json'},
)
urllib.request.urlopen(req, timeout=3)
except Exception:
pass # typing is best-effort
def get_file_from_telegram(file_id: str) -> tuple[str, bytes]:
"""Call getFile then download. Returns (file_path, file_bytes)."""
token = get_bot_token()
# getFile
url = f'https://api.telegram.org/bot{token}/getFile'
data = json.dumps({'file_id': file_id}).encode()
req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read()).get('result', {})
file_path = result.get('file_path', '')
# Download
download_url = f'https://api.telegram.org/file/bot{token}/{file_path}'
with urllib.request.urlopen(download_url, timeout=60) as resp:
file_bytes = resp.read()
return file_path, file_bytes
def extract_attachment(message: dict) -> dict | None:
"""Extract file attachment info from a Telegram message. Returns metadata dict or None."""
# Priority: document > photo > audio > video > voice > video_note
if 'document' in message:
doc = message['document']
return {'type': 'document', 'file_id': doc['file_id'],
'file_name': doc.get('file_name', 'document'), 'mime_type': doc.get('mime_type', ''),
'file_size': doc.get('file_size', 0)}
if 'photo' in message:
# Take largest photo (last in array)
photo = message['photo'][-1]
return {'type': 'photo', 'file_id': photo['file_id'],
'file_name': 'photo.jpg', 'mime_type': 'image/jpeg',
'file_size': photo.get('file_size', 0)}
if 'audio' in message:
audio = message['audio']
return {'type': 'audio', 'file_id': audio['file_id'],
'file_name': audio.get('file_name', 'audio.ogg'), 'mime_type': audio.get('mime_type', 'audio/ogg'),
'file_size': audio.get('file_size', 0)}
if 'video' in message:
video = message['video']
return {'type': 'video', 'file_id': video['file_id'],
'file_name': video.get('file_name', 'video.mp4'), 'mime_type': video.get('mime_type', 'video/mp4'),
'file_size': video.get('file_size', 0)}
if 'voice' in message:
voice = message['voice']
return {'type': 'voice', 'file_id': voice['file_id'],
'file_name': 'voice.ogg', 'mime_type': voice.get('mime_type', 'audio/ogg'),
'file_size': voice.get('file_size', 0)}
return None
def is_text_file(file_name: str, mime_type: str) -> bool:
"""Determine if a file should be inlined as text."""
ext = os.path.splitext(file_name)[1].lower()
if ext in TEXT_EXTENSIONS:
return True
if mime_type.startswith('text/'):
return True
return False
def handler(event, context):
# ── Validate Telegram webhook secret ──────────────────────────────────
expected_secret = os.environ.get('TELEGRAM_WEBHOOK_SECRET', '')
if expected_secret:
headers = event.get('headers') or {}
received = headers.get('x-telegram-bot-api-secret-token', '')
if received != expected_secret:
return {'statusCode': 403, 'body': 'Forbidden'}
# ── Parse Telegram Update ─────────────────────────────────────────────
try:
body = json.loads(event.get('body', '{}'))
except json.JSONDecodeError:
print(f'[tg-ingest] Bad JSON body')
return {'statusCode': 400, 'body': 'Bad Request'}
print(f'[tg-ingest] Update keys: {list(body.keys())}')
update_id = body.get('update_id')
# Support regular messages and edited messages
message = body.get('message') or body.get('edited_message')
if not message:
print(f'[tg-ingest] No message field, update_type={list(body.keys())}')
return {'statusCode': 200, 'body': 'ok'}
chat_id = str(message.get('chat', {}).get('id', ''))
message_thread_id = message.get('message_thread_id') # present for supergroup topics
text = message.get('text', '') or message.get('caption', '')
from_user = message.get('from', {})
timestamp = message.get('date', 0)
# ── Detect file attachment ────────────────────────────────────────────
attachment = extract_attachment(message)
attachment_meta = None
if attachment:
print(f'[tg-ingest] Attachment detected: type={attachment["type"]} name={attachment["file_name"]} size={attachment["file_size"]}')
try:
file_path, file_bytes = get_file_from_telegram(attachment['file_id'])
file_name = attachment['file_name']
mime_type = attachment['mime_type']
if is_text_file(file_name, mime_type) and len(file_bytes) <= MAX_INLINE_SIZE:
# Inline small text files
try:
text_content = file_bytes.decode('utf-8')
except UnicodeDecodeError:
text_content = file_bytes.decode('latin-1')
attachment_meta = {
'type': attachment['type'],
'file_name': file_name,
'mime_type': mime_type,
'inline_content': text_content,
}
else:
# Store to S3
bucket = os.environ.get('ATTACHMENTS_BUCKET_NAME', '')
if bucket:
s3 = boto3.client('s3')
s3_key = f'attachments/{chat_id}/{update_id}/{file_name}'
s3.put_object(Bucket=bucket, Key=s3_key, Body=file_bytes,
ContentType=mime_type or 'application/octet-stream')
attachment_meta = {
'type': attachment['type'],
'file_name': file_name,
'mime_type': mime_type,
's3_bucket': bucket,
's3_key': s3_key,
}
print(f'[tg-ingest] Stored to s3://{bucket}/{s3_key}')
else:
print(f'[tg-ingest] No ATTACHMENTS_BUCKET_NAME configured, skipping S3 upload')
attachment_meta = {
'type': attachment['type'],
'file_name': file_name,
'mime_type': mime_type,
'error': 'S3 bucket not configured',
}
except Exception as e:
print(f'[tg-ingest] Failed to process attachment: {e}')
attachment_meta = {
'type': attachment['type'],
'file_name': attachment['file_name'],
'error': str(e),
}
print(f'[tg-ingest] chat_id={chat_id} text_len={len(text)} attachment={bool(attachment_meta)} update_id={update_id}')
if not chat_id or (not text and not attachment_meta):
print(f'[tg-ingest] Dropping: chat_id={chat_id!r} text={text!r} attachment={attachment_meta}')
return {'statusCode': 200, 'body': 'ok'}
# ── Send typing action (non-blocking, background thread) ──────────────
t = threading.Thread(target=send_typing, args=(chat_id, message_thread_id))
t.daemon = True
t.start()
# ── Enqueue to SQS FIFO ───────────────────────────────────────────────
sqs = boto3.client('sqs')
msg_body: dict = {
'channel': 'telegram',
'chat_id': chat_id,
'message_thread_id': message_thread_id,
'messages': [{
'text': text,
'from_id': str(from_user.get('id', '')),
'from_username': from_user.get('username', ''),
'from_name': f"{from_user.get('first_name', '')} {from_user.get('last_name', '')}".strip(),
}],
'update_id': update_id,
'timestamp': timestamp,
}
if attachment_meta:
msg_body['attachment'] = attachment_meta
sqs.send_message(
QueueUrl=os.environ['MESSAGE_QUEUE_URL'],
MessageGroupId=chat_id,
MessageDeduplicationId=str(update_id),
MessageBody=json.dumps(msg_body),
)
return {'statusCode': 200, 'body': 'ok'}

View File

@@ -0,0 +1,292 @@
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:
from botocore.config import Config
_agentcore = boto3.client(
'bedrock-agentcore',
region_name='us-east-1',
config=Config(read_timeout=600, connect_timeout=10)
)
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',
'services': {},
}
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},
)
# Per-invocation dedup: track sent message hashes to prevent AgentCore retry duplicates
_sent_hashes: set = set()
def send_telegram_direct(chat_id: str, token: str, text: str, thread_id: int | None = None) -> None:
import hashlib
h = hashlib.md5(f'{chat_id}:{text}'.encode()).hexdigest()[:12]
if h in _sent_hashes:
print(f'[agent-runner] dedup: skipping duplicate message (hash={h})')
return
_sent_hashes.add(h)
url = f'https://api.telegram.org/bot{token}/sendMessage'
payload: dict = {'chat_id': chat_id, 'text': text}
if thread_id is not None:
payload['message_thread_id'] = thread_id
data = json.dumps(payload).encode()
req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'})
try:
resp = urllib.request.urlopen(req, timeout=10)
resp_body = resp.read()
import re
msg_id = re.search(r'"message_id":(\d+)', resp_body.decode('utf-8', errors='replace'))
print(f'[agent-runner] Telegram sendMessage -> msg_id={msg_id.group(1) if msg_id else "?"} hash={h}')
except Exception as e:
print(f'[agent-runner] Telegram sendMessage FAILED: {type(e).__name__}: {e} hash={h}')
raise
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', '')
message_thread_id = first.get('message_thread_id') # int or None
# Use sender's user ID for identity (not chat_id, which is the group ID in group chats)
from_info_early = first.get('messages', [{}])[0]
sender_id = from_info_early.get('from_id') or chat_id
actor_id = f"{channel}:{sender_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?", thread_id=message_thread_id)
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)
# ── Attach file context if present ────────────────────────────────────
attachment = first.get('attachment')
if attachment:
file_name = attachment.get('file_name', 'unknown')
if 'inline_content' in attachment:
prompt += f"\n\n[Attached file: {file_name}]\n```\n{attachment['inline_content']}\n```"
elif 's3_key' in attachment:
s3_ref = f"s3://{attachment['s3_bucket']}/{attachment['s3_key']}"
prompt += f"\n\n[Attached file: {file_name} ({attachment.get('mime_type', '')}) — stored at {s3_ref}]"
elif 'error' in attachment:
prompt += f"\n\n[Attachment {file_name} could not be processed: {attachment['error']}]"
# ── 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_accounts': user_profile.get('google_accounts', {'primary': user_profile['google_email']} if user_profile.get('google_email') else {}),
'allowed': user_profile.get('allowed', True),
'services': user_profile.get('enrolled_services', user_profile.get('services', {})),
},
'channel_adapter': {
'type': channel,
'target_id': str(chat_id),
'message_thread_id': message_thread_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(),
)
# Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive
bot_token = ''
bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '')
if bot_token_secret_arn:
sm = boto3.client('secretsmanager', region_name='us-east-1')
try:
bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString']
except Exception as e:
print(f'[agent-runner] Failed to get bot token: {e}')
body = response.get('response')
text_buffer = ''
leftover = ''
if body is not None:
for raw_chunk in body.iter_chunks():
if not raw_chunk:
continue
# AgentCore streams SSE format: "data: {...}\n\n"
text = leftover + raw_chunk.decode('utf-8', errors='replace')
parts = text.split('\n\n')
leftover = parts[-1]
for part in parts[:-1]:
for line in part.splitlines():
if not line.startswith('data: '):
continue
data = line[6:].strip()
if not data or data == '[DONE]':
continue
try:
event = json.loads(data)
except (json.JSONDecodeError, ValueError):
continue
if not isinstance(event, dict):
continue
# Extract text delta from contentBlockDelta ONLY
# Do NOT use event.get('data') — that's the full formatted summary,
# causing duplicate delivery alongside the token stream.
delta = event.get('event', {}).get('contentBlockDelta', {}).get('delta', {})
if not isinstance(delta, dict):
continue
token = delta.get('text', '')
if token:
text_buffer += token
# Only flush if buffer is very large — prevents splitting multi-turn responses
if len(text_buffer) > 1200:
print(f'[agent-runner] send chunk {len(text_buffer)}c to {chat_id}')
send_telegram_direct(str(chat_id), bot_token, text_buffer.strip(), thread_id=message_thread_id)
text_buffer = ''
# Flush any remaining text
print(f'[agent-runner] stream done buffer={len(text_buffer)} bot_token_set={bool(bot_token)}')
if text_buffer.strip() and bot_token:
# Suppress heartbeat OK responses
if text_buffer.strip().upper().startswith('HEARTBEAT_OK'):
print(f'[agent-runner] heartbeat suppressed for {actor_id}')
return
print(f'[agent-runner] flushing {len(text_buffer)}c to {chat_id}')
send_telegram_direct(str(chat_id), bot_token, text_buffer.strip(), thread_id=message_thread_id)
print(f"[agent-runner] Completed session={session_id} actor={actor_id}")

View File

@@ -0,0 +1,230 @@
import json
import os
import threading
import urllib.request
import urllib.parse
import boto3
# Cache bot token (fetched once at Lambda init)
_bot_token: str | None = None
_token_lock = threading.Lock()
TEXT_EXTENSIONS = {'.txt', '.py', '.js', '.ts', '.json', '.md', '.csv', '.xml', '.html',
'.css', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.sh', '.bash',
'.sql', '.log', '.env', '.rs', '.go', '.java', '.c', '.h', '.cpp'}
MAX_INLINE_SIZE = 50 * 1024 # 50KB
def get_bot_token() -> str:
global _bot_token
if _bot_token is None:
with _token_lock:
if _bot_token is None:
ssm = boto3.client('ssm')
_bot_token = ssm.get_parameter(
Name=os.environ['TELEGRAM_BOT_TOKEN_SSM_PARAM'],
WithDecryption=True
)['Parameter']['Value']
return _bot_token
def send_typing(chat_id: str, thread_id: int | None = None) -> None:
"""Fire-and-forget typing action (does not raise on failure)."""
try:
token = get_bot_token()
payload: dict = {'chat_id': chat_id, 'action': 'typing'}
if thread_id is not None:
payload['message_thread_id'] = thread_id
data = json.dumps(payload).encode()
req = urllib.request.Request(
f'https://api.telegram.org/bot{token}/sendChatAction',
data=data,
headers={'Content-Type': 'application/json'},
)
urllib.request.urlopen(req, timeout=3)
except Exception:
pass # typing is best-effort
def get_file_from_telegram(file_id: str) -> tuple[str, bytes]:
"""Call getFile then download. Returns (file_path, file_bytes)."""
token = get_bot_token()
# getFile
url = f'https://api.telegram.org/bot{token}/getFile'
data = json.dumps({'file_id': file_id}).encode()
req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read()).get('result', {})
file_path = result.get('file_path', '')
# Download
download_url = f'https://api.telegram.org/file/bot{token}/{file_path}'
with urllib.request.urlopen(download_url, timeout=60) as resp:
file_bytes = resp.read()
return file_path, file_bytes
def extract_attachment(message: dict) -> dict | None:
"""Extract file attachment info from a Telegram message. Returns metadata dict or None."""
# Priority: document > photo > audio > video > voice > video_note
if 'document' in message:
doc = message['document']
return {'type': 'document', 'file_id': doc['file_id'],
'file_name': doc.get('file_name', 'document'), 'mime_type': doc.get('mime_type', ''),
'file_size': doc.get('file_size', 0)}
if 'photo' in message:
# Take largest photo (last in array)
photo = message['photo'][-1]
return {'type': 'photo', 'file_id': photo['file_id'],
'file_name': 'photo.jpg', 'mime_type': 'image/jpeg',
'file_size': photo.get('file_size', 0)}
if 'audio' in message:
audio = message['audio']
return {'type': 'audio', 'file_id': audio['file_id'],
'file_name': audio.get('file_name', 'audio.ogg'), 'mime_type': audio.get('mime_type', 'audio/ogg'),
'file_size': audio.get('file_size', 0)}
if 'video' in message:
video = message['video']
return {'type': 'video', 'file_id': video['file_id'],
'file_name': video.get('file_name', 'video.mp4'), 'mime_type': video.get('mime_type', 'video/mp4'),
'file_size': video.get('file_size', 0)}
if 'voice' in message:
voice = message['voice']
return {'type': 'voice', 'file_id': voice['file_id'],
'file_name': 'voice.ogg', 'mime_type': voice.get('mime_type', 'audio/ogg'),
'file_size': voice.get('file_size', 0)}
return None
def is_text_file(file_name: str, mime_type: str) -> bool:
"""Determine if a file should be inlined as text."""
ext = os.path.splitext(file_name)[1].lower()
if ext in TEXT_EXTENSIONS:
return True
if mime_type.startswith('text/'):
return True
return False
def handler(event, context):
# ── Validate Telegram webhook secret ──────────────────────────────────
expected_secret = os.environ.get('TELEGRAM_WEBHOOK_SECRET', '')
if expected_secret:
headers = event.get('headers') or {}
received = headers.get('x-telegram-bot-api-secret-token', '')
if received != expected_secret:
return {'statusCode': 403, 'body': 'Forbidden'}
# ── Parse Telegram Update ─────────────────────────────────────────────
try:
body = json.loads(event.get('body', '{}'))
except json.JSONDecodeError:
print(f'[tg-ingest] Bad JSON body')
return {'statusCode': 400, 'body': 'Bad Request'}
print(f'[tg-ingest] Update keys: {list(body.keys())}')
update_id = body.get('update_id')
# Support regular messages and edited messages
message = body.get('message') or body.get('edited_message')
if not message:
print(f'[tg-ingest] No message field, update_type={list(body.keys())}')
return {'statusCode': 200, 'body': 'ok'}
chat_id = str(message.get('chat', {}).get('id', ''))
message_thread_id = message.get('message_thread_id') # present for supergroup topics
text = message.get('text', '') or message.get('caption', '')
from_user = message.get('from', {})
timestamp = message.get('date', 0)
# ── Detect file attachment ────────────────────────────────────────────
attachment = extract_attachment(message)
attachment_meta = None
if attachment:
print(f'[tg-ingest] Attachment detected: type={attachment["type"]} name={attachment["file_name"]} size={attachment["file_size"]}')
try:
file_path, file_bytes = get_file_from_telegram(attachment['file_id'])
file_name = attachment['file_name']
mime_type = attachment['mime_type']
if is_text_file(file_name, mime_type) and len(file_bytes) <= MAX_INLINE_SIZE:
# Inline small text files
try:
text_content = file_bytes.decode('utf-8')
except UnicodeDecodeError:
text_content = file_bytes.decode('latin-1')
attachment_meta = {
'type': attachment['type'],
'file_name': file_name,
'mime_type': mime_type,
'inline_content': text_content,
}
else:
# Store to S3
bucket = os.environ.get('ATTACHMENTS_BUCKET_NAME', '')
if bucket:
s3 = boto3.client('s3')
s3_key = f'attachments/{chat_id}/{update_id}/{file_name}'
s3.put_object(Bucket=bucket, Key=s3_key, Body=file_bytes,
ContentType=mime_type or 'application/octet-stream')
attachment_meta = {
'type': attachment['type'],
'file_name': file_name,
'mime_type': mime_type,
's3_bucket': bucket,
's3_key': s3_key,
}
print(f'[tg-ingest] Stored to s3://{bucket}/{s3_key}')
else:
print(f'[tg-ingest] No ATTACHMENTS_BUCKET_NAME configured, skipping S3 upload')
attachment_meta = {
'type': attachment['type'],
'file_name': file_name,
'mime_type': mime_type,
'error': 'S3 bucket not configured',
}
except Exception as e:
print(f'[tg-ingest] Failed to process attachment: {e}')
attachment_meta = {
'type': attachment['type'],
'file_name': attachment['file_name'],
'error': str(e),
}
print(f'[tg-ingest] chat_id={chat_id} text_len={len(text)} attachment={bool(attachment_meta)} update_id={update_id}')
if not chat_id or (not text and not attachment_meta):
print(f'[tg-ingest] Dropping: chat_id={chat_id!r} text={text!r} attachment={attachment_meta}')
return {'statusCode': 200, 'body': 'ok'}
# ── Send typing action (non-blocking, background thread) ──────────────
t = threading.Thread(target=send_typing, args=(chat_id, message_thread_id))
t.daemon = True
t.start()
# ── Enqueue to SQS FIFO ───────────────────────────────────────────────
sqs = boto3.client('sqs')
msg_body: dict = {
'channel': 'telegram',
'chat_id': chat_id,
'message_thread_id': message_thread_id,
'messages': [{
'text': text,
'from_id': str(from_user.get('id', '')),
'from_username': from_user.get('username', ''),
'from_name': f"{from_user.get('first_name', '')} {from_user.get('last_name', '')}".strip(),
}],
'update_id': update_id,
'timestamp': timestamp,
}
if attachment_meta:
msg_body['attachment'] = attachment_meta
sqs.send_message(
QueueUrl=os.environ['MESSAGE_QUEUE_URL'],
MessageGroupId=chat_id,
MessageDeduplicationId=str(update_id),
MessageBody=json.dumps(msg_body),
)
return {'statusCode': 200, 'body': 'ok'}

View File

@@ -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/78c322849179468ec994f5c3e550a0db4961592ea962b9cf484463e5f04b5a70.json",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-495395224548-us-east-1/9c45c012ea9c045aa771b1c3049eadcb15fe66ca16d02e617d50ee9745fa967a.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
"additionalDependencies": [

File diff suppressed because one or more lines are too long

View File

@@ -371,6 +371,51 @@ export class AgentClawStack extends cdk.Stack {
resources: [schedulerFn.functionArn],
}));
// ── AgentCore Runtime 1 — extended permissions ───────────────────────
// Compute/build
runtime1Role.addToPolicy(new iam.PolicyStatement({
sid: 'ComputeBuild',
actions: ['codebuild:*', 'ecr:*', 'ecs:*', 'logs:*'],
resources: ['*'],
}));
// Broad read-only across account
runtime1Role.addToPolicy(new iam.PolicyStatement({
sid: 'BroadReadOnly',
actions: [
's3:List*', 's3:GetObject',
'lambda:List*', 'lambda:Get*',
'cloudformation:Describe*', 'cloudformation:List*',
'sqs:List*', 'sqs:GetQueueAttributes',
'ec2:Describe*',
'ssm:Describe*', 'ssm:List*',
'ce:GetCostAndUsage', 'ce:GetCostForecast',
],
resources: ['*'],
}));
// IAM self-modify — scoped to own role only
runtime1Role.addToPolicy(new iam.PolicyStatement({
sid: 'IamSelfModify',
actions: ['iam:PutRolePolicy', 'iam:AttachRolePolicy', 'iam:DetachRolePolicy', 'iam:DeleteRolePolicy'],
resources: [runtime1Role.roleArn],
}));
// IAM policy management
runtime1Role.addToPolicy(new iam.PolicyStatement({
sid: 'IamPolicyManagement',
actions: ['iam:CreatePolicy', 'iam:GetPolicy', 'iam:ListPolicies'],
resources: ['*'],
}));
// SSM read for AWS MCP URL
runtime1Role.addToPolicy(new iam.PolicyStatement({
sid: 'AwsMcpUrlSsmRead',
actions: ['ssm:GetParameter', 'ssm:GetParameters'],
resources: [`arn:aws:ssm:${this.region}:${this.account}:parameter/agent-claw/aws-mcp-url`],
}));
// ── Outputs ────────────────────────────────────────────────────────────
new cdk.CfnOutput(this, 'WorkspaceMcpFunctionUrl', {

View File

@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""
Create Bedrock Application Inference Profiles for agent-claw and update SSM.
Run after: aws sso login --profile ai1
Usage:
python3 scripts/create-inference-profiles.py [--dry-run]
Creates:
agent-claw-opus — main agent
agent-claw-sonnet — aws_agent + coding_agent
agent-claw-haiku — document_agent
Then updates SSM:
/agent-claw/model-id → agent-claw-opus ARN
/agent-claw/subagents → inline model_id fields replaced with profile ARNs
"""
import argparse
import json
import sys
import boto3
from botocore.exceptions import ClientError
PROFILE = 'ai1'
REGION = 'us-east-1'
BILLING_TAGS = [
{'key': 'project', 'value': 'agent-claw'},
{'key': 'env', 'value': 'prod'},
{'key': 'owner', 'value': 'daniel'},
]
# Map: profile name → cross-region model ID to copy from
PROFILES_TO_CREATE = {
'agent-claw-opus': 'us.anthropic.claude-opus-4-6-v1:0',
'agent-claw-sonnet': 'us.anthropic.claude-sonnet-4-6-20251001-v1:0',
'agent-claw-haiku': 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
}
# SSM subagent model_id values → which profile ARN to swap in
SUBAGENT_MODEL_MAP = {
'us.anthropic.claude-sonnet-4-6': 'agent-claw-sonnet',
'us.anthropic.claude-sonnet-4-6-20251001-v1:0':'agent-claw-sonnet',
'us.anthropic.claude-haiku-4-5-20251001-v1:0': 'agent-claw-haiku',
}
def get_system_inference_profile_arn(bedrock, model_id: str) -> str:
"""Find the system inference profile ARN for a given cross-region model ID."""
paginator = bedrock.get_paginator('list_inference_profiles')
for page in paginator.paginate(typeEquals='SYSTEM_DEFINED'):
for p in page.get('inferenceProfileSummaries', []):
if p.get('inferenceProfileId', '') == model_id or \
any(m.get('modelArn', '').endswith(model_id) for m in p.get('models', [])):
return p['inferenceProfileArn']
# Fallback: construct ARN directly (works for cross-region profiles)
return f'arn:aws:bedrock:{REGION}::foundation-model/{model_id}'
def get_existing_profile(bedrock, name: str) -> dict | None:
"""Return existing application profile by name, or None."""
paginator = bedrock.get_paginator('list_inference_profiles')
for page in paginator.paginate(typeEquals='APPLICATION'):
for p in page.get('inferenceProfileSummaries', []):
if p.get('inferenceProfileName') == name:
return p
return None
def create_or_get_profile(bedrock, name: str, model_id: str, dry_run: bool) -> str:
"""Create application inference profile (idempotent). Returns ARN."""
existing = get_existing_profile(bedrock, name)
if existing:
arn = existing['inferenceProfileArn']
print(f' [exists] {name}{arn}')
return arn
source_arn = get_system_inference_profile_arn(bedrock, model_id)
print(f' [create] {name}')
print(f' source: {source_arn}')
if dry_run:
print(f' [dry-run] skipping create')
return f'arn:aws:bedrock:{REGION}:{{}account}}:application-inference-profile/{name}-DRY-RUN'
resp = bedrock.create_inference_profile(
inferenceProfileName=name,
description=f'agent-claw {name.split("-")[-1]} model with billing tags',
modelSource={'copyFrom': source_arn},
tags=BILLING_TAGS,
)
arn = resp['inferenceProfileArn']
print(f'{arn}')
return arn
def update_ssm(ssm, param: str, value: str, dry_run: bool):
print(f' [ssm] {param} = {value[:80]}...' if len(value) > 80 else f' [ssm] {param} = {value}')
if not dry_run:
ssm.put_parameter(Name=param, Value=value, Type='String', Overwrite=True)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--dry-run', action='store_true')
args = parser.parse_args()
session = boto3.Session(profile_name=PROFILE, region_name=REGION)
bedrock = session.client('bedrock')
ssm = session.client('ssm')
print('=== Creating inference profiles ===')
arns = {}
for name, model_id in PROFILES_TO_CREATE.items():
arns[name] = create_or_get_profile(bedrock, name, model_id, args.dry_run)
print('\n=== Updating SSM ===')
# Main agent model
update_ssm(ssm, '/agent-claw/model-id', arns['agent-claw-opus'], args.dry_run)
# Subagents JSON — swap model_id fields
try:
resp = ssm.get_parameter(Name='/agent-claw/subagents')
defs = json.loads(resp['Parameter']['Value'])
except ClientError as e:
print(f' [error] Could not read /agent-claw/subagents: {e}')
sys.exit(1)
changed = False
for agent in defs:
mid = agent.get('model_id', '')
profile_name = SUBAGENT_MODEL_MAP.get(mid)
if profile_name:
new_arn = arns[profile_name]
print(f' [subagent] {agent["name"]}: {mid}{profile_name}')
agent['model_id'] = new_arn
changed = True
else:
print(f' [subagent] {agent["name"]}: {mid} (no mapping, left as-is)')
if changed:
update_ssm(ssm, '/agent-claw/subagents', json.dumps(defs, indent=2), args.dry_run)
print('\n=== Done ===')
print('Profiles created and SSM updated.')
print('Redeploy not required — agent reads model IDs from SSM at startup.')
if args.dry_run:
print('\n[dry-run mode — no AWS changes were made]')
if __name__ == '__main__':
main()

35
scripts/seed_factcloud.py Normal file
View File

@@ -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}')

View File

@@ -4,7 +4,9 @@ agent-claw Runtime 1 — main assistant agent.
Entrypoint for AgentCore CodeZip deployment.
"""
import os
import boto3
from strands import Agent, tool
from strands.models import BedrockModel
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from channels.telegram import TelegramAdapter
@@ -15,6 +17,10 @@ from tools import messaging
app = BedrockAgentCoreApp()
# Read model ID from SSM once at module load (cached for warm invocations)
_ssm = boto3.client("ssm", region_name=os.environ.get("AWS_REGION_NAME", "us-east-1"))
_model_id = _ssm.get_parameter(Name="/agent-claw/model-id")["Parameter"]["Value"]
# ── Tool definitions ──────────────────────────────────────────────────────
@@ -77,6 +83,7 @@ def main(payload: dict, context) -> dict:
# Create and run Strands agent
agent = Agent(
model=BedrockModel(model_id=_model_id),
system_prompt=system_prompt,
tools=[send_message, web_search, web_fetch, read_workspace_file, write_workspace_file],
)

3
workspace/TOOLS.md Normal file
View File

@@ -0,0 +1,3 @@
# Tools
This file documents available tools for the agent-claw runtime.